Merge remote-tracking branch 'origin/master' into pp/wire

This commit is contained in:
pilfer-pandex 2021-01-29 14:17:23 -08:00
commit 0f069a08e8
23 changed files with 833 additions and 2727 deletions

View File

@ -1,11 +1,8 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: Landscape design issue - name: Submit a Landscape issue
url: https://github.com/urbit/landscape/issues/new?assignees=&labels=design+issue&template=report-a-design-issue.md&title= url: https://github.com/urbit/landscape/issues/new/choose
about: Submit non-functionality, design-specific issues to the Landscape team here. about: Issues with Landscape (Tlon's flagship client) should be filed at urbit/landscape. This includes groups, chats, collections, notebooks, and more.
- name: Landscape feature request
url: https://github.com/urbit/landscape/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=
about: Landscape is comprised of Tlon's user applications and client for Urbit. Submit Landscape feature requests here.
- name: urbit-dev mailing list - name: urbit-dev mailing list
url: https://groups.google.com/a/urbit.org/g/dev url: https://groups.google.com/a/urbit.org/g/dev
about: Developer questions and discussions also take place on the urbit-dev mailing list. about: Developer questions and discussions also take place on the urbit-dev mailing list.

View File

@ -1,39 +0,0 @@
---
name: Landscape bug report
about: 'Use this template to file a bug for any Landscape app: Chat, Publish, Links, Groups,
Weather or Clock'
title: ''
labels: landscape
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem. If possible, please also screenshot your browser's dev console. Here are [Chrome's docs](https://developers.google.com/web/tools/chrome-devtools/open) for using this feature.
**Desktop (please complete the following information):**
- OS: [e.g. MacOS 10.15.3]
- Browser [e.g. chrome, safari]
- Base hash of your urbit ship. Run `+trouble` in Dojo to see this.
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Base hash of your urbit ship. Run `+trouble` in Dojo to see this.
**Additional context**
Add any other context about the problem here.

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:271d575a87373f4ed73b195780973ed41cb72be21b428a645c42a49ab5f786ee oid sha256:6b4b198b552066fdee2a694a3134bf641b20591bebda21aa90920f4107f04f20
size 8873583 size 9065500

View File

@ -5,7 +5,7 @@
/- glob /- glob
/+ default-agent, verb, dbug /+ default-agent, verb, dbug
|% |%
++ hash 0v7.ttn7o.50403.rf6oh.63hnc.hgpc9 ++ hash 0v1.39us5.oj5a9.9as9u.od9db.0dipj
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states +$ all-states
$% state-0 $% state-0

View File

@ -726,7 +726,8 @@
$: %0 $: %0
p=time p=time
$= q $= q
$% [%add-nodes =resource:store nodes=(tree [index:store tree-node])] $% [%add-graph =resource:store =tree-graph mark=(unit ^mark) ow=?]
[%add-nodes =resource:store nodes=(tree [index:store tree-node])]
[%remove-nodes =resource:store indices=(tree index:store)] [%remove-nodes =resource:store indices=(tree index:store)]
[%add-signatures =uid:store signatures=tree-signatures] [%add-signatures =uid:store signatures=tree-signatures]
[%remove-signatures =uid:store signatures=tree-signatures] [%remove-signatures =uid:store signatures=tree-signatures]
@ -806,6 +807,14 @@
^- logged-update:store ^- logged-update:store
:+ %0 p.t :+ %0 p.t
?- -.q.t ?- -.q.t
%add-graph
:* %add-graph
resource.q.t
(remake-graph tree-graph.q.t)
mark.q.t
ow.q.t
==
::
%add-nodes %add-nodes
:- %add-nodes :- %add-nodes
:- resource.q.t :- resource.q.t

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div> <div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script> <script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script> <script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.7d4248944fe1255cb74b.js"></script> <script src="/~landscape/js/bundle/index.5fdbe84c6b57646a6a6b.js"></script>
</body> </body>
</html> </html>

View File

@ -29,8 +29,6 @@
%contact-store %contact-store
%contact-hook %contact-hook
%invite-store %invite-store
%chat-store
%chat-hook
%graph-store %graph-store
== ==
|= app=@tas |= app=@tas

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"codemirror": "^5.59.2", "codemirror": "^5.59.2",
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"formik": "^2.2.6", "formik": "^2.1.5",
"immer": "^8.0.1", "immer": "^8.0.1",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"markdown-to-jsx": "^6.11.4", "markdown-to-jsx": "^6.11.4",

View File

@ -196,10 +196,11 @@ export class HarkApi extends BaseApi<StoreState> {
}); });
} }
getMore() { async getMore(): Promise<boolean> {
const offset = this.store.state['notifications']?.size || 0; const offset = this.store.state['notifications']?.size || 0;
const count = 3; const count = 3;
return this.getSubset(offset, count, false); await this.getSubset(offset, count, false);
return offset === (this.store.state.notifications?.size || 0);
} }
async getSubset(offset:number, count:number, isArchive: boolean) { async getSubset(offset:number, count:number, isArchive: boolean) {

View File

@ -0,0 +1,51 @@
import { useEffect, RefObject, useRef, useState } from "react";
import _ from "lodash";
export function distanceToBottom(el: HTMLElement) {
const { scrollTop, scrollHeight, clientHeight } = el;
const scrolledPercent =
(scrollHeight - scrollTop - clientHeight) / scrollHeight;
return _.isNaN(scrolledPercent) ? 0 : scrolledPercent;
}
export function useLazyScroll(
ref: RefObject<HTMLElement>,
margin: number,
loadMore: () => Promise<boolean>
) {
const [isDone, setIsDone] = useState(false);
useEffect(() => {
if (!ref.current) {
return;
}
setIsDone(false);
const scroll = ref.current;
const loadUntil = (el: HTMLElement) => {
if (!isDone && distanceToBottom(el) < margin) {
return loadMore().then((done) => {
if (done) {
setIsDone(true);
return Promise.resolve();
}
return loadUntil(el);
});
}
return Promise.resolve();
};
loadUntil(scroll);
const onScroll = (e: Event) => {
const el = e.currentTarget! as HTMLElement;
loadUntil(el);
};
ref.current.addEventListener("scroll", onScroll);
return () => {
ref.current?.removeEventListener("scroll", onScroll);
};
}, [ref?.current]);
return isDone;
}

View File

@ -363,11 +363,19 @@ export function useShowNickname(contact: Contact | null, hide?: boolean): boolea
return !!(contact && contact.nickname && !hideNicknames); return !!(contact && contact.nickname && !hideNicknames);
} }
export function useHovering() { interface useHoveringInterface {
hovering: boolean;
bind: {
onMouseOver: () => void,
onMouseLeave: () => void
}
}
export const useHovering = (): useHoveringInterface => {
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);
const bind = { const bind = {
onMouseEnter: () => setHovering(true), onMouseOver: () => setHovering(true),
onMouseLeave: () => setHovering(false) onMouseLeave: () => setHovering(false)
}; };
return { hovering, bind }; return { hovering, bind };
} };

View File

@ -40,7 +40,8 @@ const useLocalState = create<LocalState>(persist((set, get) => ({
} }
})), })),
set: fn => set(produce(fn)) set: fn => set(produce(fn))
}), { }), {
blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'],
name: 'localReducer' name: 'localReducer'
})); }));
@ -55,4 +56,4 @@ function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemb
}); });
} }
export { useLocalState as default, withLocalState }; export { useLocalState as default, withLocalState };

View File

@ -1,3 +1,5 @@
import _ from 'lodash';
import BaseStore from './base'; import BaseStore from './base';
import InviteReducer from '../reducers/invite-update'; import InviteReducer from '../reducers/invite-update';
import MetadataReducer from '../reducers/metadata-update'; import MetadataReducer from '../reducers/metadata-update';
@ -40,6 +42,18 @@ export default class GlobalStore extends BaseStore<StoreState> {
launchReducer = new LaunchReducer(); launchReducer = new LaunchReducer();
connReducer = new ConnectionReducer(); connReducer = new ConnectionReducer();
pastActions: Record<string, any> = {}
constructor() {
super();
(window as any).debugStore = this.debugStore.bind(this);
}
debugStore(tag: string, ...stateKeys: string[]) {
console.log(this.pastActions[tag]);
console.log(_.pick(this.state, stateKeys));
}
rehydrate() { rehydrate() {
this.localReducer.rehydrate(this.state); this.localReducer.rehydrate(this.state);
} }
@ -94,6 +108,11 @@ export default class GlobalStore extends BaseStore<StoreState> {
} }
reduce(data: Cage, state: StoreState) { reduce(data: Cage, state: StoreState) {
// debug shim
const tag = Object.keys(data)[0];
const oldActions = this.pastActions[tag] || [];
this.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)];
this.inviteReducer.reduce(data, this.state); this.inviteReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state); this.metadataReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state); this.localReducer.reduce(data, this.state);

View File

@ -1,4 +1,5 @@
import { Serial, PatpNoSig, Path } from './noun'; import { Serial, PatpNoSig, Path } from './noun';
import {Resource} from './group-update';
export type InviteUpdate = export type InviteUpdate =
InviteUpdateInitial InviteUpdateInitial
@ -60,8 +61,8 @@ export type AppInvites = {
export interface Invite { export interface Invite {
app: string; app: string;
path: Path; recipient: PatpNoSig;
recipeint: PatpNoSig; resource: Resource;
ship: PatpNoSig; ship: PatpNoSig;
text: string; text: string;
} }

View File

@ -4,7 +4,7 @@ import _ from "lodash";
import { Box, Row, Text, Rule } from "@tlon/indigo-react"; import { Box, Row, Text, Rule } from "@tlon/indigo-react";
import OverlaySigil from '~/views/components/OverlaySigil'; import OverlaySigil from '~/views/components/OverlaySigil';
import { uxToHex, cite, writeText, useShowNickname } from '~/logic/lib/util'; import { uxToHex, cite, writeText, useShowNickname, useHovering } from '~/logic/lib/util';
import { Group, Association, Contacts, Post } from "~/types"; import { Group, Association, Contacts, Post } from "~/types";
import TextContent from './content/text'; import TextContent from './content/text';
import CodeContent from './content/code'; import CodeContent from './content/code';
@ -134,6 +134,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
className={containerClass} className={containerClass}
style={style} style={style}
mb={1} mb={1}
position="relative"
> >
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null} {dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null}
{renderSigil {renderSigil
@ -194,6 +195,8 @@ export const MessageWithSigil = (props) => {
} }
}; };
const { hovering, bind } = useHovering();
return ( return (
<> <>
<OverlaySigil <OverlaySigil
@ -206,9 +209,11 @@ export const MessageWithSigil = (props) => {
history={history} history={history}
api={api} api={api}
bg="white" bg="white"
className="fl pr3 v-top pt1" className="fl v-top pt1"
pr={3}
pl={2}
/> />
<Box flexGrow={1} display='block' className="clamp-message"> <Box flexGrow={1} display='block' className="clamp-message" {...bind}>
<Box <Box
flexShrink={0} flexShrink={0}
className="hide-child" className="hide-child"
@ -231,8 +236,15 @@ export const MessageWithSigil = (props) => {
}} }}
title={`~${msg.author}`} title={`~${msg.author}`}
>{name}</Text> >{name}</Text>
<Text flexShrink='0' fontSize='0' gray mono className="v-mid">{timestamp}</Text> <Text flexShrink={0} fontSize={0} gray mono>{timestamp}</Text>
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text> <Text
flexShrink={0}
fontSize={0}
gray
mono
ml={2}
display={['none', hovering ? 'block' : 'none']}
>{datestamp}</Text>
</Box> </Box>
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}> <ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
{msg.contents.map(c => {msg.contents.map(c =>
@ -257,20 +269,40 @@ const ContentBox = styled(Box)`
`; `;
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => ( export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => {
<> const { hovering, bind } = useHovering();
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child" fontSize='0'>{timestamp}</Text> return (
<ContentBox flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}> <>
{msg.contents.map((c, i) => ( <Text
<MessageContent flexShrink={0}
key={i} mono
contacts={contacts} gray
content={c} display={hovering ? 'block': 'none'}
group={group} pt='2px'
measure={measure}/>))} lineHeight='tall'
</ContentBox> fontSize={0}
</> position="absolute"
); left={1}
>{timestamp}</Text>
<ContentBox
flexShrink={0}
fontSize='14px'
className="clamp-message"
style={{ flexGrow: 1 }}
{...bind}
pl={6}
>
{msg.contents.map((c, i) => (
<MessageContent
key={i}
contacts={contacts}
content={c}
group={group}
measure={measure}/>))}
</ContentBox>
</>
)
};
export const MessageContent = ({ content, contacts, measure, fontSize, group }) => { export const MessageContent = ({ content, contacts, measure, fontSize, group }) => {
if ('code' in content) { if ('code' in content) {
@ -292,7 +324,8 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group })
}} }}
textProps={{style: { textProps={{style: {
fontSize: 'inherit', fontSize: 'inherit',
textDecoration: 'underline' borderBottom: '1px solid',
textDecoration: 'none'
}}} }}}
/> />
</Box> </Box>

View File

@ -53,6 +53,9 @@ const MessageMarkdown = React.memo(props => (
{...props} {...props}
unwrapDisallowed={true} unwrapDisallowed={true}
renderers={renderers} renderers={renderers}
// shim until we uncover why RemarkBreaks and
// RemarkDisableTokenizers can't be loaded simultaneously
disallowedTypes={['heading', 'list', 'listItem', 'link']}
allowNode={(node, index, parent) => { allowNode={(node, index, parent) => {
if ( if (
node.type === 'blockquote' node.type === 'blockquote'
@ -67,11 +70,7 @@ const MessageMarkdown = React.memo(props => (
return true; return true;
}} }}
plugins={[[ plugins={[RemarkBreaks]} />
RemarkBreaks,
RemarkDisableTokenizers,
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
]]} />
)); ));

View File

@ -60,7 +60,7 @@ const ModalButton = (props) => {
</Box> </Box>
</Box> </Box>
)} )}
<Box <Button
onClick={() => setModalShown(true)} onClick={() => setModalShown(true)}
display="flex" display="flex"
alignItems="center" alignItems="center"
@ -73,7 +73,7 @@ const ModalButton = (props) => {
{...rest} {...rest}
> >
<Icon icon={props.icon} mr={2} color={color}></Icon><Text color={color}>{props.text}</Text> <Icon icon={props.icon} mr={2} color={color}></Icon><Text color={color}>{props.text}</Text>
</Box> </Button>
</> </>
); );
} }

View File

@ -48,10 +48,9 @@ export function LinkWindow(props: LinkWindowProps) {
}, [graph.size]); }, [graph.size]);
const first = graph.peekLargest()?.[0]; const first = graph.peekLargest()?.[0];
const [,,ship, name] = association['app-path'].split('/'); const [,,ship, name] = association['app-path'].split('/');
const style = useMemo(() => const style = useMemo(() =>
({ ({
height: "100%", height: "100%",
width: "100%", width: "100%",
@ -60,6 +59,14 @@ export function LinkWindow(props: LinkWindowProps) {
alignItems: 'center' alignItems: 'center'
}), []); }), []);
if (!first) {
return (
<Col key={0} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink={0} px={3}>
<LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} />
</Col>
);
}
return ( return (
<VirtualScroller <VirtualScroller
ref={(l) => (virtualList.current = l ?? undefined)} ref={(l) => (virtualList.current = l ?? undefined)}
@ -82,7 +89,7 @@ export function LinkWindow(props: LinkWindowProps) {
if(index.eq(first ?? bigInt.zero)) { if(index.eq(first ?? bigInt.zero)) {
return ( return (
<> <>
<Col key={index.toString()} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink='0' px={3}> <Col key={index.toString()} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink={0} px={3}>
<LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} /> <LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} />
</Col> </Col>
<LinkItem {...linkProps} /> <LinkItem {...linkProps} />

View File

@ -1,18 +1,16 @@
import React, { useEffect, useCallback } from "react"; import React, { useEffect, useCallback, useRef, useState } from "react";
import f from "lodash/fp"; import f from "lodash/fp";
import _ from "lodash"; import _ from "lodash";
import { Icon, Col, Row, Box, Text, Anchor, Rule } from "@tlon/indigo-react"; import { Icon, Col, Row, Box, Text, Anchor, Rule, Center } from "@tlon/indigo-react";
import moment from "moment"; import moment from "moment";
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups } from "~/types"; import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util"; import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
import { BigInteger } from "big-integer"; import { BigInteger } from "big-integer";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { Notification } from "./notification"; import { Notification } from "./notification";
import { Associations } from "~/types"; import { Associations } from "~/types";
import { cite } from '~/logic/lib/util'; import {Invites} from "./invites";
import { InviteItem } from '~/views/components/Invite'; import {useLazyScroll} from "~/logic/lib/useLazyScroll";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { useHistory } from "react-router-dom";
type DatedTimebox = [BigInteger, Timebox]; type DatedTimebox = [BigInteger, Timebox];
@ -45,10 +43,10 @@ export default function Inbox(props: {
contacts: Rolodex; contacts: Rolodex;
filter: string[]; filter: string[];
invites: any; invites: any;
notificationsGroupConfig: GroupNotificationsConfig;
notificationsGraphConfig: NotificationGraphConfig;
}) { }) {
const { api, associations, invites } = props; const { api, associations, invites } = props;
const waiter = useWaitForProps(props)
const history = useHistory();
useEffect(() => { useEffect(() => {
let seen = false; let seen = false;
setTimeout(() => { setTimeout(() => {
@ -75,12 +73,12 @@ export default function Inbox(props: {
}; };
let notificationsByDay = f.flow( let notificationsByDay = f.flow(
f.map<DatedTimebox>(([date, nots]) => [ f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
date, date,
nots.filter(filterNotification(associations, props.filter)), nots.filter(filterNotification(associations, props.filter)),
]), ]),
f.groupBy<DatedTimebox>(([date]) => { f.groupBy<DatedTimebox>(([d]) => {
date = moment(daToUnix(date)); const date = moment(daToUnix(d));
if (moment().subtract(6, 'hours').isBefore(date)) { if (moment().subtract(6, 'hours').isBefore(date)) {
return 'latest'; return 'latest';
} else { } else {
@ -88,69 +86,27 @@ export default function Inbox(props: {
} }
}), }),
)(notifications); )(notifications);
notificationsByDay = new Map(Object.keys(notificationsByDay).sort().reverse().map(timebox => {
return [timebox, notificationsByDay[timebox]];
}));
useEffect(() => { const notificationsByDayMap = new Map<string, DatedTimebox[]>(
api.hark.getMore(props.showArchive); Object.keys(notificationsByDay).map(timebox => {
}, [props.showArchive]); return [timebox, notificationsByDay[timebox]];
})
);
const onScroll = useCallback((e) => { const scrollRef = useRef(null);
let container = e.target;
const { scrollHeight, scrollTop, clientHeight } = container;
if((scrollHeight - scrollTop) < 1.5 * clientHeight) {
api.hark.getMore(props.showArchive);
}
}, [props.showArchive]);
const acceptInvite = (app: string, uid: string) => async (invite) => { const loadMore = useCallback(async () => {
const resource = { return api.hark.getMore();
ship: `~${invite.resource.ship}`, }, [api]);
name: invite.resource.name
};
const resourcePath = resourceAsPath(invite.resource); const loadedAll = useLazyScroll(scrollRef, 0.2, loadMore);
if(app === 'contacts') {
await api.contacts.join(resource);
await waiter(p => resourcePath in p.associations?.contacts);
await api.invite.accept(app, uid);
history.push(`/~landscape${resourcePath}`);
} else if ( app === 'chat') {
await api.invite.accept(app, uid);
history.push(`/~landscape/home/resource/chat${resourcePath.slice(5)}`);
} else if ( app === 'graph') {
await api.invite.accept(app, uid);
history.push(`/~graph/join${resourcePath}`);
}
};
const inviteItems = (invites, api) => {
const returned = [];
Object.keys(invites).map((appKey) => {
const app = invites[appKey];
Object.keys(app).map((uid) => {
const invite = app[uid];
const inviteItem =
<InviteItem
key={uid}
invite={invite}
onAccept={acceptInvite(appKey, uid)}
onDecline={() => api.invite.decline(appKey, uid)}
/>;
returned.push(inviteItem);
});
});
return returned;
};
return ( return (
<Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} > <Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky" flexShrink={0}> <Invites invites={invites} api={api} associations={associations} />
{inviteItems(invites, api)} {[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
</Col> const timeboxes = notificationsByDayMap.get(day)!;
{[...notificationsByDay.keys()].map((day, index) => {
const timeboxes = notificationsByDay.get(day);
return timeboxes.length > 0 && ( return timeboxes.length > 0 && (
<DaySection <DaySection
key={day} key={day}
@ -163,10 +119,14 @@ export default function Inbox(props: {
groups={props.groups} groups={props.groups}
graphConfig={props.notificationsGraphConfig} graphConfig={props.notificationsGraphConfig}
groupConfig={props.notificationsGroupConfig} groupConfig={props.notificationsGroupConfig}
chatConfig={props.notificationsChatConfig}
/> />
); );
})} })}
{loadedAll && (
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="washedGray" width="100%" height="96px">
<Text gray fontSize="1">No more notifications</Text>
</Center>
)}
</Col> </Col>
); );
} }
@ -192,7 +152,6 @@ function DaySection({
api, api,
groupConfig, groupConfig,
graphConfig, graphConfig,
chatConfig,
}) { }) {
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0); const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
@ -202,23 +161,22 @@ function DaySection({
return ( return (
<> <>
<Box position="sticky" zIndex="3" top="-1px" bg="white"> <Box position="sticky" zIndex={3} top="-1px" bg="white">
<Box p="2" bg="scales.black05"> <Box p="2" bg="scales.black05">
<Text> <Text>
{label} {label}
</Text> </Text>
</Box> </Box>
</Box> </Box>
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) => {_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
_.map(nots.sort(sortIndexedNotification), (not, j: number) => ( _.map(nots.sort(sortIndexedNotification), (not, j: number) => (
<React.Fragment key={j}> <React.Fragment key={j}>
{(i !== 0 || j !== 0) && ( {(i !== 0 || j !== 0) && (
<Box flexShrink="0" height="4px" bg="scales.black05" /> <Box flexShrink={0} height="4px" bg="scales.black05" />
)} )}
<Notification <Notification
graphConfig={graphConfig} graphConfig={graphConfig}
groupConfig={groupConfig} groupConfig={groupConfig}
chatConfig={chatConfig}
api={api} api={api}
associations={associations} associations={associations}
notification={not} notification={not}

View File

@ -0,0 +1,74 @@
import React, { useCallback } from "react";
import { Box, Row, Col } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Invites as IInvites, Associations, Invite } from "~/types";
import { resourceAsPath } from "~/logic/lib/util";
import { useHistory } from "react-router-dom";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import InviteItem from "~/views/components/Invite";
interface InvitesProps {
api: GlobalApi;
invites: IInvites;
associations: Associations;
}
export function Invites(props: InvitesProps) {
const { api, invites } = props;
const history = useHistory();
const waiter = useWaitForProps(props);
const acceptInvite = (
app: string,
uid: string,
invite: Invite
) => async () => {
const resource = {
ship: `~${invite.resource.ship}`,
name: invite.resource.name,
};
const resourcePath = resourceAsPath(invite.resource);
if (app === "contacts") {
await api.contacts.join(resource);
await waiter((p) => resourcePath in p.associations?.contacts);
await api.invite.accept(app, uid);
history.push(`/~landscape${resourcePath}`);
} else if (app === "graph") {
await api.invite.accept(app, uid);
history.push(`/~graph/join${resourcePath}`);
}
};
const declineInvite = useCallback(
(app: string, uid: string) => () => api.invite.decline(app, uid),
[api]
);
return (
<Col
zIndex={4}
gapY={2}
bg="white"
top="0px"
position="sticky"
flexShrink={0}
>
{Object.keys(invites).reduce((items, appKey) => {
const app = invites[appKey];
let appItems = Object.keys(app).map((uid) => {
const invite = app[uid];
return (
<InviteItem
key={uid}
invite={invite}
onAccept={acceptInvite(appKey, uid, invite)}
onDecline={declineInvite(appKey, uid)}
/>
);
});
return [...items, ...appItems];
}, [] as JSX.Element[])}
</Col>
);
}

View File

@ -29,7 +29,6 @@ interface NotificationProps {
contacts: Contacts; contacts: Contacts;
graphConfig: NotificationGraphConfig; graphConfig: NotificationGraphConfig;
groupConfig: GroupNotificationsConfig; groupConfig: GroupNotificationsConfig;
chatConfig: string[];
} }
function getMuted( function getMuted(

View File

@ -90,6 +90,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
api, api,
sigilClass, sigilClass,
hideAvatars, hideAvatars,
pr = 0,
pl = 0,
...rest ...rest
} = this.props; } = this.props;
@ -113,6 +115,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
onClick={this.profileShow} onClick={this.profileShow}
ref={this.containerRef} ref={this.containerRef}
className={className} className={className}
pr={pr}
pl={pl}
> >
{state.clicked && ( {state.clicked && (
<ProfileOverlay <ProfileOverlay