landscape: update notification reducing for new hark

This commit is contained in:
Liam Fitzgerald 2021-10-05 14:10:02 +10:00
parent 36a9fceab3
commit 623303c893
4 changed files with 141 additions and 43 deletions

View File

@ -1,7 +1,9 @@
import {
HarkPlace,
Timebox,
HarkStats
HarkStats,
harkBinToId,
makePatDa
} from '@urbit/api';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import _ from 'lodash';
@ -12,7 +14,7 @@ import { HarkState as State } from '../state/hark';
type HarkState = State & BaseState<State>;
function calculateCount(json: any, state: HarkState) {
state.notificationsCount = Object.keys(state.unreadNotes).length;
state.notificationsCount = Object.keys(state.unseen).length;
return state;
}
@ -170,7 +172,8 @@ function allStats(json: any, state: HarkState): HarkState {
function clearState(state: HarkState): HarkState {
const initialState = {
notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(),
unseen: {},
seen: {},
notificationsGroupConfig: [],
notificationsGraphConfig: {
watchOnSelf: false,
@ -204,6 +207,87 @@ function more(json: any, state: HarkState): HarkState {
return state;
}
function added(json: any, state: HarkState): HarkState {
if('added' in json) {
const { bin } = json.added;
const binId = harkBinToId(bin);
state.unseen[binId] = json.added;
}
return state;
}
function archived(json: any, state: HarkState): HarkState {
if('archived' in json) {
const { lid, notification } = json.archived;
const seen = 'seen' in lid ? 'seen' : 'unseen';
const binId = harkBinToId(notification.bin);
delete state[seen][binId];
const time = makePatDa(json.archived.time);
const timebox = state.archive?.get(time) || {};
timebox[binId] = notification;
state.archive = state.archive.set(time, timebox);
}
return state;
}
function timebox(json: any, state: HarkState): HarkState {
if('timebox' in json) {
const { timebox } = json;
const { lid, notifications } = timebox;
if('archive' in lid) {
const time = makePatDa(lid.archive);
const old = state.archive.get(time) || {};
notifications.forEach((note: any) => {
const binId = harkBinToId(note.bin);
old[binId] = note;
});
state.archive = state.archive.set(time, old);
} else {
const seen = 'seen' in lid ? 'seen' : 'unseen';
notifications.forEach((note: any) => {
const binId = harkBinToId(note.bin);
state[seen][binId] = note;
});
}
}
return state;
}
function opened(json: any, state: HarkState): HarkState {
if('opened' in json) {
const bins = Object.keys(state.unseen);
bins.forEach((bin) => {
const old = state.seen[bin];
const curr = state.unseen[bin];
curr.body = [...curr.body, ...(old?.body || [])];
state.seen[bin] = curr;
delete state.unseen[bin];
});
}
return state;
}
function delPlace(json: any, state: HarkState): HarkState {
if('del-place' in json) {
const { path, desk } = json['del-place'];
const pathId = `${desk}${path}`;
const wipeBox = (t: Timebox) => {
Object.keys(t).forEach((bin) => {
if (bin.startsWith(pathId)) {
delete t[bin];
}
});
};
wipeBox(state.unseen);
wipeBox(state.seen);
state.archive.keys().forEach((key) => {
wipeBox(state.archive.get(key)!);
});
}
return state;
}
export function reduce(data, state) {
const reducers = [
calculateCount,
@ -215,7 +299,12 @@ export function reduce(data, state) {
unreadSince,
unreadEach,
seenIndex,
readAll
readAll,
added,
timebox,
archived,
opened,
delPlace
];
const reducer = compose(reducers.map(r => (s) => {
return r(data, s);

View File

@ -2,9 +2,13 @@ import {
archive,
HarkBin,
markCountAsRead,
Notification,
NotificationGraphConfig,
Unreads
Unreads,
Timebox,
HarkLid,
harkBinToId,
decToUd,
unixToDa
} from '@urbit/api';
import { Poke } from '@urbit/http-api';
import { patp2dec } from 'urbit-ob';
@ -17,40 +21,31 @@ 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<Notification[]>;
archive: BigIntOrderedMap<Timebox>;
doNotDisturb: boolean;
poke: (poke: Poke<any>) => Promise<void>;
getMore: () => Promise<boolean>;
getSubset: (
offset: number,
count: number,
isArchive: boolean
) => Promise<void>;
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
notifications: BigIntOrderedMap<Notification[]>;
unreadNotes: Notification[];
unseen: Timebox;
seen: Timebox;
notificationsCount: number;
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
notificationsGroupConfig: string[];
unreads: Unreads;
archive: (bin: HarkBin, time?: BigInteger) => Promise<void>;
readNote: (bin: HarkBin) => Promise<void>;
readCount: (path: string) => Promise<void>;
archiveNote: (bin: HarkBin, lid: HarkLid) => Promise<void>;
}
const useHarkState = createState<HarkState>(
'Hark',
(set, get) => ({
archivedNotifications: new BigIntOrderedMap<Notification[]>(),
archive: new BigIntOrderedMap<Timebox>(),
doNotDisturb: false,
unreadNotes: [],
poke: async (poke: Poke<any>) => {
@ -60,29 +55,31 @@ const useHarkState = createState<HarkState>(
const poke = markCountAsRead({ desk: (window as any).desk, path });
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
},
archive: async (bin: HarkBin, time?: BigInteger) => {
const poke = archive(bin, time);
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
},
readNote: async (bin) => {
await pokeOptimisticallyN(useHarkState, readNote(bin), [reduce]);
archiveNote: async (bin: HarkBin, lid: HarkLid) => {
const poke = archive(bin, lid);
get().set((draft) => {
const key = 'seen' in lid ? 'seen' : 'unseen';
const binId = harkBinToId(bin);
delete draft[key][binId];
});
await api.poke(poke);
},
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);
},
getSubset: async (offset, count, isArchive): Promise<void> => {
const where = isArchive ? 'archive' : 'inbox';
const { harkUpdate } = await api.scry({
const oldSize = state.archive?.size || 0;
const offset = decToUd(
state.archive?.peekSmallest()?.[0].toString()
|| unixToDa(Date.now() * 1000).toString()
);
const update = await api.scry({
app: 'hark-store',
path: `/recent/${where}/${offset}/${count}`
path: `/recent/inbox/${offset}/5`
});
reduceState(useHarkState, harkUpdate, [reduce]);
reduceStateN(useHarkState.getState(), update, [reduce]);
return get().archive?.size === oldSize;
},
notifications: new BigIntOrderedMap<Notification[]>(),
unseen: {},
seen: {},
notificationsCount: 0,
notificationsGraphConfig: {
watchOnSelf: false,
@ -93,9 +90,9 @@ const useHarkState = createState<HarkState>(
unreads: {}
}),
[
'unreadNotes',
'notifications',
'archivedNotifications',
'seen',
'unseen',
'archive',
'unreads',
'notificationsCount'
],

View File

@ -39,6 +39,7 @@ interface OmniboxResultProps {
shiftLink?: string;
shiftDescription?: string;
description?: string;
hasNotifications?: boolean;
}
interface OmniboxResultState {
@ -142,14 +143,20 @@ export class OmniboxResult extends Component<OmniboxResultProps, OmniboxResultSt
);
} else if (icon === 'notifications') {
graphic = (
<Box mr="2" height="18px" width="18px" position="relative" display="inline-block">
<Icon
display='inline-block'
verticalAlign='middle'
icon='Notifications'
mr={2}
size='18px'
color={iconFill}
/>
{this.props.hasNotifications ? (
<Box position="absolute" right="-6px" top="-4px">
<Icon icon="Bullet" color={(this.state.hovered || selected === link) ? 'white' : 'blue'} />
</Box>
) : null}
</Box>
);
} else if (icon === 'messages') {
graphic = (

View File

@ -70,8 +70,6 @@ export const opened = harkAction({
opened: null
});
export const markCountAsRead = (place: HarkPlace): Poke<unknown> =>
harkAction({
'read-count': place
@ -147,3 +145,10 @@ export function harkBinEq(a: HarkBin, b: HarkBin): boolean {
a.path === b.path
);
}
export function harkLidToId(lid: HarkLid): string {
if('time' in lid) {
return `archive-${lid.time}`;
}
return Object.keys(lid)[0];
}