mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-09-21 15:38:59 +03:00
Merge pull request #5014 from urbit/lf/perf-sweep
interface: general performance sweep
This commit is contained in:
commit
df45d21128
@ -1,7 +1,7 @@
|
||||
import Urbit from '@urbit/http-api';
|
||||
const api = new Urbit('', '');
|
||||
api.ship = window.ship;
|
||||
api.verbose = true;
|
||||
// api.verbose = true;
|
||||
// @ts-ignore TODO window typings
|
||||
window.api = api;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DragEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { DragEvent, useCallback, useEffect, useState, useMemo } from 'react';
|
||||
|
||||
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
|
||||
const files: File[] = [];
|
||||
@ -43,7 +43,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
|
||||
}
|
||||
setDragging(true);
|
||||
},
|
||||
[setDragging]
|
||||
[]
|
||||
);
|
||||
|
||||
const onDrop = useCallback(
|
||||
@ -56,7 +56,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
|
||||
e.preventDefault();
|
||||
dragged(files, e);
|
||||
},
|
||||
[setDragging, dragged]
|
||||
[dragged]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback(
|
||||
@ -77,7 +77,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
|
||||
setDragging(false);
|
||||
}
|
||||
},
|
||||
[setDragging]
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -92,12 +92,12 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi
|
||||
};
|
||||
}, []);
|
||||
|
||||
const bind = {
|
||||
const bind = useMemo(() => ({
|
||||
onDragLeave,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onDragEnter
|
||||
};
|
||||
}), [onDragEnter, onDragOver, onDrop, onDragEnter]);
|
||||
|
||||
return { bind, dragging };
|
||||
return useMemo(() => ({ bind, dragging }), [bind, dragging]);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useMemo, useEffect, useState } from 'react';
|
||||
|
||||
function retrieve<T>(key: string, initial: T): T {
|
||||
const s = localStorage.getItem(key);
|
||||
@ -12,26 +12,16 @@ function retrieve<T>(key: string, initial: T): T {
|
||||
return initial;
|
||||
}
|
||||
|
||||
interface SetStateFunc<T> {
|
||||
(t: T): T;
|
||||
}
|
||||
// See microsoft/typescript#37663 for filed bug
|
||||
type SetState<T> = T extends any ? SetStateFunc<T> : never;
|
||||
export function useLocalStorageState<T>(key: string, initial: T): any {
|
||||
const [state, _setState] = useState(() => retrieve(key, initial));
|
||||
const [state, setState] = useState(() => retrieve(key, initial));
|
||||
|
||||
useEffect(() => {
|
||||
_setState(retrieve(key, initial));
|
||||
setState(retrieve(key, initial));
|
||||
}, [key]);
|
||||
|
||||
const setState = useCallback(
|
||||
(s: SetState<T>) => {
|
||||
const updated = typeof s === 'function' ? s(state) : s;
|
||||
_setState(updated);
|
||||
localStorage.setItem(key, JSON.stringify(updated));
|
||||
},
|
||||
[_setState, key, state]
|
||||
);
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, JSON.stringify(state));
|
||||
}, [state]);
|
||||
|
||||
return [state, setState] as const;
|
||||
return useMemo(() => [state, setState] as const, [state, setState]);
|
||||
}
|
||||
|
@ -523,3 +523,19 @@ export const favicon = () => {
|
||||
});
|
||||
return svg;
|
||||
};
|
||||
|
||||
export function binaryIndexOf(arr: BigInteger[], target: BigInteger): number | undefined {
|
||||
let leftBound = 0;
|
||||
let rightBound = arr.length - 1;
|
||||
while(leftBound <= rightBound) {
|
||||
const halfway = Math.floor((leftBound + rightBound) / 2);
|
||||
if(arr[halfway].greater(target)) {
|
||||
leftBound = halfway + 1;
|
||||
} else if (arr[halfway].lesser(target)) {
|
||||
rightBound = halfway - 1;
|
||||
} else {
|
||||
return halfway;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
import { patp2dec } from 'urbit-ob';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { Association, deSig, GraphNode, Graphs, FlatGraphs, resourceFromPath, ThreadGraphs, getGraph, getShallowChildren } from '@urbit/api';
|
||||
import { useCallback } from 'react';
|
||||
@ -275,7 +276,11 @@ export function useGraphTimesentMap(ship: string, name: string) {
|
||||
useCallback(s => s.graphTimesentMap[`${deSig(ship)}/${name}`], [ship, name])
|
||||
);
|
||||
}
|
||||
const emptyObject = {};
|
||||
|
||||
export function useGraphTimesent(key: string) {
|
||||
return useGraphState(useCallback(s => s.graphTimesentMap[key] || emptyObject, [key]), shallow);
|
||||
}
|
||||
export function useGraphForAssoc(association: Association) {
|
||||
const { resource } = association;
|
||||
const { ship, name } = resourceFromPath(resource);
|
||||
|
8
pkg/interface/src/logic/state/join.ts
Normal file
8
pkg/interface/src/logic/state/join.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { useOsDark } from './local';
|
||||
import { useTheme } from './settings';
|
||||
|
||||
export function useDark() {
|
||||
const osDark = useOsDark();
|
||||
const theme = useTheme();
|
||||
return theme === 'dark' || (osDark && theme === 'auto');
|
||||
}
|
@ -130,4 +130,9 @@ function withLocalState<P, S extends keyof LocalState, C extends React.Component
|
||||
});
|
||||
}
|
||||
|
||||
const selOsDark = (s: LocalState) => s.dark;
|
||||
export function useOsDark() {
|
||||
return useLocalState(selOsDark);
|
||||
}
|
||||
|
||||
export { useLocalState as default, withLocalState };
|
||||
|
@ -39,9 +39,9 @@ const useMetadataState = createState<MetadataState>(
|
||||
if('metadata-hook-update' in preview) {
|
||||
const newState = get();
|
||||
newState.set((s) => {
|
||||
s.previews[group] = preview['metadata-hook-update'];
|
||||
s.previews[group] = preview['metadata-hook-update'].preview;
|
||||
});
|
||||
return preview['metadata-hook-update'];
|
||||
return preview['metadata-hook-update'].preview;
|
||||
} else {
|
||||
throw 'no-permissions';
|
||||
}
|
||||
@ -82,9 +82,11 @@ export function useAssocForGroup(group: string) {
|
||||
);
|
||||
}
|
||||
|
||||
const selPreview = (s: MetadataState) => [s.previews, s.getPreview] as const;
|
||||
|
||||
export function usePreview(group: string) {
|
||||
const [error, setError] = useState(null);
|
||||
const [previews, getPreview] = useMetadataState(s => [s.previews, s.getPreview]);
|
||||
const [previews, getPreview] = useMetadataState(selPreview);
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
@ -100,7 +102,7 @@ export function usePreview(group: string) {
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
});
|
||||
}, [group]);
|
||||
|
||||
const preview = previews[group];
|
||||
|
||||
|
@ -124,4 +124,10 @@ export function useShortcut<T extends keyof ShortcutMapping>(
|
||||
return usePlainShortcut(key, cb);
|
||||
}
|
||||
|
||||
const selTheme = (s: SettingsState) => s.display.theme;
|
||||
|
||||
export function useTheme() {
|
||||
return useSettingsState(selTheme);
|
||||
}
|
||||
|
||||
export default useSettingsState;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import dark from '@tlon/indigo-dark';
|
||||
import light from '@tlon/indigo-light';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import shallow from 'zustand/shallow';
|
||||
import 'mousetrap-global-bind';
|
||||
import * as React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
@ -12,7 +13,6 @@ import gcpManager from '~/logic/lib/gcpManager';
|
||||
import { favicon, svgDataURL } from '~/logic/lib/util';
|
||||
import withState from '~/logic/lib/withState';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
@ -131,7 +131,7 @@ class App extends React.Component {
|
||||
render() {
|
||||
const theme = this.getTheme();
|
||||
|
||||
const ourContact = this.props.contacts[`~${this.ship}`] || null;
|
||||
const { ourContact } = this.props;
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ShortcutContextProvider>
|
||||
@ -173,12 +173,38 @@ class App extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
const WarmApp = process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||
|
||||
const selContacts = s => s.contacts[`~${window.ship}`];
|
||||
const selLocal = s => [s.set, s.omniboxShown, s.toggleOmnibox];
|
||||
const selSettings = s => [s.display, s.getAll];
|
||||
const selGraph = s => s.getShallowChildren;
|
||||
const selLaunch = s => [s.getRuntimeLag, s.getBaseHash];
|
||||
|
||||
const WithApp = React.forwardRef((props, ref) => {
|
||||
const ourContact = useContactState(selContacts);
|
||||
const [display, getAll] = useSettingsState(selSettings, shallow);
|
||||
const [setLocal, omniboxShown, toggleOmnibox] = useLocalState(selLocal);
|
||||
const getShallowChildren = useGraphState(selGraph);
|
||||
const [getRuntimeLag, getBaseHash] = useLaunchState(selLaunch, shallow);
|
||||
|
||||
return (
|
||||
<WarmApp
|
||||
ref={ref}
|
||||
ourContact={ourContact}
|
||||
display={display}
|
||||
getAll={getAll}
|
||||
set={setLocal}
|
||||
getShallowChildren={getShallowChildren}
|
||||
getRuntimeLag={getRuntimeLag}
|
||||
getBaseHash={getBaseHash}
|
||||
toggleOmnibox={toggleOmnibox}
|
||||
omniboxShown={omniboxShown}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
WarmApp.whyDidYouRender = true;
|
||||
|
||||
export default WithApp;
|
||||
|
||||
export default withState(process.env.NODE_ENV === 'production' ? App : hot(App), [
|
||||
[useGroupState],
|
||||
[useContactState],
|
||||
[useSettingsState, ['display', 'getAll']],
|
||||
[useLocalState],
|
||||
[useGraphState, ['getShallowChildren']],
|
||||
[useLaunchState, ['getRuntimeLag', 'getBaseHash']]
|
||||
]);
|
||||
|
@ -86,7 +86,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
);
|
||||
return `${url}\n~${msg.author}: `;
|
||||
},
|
||||
[association]
|
||||
[association.resource]
|
||||
);
|
||||
|
||||
const isAdmin = useMemo(
|
||||
@ -107,7 +107,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
if (newer) {
|
||||
const index = graph.peekLargest()?.[0];
|
||||
if (!index) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
await getYoungerSiblings(
|
||||
ship,
|
||||
@ -119,10 +119,12 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
} else {
|
||||
const index = graph.peekSmallest()?.[0];
|
||||
if (!index) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
await getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
||||
const done = expectedSize !== getCurrGraphSize(ship.slice(1), name);
|
||||
const currSize = getCurrGraphSize(ship.slice(1), name);
|
||||
console.log(currSize);
|
||||
const done = expectedSize !== currSize;
|
||||
return done;
|
||||
}
|
||||
}, [graph, resource]);
|
||||
@ -144,7 +146,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
const getPermalink = useCallback(
|
||||
(index: BigInteger) =>
|
||||
getPermalinkForGraph(association.group, resource, `/${index.toString()}`),
|
||||
[association]
|
||||
[association.resource]
|
||||
);
|
||||
|
||||
if (!graph) {
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
cite, daToUnix, useHovering, useShowNickname, uxToHex
|
||||
} from '~/logic/lib/util';
|
||||
import { useContact } from '~/logic/state/contact';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
import { useDark } from '~/logic/state/join';
|
||||
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
import ProfileOverlay from '~/views/components/ProfileOverlay';
|
||||
@ -53,16 +53,13 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
||||
</Row>
|
||||
);
|
||||
|
||||
export const MessageAuthor = ({
|
||||
export const MessageAuthor = React.memo<any>(({
|
||||
timestamp,
|
||||
msg,
|
||||
showOurContact,
|
||||
...props
|
||||
}) => {
|
||||
const osDark = useLocalState(state => state.dark);
|
||||
|
||||
const theme = useSettingsState(s => s.display.theme);
|
||||
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||
const dark = useDark();
|
||||
let contact: Contact | null = useContact(`~${msg.author}`);
|
||||
|
||||
const date = daToUnix(bigInt(msg.index.split('/').reverse()[0]));
|
||||
@ -177,7 +174,8 @@ export const MessageAuthor = ({
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
MessageAuthor.displayName = 'MessageAuthor';
|
||||
|
||||
type MessageProps = { timestamp: string; timestampHover: boolean; }
|
||||
& Pick<ChatMessageProps, 'msg' | 'transcluded' | 'showOurContact'>
|
||||
@ -392,6 +390,7 @@ interface ChatMessageProps {
|
||||
showOurContact: boolean;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
const emptyCallback = () => {};
|
||||
|
||||
function ChatMessage(props: ChatMessageProps) {
|
||||
let { highlighted } = props;
|
||||
@ -416,10 +415,10 @@ function ChatMessage(props: ChatMessageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const onReply = props?.onReply ?? (() => {});
|
||||
const onDelete = props?.onDelete ?? (() => {});
|
||||
const transcluded = props?.transcluded ?? 0;
|
||||
const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) ||
|
||||
const onReply = props?.onReply || emptyCallback;
|
||||
const onDelete = props?.onDelete || emptyCallback;
|
||||
const transcluded = props?.transcluded || 0;
|
||||
const renderSigil = props.renderSigil || (Boolean(nextMsg && msg.author !== nextMsg.author) ||
|
||||
!nextMsg
|
||||
);
|
||||
|
||||
@ -509,9 +508,9 @@ function ChatMessage(props: ChatMessageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
|
||||
export default React.memo(React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
|
||||
<ChatMessage {...props} innerRef={ref} />
|
||||
));
|
||||
)));
|
||||
|
||||
export const MessagePlaceholder = ({
|
||||
height,
|
||||
|
@ -6,7 +6,7 @@ import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'r
|
||||
import { useFileDrag } from '~/logic/lib/useDrag';
|
||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||
import { useOurContact } from '~/logic/state/contact';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import { useGraphTimesent } from '~/logic/state/graph';
|
||||
import ShareProfile from '~/views/apps/chat/components/ShareProfile';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||
@ -77,7 +77,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
||||
promptShare = [],
|
||||
fetchMessages
|
||||
} = props;
|
||||
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
||||
const graphTimesentMap = useGraphTimesent(id);
|
||||
const ourContact = useOurContact();
|
||||
const chatInput = useRef<NakedChatInput>();
|
||||
|
||||
@ -88,7 +88,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
||||
}
|
||||
(chatInput.current as NakedChatInput)?.uploadFiles(files);
|
||||
},
|
||||
[chatInput.current]
|
||||
[chatInput]
|
||||
);
|
||||
|
||||
const { bind, dragging } = useFileDrag(onFileDrag);
|
||||
@ -147,7 +147,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
||||
graphSize={graph.size}
|
||||
unreadCount={unreadCount}
|
||||
showOurContact={promptShare.length === 0 && !showBanner}
|
||||
pendingSize={Object.keys(graphTimesentMap[id] || {}).length}
|
||||
pendingSize={Object.keys(graphTimesentMap).length}
|
||||
onReply={onReply}
|
||||
onDelete={onDelete}
|
||||
dismissUnread={dismissUnread}
|
||||
@ -170,3 +170,5 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
ChatPane.whyDidYouRender = true;
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import React, { Component } from 'react';
|
||||
import VirtualScroller from '~/views/components/VirtualScroller';
|
||||
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import UnreadNotice from './UnreadNotice';
|
||||
|
||||
const IDLE_THRESHOLD = 64;
|
||||
@ -57,7 +57,7 @@ class ChatWindow extends Component<
|
||||
this.state = {
|
||||
fetchPending: false,
|
||||
idle: true,
|
||||
initialized: false,
|
||||
initialized: true,
|
||||
unreadIndex: bigInt.zero
|
||||
};
|
||||
|
||||
@ -72,14 +72,10 @@ class ChatWindow extends Component<
|
||||
|
||||
componentDidMount() {
|
||||
this.calculateUnreadIndex();
|
||||
setTimeout(() => {
|
||||
this.setState({ initialized: true }, () => {
|
||||
if(this.props.scrollTo) {
|
||||
this.virtualList!.scrollLocked = false;
|
||||
this.virtualList!.scrollToIndex(this.props.scrollTo);
|
||||
}
|
||||
});
|
||||
}, this.INITIALIZATION_MAX_TIME);
|
||||
if(this.props.scrollTo) {
|
||||
this.virtualList!.scrollLocked = false;
|
||||
this.virtualList!.scrollToIndex(this.props.scrollTo);
|
||||
}
|
||||
}
|
||||
|
||||
calculateUnreadIndex() {
|
||||
@ -205,15 +201,6 @@ class ChatWindow extends Component<
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
if (!this.state.initialized) {
|
||||
return (
|
||||
<MessagePlaceholder
|
||||
key={index.toString()}
|
||||
height='64px'
|
||||
index={index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
||||
const isLastMessage = index.eq(
|
||||
graph.peekLargest()?.[0] ?? bigInt.zero
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useModal } from '~/logic/lib/useModal';
|
||||
import useMetadataState, { usePreview } from '~/logic/state/metadata';
|
||||
@ -15,9 +15,10 @@ export function GroupLink(
|
||||
): ReactElement {
|
||||
const { resource, ...rest } = props;
|
||||
const name = resource.slice(6);
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
const joined = useMetadataState(
|
||||
useCallback(s => resource in s.associations.groups, [resource])
|
||||
);
|
||||
const history = useHistory();
|
||||
const joined = resource in associations.groups;
|
||||
|
||||
const { modal, showModal } = useModal({
|
||||
modal: <JoinGroup autojoin={name} />
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { cite, uxToHex } from '@urbit/api';
|
||||
import shallow from 'zustand/shallow';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
@ -20,7 +21,7 @@ import { useCopy } from '~/logic/lib/useCopy';
|
||||
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
|
||||
import { useShowNickname } from '~/logic/lib/util';
|
||||
import { useContact } from '~/logic/state/contact';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import useSettingsState, { SettingsState } from '~/logic/state/settings';
|
||||
import { Portal } from './Portal';
|
||||
import { ProfileStatus } from './ProfileStatus';
|
||||
import RichText from './RichText';
|
||||
@ -40,6 +41,8 @@ type ProfileOverlayProps = BoxProps & {
|
||||
color?: string;
|
||||
};
|
||||
|
||||
const selSettings = (s: SettingsState) => [s.calm.hideAvatars, s.calm.hideNicknames];
|
||||
|
||||
const ProfileOverlay = (props: ProfileOverlayProps) => {
|
||||
const {
|
||||
ship,
|
||||
@ -53,8 +56,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
||||
const history = useHistory();
|
||||
const outerRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const hideAvatars = useSettingsState(state => state.calm.hideAvatars);
|
||||
const hideNicknames = useSettingsState(state => state.calm.hideNicknames);
|
||||
const [hideAvatars, hideNicknames] = useSettingsState(selSettings, shallow);
|
||||
const isOwn = useMemo(() => window.ship === ship, [ship]);
|
||||
const { copyDisplay, doCopy, didCopy } = useCopy(`~${ship}`);
|
||||
|
||||
@ -128,7 +130,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
||||
|
||||
return (
|
||||
<Box ref={outerRef} {...rest} onClick={setOpen} cursor="pointer">
|
||||
<VisibilitySensor onChange={setVisible}>
|
||||
<VisibilitySensor active={open} onChange={setVisible}>
|
||||
{children}
|
||||
</VisibilitySensor>
|
||||
{ open && (
|
||||
|
@ -47,6 +47,7 @@ class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
|
||||
private fetchController: AbortController | undefined;
|
||||
containerRef: HTMLDivElement | null = null;
|
||||
private saving = false;
|
||||
private isOembed = false;
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@ -60,6 +61,7 @@ class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
|
||||
this.wrapInLink = this.wrapInLink.bind(this);
|
||||
this.onError = this.onError.bind(this);
|
||||
this.toggleArrow = this.toggleArrow.bind(this);
|
||||
this.isOembed = hasProvider(props.url);
|
||||
}
|
||||
|
||||
save = () => {
|
||||
@ -204,7 +206,6 @@ return;
|
||||
const isImage = IMAGE_REGEX.test(url);
|
||||
const isAudio = AUDIO_REGEX.test(url);
|
||||
const isVideo = VIDEO_REGEX.test(url);
|
||||
const isOembed = hasProvider(url);
|
||||
|
||||
const isTranscluded = () => {
|
||||
return transcluded;
|
||||
@ -315,7 +316,7 @@ return;
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
} else if (isOembed && remoteContentPolicy.oembedShown) {
|
||||
} else if (this.isOembed && remoteContentPolicy.oembedShown) {
|
||||
if (!this.state.embed || this.state.embed?.html === '') {
|
||||
this.loadOembed();
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ export const Content = (props) => {
|
||||
history.goBack();
|
||||
}, [history.goBack]));
|
||||
|
||||
|
||||
const [hasProtocol, setHasProtocol] = useLocalStorageState(
|
||||
'registeredProtocol', false
|
||||
);
|
||||
@ -78,16 +77,9 @@ export const Content = (props) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path='/~landscape'
|
||||
render={p => (
|
||||
<Landscape
|
||||
location={p.location}
|
||||
match={p.match}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path='/~landscape'>
|
||||
<Landscape />
|
||||
</Route>
|
||||
<Route
|
||||
path="/~profile"
|
||||
render={ p => (
|
||||
|
@ -121,7 +121,6 @@ function stitchInline(a: any, b: any) {
|
||||
}
|
||||
const lastParaIdx = a.children.length - 1;
|
||||
const last = a.children[lastParaIdx];
|
||||
console.log(last);
|
||||
if (last?.children) {
|
||||
const ros = {
|
||||
...a,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AppName, readGroup } from '@urbit/api';
|
||||
import { readGroup } from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
@ -27,10 +27,10 @@ import { Resource } from './Resource';
|
||||
import { Skeleton } from './Skeleton';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
type GroupsPaneProps = {
|
||||
interface GroupsPaneProps {
|
||||
baseUrl: string;
|
||||
workspace: Workspace;
|
||||
};
|
||||
}
|
||||
|
||||
export function GroupsPane(props: GroupsPaneProps) {
|
||||
const { baseUrl, workspace } = props;
|
||||
@ -114,8 +114,6 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
string
|
||||
>;
|
||||
|
||||
const appName = app as AppName;
|
||||
|
||||
const resource = `/ship/${host}/${name}`;
|
||||
const association = associations.graph[resource];
|
||||
const resourceUrl = `${baseUrl}/resource/${app}${resource}`;
|
||||
@ -129,12 +127,11 @@ export function GroupsPane(props: GroupsPaneProps) {
|
||||
mobileHide
|
||||
recentGroups={recentGroups}
|
||||
selected={resource}
|
||||
selectedApp={appName}
|
||||
{...props}
|
||||
baseUrl={resourceUrl}
|
||||
>
|
||||
<Resource
|
||||
{...props}
|
||||
workspace={props.workspace}
|
||||
association={association}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
|
@ -13,11 +13,11 @@ import { PublishResource } from '~/views/apps/publish/PublishResource';
|
||||
import { ChannelPopoverRoutes } from './ChannelPopoverRoutes';
|
||||
import { ResourceSkeleton } from './ResourceSkeleton';
|
||||
|
||||
type ResourceProps = {
|
||||
interface ResourceProps {
|
||||
association: Association;
|
||||
baseUrl: string;
|
||||
workspace: Workspace;
|
||||
};
|
||||
}
|
||||
|
||||
export function Resource(props: ResourceProps): ReactElement {
|
||||
const { association } = props;
|
||||
|
@ -106,7 +106,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
canWrite = isOwn;
|
||||
}
|
||||
|
||||
const BackLink = () => (
|
||||
const backLink = (
|
||||
<Box
|
||||
borderRight={1}
|
||||
borderRightColor='gray'
|
||||
@ -123,7 +123,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
</Box>
|
||||
);
|
||||
|
||||
const Title = () => (
|
||||
const titleText = (
|
||||
<Text
|
||||
mono={urbitOb.isValidPatp(title)}
|
||||
fontSize={2}
|
||||
@ -143,7 +143,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
</Text>
|
||||
);
|
||||
|
||||
const Description = () => (
|
||||
const description = (
|
||||
<TruncatedText
|
||||
display={['none','inline']}
|
||||
mono={workspace === '/messages' && !association?.metadata?.description}
|
||||
@ -160,9 +160,8 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
</TruncatedText>
|
||||
);
|
||||
|
||||
const ExtraControls = () => {
|
||||
if (workspace === '/messages' && isOwn && !resource.startsWith('dm-')) {
|
||||
return (
|
||||
const extraControls =
|
||||
(workspace === '/messages' && isOwn && !resource.startsWith('dm-')) ? (
|
||||
<Dropdown
|
||||
flexShrink={0}
|
||||
dropWidth='300px'
|
||||
@ -186,21 +185,15 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
+ Add Ship
|
||||
</Text>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
if (canWrite) {
|
||||
return (
|
||||
) : canWrite ? (
|
||||
<Link to={resourcePath('/new')}>
|
||||
<Text bold pr='3' color='blue'>
|
||||
+ New Post
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
) : null;
|
||||
|
||||
const MenuControl = () => (
|
||||
const menuControl = (
|
||||
<Link to={`${baseUrl}/settings`}>
|
||||
<Icon icon='Menu' color='gray' pr={2} />
|
||||
</Link>
|
||||
@ -229,9 +222,9 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
||||
flexShrink={0}
|
||||
>
|
||||
<BackLink />
|
||||
<Title />
|
||||
<Description />
|
||||
{backLink}
|
||||
{titleText}
|
||||
{description}
|
||||
</Box>
|
||||
<Box
|
||||
ml={3}
|
||||
@ -240,8 +233,8 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
flexShrink={0}
|
||||
ref={actionsRef}
|
||||
>
|
||||
{ExtraControls()}
|
||||
<MenuControl />
|
||||
{extraControls}
|
||||
{menuControl}
|
||||
</Box>
|
||||
</Box>
|
||||
{children}
|
||||
|
@ -12,7 +12,7 @@ import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||
import { GroupSwitcher } from '../GroupSwitcher';
|
||||
import { SidebarList } from './SidebarList';
|
||||
import { SidebarListHeader } from './SidebarListHeader';
|
||||
import { SidebarAppConfigs, SidebarListConfig } from './types';
|
||||
import { SidebarListConfig } from './types';
|
||||
|
||||
const ScrollbarLessCol = styled(Col)`
|
||||
scrollbar-width: none !important;
|
||||
@ -25,8 +25,6 @@ const ScrollbarLessCol = styled(Col)`
|
||||
interface SidebarProps {
|
||||
recentGroups: string[];
|
||||
selected?: string;
|
||||
selectedGroup?: string;
|
||||
apps: SidebarAppConfigs;
|
||||
baseUrl: string;
|
||||
mobileHide?: boolean;
|
||||
workspace: Workspace;
|
||||
@ -84,7 +82,6 @@ export function Sidebar(props: SidebarProps): ReactElement | null {
|
||||
config={config}
|
||||
selected={selected}
|
||||
group={groupPath}
|
||||
apps={props.apps}
|
||||
baseUrl={props.baseUrl}
|
||||
workspace={workspace}
|
||||
/>
|
||||
|
@ -12,9 +12,30 @@ import useContactState, { useContact } from '~/logic/state/contact';
|
||||
import { getItemTitle, getModuleIcon, uxToHex } from '~/logic/lib/util';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import Dot from '~/views/components/Dot';
|
||||
import { SidebarAppConfigs } from './types';
|
||||
import { useHarkDm } from '~/logic/state/hark';
|
||||
import useHarkState, { useHarkDm } from '~/logic/state/hark';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
|
||||
function useAssociationStatus(resource: string) {
|
||||
const [, , ship, name] = resource.split('/');
|
||||
const graphKey = `${ship.slice(1)}/${name}`;
|
||||
const isSubscribed = useGraphState(s => s.graphKeys.has(graphKey));
|
||||
const { unreads, notifications } = useHarkState(
|
||||
s => s.unreads.graph?.[resource]?.['/'] || { unreads: 0, notifications: 0, last: 0 }
|
||||
);
|
||||
const hasNotifications =
|
||||
(typeof notifications === 'number' && notifications > 0) ||
|
||||
(typeof notifications === 'object' && notifications.length);
|
||||
const hasUnread =
|
||||
typeof unreads === 'number' ? unreads > 0 : unreads?.size ?? 0 > 0;
|
||||
return hasNotifications
|
||||
? 'notification'
|
||||
: hasUnread
|
||||
? 'unread'
|
||||
: isSubscribed
|
||||
? undefined
|
||||
: 'unsubscribed';
|
||||
}
|
||||
|
||||
function SidebarItemBase(props: {
|
||||
to: string;
|
||||
@ -36,7 +57,11 @@ function SidebarItemBase(props: {
|
||||
isSynced = false,
|
||||
mono = false
|
||||
} = props;
|
||||
const color = isSynced ? (hasUnread || hasNotification) ? 'black' : 'gray' : 'lightGray';
|
||||
const color = isSynced
|
||||
? hasUnread || hasNotification
|
||||
? 'black'
|
||||
: 'gray'
|
||||
: 'lightGray';
|
||||
|
||||
const fontWeight = hasUnread || hasNotification ? '500' : 'normal';
|
||||
|
||||
@ -95,17 +120,18 @@ function SidebarItemBase(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarDmItem(props: {
|
||||
export const SidebarDmItem = React.memo((props: {
|
||||
ship: string;
|
||||
selected?: boolean;
|
||||
workspace: Workspace;
|
||||
}) {
|
||||
}) => {
|
||||
const { ship, selected = false } = props;
|
||||
const contact = useContact(ship);
|
||||
const { hideAvatars, hideNicknames } = useSettingsState(s => s.calm);
|
||||
const title = (!hideNicknames && contact?.nickname)
|
||||
? contact?.nickname
|
||||
: (cite(ship) ?? ship);
|
||||
const { hideAvatars, hideNicknames } = useSettingsState(s => s.calm);
|
||||
const title =
|
||||
!hideNicknames && contact?.nickname
|
||||
? contact?.nickname
|
||||
: cite(ship) ?? ship;
|
||||
const { unreads } = useHarkDm(ship) || { unreads: 0 };
|
||||
const img =
|
||||
contact?.avatar && !hideAvatars ? (
|
||||
@ -139,17 +165,15 @@ export function SidebarDmItem(props: {
|
||||
{img}
|
||||
</SidebarItemBase>
|
||||
);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
export function SidebarAssociationItem(props: {
|
||||
export const SidebarAssociationItem = React.memo((props: {
|
||||
hideUnjoined: boolean;
|
||||
association: Association;
|
||||
path: string;
|
||||
selected: boolean;
|
||||
apps: SidebarAppConfigs;
|
||||
workspace: Workspace;
|
||||
}) {
|
||||
const { association, path, selected, apps } = props;
|
||||
}) => {
|
||||
const { association, selected } = props;
|
||||
const title = getItemTitle(association) || '';
|
||||
const appName = association?.['app-name'];
|
||||
let mod = appName;
|
||||
@ -158,7 +182,7 @@ export function SidebarAssociationItem(props: {
|
||||
}
|
||||
const rid = association?.resource;
|
||||
const groupPath = association?.group;
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const group = useGroupState(state => state.groups[groupPath]);
|
||||
const { hideNicknames } = useSettingsState(s => s.calm);
|
||||
const contacts = useContactState(s => s.contacts);
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null);
|
||||
@ -167,13 +191,9 @@ export function SidebarAssociationItem(props: {
|
||||
groupPath === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||
anchorRef
|
||||
);
|
||||
const app = apps[appName];
|
||||
const isUnmanaged = groups?.[groupPath]?.hidden || false;
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
const isUnmanaged = group?.hidden || false;
|
||||
const DM = isUnmanaged && props.workspace?.type === 'messages';
|
||||
const itemStatus = app.getStatus(path);
|
||||
const itemStatus = useAssociationStatus(rid);
|
||||
const hasNotification = itemStatus === 'notification';
|
||||
const hasUnread = itemStatus === 'unread';
|
||||
const isSynced = itemStatus !== 'unsubscribed';
|
||||
@ -194,7 +214,11 @@ export function SidebarAssociationItem(props: {
|
||||
}
|
||||
|
||||
const participantNames = (str: string) => {
|
||||
const color = isSynced ? (hasUnread || hasNotification) ? 'black' : 'gray' : 'lightGray';
|
||||
const color = isSynced
|
||||
? hasUnread || hasNotification
|
||||
? 'black'
|
||||
: 'gray'
|
||||
: 'lightGray';
|
||||
if (_.includes(str, ',') && _.startsWith(str, '~')) {
|
||||
const names = _.split(str, ', ');
|
||||
return names.map((name, idx) => {
|
||||
@ -209,9 +233,7 @@ export function SidebarAssociationItem(props: {
|
||||
return (
|
||||
<Text key={name} mono bold={hasUnread} color={color}>
|
||||
{name}
|
||||
<Text color={color}>
|
||||
{idx + 1 != names.length ? ', ' : null}
|
||||
</Text>
|
||||
<Text color={color}>{idx + 1 != names.length ? ', ' : null}</Text>
|
||||
</Text>
|
||||
);
|
||||
} else {
|
||||
@ -230,9 +252,7 @@ export function SidebarAssociationItem(props: {
|
||||
hasUnread={hasUnread}
|
||||
isSynced={isSynced}
|
||||
title={
|
||||
DM && !urbitOb.isValidPatp(title)
|
||||
? participantNames(title)
|
||||
: title
|
||||
DM && !urbitOb.isValidPatp(title) ? participantNames(title) : title
|
||||
}
|
||||
hasNotification={hasNotification}
|
||||
>
|
||||
@ -255,4 +275,4 @@ export function SidebarAssociationItem(props: {
|
||||
)}
|
||||
</SidebarItemBase>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
|
||||
import useGraphState, { useInbox } from '~/logic/state/graph';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { alphabeticalOrder, getResourcePath, modulo } from '~/logic/lib/util';
|
||||
import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types';
|
||||
import { SidebarListConfig, SidebarSort } from './types';
|
||||
import { Workspace } from '~/types/workspace';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import { useHistory } from 'react-router';
|
||||
@ -14,8 +14,7 @@ import { useShortcut } from '~/logic/state/settings';
|
||||
|
||||
function sidebarSort(
|
||||
associations: AppAssociations,
|
||||
apps: SidebarAppConfigs,
|
||||
inboxUnreads: Record<string, UnreadStats>
|
||||
unreads: Record<string, Record<string, UnreadStats>>
|
||||
): Record<SidebarSort, (a: string, b: string) => number> {
|
||||
const alphabetical = (a: string, b: string) => {
|
||||
const aAssoc = associations[a];
|
||||
@ -29,16 +28,16 @@ function sidebarSort(
|
||||
const lastUpdated = (a: string, b: string) => {
|
||||
const aAssoc = associations[a];
|
||||
const bAssoc = associations[b];
|
||||
const aAppName = aAssoc?.['app-name'];
|
||||
const bAppName = bAssoc?.['app-name'];
|
||||
const aResource = aAssoc.resource;
|
||||
const bResource = bAssoc.resource;
|
||||
|
||||
const aUpdated = a.startsWith('~')
|
||||
? (inboxUnreads?.[`/${patp2dec(a)}`]?.last || 0)
|
||||
: (apps[aAppName]?.lastUpdated(a) || 0);
|
||||
? (unreads?.[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(a)}`]?.last || 0)
|
||||
: ((unreads?.[aResource]?.['/']?.last) || 0);
|
||||
|
||||
const bUpdated = b.startsWith('~')
|
||||
? (inboxUnreads?.[`/${patp2dec(b)}`]?.last || 0)
|
||||
: (apps[bAppName]?.lastUpdated(b) || 0);
|
||||
? (unreads?.[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(b)}`]?.last || 0)
|
||||
: ((unreads?.[bResource]?.['/']?.last) || 0);
|
||||
|
||||
return bUpdated - aUpdated || alphabetical(a, b);
|
||||
};
|
||||
@ -86,7 +85,6 @@ function getItems(associations: Associations, workspace: Workspace, inbox: Graph
|
||||
}
|
||||
|
||||
export function SidebarList(props: {
|
||||
apps: SidebarAppConfigs;
|
||||
config: SidebarListConfig;
|
||||
baseUrl: string;
|
||||
group?: string;
|
||||
@ -96,11 +94,11 @@ export function SidebarList(props: {
|
||||
const { selected, config, workspace } = props;
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
const inbox = useInbox();
|
||||
const unreads = useHarkState(s => s.unreads.graph?.[`/ship/~${window.ship}/dm-inbox`]);
|
||||
const unreads = useHarkState(s => s.unreads.graph);
|
||||
const graphKeys = useGraphState(s => s.graphKeys);
|
||||
|
||||
const ordered = getItems(associations, workspace, inbox)
|
||||
.sort(sidebarSort(associations.graph, props.apps, unreads)[config.sortBy]);
|
||||
.sort(sidebarSort(associations.graph, unreads)[config.sortBy]);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
@ -139,10 +137,8 @@ export function SidebarList(props: {
|
||||
return pathOrShip.startsWith('/') ? (
|
||||
<SidebarAssociationItem
|
||||
key={pathOrShip}
|
||||
path={pathOrShip}
|
||||
selected={pathOrShip === selected}
|
||||
association={associations.graph[pathOrShip]}
|
||||
apps={props.apps}
|
||||
hideUnjoined={config.hideUnjoined}
|
||||
workspace={workspace}
|
||||
/>
|
||||
|
@ -1,39 +1,24 @@
|
||||
import React, { ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import React, { ReactElement, ReactNode, useCallback, useState } from 'react';
|
||||
import { Sidebar } from './Sidebar/Sidebar';
|
||||
import { AppName } from '@urbit/api';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { Workspace } from '~/types/workspace';
|
||||
import { Body } from '~/views/components/Body';
|
||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import { useShortcut } from '~/logic/state/settings';
|
||||
import { useGraphModule } from './Sidebar/Apps';
|
||||
|
||||
interface SkeletonProps {
|
||||
children: ReactNode;
|
||||
recentGroups: string[];
|
||||
selected?: string;
|
||||
selectedApp?: AppName;
|
||||
baseUrl: string;
|
||||
mobileHide?: boolean;
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export function Skeleton(props: SkeletonProps): ReactElement {
|
||||
export const Skeleton = React.memo((props: SkeletonProps): ReactElement => {
|
||||
const [sidebar, setSidebar] = useState(true);
|
||||
useShortcut('hideSidebar', useCallback(() => {
|
||||
setSidebar(s => !s);
|
||||
}, []));
|
||||
const graphs = useGraphState(state => state.graphs);
|
||||
const graphKeys = useGraphState(state => state.graphKeys);
|
||||
const unreads = useHarkState(state => state.unreads);
|
||||
const graphConfig = useGraphModule(graphKeys, graphs, unreads.graph);
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
graph: graphConfig
|
||||
}),
|
||||
[graphConfig]
|
||||
);
|
||||
|
||||
return !sidebar ? (<Body> {props.children} </Body>) : (
|
||||
<Body
|
||||
@ -47,7 +32,6 @@ export function Skeleton(props: SkeletonProps): ReactElement {
|
||||
<Sidebar
|
||||
recentGroups={props.recentGroups}
|
||||
selected={props.selected}
|
||||
apps={config}
|
||||
baseUrl={props.baseUrl}
|
||||
mobileHide={props.mobileHide}
|
||||
workspace={props.workspace}
|
||||
@ -56,4 +40,4 @@ export function Skeleton(props: SkeletonProps): ReactElement {
|
||||
{props.children}
|
||||
</Body>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ import { GroupsPane } from './components/GroupsPane';
|
||||
import { JoinGroup } from './components/JoinGroup';
|
||||
import { NewGroup } from './components/NewGroup';
|
||||
import './css/custom.css';
|
||||
import _ from 'lodash';
|
||||
|
||||
moment.updateLocale('en', {
|
||||
relativeTime : {
|
||||
@ -32,7 +33,12 @@ moment.updateLocale('en', {
|
||||
}
|
||||
});
|
||||
|
||||
export default function Landscape(props) {
|
||||
const makeGroupWorkspace = _.memoize((group: string): Workspace => ({ type: 'group', group }));
|
||||
|
||||
const homeWorkspace: Workspace = { type: 'home' };
|
||||
const messagesWorkspace: Workspace = { type: 'messages' };
|
||||
|
||||
export default function Landscape() {
|
||||
const notificationsCount = useHarkState(s => s.notificationsCount);
|
||||
|
||||
return (
|
||||
@ -49,40 +55,26 @@ export default function Landscape(props) {
|
||||
} = routeProps.match.params as Record<string, string>;
|
||||
const groupPath = `/ship/${host}/${name}`;
|
||||
const baseUrl = `/~landscape${groupPath}`;
|
||||
const ws: Workspace = { type: 'group', group: groupPath };
|
||||
const ws: Workspace = makeGroupWorkspace(groupPath);
|
||||
|
||||
return (
|
||||
<GroupsPane workspace={ws} baseUrl={baseUrl} {...props} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path="/~landscape/home"
|
||||
render={() => {
|
||||
const ws: Workspace = { type: 'home' };
|
||||
return (
|
||||
<GroupsPane workspace={ws} baseUrl="/~landscape/home" {...props} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path="/~landscape/messages"
|
||||
render={() => {
|
||||
const ws: Workspace = { type: 'messages' };
|
||||
return (
|
||||
<GroupsPane workspace={ws} baseUrl="/~landscape/messages" {...props} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path="/~landscape/new"
|
||||
render={() => {
|
||||
return (
|
||||
<Body>
|
||||
<Box maxWidth="300px">
|
||||
<NewGroup />
|
||||
</Box>
|
||||
</Body>
|
||||
<GroupsPane workspace={ws} baseUrl={baseUrl} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path="/~landscape/home">
|
||||
<GroupsPane workspace={homeWorkspace} baseUrl="/~landscape/home" />
|
||||
</Route>
|
||||
<Route path="/~landscape/messages">
|
||||
<GroupsPane workspace={messagesWorkspace} baseUrl="/~landscape/messages" />
|
||||
</Route>
|
||||
<Route path="/~landscape/new">
|
||||
<Body>
|
||||
<Box maxWidth="300px">
|
||||
<NewGroup />
|
||||
</Box>
|
||||
</Body>
|
||||
</Route>
|
||||
<Route path="/~landscape/join/:ship?/:name?"
|
||||
render={(routeProps) => {
|
||||
const { ship, name } = routeProps.match.params;
|
||||
@ -103,4 +95,3 @@ export default function Landscape(props) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,10 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
||||
}
|
||||
|
||||
get size() {
|
||||
return Object.keys(this.root).length;
|
||||
if(this.cachedIter) {
|
||||
return this.cachedIter.length;
|
||||
}
|
||||
return this.generateCachedIter().length;
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user