diff --git a/pkg/arvo/ted/graph/create.hoon b/pkg/arvo/ted/graph/create.hoon index a883d822c..745020ef7 100644 --- a/pkg/arvo/ted/graph/create.hoon +++ b/pkg/arvo/ted/graph/create.hoon @@ -1,6 +1,6 @@ /- spider, graph=graph-store, - *metadata-store, + met=metadata-store, *group, group-store, inv=invite-store @@ -57,18 +57,18 @@ :: :: Setup metadata :: -=/ =metadata - %* . *metadata +=/ =metadatum:met + %* . *metadatum:met title title.action description description.action date-created now.bowl creator our.bowl module module.action == -=/ =metadata-action - [%add group graph+rid.action metadata] +=/ met-action=action:met + [%add group graph+rid.action metadatum] ;< ~ bind:m - (poke-our %metadata-store %metadata-action !>(metadata-action)) + (poke-our %metadata-store %metadata-action !>(met-action)) ;< ~ bind:m (poke-our %metadata-push-hook %push-hook-action !>([%add group])) :: diff --git a/pkg/arvo/ted/graph/delete.hoon b/pkg/arvo/ted/graph/delete.hoon index f98ddf075..d921e4723 100644 --- a/pkg/arvo/ted/graph/delete.hoon +++ b/pkg/arvo/ted/graph/delete.hoon @@ -1,4 +1,4 @@ -/- spider, graph-view, graph=graph-store, *metadata-store, *group +/- spider, graph-view, graph=graph-store, met=metadata-store, *group /+ strandio, resource => |% @@ -9,16 +9,14 @@ ++ scry-metadata |= rid=resource =/ m (strand ,(unit resource)) - ;< paxs=(unit (set path)) bind:m - %+ scry:strandio ,(unit (set path)) + ;< group=(unit resource) bind:m + %+ scry:strandio ,(unit resource) ;: weld /gx/metadata-store/resource/graph (en-path:resource rid) /noun == - ?~ paxs (pure:m ~) - ?~ u.paxs (pure:m ~) - (pure:m `(de-path:resource n.u.paxs)) + (pure:m group) :: ++ scry-group |= rid=resource @@ -42,11 +40,7 @@ ;< ~ bind:m (poke-our %graph-push-hook %push-hook-action !>([%remove rid])) ;< ~ bind:m - %+ poke-our %metadata-hook - :- %metadata-action - !> :+ %remove - (en-path:resource group-rid) - [%graph (en-path:resource rid)] + (poke-our %metadata-push-hook %push-hook-action !>([%remove rid])) (pure:m ~) -- :: @@ -74,6 +68,5 @@ (poke-our %group-push-hook %push-hook-action !>([%remove rid.action])) ;< ~ bind:m (delete-graph u.ugroup-rid rid.action) ;< ~ bind:m - %+ poke-our %metadata-hook - metadata-hook-action+!>([%remove (en-path:resource u.ugroup-rid)]) + (poke-our %metadata-push-hook %push-hook-action !>([%remove rid.action])) (pure:m !>(~)) diff --git a/pkg/interface/src/logic/api/contacts.ts b/pkg/interface/src/logic/api/contacts.ts index 34db3c773..fab1ed90a 100644 --- a/pkg/interface/src/logic/api/contacts.ts +++ b/pkg/interface/src/logic/api/contacts.ts @@ -22,6 +22,8 @@ export default class ContactsApi extends BaseApi { {color: 'fff'} // with no 0x prefix {avatar: null} {avatar: ''} + {add-group: {ship, name}} + {remove-group: {ship, name}} */ console.log(ship, editField); return this.storeAction({ diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index 6473d86ce..0c2e27c3e 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -116,7 +116,7 @@ export default function index(contacts, associations, apps, currentGroup, groups title, `/~landscape${group}/join/${app}${each.resource}`, app.charAt(0).toUpperCase() + app.slice(1), - (associations?.contacts?.[each.group]?.metadata?.title || null) + (associations?.groups?.[each.group]?.metadata?.title || null) ); subscriptions.push(obj); } diff --git a/pkg/interface/src/logic/lib/workspace.ts b/pkg/interface/src/logic/lib/workspace.ts index 2ab8ce65b..7532bac6e 100644 --- a/pkg/interface/src/logic/lib/workspace.ts +++ b/pkg/interface/src/logic/lib/workspace.ts @@ -8,7 +8,7 @@ export function getTitleFromWorkspace( case "home": return "DMs + Drafts"; case "group": - const association = associations.contacts[workspace.group]; + const association = associations.groups[workspace.group]; return association?.metadata?.title || ""; } } diff --git a/pkg/interface/src/types/noun.ts b/pkg/interface/src/types/noun.ts index 95dedea82..f566f0240 100644 --- a/pkg/interface/src/types/noun.ts +++ b/pkg/interface/src/types/noun.ts @@ -18,7 +18,7 @@ export type Serial = string; export type Jug = Map>; // name of app -export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph'; +export type AppName = 'contacts' | 'groups' | 'graph'; export function getTagFromFrond(frond: O): keyof O { const tags = Object.keys(frond) as Array; diff --git a/pkg/interface/src/views/apps/launch/components/Groups.tsx b/pkg/interface/src/views/apps/launch/components/Groups.tsx index 42eab90c6..8a23536d2 100644 --- a/pkg/interface/src/views/apps/launch/components/Groups.tsx +++ b/pkg/interface/src/views/apps/launch/components/Groups.tsx @@ -16,7 +16,7 @@ const sortGroupsAlph = (a: Association, b: Association) => alphabeticalOrder(a.metadata.title, b.metadata.title); -const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) => +const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) => f.flow( f.pickBy((a: Association) => a.group === path), f.map('resource'), @@ -24,7 +24,7 @@ const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: f.reduce(f.add, 0) )(associations.graph); -const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) => +const getGraphNotifications = (associations: Associations, unreads: Unreads) => (path: string) => f.flow( f.pickBy((a: Association) => a.group === path), f.map('resource'), @@ -36,7 +36,7 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) => export default function Groups(props: GroupsProps & Parameters[0]) { const { associations, unreads, inbox, ...boxProps } = props; - const groups = Object.values(associations?.contacts || {}) + const groups = Object.values(associations?.groups || {}) .filter((e) => e?.group in props.groups) .sort(sortGroupsAlph); const graphUnreads = getGraphUnreads(associations || {}, unreads); @@ -78,10 +78,10 @@ function Group(props: GroupProps) { {title} - {unreads > 0 && + {unreads > 0 && ({unreads} unread{unreads !== 1 && 's'} ) } - {updates > 0 && + {updates > 0 && ({updates} update{updates !== 1 && 's'} ) } diff --git a/pkg/interface/src/views/apps/notifications/header.tsx b/pkg/interface/src/views/apps/notifications/header.tsx index b987a0693..e27a4ef31 100644 --- a/pkg/interface/src/views/apps/notifications/header.tsx +++ b/pkg/interface/src/views/apps/notifications/header.tsx @@ -63,7 +63,7 @@ export function Header(props: { const time = moment(props.time).format("HH:mm"); const groupTitle = - props.associations.contacts?.[props.group]?.metadata?.title; + props.associations.groups?.[props.group]?.metadata?.title; const app = props.chat ? 'chat' : 'graph'; const channelTitle = diff --git a/pkg/interface/src/views/apps/notifications/invites.tsx b/pkg/interface/src/views/apps/notifications/invites.tsx index 5b77fa212..b01f6ff04 100644 --- a/pkg/interface/src/views/apps/notifications/invites.tsx +++ b/pkg/interface/src/views/apps/notifications/invites.tsx @@ -31,7 +31,7 @@ export function Invites(props: InvitesProps) { const resourcePath = resourceAsPath(invite.resource); if (app === "contacts") { await api.contacts.join(resource); - await waiter((p) => resourcePath in p.associations?.contacts); + await waiter((p) => resourcePath in p.associations?.groups); await api.invite.accept(app, uid); history.push(`/~landscape${resourcePath}`); } else if (app === "graph") { diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index c4567cc09..16f2658b2 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -44,7 +44,7 @@ export default function NotificationsScreen(props: any) { filter.groups.length === 0 ? "All" : filter.groups - .map((g) => props.associations?.contacts?.[g]?.metadata?.title) + .map((g) => props.associations?.groups?.[g]?.metadata?.title) .join(", "); return ( diff --git a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx index 70b94096d..35e3a135f 100644 --- a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx @@ -21,6 +21,8 @@ import { AsyncButton } from "~/views/components/AsyncButton"; import { ColorInput } from "~/views/components/ColorInput"; import { ImageInput } from "~/views/components/ImageInput"; import { MarkdownField } from "~/views/apps/publish/components/MarkdownField"; +import { resourceFromPath } from "~/logic/lib/group"; +import GroupSearch from "~/views/components/GroupSearch"; const formSchema = Yup.object({ @@ -62,8 +64,15 @@ export function EditProfile(props: any) { return acc.then(() => api.contacts.setPublic(newValue) ); + } else if (key === 'groups') { + newValue.map((e) => { + if (!contact['groups']?.[e]) { + return acc.then(() => { + api.contacts.edit(ship, { 'add-group': resourceFromPath(e) }); + }); + } + }) } else if ( - key !== "groups" && key !== "last-updated" && key !== "isPublic" ) { @@ -93,7 +102,7 @@ export function EditProfile(props: any) { Description - + @@ -105,7 +114,8 @@ export function EditProfile(props: any) { - + + Submit diff --git a/pkg/interface/src/views/apps/profile/components/Profile.tsx b/pkg/interface/src/views/apps/profile/components/Profile.tsx index 5c3cf67d7..64e1ec0e8 100644 --- a/pkg/interface/src/views/apps/profile/components/Profile.tsx +++ b/pkg/interface/src/views/apps/profile/components/Profile.tsx @@ -66,6 +66,8 @@ export function Profile(props: any) { contact={contact} s3={props.s3} api={props.api} + groups={props.groups} + associations={props.associations} isPublic={isPublic}/> ) : ( diff --git a/pkg/interface/src/views/apps/profile/profile.tsx b/pkg/interface/src/views/apps/profile/profile.tsx index e2fdfed4d..eb7853f26 100644 --- a/pkg/interface/src/views/apps/profile/profile.tsx +++ b/pkg/interface/src/views/apps/profile/profile.tsx @@ -45,6 +45,8 @@ export default function ProfileScreen(props: any) { ` &:hover { - background-color: ${(p) => p.theme.colors.washedGray}; + background-color: ${p => p.theme.colors.washedGray}; } `; @@ -64,38 +65,45 @@ function renderCandidate( export function GroupSearch(props: InviteSearchProps) { const { id, caption, label } = props; + const [selected, setSelected] = useState([] as string[]); const groups: Association[] = useMemo(() => { return props.adminOnly ? Object.values( - Object.keys(props.associations?.contacts) + Object.keys(props.associations?.groups) .filter( - (e) => roleForShip(props.groups[e], window.ship) === "admin" + e => roleForShip(props.groups[e], window.ship) === 'admin' ) .reduce((obj, key) => { - obj[key] = props.associations?.contacts[key]; + obj[key] = props.associations?.groups[key]; return obj; }, {}) || {} ) - : Object.values(props.associations?.contacts || {}); - }, [props.associations?.contacts]); + : Object.values(props.associations?.groups || {}); + }, [props.associations?.groups]); const [{ value }, meta, { setValue, setTouched }] = useField(props.id); + useEffect(() => { + setValue(selected); + }, [selected]) + const { title: groupTitle } = - props.associations.contacts?.[value]?.metadata || {}; + props.associations.groups?.[value]?.metadata || {}; const onSelect = useCallback( - (a: Association) => { - setValue(a.group); + (s: string) => { setTouched(true); + setSelected(v => _.uniq([...v, s])); }, - [setValue] + [setTouched, setSelected] ); - const onUnselect = useCallback(() => { - setValue(undefined); - setTouched(true); - }, [setValue]); + const onRemove = useCallback( + (s: string) => { + setSelected(groups => groups.filter(group => group !== s)) + }, + [setSelected] + ); return ( @@ -105,25 +113,11 @@ export function GroupSearch(props: InviteSearchProps) { {caption} )} - {value && ( - - {groupTitle || value} - - - )} - {!value && ( mt="2" candidates={groups} + placeholder="Search for groups..." + disabled={props.maxLength ? selected.length >= props.maxLength : false} renderCandidate={renderCandidate} search={(s: string, a: Association) => a.metadata.title.toLowerCase().startsWith(s.toLowerCase()) @@ -131,8 +125,27 @@ export function GroupSearch(props: InviteSearchProps) { getKey={(a: Association) => a.group} onSelect={onSelect} /> + {value?.length > 0 && ( + value.map((e) => { + return ( + + {groupTitle || e} + + + ); + }) )} - + {meta.error} diff --git a/pkg/interface/src/views/components/ShipSearch.tsx b/pkg/interface/src/views/components/ShipSearch.tsx index f26ed4253..6c9db1760 100644 --- a/pkg/interface/src/views/components/ShipSearch.tsx +++ b/pkg/interface/src/views/components/ShipSearch.tsx @@ -173,7 +173,7 @@ export function ShipSearch(props: InviteSearchProps) { const result = ob.isValidPatp(ship); return result ? deSig(s) ?? undefined : undefined; }} - placeholder="Search for ships" + placeholder="Search for ships..." candidates={peers} renderCandidate={renderCandidate} disabled={props.maxLength ? selected.length >= props.maxLength : false} diff --git a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx index 1a78f0299..e98448f90 100644 --- a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx @@ -42,9 +42,9 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) { Recent Groups {props.recent.filter((e) => { - return (e in associations?.contacts); + return (e in associations?.groups); }).slice(1, 5).map((g) => { - const assoc = associations.contacts[g]; + const assoc = associations.groups[g]; const color = uxToHex(assoc?.metadata?.color || '0x0'); return ( @@ -78,7 +78,7 @@ export function GroupSwitcher(props: { }) { const { associations, workspace, isAdmin } = props; const title = getTitleFromWorkspace(associations, workspace); - const metadata = workspace.type === 'home' ? undefined : associations.contacts[workspace.group].metadata; + const metadata = workspace.type === 'home' ? undefined : associations.groups[workspace.group].metadata; const navTo = (to: string) => `${props.baseUrl}${to}`; return ( diff --git a/pkg/interface/src/views/landscape/components/GroupifyForm.tsx b/pkg/interface/src/views/landscape/components/GroupifyForm.tsx index e8ba57af2..6e8424a2b 100644 --- a/pkg/interface/src/views/landscape/components/GroupifyForm.tsx +++ b/pkg/interface/src/views/landscape/components/GroupifyForm.tsx @@ -14,7 +14,7 @@ const formSchema = Yup.object({ }); interface FormSchema { - group: string | null; + group: string[] | null; } interface GroupifyFormProps { @@ -37,7 +37,7 @@ export function GroupifyForm(props: GroupifyFormProps) { await props.api.graph.groupifyGraph( ship, name, - values.group || undefined + values.group?.toString() || undefined ); const mod = association.metadata.module || association['app-name']; const newGroup = values.group || association.group; @@ -79,6 +79,7 @@ export function GroupifyForm(props: GroupifyFormProps) { groups={props.groups} associations={props.associations} adminOnly + maxLength={1} /> Groupify diff --git a/pkg/interface/src/views/landscape/components/GroupsPane.tsx b/pkg/interface/src/views/landscape/components/GroupsPane.tsx index be8344c17..b1e5b08eb 100644 --- a/pkg/interface/src/views/landscape/components/GroupsPane.tsx +++ b/pkg/interface/src/views/landscape/components/GroupsPane.tsx @@ -43,7 +43,7 @@ export function GroupsPane(props: GroupsPaneProps) { const groupContacts = (groupPath && contacts[groupPath]) || undefined; const rootIdentity = contacts?.["/~/default"]?.[window.ship]; const groupAssociation = - (groupPath && associations.contacts[groupPath]) || undefined; + (groupPath && associations.groups[groupPath]) || undefined; const group = (groupPath && groups[groupPath]) || undefined; const [recentGroups, setRecentGroups] = useLocalStorageState( "recent-groups", @@ -196,7 +196,7 @@ export function GroupsPane(props: GroupsPaneProps) { let summary: ReactNode; if(groupAssociation?.group) { const memberCount = props.groups[groupAssociation.group].members.size; - summary = { - return path in contacts && path in groups && path in associations.contacts; + return path in contacts && path in groups && path in associations.groups; }); actions.setStatus({ success: null }); diff --git a/pkg/interface/src/views/landscape/components/Resource.tsx b/pkg/interface/src/views/landscape/components/Resource.tsx index ddfd316c5..8c3cd7d7c 100644 --- a/pkg/interface/src/views/landscape/components/Resource.tsx +++ b/pkg/interface/src/views/landscape/components/Resource.tsx @@ -37,8 +37,8 @@ export function Resource(props: ResourceProps) { const skelProps = { api, association }; let title = props.association.metadata.title; if ('workspace' in props) { - if ('group' in props.workspace && props.workspace.group in props.associations.contacts) { - title = `${props.associations.contacts[props.workspace.group].metadata.title} - ${props.association.metadata.title}`; + if ('group' in props.workspace && props.workspace.group in props.associations.groups) { + title = `${props.associations.groups[props.workspace.group].metadata.title} - ${props.association.metadata.title}`; } } return ( diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx index ee5893996..a3d155fd8 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx @@ -57,7 +57,7 @@ export function SidebarList(props: { const assoc = associations[a]; return group ? assoc.group === group - : !(assoc.group in props.associations.contacts); + : !(assoc.group in props.associations.groups); }) .sort(sidebarSort(associations, props.apps)[config.sortBy]);