interface: update hark callsites & state

This commit is contained in:
Liam Fitzgerald 2021-09-06 14:05:20 +10:00
parent c2cc13d96a
commit 12b4e4c59b
14 changed files with 256 additions and 336 deletions

View File

@ -554,3 +554,14 @@ export async function jsonFetch<T>(info: RequestInfo, init?: RequestInit): Promi
export function clone<T>(a: T) {
return JSON.parse(JSON.stringify(a)) as T;
}
export function toHarkPath(path: string, index = '') {
return `/graph/${path.slice(6)}${index}`;
}
export function toHarkPlace(graph: string, index = '') {
return {
desk: (window as any).desk,
path: toHarkPath(graph, index)
};
}

View File

@ -1,13 +1,11 @@
import {
NotificationContents,
NotifIndex,
Timebox
HarkPlace,
Timebox,
HarkStats
} from '@urbit/api';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import _ from 'lodash';
import { compose } from 'lodash/fp';
import { makePatDa } from '~/logic/lib/util';
import { describeNotification, getReferent } from '../lib/hark';
import { BaseState } from '../state/base';
import { HarkState as State } from '../state/hark';
@ -97,18 +95,21 @@ function readAll(json: any, state: HarkState): HarkState {
return state;
}
function removeGraph(json: any, state: HarkState): HarkState {
const data = _.get(json, 'remove-graph');
if(data) {
delete state.unreads.graph[data];
}
return state;
const emptyStats = () => ({
each: [],
count: 0,
last: 0
});
function updateNotificationStats(state: HarkState, place: HarkPlace, f: (s: HarkStats) => Partial<HarkStats>) {
const old = state.unreads?.[place.path] || emptyStats();
state.unreads[place.path] = { ...old, ...f(old) };
}
function seenIndex(json: any, state: HarkState): HarkState {
const data = _.get(json, 'seen-index');
if(data) {
updateNotificationStats(state, data.index, 'last', () => data.time);
updateNotificationStats(state, data, s => ({ last: Date.now() }));
}
return state;
}
@ -116,7 +117,8 @@ function seenIndex(json: any, state: HarkState): HarkState {
function readEach(json: any, state: HarkState): HarkState {
const data = _.get(json, 'read-each');
if (data) {
updateUnreads(state, data.index, u => u.delete(data.target));
const { place, path } = data;
updateNotificationStats(state, place, s => ({ each: s.each.filter(e => e !== path) }));
}
return state;
}
@ -124,7 +126,8 @@ function readEach(json: any, state: HarkState): HarkState {
function readSince(json: any, state: HarkState): HarkState {
const data = _.get(json, 'read-count');
if(data) {
updateUnreadCount(state, data, () => 0);
console.log(data);
updateNotificationStats(state, data, s => ({ count: 0 }));
}
return state;
}
@ -132,7 +135,9 @@ function readSince(json: any, state: HarkState): HarkState {
function unreadSince(json: any, state: HarkState): HarkState {
const data = _.get(json, 'unread-count');
if (data) {
updateUnreadCount(state, data.index, u => u + 1);
console.log(data);
const { inc, count, place } = data;
updateNotificationStats(state, place, s => ({ count: inc ? s.count + count : s.count - count }));
}
return state;
}
@ -140,34 +145,17 @@ function unreadSince(json: any, state: HarkState): HarkState {
function unreadEach(json: any, state: HarkState): HarkState {
const data = _.get(json, 'unread-each');
if(data) {
updateUnreads(state, data.index, us => us.add(data.target));
const { place, path } = data;
updateNotificationStats(state, place, s => ({ each: [...s.each, path] }));
}
return state;
}
function unreads(json: any, state: HarkState): HarkState {
const data = _.get(json, 'unreads');
if(data) {
clearState(state);
data.forEach(({ index, stats }) => {
const { unreads, notifications, last } = stats;
updateNotificationStats(state, index, 'last', () => last);
if(index.graph.graph === '/ship/~hastuc-dibtux/test-book-7531') {
console.log(index, stats);
}
_.each(notifications, ({ time, index }) => {
if(!time) {
addNotificationToUnread(state, index);
}
});
if('count' in unreads) {
updateUnreadCount(state, index, (u = 0) => u + unreads.count);
} else {
updateUnreads(state, index, s => new Set());
unreads.each.forEach((u: string) => {
updateUnreads(state, index, s => s.add(u));
});
}
function allStats(json: any, state: HarkState): HarkState {
if('all-stats' in json) {
const data = json['all-stats'];
data.forEach(({ place, stats }) => {
state.unreads[place.path] = stats;
});
}
return state;
@ -183,10 +171,7 @@ function clearState(state: HarkState): HarkState {
mentions: false,
watching: []
},
unreads: {
graph: {},
group: {}
},
unreads: {},
notificationsCount: 0,
unreadNotes: {}
};
@ -195,98 +180,6 @@ function clearState(state: HarkState): HarkState {
return state;
}
function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number): HarkState {
if(!('graph' in index)) {
return state;
}
const property = [index.graph.graph, index.graph.index, 'unreads'];
const curr = _.get(state.unreads.graph, property, 0);
if(typeof curr !== 'number') {
return state;
}
const newCount = count(curr);
_.set(state.unreads.graph, property, newCount);
return state;
}
function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>) => void): HarkState {
if(!('graph' in index)) {
return state;
}
const unreads: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
f(unreads);
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
return state;
}
function addNotificationToUnread(state: HarkState, index: NotifIndex) {
if('graph' in index) {
const path = [index.graph.graph, index.graph.index, 'notifications'];
const curr = _.get(state.unreads.graph, path, []);
_.set(state.unreads.graph, path,
[
...curr.filter((c) => {
return !(notifIdxEqual(c.index, index));
}),
{ index }
]
);
} else if ('group' in index) {
const path = [index.group.group, 'notifications'];
const curr = _.get(state.unreads.group, path, []);
_.set(state.unreads.group, path,
[
...curr.filter(c => !notifIdxEqual(c.index, index)),
{ index }
]
);
}
}
function removeNotificationFromUnread(state: HarkState, index: NotifIndex) {
if('graph' in index) {
const path = [index.graph.graph, index.graph.index, 'notifications'];
const curr = _.get(state.unreads.graph, path, []);
_.set(state.unreads.graph, path, curr.filter(c => !notifIdxEqual(c.index, index)));
} else if ('group' in index) {
const path = [index.group.group, 'notifications'];
const curr = _.get(state.unreads.group, path, []);
_.set(state.unreads.group, path, curr.filter(c => !notifIdxEqual(c.index, index)));
}
}
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number, notify = false) {
if('graph' in index) {
const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
} else if('group' in index) {
const curr: any = _.get(state.unreads.group, [index.group.group, statField], 0);
_.set(state.unreads.group, [index.group.group, statField], f(curr));
}
}
function added(json: any, state: HarkState): HarkState {
const data = _.get(json, 'added', false);
if (data) {
const { index, notification } = data;
const [fresh] = _.partition(state.unreadNotes, ({ index: idx }) => !notifIdxEqual(index, idx));
state.unreadNotes = [...fresh, { index, notification }];
if ('Notification' in window && !state.doNotDisturb) {
const description = describeNotification(data);
const referent = getReferent(data);
new Notification(`${description} ${referent}`, {
tag: 'landscape',
image: '/img/favicon.png',
icon: '/img/favicon.png',
badge: '/img/favicon.png',
renotify: true
});
}
}
return state;
}
const dnd = (json: any, state: HarkState): HarkState => {
const data = _.get(json, 'set-dnd', undefined);
if (!_.isUndefined(data)) {
@ -295,22 +188,6 @@ const dnd = (json: any, state: HarkState): HarkState => {
return state;
};
const timebox = (json: any, state: HarkState): HarkState => {
const data = _.get(json, 'timebox', false);
if (data) {
if (data.time) {
const time = makePatDa(data.time);
state.notifications = state.notifications.set(time, data.notifications);
} else {
state.unreadNotes = data.notifications;
_.each(data.notifications, ({ index }) => {
addNotificationToUnread(state, index);
});
}
}
return state;
};
function more(json: any, state: HarkState): HarkState {
const data = _.get(json, 'more', false);
if (data) {
@ -321,98 +198,17 @@ function more(json: any, state: HarkState): HarkState {
return state;
}
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
if ('graph' in a && 'graph' in b) {
return (
a.graph.graph === b.graph.graph &&
a.graph.group === b.graph.group &&
a.graph.mark === b.graph.mark &&
a.graph.description === b.graph.description
);
} else if ('group' in a && 'group' in b) {
return (
a.group.group === b.group.group &&
a.group.description === b.group.description
);
}
return false;
}
function mergeNotifs(a: NotificationContents, b: NotificationContents) {
if ('graph' in a && 'graph' in b) {
return {
graph: [...a.graph, ...b.graph]
};
} else if ('group' in a && 'group' in b) {
return {
group: [...a.group, ...b.group]
};
}
return a;
}
function read(json: any, state: HarkState): HarkState {
const data = _.get(json, 'note-read', false);
if (data) {
const { index } = data;
const time = makePatDa(data.time);
const [read, unread] = _.partition(state.unreadNotes,({ index: idx }) => notifIdxEqual(index, idx));
state.unreadNotes = unread;
const oldTimebox = state.notifications.get(time) ?? [];
const [toMerge, rest] = _.partition(oldTimebox, i => notifIdxEqual(index, i.index));
if(toMerge.length > 0 && read.length > 0) {
read[0].notification.contents = mergeNotifs(read[0].notification.contents, toMerge[0].notification.contents);
}
state.notifications = state.notifications.set(time, [...read, ...rest]);
removeNotificationFromUnread(state, index);
}
return state;
}
function archive(json: any, state: HarkState): HarkState {
const data = _.get(json, 'archive', false);
if (data) {
const { index } = data;
if(data.time) {
const time = makePatDa(data.time);
const timebox = state.notifications.get(time);
if (!timebox) {
console.warn('Modifying nonexistent timebox');
return state;
}
const unarchived = _.filter(timebox, idxNotif =>
!notifIdxEqual(index, idxNotif.index)
);
if(unarchived.length === 0) {
console.log('deleting entire timebox');
state.notifications = state.notifications.delete(time);
} else {
state.notifications = state.notifications.set(time, unarchived);
}
} else {
state.unreadNotes = state.unreadNotes.filter(({ index: idx }) => !notifIdxEqual(idx, index));
removeNotificationFromUnread(state, index);
}
}
return state;
}
export function reduce(data, state) {
const reducers = [
calculateCount,
read,
archive,
timebox,
allStats,
more,
dnd,
added,
unreads,
readEach,
readSince,
unreadSince,
unreadEach,
seenIndex,
removeGraph,
readAll
];
const reducer = compose(reducers.map(r => (s) => {

View File

@ -1,10 +1,10 @@
import {
archive,
HarkBin,
markCountAsRead,
Notification,
NotificationGraphConfig,
NotifIndex,
readNote,
Timebox,
Unreads
} from '@urbit/api';
import { Poke } from '@urbit/http-api';
@ -12,58 +12,68 @@ import { patp2dec } from 'urbit-ob';
import _ from 'lodash';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import api from '~/logic/api';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { createState, createSubscription, pokeOptimisticallyN, reduceState, reduceStateN } from './base';
import {
createState,
createSubscription,
pokeOptimisticallyN,
reduceState,
reduceStateN
} from './base';
import { reduce, reduceGraph, reduceGroup } from '../reducers/hark-update';
import { BigInteger } from 'big-integer';
export const HARK_FETCH_MORE_COUNT = 3;
export interface HarkState {
archivedNotifications: BigIntOrderedMap<Timebox>;
archivedNotifications: BigIntOrderedMap<Notification[]>;
doNotDisturb: boolean;
poke: (poke: Poke<any>) => Promise<void>;
getMore: () => Promise<boolean>;
getSubset: (offset: number, count: number, isArchive: boolean) => Promise<void>;
getSubset: (
offset: number,
count: number,
isArchive: boolean
) => Promise<void>;
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
notifications: BigIntOrderedMap<Timebox>;
unreadNotes: Timebox;
notifications: BigIntOrderedMap<Notification[]>;
unreadNotes: Notification[];
notificationsCount: number;
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
notificationsGroupConfig: string[];
unreads: Unreads;
archive: (index: NotifIndex, time?: BigInteger) => Promise<void>;
readNote: (index: NotifIndex) => Promise<void>;
readCount: (resource: string, index?: string) => Promise<void>;
archive: (bin: HarkBin, time?: BigInteger) => Promise<void>;
readNote: (bin: HarkBin) => Promise<void>;
readCount: (path: string) => Promise<void>;
}
const useHarkState = createState<HarkState>(
'Hark',
(set, get) => ({
archivedNotifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Notification[]>(),
doNotDisturb: false,
unreadNotes: [],
poke: async (poke: Poke<any>) => {
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
},
readCount: async (resource: string, index?: string) => {
const poke = markCountAsRead(resource, index);
readCount: async (path) => {
const poke = markCountAsRead({ desk: (window as any).desk, path });
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
},
archive: async (index: NotifIndex, time?: BigInteger) => {
const poke = archive(index, time);
archive: async (bin: HarkBin, time?: BigInteger) => {
const poke = archive(bin, time);
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
},
readNote: async (index) => {
await pokeOptimisticallyN(useHarkState, readNote(index), [reduce]);
readNote: async (bin) => {
await pokeOptimisticallyN(useHarkState, readNote(bin), [reduce]);
},
getMore: async (): Promise<boolean> => {
const state = get();
const offset = state.notifications.size || 0;
await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false);
const newState = get();
return offset === (newState?.notifications?.size || 0);
const state = get();
const offset = state.notifications.size || 0;
await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false);
const newState = get();
return offset === (newState?.notifications?.size || 0);
},
getSubset: async (offset, count, isArchive): Promise<void> => {
const where = isArchive ? 'archive' : 'inbox';
@ -73,7 +83,7 @@ const useHarkState = createState<HarkState>(
});
reduceState(useHarkState, harkUpdate, [reduce]);
},
notifications: new BigIntOrderedMap<Timebox>(),
notifications: new BigIntOrderedMap<Notification[]>(),
notificationsCount: 0,
notificationsGraphConfig: {
watchOnSelf: false,
@ -81,10 +91,7 @@ const useHarkState = createState<HarkState>(
watching: []
},
notificationsGroupConfig: [],
unreads: {
graph: {},
group: {}
}
unreads: {}
}),
[
'unreadNotes',
@ -94,38 +101,69 @@ const useHarkState = createState<HarkState>(
'notificationsCount'
],
[
(set, get) => createSubscription('hark-store', '/updates', (j) => {
const d = _.get(j, 'harkUpdate', false);
if (d) {
(set, get) =>
createSubscription('hark-store', '/updates', (d) => {
reduceStateN(get(), d, [reduce]);
}
}),
(set, get) => createSubscription('hark-graph-hook', '/updates', (j) => {
const graphHookData = _.get(j, 'hark-graph-hook-update', false);
if (graphHookData) {
reduceStateN(get(), graphHookData, reduceGraph);
}
}),
(set, get) => createSubscription('hark-group-hook', '/updates', (j) => {
const data = _.get(j, 'hark-group-hook-update', false);
if (data) {
reduceStateN(get(), data, reduceGroup);
}
})
}),
(set, get) =>
createSubscription('hark-graph-hook', '/updates', (j) => {
const graphHookData = _.get(j, 'hark-graph-hook-update', false);
if (graphHookData) {
reduceStateN(get(), graphHookData, reduceGraph);
}
}),
(set, get) =>
createSubscription('hark-group-hook', '/updates', (j) => {
const data = _.get(j, 'hark-group-hook-update', false);
if (data) {
reduceStateN(get(), data, reduceGroup);
}
})
]
);
const emptyStats = () => ({
last: 0,
count: 0,
each: []
});
export function useHarkDm(ship: string) {
return useHarkState(
useCallback(
(s) => {
return s.unreads.graph[`/ship/~${window.ship}/dm-inbox`]?.[
return s.unreads[`/graph/~${window.ship}/dm-inbox`]?.[
`/${patp2dec(ship)}`
];
] || emptyStats();
},
[ship]
)
);
}
export function useHarkStat(path: string) {
return useHarkState(
useCallback(s => s.unreads[path] || emptyStats(), [path])
);
}
export function useHarkGraph(graph: string) {
const [, ship, name] = useMemo(() => graph.split('/'), [graph]);
return useHarkState(
useCallback(s => s.unreads[`/graph/${ship}/${name}`], [ship, name])
);
}
export function useHarkGraphIndex(graph: string, index: string) {
const [, ship, name] = useMemo(() => graph.split('/'), [graph]);
return useHarkState(
useCallback(s => s.unreads[`/graph/${ship}/${name}/index`], [
ship,
name,
index
])
);
}
window.hark = useHarkState.getState;
export default useHarkState;

View File

@ -173,7 +173,7 @@ class App extends React.Component {
: null}
</Helmet>
<Root>
<Router>
<Router basename="/apps/landscape/">
<TutorialModal />
<ErrorBoundary>
<StatusBarWithRouter

View File

@ -11,12 +11,13 @@ import { isWriter, resourceFromPath } from '~/logic/lib/group';
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
import useGraphState, { useGraphForAssoc } from '~/logic/state/graph';
import { useGroupForAssoc } from '~/logic/state/group';
import useHarkState from '~/logic/state/hark';
import useHarkState, { useHarkStat } from '~/logic/state/hark';
import { Loading } from '~/views/components/Loading';
import { ChatPane } from './components/ChatPane';
import airlock from '~/logic/api';
import { disallowedShipsForOurContact } from '~/logic/lib/contact';
import shallow from 'zustand/shallow';
import { toHarkPath } from '~/logic/lib/util';
const getCurrGraphSize = (ship: string, name: string) => {
const { graphs } = useGraphState.getState();
@ -35,9 +36,8 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
const [toShare, setToShare] = useState<string[] | string | undefined>();
const group = useGroupForAssoc(association)!;
const graph = useGraphForAssoc(association);
const unreads = useHarkState(state => state.unreads);
const unreadCount =
(unreads.graph?.[resource]?.['/']?.unreads as number) || 0;
const stats = useHarkStat(toHarkPath(association.resource));
const unreadCount = stats.count;
const canWrite = group ? isWriter(group, resource) : false;
const [
getNewest,
@ -94,8 +94,8 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
[group]
);
const fetchMessages = useCallback(async (newer: boolean) => {
const pageSize = 100;
const fetchMessages = useCallback(async (newer: boolean) => {
const pageSize = 100;
const [, , ship, name] = resource.split('/');
const graphSize = graph?.size ?? 0;
@ -140,7 +140,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
}, [resource]);
const dismissUnread = useCallback(() => {
useHarkState.getState().readCount(association.resource);
useHarkState.getState().readCount(toHarkPath(association.resource));
}, [association.resource]);
const getPermalink = useCallback(

View File

@ -52,7 +52,7 @@ export function DmResource(props: DmResourceProps) {
const { ship } = props;
const dm = useDM(ship);
const hark = useHarkDm(ship);
const unreadCount = (hark?.unreads as number) ?? 0;
const unreadCount = hark.count;
const contact = useContact(ship);
const { hideNicknames } = useSettingsState(selectCalmState);
const showNickname = !hideNicknames && Boolean(contact);
@ -111,8 +111,8 @@ export function DmResource(props: DmResourceProps) {
);
const dismissUnread = useCallback(() => {
const resource = `/ship/~${window.ship}/dm-inbox`;
useHarkState.getState().readCount(resource, `/${patp2dec(ship)}`);
const harkPath = `/graph/~${window.ship}/dm-inbox/${patp2dec(ship)}`;
useHarkState.getState().readCount(harkPath);
}, [ship]);
const onSubmit = useCallback(

View File

@ -1,11 +1,13 @@
import { Col, Row, RowProps } from '@tlon/indigo-react';
import { Association, GraphNode, TextContent, UrlContent } from '@urbit/api';
import React from 'react';
import { Association, GraphNode, markEachAsRead, TextContent, UrlContent } from '@urbit/api';
import React, { useEffect } from 'react';
import { useGroup } from '~/logic/state/group';
import Author from '~/views/components/Author';
import Comments from '~/views/components/Comments';
import { TruncatedText } from '~/views/components/TruncatedText';
import { LinkBlockItem } from './LinkBlockItem';
import airlock from '~/logic/api';
import { toHarkPlace } from '~/logic/lib/util';
export interface LinkDetailProps extends RowProps {
node: GraphNode;
@ -17,6 +19,10 @@ export function LinkDetail(props: LinkDetailProps) {
const { node, association, baseUrl, ...rest } = props;
const group = useGroup(association.group);
const { post } = node;
useEffect(() => {
airlock.poke(markEachAsRead(toHarkPlace(association.resource), node.post.index));
}, [association, node]);
const [{ text: title }] = post.contents as [TextContent, UrlContent];
return (
/* @ts-ignore indio props?? */

View File

@ -5,7 +5,7 @@ import { Link, Redirect } from 'react-router-dom';
import { roleForShip } from '~/logic/lib/group';
import { getPermalinkForGraph, referenceToPermalink } from '~/logic/lib/permalinks';
import { useCopy } from '~/logic/lib/useCopy';
import useHarkState from '~/logic/state/hark';
import { useHarkStat } from '~/logic/state/hark';
import Author from '~/views/components/Author';
import { Dropdown } from '~/views/components/Dropdown';
import RemoteContent from '~/views/components/RemoteContent';
@ -40,8 +40,12 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
}, []);
const index = node.post.index.split('/')[1];
const [ship, name] = resource.split('/');
const harkPath = `/graph/~${ship}/${name}`;
const markRead = useCallback(() => {
airlock.poke(markEachAsRead(resource, '/', `/${index}`));
airlock.poke(
markEachAsRead({ desk: (window as any).desk, path: harkPath }, `/${index}`)
);
}, [resource, index]);
useEffect(() => {
@ -74,7 +78,6 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
const baseUrl = props.baseUrl || `/~404/${resource}`;
const ourRole = group ? roleForShip(group, window.ship) : undefined;
const [ship, name] = resource.split('/');
const permalink = getPermalinkForGraph(
association.group,
@ -98,11 +101,10 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
}
};
const appPath = `/ship/~${resource}`;
const unreads = useHarkState(state => state.unreads?.[appPath]);
const commColor = (unreads?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
// @ts-ignore hark will have to choose between sets and numbers
const isUnread = unreads?.['/']?.unreads?.has?.(node.post.index);
const linkStats = useHarkStat(harkPath);
const commStats = useHarkStat(`${harkPath}/${index}`);
const commColor = commStats.count > 0 ? 'blue' : 'gray';
const isUnread = linkStats.each.includes(`/${index}`);
return (
<Box

View File

@ -13,6 +13,7 @@ import { Spinner } from '~/views/components/Spinner';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import { NoteNavigation } from './NoteNavigation';
import airlock from '~/logic/api';
import { toHarkPlace } from '~/logic/lib/util';
interface NoteProps {
ship: string;
@ -59,8 +60,8 @@ export function Note(props: NoteProps & RouteComponentProps) {
const noteId = bigInt(index[1]);
useEffect(() => {
airlock.poke(markEachAsRead(props.association.resource, '/',`/${index[1]}/1/1`));
}, [props.association, props.note]);
airlock.poke(markEachAsRead(toHarkPlace(association.resource), `/${index[1]}`));
}, [association, props.note]);
const adminLinks: JSX.Element[] = [];
const ourRole = roleForShip(group, window.ship);

View File

@ -10,7 +10,7 @@ import {
getLatestRevision,
getSnippet
} from '~/logic/lib/publish';
import useHarkState from '~/logic/state/hark';
import { useHarkStat } from '~/logic/state/hark';
import Author from '~/views/components/Author';
interface NotePreviewProps {
@ -67,14 +67,15 @@ export function NotePreview(props: NotePreviewProps) {
const url = `${props.baseUrl}/note/${noteId}`;
const [, title, body] = getLatestRevision(node);
const appPath = `/ship/${props.host}/${props.book}`;
const unreads = useHarkState(state => state.unreads.graph?.[appPath]);
const harkPath = `/graph/${props.host}/${props.book}`;
const bookStats = useHarkStat(harkPath);
const noteStats = useHarkStat(`${harkPath}/${noteId}`);
// @ts-ignore hark will have to choose between sets and numbers
const isUnread = (unreads?.['/'].unreads ?? new Set()).has(`/${noteId}/1/1`);
const isUnread = bookStats.each.includes(`/${noteId}`);
const snippet = getSnippet(body);
const commColor = (unreads?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
const commColor = noteStats.count > 0 ? 'blue' : 'gray';
const cursorStyle = post.pending ? 'default' : 'pointer';

View File

@ -25,6 +25,7 @@ import { CommentItem } from './CommentItem';
import airlock from '~/logic/api';
import useGraphState from '~/logic/state/graph';
import { useHistory } from 'react-router';
import { toHarkPlace } from '~/logic/lib/util';
interface CommentsProps {
comments: GraphNode;
@ -126,10 +127,11 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
const children = Array.from(comments.children);
useEffect(() => {
console.log(parentIndex);
return () => {
airlock.poke(markCountAsRead(association.resource));
airlock.poke(markCountAsRead(toHarkPlace(association.resource, parentIndex)));
};
}, [comments.post?.index]);
}, [comments.post?.index, association.resource]);
const unreads = useHarkState(state => state.unreads);
const readCount = children.length - getUnreadCount(unreads, association.resource, parentIndex);

View File

@ -1,8 +1,10 @@
import { Box } from '@tlon/indigo-react';
import React, { useCallback, useEffect } from 'react';
import { Route, Switch, useHistory } from 'react-router-dom';
import { Route, Switch, useHistory, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import ob from 'urbit-ob';
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
import useMetadataState from '~/logic/state/metadata';
import LaunchApp from '~/views/apps/launch/App';
import Notifications from '~/views/apps/notifications/notifications';
import { PermalinkRoutes } from '~/views/apps/permalinks/app';
@ -22,8 +24,80 @@ export const Container = styled(Box)`
height: calc(100% - 62px);
`;
function getGroupResourceRedirect(key: string) {
const association = useMetadataState.getState().associations.graph[`/ship/${key}`];
const { metadata } = association;
if(!association || !('graph' in metadata.config)) {
return '';
}
return `/~landscape${association.group}/resource/${metadata.config.graph}${association.resource}`;
}
function getPostRedirect(key: string, segs: string[]) {
const association = useMetadataState.getState().associations.graph[`/ship/${key}`];
const { metadata } = association;
if(!association || !('graph' in metadata.config)) {
return '';
}
return `/~landscape${association.group}/feed/thread/${segs.slice(0, -1).join('/')}`;
}
function getChatRedirect(chat: string, segs: string[]) {
const qs = segs.length > 0 ? `?msg=${segs[0]}` : '';
return `${getGroupResourceRedirect(chat)}${qs}`;
}
function getPublishRedirect(graphKey: string, segs: string[]) {
const base = getGroupResourceRedirect(graphKey);
if(segs.length === 3) {
return `${base}/note/${segs[0]}`;
} else if (segs.length === 4) {
return `${base}/note/${segs[0]}?selected=${segs[2]}`;
}
return base;
}
function getLinkRedirect(graphKey: string, segs: string[]) {
const base = getGroupResourceRedirect(graphKey);
if(segs.length === 1) {
return `${base}/index/${segs[0]}`;
} else if (segs.length === 3) {
return `${base}/index/${segs[0]}?selected=${segs[1]}`;
}
return base;
}
function getNotificationRedirect(link: string) {
const [,mark, ship, name, ...rest] = link.split('/');
const graphKey = `${ship}/${name}`;
switch(mark) {
case 'graph-validator-dm':
return `/~landscape/messages/dm/${ob.patp(rest[0])}`;
case 'graph-validator-chat':
return getChatRedirect(graphKey, rest);
case 'graph-validator-publish':
return getPublishRedirect(graphKey, rest);
case 'graph-validator-link':
return getLinkRedirect(graphKey, rest);
case 'graph-validator-post':
return getPostRedirect(graphKey, rest);
default:
return'';
}
}
export const Content = (props) => {
const history = useHistory();
const location = useLocation();
const associations = useMetadataState(s => s.associations.graph);
useEffect(() => {
const query = new URLSearchParams(location.search);
if(Object.keys(associations).length > 0 && query.has('grid-note')) {
history.push(getNotificationRedirect(query.get('grid-note')));
console.log(query.get('grid-note'));
}
}, [location.search]);
useShortcut('navForward', useCallback((e) => {
e.preventDefault();

View File

@ -12,7 +12,7 @@ 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 useHarkState, { useHarkDm } from '~/logic/state/hark';
import { useHarkDm, useHarkStat } from '~/logic/state/hark';
import useSettingsState from '~/logic/state/settings';
import useGraphState from '~/logic/state/graph';
@ -20,14 +20,10 @@ 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;
const stats = useHarkStat(`/graph/~${graphKey}`);
const { count, each } = stats;
const hasNotifications = false;
const hasUnread = count > 0 || each.length > 0;
return hasNotifications
? 'notification'
: hasUnread

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useCallback } from 'react';
import { AppAssociations, Associations, Graph, UnreadStats } from '@urbit/api';
import { Associations, Graph } from '@urbit/api';
import { patp, patp2dec } from 'urbit-ob';
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
@ -12,10 +12,9 @@ import useMetadataState from '~/logic/state/metadata';
import { useHistory } from 'react-router';
import { useShortcut } from '~/logic/state/settings';
function sidebarSort(
associations: AppAssociations,
unreads: Record<string, Record<string, UnreadStats>>
): Record<SidebarSort, (a: string, b: string) => number> {
function sidebarSort(): Record<SidebarSort, (a: string, b: string) => number> {
const { associations } = useMetadataState.getState();
const { unreads } = useHarkState.getState();
const alphabetical = (a: string, b: string) => {
const aAssoc = associations[a];
const bAssoc = associations[b];
@ -26,18 +25,13 @@ function sidebarSort(
};
const lastUpdated = (a: string, b: string) => {
const aAssoc = associations[a];
const bAssoc = associations[b];
const aResource = aAssoc?.resource;
const bResource = bAssoc?.resource;
const aUpdated = a.startsWith('~')
? (unreads?.[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(a)}`]?.last || 0)
: ((unreads?.[aResource]?.['/']?.last) || 0);
? (unreads?.[`/graph/~${window.ship}/dm-inbox/${patp2dec(a)}`]?.last || 0)
: (unreads?.[`/graph/${a.slice(6)}`]?.last || 0);
const bUpdated = b.startsWith('~')
? (unreads?.[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(b)}`]?.last || 0)
: ((unreads?.[bResource]?.['/']?.last) || 0);
? (unreads?.[`/graph/~${window.ship}/dm-inbox/${patp2dec(b)}`]?.last || 0)
: (unreads?.[`/graph/${b.slice(6)}`]?.last || 0);
return bUpdated - aUpdated || alphabetical(a, b);
};
@ -94,11 +88,10 @@ 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);
const graphKeys = useGraphState(s => s.graphKeys);
const ordered = getItems(associations, workspace, inbox)
.sort(sidebarSort(associations.graph, unreads)[config.sortBy]);
.sort(sidebarSort()[config.sortBy]);
const history = useHistory();