From 048bd02604e528f5ddee75b53b58ab83c3432904 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Mon, 19 Apr 2021 13:15:46 +1000 Subject: [PATCH 1/3] notifications: fix broken pagination Fixes urbit/landscape#788 --- pkg/interface/src/logic/api/hark.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index b807201a1a..42fe0ae316 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -4,6 +4,11 @@ import { dateToDa, decToUd } from '../lib/util'; import { NotifIndex, IndexedNotification, Association, GraphNotifDescription } from '@urbit/api'; import { BigInteger } from 'big-integer'; import { getParentIndex } from '../lib/notification'; +import useHarkState from '../state/hark'; + +function getHarkSize() { + return useHarkState.getState().notifications.size ?? 0; +} export class HarkApi extends BaseApi { private harkAction(action: any): Promise { @@ -172,10 +177,10 @@ export class HarkApi extends BaseApi { } async getMore(): Promise { - const offset = this.store.state['notifications']?.size || 0; + const offset = getHarkSize(); const count = 3; await this.getSubset(offset, count, false); - return offset === (this.store.state.notifications?.size || 0); + return offset === getHarkSize(); } async getSubset(offset:number, count:number, isArchive: boolean) { From 0ef452f8b3def10834f0940a9b41770797a7ddca Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Mon, 19 Apr 2021 13:26:21 +1000 Subject: [PATCH 2/3] notifications: update dismiss action styling --- pkg/interface/src/logic/lib/platform.ts | 4 +++ .../views/apps/notifications/notification.tsx | 36 ++++++++----------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/pkg/interface/src/logic/lib/platform.ts b/pkg/interface/src/logic/lib/platform.ts index b02870fea1..19234e0ddf 100644 --- a/pkg/interface/src/logic/lib/platform.ts +++ b/pkg/interface/src/logic/lib/platform.ts @@ -4,3 +4,7 @@ const ua = window.navigator.userAgent; export const IS_IOS = ua.includes('iPhone'); export const IS_SAFARI = ua.includes('Safari') && !ua.includes('Chrome'); + +export const IS_ANDROID = ua.includes('Android'); + +export const IS_MOBILE = IS_IOS || IS_ANDROID; diff --git a/pkg/interface/src/views/apps/notifications/notification.tsx b/pkg/interface/src/views/apps/notifications/notification.tsx index 2676ddd62b..879bf03e3a 100644 --- a/pkg/interface/src/views/apps/notifications/notification.tsx +++ b/pkg/interface/src/views/apps/notifications/notification.tsx @@ -1,5 +1,5 @@ import React, { ReactNode, useCallback, useMemo, useState } from "react"; -import { Row, Box } from "@tlon/indigo-react"; +import { Row, Box, Icon } from "@tlon/indigo-react"; import _ from "lodash"; import { GraphNotificationContents, @@ -19,6 +19,7 @@ import { GraphNotification } from "./graph"; import { BigInteger } from "big-integer"; import { useHovering } from "~/logic/lib/util"; import useHarkState from "~/logic/state/hark"; +import {IS_MOBILE} from "~/logic/lib/platform"; interface NotificationProps { notification: IndexedNotification; @@ -102,39 +103,30 @@ export function NotificationWrapper(props: { } borderRadius={2} display="grid" - gridTemplateColumns={["1fr", "1fr 200px"]} + gridTemplateColumns={["1fr 24px", "1fr 200px"]} gridTemplateRows="auto" - gridTemplateAreas={["'header' 'main'", "'header actions' 'main main'"]} + gridTemplateAreas="'header actions' 'main main'" p={2} m={2} {...bind} > {children} {time && notification && ( - <> - - {changeMuteDesc} - - - Dismiss - - + + + )} From 6128c8d0967996c6bdfa6e214f94ac18ccfc5c37 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Mon, 19 Apr 2021 15:19:13 +1000 Subject: [PATCH 3/3] notifications: bring preferences to spec --- pkg/interface/src/logic/lib/hark.ts | 12 +- pkg/interface/src/logic/state/hark.ts | 2 +- pkg/interface/src/logic/state/metadata.ts | 6 + .../apps/notifications/notifications.tsx | 57 ++------ .../components/lib/GroupChannelPicker.tsx | 135 ++++++++++++++++++ .../components/lib/NotificationPref.tsx | 28 ++++ 6 files changed, 196 insertions(+), 44 deletions(-) create mode 100644 pkg/interface/src/views/apps/settings/components/lib/GroupChannelPicker.tsx diff --git a/pkg/interface/src/logic/lib/hark.ts b/pkg/interface/src/logic/lib/hark.ts index 54dd37e09b..2e7a6b3d9e 100644 --- a/pkg/interface/src/logic/lib/hark.ts +++ b/pkg/interface/src/logic/lib/hark.ts @@ -1,6 +1,6 @@ import bigInt, { BigInteger } from 'big-integer'; import f from 'lodash/fp'; -import { Unreads } from '@urbit/api'; +import { Unreads, NotificationGraphConfig } from '@urbit/api'; export function getLastSeen( unreads: Unreads, @@ -34,3 +34,13 @@ export function getNotificationCount( .map(index => unread[index]?.notifications?.length || 0) .reduce(f.add, 0); } + +export function isWatching( + config: NotificationGraphConfig, + graph: string, + index = "/" +) { + return !!config.watching.find( + watch => watch.graph === graph && watch.index === index + ); +} diff --git a/pkg/interface/src/logic/state/hark.ts b/pkg/interface/src/logic/state/hark.ts index d02402c098..047723c947 100644 --- a/pkg/interface/src/logic/state/hark.ts +++ b/pkg/interface/src/logic/state/hark.ts @@ -15,7 +15,7 @@ export interface HarkState extends BaseState { notifications: BigIntOrderedMap; notificationsCount: number; notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere - notificationsGroupConfig: []; // TODO type this + notificationsGroupConfig: string[]; unreads: Unreads; }; diff --git a/pkg/interface/src/logic/state/metadata.ts b/pkg/interface/src/logic/state/metadata.ts index f09e824eed..a3c462193d 100644 --- a/pkg/interface/src/logic/state/metadata.ts +++ b/pkg/interface/src/logic/state/metadata.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import _ from 'lodash'; import { MetadataUpdatePreview, Association, Associations } from "@urbit/api"; import { BaseState, createState } from "./base"; @@ -18,6 +19,11 @@ export function useAssocForGroup(group: string) { return useMetadataState(useCallback(s => s.associations.groups[group] as Association | undefined, [group])); } +export function useGraphsForGroup(group: string) { + const graphs = useMetadataState(s => s.associations.graph); + return _.pickBy(graphs, (a: Association) => a.group === group); +} + const useMetadataState = createState('Metadata', { associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} }, // preview: async (group): Promise => { diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index 1e801b538b..cb916dd684 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import { Link, Switch, Route } from 'react-router-dom'; import Helmet from 'react-helmet'; -import { Box, Col, Text, Row } from '@tlon/indigo-react'; +import { Box, Icon, Col, Text, Row } from '@tlon/indigo-react'; import { Body } from '~/views/components/Body'; import { PropFunc } from '~/types/util'; @@ -15,6 +15,7 @@ import { useTutorialModal } from '~/views/components/useTutorialModal'; import useHarkState from '~/logic/state/hark'; import useMetadataState from '~/logic/state/metadata'; import useGroupState from '~/logic/state/group'; +import {StatelessAsyncAction} from '~/views/components/StatelessAsyncAction'; const baseUrl = '/~notifications'; @@ -46,8 +47,8 @@ export default function NotificationsScreen(props: any): ReactElement { const onSubmit = async ({ groups } : NotificationFilter) => { setFilter({ groups }); }; - const onReadAll = useCallback(() => { - props.api.hark.readAll(); + const onReadAll = useCallback(async () => { + await props.api.hark.readAll(); }, []); const groupFilterDesc = filter.groups.length === 0 @@ -81,53 +82,25 @@ export default function NotificationsScreen(props: any): ReactElement { borderBottomColor="lightGray" > - Notifications + + Notifications + - - - Mark All Read - - - - - - - - - } > + Mark All Read + + - - Filter: - - {groupFilterDesc} + - + {!view && s.associations); + + return ( + + {_.map(associations.groups, (assoc: Association, group: string) => ( + + ))} + + ); +} + +function GroupWithChannels(props: { association: Association }) { + const { association } = props; + const { metadata } = association; + + const groupWatched = useHarkState((s) => + s.notificationsGroupConfig.includes(association.group) + ); + + const [{ value }, meta, { setValue }] = useField( + `groups["${association.group}"]` + ); + + + const onChange = () => { + setValue(!value); + }; + + + useEffect(() => { + setValue(groupWatched); + }, []); + + const graphs = useGraphsForGroup(association.group); + const joinedGraphs = useGraphState((s) => s.graphKeys); + const joinedGroupGraphs = _.pickBy(graphs, (_, graph: string) => { + const { ship, name } = resourceFromPath(graph); + return joinedGraphs.has(`${ship.slice(1)}/${name}`); + }); + + const [open, setOpen] = useState(false); + + return ( + + {Object.keys(joinedGroupGraphs).length > 0 && ( +
setOpen((o) => !o)} + gridArea="arrow" + > + +
+ )} + + + {metadata.title} + + + + + {open && + _.map(joinedGroupGraphs, (a: Association, graph: string) => ( + + ))} +
+ ); +} +function Channel(props: { association: Association }) { + const { association } = props; + const { metadata } = association; + const watching = useHarkState((s) => { + const config = s.notificationsGraphConfig; + return isWatching(config, association.resource); + }); + + const [{ value }, meta, { setValue }] = useField( + `graph["${association.resource}"]` + ); + + useEffect(() => { + setValue(watching); + }, [watching]); + + const onChange = () => { + setValue(!value); + }; + + const icon = getModuleIcon(metadata.config?.graph); + + return ( + <> +
+ +
+ + {metadata.title} + + + + + + ); +} diff --git a/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx b/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx index c673f326a8..3759572462 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx @@ -10,11 +10,19 @@ import GlobalApi from "~/logic/api/global"; import useHarkState from "~/logic/state/hark"; import _ from "lodash"; import {AsyncButton} from "~/views/components/AsyncButton"; +import {GroupChannelPicker} from "./GroupChannelPicker"; +import {isWatching} from "~/logic/lib/hark"; interface FormSchema { mentions: boolean; dnd: boolean; watchOnSelf: boolean; + graph: { + [rid: string]: boolean; + }; + groups: { + [rid: string]: boolean; + } } export function NotificationPreferences(props: { @@ -23,6 +31,7 @@ export function NotificationPreferences(props: { const { api } = props; const dnd = useHarkState(state => state.doNotDisturb); const graphConfig = useHarkState(state => state.notificationsGraphConfig); + const groupConfig = useHarkState(s => s.notificationsGroupConfig); const initialValues = { mentions: graphConfig.mentions, dnd: dnd, @@ -41,6 +50,16 @@ export function NotificationPreferences(props: { if (values.dnd !== dnd && !_.isUndefined(values.dnd)) { promises.push(api.hark.setDoNotDisturb(values.dnd)) } + _.forEach(values.graph, (listen: boolean, graph: string) => { + if(listen !== isWatching(graphConfig, graph)) { + promises.push(api.hark[listen ? "listenGraph" : "ignoreGraph"](graph, "/")) + } + }); + _.forEach(values.groups, (listen: boolean, group: string) => { + if(listen !== groupConfig.includes(group)) { + promises.push(api.hark[listen ? "listenGroup" : "ignoreGroup"](group)); + } + }); await Promise.all(promises); actions.setStatus({ success: null }); @@ -81,6 +100,15 @@ export function NotificationPreferences(props: { id="mentions" caption="Notify me if someone mentions my @p in a channel I've joined" /> + + + Activity + + + Set which groups will send you notifications. + + + Save