interface: convert metadata store to zustand

This commit is contained in:
Tyler Brown Cifu Shuster 2021-02-26 09:07:15 -08:00
parent d17794f93d
commit 041be1d8fe
46 changed files with 286 additions and 239 deletions

View File

@ -1,103 +1,111 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { compose } from 'lodash/fp';
import { MetadataUpdate } from '@urbit/api/metadata';
import { Cage } from '~/types/cage';
import useMetadataState, { MetadataState } from '../state/metadata';
type MetadataState = Pick<StoreState, 'associations'>;
export default class MetadataReducer<S extends MetadataState> {
reduce(json: Cage, state: S) {
export default class MetadataReducer {
reduce(json: Cage) {
const data = json['metadata-update'];
if (data) {
console.log(data);
this.associations(data, state);
this.add(data, state);
this.update(data, state);
this.remove(data, state);
this.groupInitial(data, state);
useMetadataState.setState(
compose([
associations,
add,
update,
remove,
groupInitial,
].map(reducer => reducer.bind(reducer, data))
)(useMetadataState.getState())
);
}
}
}
groupInitial(json: MetadataUpdate, state: S) {
const data = _.get(json, 'initial-group', false);
console.log(data);
if(data) {
this.associations(data, state);
}
const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'initial-group', false);
console.log(data);
if(data) {
state = associations(data, state);
}
return state;
}
associations(json: MetadataUpdate, state: S) {
const data = _.get(json, 'associations', false);
if (data) {
const metadata = state.associations;
Object.keys(data).forEach((key) => {
const val = data[key];
const appName = val['app-name'];
const rid = val.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
}
if (!(rid in metadata[appName])) {
metadata[appName][rid] = {};
}
metadata[appName][rid] = val;
});
state.associations = metadata;
}
}
add(json: MetadataUpdate, state: S) {
const data = _.get(json, 'add', false);
if (data) {
const metadata = state.associations;
const appName = data['app-name'];
const appPath = data.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
}
if (!(appPath in metadata[appName])) {
metadata[appName][appPath] = {};
}
metadata[appName][appPath] = data;
state.associations = metadata;
}
}
update(json: MetadataUpdate, state: S) {
const data = _.get(json, 'update-metadata', false);
if (data) {
const metadata = state.associations;
const appName = data['app-name'];
const rid = data.resource;
const associations = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'associations', false);
if (data) {
const metadata = state.associations;
Object.keys(data).forEach((key) => {
const val = data[key];
const appName = val['app-name'];
const rid = val.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
}
if (!(rid in metadata[appName])) {
metadata[appName][rid] = {};
}
metadata[appName][rid] = data;
metadata[appName][rid] = val;
});
state.associations = metadata;
}
}
remove(json: MetadataUpdate, state: S) {
const data = _.get(json, 'remove', false);
if (data) {
const metadata = state.associations;
const appName = data['app-name'];
const rid = data.resource;
if (appName in metadata && rid in metadata[appName]) {
delete metadata[appName][rid];
}
state.associations = metadata;
}
state.associations = metadata;
}
return state;
}
const add = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'add', false);
if (data) {
const metadata = state.associations;
const appName = data['app-name'];
const appPath = data.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
}
if (!(appPath in metadata[appName])) {
metadata[appName][appPath] = {};
}
metadata[appName][appPath] = data;
state.associations = metadata;
}
return state;
}
const update = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'update-metadata', false);
if (data) {
const metadata = state.associations;
const appName = data['app-name'];
const rid = data.resource;
if (!(appName in metadata)) {
metadata[appName] = {};
}
if (!(rid in metadata[appName])) {
metadata[appName][rid] = {};
}
metadata[appName][rid] = data;
state.associations = metadata;
}
return state;
}
const remove = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'remove', false);
if (data) {
const metadata = state.associations;
const appName = data['app-name'];
const rid = data.resource;
if (appName in metadata && rid in metadata[appName]) {
delete metadata[appName][rid];
}
state.associations = metadata;
}
return state;
}

View File

@ -34,6 +34,7 @@ const useContactState = create<ContactState>(persist((set, get) => ({
// },
set: fn => stateSetter(fn, set)
}), {
blacklist: ['nackedContacts'],
name: 'LandscapeContactState'
}));

View File

@ -0,0 +1,76 @@
import React from "react";
import create, { State } from 'zustand';
import { persist } from 'zustand/middleware';
import { MetadataUpdatePreview, Associations } from "@urbit/api";
// import useApi from "~/logic/lib/useApi";
import { stateSetter } from "~/logic/lib/util";
export const METADATA_MAX_PREVIEW_WAIT = 150000;
export interface MetadataState extends State {
associations: Associations;
// preview: (group: string) => Promise<MetadataUpdatePreview>;
set: (fn: (state: MetadataState) => void) => void;
};
const useMetadataState = create<MetadataState>(persist((set, get) => ({
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
// preview: async (group): Promise<MetadataUpdatePreview> => {
// return new Promise<MetadataUpdatePreview>((resolve, reject) => {
// const api = useApi();
// let done = false;
// setTimeout(() => {
// if (done) {
// return;
// }
// done = true;
// reject(new Error('offline'));
// }, METADATA_MAX_PREVIEW_WAIT);
// api.subscribe({
// app: 'metadata-pull-hook',
// path: `/preview${group}`,
// // TODO type this message?
// event: (message) => {
// if ('metadata-hook-update' in message) {
// done = true;
// const update = message['metadata-hook-update'].preview as MetadataUpdatePreview;
// resolve(update);
// } else {
// done = true;
// reject(new Error('no-permissions'));
// }
// // TODO how to delete this subscription? Perhaps return the susbcription ID as the second parameter of all the handlers
// },
// err: (error) => {
// console.error(error);
// reject(error);
// },
// quit: () => {
// if (!done) {
// reject(new Error('offline'));
// }
// }
// });
// });
// },
set: fn => stateSetter(fn, set),
}), {
name: 'LandscapeMetadataState'
}));
function withMetadataState<P, S extends keyof MetadataState>(Component: any, stateMemberKeys?: S[]) {
return React.forwardRef((props: Omit<P, S>, ref) => {
const metadataState = stateMemberKeys ? useMetadataState(
state => stateMemberKeys.reduce(
(object, key) => ({ ...object, [key]: state[key] }), {}
)
): useMetadataState();
return <Component ref={ref} {...metadataState} {...props} />
});
}
export { useMetadataState as default, withMetadataState };

View File

@ -102,7 +102,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)];
this.inviteReducer.reduce(data);
this.metadataReducer.reduce(data, this.state);
this.metadataReducer.reduce(data);
this.localReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data);

View File

@ -154,7 +154,6 @@ class App extends React.Component {
<ErrorBoundary>
<StatusBarWithRouter
props={this.props}
associations={associations}
ourContact={ourContact}
api={this.api}
connection={this.state.connection}

View File

@ -165,7 +165,6 @@ export function ChatResource(props: ChatResourceProps) {
contacts : modifiedContacts
}
association={props.association}
associations={props.associations}
group={group}
ship={owner}
station={station}

View File

@ -131,8 +131,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
api,
highlighted,
fontSize,
groups,
associations
} = this.props;
let { renderSigil } = this.props;
@ -174,8 +172,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
scrollWindow,
highlighted,
fontSize,
associations,
groups
};
const unreadContainerStyle = {
@ -221,8 +217,6 @@ export const MessageAuthor = ({
measure,
group,
api,
associations,
groups,
history,
scrollWindow,
...rest
@ -366,8 +360,6 @@ export const Message = ({
measure,
group,
api,
associations,
groups,
scrollWindow,
timestampHover,
...rest
@ -396,7 +388,6 @@ export const Message = ({
case 'text':
return (
<TextContent
associations={associations}
measure={measure}
api={api}
fontSize={1}

View File

@ -38,7 +38,6 @@ type ChatWindowProps = RouteComponentProps<{
station: any;
api: GlobalApi;
scrollTo?: number;
associations: Associations;
};
interface ChatWindowState {

View File

@ -134,7 +134,6 @@ export default function TextContent(props) {
measure={props.measure}
resource={resource}
api={props.api}
associations={props.associations}
pl='2'
border='1'
borderRadius='2'

View File

@ -3,55 +3,52 @@ import { Switch, Route } from 'react-router-dom';
import { Center, Text } from "@tlon/indigo-react";
import { deSig } from '~/logic/lib/util';
import useGraphState from '~/logic/state/graph';
import useMetadataState from '~/logic/state/metadata';
const GraphApp = (props) => {
const associations= useMetadataState(state => state.associations);
const graphKeys = useGraphState(state => state.graphKeys);
const { api } = this.props;
return (
<Switch>
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
render={ (props) => {
const resource =
`${deSig(props.match.params.ship)}/${props.match.params.name}`;
const { ship, name } = props.match.params;
const path = `/ship/~${deSig(ship)}/${name}`;
const association = associations.graph[path];
export default class GraphApp extends PureComponent {
render() {
const { props } = this;
const associations =
props.associations ? props.associations : { graph: {}, contacts: {} };
const graphKeys = useGraphState(state => state.graphKeys);
const { api } = this.props;
return (
<Switch>
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
render={ (props) => {
const resource =
`${deSig(props.match.params.ship)}/${props.match.params.name}`;
const { ship, name } = props.match.params;
const path = `/ship/~${deSig(ship)}/${name}`;
const association = associations.graph[path];
const autoJoin = () => {
try {
api.graph.joinGraph(
`~${deSig(props.match.params.ship)}`,
props.match.params.name
);
} catch(err) {
setTimeout(autoJoin, 2000);
}
};
if(!graphKeys.has(resource)) {
autoJoin();
} else if(!!association) {
props.history.push(`/~landscape/home/resource/${association.metadata.module}${path}`);
const autoJoin = () => {
try {
api.graph.joinGraph(
`~${deSig(props.match.params.ship)}`,
props.match.params.name
);
} catch(err) {
setTimeout(autoJoin, 2000);
}
return (
<Center width="100%" height="100%">
<Text fontSize={1}>Redirecting...</Text>
</Center>
);
}}
/>
</Switch>
);
}
};
if(!graphKeys.has(resource)) {
autoJoin();
} else if(!!association) {
props.history.push(`/~landscape/home/resource/${association.metadata.module}${path}`);
}
return (
<Center width="100%" height="100%">
<Text fontSize={1}>Redirecting...</Text>
</Center>
);
}}
/>
</Switch>
);
}
export default GraphApp;

View File

@ -197,7 +197,7 @@ export default function LaunchApp(props) {
<JoinGroup {...props} />
</ModalButton>
<Groups associations={props.associations} />
<Groups />
</Box>
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
</ScrollbarLessBox>

View File

@ -11,10 +11,9 @@ import { useTutorialModal } from '~/views/components/useTutorialModal';
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
import useGroupState from '~/logic/state/groups';
import useHarkState from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
interface GroupsProps {
associations: Associations;
}
interface GroupsProps {}
const sortGroupsAlph = (a: Association, b: Association) =>
alphabeticalOrder(a.metadata.title, b.metadata.title);
@ -36,9 +35,10 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) =>
)(associations.graph);
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const { associations, inbox, ...boxProps } = props;
const { inbox, ...boxProps } = props;
const unreads = useHarkState(state => state.unreads);
const groupState = useGroupState(state => state.groups);
const associations = useMetadataState(state => state.associations);
const groups = Object.values(associations?.groups || {})
.filter(e => e?.group in groupState)

View File

@ -14,6 +14,7 @@ import { Comments } from '~/views/components/Comments';
import './css/custom.css';
import { Association } from '@urbit/api/metadata';
import useGraphState from '~/logic/state/graph';
import useMetadataState from '~/logic/state/metadata';
const emptyMeasure = () => {};
@ -29,13 +30,13 @@ export function LinkResource(props: LinkResourceProps) {
api,
baseUrl,
groups,
associations,
s3,
} = props;
const rid = association.resource;
const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`;
const associations = useMetadataState(state => state.associations);
const [, , ship, name] = rid.split('/');
const resourcePath = `${ship.slice(1)}/${name}`;

View File

@ -233,7 +233,6 @@ export function GraphNotification(props: {
read: boolean;
time: number;
timebox: BigInteger;
associations: Associations;
api: GlobalApi;
}) {
const { contents, index, read, time, api, timebox } = props;
@ -265,7 +264,6 @@ export function GraphNotification(props: {
channel={graph}
group={group}
description={desc}
associations={props.associations}
/>
<Box flexGrow={1} width='100%' pl={5} gridArea='main'>
{_.map(contents, (content, idx) => (

View File

@ -41,12 +41,11 @@ interface GroupNotificationProps {
read: boolean;
time: number;
timebox: BigInteger;
associations: Associations;
api: GlobalApi;
}
export function GroupNotification(props: GroupNotificationProps): ReactElement {
const { contents, index, read, time, api, timebox, associations } = props;
const { contents, index, read, time, api, timebox } = props;
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
@ -70,7 +69,6 @@ export function GroupNotification(props: GroupNotificationProps): ReactElement {
group={group}
authors={authors}
description={desc}
associations={associations}
/>
</Col>
);

View File

@ -10,6 +10,7 @@ import { PropFunc } from '~/types/util';
import { useShowNickname } from '~/logic/lib/util';
import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contacts';
import useMetadataState from '~/logic/state/metadata';
const Text = (props: PropFunc<typeof Text>) => (
<NormalText fontWeight="500" {...props} />
@ -39,9 +40,9 @@ export function Header(props: {
moduleIcon?: string;
time: number;
read: boolean;
associations: Associations;
} & PropFunc<typeof Row> ): ReactElement {
const { description, channel, moduleIcon, read } = props;
const associations = useMetadataState(state => state.associations);
const authors = _.uniq(props.authors);
@ -65,11 +66,11 @@ export function Header(props: {
const time = moment(props.time).format('HH:mm');
const groupTitle =
props.associations.groups?.[props.group]?.metadata?.title;
associations.groups?.[props.group]?.metadata?.title;
const app = 'graph';
const channelTitle =
(channel && props.associations?.[app]?.[channel]?.metadata?.title) ||
(channel && associations?.[app]?.[channel]?.metadata?.title) ||
channel;
return (

View File

@ -48,11 +48,10 @@ export default function Inbox(props: {
archive: Notifications;
showArchive?: boolean;
api: GlobalApi;
associations: Associations;
filter: string[];
pendingJoin: JoinRequests;
}) {
const { api, associations } = props;
const { api } = props;
useEffect(() => {
let seen = false;
setTimeout(() => {
@ -117,7 +116,7 @@ export default function Inbox(props: {
return (
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
<Invites pendingJoin={props.pendingJoin} api={api} associations={associations} />
<Invites pendingJoin={props.pendingJoin} api={api} />
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
const timeboxes = notificationsByDayMap.get(day)!;
return timeboxes.length > 0 && (
@ -126,7 +125,6 @@ export default function Inbox(props: {
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
timeboxes={timeboxes}
archive={Boolean(props.showArchive)}
associations={props.associations}
api={api}
/>
);
@ -161,7 +159,6 @@ function DaySection({
label,
archive,
timeboxes,
associations,
api,
}) {
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
@ -186,7 +183,6 @@ function DaySection({
)}
<Notification
api={api}
associations={associations}
notification={not}
archived={archive}
time={date}

View File

@ -11,7 +11,6 @@ import useInviteState from '~/logic/state/invite';
interface InvitesProps {
api: GlobalApi;
associations: Associations;
pendingJoin: JoinRequests;
}
@ -53,7 +52,6 @@ export function Invites(props: InvitesProps): ReactElement {
return (
<InviteItem
key={resource}
associations={props.associations}
resource={resource}
pendingJoin={pendingJoin}
api={api}
@ -71,7 +69,6 @@ export function Invites(props: InvitesProps): ReactElement {
uid={uid}
pendingJoin={pendingJoin}
resource={resource}
associations={props.associations}
/>
);
}

View File

@ -23,7 +23,6 @@ import useHarkState from '~/logic/state/hark';
interface NotificationProps {
notification: IndexedNotification;
time: BigInteger;
associations: Associations;
api: GlobalApi;
archived: boolean;
}
@ -136,7 +135,6 @@ export function Notification(props: NotificationProps) {
archived={archived}
timebox={props.time}
time={time}
associations={associations}
/>
</Wrapper>
);
@ -154,7 +152,6 @@ export function Notification(props: NotificationProps) {
timebox={props.time}
archived={archived}
time={time}
associations={associations}
/>
</Wrapper>
);

View File

@ -14,6 +14,7 @@ import { FormikOnBlur } from '~/views/components/FormikOnBlur';
import GroupSearch from '~/views/components/GroupSearch';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import useHarkState from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
const baseUrl = '/~notifications';
@ -40,6 +41,7 @@ export default function NotificationsScreen(props: any): ReactElement {
const relativePath = (p: string) => baseUrl + p;
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
const associations = useMetadataState(state => state.associations);
const onSubmit = async ({ groups } : NotificationFilter) => {
setFilter({ groups });
};
@ -50,7 +52,7 @@ export default function NotificationsScreen(props: any): ReactElement {
filter.groups.length === 0
? 'All'
: filter.groups
.map(g => props.associations?.groups?.[g]?.metadata?.title)
.map(g => associations.groups?.[g]?.metadata?.title)
.join(', ');
const anchorRef = useRef<HTMLElement | null>(null);
useTutorialModal('notifications', true, anchorRef.current);
@ -124,7 +126,6 @@ export default function NotificationsScreen(props: any): ReactElement {
id="groups"
label="Filter Groups"
caption="Only show notifications from this group"
associations={props.associations}
/>
</FormikOnBlur>
</Col>

View File

@ -121,7 +121,7 @@ export function EditProfile(props: any): ReactElement {
</Col>
</Row>
<Checkbox mb={3} id="isPublic" label="Public Profile" />
<GroupSearch label="Pinned Groups" id="groups" associations={props.associations} publicOnly />
<GroupSearch label="Pinned Groups" id="groups" publicOnly />
<AsyncButton primary loadingText="Updating..." border mt={3}>
Submit
</AsyncButton>

View File

@ -110,7 +110,6 @@ export function Profile(props: any): ReactElement {
contact={contact}
s3={props.s3}
api={props.api}
associations={props.associations}
/>
) : (
<ViewProfile
@ -118,7 +117,6 @@ export function Profile(props: any): ReactElement {
nacked={nacked}
ship={ship}
contact={contact}
associations={props.associations}
/>
) }
</Box>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { ReactElement } from 'react';
import _ from 'lodash';
import { useHistory } from 'react-router-dom';
@ -20,7 +20,7 @@ export function ViewProfile(props: any): ReactElement {
const { hideNicknames } = useLocalState(({ hideNicknames }) => ({
hideNicknames
}));
const { api, contact, nacked, ship, associations, groups } = props;
const { api, contact, nacked, ship } = props;
const isPublic = useContactState(state => state.isContactPublic);
@ -66,7 +66,6 @@ export function ViewProfile(props: any): ReactElement {
<GroupLink
api={api}
resource={g}
associations={associations}
measure={() => {}}
/>
))}

View File

@ -39,7 +39,6 @@ export default function ProfileScreen(props: any) {
<Profile
ship={ship}
hasLoaded={Object.keys(contacts).length !== 0}
associations={props.associations}
contact={contact}
api={props.api}
s3={props.s3}

View File

@ -24,7 +24,6 @@ export function PublishResource(props: PublishResourceProps) {
api={api}
ship={ship}
book={book}
associations={props.associations}
association={association}
rootUrl={baseUrl}
baseUrl={`${baseUrl}/resource/publish/ship/${ship}/${book}`}

View File

@ -16,13 +16,12 @@ interface NotebookProps {
book: string;
graph: Graph;
association: Association;
associations: Associations;
baseUrl: string;
rootUrl: string;
unreads: Unreads;
}
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement {
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null {
const {
ship,
book,

View File

@ -27,7 +27,6 @@ interface NotebookRoutesProps {
baseUrl: string;
rootUrl: string;
association: Association;
associations: Associations;
s3: S3State;
}

View File

@ -9,21 +9,22 @@ import { JoinGroup } from '../landscape/components/JoinGroup';
import { useModal } from '~/logic/lib/useModal';
import { GroupSummary } from '../landscape/components/GroupSummary';
import { PropFunc } from '~/types';
import useMetadataState from '~/logic/state/metadata';
export function GroupLink(
props: {
api: GlobalApi;
resource: string;
associations: Associations;
measure: () => void;
detailed?: boolean;
} & PropFunc<typeof Row>
): ReactElement {
const { resource, api, associations, measure, ...rest } = props;
const { resource, api, measure, ...rest } = props;
const name = resource.slice(6);
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
const associations = useMetadataState(state => state.associations);
const joined = resource in props.associations.groups;
const joined = resource in associations.groups;
const { modal, showModal } = useModal({
modal:
@ -37,7 +38,6 @@ export function GroupLink(
</Box>
) : (
<JoinGroup
associations={associations}
api={api}
autojoin={name}
/>

View File

@ -19,12 +19,12 @@ import { Associations, Association } from '@urbit/api/metadata';
import { roleForShip } from '~/logic/lib/group';
import { DropdownSearch } from './DropdownSearch';
import useGroupState from '~/logic/state/groups';
import useMetadataState from '~/logic/state/metadata';
interface GroupSearchProps<I extends string> {
disabled?: boolean;
adminOnly?: boolean;
publicOnly?: boolean;
associations: Associations;
label: string;
caption?: string;
id: I;
@ -87,34 +87,35 @@ export function GroupSearch<I extends string, V extends FormValues<I>>(props: Gr
const touched = touchedFields[id] ?? false;
const error = _.compact(errors[id] as string[]);
const groupState = useGroupState(state => state.groups);
const associations = useMetadataState(state => state.associations);
const groups: Association[] = useMemo(() => {
if (props.adminOnly) {
return Object.values(
Object.keys(props.associations?.groups)
Object.keys(associations.groups)
.filter(
e => roleForShip(groupState[e], window.ship) === 'admin'
)
.reduce((obj, key) => {
obj[key] = props.associations?.groups[key];
obj[key] = associations.groups[key];
return obj;
}, {}) || {}
);
} else if (props.publicOnly) {
return Object.values(
Object.keys(props.associations?.groups)
Object.keys(associations.groups)
.filter(
e => groupState?.[e]?.policy?.open
)
.reduce((obj, key) => {
obj[key] = props.associations?.groups[key];
obj[key] = associations.groups[key];
return obj;
}, {}) || {}
);
} else {
return Object.values(props.associations?.groups || {});
return Object.values(associations.groups || {});
}
}, [props.associations?.groups]);
}, [associations.groups]);
return (
<FieldArray
@ -156,7 +157,7 @@ export function GroupSearch<I extends string, V extends FormValues<I>>(props: Gr
{value?.length > 0 && (
value.map((e, idx: number) => {
const { title } =
props.associations.groups?.[e]?.metadata || {};
associations.groups?.[e]?.metadata || {};
return (
<Row
key={e}

View File

@ -19,12 +19,11 @@ import { InviteSkeleton } from './InviteSkeleton';
import { JoinSkeleton } from './JoinSkeleton';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import useGroupState from '~/logic/state/groups';
import useMetadataState from '~/logic/state/metadata';
interface InviteItemProps {
invite?: Invite;
resource: string;
associations: Associations;
pendingJoin: JoinRequests;
app?: string;
uid?: string;
@ -33,11 +32,12 @@ interface InviteItemProps {
export function InviteItem(props: InviteItemProps) {
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
const { associations, pendingJoin, invite, resource, uid, app, api } = props;
const { pendingJoin, invite, resource, uid, app, api } = props;
const { ship, name } = resourceFromPath(resource);
const waiter = useWaitForProps(props, 50000);
const status = pendingJoin[resource];
const groups = useGroupState(state => state.groups);
const associations = useMetadataState(state => state.associations);
const history = useHistory();
const inviteAccept = useCallback(async () => {

View File

@ -21,9 +21,9 @@ import useGroupState from '~/logic/state/groups';
import useHarkState from '~/logic/state/hark';
import useInviteState from '~/logic/state/invite';
import useLaunchState from '~/logic/state/launch';
import useMetadataState from '~/logic/state/metadata';
interface OmniboxProps {
associations: Associations;
tiles: {
[app: string]: Tile;
};
@ -55,6 +55,7 @@ export function Omnibox(props: OmniboxProps) {
}, [contactState, query]);
const groups = useGroupState(state => state.groups);
const associations = useMetadataState(state => state.associations);
const index = useMemo(() => {
const selectedGroup = location.pathname.startsWith('/~landscape/ship/')
@ -62,12 +63,12 @@ export function Omnibox(props: OmniboxProps) {
: null;
return makeIndex(
contacts,
props.associations,
associations,
tiles,
selectedGroup,
groups
);
}, [location.pathname, contacts, props.associations, groups, tiles]);
}, [location.pathname, contacts, associations, groups, tiles]);
const onOutsideClick = useCallback(() => {
props.show && props.toggle();

View File

@ -7,16 +7,17 @@ import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import { getModuleIcon } from '~/logic/lib/util';
import { Dropdown } from '~/views/components/Dropdown';
import { resourceFromPath, roleForShip } from '~/logic/lib/group';
import useMetadataState from '~/logic/state/metadata';
interface GroupChannelSettingsProps {
group: Group;
association: Association;
associations: Associations;
api: GlobalApi;
}
export function GroupChannelSettings(props: GroupChannelSettingsProps) {
const { api, associations, association, group } = props;
const { api, association, group } = props;
const associations = useMetadataState(state => state.associations);
const channels = Object.values(associations.graph).filter(
({ group }) => association.group === group
);

View File

@ -20,7 +20,6 @@ const Section = ({ children }) => (
interface GroupSettingsProps {
group: Group;
association: Association;
associations: Associations;
api: GlobalApi;
notificationsGroupConfig: GroupNotificationsConfig;
s3: S3State;

View File

@ -14,6 +14,7 @@ import { Dropdown } from '~/views/components/Dropdown';
import { getTitleFromWorkspace } from '~/logic/lib/workspace';
import { MetadataIcon } from './MetadataIcon';
import { Workspace } from '~/types/workspace';
import useMetadataState from '~/logic/state/metadata';
const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => (
<Link to={to}>
@ -31,10 +32,11 @@ const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => (
);
function RecentGroups(props: { recent: string[]; associations: Associations }) {
const { associations, recent } = props;
const { recent } = props;
if (recent.length < 2) {
return null;
}
const associations = useMetadataState(state => state.associations);
return (
<Col borderBottom={1} borderBottomColor="lightGray" p={1}>
@ -70,13 +72,13 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) {
}
export function GroupSwitcher(props: {
associations: Associations;
workspace: Workspace;
baseUrl: string;
recentGroups: string[];
isAdmin: any;
}) {
const { associations, workspace, isAdmin } = props;
const { workspace, isAdmin } = props;
const associations = useMetadataState(state => state.associations);
const title = getTitleFromWorkspace(associations, workspace);
const metadata = (workspace.type === 'home' || workspace.type === 'messages')
? undefined
@ -136,7 +138,6 @@ export function GroupSwitcher(props: {
</GroupSwitcherItem>}
<RecentGroups
recent={props.recentGroups}
associations={props.associations}
/>
<GroupSwitcherItem to="/~landscape/new">
<Icon mr="2" color="gray" icon="CreateGroup" />

View File

@ -21,7 +21,6 @@ interface FormSchema {
interface GroupifyFormProps {
api: GlobalApi;
associations: Associations;
association: Association;
}
@ -78,7 +77,6 @@ export function GroupifyForm(props: GroupifyFormProps) {
id="group"
label="Group"
caption="Optionally, if you have admin privileges, you can add this channel to a group, or leave this blank to place the channel in its own group"
associations={props.associations}
adminOnly
maxLength={1}
/>

View File

@ -30,6 +30,7 @@ import { Workspace } from '~/types/workspace';
import useContactState from '~/logic/state/contacts';
import useGroupState from '~/logic/state/groups';
import useHarkState from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
type GroupsPaneProps = StoreState & {
baseUrl: string;
@ -38,7 +39,8 @@ type GroupsPaneProps = StoreState & {
};
export function GroupsPane(props: GroupsPaneProps) {
const { baseUrl, associations, api, workspace } = props;
const { baseUrl, api, workspace } = props;
const associations = useMetadataState(state => state.associations);
const contacts = useContactState(state => state.contacts);
const notificationsCount = useHarkState(state => state.notificationsCount);
const relativePath = (path: string) => baseUrl + path;
@ -77,7 +79,6 @@ export function GroupsPane(props: GroupsPaneProps) {
group={group!}
api={api}
s3={props.s3}
associations={associations}
{...routeProps}
baseUrl={baseUrl}
@ -180,7 +181,6 @@ export function GroupsPane(props: GroupsPaneProps) {
{...routeProps}
api={api}
baseUrl={baseUrl}
associations={associations}
group={groupPath}
workspace={workspace}
/>

View File

@ -23,6 +23,7 @@ import { getModuleIcon } from '~/logic/lib/util';
import { FormError } from '~/views/components/FormError';
import { GroupSummary } from './GroupSummary';
import useGroupState from '~/logic/state/groups';
import useMetadataState from '~/logic/state/metadata';
const formSchema = Yup.object({
group: Yup.string()
@ -41,7 +42,6 @@ interface FormSchema {
}
interface JoinGroupProps {
associations: Associations;
api: GlobalApi;
autojoin?: string;
}
@ -59,7 +59,8 @@ function Autojoin(props: { autojoin: string | null }) {
}
export function JoinGroup(props: JoinGroupProps): ReactElement {
const { api, autojoin, associations } = props;
const { api, autojoin } = props;
const associations = useMetadataState(state => state.associations);
const groups = useGroupState(state => state.groups);
const history = useHistory();
const initialValues: FormSchema = {

View File

@ -42,7 +42,6 @@ const formSchema = (members?: string[]) => Yup.object({
interface NewChannelProps {
api: GlobalApi;
associations: Associations;
group?: string;
workspace: Workspace;
}

View File

@ -17,6 +17,7 @@ import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import GlobalApi from '~/logic/api/global';
import { stringToSymbol } from '~/logic/lib/util';
import useGroupState from '~/logic/state/groups';
import useMetadataState from '~/logic/state/metadata';
const formSchema = Yup.object({
title: Yup.string().required('Group must have a name'),
@ -31,7 +32,6 @@ interface FormSchema {
}
interface NewGroupProps {
associations: Associations;
api: GlobalApi;
}
@ -45,6 +45,7 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps): ReactEleme
const waiter = useWaitForProps(props);
const groups = useGroupState(state => state.groups);
const associations = useMetadataState(state => state.associations);
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
@ -65,7 +66,7 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps): ReactEleme
};
await api.groups.create(name, policy, title, description);
const path = `/ship/~${window.ship}/${name}`;
await waiter(({ associations }) => {
await waiter(() => {
return path in groups && path in associations.groups;
});

View File

@ -22,7 +22,6 @@ export function PopoverRoutes(
baseUrl: string;
group: Group;
association: Association;
associations: Associations;
s3: S3State;
api: GlobalApi;
notificationsGroupConfig: GroupNotificationsConfig;
@ -125,8 +124,6 @@ export function PopoverRoutes(
group={props.group}
association={props.association}
api={props.api}
notificationsGroupConfig={props.notificationsGroupConfig}
associations={props.associations}
s3={props.s3}
/>
)}

View File

@ -14,6 +14,7 @@ import { ChannelPopoverRoutes } from './ChannelPopoverRoutes';
import useGroupState from '~/logic/state/groups';
import useContactState from '~/logic/state/contacts';
import useHarkState from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
type ResourceProps = StoreState & {
association: Association;
@ -25,6 +26,7 @@ export function Resource(props: ResourceProps): ReactElement {
const { association, api, notificationsGraphConfig } = props;
const groups = useGroupState(state => state.groups);
const notificationsCount = useHarkState(state => state.notificationsCount);
const associations = useMetadataState(state => state.associations);
const contacts = useContactState(state => state.contacts);
const app = association.metadata.module || association['app-name'];
const rid = association.resource;
@ -34,8 +36,8 @@ export function Resource(props: ResourceProps): ReactElement {
const skelProps = { api, association, groups, contacts };
let title = props.association.metadata.title;
if ('workspace' in props) {
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}`;
if ('group' in props.workspace && props.workspace.group in associations.groups) {
title = `${associations.groups[props.workspace.group].metadata.title} - ${props.association.metadata.title}`;
}
}
return (
@ -66,7 +68,6 @@ export function Resource(props: ResourceProps): ReactElement {
api={props.api}
baseUrl={relativePath('')}
rootUrl={props.baseUrl}
notificationsGraphConfig={notificationsGraphConfig}
/>
);
}}

View File

@ -21,6 +21,7 @@ import { SidebarList } from './SidebarList';
import { roleForShip } from '~/logic/lib/group';
import { useTutorialModal } from '~/views/components/useTutorialModal';
import useGroupState from '~/logic/state/groups';
import useMetadataState from '~/logic/state/metadata';
const ScrollbarLessCol = styled(Col)`
scrollbar-width: none !important;
@ -34,7 +35,6 @@ interface SidebarProps {
children: ReactNode;
recentGroups: string[];
api: GlobalApi;
associations: Associations;
selected?: string;
selectedGroup?: string;
includeUnmanaged?: boolean;
@ -44,8 +44,9 @@ interface SidebarProps {
workspace: Workspace;
}
export function Sidebar(props: SidebarProps): ReactElement {
const { associations, selected, workspace } = props;
export function Sidebar(props: SidebarProps): ReactElement | null {
const { selected, workspace } = props;
const associations = useMetadataState(state => state.associations);
const groupPath = getGroupFromWorkspace(workspace);
const display = props.mobileHide ? ['none', 'flex'] : 'flex';
if (!associations) {
@ -83,14 +84,12 @@ export function Sidebar(props: SidebarProps): ReactElement {
position="relative"
>
<GroupSwitcher
associations={associations}
recentGroups={props.recentGroups}
baseUrl={props.baseUrl}
isAdmin={isAdmin}
workspace={props.workspace}
/>
<SidebarListHeader
associations={associations}
baseUrl={props.baseUrl}
initialValues={config}
handleSubmit={setConfig}
@ -101,7 +100,6 @@ export function Sidebar(props: SidebarProps): ReactElement {
/>
<SidebarList
config={config}
associations={associations}
selected={selected}
group={groupPath}
apps={props.apps}

View File

@ -5,6 +5,7 @@ import { alphabeticalOrder } from '~/logic/lib/util';
import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types';
import { SidebarItem } from './SidebarItem';
import { Workspace } from '~/types/workspace';
import useMetadataState from '~/logic/state/metadata';
function sidebarSort(
associations: AppAssociations,
@ -40,24 +41,24 @@ function sidebarSort(
export function SidebarList(props: {
apps: SidebarAppConfigs;
config: SidebarListConfig;
associations: Associations;
baseUrl: string;
group?: string;
selected?: string;
workspace: Workspace;
}): ReactElement {
const { selected, group, config, workspace } = props;
const associations = { ...props.associations.graph };
const associationState = useMetadataState(state => state.associations);
const associations = { ...associationState.graph };
const ordered = Object.keys(associations)
.filter((a) => {
const assoc = associations[a];
if (workspace?.type === 'messages') {
return (!(assoc.group in props.associations.groups) && assoc.metadata.module === 'chat');
return (!(assoc.group in associationState.groups) && assoc.metadata.module === 'chat');
} else {
return group
? assoc.group === group
: (!(assoc.group in props.associations.groups) && assoc.metadata.module !== 'chat');
: (!(assoc.group in associationState.groups) && assoc.metadata.module !== 'chat');
}
})
.sort(sidebarSort(associations, props.apps)[config.sortBy]);

View File

@ -22,11 +22,11 @@ import { NewChannel } from '~/views/landscape/components/NewChannel';
import GlobalApi from '~/logic/api/global';
import { Workspace } from '~/types/workspace';
import useGroupState from '~/logic/state/groups';
import useMetadataState from '~/logic/state/metadata';
export function SidebarListHeader(props: {
api: GlobalApi;
initialValues: SidebarListConfig;
associations: Associations;
baseUrl: string;
selected: string;
workspace: Workspace;
@ -43,8 +43,9 @@ export function SidebarListHeader(props: {
const groupPath = getGroupFromWorkspace(props.workspace);
const role = groupPath && groups?.[groupPath] ? roleForShip(groups[groupPath], window.ship) : undefined;
const associations = useMetadataState(state => state.associations);
const memberMetadata =
groupPath ? props.associations.groups?.[groupPath].metadata.vip === 'member-metadata' : false;
groupPath ? associations.groups?.[groupPath].metadata.vip === 'member-metadata' : false;
const isAdmin = memberMetadata || (role === 'admin') || (props.workspace?.type === 'home') || (props.workspace?.type === 'messages');
@ -86,7 +87,6 @@ export function SidebarListHeader(props: {
<NewChannel
api={props.api}
history={props.history}
associations={props.associations}
workspace={props.workspace}
/>
</Col>

View File

@ -15,7 +15,6 @@ import useHarkState from '~/logic/state/hark';
interface SkeletonProps {
children: ReactNode;
recentGroups: string[];
associations: Associations;
linkListening: Set<Path>;
selected?: string;
selectedApp?: AppName;
@ -51,7 +50,6 @@ export function Skeleton(props: SkeletonProps): ReactElement {
api={props.api}
recentGroups={props.recentGroups}
selected={props.selected}
associations={props.associations}
apps={config}
baseUrl={props.baseUrl}
mobileHide={props.mobileHide}

View File

@ -118,7 +118,6 @@ class Landscape extends Component<LandscapeProps, Record<string, never>> {
<Body>
<Box maxWidth="300px">
<NewGroup
associations={props.associations}
api={props.api}
{...routeProps}
/>