hark-fe: add store, api logic

This commit is contained in:
Liam Fitzgerald 2020-10-27 15:32:26 +10:00
parent 8b090400f1
commit 1d65e52351
8 changed files with 533 additions and 0 deletions

View File

@ -0,0 +1,67 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { dateToDa, decToUd } from "../lib/util";
import {NotifIndex} from "~/types";
export class HarkApi extends BaseApi<StoreState> {
private harkAction(action: any): Promise<any> {
return this.action("hark-store", "hark-action", action);
}
private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) {
const time = decToUd(intTime.toString());
return this.harkAction({
[frond]: {
time,
index
}
});
}
private graphHookAction(action: any) {
return this.action("hark-graph-hook", "hark-graph-hook-action", action);
}
setMentions(mentions: boolean) {
return this.graphHookAction({
'set-mentions': mentions
});
}
setWatchOnSelf(watchSelf: boolean) {
return this.graphHookAction({
'set-watch-on-self': watchSelf
});
}
setDoNotDisturb(dnd: boolean) {
return this.harkAction({
'set-dnd': dnd
});
}
archive(time: BigInteger, index: NotifIndex) {
return this.actOnNotification('archive', time, index);
}
read(time: BigInteger, index: NotifIndex) {
return this.actOnNotification('read', time, index);
}
unread(time: BigInteger, index: NotifIndex) {
return this.actOnNotification('unread', time, index);
}
seen() {
return this.harkAction({ seen: null });
}
async getTimeSubset(start?: Date, end?: Date) {
const s = start ? dateToDa(start) : "-";
const e = end ? dateToDa(end) : "-";
const result = await this.scry("hark-hook", `/time-subset/${s}/${e}`);
this.store.handleEvent({
data: result,
});
}
}

View File

@ -0,0 +1,187 @@
import bigInt, { BigInteger } from "big-integer";
interface NonemptyNode<V> {
n: [BigInteger, V];
l: MapNode<V>;
r: MapNode<V>;
}
type MapNode<V> = NonemptyNode<V> | null;
/**
* An implementation of ordered maps for JS
* Plagiarised wholesale from sys/zuse
*/
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
private root: MapNode<V> = null;
constructor() {}
/**
* Retrieve an value for a key
*/
get(key: BigInteger): V | null {
const inner = (node: MapNode<V>) => {
if (!node) {
return node;
}
const [k, v] = node.n;
if (key.eq(k)) {
return v;
}
if (key.gt(k)) {
return inner(node.l);
} else {
return inner(node.r);
}
};
return inner(this.root);
}
/**
* Put an item by a key
*/
set(key: BigInteger, value: V): void {
const inner = (node: MapNode<V>) => {
if (!node) {
return {
n: [key, value],
l: null,
r: null,
};
}
const [k] = node.n;
if (key.eq(k)) {
return {
...node,
n: [k, value],
};
}
if (key.gt(k)) {
const l = inner(node.l);
if (!l) {
throw new Error("invariant violation");
}
return {
...node,
l,
};
}
const r = inner(node.r);
if (!r) {
throw new Error("invariant violation");
}
return { ...node, r };
};
this.root = inner(this.root);
}
/**
* Remove all entries
*/
clear() {
this.root = null;
}
/**
* Predicate testing if map contains key
*/
has(key: BigInteger): boolean {
const inner = (node: MapNode<V>) => {
if (!node) {
return false;
}
const [k] = node.n;
if (k.eq(key)) {
return true;
}
if (key.gt(k)) {
return inner(node.l);
}
return inner(node.r);
};
return inner(this.root);
}
/**
* Remove value associated with key, returning whether that key
* existed in the first place
*/
delete(key: BigInteger) {
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
if (!node) {
return [false, null];
}
const [k] = node.n;
if (k.eq(key)) {
return [true, this.nip(node)];
}
if (key.gt(k)) {
const [bool, l] = inner(node.l);
return [
bool,
{
...node,
l,
},
];
}
const [bool, r] = inner(node.r);
return [
bool,
{
...node,
r,
},
];
};
const [ret, newRoot] = inner(this.root);
this.root = newRoot;
return ret;
}
private nip(nod: NonemptyNode<V>): MapNode<V> {
const inner = (node: NonemptyNode<V>) => {
if (!node.l) {
return node.r;
}
if (!node.r) {
return node.l;
}
return {
...node.l,
r: inner(node.r),
};
};
return inner(nod);
}
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
let result: [BigInteger, V][] = [];
const inner = (node: MapNode<V>) => {
if (!node) {
return;
}
result.push(node.n);
inner(node.l);
inner(node.r);
};
inner(this.root);
let idx = 0;
return {
[Symbol.iterator]: this[Symbol.iterator],
next: (): IteratorResult<[BigInteger, V]> => {
if (idx < result.length) {
return { value: result[idx++], done: false };
}
return { done: true, value: null };
},
};
}
}

View File

@ -0,0 +1,202 @@
import { Notifications, Notification, NotifIndex, NotificationGraphConfig, GroupNotificationsConfig } from "~/types";
import { makePatDa } from "~/logic/lib/util";
import _ from "lodash";
type HarkState = {
notifications: Notifications;
archivedNotifications: Notifications;
notificationsCount: number;
notificationsGraphConfig: NotificationGraphConfig;
groupNotifications: GroupNotificationsConfig;
};
export const HarkReducer = (json: any, state: HarkState) => {
const data = _.get(json, "harkUpdate", false);
if (data) {
reduce(data, state);
}
const graphHookData = _.get(json, "hark-graph-hook-update", false);
if (graphHookData) {
console.log(graphHookData);
graphInitial(graphHookData, state);
graphWatchSelf(graphHookData, state);
graphMentions(graphHookData, state);
}
const groupHookData = _.get(json, "hark-group-hook-update", false);
if(groupHookData) {
groupInitial(groupHookData, state);
groupListen(groupHookData, state);
groupIgnore(groupHookData, state);
}
};
function groupInitial(json: any, state: HarkState) {
const data = _.get(json, 'initial', false);
if(data) {
state.groupNotifications = data;
}
}
function graphInitial(json: any, state: HarkState) {
const data = _.get(json, "initial", false);
if (data) {
state.notificationsGraphConfig = data;
}
}
function groupListen(json: any, state: HarkState) {
const data = _.get(json, "listen", false);
if (data) {
state.groupNotifications = [...state.groupNotifications, data];
}
}
function groupIgnore(json: any, state: HarkState) {
const data = _.get(json, "ignore", false);
if (data) {
state.groupNotifications = state.groupNotifications.filter(n => n!== data);
}
}
function graphMentions(json: any, state: HarkState) {
const data = _.get(json, "set-mentions", undefined);
if (!_.isUndefined(data)) {
state.notificationsGraphConfig.mentions = data;
}
}
function graphWatchSelf(json: any, state: HarkState) {
const data = _.get(json, "set-watch-on-self", undefined);
if (!_.isUndefined(data)) {
state.notificationsGraphConfig.watchOnSelf = data;
}
}
function reduce(data: any, state: HarkState) {
unread(data, state);
read(data, state);
archive(data, state);
timebox(data, state);
more(data, state);
dnd(data, state);
count(data, state);
}
function count(json: any, state: HarkState) {
const data = _.get(json, "count", false);
if (data !== false) {
state.notificationsCount = data;
}
}
const dnd = (json: any, state: HarkState) => {
const data = _.get(json, "set-dnd", undefined);
if (!_.isUndefined(data)) {
state.doNotDisturb = data;
}
};
const timebox = (json: any, state: HarkState) => {
const data = _.get(json, "timebox", false);
if (data) {
const time = makePatDa(data.time);
if (data.archive) {
state.archivedNotifications.set(time, data.notifications);
} else {
state.notifications.set(time, data.notifications);
}
}
};
function more(json: any, state: HarkState) {
const data = _.get(json, "more", false);
if (data) {
_.forEach(data, (d) => reduce(d, 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.module === b.graph.module &&
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 setRead(
time: string,
index: NotifIndex,
read: boolean,
state: HarkState
) {
const patDa = makePatDa(time);
const timebox = state.notifications.get(patDa);
if (_.isNull(timebox)) {
console.warn("Modifying nonexistent timebox");
return;
}
const arrIdx = timebox.findIndex((idxNotif) =>
notifIdxEqual(index, idxNotif.index)
);
if (arrIdx === -1) {
console.warn("Modifying nonexistent index");
return;
}
timebox[arrIdx].notification.read = read;
state.notifications.set(patDa, timebox);
}
function read(json: any, state: HarkState) {
const data = _.get(json, "read", false);
if (data) {
const { time, index } = data;
state.notificationsCount--;
setRead(time, index, true, state);
}
}
function unread(json: any, state: HarkState) {
const data = _.get(json, "unread", false);
if (data) {
const { time, index } = data;
state.notificationsCount++;
setRead(time, index, false, state);
}
}
function archive(json: any, state: HarkState) {
const data = _.get(json, "archive", false);
if (data) {
const { index } = data;
const time = makePatDa(data.time);
const timebox = state.notifications.get(time);
if (!timebox) {
console.warn("Modifying nonexistent timebox");
return;
}
const [archived, unarchived] = _.partition(timebox, (idxNotif) =>
notifIdxEqual(index, idxNotif.index)
);
state.notifications.set(time, unarchived);
const archiveBox = state.archivedNotifications.get(time) || [];
state.notificationsCount -= archived.filter(
({ notification }) => !notification.read
).length;
state.archivedNotifications.set(time, [
...archiveBox,
...archived.map(({ notification, index }) => ({
notification: { ...notification, read: true },
index,
})),
]);
}
}

View File

@ -5,15 +5,19 @@ import LocalReducer from '../reducers/local';
import ChatReducer from '../reducers/chat-update';
import { StoreState } from './type';
import { Timebox } from '~/types';
import { Cage } from '~/types/cage';
import ContactReducer from '../reducers/contact-update';
import S3Reducer from '../reducers/s3-update';
import { GraphReducer } from '../reducers/graph-update';
import { HarkReducer } from '../reducers/hark-update';
import GroupReducer from '../reducers/group-update';
import PublishUpdateReducer from '../reducers/publish-update';
import PublishResponseReducer from '../reducers/publish-response';
import LaunchReducer from '../reducers/launch-update';
import ConnectionReducer from '../reducers/connection';
import {OrderedMap} from '../lib/OrderedMap';
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
export const homeAssociation = {
"app-path": "/home",
@ -98,6 +102,15 @@ export default class GlobalStore extends BaseStore<StoreState> {
dark: false,
inbox: {},
chatSynced: null,
notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(),
notificationsGraphConfig: {
watchOnSelf: false,
mentions: false,
watching: [],
watchingIndices: {}
},
notificationsCount: 0
};
}
@ -114,5 +127,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.launchReducer.reduce(data, this.state);
this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state);
HarkReducer(data, this.state);
}
}

View File

@ -11,6 +11,7 @@ import { LaunchState, WeatherState } from '~/types/launch-update';
import { ConnectionStatus } from '~/types/connection';
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
import {Graphs} from '~/types/graph-update';
import { Notifications, NotificationGraphConfig } from "~/types";
export interface StoreState {
// local state
@ -53,4 +54,9 @@ export interface StoreState {
chatSynced: ChatHookUpdate | null;
inbox: Inbox;
pendingMessages: Map<Path, Envelope[]>;
notifications: Notifications;
notificationsGraphConfig: NotificationGraphConfig;
notificationsCount: number,
doNotDisturb: boolean;
}

View File

@ -51,6 +51,9 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/all', 'launch');
this.subscribe('/all', 'weather');
this.subscribe('/keys', 'graph-store');
this.subscribe('/updates', 'hark-store');
this.subscribe('/updates', 'hark-graph-hook');
this.subscribe('/updates', 'hark-group-hook');
}
restart() {

View File

@ -0,0 +1,53 @@
import _ from "lodash";
import { Post } from "./graph-update";
import { GroupUpdate } from "./group-update";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMapCustom";
type GraphNotifDescription = "link" | "comment";
export interface GraphNotifIndex {
graph: string;
group: string;
description: GraphNotifDescription;
module: string;
}
export interface GroupNotifIndex {
group: string;
description: string;
}
export type NotifIndex =
| { graph: GraphNotifIndex }
| { group: GroupNotifIndex };
export type GraphNotificationContents = Post[];
export type GroupNotificationContents = GroupUpdate[];
export type NotificationContents =
| { graph: GraphNotificationContents }
| { group: GroupNotificationContents };
interface Notification {
read: boolean;
time: number;
contents: NotificationContents;
}
export interface IndexedNotification {
index: NotifIndex;
notification: Notification;
}
export type Timebox = IndexedNotification[];
export type Notifications = BigIntOrderedMap<Timebox>;
export interface NotificationGraphConfig {
watchOnSelf: boolean;
mentions: boolean;
watching: string[];
}
export type GroupNotificationsConfig = string[];

View File

@ -6,6 +6,7 @@ export * from './contact-update';
export * from './global';
export * from './group-update';
export * from './graph-update';
export * from './hark-update';
export * from './invite-update';
export * from './launch-update';
export * from './link-listen-update';