Merge pull request #4345 from urbit/mp/profile/stubs

profile: more stubs
This commit is contained in:
L 2021-01-29 17:06:37 -06:00 committed by GitHub
commit 6927327c7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 109 additions and 86 deletions

View File

@ -1,6 +1,6 @@
/- spider, /- spider,
graph=graph-store, graph=graph-store,
*metadata-store, met=metadata-store,
*group, *group,
group-store, group-store,
inv=invite-store inv=invite-store
@ -57,18 +57,18 @@
:: ::
:: Setup metadata :: Setup metadata
:: ::
=/ =metadata =/ =metadatum:met
%* . *metadata %* . *metadatum:met
title title.action title title.action
description description.action description description.action
date-created now.bowl date-created now.bowl
creator our.bowl creator our.bowl
module module.action module module.action
== ==
=/ =metadata-action =/ met-action=action:met
[%add group graph+rid.action metadata] [%add group graph+rid.action metadatum]
;< ~ bind:m ;< ~ bind:m
(poke-our %metadata-store %metadata-action !>(metadata-action)) (poke-our %metadata-store %metadata-action !>(met-action))
;< ~ bind:m ;< ~ bind:m
(poke-our %metadata-push-hook %push-hook-action !>([%add group])) (poke-our %metadata-push-hook %push-hook-action !>([%add group]))
:: ::

View File

@ -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 /+ strandio, resource
=> =>
|% |%
@ -9,16 +9,14 @@
++ scry-metadata ++ scry-metadata
|= rid=resource |= rid=resource
=/ m (strand ,(unit resource)) =/ m (strand ,(unit resource))
;< paxs=(unit (set path)) bind:m ;< group=(unit resource) bind:m
%+ scry:strandio ,(unit (set path)) %+ scry:strandio ,(unit resource)
;: weld ;: weld
/gx/metadata-store/resource/graph /gx/metadata-store/resource/graph
(en-path:resource rid) (en-path:resource rid)
/noun /noun
== ==
?~ paxs (pure:m ~) (pure:m group)
?~ u.paxs (pure:m ~)
(pure:m `(de-path:resource n.u.paxs))
:: ::
++ scry-group ++ scry-group
|= rid=resource |= rid=resource
@ -42,11 +40,7 @@
;< ~ bind:m ;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%remove rid])) (poke-our %graph-push-hook %push-hook-action !>([%remove rid]))
;< ~ bind:m ;< ~ bind:m
%+ poke-our %metadata-hook (poke-our %metadata-push-hook %push-hook-action !>([%remove rid]))
:- %metadata-action
!> :+ %remove
(en-path:resource group-rid)
[%graph (en-path:resource rid)]
(pure:m ~) (pure:m ~)
-- --
:: ::
@ -74,6 +68,5 @@
(poke-our %group-push-hook %push-hook-action !>([%remove rid.action])) (poke-our %group-push-hook %push-hook-action !>([%remove rid.action]))
;< ~ bind:m (delete-graph u.ugroup-rid rid.action) ;< ~ bind:m (delete-graph u.ugroup-rid rid.action)
;< ~ bind:m ;< ~ bind:m
%+ poke-our %metadata-hook (poke-our %metadata-push-hook %push-hook-action !>([%remove rid.action]))
metadata-hook-action+!>([%remove (en-path:resource u.ugroup-rid)])
(pure:m !>(~)) (pure:m !>(~))

View File

@ -22,6 +22,8 @@ export default class ContactsApi extends BaseApi<StoreState> {
{color: 'fff'} // with no 0x prefix {color: 'fff'} // with no 0x prefix
{avatar: null} {avatar: null}
{avatar: ''} {avatar: ''}
{add-group: {ship, name}}
{remove-group: {ship, name}}
*/ */
console.log(ship, editField); console.log(ship, editField);
return this.storeAction({ return this.storeAction({

View File

@ -116,7 +116,7 @@ export default function index(contacts, associations, apps, currentGroup, groups
title, title,
`/~landscape${group}/join/${app}${each.resource}`, `/~landscape${group}/join/${app}${each.resource}`,
app.charAt(0).toUpperCase() + app.slice(1), app.charAt(0).toUpperCase() + app.slice(1),
(associations?.contacts?.[each.group]?.metadata?.title || null) (associations?.groups?.[each.group]?.metadata?.title || null)
); );
subscriptions.push(obj); subscriptions.push(obj);
} }

View File

@ -8,7 +8,7 @@ export function getTitleFromWorkspace(
case "home": case "home":
return "DMs + Drafts"; return "DMs + Drafts";
case "group": case "group":
const association = associations.contacts[workspace.group]; const association = associations.groups[workspace.group];
return association?.metadata?.title || ""; return association?.metadata?.title || "";
} }
} }

View File

@ -18,7 +18,7 @@ export type Serial = string;
export type Jug<K,V> = Map<K,Set<V>>; export type Jug<K,V> = Map<K,Set<V>>;
// name of app // name of app
export type AppName = 'chat' | 'link' | 'contacts' | 'publish' | 'graph'; export type AppName = 'contacts' | 'groups' | 'graph';
export function getTagFromFrond<O>(frond: O): keyof O { export function getTagFromFrond<O>(frond: O): keyof O {
const tags = Object.keys(frond) as Array<keyof O>; const tags = Object.keys(frond) as Array<keyof O>;

View File

@ -36,7 +36,7 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) { export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const { associations, unreads, inbox, ...boxProps } = props; 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) .filter((e) => e?.group in props.groups)
.sort(sortGroupsAlph); .sort(sortGroupsAlph);
const graphUnreads = getGraphUnreads(associations || {}, unreads); const graphUnreads = getGraphUnreads(associations || {}, unreads);

View File

@ -63,7 +63,7 @@ export function Header(props: {
const time = moment(props.time).format("HH:mm"); const time = moment(props.time).format("HH:mm");
const groupTitle = const groupTitle =
props.associations.contacts?.[props.group]?.metadata?.title; props.associations.groups?.[props.group]?.metadata?.title;
const app = props.chat ? 'chat' : 'graph'; const app = props.chat ? 'chat' : 'graph';
const channelTitle = const channelTitle =

View File

@ -31,7 +31,7 @@ export function Invites(props: InvitesProps) {
const resourcePath = resourceAsPath(invite.resource); const resourcePath = resourceAsPath(invite.resource);
if (app === "contacts") { if (app === "contacts") {
await api.contacts.join(resource); 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); await api.invite.accept(app, uid);
history.push(`/~landscape${resourcePath}`); history.push(`/~landscape${resourcePath}`);
} else if (app === "graph") { } else if (app === "graph") {

View File

@ -44,7 +44,7 @@ export default function NotificationsScreen(props: any) {
filter.groups.length === 0 filter.groups.length === 0
? "All" ? "All"
: filter.groups : filter.groups
.map((g) => props.associations?.contacts?.[g]?.metadata?.title) .map((g) => props.associations?.groups?.[g]?.metadata?.title)
.join(", "); .join(", ");
return ( return (
<Switch> <Switch>

View File

@ -21,6 +21,8 @@ import { AsyncButton } from "~/views/components/AsyncButton";
import { ColorInput } from "~/views/components/ColorInput"; import { ColorInput } from "~/views/components/ColorInput";
import { ImageInput } from "~/views/components/ImageInput"; import { ImageInput } from "~/views/components/ImageInput";
import { MarkdownField } from "~/views/apps/publish/components/MarkdownField"; import { MarkdownField } from "~/views/apps/publish/components/MarkdownField";
import { resourceFromPath } from "~/logic/lib/group";
import GroupSearch from "~/views/components/GroupSearch";
const formSchema = Yup.object({ const formSchema = Yup.object({
@ -62,8 +64,15 @@ export function EditProfile(props: any) {
return acc.then(() => return acc.then(() =>
api.contacts.setPublic(newValue) 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 ( } else if (
key !== "groups" &&
key !== "last-updated" && key !== "last-updated" &&
key !== "isPublic" key !== "isPublic"
) { ) {
@ -105,7 +114,8 @@ export function EditProfile(props: any) {
</Col> </Col>
</Row> </Row>
<Checkbox mb={3} id="isPublic" label="Public Profile" /> <Checkbox mb={3} id="isPublic" label="Public Profile" />
<AsyncButton primary loadingText="Updating..." border> <GroupSearch label="Pinned Groups" id="groups" groups={props.groups} associations={props.associations} />
<AsyncButton primary loadingText="Updating..." border mt={3}>
Submit Submit
</AsyncButton> </AsyncButton>
</Form> </Form>

View File

@ -66,6 +66,8 @@ export function Profile(props: any) {
contact={contact} contact={contact}
s3={props.s3} s3={props.s3}
api={props.api} api={props.api}
groups={props.groups}
associations={props.associations}
isPublic={isPublic}/> isPublic={isPublic}/>
) : ( ) : (
<ViewProfile ship={ship} contact={contact} isPublic={isPublic} /> <ViewProfile ship={ship} contact={contact} isPublic={isPublic} />

View File

@ -45,6 +45,8 @@ export default function ProfileScreen(props: any) {
<Box> <Box>
<Profile <Profile
ship={ship} ship={ship}
associations={props.associations}
groups={props.groups}
contact={contact} contact={contact}
api={props.api} api={props.api}
s3={props.s3} s3={props.s3}

View File

@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from "react"; import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { import {
Box, Box,
Text, Text,
@ -6,17 +6,17 @@ import {
Row, Row,
Col, Col,
Icon, Icon,
ErrorLabel, ErrorLabel
} from "@tlon/indigo-react"; } from '@tlon/indigo-react';
import _ from "lodash"; import _ from 'lodash';
import { useField } from "formik"; import { useField } from 'formik';
import styled from "styled-components"; import styled from 'styled-components';
import { roleForShip } from "~/logic/lib/group"; import { roleForShip } from '~/logic/lib/group';
import { DropdownSearch } from "./DropdownSearch"; import { DropdownSearch } from './DropdownSearch';
import { Groups } from "~/types"; import { Groups } from '~/types';
import { Associations, Association } from "~/types/metadata-update"; import { Associations, Association } from '~/types/metadata-update';
interface InviteSearchProps { interface InviteSearchProps {
disabled?: boolean; disabled?: boolean;
@ -26,11 +26,12 @@ interface InviteSearchProps {
label: string; label: string;
caption?: string; caption?: string;
id: string; id: string;
maxLength?: number;
} }
const CandidateBox = styled(Box)<{ selected: boolean }>` const CandidateBox = styled(Box)<{ selected: boolean }>`
&:hover { &: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) { export function GroupSearch(props: InviteSearchProps) {
const { id, caption, label } = props; const { id, caption, label } = props;
const [selected, setSelected] = useState([] as string[]);
const groups: Association[] = useMemo(() => { const groups: Association[] = useMemo(() => {
return props.adminOnly return props.adminOnly
? Object.values( ? Object.values(
Object.keys(props.associations?.contacts) Object.keys(props.associations?.groups)
.filter( .filter(
(e) => roleForShip(props.groups[e], window.ship) === "admin" e => roleForShip(props.groups[e], window.ship) === 'admin'
) )
.reduce((obj, key) => { .reduce((obj, key) => {
obj[key] = props.associations?.contacts[key]; obj[key] = props.associations?.groups[key];
return obj; return obj;
}, {}) || {} }, {}) || {}
) )
: Object.values(props.associations?.contacts || {}); : Object.values(props.associations?.groups || {});
}, [props.associations?.contacts]); }, [props.associations?.groups]);
const [{ value }, meta, { setValue, setTouched }] = useField(props.id); const [{ value }, meta, { setValue, setTouched }] = useField(props.id);
useEffect(() => {
setValue(selected);
}, [selected])
const { title: groupTitle } = const { title: groupTitle } =
props.associations.contacts?.[value]?.metadata || {}; props.associations.groups?.[value]?.metadata || {};
const onSelect = useCallback( const onSelect = useCallback(
(a: Association) => { (s: string) => {
setValue(a.group);
setTouched(true); setTouched(true);
setSelected(v => _.uniq([...v, s]));
}, },
[setValue] [setTouched, setSelected]
); );
const onUnselect = useCallback(() => { const onRemove = useCallback(
setValue(undefined); (s: string) => {
setTouched(true); setSelected(groups => groups.filter(group => group !== s))
}, [setValue]); },
[setSelected]
);
return ( return (
<Col> <Col>
@ -105,25 +113,11 @@ export function GroupSearch(props: InviteSearchProps) {
{caption} {caption}
</Label> </Label>
)} )}
{value && (
<Row
borderRadius="1"
mt="2"
width="fit-content"
border="1"
borderColor="gray"
height="32px"
px="2"
alignItems="center"
>
<Text mr="2">{groupTitle || value}</Text>
<Icon onClick={onUnselect} icon="X" />
</Row>
)}
{!value && (
<DropdownSearch<Association> <DropdownSearch<Association>
mt="2" mt="2"
candidates={groups} candidates={groups}
placeholder="Search for groups..."
disabled={props.maxLength ? selected.length >= props.maxLength : false}
renderCandidate={renderCandidate} renderCandidate={renderCandidate}
search={(s: string, a: Association) => search={(s: string, a: Association) =>
a.metadata.title.toLowerCase().startsWith(s.toLowerCase()) a.metadata.title.toLowerCase().startsWith(s.toLowerCase())
@ -131,8 +125,27 @@ export function GroupSearch(props: InviteSearchProps) {
getKey={(a: Association) => a.group} getKey={(a: Association) => a.group}
onSelect={onSelect} onSelect={onSelect}
/> />
{value?.length > 0 && (
value.map((e) => {
return (
<Row
key={e}
borderRadius="1"
mt="2"
width="fit-content"
border="1"
borderColor="gray"
height="32px"
px="2"
alignItems="center"
>
<Text mr="2">{groupTitle || e}</Text>
<Icon onClick={onRemove} icon="X" />
</Row>
);
})
)} )}
<ErrorLabel hasError={!!(meta.touched && meta.error)}> <ErrorLabel hasError={Boolean(meta.touched && meta.error)}>
{meta.error} {meta.error}
</ErrorLabel> </ErrorLabel>
</Col> </Col>

View File

@ -173,7 +173,7 @@ export function ShipSearch(props: InviteSearchProps) {
const result = ob.isValidPatp(ship); const result = ob.isValidPatp(ship);
return result ? deSig(s) ?? undefined : undefined; return result ? deSig(s) ?? undefined : undefined;
}} }}
placeholder="Search for ships" placeholder="Search for ships..."
candidates={peers} candidates={peers}
renderCandidate={renderCandidate} renderCandidate={renderCandidate}
disabled={props.maxLength ? selected.length >= props.maxLength : false} disabled={props.maxLength ? selected.length >= props.maxLength : false}

View File

@ -42,9 +42,9 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) {
Recent Groups Recent Groups
</Box> </Box>
{props.recent.filter((e) => { {props.recent.filter((e) => {
return (e in associations?.contacts); return (e in associations?.groups);
}).slice(1, 5).map((g) => { }).slice(1, 5).map((g) => {
const assoc = associations.contacts[g]; const assoc = associations.groups[g];
const color = uxToHex(assoc?.metadata?.color || '0x0'); const color = uxToHex(assoc?.metadata?.color || '0x0');
return ( return (
<Link key={g} style={{ minWidth: 0 }} to={`/~landscape${g}`}> <Link key={g} style={{ minWidth: 0 }} to={`/~landscape${g}`}>
@ -78,7 +78,7 @@ export function GroupSwitcher(props: {
}) { }) {
const { associations, workspace, isAdmin } = props; const { associations, workspace, isAdmin } = props;
const title = getTitleFromWorkspace(associations, workspace); 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}`; const navTo = (to: string) => `${props.baseUrl}${to}`;
return ( return (
<Row width="100%" alignItems="center" height='48px' backgroundColor="white" zIndex="2" position="sticky" top="0px" pl='3' borderBottom='1px solid' borderColor='washedGray'> <Row width="100%" alignItems="center" height='48px' backgroundColor="white" zIndex="2" position="sticky" top="0px" pl='3' borderBottom='1px solid' borderColor='washedGray'>

View File

@ -14,7 +14,7 @@ const formSchema = Yup.object({
}); });
interface FormSchema { interface FormSchema {
group: string | null; group: string[] | null;
} }
interface GroupifyFormProps { interface GroupifyFormProps {
@ -37,7 +37,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
await props.api.graph.groupifyGraph( await props.api.graph.groupifyGraph(
ship, ship,
name, name,
values.group || undefined values.group?.toString() || undefined
); );
const mod = association.metadata.module || association['app-name']; const mod = association.metadata.module || association['app-name'];
const newGroup = values.group || association.group; const newGroup = values.group || association.group;
@ -79,6 +79,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
groups={props.groups} groups={props.groups}
associations={props.associations} associations={props.associations}
adminOnly adminOnly
maxLength={1}
/> />
<AsyncButton primary loadingText="Groupifying..." border> <AsyncButton primary loadingText="Groupifying..." border>
Groupify Groupify

View File

@ -43,7 +43,7 @@ export function GroupsPane(props: GroupsPaneProps) {
const groupContacts = (groupPath && contacts[groupPath]) || undefined; const groupContacts = (groupPath && contacts[groupPath]) || undefined;
const rootIdentity = contacts?.["/~/default"]?.[window.ship]; const rootIdentity = contacts?.["/~/default"]?.[window.ship];
const groupAssociation = const groupAssociation =
(groupPath && associations.contacts[groupPath]) || undefined; (groupPath && associations.groups[groupPath]) || undefined;
const group = (groupPath && groups[groupPath]) || undefined; const group = (groupPath && groups[groupPath]) || undefined;
const [recentGroups, setRecentGroups] = useLocalStorageState<string[]>( const [recentGroups, setRecentGroups] = useLocalStorageState<string[]>(
"recent-groups", "recent-groups",

View File

@ -65,7 +65,7 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps) {
await api.contacts.create(name, policy, title, description); await api.contacts.create(name, policy, title, description);
const path = `/ship/~${window.ship}/${name}`; const path = `/ship/~${window.ship}/${name}`;
await waiter(({ contacts, groups, associations }) => { await waiter(({ contacts, groups, associations }) => {
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 }); actions.setStatus({ success: null });

View File

@ -37,8 +37,8 @@ export function Resource(props: ResourceProps) {
const skelProps = { api, association }; const skelProps = { api, association };
let title = props.association.metadata.title; let title = props.association.metadata.title;
if ('workspace' in props) { if ('workspace' in props) {
if ('group' in props.workspace && props.workspace.group in props.associations.contacts) { if ('group' in props.workspace && props.workspace.group in props.associations.groups) {
title = `${props.associations.contacts[props.workspace.group].metadata.title} - ${props.association.metadata.title}`; title = `${props.associations.groups[props.workspace.group].metadata.title} - ${props.association.metadata.title}`;
} }
} }
return ( return (

View File

@ -57,7 +57,7 @@ export function SidebarList(props: {
const assoc = associations[a]; const assoc = associations[a];
return group return group
? assoc.group === group ? assoc.group === group
: !(assoc.group in props.associations.contacts); : !(assoc.group in props.associations.groups);
}) })
.sort(sidebarSort(associations, props.apps)[config.sortBy]); .sort(sidebarSort(associations, props.apps)[config.sortBy]);