diff --git a/pkg/interface/src/views/apps/notifications/invites.tsx b/pkg/interface/src/views/apps/notifications/invites.tsx index 0cf264e28..a856af68d 100644 --- a/pkg/interface/src/views/apps/notifications/invites.tsx +++ b/pkg/interface/src/views/apps/notifications/invites.tsx @@ -1,8 +1,9 @@ import React, { useCallback, useState } from "react"; +import _ from 'lodash'; import { Box, Row, Col } from "@tlon/indigo-react"; import GlobalApi from "~/logic/api/global"; -import { Invites as IInvites, Associations, Invite, JoinRequests, Groups } from "~/types"; -import { resourceAsPath } from "~/logic/lib/util"; +import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contacts, AppInvites, JoinProgress } from "~/types"; +import { resourceAsPath, alphabeticalOrder } from "~/logic/lib/util"; import { useHistory } from "react-router-dom"; import { useWaitForProps } from "~/logic/lib/useWaitForProps"; import InviteItem from "~/views/components/Invite"; @@ -14,10 +15,17 @@ interface InvitesProps { api: GlobalApi; invites: IInvites; groups: Groups; + contacts: Contacts; associations: Associations; pendingJoin: JoinRequests; } +interface InviteRef { + uid: string; + app: string + invite: Invite; +} + export function Invites(props: InvitesProps) { const { api, invites, pendingJoin } = props; const [selected, setSelected] = useState<[string, string, Invite] | undefined>() @@ -48,7 +56,18 @@ export function Invites(props: InvitesProps) { inviteUid={uid} inviteApp={app} /> - )}}); + )}}); + + const inviteArr: InviteRef[] = _.reduce(invites, (acc: InviteRef[], val: AppInvites, app: string) => { + const appInvites = _.reduce(val, (invs: InviteRef[], invite: Invite, uid: string) => { + return [...invs, { invite, uid, app }]; + + }, []); + return [...acc, ...appInvites]; + }, []); + + const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } = + {..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin }; @@ -61,33 +80,42 @@ export function Invites(props: InvitesProps) { position="sticky" flexShrink={0} > - {modal} { Object - .keys(props.pendingJoin) - .map(resource => ( - - )) - } + .keys(invitesAndStatus) + .sort(alphabeticalOrder) + .map(resource => { + const inviteOrStatus = invitesAndStatus[resource]; + if(typeof inviteOrStatus === 'string') { + return ( + + ) - {Object.keys(invites).reduce((items, appKey) => { - const app = invites[appKey]; - let appItems = Object.keys(app).map((uid) => { - const invite = app[uid]; + } else { + const { app, uid, invite } = inviteOrStatus; + console.log(inviteOrStatus); return ( - ); - }); - return [...items, ...appItems]; - }, [] as JSX.Element[])} + ) + } + })} ); } diff --git a/pkg/interface/src/views/apps/notifications/joining.tsx b/pkg/interface/src/views/apps/notifications/joining.tsx index 5fb3e2511..7b2295fb2 100644 --- a/pkg/interface/src/views/apps/notifications/joining.tsx +++ b/pkg/interface/src/views/apps/notifications/joining.tsx @@ -1,49 +1,45 @@ import React, { useState, useEffect } from "react"; -import { Col, Text, SegmentedProgressBar } from "@tlon/indigo-react"; +import { Col, Row, Text, SegmentedProgressBar, Box } from "@tlon/indigo-react"; import GlobalApi from "~/logic/api/global"; -import { JoinProgress, joinProgress, MetadataUpdatePreview, joinError } from "~/types"; +import { + JoinProgress, + joinProgress, + MetadataUpdatePreview, + joinError, +} from "~/types"; import { clamp } from "~/logic/lib/util"; interface JoiningStatusProps { - resource: string; - api: GlobalApi; status: JoinProgress; } -const description: string[] = - ["Attempting to contact group host", - "Retrieving group data", - "Finished join", - "Unable to join group, you do not have the correct permissions", - "Internal error, please file an issue" - ]; - - +const description: string[] = [ + "Attempting to contact host", + "Retrieving data", + "Finished join", + "Unable to join, you do not have the correct permissions", + "Internal error, please file an issue", +]; export function JoiningStatus(props: JoiningStatusProps) { - const { resource, status, api } = props; - - const [preview, setPreview] = useState(null); - - useEffect(() => { - (async () => { - const prev = await api.metadata.preview(resource); - setPreview(prev) - })(); - return () => { - setPreview(null); - } - }, [resource]) + const { status } = props; const current = joinProgress.indexOf(status); const desc = description?.[current] || ""; - const title = preview?.metadata?.title ?? resource; const isError = joinError.indexOf(status as any) !== -1; return ( - - {isError ? "Failed to join " : "Joining "} {title} - {desc} - - + + + + + + {desc} + + ); } diff --git a/pkg/interface/src/views/components/Invite.tsx b/pkg/interface/src/views/components/Invite.tsx deleted file mode 100644 index 70c16ab4c..000000000 --- a/pkg/interface/src/views/components/Invite.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { Component } from 'react'; -import { Invite } from '~/types/invite-update'; -import { Text, Box, Button, Row, Rule } from '@tlon/indigo-react'; -import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; -import { cite } from '~/logic/lib/util'; - -export class InviteItem extends Component<{invite: Invite, onAccept: (i: any) => Promise, onDecline: (i: any) => Promise}, {}> { - render() { - const { props } = this; - - return ( - <> - - - - {cite(props.invite.resource.ship)} - {" "}invited you to{" "} - {props.invite.resource.name} - - {props.invite.text && ( - - {props.invite.text} - - )} - - props.onAccept(props.invite)} - color='blue' - mr='2' - > - Accept - - props.onDecline(props.invite)} - > - Reject - - - - - - - ); - } -} - -export default InviteItem; diff --git a/pkg/interface/src/views/components/Invite/Group.tsx b/pkg/interface/src/views/components/Invite/Group.tsx new file mode 100644 index 000000000..ea5c37b98 --- /dev/null +++ b/pkg/interface/src/views/components/Invite/Group.tsx @@ -0,0 +1,76 @@ +import React, { ReactNode } from "react"; +import { Text, Box, Button, Icon, Row, Rule, Col } from "@tlon/indigo-react"; + +import { cite } from "~/logic/lib/util"; +import { MetadataUpdatePreview, JoinProgress, Invite } from "~/types"; +import { GroupSummary } from "~/views/landscape/components/GroupSummary"; +import { InviteSkeleton } from "./InviteSkeleton"; +import { JoinSkeleton } from "./JoinSkeleton"; + +interface GroupInviteProps { + preview: MetadataUpdatePreview; + status?: JoinProgress; + invite?: Invite; + onAccept: () => Promise; + onDecline: () => Promise; +} + +export function GroupInvite(props: GroupInviteProps) { + const { preview, invite, status, onAccept, onDecline } = props; + const { metadata, members } = props.preview; + + let inner: ReactNode = null; + let Outer: (p: { children: ReactNode }) => JSX.Element = (p) => ( + <>{p.children} + ); + + if (status) { + inner = ( + + You are joining {metadata.title} + + ); + Outer = ({ children }) => ( + + {children} + + ); + } else if (invite) { + Outer = ({ children }) => ( + + {children} + + ); + inner = ( + <> + + {cite(`~${invite!.ship}`)} + + invited you to + {metadata.title} + + ); + } + return ( + + + + {inner} + + + + + + ); +} diff --git a/pkg/interface/src/views/components/Invite/InviteSkeleton.tsx b/pkg/interface/src/views/components/Invite/InviteSkeleton.tsx new file mode 100644 index 000000000..c1b4d3899 --- /dev/null +++ b/pkg/interface/src/views/components/Invite/InviteSkeleton.tsx @@ -0,0 +1,53 @@ +import React, { ReactNode } from "react"; +import { Text, Box, Button, Icon, Row, Rule, Col } from "@tlon/indigo-react"; + +import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; +import { PropFunc } from "~/types"; + +export interface InviteSkeletonProps { + onAccept: () => Promise; + onDecline: () => Promise; + acceptDesc: string; + declineDesc: string; + children: ReactNode; +} + +export function InviteSkeleton( + props: InviteSkeletonProps & PropFunc +) { + const { + children, + acceptDesc, + declineDesc, + onAccept, + onDecline, + ...rest + } = props; + return ( + <> + + {children} + + + {acceptDesc} + + + {declineDesc} + + + + + + ); +} diff --git a/pkg/interface/src/views/components/Invite/JoinSkeleton.tsx b/pkg/interface/src/views/components/Invite/JoinSkeleton.tsx new file mode 100644 index 000000000..c3e39bd63 --- /dev/null +++ b/pkg/interface/src/views/components/Invite/JoinSkeleton.tsx @@ -0,0 +1,22 @@ +import React, { ReactNode } from "react"; +import { Col, Row, SegmentedProgressBar, Text, Rule } from "@tlon/indigo-react"; +import { JoiningStatus } from "~/views/apps/notifications/joining"; +import { JoinProgress, PropFunc } from "~/types"; + +type JoinSkeletonProps = { + children: ReactNode; + status: JoinProgress; +} & PropFunc; + +export function JoinSkeleton(props: JoinSkeletonProps) { + const { children, status, ...rest } = props; + return ( + <> + + {children} + + + + + ); +} diff --git a/pkg/interface/src/views/components/Invite/index.tsx b/pkg/interface/src/views/components/Invite/index.tsx new file mode 100644 index 000000000..fc6e9b192 --- /dev/null +++ b/pkg/interface/src/views/components/Invite/index.tsx @@ -0,0 +1,172 @@ +import React, { Component, useState, useEffect, useCallback, useMemo } from "react"; +import { Invite } from "~/types/invite-update"; +import { Text, Box, Button, Icon, Row, Rule, Col } from "@tlon/indigo-react"; +import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; +import { cite } from "~/logic/lib/util"; +import { + MetadataUpdatePreview, + Contacts, + JoinRequests, + JoinProgress, + Groups, + Associations, +} from "~/types"; +import GlobalApi from "~/logic/api/global"; +import { GroupSummary } from "~/views/landscape/components/GroupSummary"; +import { JoiningStatus } from "~/views/apps/notifications/joining"; +import { resourceFromPath } from "~/logic/lib/group"; +import { GroupInvite } from "./Group"; +import { InviteSkeleton } from "./InviteSkeleton"; +import { JoinSkeleton } from "./JoinSkeleton"; +import { useWaitForProps } from "~/logic/lib/useWaitForProps"; +import { useHistory } from "react-router-dom"; + +interface InviteItemProps { + invite?: Invite; + resource: string; + groups: Groups; + associations: Associations; + + pendingJoin: JoinRequests; + app?: string; + uid?: string; + api: GlobalApi; + contacts: Contacts; +} + +export function InviteItem(props: InviteItemProps) { + const [preview, setPreview] = useState(null); + const { associations, pendingJoin, invite, resource, uid, app, api } = props; + const { ship, name } = resourceFromPath(resource); + const waiter = useWaitForProps(props, 50000); + const status = pendingJoin[resource]; + + const history = useHistory(); + const inviteAccept = useCallback(async () => { + if (!(app && invite && uid)) { + return; + } + + api.groups.join(ship, name); + await waiter(p => resource in p.pendingJoin); + + api.invite.accept(app, uid); + await waiter((p) => { + return ( + resource in p.groups && + (resource in (p.associations?.graph ?? {}) || + resource in (p.associations?.groups ?? {})) + ); + }); + + if (props.groups?.[resource]?.hidden) { + const { metadata } = associations.graph[resource]; + if (name.startsWith("dm--")) { + history.push(`/~landscape/messages/resource/${metadata.module}${resource}`); + } else { + history.push(`/~landscape/home/resource/${metadata.module}${resource}`); + } + } else { + history.push(`/~landscape${resource}`); + } + }, [app, invite, uid, resource, props.groups, associations]); + + const inviteDecline = useCallback(async () => { + if(!(app && uid)) { + return; + } + await api.invite.decline(app, uid); + }, [app, uid]); + + const handlers = { onAccept: inviteAccept, onDecline: inviteDecline } + + useEffect(() => { + if (!app || app === "groups") { + (async () => { + setPreview(await api.metadata.preview(resource)); + })(); + return () => { + setPreview(null); + }; + } else { + return () => {}; + } + }, [invite]); + + if (preview) { + return ( + + ); + } else if (invite && name.startsWith("dm--")) { + return ( + + + + + {cite(`~${invite!.ship}`)} + + invited you to a DM + + + ); + } else if (status && name.startsWith("dm--")) { + return ( + + + + You are joining a DM with + + {cite("~hastuc-dibtux")} + + + + ); + } else if (invite) { + return ( + + + + + {cite(`~${invite!.ship}`)} + + + invited you to ~{invite.resource.ship}/{invite.resource.name} + + + + ); + } else if (status) { + const [, , ship, name] = resource.split("/"); + return ( + + + + + You are joining + + + {cite(ship)}/{name} + + + + ); + } + return null; +} + +export default InviteItem; diff --git a/pkg/interface/src/views/landscape/components/GroupSummary.tsx b/pkg/interface/src/views/landscape/components/GroupSummary.tsx index 78dec5933..6ea0259cc 100644 --- a/pkg/interface/src/views/landscape/components/GroupSummary.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSummary.tsx @@ -1,5 +1,5 @@ import React, { ReactNode, useRef } from "react"; -import { Metadata } from "~/types"; +import { Metadata, PropFunc } from "~/types"; import { Col, Row, Text } from "@tlon/indigo-react"; import { MetadataIcon } from "./MetadataIcon"; import { useTutorialModal } from "~/views/components/useTutorialModal"; @@ -11,10 +11,11 @@ interface GroupSummaryProps { channelCount: number; resource?: string; children?: ReactNode; + gray?: boolean; } -export function GroupSummary(props: GroupSummaryProps) { - const { channelCount, memberCount, metadata, resource, children } = props; +export function GroupSummary(props: GroupSummaryProps & PropFunc) { + const { channelCount, memberCount, metadata, resource, children, ...rest } = props; const anchorRef = useRef(null); useTutorialModal( "group-desc", @@ -22,7 +23,7 @@ export function GroupSummary(props: GroupSummaryProps) { anchorRef.current ); return ( - + {metadata.title} - + {memberCount} participants @@ -51,7 +52,8 @@ export function GroupSummary(props: GroupSummaryProps) { {metadata.description && - { const [,,ship,name] = group.split('/'); await api.groups.join(ship, name); - if (props.inviteUid && props.inviteApp) { - api.invite.accept(props.inviteApp, props.inviteUid); - } try { await waiter((p: JoinGroupProps) => { return group in p.groups && @@ -99,17 +93,13 @@ export function JoinGroup(props: JoinGroupProps) { // drop them into inbox to show join request still pending history.push('/~notifications'); } - }, [api, props.inviteApp, props.inviteUid, waiter, history, associations, groups]); + }, [api, waiter, history, associations, groups]); const onSubmit = useCallback( async (values: FormSchema, actions: FormikHelpers) => { const [ship, name] = values.group.split("/"); const path = `/ship/${ship}/${name}`; // skip if it's unmanaged - if(!!autojoin && props.inviteApp !== 'groups') { - await onConfirm(path); - return; - } try { const prev = await api.metadata.preview(path); actions.setStatus({ success: null }); @@ -127,7 +117,7 @@ export function JoinGroup(props: JoinGroupProps) { } } }, - [api, waiter, history, onConfirm, props.inviteApp] + [api, waiter, history, onConfirm] ); return ( @@ -152,7 +142,7 @@ export function JoinGroup(props: JoinGroupProps) { { Object.keys(preview.channels).length > 0 && (