interface: covert contact store to zustand

This commit is contained in:
Tyler Brown Cifu Shuster 2021-02-25 19:46:45 -08:00
parent 72aa7f5aee
commit 12645644f7
53 changed files with 166 additions and 155 deletions

View File

@ -2,8 +2,9 @@ import { useEffect, useState } from 'react';
import _ from 'lodash';
import f, { memoize } from 'lodash/fp';
import bigInt, { BigInteger } from 'big-integer';
import { Contact } from '@urbit/api';
import { Association, Contact } from '@urbit/api';
import useLocalState from '../state/local';
import produce from 'immer';
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
@ -408,3 +409,8 @@ export function getItemTitle(association: Association) {
}
return association.metadata.title || association.resource;
}
export const stateSetter = <StateType>(fn: (state: StateType) => void, set): void => {
// TODO this is a stub for the store debugging
return set(produce(fn));
};

View File

@ -1,44 +1,53 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { ContactUpdate } from '@urbit/api/contacts';
import { resourceAsPath } from '../lib/util';
import { compose } from 'lodash/fp';
type ContactState = Pick<StoreState, 'contacts'>;
import { ContactUpdate } from '@urbit/api';
export const ContactReducer = (json, state) => {
const data = _.get(json, 'contact-update', false);
import useContactState, { ContactState } from '../state/contacts';
export const ContactReducer = (json) => {
const data: ContactUpdate = _.get(json, 'contact-update', false);
if (data) {
initial(data, state);
add(data, state);
remove(data, state);
edit(data, state);
setPublic(data, state);
useContactState.setState(
compose([
initial,
add,
remove,
edit,
setPublic
].map(reducer => reducer.bind(reducer, data))
)(useContactState.getState())
);
}
// TODO: better isolation
const res = _.get(json, 'resource', false);
if(res) {
state.nackedContacts = state.nackedContacts.add(`~${res.ship}`);
if (res) {
useContactState.setState({
nackedContacts: useContactState.getState().nackedContacts.add(`~${res.ship}`)
});
}
};
const initial = (json: ContactUpdate, state: S) => {
const initial = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'initial', false);
if (data) {
state.contacts = data.rolodex;
state.isContactPublic = data['is-public'];
}
return state;
};
const add = (json: ContactUpdate, state: S) => {
const add = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'add', false);
if (data) {
state.contacts[data.ship] = data.contact;
}
return state;
};
const remove = (json: ContactUpdate, state: S) => {
const remove = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'remove', false);
if (
data &&
@ -46,9 +55,10 @@ const remove = (json: ContactUpdate, state: S) => {
) {
delete state.contacts[data.ship];
}
return state;
};
const edit = (json: ContactUpdate, state: S) => {
const edit = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'edit', false);
const ship = `~${data.ship}`;
if (
@ -57,7 +67,7 @@ const edit = (json: ContactUpdate, state: S) => {
) {
const [field] = Object.keys(data['edit-field']);
if (!field) {
return;
return state;
}
const value = data['edit-field'][field];
@ -71,10 +81,12 @@ const edit = (json: ContactUpdate, state: S) => {
state.contacts[ship][field] = value;
}
}
return state;
};
const setPublic = (json: ContactUpdate, state: S) => {
const setPublic = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'set-public', state.isContactPublic);
state.isContactPublic = data;
return state;
};

View File

@ -0,0 +1,51 @@
import React from "react";
import create, { State } from 'zustand';
import { persist } from 'zustand/middleware';
import { Patp, Rolodex, Scry } from "@urbit/api";
import { stateSetter } from "~/logic/lib/util";
// import useApi from "~/logic/lib/useApi";
export interface ContactState extends State {
contacts: Rolodex;
isContactPublic: boolean;
nackedContacts: Set<Patp>;
// fetchIsAllowed: (entity, name, ship, personal) => Promise<boolean>;
set: (fn: (state: ContactState) => void) => void;
};
const useContactState = create<ContactState>(persist((set, get) => ({
contacts: {},
nackedContacts: new Set(),
isContactPublic: false,
// fetchIsAllowed: async (
// entity,
// name,
// ship,
// personal
// ): Promise<boolean> => {
// const isPersonal = personal ? 'true' : 'false';
// const api = useApi();
// return api.scry({
// app: 'contact-store',
// path: `/is-allowed/${entity}/${name}/${ship}/${isPersonal}`
// });
// },
set: fn => stateSetter(fn, set)
}), {
name: 'LandscapeContactState'
}));
function withContactState<P, S extends keyof ContactState>(Component: any, stateMemberKeys?: S[]) {
return React.forwardRef((props: Omit<P, S>, ref) => {
const contactState = stateMemberKeys ? useContactState(
state => stateMemberKeys.reduce(
(object, key) => ({ ...object, [key]: state[key] }), {}
)
): useContactState();
return <Component ref={ref} {...contactState} {...props} />
});
}
export { useContactState as default, withContactState };

View File

@ -77,9 +77,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
},
credentials: null
},
isContactPublic: false,
contacts: {},
nackedContacts: new Set(),
notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(),
notificationsGroupConfig: [],
@ -113,7 +110,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state);
HarkReducer(data, this.state);
ContactReducer(data, this.state);
ContactReducer(data);
this.settingsReducer.reduce(data, this.state);
GroupViewReducer(data, this.state);
}

View File

@ -25,12 +25,9 @@ export interface StoreState {
invites: Invites;
// metadata state
associations: Associations;
// contact state
contacts: Rolodex;
// groups state
groups: Groups;
groupKeys: Set<Path>;
nackedContacts: Set<Patp>
s3: S3State;
graphs: Graphs;
graphKeys: Set<string>;

View File

@ -28,6 +28,7 @@ import GlobalApi from '~/logic/api/global';
import { uxToHex } from '~/logic/lib/util';
import { foregroundFromBackground } from '~/logic/lib/sigil';
import { withLocalState } from '~/logic/state/local';
import { withContactState } from '~/logic/state/contacts';
const Root = styled.div`
@ -116,8 +117,8 @@ class App extends React.Component {
faviconString() {
let background = '#ffffff';
if (this.state.contacts.hasOwnProperty('/~/default')) {
background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`;
if (this.props.contacts.hasOwnProperty('/~/default')) {
background = `#${uxToHex(this.props.contacts['/~/default'][window.ship].color)}`;
}
const foreground = foregroundFromBackground(background);
const svg = sigiljs({
@ -139,7 +140,7 @@ class App extends React.Component {
const notificationsCount = state.notificationsCount || 0;
const doNotDisturb = state.doNotDisturb || false;
const ourContact = this.state.contacts[`~${this.ship}`] || null;
const ourContact = this.props.contacts[`~${this.ship}`] || null;
return (
<ThemeProvider theme={theme}>
@ -171,7 +172,6 @@ class App extends React.Component {
apps={state.launch}
tiles={state.launch.tiles}
api={this.api}
contacts={state.contacts}
notifications={state.notificationsCount}
invites={state.invites}
groups={state.groups}
@ -195,5 +195,5 @@ class App extends React.Component {
}
}
export default withLocalState(process.env.NODE_ENV === 'production' ? App : hot(App));
export default withContactState(withLocalState(process.env.NODE_ENV === 'production' ? App : hot(App)));

View File

@ -17,6 +17,7 @@ import useS3 from '~/logic/lib/useS3';
import { isWriter, resourceFromPath } from '~/logic/lib/group';
import './css/custom.css';
import useContactState from '~/logic/state/contacts';
type ChatResourceProps = StoreState & {
association: Association;
@ -28,7 +29,7 @@ export function ChatResource(props: ChatResourceProps) {
const station = props.association.resource;
const groupPath = props.association.group;
const group = props.groups[groupPath];
const contacts = props.contacts;
const contacts = useContactState(state => state.contacts);
const graph = props.graphs[station.slice(7)];
const isChatMissing = !props.graphKeys.has(station.slice(7));
const unreadCount = props.unreads.graph?.[station]?.['/']?.unreads || 0;

View File

@ -18,7 +18,6 @@ type ChatInputProps = IuseS3 & {
station: unknown;
ourContact: unknown;
envelopes: Envelope[];
contacts: Contacts;
onUnmount(msg: string): void;
s3: unknown;
placeholder: string;

View File

@ -33,6 +33,7 @@ import { Mention } from '~/views/components/MentionText';
import styled from 'styled-components';
import useLocalState from '~/logic/state/local';
import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contacts';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
@ -85,7 +86,6 @@ interface ChatMessageProps {
isLastRead: boolean;
group: Group;
association: Association;
contacts: Contacts;
className?: string;
isPending: boolean;
style?: unknown;
@ -120,7 +120,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
isLastRead,
group,
association,
contacts,
className = '',
isPending,
style,
@ -164,7 +163,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
const messageProps = {
msg,
timestamp,
contacts,
association,
group,
measure: reboundMeasure.bind(this),
@ -219,7 +217,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
export const MessageAuthor = ({
timestamp,
contacts,
msg,
measure,
group,
@ -231,6 +228,7 @@ export const MessageAuthor = ({
...rest
}) => {
const dark = useLocalState((state) => state.dark);
const contacts = useContactState(state => state.contacts);
const datestamp = moment
.unix(msg['time-sent'] / 1000)
@ -364,7 +362,6 @@ export const MessageAuthor = ({
export const Message = ({
timestamp,
contacts,
msg,
measure,
group,
@ -376,6 +373,7 @@ export const Message = ({
...rest
}) => {
const { hovering, bind } = useHovering();
const contacts = useContactState(state => state.contacts);
return (
<Box position='relative' {...rest}>
{timestampHover ? (

View File

@ -32,7 +32,6 @@ type ChatWindowProps = RouteComponentProps<{
}> & {
unreadCount: number;
graph: Graph;
contacts: Contacts;
association: Association;
group: Group;
ship: Patp;
@ -240,11 +239,8 @@ export default class ChatWindow extends Component<
const {
unreadCount,
api,
ship,
station,
association,
group,
contacts,
graph,
history,
groups,
@ -255,7 +251,6 @@ export default class ChatWindow extends Component<
const messageProps = {
association,
group,
contacts,
unreadMarkerRef,
history,
api,

View File

@ -7,7 +7,6 @@ import { deSig } from '~/logic/lib/util';
export default class GraphApp extends PureComponent {
render() {
const { props } = this;
const contacts = props.contacts ? props.contacts : {};
const groups = props.groups ? props.groups : {};
const associations =
props.associations ? props.associations : { graph: {}, contacts: {} };

View File

@ -7,7 +7,7 @@ import _ from 'lodash';
import { Col, Button, Box, Row, Icon, Text } from '@tlon/indigo-react';
import './css/custom.css';
import useContactState from '~/logic/state/contacts';
import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import Groups from './components/Groups';
@ -41,7 +41,6 @@ const ScrollbarLessBox = styled(Box)`
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep']);
export default function LaunchApp(props) {
const history = useHistory();
const [hashText, setHashText] = useState(props.baseHash);
const hashBox = (
<Box
@ -133,7 +132,8 @@ export default function LaunchApp(props) {
</Col>
)}
});
const hasLoaded = useMemo(() => Object.keys(props.contacts).length > 0, [props.contacts]);
const contacts = useContactState(state => state.contacts);
const hasLoaded = useMemo(() => Object.keys(contacts).length > 0, [contacts]);
useEffect(() => {
const seenTutorial = _.get(props.settings, ['tutorial', 'seen'], true);

View File

@ -28,7 +28,6 @@ export function LinkResource(props: LinkResourceProps) {
api,
baseUrl,
graphs,
contacts,
groups,
associations,
graphKeys,
@ -70,7 +69,6 @@ export function LinkResource(props: LinkResourceProps) {
<LinkWindow
s3={s3}
association={resource}
contacts={contacts}
resource={resourcePath}
graph={graph}
unreads={unreads}
@ -103,7 +101,6 @@ export function LinkResource(props: LinkResourceProps) {
<Col width="100%" p={3} maxWidth="768px">
<Link to={resourceUrl}><Text px={3} bold>{'<- Back'}</Text></Link>
<LinkItem
contacts={contacts}
key={node.post.index}
resource={resourcePath}
node={node}
@ -122,7 +119,6 @@ export function LinkResource(props: LinkResourceProps) {
resource={resourcePath}
association={association}
unreads={unreads}
contacts={contacts}
api={api}
editCommentId={editCommentId}
history={props.history}

View File

@ -17,7 +17,6 @@ interface LinkItemProps {
api: GlobalApi;
group: Group;
path: string;
contacts: Rolodex;
unreads: Unreads;
measure: (el: any) => void;
}
@ -29,7 +28,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
api,
group,
path,
contacts,
measure,
...rest
} = props;
@ -157,7 +155,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
<Author
showImage
contacts={contacts}
ship={author}
date={node.post['time-sent']}
group={group}

View File

@ -18,6 +18,7 @@ import { getSnippet } from '~/logic/lib/publish';
import styled from 'styled-components';
import { MentionText } from '~/views/components/MentionText';
import ChatMessage from '../chat/components/ChatMessage';
import useContactState from '~/logic/state/contacts';
function getGraphModuleIcon(module: string) {
if (module === 'link') {
@ -67,7 +68,6 @@ const GraphUrl = ({ url, title }) => (
const GraphNodeContent = ({
group,
post,
contacts,
mod,
description,
index,
@ -81,7 +81,7 @@ const GraphNodeContent = ({
return <GraphUrl title={text} url={url} />;
} else if (idx.length === 3) {
return (
<MentionText content={contents} contacts={contacts} group={group} />
<MentionText content={contents} group={group} />
);
}
return null;
@ -92,7 +92,6 @@ const GraphNodeContent = ({
<MentionText
content={contents}
group={group}
contacts={contacts}
fontSize='14px'
lineHeight='tall'
/>
@ -134,7 +133,6 @@ const GraphNodeContent = ({
containerClass='items-top cf hide-child'
measure={() => {}}
group={group}
contacts={contacts}
groups={{}}
associations={{ graph: {}, groups: {} }}
msg={post}
@ -174,7 +172,6 @@ function getNodeUrl(
}
const GraphNode = ({
post,
contacts,
author,
mod,
description,
@ -189,6 +186,7 @@ const GraphNode = ({
}) => {
author = deSig(author);
const history = useHistory();
const contacts = useContactState(state => state.contacts);
const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index);
@ -207,7 +205,6 @@ const GraphNode = ({
{showContact && (
<Author
showImage
contacts={contacts}
ship={author}
date={time}
group={group}
@ -215,7 +212,6 @@ const GraphNode = ({
)}
<Row width='100%' p='1' flexDirection='column'>
<GraphNodeContent
contacts={contacts}
post={post}
mod={mod}
description={description}
@ -238,7 +234,6 @@ export function GraphNotification(props: {
timebox: BigInteger;
associations: Associations;
groups: Groups;
contacts: Rolodex;
api: GlobalApi;
}) {
const { contents, index, read, time, api, timebox, groups } = props;
@ -266,7 +261,6 @@ export function GraphNotification(props: {
authors={authors}
moduleIcon={icon}
channel={graph}
contacts={props.contacts}
group={group}
description={desc}
associations={props.associations}
@ -276,7 +270,6 @@ export function GraphNotification(props: {
<GraphNode
post={content}
author={content.author}
contacts={props.contacts}
mod={index.module}
time={content?.['time-sent']}
description={index.description}

View File

@ -69,7 +69,6 @@ export function GroupNotification(props: GroupNotificationProps): ReactElement {
time={time}
read={read}
group={group}
contacts={props.contacts}
authors={authors}
description={desc}
associations={associations}

View File

@ -9,13 +9,15 @@ import { Associations, Contact, Contacts, Rolodex } from '@urbit/api';
import { PropFunc } from '~/types/util';
import { useShowNickname } from '~/logic/lib/util';
import Timestamp from '~/views/components/Timestamp';
import useContactState from '~/logic/state/contacts';
const Text = (props: PropFunc<typeof Text>) => (
<NormalText fontWeight="500" {...props} />
);
function Author(props: { patp: string; contacts: Contacts; last?: boolean }): ReactElement {
const contact: Contact | undefined = props.contacts?.[`~${props.patp}`];
function Author(props: { patp: string; last?: boolean }): ReactElement {
const contacts = useContactState(state => state.contacts);
const contact: Contact | undefined = contacts?.[`~${props.patp}`];
const showNickname = useShowNickname(contact);
const name = contact?.nickname || `~${props.patp}`;
@ -33,14 +35,13 @@ export function Header(props: {
archived?: boolean;
channel?: string;
group: string;
contacts: Rolodex;
description: string;
moduleIcon?: string;
time: number;
read: boolean;
associations: Associations;
} & PropFunc<typeof Row> ): ReactElement {
const { description, channel, contacts, moduleIcon, read } = props;
const { description, channel, moduleIcon, read } = props;
const authors = _.uniq(props.authors);
@ -50,7 +51,7 @@ export function Header(props: {
f.map(([idx, p]: [string, string]) => {
const lent = Math.min(3, authors.length);
const last = lent - 1 === parseInt(idx, 10);
return <Author key={idx} contacts={contacts} patp={p} last={last} />;
return <Author key={idx} patp={p} last={last} />;
}),
auths => (
<React.Fragment>

View File

@ -50,7 +50,6 @@ export default function Inbox(props: {
showArchive?: boolean;
api: GlobalApi;
associations: Associations;
contacts: Rolodex;
filter: string[];
invites: InviteType;
pendingJoin: JoinRequests;
@ -127,7 +126,6 @@ export default function Inbox(props: {
key={day}
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
timeboxes={timeboxes}
contacts={props.contacts}
archive={Boolean(props.showArchive)}
associations={props.associations}
api={api}
@ -165,7 +163,6 @@ function sortIndexedNotification(
function DaySection({
label,
contacts,
groups,
archive,
timeboxes,
@ -201,7 +198,6 @@ function DaySection({
associations={associations}
notification={not}
archived={archive}
contacts={contacts}
groups={groups}
time={date}
/>

View File

@ -12,7 +12,6 @@ interface InvitesProps {
api: GlobalApi;
invites: IInvites;
groups: Groups;
contacts: Contacts;
associations: Associations;
pendingJoin: JoinRequests;
}
@ -54,7 +53,6 @@ export function Invites(props: InvitesProps): ReactElement {
return (
<InviteItem
key={resource}
contacts={props.contacts}
groups={props.groups}
associations={props.associations}
resource={resource}
@ -74,7 +72,6 @@ export function Invites(props: InvitesProps): ReactElement {
uid={uid}
pendingJoin={pendingJoin}
resource={resource}
contacts={props.contacts}
groups={props.groups}
associations={props.associations}
/>

View File

@ -26,7 +26,6 @@ interface NotificationProps {
api: GlobalApi;
archived: boolean;
groups: Groups;
contacts: Contacts;
graphConfig: NotificationGraphConfig;
groupConfig: GroupNotificationsConfig;
}
@ -136,7 +135,6 @@ export function Notification(props: NotificationProps) {
api={props.api}
index={index}
contents={c}
contacts={props.contacts}
groups={props.groups}
read={read}
archived={archived}
@ -156,7 +154,6 @@ export function Notification(props: NotificationProps) {
api={props.api}
index={index}
contents={c}
contacts={props.contacts}
groups={props.groups}
read={read}
timebox={props.time}

View File

@ -5,8 +5,10 @@ import Helmet from 'react-helmet';
import { Box } from '@tlon/indigo-react';
import { Profile } from './components/Profile';
import useContactState from '~/logic/state/contacts';
export default function ProfileScreen(props: any) {
const contacts = useContactState(state => state.contacts);
return (
<>
<Helmet defer={false}>
@ -18,7 +20,7 @@ export default function ProfileScreen(props: any) {
const ship = match.params.ship;
const isEdit = match.url.includes('edit');
const isPublic = props.isContactPublic;
const contact = props.contacts?.[ship];
const contact = contacts?.[ship];
return (
<Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
@ -35,7 +37,7 @@ export default function ProfileScreen(props: any) {
<Box>
<Profile
ship={ship}
hasLoaded={Object.keys(props.contacts).length !== 0}
hasLoaded={Object.keys(contacts).length !== 0}
associations={props.associations}
groups={props.groups}
contact={contact}

View File

@ -24,7 +24,6 @@ export function PublishResource(props: PublishResourceProps) {
api={api}
ship={ship}
book={book}
contacts={props.contacts}
groups={props.groups}
associations={props.associations}
association={association}

View File

@ -22,7 +22,6 @@ interface MetadataFormProps {
host: string;
book: string;
association: Association;
contacts: Contacts;
api: GlobalApi;
}

View File

@ -19,7 +19,6 @@ interface NoteProps {
unreads: Unreads;
association: Association;
notebook: Graph;
contacts: Contacts;
api: GlobalApi;
rootUrl: string;
baseUrl: string;
@ -29,7 +28,7 @@ interface NoteProps {
export function Note(props: NoteProps & RouteComponentProps) {
const [deleting, setDeleting] = useState(false);
const { notebook, note, contacts, ship, book, api, rootUrl, baseUrl, group } = props;
const { notebook, note, ship, book, api, rootUrl, baseUrl, group } = props;
const editCommentId = props.match.params.commentId;
const deletePost = async () => {
@ -100,7 +99,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
<Box display="flex">
<Author
ship={post?.author}
contacts={contacts}
date={post?.['time-sent']}
/>
<Text ml={2}>{adminLinks}</Text>
@ -120,7 +118,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
name={props.book}
unreads={props.unreads}
comments={comments}
contacts={props.contacts}
association={props.association}
api={props.api}
baseUrl={baseUrl}

View File

@ -21,7 +21,6 @@ interface NotePreviewProps {
node: GraphNode;
baseUrl: string;
unreads: Unreads;
contacts: Contacts;
api: GlobalApi;
group: Group;
}
@ -31,7 +30,7 @@ const WrappedBox = styled(Box)`
`;
export function NotePreview(props: NotePreviewProps) {
const { node, contacts, group } = props;
const { node, group } = props;
const { post } = node;
if (!post) {
return null;
@ -79,7 +78,6 @@ export function NotePreview(props: NotePreviewProps) {
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
<Author
showImage
contacts={contacts}
ship={post?.author}
date={post?.['time-sent']}
group={group}

View File

@ -14,7 +14,6 @@ interface NoteRoutesProps {
note: GraphNode;
noteId: number;
notebook: Graph;
contacts: Contacts;
api: GlobalApi;
association: Association;
baseUrl?: string;

View File

@ -7,6 +7,7 @@ import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads }
import { NotebookPosts } from './NotebookPosts';
import GlobalApi from '~/logic/api/global';
import { useShowNickname } from '~/logic/lib/util';
import useContactState from '~/logic/state/contacts';
interface NotebookProps {
api: GlobalApi;
@ -15,7 +16,6 @@ interface NotebookProps {
graph: Graph;
association: Association;
associations: Associations;
contacts: Rolodex;
groups: Groups;
baseUrl: string;
rootUrl: string;
@ -26,7 +26,6 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
const {
ship,
book,
contacts,
groups,
association,
graph
@ -38,6 +37,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
}
const relativePath = (p: string) => props.baseUrl + p;
const contacts = useContactState(state => state.contacts);
const contact = contacts?.[`~${ship}`];
console.log(association.resource);
@ -60,7 +60,6 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
graph={graph}
host={ship}
book={book}
contacts={contacts}
unreads={props.unreads}
baseUrl={props.baseUrl}
api={props.api}

View File

@ -2,9 +2,9 @@ import React, { Component } from 'react';
import { Col } from '@tlon/indigo-react';
import { NotePreview } from './NotePreview';
import { Contacts, Graph, Unreads, Group } from '@urbit/api';
import useContactState from '~/logic/state/contacts';
interface NotebookPostsProps {
contacts: Contacts;
graph: Graph;
host: string;
book: string;
@ -17,6 +17,7 @@ interface NotebookPostsProps {
}
export function NotebookPosts(props: NotebookPostsProps) {
const contacts = useContactState(state => state.contacts);
return (
<Col>
{Array.from(props.graph || []).map(
@ -27,8 +28,7 @@ export function NotebookPosts(props: NotebookPostsProps) {
host={props.host}
book={props.book}
unreads={props.unreads}
contact={props.contacts[`~${node.post.author}`]}
contacts={props.contacts}
contact={contacts[`~${node.post.author}`]}
node={node}
baseUrl={props.baseUrl}
api={props.api}

View File

@ -24,7 +24,6 @@ interface NotebookRoutesProps {
book: string;
graphs: Graphs;
unreads: Unreads;
contacts: Rolodex;
groups: Groups;
baseUrl: string;
rootUrl: string;
@ -36,7 +35,7 @@ interface NotebookRoutesProps {
export function NotebookRoutes(
props: NotebookRoutesProps & RouteComponentProps
) {
const { ship, book, api, contacts, baseUrl, rootUrl, groups } = props;
const { ship, book, api, baseUrl, rootUrl, groups } = props;
useEffect(() => {
ship && book && api.graph.getGraph(ship, book);
@ -59,7 +58,6 @@ export function NotebookRoutes(
return <Notebook
{...props}
graph={graph}
contacts={contacts}
association={props.association}
rootUrl={rootUrl}
baseUrl={baseUrl}
@ -106,7 +104,6 @@ export function NotebookRoutes(
notebook={graph}
unreads={props.unreads}
noteId={noteIdNum}
contacts={contacts}
association={props.association}
group={group}
s3={props.s3}

View File

@ -7,7 +7,7 @@ import { AsyncButton } from '~/views/components/AsyncButton';
export class Writers extends Component {
render() {
const { association, groups, contacts, api } = this.props;
const { association, groups, api } = this.props;
const resource = resourceFromPath(association?.group);
@ -40,7 +40,6 @@ export class Writers extends Component {
<Form>
<ShipSearch
groups={groups}
contacts={contacts}
id="ships"
label=""
maxLength={undefined}

View File

@ -11,9 +11,9 @@ import OverlaySigil from './OverlaySigil';
import { Sigil } from '~/logic/lib/sigil';
import GlobalApi from '~/logic/api/global';
import Timestamp from './Timestamp';
import useContactState from '~/logic/state/contacts';
interface AuthorProps {
contacts: Contacts;
ship: string;
date: number;
showImage?: boolean;
@ -25,9 +25,10 @@ interface AuthorProps {
// eslint-disable-next-line max-lines-per-function
export default function Author(props: AuthorProps): ReactElement {
const { contacts, ship = '', date, showImage, group } = props;
const { ship = '', date, showImage, group } = props;
const history = useHistory();
let contact;
const contacts = useContactState(state => state.contacts);
if (contacts) {
contact = `~${deSig(ship)}` in contacts ? contacts[`~${deSig(ship)}`] : null;
}

View File

@ -21,7 +21,6 @@ interface CommentItemProps {
pending?: boolean;
comment: GraphNode;
baseUrl: string;
contacts: Contacts;
unread: boolean;
name: string;
ship: string;
@ -30,7 +29,7 @@ interface CommentItemProps {
}
export function CommentItem(props: CommentItemProps): ReactElement {
const { ship, contacts, name, api, comment, group } = props;
const { ship, name, api, comment, group } = props;
const [, post] = getLatestCommentRevision(comment);
const disabled = props.pending || window.ship !== post?.author;
@ -47,7 +46,6 @@ export function CommentItem(props: CommentItemProps): ReactElement {
<Row bg="white" my={3}>
<Author
showImage
contacts={contacts}
ship={post?.author}
date={post?.['time-sent']}
unread={props.unread}
@ -73,7 +71,6 @@ export function CommentItem(props: CommentItemProps): ReactElement {
</Row>
<Box mb={2}>
<MentionText
contacts={contacts}
group={group}
content={post?.contents}
/>

View File

@ -21,7 +21,6 @@ interface CommentsProps {
ship: string;
editCommentId: string;
baseUrl: string;
contacts: Contacts;
api: GlobalApi;
group: Group;
}
@ -130,7 +129,6 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
<CommentItem
comment={comment}
key={idx.toString()}
contacts={props.contacts}
api={api}
name={name}
ship={ship}

View File

@ -29,7 +29,6 @@ interface InviteItemProps {
app?: string;
uid?: string;
api: GlobalApi;
contacts: Contacts;
}
export function InviteItem(props: InviteItemProps) {

View File

@ -6,18 +6,18 @@ import RichText from '~/views/components/RichText';
import { cite, useShowNickname, uxToHex } from '~/logic/lib/util';
import OverlaySigil from '~/views/components/OverlaySigil';
import { useHistory } from 'react-router-dom';
import useContactState from '~/logic/state/contacts';
interface MentionTextProps {
contact?: Contact;
contacts?: Contacts;
content: Content[];
group: Group;
}
export function MentionText(props: MentionTextProps) {
const { content, contacts, contact, group, ...rest } = props;
const { content, contact, group, ...rest } = props;
return (
<RichText contacts={contacts} contact={contact} group={group} {...rest}>
<RichText contact={contact} group={group} {...rest}>
{content.reduce((accum, c) => {
if ('text' in c) {
return accum + c.text;
@ -34,14 +34,14 @@ export function MentionText(props: MentionTextProps) {
export function Mention(props: {
contact: Contact;
contacts?: Contacts;
group: Group;
scrollWindow?: HTMLElement;
ship: string;
first?: Boolean;
}) {
const { contacts, ship, scrollWindow, first, ...rest } = props;
const { ship, scrollWindow, first, ...rest } = props;
let { contact } = props;
const contacts = useContactState(state => state.contacts);
contact = contact?.color ? contact : contacts?.[ship];
const history = useHistory();
const showNickname = useShowNickname(contact);

View File

@ -42,7 +42,7 @@ const RichText = React.memo(({ disableRemoteContent, ...props }) => (
linkReference: (linkProps) => {
const linkText = String(linkProps.children[0].props.children);
if (isValidPatp(linkText)) {
return <Mention contacts={props.contacts || {}} contact={props.contact || {}} group={props.group} ship={deSig(linkText)} />;
return <Mention contact={props.contact || {}} group={props.group} ship={deSig(linkText)} />;
}
return linkText;
},

View File

@ -24,6 +24,7 @@ import { Rolodex, Groups } from '@urbit/api';
import { DropdownSearch } from './DropdownSearch';
import { cite, deSig } from '~/logic/lib/util';
import { HoverBox } from './HoverBox';
import useContactState from '~/logic/state/contacts';
interface InviteSearchProps<I extends string> {
autoFocus?: boolean;
@ -121,9 +122,11 @@ export function ShipSearch<I extends string, V extends Value<I>>(
const pills = selected.slice(0, inputIdx.current);
const contacts = useContactState(state => state.contacts);
const [peers, nicknames] = useMemo(
() => getNicknameForShips(props.groups, props.contacts),
[props.contacts, props.groups]
() => getNicknameForShips(props.groups, contacts),
[contacts, props.groups]
);
const renderCandidate = useCallback(

View File

@ -16,10 +16,10 @@ import defaultApps from '~/logic/lib/default-apps';
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
import { Portal } from '../Portal';
import { Tile } from '~/types';
import useContactState from '~/logic/state/contacts';
interface OmniboxProps {
associations: Associations;
contacts: Contacts;
groups: Groups;
tiles: {
[app: string]: Tile;
@ -40,13 +40,14 @@ export function Omnibox(props: OmniboxProps) {
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<[] | [string, string]>([]);
const contactState = useContactState(state => state.contacts);
const contacts = useMemo(() => {
const maybeShip = `~${deSig(query)}`;
return ob.isValidPatp(maybeShip)
? { ...props.contacts, [maybeShip]: {} }
: props.contacts;
}, [props.contacts, query]);
? { ...contactState, [maybeShip]: {} }
: contactState;
}, [contactState, query]);
const index = useMemo(() => {
const selectedGroup = location.pathname.startsWith('/~landscape/ship/')
@ -257,7 +258,6 @@ export function Omnibox(props: OmniboxProps) {
selected={sel}
invites={props.invites}
notifications={props.notifications}
contacts={props.contacts}
/>
))}
</Box>
@ -265,7 +265,7 @@ export function Omnibox(props: OmniboxProps) {
})
}
</Box>;
}, [results, navigate, selected, props.contacts, props.notifications, props.invites]);
}, [results, navigate, selected, contactState, props.notifications, props.invites]);
return (
<Portal>

View File

@ -177,7 +177,7 @@ export function GraphPermissions(props: GraphPermissionsProps) {
vip={association.metadata.vip}
/>
</Col>
<ChannelWritePerms contacts={props.contacts} groups={props.groups} />
<ChannelWritePerms groups={props.groups} />
{association.metadata.module !== 'chat' && (
<Checkbox
id="readerComments"

View File

@ -34,7 +34,6 @@ export function ChannelWritePerms<
{values.writePerms === 'subset' && (
<ShipSearch
groups={props.groups}
contacts={props.contacts}
id="writers"
label=""
maxLength={undefined}

View File

@ -27,6 +27,7 @@ import '~/views/apps/publish/css/custom.css';
import { getGroupFromWorkspace } from '~/logic/lib/workspace';
import { GroupSummary } from './GroupSummary';
import { Workspace } from '~/types/workspace';
import useContactState from '~/logic/state/contacts';
type GroupsPaneProps = StoreState & {
baseUrl: string;
@ -35,7 +36,8 @@ type GroupsPaneProps = StoreState & {
};
export function GroupsPane(props: GroupsPaneProps) {
const { baseUrl, associations, groups, contacts, api, workspace } = props;
const { baseUrl, associations, groups, api, workspace } = props;
const contacts = useContactState(state => state.contacts);
const relativePath = (path: string) => baseUrl + path;
const groupPath = getGroupFromWorkspace(workspace);
@ -82,7 +84,6 @@ export function GroupsPane(props: GroupsPaneProps) {
association={groupAssociation!}
baseUrl={baseUrl}
groups={props.groups}
contacts={props.contacts}
workspace={workspace}
/>
</>
@ -181,7 +182,6 @@ export function GroupsPane(props: GroupsPaneProps) {
associations={associations}
groups={groups}
group={groupPath}
contacts={props.contacts}
workspace={workspace}
/>
{popovers(routeProps, baseUrl)}

View File

@ -115,7 +115,6 @@ export function InvitePopover(props: InvitePopoverProps) {
</Box>
<ShipSearch
groups={props.groups}
contacts={props.contacts}
id="ships"
label=""
autoFocus

View File

@ -178,13 +178,11 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactE
{(workspace?.type === 'home' || workspace?.type === 'messages') ? (
<ShipSearch
groups={props.groups}
contacts={props.contacts}
id="ships"
label="Invitees"
/>) : (
<ChannelWritePerms
groups={props.groups}
contacts={props.contacts}
/>
)}
<Box justifySelf="start">

View File

@ -30,6 +30,7 @@ import { Dropdown } from '~/views/components/Dropdown';
import GlobalApi from '~/logic/api/global';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import useLocalState from '~/logic/state/local';
import useContactState from '~/logic/state/contacts';
const TruncText = styled(Text)`
white-space: nowrap;
@ -103,7 +104,6 @@ const Tab = ({ selected, id, label, setSelected }) => (
);
export function Participants(props: {
contacts: Contacts;
group: Group;
association: Association;
api: GlobalApi;
@ -136,9 +136,10 @@ export function Participants(props: {
const adminCount = props.group.tags?.role?.admin?.size || 0;
const isInvite = 'invite' in props.group.policy;
const contacts = useContactState(state => state.contacts);
const [participants, pendingCount, memberCount] = getParticipants(
props.contacts,
contacts,
props.group
);

View File

@ -20,7 +20,6 @@ import { S3State } from '~/types';
export function PopoverRoutes(
props: {
baseUrl: string;
contacts: Contacts;
group: Group;
association: Association;
associations: Associations;
@ -134,7 +133,6 @@ export function PopoverRoutes(
{view === 'participants' && (
<Participants
group={props.group}
contacts={props.contacts}
association={props.association}
api={props.api}
/>

View File

@ -58,7 +58,6 @@ export function Resource(props: ResourceProps): ReactElement {
association={association}
group={props.groups?.[selectedGroup]}
groups={props.groups}
contacts={props.contacts}
api={props.api}
baseUrl={relativePath('')}
rootUrl={props.baseUrl}

View File

@ -11,6 +11,7 @@ import RichText from '~/views/components/RichText';
import GlobalApi from '~/logic/api/global';
import { isWriter } from '~/logic/lib/group';
import { getItemTitle } from '~/logic/lib/util';
import useContactState from '~/logic/state/contacts';
const TruncatedBox = styled(Box)`
white-space: pre;
@ -48,9 +49,11 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
let recipient = false;
const contacts = useContactState(state => state.contacts);
if (urbitOb.isValidPatp(title)) {
recipient = title;
title = (props.contacts?.[title]?.nickname) ? props.contacts[title].nickname : title;
title = (contacts?.[title]?.nickname) ? contacts[title].nickname : title;
}
const [, , ship, resource] = rid.split('/');

View File

@ -91,7 +91,6 @@ export function Sidebar(props: SidebarProps): ReactElement {
/>
<SidebarListHeader
associations={associations}
contacts={props.contacts}
baseUrl={props.baseUrl}
groups={props.groups}
initialValues={config}
@ -110,7 +109,6 @@ export function Sidebar(props: SidebarProps): ReactElement {
apps={props.apps}
baseUrl={props.baseUrl}
workspace={workspace}
contacts={props.contacts}
/>
</ScrollbarLessCol>
);

View File

@ -12,6 +12,7 @@ import useLocalState from '~/logic/state/local';
import { TUTORIAL_HOST, TUTORIAL_GROUP } from '~/logic/lib/tutorialModal';
import { SidebarAppConfigs, SidebarItemStatus } from './types';
import { Workspace } from '~/types/workspace';
import useContactState from '~/logic/state/contacts';
function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
switch (props.status) {
@ -84,14 +85,16 @@ export function SidebarItem(props: {
let img = null;
const contacts = useContactState(state => state.contacts);
if (urbitOb.isValidPatp(title)) {
if (props.contacts?.[title]?.avatar && !hideAvatars) {
img = <BaseImage src={props.contacts[title].avatar} width='16px' height='16px' borderRadius={2} />;
if (contacts?.[title]?.avatar && !hideAvatars) {
img = <BaseImage src={contacts[title].avatar} width='16px' height='16px' borderRadius={2} />;
} else {
img = <Sigil ship={title} color={`#${uxToHex(props.contacts?.[title]?.color || '0x0')}`} icon padding={2} size={16} />;
img = <Sigil ship={title} color={`#${uxToHex(contacts?.[title]?.color || '0x0')}`} icon padding={2} size={16} />;
}
if (props.contacts?.[title]?.nickname && !hideNicknames) {
title = props.contacts[title].nickname;
if (contacts?.[title]?.nickname && !hideNicknames) {
title = contacts[title].nickname;
}
} else {
img = <Box flexShrink={0} height={16} width={16} borderRadius={2} backgroundColor={`#${uxToHex(props?.association?.metadata?.color)}` || '#000000'} />;

View File

@ -77,7 +77,6 @@ export function SidebarList(props: {
apps={props.apps}
hideUnjoined={config.hideUnjoined}
groups={props.groups}
contacts={props.contacts}
workspace={workspace}
/>
);

View File

@ -87,7 +87,6 @@ export function SidebarListHeader(props: {
api={props.api}
history={props.history}
associations={props.associations}
contacts={props.contacts}
groups={props.groups}
workspace={props.workspace}
/>

View File

@ -49,7 +49,6 @@ export function Skeleton(props: SkeletonProps): ReactElement {
gridTemplateRows="100%"
>
<Sidebar
contacts={props.contacts}
api={props.api}
recentGroups={props.recentGroups}
selected={props.selected}

View File

@ -117,7 +117,6 @@ export default class Landscape extends Component<LandscapeProps, Record<string,
<NewGroup
associations={props.associations}
groups={props.groups}
contacts={props.contacts}
api={props.api}
{...routeProps}
/>
@ -141,7 +140,6 @@ export default class Landscape extends Component<LandscapeProps, Record<string,
<Box maxWidth="300px">
<JoinGroup
groups={props.groups}
contacts={props.contacts}
api={props.api}
autojoin={autojoin}
{...routeProps}