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 && (