mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 13:54:20 +03:00
hark-fe: add store, api logic
This commit is contained in:
parent
8b090400f1
commit
1d65e52351
67
pkg/interface/src/logic/api/hark.ts
Normal file
67
pkg/interface/src/logic/api/hark.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
187
pkg/interface/src/logic/lib/BigIntOrderedMap.ts
Normal file
187
pkg/interface/src/logic/lib/BigIntOrderedMap.ts
Normal 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 };
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
202
pkg/interface/src/logic/reducers/hark-update.ts
Normal file
202
pkg/interface/src/logic/reducers/hark-update.ts
Normal 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,
|
||||
})),
|
||||
]);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
53
pkg/interface/src/types/hark-update.ts
Normal file
53
pkg/interface/src/types/hark-update.ts
Normal 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[];
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user