mirror of
https://github.com/urbit/shrub.git
synced 2025-01-03 01:54:43 +03:00
interface: update hark callsites & state
This commit is contained in:
parent
c2cc13d96a
commit
12b4e4c59b
@ -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)
|
||||
};
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -173,7 +173,7 @@ class App extends React.Component {
|
||||
: null}
|
||||
</Helmet>
|
||||
<Root>
|
||||
<Router>
|
||||
<Router basename="/apps/landscape/">
|
||||
<TutorialModal />
|
||||
<ErrorBoundary>
|
||||
<StatusBarWithRouter
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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?? */
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user