interface: dismiss join on view

This commit is contained in:
Liam Fitzgerald 2021-11-17 16:34:35 -05:00
parent fd64a627c3
commit c91784d3bf
4 changed files with 299 additions and 193 deletions

View File

@ -1,9 +1,9 @@
import { Col, Row, Text, Icon } from '@tlon/indigo-react';
import { Metadata } from '@urbit/api';
import React, { ReactElement, ReactNode } from 'react';
import { PropFunc, IconRef } from '~/types';
import { MetadataIcon } from './MetadataIcon';
import { useCopy } from '~/logic/lib/useCopy';
import { Col, Row, Text, Icon } from "@tlon/indigo-react";
import { Metadata } from "@urbit/api";
import React, { ReactElement, ReactNode } from "react";
import { PropFunc, IconRef } from "~/types";
import { MetadataIcon } from "./MetadataIcon";
import { useCopy } from "~/logic/lib/useCopy";
interface GroupSummaryProps {
metadata: Metadata;
memberCount: number;
@ -28,11 +28,12 @@ export function GroupSummary(
} = props;
const { doCopy, copyDisplay } = useCopy(
`web+urbitgraph://group${resource?.slice(5)}`,
'Copy',
'Checkmark'
"Copy",
"Checkmark"
);
return (
<Col {...rest} gapY={4} maxWidth={['100%', '288px']}>
<Col {...rest} gapY={4} maxWidth={["100%", "288px"]}>
<Row gapX={2} width="100%">
<MetadataIcon
width="40px"
@ -53,9 +54,9 @@ export function GroupSummary(
{props?.AllowCopy && (
<Icon
color="gray"
icon={props?.locked ? 'Locked' : (copyDisplay as IconRef)}
icon={props?.locked ? "Locked" : (copyDisplay as IconRef)}
onClick={!props?.locked ? doCopy : null}
cursor={props?.locked ? 'default' : 'pointer'}
cursor={props?.locked ? "default" : "pointer"}
/>
)}
</Row>
@ -69,8 +70,8 @@ export function GroupSummary(
</Row>
</Col>
</Row>
<Row width="100%">
{metadata.description && (
{metadata.description.length > 0 && (
<Row width="100%">
<Text
gray
width="100%"
@ -80,8 +81,8 @@ export function GroupSummary(
>
{metadata.description}
</Text>
)}
</Row>
</Row>
)}
{children}
</Col>
);

View File

@ -2,6 +2,7 @@ import { readGroup } from '@urbit/api';
import _ from 'lodash';
import React, { useCallback, useEffect } from 'react';
import Helmet from 'react-helmet';
import { Box } from '@tlon/indigo-react';
import {
Route,
RouteComponentProps, Switch
@ -26,6 +27,7 @@ import { PopoverRoutes } from './PopoverRoutes';
import { Resource } from './Resource';
import { Skeleton } from './Skeleton';
import airlock from '~/logic/api';
import {Join, JoinRoute} from './Join';
interface GroupsPaneProps {
baseUrl: string;
@ -59,6 +61,13 @@ export function GroupsPane(props: GroupsPaneProps) {
if (workspace.type !== 'group') {
return;
}
const { pendingJoin, doneJoin } = useGroupState.getState();
const group = getGroupFromWorkspace(workspace)!;
if(group in pendingJoin) {
doneJoin(group);
}
return () => {
setRecentGroups(gs => _.uniq([workspace.group, ...gs]));
};
@ -175,7 +184,31 @@ export function GroupsPane(props: GroupsPaneProps) {
</>
);
}}
/>
/>
<Route
path={relativePath('/pending/:ship/:name')}
render={(routeProps) => {
const { ship, name } = routeProps.match.params as Record<string, string>;
const desc = {
group: `/ship/${ship}/${name}`,
kind: 'graph' as const
};
return (<Skeleton
mobileHide
recentGroups={recentGroups}
{...props}
baseUrl={baseUrl}
>
<Box width="100%">
<Join desc={desc} />
</Box>
</Skeleton>
)
}}
>
</Route>
<Route
path={relativePath('/new')}
render={(routeProps) => {

View File

@ -13,11 +13,12 @@ import Dot from '~/views/components/Dot';
import { useHarkDm, useHarkStat } from '~/logic/state/hark';
import useSettingsState from '~/logic/state/settings';
import useGraphState from '~/logic/state/graph';
import {usePreview} from '~/logic/state/metadata';
function useAssociationStatus(resource: string) {
const [, , ship, name] = resource.split('/');
const [, , ship, name] = resource.split("/");
const graphKey = `${deSig(ship)}/${name}`;
const isSubscribed = useGraphState(s => s.graphKeys.has(graphKey));
const isSubscribed = useGraphState((s) => s.graphKeys.has(graphKey));
const stats = useHarkStat(`/graph/~${graphKey}`);
const { count, each } = stats;
const hasNotifications = false;
@ -43,6 +44,7 @@ function SidebarItemBase(props: {
title: string | ReactNode;
mono?: boolean;
pending?: boolean;
onClick?: () => void;
}) {
const {
title,
@ -53,22 +55,24 @@ function SidebarItemBase(props: {
hasUnread,
isSynced = false,
mono = false,
pending = false
pending = false,
onClick
} = props;
const color = isSynced
? hasUnread || hasNotification
? 'black'
: 'gray'
: 'lightGray';
? "black"
: "gray"
: "lightGray";
const fontWeight = hasUnread || hasNotification ? '500' : 'normal';
const fontWeight = hasUnread || hasNotification ? "500" : "normal";
return (
<HoverBoxLink
// ref={anchorRef}
to={to}
bg={pending ? 'lightBlue' : 'white'}
bgActive={pending ? 'washedBlue' : 'washedGray'}
onClick={onClick}
bg={pending ? "lightBlue" : "white"}
bgActive={pending ? "washedBlue" : "washedGray"}
width="100%"
display="flex"
justifyContent="space-between"
@ -108,7 +112,7 @@ function SidebarItemBase(props: {
mono={mono}
color={color}
fontWeight={fontWeight}
style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}
style={{ textOverflow: "ellipsis", whiteSpace: "pre" }}
>
{title}
</Text>
@ -118,156 +122,201 @@ function SidebarItemBase(props: {
);
}
export const SidebarDmItem = React.memo((props: {
ship: string;
selected?: boolean;
workspace: Workspace;
pending?: boolean;
}) => {
const { ship, selected = false, pending = false } = props;
const contact = useContact(ship);
const { hideAvatars, hideNicknames } = useSettingsState(s => s.calm);
const title =
!hideNicknames && contact?.nickname
? contact?.nickname
: cite(ship) ?? ship;
const { count, each } = useHarkDm(ship);
const unreads = count + each.length;
const img =
contact?.avatar && !hideAvatars ? (
<BaseImage
referrerPolicy="no-referrer"
src={contact.avatar}
width="16px"
height="16px"
borderRadius={2}
/>
) : (
<Sigil
ship={ship}
color={`#${uxToHex(contact?.color || '0x0')}`}
icon
padding={2}
size={16}
/>
);
return (
<SidebarItemBase
selected={selected}
hasNotification={false}
hasUnread={(unreads as number) > 0}
to={`/~landscape/messages/dm/${ship}`}
title={title}
mono={hideAvatars || !contact?.nickname}
isSynced
pending={pending}
>
{img}
</SidebarItemBase>
);
});
// eslint-disable-next-line max-lines-per-function
export const SidebarAssociationItem = React.memo((props: {
hideUnjoined: boolean;
association: Association;
export const SidebarPendingItem = (props: {
path: string;
selected: boolean;
workspace: Workspace;
}) => {
const { association, selected } = props;
const title = getItemTitle(association) || '';
const appName = association?.['app-name'];
let mod: string = appName;
if (association?.metadata?.config && 'graph' in association.metadata.config) {
mod = association.metadata.config.graph ;
}
const rid = association?.resource;
const groupPath = association?.group;
const group = useGroupState(state => state.groups[groupPath]);
const { hideNicknames } = useSettingsState(s => s.calm);
const contacts = useContactState(s => s.contacts);
const isUnmanaged = group?.hidden || false;
const DM = isUnmanaged && props.workspace?.type === 'messages';
const itemStatus = useAssociationStatus(rid);
const hasNotification = itemStatus === 'notification';
const hasUnread = itemStatus === 'unread';
const isSynced = itemStatus !== 'unsubscribed';
let baseUrl = `/~landscape${groupPath}`;
if (DM) {
baseUrl = '/~landscape/messages';
} else if (isUnmanaged) {
baseUrl = '/~landscape/home';
}
const to = isSynced
? `${baseUrl}/resource/${mod}${rid}`
: `${baseUrl}/join/${mod}${rid}`;
if (props.hideUnjoined && !isSynced) {
return null;
}
const participantNames = (str: string) => {
const color = isSynced
? hasUnread || hasNotification
? 'black'
: 'gray'
: 'lightGray';
if (_.includes(str, ',') && _.startsWith(str, '~')) {
const names = _.split(str, ', ');
return names.map((name, idx) => {
if (urbitOb.isValidPatp(name)) {
if (contacts[name]?.nickname && !hideNicknames)
return (
<Text key={name} bold={hasUnread} color={color}>
{contacts[name]?.nickname}
{idx + 1 != names.length ? ', ' : null}
</Text>
);
return (
<Text key={name} mono bold={hasUnread} color={color}>
{name}
<Text color={color}>{idx + 1 != names.length ? ', ' : null}</Text>
</Text>
);
} else {
return name;
}
});
} else {
return str;
}
};
const { path, selected } = props;
const { preview, error } = usePreview(path);
const color = `#${uxToHex(preview?.metadata?.color || "0x0")}`;
const title = preview?.metadata?.title || path;
const to = `/~landscape/messages/pending/${path.slice(6)}`;
return (
<SidebarItemBase
to={to}
title={title}
selected={selected}
hasUnread={hasUnread}
isSynced={isSynced}
title={
DM && !urbitOb.isValidPatp(title) ? participantNames(title) : title
}
hasNotification={hasNotification}
hasNotification={false}
hasUnread={false}
pending
>
{DM ? (
<Box
flexShrink={0}
height={16}
width={16}
borderRadius={2}
backgroundColor={
`#${uxToHex(props?.association?.metadata?.color)}` || '#000000'
}
/>
) : (
<Icon
display="block"
color={isSynced ? 'black' : 'lightGray'}
icon={getModuleIcon(mod as any)}
/>
)}
<Box
flexShrink={0}
height={16}
width={16}
borderRadius={2}
backgroundColor={color}
/>
</SidebarItemBase>
);
});
}
export const SidebarDmItem = React.memo(
(props: {
ship: string;
selected?: boolean;
workspace: Workspace;
pending?: boolean;
}) => {
const { ship, selected = false, pending = false } = props;
const contact = useContact(ship);
const { hideAvatars, hideNicknames } = useSettingsState((s) => s.calm);
const title =
!hideNicknames && contact?.nickname
? contact?.nickname
: cite(ship) ?? ship;
const { count, each } = useHarkDm(ship);
const unreads = count + each.length;
const img =
contact?.avatar && !hideAvatars ? (
<BaseImage
referrerPolicy="no-referrer"
src={contact.avatar}
width="16px"
height="16px"
borderRadius={2}
/>
) : (
<Sigil
ship={ship}
color={`#${uxToHex(contact?.color || "0x0")}`}
icon
padding={2}
size={16}
/>
);
return (
<SidebarItemBase
selected={selected}
hasNotification={false}
hasUnread={(unreads as number) > 0}
to={`/~landscape/messages/dm/${ship}`}
title={title}
mono={hideAvatars || !contact?.nickname}
isSynced
pending={pending}
>
{img}
</SidebarItemBase>
);
}
);
// eslint-disable-next-line max-lines-per-function
export const SidebarAssociationItem = React.memo(
(props: {
hideUnjoined: boolean;
association: Association;
selected: boolean;
workspace: Workspace;
}) => {
const { association, selected } = props;
const title = association ? getItemTitle(association) || "" : "";
const appName = association?.["app-name"];
let mod: string = appName;
if (
association?.metadata?.config &&
"graph" in association.metadata.config
) {
mod = association.metadata.config.graph;
}
const pending = useGroupState(s => association.group in s.pendingJoin);
console.log(pending);
const rid = association?.resource;
const { hideNicknames } = useSettingsState((s) => s.calm);
const contacts = useContactState((s) => s.contacts);
const group = useGroupState(s => association ? s.groups[association.group] : undefined);
const isUnmanaged = group?.hidden || false;
const DM = isUnmanaged && props.workspace?.type === "messages";
const itemStatus = useAssociationStatus(rid);
const hasNotification = itemStatus === "notification";
const hasUnread = itemStatus === "unread";
const isSynced = itemStatus !== "unsubscribed";
let baseUrl = `/~landscape${association.group}`;
if (DM) {
baseUrl = "/~landscape/messages";
} else if (isUnmanaged) {
baseUrl = "/~landscape/home";
}
const to = isSynced
? `${baseUrl}/resource/${mod}${rid}`
: `${baseUrl}/join/${mod}${rid}`;
const onClick = pending ? () => {
useGroupState.getState().doneJoin(rid);
} : undefined;
if (props.hideUnjoined && !isSynced) {
return null;
}
const participantNames = (str: string) => {
const color = isSynced
? hasUnread || hasNotification
? "black"
: "gray"
: "lightGray";
if (_.includes(str, ",") && _.startsWith(str, "~")) {
const names = _.split(str, ", ");
return names.map((name, idx) => {
if (urbitOb.isValidPatp(name)) {
if (contacts[name]?.nickname && !hideNicknames)
return (
<Text key={name} bold={hasUnread} color={color}>
{contacts[name]?.nickname}
{idx + 1 != names.length ? ", " : null}
</Text>
);
return (
<Text key={name} mono bold={hasUnread} color={color}>
{name}
<Text color={color}>
{idx + 1 != names.length ? ", " : null}
</Text>
</Text>
);
} else {
return name;
}
});
} else {
return str;
}
};
return (
<SidebarItemBase
to={to}
selected={selected}
hasUnread={hasUnread}
isSynced={isSynced}
title={
DM && !urbitOb.isValidPatp(title) ? participantNames(title) : title
}
hasNotification={hasNotification}
pending={pending}
onClick={onClick}
>
{DM ? (
<Box
flexShrink={0}
height={16}
width={16}
borderRadius={2}
backgroundColor={
`#${uxToHex(props?.association?.metadata?.color)}` || "#000000"
}
/>
) : (
<Icon
display="block"
color={isSynced ? "black" : "lightGray"}
icon={getModuleIcon(mod as any)}
/>
)}
</SidebarItemBase>
);
}
);

View File

@ -3,7 +3,7 @@ import { Associations, Graph, Unreads } from '@urbit/api';
import { patp, patp2dec } from 'urbit-ob';
import _ from 'lodash';
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
import { SidebarAssociationItem, SidebarDmItem, SidebarPendingItem } from './SidebarItem';
import useGraphState, { useInbox } from '~/logic/state/graph';
import useHarkState from '~/logic/state/hark';
import { alphabeticalOrder, getResourcePath, modulo } from '~/logic/lib/util';
@ -12,8 +12,10 @@ import { Workspace } from '~/types/workspace';
import useMetadataState from '~/logic/state/metadata';
import { useHistory } from 'react-router';
import { useShortcut } from '~/logic/state/settings';
import useGroupState from '~/logic/state/group';
import useInviteState from '~/logic/state/invite';
function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort, (a: string, b: string) => number> {
function sidebarSort(unreads: Unreads, pending: string[]): Record<SidebarSort, (a: string, b: string) => number> {
const { associations } = useMetadataState.getState();
const alphabetical = (a: string, b: string) => {
const aAssoc = associations[a];
@ -25,8 +27,8 @@ function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort
};
const lastUpdated = (a: string, b: string) => {
const aPend = pending.has(a.slice(1));
const bPend = pending.has(b.slice(1));
const aPend = pending.includes(a);
const bPend = pending.includes(b);
if(aPend && !bPend) {
return -1;
}
@ -50,7 +52,7 @@ function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort
};
}
function getItems(associations: Associations, workspace: Workspace, inbox: Graph, pending: Set<string>) {
function getItems(associations: Associations, workspace: Workspace, inbox: Graph, pending: string[]) {
const filtered = Object.keys(associations.graph).filter((a) => {
const assoc = associations.graph[a];
if(!('graph' in assoc.metadata.config)) {
@ -84,9 +86,9 @@ function getItems(associations: Associations, workspace: Workspace, inbox: Graph
: inbox.keys().map(x => patp(x.toString()));
const pend = workspace.type !== 'messages'
? []
: Array.from(pending).map(s => `~${s}`);
: pending
return [...filtered, ..._.union(direct, pend)];
return _.union(direct, pend, filtered);
}
export function SidebarList(props: {
@ -98,9 +100,18 @@ export function SidebarList(props: {
}): ReactElement {
const { selected, config, workspace } = props;
const associations = useMetadataState(state => state.associations);
const groups = useGroupState(s => s.groups);
const inbox = useInbox();
const graphKeys = useGraphState(s => s.graphKeys);
const pending = useGraphState(s => s.pendingDms);
const pendingDms = useGraphState(s => [...s.pendingDms].map(s => `~${s}`));
const pendingGroupChats = useGroupState(s => _.pickBy(s.pendingJoin, (req, rid) => !(rid in groups) && req.app === 'graph'));
const inviteGroupChats = useInviteState(
s => Object.values(s.invites?.['graph'] || {})
.map(inv => {
return `/ship/~${inv.resource.ship}/${inv.resource.name}`
})
);
const pending = [...pendingDms, ...Object.keys(pendingGroupChats), ...inviteGroupChats];
const unreads = useHarkState(s => s.unreads);
const ordered = getItems(associations, workspace, inbox, pending)
@ -118,10 +129,16 @@ export function SidebarList(props: {
if(newChannel.startsWith('~')) {
path = `/~landscape/messages/dm/${newChannel}`;
} else {
const { metadata, resource } = associations.graph[ordered[newIdx]];
const joined = graphKeys.has(resource.slice(7));
if ('graph' in metadata.config) {
path = getResourcePath(workspace, resource, joined, metadata.config.graph);
const association = associations.graph[ordered[newIdx]];
if(!association) {
path = `/~landscape/messages`
return;
} else {
const { metadata, resource } = association;
const joined = graphKeys.has(resource.slice(7));
if ('graph' in metadata.config) {
path = getResourcePath(workspace, resource, joined, metadata.config.graph);
}
}
}
history.push(path);
@ -140,7 +157,22 @@ export function SidebarList(props: {
return (
<>
{ordered.map((pathOrShip) => {
return pathOrShip.startsWith('/') ? (
return pathOrShip.startsWith('~') ? (
<SidebarDmItem
key={pathOrShip}
ship={pathOrShip}
workspace={workspace}
selected={pathOrShip === selected}
pending={pending.includes(pathOrShip)}
/>
) : pending.includes(pathOrShip) ? (
<SidebarPendingItem
key={pathOrShip}
path={pathOrShip}
selected={pathOrShip === selected}
/>
) : (
<SidebarAssociationItem
key={pathOrShip}
selected={pathOrShip === selected}
@ -148,16 +180,7 @@ export function SidebarList(props: {
hideUnjoined={config.hideUnjoined}
workspace={workspace}
/>
) : (
<SidebarDmItem
key={pathOrShip}
ship={pathOrShip}
workspace={workspace}
selected={pathOrShip === selected}
pending={pending.has(pathOrShip.slice(1))}
/>
);
) ;
})}
</>
);