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 _ from 'lodash';
import { compose } from 'lodash/fp';
import { StoreState } from '../../store/type';
import { MetadataUpdate } from '@urbit/api/metadata'; import { MetadataUpdate } from '@urbit/api/metadata';
import { Cage } from '~/types/cage'; import { Cage } from '~/types/cage';
import useMetadataState, { MetadataState } from '../state/metadata';
type MetadataState = Pick<StoreState, 'associations'>; export default class MetadataReducer {
reduce(json: Cage) {
export default class MetadataReducer<S extends MetadataState> {
reduce(json: Cage, state: S) {
const data = json['metadata-update']; const data = json['metadata-update'];
if (data) { if (data) {
console.log(data); useMetadataState.setState(
this.associations(data, state); compose([
this.add(data, state); associations,
this.update(data, state); add,
this.remove(data, state); update,
this.groupInitial(data, state); remove,
groupInitial,
].map(reducer => reducer.bind(reducer, data))
)(useMetadataState.getState())
);
} }
} }
}
groupInitial(json: MetadataUpdate, state: S) { const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'initial-group', false); const data = _.get(json, 'initial-group', false);
console.log(data); console.log(data);
if(data) { if(data) {
this.associations(data, state); state = associations(data, state);
}
} }
return state;
}
associations(json: MetadataUpdate, state: S) { const associations = (json: MetadataUpdate, state: MetadataState): MetadataState => {
const data = _.get(json, 'associations', false); const data = _.get(json, 'associations', false);
if (data) { if (data) {
const metadata = state.associations; const metadata = state.associations;
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
const val = data[key]; const val = data[key];
const appName = val['app-name']; const appName = val['app-name'];
const rid = val.resource; 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;
if (!(appName in metadata)) { if (!(appName in metadata)) {
metadata[appName] = {}; metadata[appName] = {};
} }
if (!(rid in metadata[appName])) { if (!(rid in metadata[appName])) {
metadata[appName][rid] = {}; metadata[appName][rid] = {};
} }
metadata[appName][rid] = data; metadata[appName][rid] = val;
});
state.associations = metadata; 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;
}
} }
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) set: fn => stateSetter(fn, set)
}), { }), {
blacklist: ['nackedContacts'],
name: 'LandscapeContactState' 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.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)];
this.inviteReducer.reduce(data); this.inviteReducer.reduce(data);
this.metadataReducer.reduce(data, this.state); this.metadataReducer.reduce(data);
this.localReducer.reduce(data, this.state); this.localReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state); this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data); this.groupReducer.reduce(data);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,55 +3,52 @@ import { Switch, Route } from 'react-router-dom';
import { Center, Text } from "@tlon/indigo-react"; import { Center, Text } from "@tlon/indigo-react";
import { deSig } from '~/logic/lib/util'; import { deSig } from '~/logic/lib/util';
import useGraphState from '~/logic/state/graph'; 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 { const autoJoin = () => {
render() { try {
const { props } = this; api.graph.joinGraph(
const associations = `~${deSig(props.match.params.ship)}`,
props.associations ? props.associations : { graph: {}, contacts: {} }; props.match.params.name
const graphKeys = useGraphState(state => state.graphKeys); );
const { api } = this.props;
} catch(err) {
return ( setTimeout(autoJoin, 2000);
<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}`);
} }
return ( };
<Center width="100%" height="100%">
<Text fontSize={1}>Redirecting...</Text> if(!graphKeys.has(resource)) {
</Center> autoJoin();
); } else if(!!association) {
}} props.history.push(`/~landscape/home/resource/${association.metadata.module}${path}`);
/> }
</Switch> 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} /> <JoinGroup {...props} />
</ModalButton> </ModalButton>
<Groups associations={props.associations} /> <Groups />
</Box> </Box>
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box> <Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
</ScrollbarLessBox> </ScrollbarLessBox>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,6 @@ interface FormSchema {
interface GroupifyFormProps { interface GroupifyFormProps {
api: GlobalApi; api: GlobalApi;
associations: Associations;
association: Association; association: Association;
} }
@ -78,7 +77,6 @@ export function GroupifyForm(props: GroupifyFormProps) {
id="group" id="group"
label="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" 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 adminOnly
maxLength={1} maxLength={1}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import { alphabeticalOrder } from '~/logic/lib/util';
import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types'; import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types';
import { SidebarItem } from './SidebarItem'; import { SidebarItem } from './SidebarItem';
import { Workspace } from '~/types/workspace'; import { Workspace } from '~/types/workspace';
import useMetadataState from '~/logic/state/metadata';
function sidebarSort( function sidebarSort(
associations: AppAssociations, associations: AppAssociations,
@ -40,24 +41,24 @@ function sidebarSort(
export function SidebarList(props: { export function SidebarList(props: {
apps: SidebarAppConfigs; apps: SidebarAppConfigs;
config: SidebarListConfig; config: SidebarListConfig;
associations: Associations;
baseUrl: string; baseUrl: string;
group?: string; group?: string;
selected?: string; selected?: string;
workspace: Workspace; workspace: Workspace;
}): ReactElement { }): ReactElement {
const { selected, group, config, workspace } = props; 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) const ordered = Object.keys(associations)
.filter((a) => { .filter((a) => {
const assoc = associations[a]; const assoc = associations[a];
if (workspace?.type === 'messages') { 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 { } else {
return group return group
? assoc.group === 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]); .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 GlobalApi from '~/logic/api/global';
import { Workspace } from '~/types/workspace'; import { Workspace } from '~/types/workspace';
import useGroupState from '~/logic/state/groups'; import useGroupState from '~/logic/state/groups';
import useMetadataState from '~/logic/state/metadata';
export function SidebarListHeader(props: { export function SidebarListHeader(props: {
api: GlobalApi; api: GlobalApi;
initialValues: SidebarListConfig; initialValues: SidebarListConfig;
associations: Associations;
baseUrl: string; baseUrl: string;
selected: string; selected: string;
workspace: Workspace; workspace: Workspace;
@ -43,8 +43,9 @@ export function SidebarListHeader(props: {
const groupPath = getGroupFromWorkspace(props.workspace); const groupPath = getGroupFromWorkspace(props.workspace);
const role = groupPath && groups?.[groupPath] ? roleForShip(groups[groupPath], window.ship) : undefined; const role = groupPath && groups?.[groupPath] ? roleForShip(groups[groupPath], window.ship) : undefined;
const associations = useMetadataState(state => state.associations);
const memberMetadata = 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'); const isAdmin = memberMetadata || (role === 'admin') || (props.workspace?.type === 'home') || (props.workspace?.type === 'messages');
@ -86,7 +87,6 @@ export function SidebarListHeader(props: {
<NewChannel <NewChannel
api={props.api} api={props.api}
history={props.history} history={props.history}
associations={props.associations}
workspace={props.workspace} workspace={props.workspace}
/> />
</Col> </Col>

View File

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

View File

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