mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 00:13:12 +03:00
Merge pull request #5302 from urbit/lf/landscape-notifications
landscape: revive notifications
This commit is contained in:
commit
f2e00ba091
90
pkg/interface/src/logic/lib/notificationRedirects.ts
Normal file
90
pkg/interface/src/logic/lib/notificationRedirects.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import useMetadataState from '../state/metadata';
|
||||
import ob from 'urbit-ob';
|
||||
|
||||
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 getGraphRedirect(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'';
|
||||
}
|
||||
}
|
||||
|
||||
function getInviteRedirect(link: string) {
|
||||
const [,,app,uid] = link.split('/');
|
||||
return `/invites/${app}/${uid}`;
|
||||
}
|
||||
|
||||
function getDmRedirect(link: string) {
|
||||
const [,,ship] = link.split('/');
|
||||
return `/~landscape/messages/dm/${ship}`;
|
||||
}
|
||||
function getGroupRedirect(link: string) {
|
||||
const [,,ship,name] = link.split('/');
|
||||
return `/~landscape/ship/${ship}/${name}`;
|
||||
}
|
||||
|
||||
export function getNotificationRedirect(link: string) {
|
||||
if(link.startsWith('/graph-validator')) {
|
||||
return getGraphRedirect(link);
|
||||
} else if (link.startsWith('/invite')) {
|
||||
return getInviteRedirect(link);
|
||||
} else if (link.startsWith('/dm')) {
|
||||
return getDmRedirect(link);
|
||||
} else if (link.startsWith('/groups')) {
|
||||
return getGroupRedirect(link);
|
||||
}
|
||||
}
|
@ -89,6 +89,7 @@ const otherIndex = function(config) {
|
||||
const idx = {
|
||||
mychannel: result('My Channels', '/~landscape/home', 'home', null),
|
||||
profile: result('Profile', `/~profile/~${window.ship}`, 'profile', null),
|
||||
updates: result('Notifications', '/~notifications', 'notifications', null),
|
||||
messages: result('Messages', '/~landscape/messages', 'messages', null),
|
||||
logout: result('Log Out', '/~/logout', 'logout', null)
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -2,9 +2,14 @@ import {
|
||||
archive,
|
||||
HarkBin,
|
||||
markCountAsRead,
|
||||
Notification,
|
||||
NotificationGraphConfig,
|
||||
Unreads
|
||||
Unreads,
|
||||
Timebox,
|
||||
HarkLid,
|
||||
harkBinToId,
|
||||
decToUd,
|
||||
unixToDa,
|
||||
opened
|
||||
} from '@urbit/api';
|
||||
import { Poke } from '@urbit/http-api';
|
||||
import { patp2dec } from 'urbit-ob';
|
||||
@ -17,40 +22,32 @@ 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>;
|
||||
opened: () => 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 +57,36 @@ 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]);
|
||||
opened: async () => {
|
||||
reduceStateN(get(), { opened: null }, [reduce]);
|
||||
|
||||
await api.poke(opened);
|
||||
},
|
||||
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 +97,9 @@ const useHarkState = createState<HarkState>(
|
||||
unreads: {}
|
||||
}),
|
||||
[
|
||||
'unreadNotes',
|
||||
'notifications',
|
||||
'archivedNotifications',
|
||||
'seen',
|
||||
'unseen',
|
||||
'archive',
|
||||
'unreads',
|
||||
'notificationsCount'
|
||||
],
|
||||
|
31
pkg/interface/src/views/apps/notifications/Archive.tsx
Normal file
31
pkg/interface/src/views/apps/notifications/Archive.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import React, { useEffect } from 'react';
|
||||
import useHarkState, { HarkState } from '~/logic/state/hark';
|
||||
import { Notification } from './notification';
|
||||
|
||||
const selArchive = (s: HarkState) => s.archive;
|
||||
|
||||
export function Archive() {
|
||||
const archive = useHarkState(selArchive);
|
||||
const keys = archive.keys();
|
||||
|
||||
useEffect(() => {
|
||||
useHarkState.getState().getMore();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box pt="2" overflowY="auto" overflowX="hidden">
|
||||
{keys.map(key =>
|
||||
Object.entries(archive.get(key)!)
|
||||
.sort(([, a], [, b]) => b.time - a.time)
|
||||
.map(([binId, n]) => (
|
||||
<Notification
|
||||
key={`${key.toString()}-${binId}`}
|
||||
lid={{ time: key.toString() }}
|
||||
notification={n}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
58
pkg/interface/src/views/apps/notifications/NewBox.tsx
Normal file
58
pkg/interface/src/views/apps/notifications/NewBox.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Box, Center, Col, Text } from '@tlon/indigo-react';
|
||||
import React from 'react';
|
||||
import useHarkState, { HarkState } from '~/logic/state/hark';
|
||||
import { harkBinToId, HarkLid, Timebox } from '../../../../../npm/api/dist';
|
||||
import { Notification } from './notification';
|
||||
|
||||
const unseenLid = { unseen: null };
|
||||
const seenLid = { seen: null };
|
||||
const selUnseen = (s: HarkState) => s.unseen;
|
||||
const selSeen = (s: HarkState) => s.seen;
|
||||
export function NewBox() {
|
||||
const seen = useHarkState(selSeen);
|
||||
const unseen = useHarkState(selUnseen);
|
||||
const empty = Object.keys(seen).length + Object.keys(unseen).length === 0;
|
||||
|
||||
return (
|
||||
<Box pt="2" overflowY="auto" overflowX="hidden">
|
||||
{empty ? (
|
||||
<Center p="3">
|
||||
<Text>All clear!</Text>
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<Lid lid={unseenLid} timebox={unseen} title="Unseen" />
|
||||
<Lid lid={seenLid} timebox={seen} title="Seen" />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Lid({
|
||||
lid,
|
||||
timebox,
|
||||
title
|
||||
}: {
|
||||
lid: HarkLid;
|
||||
timebox: Timebox;
|
||||
title: string;
|
||||
}) {
|
||||
if(Object.keys(timebox).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Text gray p="2">
|
||||
{title}
|
||||
</Text>
|
||||
<Col>
|
||||
{Object.entries(timebox)
|
||||
.sort(([, a], [, b]) => b.time - a.time)
|
||||
.map(([binId, n]) => (
|
||||
<Notification key={harkBinToId(n.bin)} lid={lid} notification={n} />
|
||||
))}
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
import { Col } from '@tlon/indigo-react';
|
||||
import {
|
||||
IndexedNotification,
|
||||
JoinRequests,
|
||||
Notifications,
|
||||
seen,
|
||||
Timebox,
|
||||
unixToDa
|
||||
} from '@urbit/api';
|
||||
import { BigInteger } from 'big-integer';
|
||||
import _ from 'lodash';
|
||||
import f from 'lodash/fp';
|
||||
import moment from 'moment';
|
||||
import React, { useEffect } from 'react';
|
||||
import { getNotificationKey } from '~/logic/lib/hark';
|
||||
import { daToUnix } from '~/logic/lib/util';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { Invites } from './invites';
|
||||
import { Notification } from './notification';
|
||||
import airlock from '~/logic/api';
|
||||
|
||||
type DatedTimebox = [BigInteger, Timebox];
|
||||
|
||||
function filterNotification(groups: string[]) {
|
||||
if (groups.length === 0) {
|
||||
return () => true;
|
||||
}
|
||||
return (n: IndexedNotification) => {
|
||||
if ('graph' in n.index) {
|
||||
const { group } = n.index.graph;
|
||||
return groups.findIndex(g => group === g) !== -1;
|
||||
} else if ('group' in n.index) {
|
||||
const { group } = n.index.group;
|
||||
return groups.findIndex(g => group === g) !== -1;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Inbox(props: {
|
||||
archive: Notifications;
|
||||
showArchive?: boolean;
|
||||
filter: string[];
|
||||
pendingJoin: JoinRequests;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
let hasSeen = false;
|
||||
setTimeout(() => {
|
||||
hasSeen = true;
|
||||
}, 3000);
|
||||
return () => {
|
||||
if (hasSeen) {
|
||||
airlock.poke(seen());
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const ready = useHarkState(
|
||||
s => Object.keys(s.unreads.graph).length > 0
|
||||
);
|
||||
|
||||
const getMore = useHarkState(s => s.getMore);
|
||||
|
||||
const notificationState = useHarkState(state => state.notifications);
|
||||
const unreadNotes = useHarkState(s => s.unreadNotes);
|
||||
const archivedNotifications = useHarkState(state => state.archivedNotifications);
|
||||
|
||||
const notifications =
|
||||
Array.from(props.showArchive ? archivedNotifications : notificationState) || [];
|
||||
|
||||
const notificationsByDay = f.flow(
|
||||
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
||||
date,
|
||||
nots.filter(filterNotification(props.filter))
|
||||
]),
|
||||
f.groupBy<DatedTimebox>(([d]) => {
|
||||
const date = moment(daToUnix(d));
|
||||
if (moment().subtract(6, 'hours').isBefore(date)) {
|
||||
return 'latest';
|
||||
} else {
|
||||
return date.format('YYYYMMDD');
|
||||
}
|
||||
})
|
||||
)(notifications);
|
||||
|
||||
const notificationsByDayMap = new Map<string, DatedTimebox[]>(
|
||||
Object.keys(notificationsByDay).map((timebox) => {
|
||||
return [timebox, notificationsByDay[timebox]];
|
||||
})
|
||||
);
|
||||
|
||||
const date = unixToDa(Date.now());
|
||||
|
||||
return (
|
||||
<Col p={1} position="relative" height="100%" overflowY="auto" overflowX="hidden">
|
||||
<Invites pendingJoin={props.pendingJoin} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
function sortTimeboxes([a]: DatedTimebox, [b]: DatedTimebox) {
|
||||
return b.subtract(a);
|
||||
}
|
||||
|
||||
function sortIndexedNotification(
|
||||
{ notification: a }: IndexedNotification,
|
||||
{ notification: b }: IndexedNotification
|
||||
) {
|
||||
return b.time - a.time;
|
||||
}
|
||||
|
||||
function DaySection({
|
||||
timeboxes,
|
||||
unread = false
|
||||
}) {
|
||||
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
||||
if (lent === 0 || timeboxes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
|
||||
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
||||
<Notification
|
||||
key={getNotificationKey(date, not)}
|
||||
notification={not}
|
||||
unread={unread}
|
||||
time={!unread ? date : undefined}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,64 +1,112 @@
|
||||
import { Box, Button, Icon, Row } from '@tlon/indigo-react';
|
||||
import { Box, Col, Text, Button, Icon, Row } from '@tlon/indigo-react';
|
||||
import {
|
||||
GraphNotificationContents,
|
||||
GroupNotificationContents,
|
||||
IndexedNotification
|
||||
HarkLid,
|
||||
harkLidToId,
|
||||
harkBinToId,
|
||||
Notification as INotification,
|
||||
HarkContent
|
||||
} from '@urbit/api';
|
||||
import { BigInteger } from 'big-integer';
|
||||
import React, { ReactNode, useCallback } from 'react';
|
||||
import { getNotificationKey } from '~/logic/lib/hark';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHovering } from '~/logic/lib/util';
|
||||
import useLocalState from '~/logic/state/local';
|
||||
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
||||
import { SwipeMenu } from '~/views/components/SwipeMenu';
|
||||
import { GraphNotification } from './graph';
|
||||
import { GroupNotification } from './group';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import shallow from 'zustand/shallow';
|
||||
import { map, take, uniqBy } from 'lodash';
|
||||
import { Mention } from '~/views/components/MentionText';
|
||||
import { PropFunc } from '~/types';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { getNotificationRedirect } from '~/logic/lib/notificationRedirects';
|
||||
|
||||
export interface NotificationProps {
|
||||
notification: IndexedNotification;
|
||||
notification: INotification;
|
||||
time: BigInteger;
|
||||
unread: boolean;
|
||||
}
|
||||
|
||||
export function NotificationWrapper(props: {
|
||||
time?: BigInteger;
|
||||
read?: boolean;
|
||||
notification?: IndexedNotification;
|
||||
children: ReactNode;
|
||||
const MAX_CONTENTS = 5;
|
||||
|
||||
interface NotificationTextProps extends PropFunc<typeof Box> {
|
||||
contents: HarkContent[];
|
||||
}
|
||||
const NotificationText = ({ contents, ...rest }: NotificationTextProps) => {
|
||||
return (
|
||||
<>
|
||||
{contents.map((content, idx) => {
|
||||
if ('ship' in content) {
|
||||
return (
|
||||
<Mention
|
||||
key={idx}
|
||||
ship={content.ship}
|
||||
first={idx === 0}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <Text key={idx} {...rest}>{content.text}</Text>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export function Notification(props: {
|
||||
lid: HarkLid;
|
||||
notification: INotification;
|
||||
}) {
|
||||
const { time, notification, children, read = false } = props;
|
||||
const { notification, lid } = props;
|
||||
const read = !('unseen' in lid);
|
||||
const key = `${harkLidToId(lid)}-${harkBinToId(notification.bin)}`;
|
||||
const history = useHistory();
|
||||
|
||||
const isMobile = useLocalState(s => s.mobile);
|
||||
|
||||
const [archive, readNote] = useHarkState(s => [s.archive, s.readNote], shallow);
|
||||
|
||||
const onArchive = useCallback(async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
await archive(notification.index, time);
|
||||
}, [time, notification]);
|
||||
|
||||
const onClick = (e: any) => {
|
||||
if (!notification || read) {
|
||||
return;
|
||||
}
|
||||
return readNote(notification.index);
|
||||
};
|
||||
const onArchive = useCallback(
|
||||
async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!notification) {
|
||||
return;
|
||||
}
|
||||
useHarkState.getState().archiveNote(notification.bin, lid);
|
||||
},
|
||||
[notification, lid]
|
||||
);
|
||||
|
||||
const { hovering, bind } = useHovering();
|
||||
const dedupedBody = uniqBy(notification.body, item => item.link);
|
||||
const contents = map(dedupedBody, 'content').filter(
|
||||
c => c.length > 0
|
||||
);
|
||||
const first = notification.body[0];
|
||||
if (!first) {
|
||||
// should be unreachable
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClick = (e: any) => {
|
||||
const redirect = getNotificationRedirect(first.link);
|
||||
if(redirect) {
|
||||
history.push(redirect);
|
||||
} else {
|
||||
console.log('no redirect');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SwipeMenu
|
||||
key={(time && notification && getNotificationKey(time, notification)) ?? 'unknown'}
|
||||
key={key}
|
||||
m={2}
|
||||
menuWidth={100}
|
||||
disabled={!isMobile}
|
||||
menu={
|
||||
<Button onClick={onArchive} ml={2} height="100%" width="92px" primary destructive>
|
||||
<Button
|
||||
onClick={onArchive}
|
||||
ml={2}
|
||||
height="100%"
|
||||
width="92px"
|
||||
primary
|
||||
destructive
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
}
|
||||
@ -71,10 +119,27 @@ export function NotificationWrapper(props: {
|
||||
gridTemplateColumns={['1fr 24px', '1fr 200px']}
|
||||
gridTemplateRows="auto"
|
||||
gridTemplateAreas="'header actions' 'main main'"
|
||||
p={2}
|
||||
p={3}
|
||||
{...bind}
|
||||
>
|
||||
{children}
|
||||
<Col gapY={contents.length === 0 ? 0 : 2}>
|
||||
<Row>
|
||||
<NotificationText contents={first.title} fontWeight="medium" />
|
||||
</Row>
|
||||
<Col gapY="2">
|
||||
{take(contents, MAX_CONTENTS).map((content, i) => (
|
||||
<Box key={i}>
|
||||
<NotificationText lineHeight="tall" contents={content} />
|
||||
</Box>
|
||||
))}
|
||||
</Col>
|
||||
{contents.length > MAX_CONTENTS ? (
|
||||
<Text mt="2" gray display="block">
|
||||
and {contents.length - MAX_CONTENTS} more
|
||||
</Text>
|
||||
) : null}
|
||||
</Col>
|
||||
|
||||
<Row
|
||||
alignItems="flex-start"
|
||||
gapX={2}
|
||||
@ -82,7 +147,7 @@ export function NotificationWrapper(props: {
|
||||
justifyContent="flex-end"
|
||||
opacity={[0, hovering ? 1 : 0]}
|
||||
>
|
||||
{notification && (
|
||||
{!('time' in lid) && (
|
||||
<StatelessAsyncAction
|
||||
name=""
|
||||
borderRadius={1}
|
||||
@ -97,46 +162,3 @@ export function NotificationWrapper(props: {
|
||||
</SwipeMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export function Notification(props: NotificationProps) {
|
||||
const { notification, unread } = props;
|
||||
const { contents, time } = notification.notification;
|
||||
|
||||
const wrapperProps = {
|
||||
notification,
|
||||
read: !unread,
|
||||
time: props.time
|
||||
};
|
||||
|
||||
if ('graph' in notification.index) {
|
||||
const index = notification.index.graph;
|
||||
const c: GraphNotificationContents = (contents as any).graph;
|
||||
|
||||
return (
|
||||
<NotificationWrapper {...wrapperProps}>
|
||||
<GraphNotification
|
||||
index={index}
|
||||
contents={c}
|
||||
read={!unread}
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
/>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
if ('group' in notification.index) {
|
||||
const index = notification.index.group;
|
||||
const c: GroupNotificationContents = (contents as any).group;
|
||||
return (
|
||||
<NotificationWrapper {...wrapperProps}>
|
||||
<GroupNotification
|
||||
index={index}
|
||||
contents={c}
|
||||
time={time}
|
||||
/>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -1,29 +1,65 @@
|
||||
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import React, { ReactElement, useCallback, useRef } from 'react';
|
||||
import { Action, Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import React, { ReactElement, ReactNode, useEffect, useRef } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Link, Route, Switch } from 'react-router-dom';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import { Link, Route, Switch, useHistory, useLocation } from 'react-router-dom';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { Body } from '~/views/components/Body';
|
||||
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||
import Inbox from './inbox';
|
||||
import airlock from '~/logic/api';
|
||||
import { readAll } from '@urbit/api';
|
||||
import { Archive } from './Archive';
|
||||
import { NewBox } from './NewBox';
|
||||
|
||||
const baseUrl = '/~notifications';
|
||||
|
||||
export function NavLink({
|
||||
href,
|
||||
children
|
||||
}: {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const { push } = useHistory();
|
||||
|
||||
const isActive = href === location.pathname;
|
||||
|
||||
const onClick = () => {
|
||||
push(href);
|
||||
};
|
||||
|
||||
return (
|
||||
<Action
|
||||
backgroundColor="transparent"
|
||||
onClick={onClick}
|
||||
color={isActive ? 'black' : 'gray'}
|
||||
>
|
||||
{children}
|
||||
</Action>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NotificationsScreen(props: any): ReactElement {
|
||||
const relativePath = (p: string) => baseUrl + p;
|
||||
|
||||
const pendingJoin = useGroupState(s => s.pendingJoin);
|
||||
const onReadAll = useCallback(async () => {
|
||||
await airlock.poke(readAll());
|
||||
}, []);
|
||||
|
||||
const anchorRef = useRef<HTMLElement | null>(null);
|
||||
useTutorialModal('notifications', true, anchorRef);
|
||||
const notificationsCount = useHarkState(state => state.notificationsCount);
|
||||
const onReadAll = async () => {};
|
||||
|
||||
useEffect(() => {
|
||||
function visibilitychange() {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
useHarkState.getState().opened();
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', visibilitychange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', visibilitychange);
|
||||
useHarkState.getState().opened();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
@ -33,7 +69,10 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape - Notifications</title>
|
||||
<title>
|
||||
{notificationsCount ? `(${String(notificationsCount)}) ` : ''}
|
||||
Groups - Notifications
|
||||
</title>
|
||||
</Helmet>
|
||||
<Body>
|
||||
<Col overflowY="hidden" height="100%">
|
||||
@ -46,22 +85,29 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
borderBottom={1}
|
||||
borderBottomColor="lightGray"
|
||||
>
|
||||
|
||||
<Text fontWeight="bold" fontSize={2} lineHeight={1} ref={anchorRef}>
|
||||
Notifications
|
||||
</Text>
|
||||
<Row
|
||||
justifyContent="space-between"
|
||||
gapX={3}
|
||||
<Text
|
||||
fontWeight="bold"
|
||||
fontSize={2}
|
||||
lineHeight={1}
|
||||
ref={anchorRef}
|
||||
>
|
||||
<StatelessAsyncAction
|
||||
overflow="hidden"
|
||||
color="black"
|
||||
backgroundColor="white"
|
||||
onClick={onReadAll}
|
||||
>
|
||||
Mark All Read
|
||||
</StatelessAsyncAction>
|
||||
Notifications
|
||||
</Text>
|
||||
<Row gapX="2">
|
||||
<NavLink href="/~notifications">New</NavLink>
|
||||
<NavLink href="/~notifications/archive">Archive</NavLink>
|
||||
</Row>
|
||||
<Row justifyContent="space-between" gapX={3}>
|
||||
{ (false as boolean) ? (
|
||||
<StatelessAsyncAction
|
||||
overflow="hidden"
|
||||
color="black"
|
||||
backgroundColor="white"
|
||||
onClick={onReadAll}
|
||||
>
|
||||
Mark All Read
|
||||
</StatelessAsyncAction>
|
||||
) : null}
|
||||
<Link to="/~settings#notifications">
|
||||
<Box>
|
||||
<Icon lineHeight={1} icon="Adjust" />
|
||||
@ -69,11 +115,9 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
</Link>
|
||||
</Row>
|
||||
</Row>
|
||||
{!view && <Inbox
|
||||
pendingJoin={pendingJoin}
|
||||
{...props}
|
||||
filter={[]}
|
||||
/>}
|
||||
{ view === 'archive' ? (
|
||||
<Archive />
|
||||
) : <NewBox /> }
|
||||
</Col>
|
||||
</Body>
|
||||
</>
|
||||
|
@ -20,6 +20,7 @@ import ReconnectButton from './ReconnectButton';
|
||||
import { StatusBarItem } from './StatusBarItem';
|
||||
import { useTutorialModal } from './useTutorialModal';
|
||||
import { StatusBarJoins } from './StatusBarJoins';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
|
||||
const localSel = selectLocalState(['toggleOmnibox']);
|
||||
|
||||
@ -29,6 +30,7 @@ const StatusBar = (props) => {
|
||||
const metaKey = window.navigator.platform.includes('Mac') ? '⌘' : 'Ctrl+';
|
||||
const { toggleOmnibox } = useLocalState(localSel);
|
||||
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||
const notificationsCount = useHarkState(s => s.notificationsCount);
|
||||
|
||||
const color = ourContact ? `#${uxToHex(ourContact.color)}` : '#000';
|
||||
const xPadding = !hideAvatars && ourContact?.avatar ? '0' : '2';
|
||||
@ -75,7 +77,7 @@ const StatusBar = (props) => {
|
||||
>
|
||||
<Icon icon='Dashboard' color='black' />
|
||||
</Button>
|
||||
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
||||
<StatusBarItem position="relative" float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
||||
<Icon icon='LeapArrow' />
|
||||
<Text ref={anchorRef} ml={2} color='black'>
|
||||
Leap
|
||||
@ -83,6 +85,11 @@ const StatusBar = (props) => {
|
||||
<Text display={['none', 'inline']} ml={2} color='gray'>
|
||||
{metaKey}/
|
||||
</Text>
|
||||
{ notificationsCount > 0 && (
|
||||
<Box position="absolute" right="-8px" top="-8px">
|
||||
<Icon icon="Bullet" color="blue" />
|
||||
</Box>
|
||||
)}
|
||||
</StatusBarItem>
|
||||
<StatusBarJoins />
|
||||
<ReconnectButton />
|
||||
|
@ -55,7 +55,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
const [query, setQuery] = useState('');
|
||||
const [selected, setSelected] = useState<[] | [string, string]>([]);
|
||||
const contactState = useContactState(state => state.contacts);
|
||||
const notifications = useHarkState(state => state.notifications);
|
||||
const notificationCount = useHarkState(state => state.notificationsCount);
|
||||
const invites = useInviteState(state => state.invites);
|
||||
const tiles = useLaunchState(state => state.tiles);
|
||||
const [leapCursor, setLeapCursor] = useState('pointer');
|
||||
@ -119,7 +119,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
if (category === 'other') {
|
||||
return [
|
||||
'other',
|
||||
index.get('other').filter(({ app }) => app !== 'tutorial' && app !== 'inbox')
|
||||
index.get('other').filter(({ app }) => app !== 'tutorial')
|
||||
];
|
||||
}
|
||||
return [category, []];
|
||||
@ -163,7 +163,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
app === 'Links' ||
|
||||
app === 'Terminal' ||
|
||||
app === 'home' ||
|
||||
app === 'inbox'
|
||||
app === 'notifications'
|
||||
) {
|
||||
if(shift && app === 'profile') {
|
||||
// TODO: hacky, fix
|
||||
@ -351,6 +351,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
navigate={() => navigate(result.app, result.link, false)}
|
||||
setSelection={() => setSelection(result.app, result.link)}
|
||||
selected={sel}
|
||||
hasNotifications={notificationCount !== 0}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@ -358,7 +359,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}, [results, navigate, selected, contactState, notifications, invites]);
|
||||
}, [results, navigate, selected, contactState, invites]);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
|
@ -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 = (
|
||||
|
@ -2,7 +2,6 @@ import { Box } from '@tlon/indigo-react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
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';
|
||||
@ -15,6 +14,7 @@ import { useShortcut } from '~/logic/state/settings';
|
||||
|
||||
import Landscape from '~/views/landscape/index';
|
||||
import GraphApp from '../../apps/graph/App';
|
||||
import { getNotificationRedirect } from '~/logic/lib/notificationRedirects';
|
||||
|
||||
export const Container = styled(Box)`
|
||||
flex-grow: 1;
|
||||
@ -23,94 +23,6 @@ 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 getGraphRedirect(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'';
|
||||
}
|
||||
}
|
||||
|
||||
function getInviteRedirect(link: string) {
|
||||
const [,,app,uid] = link.split('/');
|
||||
return `/invites/${app}/${uid}`;
|
||||
}
|
||||
|
||||
function getDmRedirect(link: string) {
|
||||
const [,,ship] = link.split('/');
|
||||
return `/~landscape/messages/dm/${ship}`;
|
||||
}
|
||||
function getGroupRedirect(link: string) {
|
||||
const [,,ship,name] = link.split('/');
|
||||
return `/~landscape/ship/${ship}/${name}`;
|
||||
}
|
||||
|
||||
function getNotificationRedirect(link: string) {
|
||||
if(link.startsWith('/graph-validator')) {
|
||||
return getGraphRedirect(link);
|
||||
} else if (link.startsWith('/invite')) {
|
||||
return getInviteRedirect(link);
|
||||
} else if (link.startsWith('/dm')) {
|
||||
return getDmRedirect(link);
|
||||
} else if (link.startsWith('/groups')) {
|
||||
return getGroupRedirect(link);
|
||||
}
|
||||
}
|
||||
|
||||
export const Content = (props) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
@ -119,7 +31,7 @@ export const Content = (props) => {
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(location.search);
|
||||
if(mdLoaded && query.has('grid-note')) {
|
||||
history.push(getNotificationRedirect(query.get('grid-note')));
|
||||
history.push(getNotificationRedirect(query.get('grid-note')!));
|
||||
} else if(mdLoaded && query.has('grid-link')) {
|
||||
const link = decodeURIComponent(query.get('grid-link')!);
|
||||
history.push(`/perma${link}`);
|
||||
|
@ -1,7 +1,7 @@
|
||||
:~ title+'Groups'
|
||||
info+'A suite of applications to communicate on Urbit'
|
||||
color+0xee.5432
|
||||
glob-http+['https://bootstrap.urbit.org/glob-0v4d800.vqmrl.r0js2.ip98v.dd8sh.glob' 0v4d800.vqmrl.r0js2.ip98v.dd8sh]
|
||||
glob-http+['https://bootstrap.urbit.org/glob-0v1.9ge2e.2cb12.25972.u0l29.j0kuv.glob' 0v1.9ge2e.2cb12.25972.u0l29.j0kuv]
|
||||
base+'landscape'
|
||||
version+[1 3 5]
|
||||
website+'https://tlon.io'
|
||||
|
@ -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];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user