Merge pull request #4782 from urbit/lf/notif-fixes

notifications: preferences screen, fix pagination
This commit is contained in:
matildepark 2021-04-19 17:18:03 -04:00 committed by GitHub
commit fe61b8b7a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 221 additions and 68 deletions

View File

@ -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<StoreState> {
private harkAction(action: any): Promise<any> {
@ -172,10 +177,10 @@ export class HarkApi extends BaseApi<StoreState> {
}
async getMore(): Promise<boolean> {
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) {

View File

@ -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
);
}

View File

@ -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;

View File

@ -15,7 +15,7 @@ export interface HarkState extends BaseState<HarkState> {
notifications: BigIntOrderedMap<Timebox>;
notificationsCount: number;
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
notificationsGroupConfig: []; // TODO type this
notificationsGroupConfig: string[];
unreads: Unreads;
};

View File

@ -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<MetadataState>('Metadata', {
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
// preview: async (group): Promise<MetadataUpdatePreview> => {

View File

@ -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}
<Row
display={["none", "flex"]}
alignItems="center"
alignItems="flex-start"
gapX="2"
gridArea="actions"
justifyContent="flex-end"
opacity={[1, hovering ? 1 : 0]}
opacity={[1, (hovering || IS_MOBILE) ? 1 : 0]}
>
{time && notification && (
<>
<StatelessAsyncAction
name={changeMuteDesc}
onClick={onChangeMute}
backgroundColor="transparent"
>
{changeMuteDesc}
</StatelessAsyncAction>
<StatelessAsyncAction
name={time.toString()}
borderRadius={1}
onClick={onArchive}
backgroundColor="transparent"
backgroundColor="white"
>
Dismiss
<Icon lineHeight="24px" size={16} icon="X" />
</StatelessAsyncAction>
</>
)}
</Row>
</Box>

View File

@ -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"
>
<Text ref={anchorRef}>Notifications</Text>
<Text fontWeight="bold" fontSize="2" lineHeight="1" ref={anchorRef}>
Notifications
</Text>
<Row
justifyContent="space-between"
gapX="3"
>
<Box
mr="1"
<StatelessAsyncAction
overflow="hidden"
color="black"
onClick={onReadAll}
cursor="pointer"
>
<Text mr="1" color="blue">
Mark All Read
</Text>
</Box>
<Dropdown
alignX="right"
alignY="top"
options={
<Col
p="2"
backgroundColor="white"
border={1}
borderRadius={1}
borderColor="lightGray"
gapY="2"
>
<FormikOnBlur
initialValues={filter}
onSubmit={onSubmit}
>
<GroupSearch
id="groups"
label="Filter Groups"
caption="Only show notifications from this group"
/>
</FormikOnBlur>
</Col>
}
>
</StatelessAsyncAction>
<Link to="/~settings#notifications">
<Box>
<Text mr="1" gray>
Filter:
</Text>
<Text>{groupFilterDesc}</Text>
<Icon lineHeight="1" icon="Adjust" />
</Box>
</Dropdown>
</Link>
</Row>
</Row>
{!view && <Inbox

View File

@ -0,0 +1,135 @@
import React, { useState, useEffect } from "react";
import {
Box,
Text,
Icon,
ManagedToggleSwitchField,
StatelessToggleSwitchField,
Col,
Center,
} from "@tlon/indigo-react";
import _ from "lodash";
import useMetadataState, { useGraphsForGroup } from "~/logic/state/metadata";
import { Association, resourceFromPath } from "@urbit/api";
import { MetadataIcon } from "~/views/landscape/components/MetadataIcon";
import useGraphState from "~/logic/state/graph";
import { useField } from "formik";
import useHarkState from "~/logic/state/hark";
import { getModuleIcon } from "~/logic/lib/util";
import {isWatching} from "~/logic/lib/hark";
export function GroupChannelPicker(props: {}) {
const associations = useMetadataState((s) => s.associations);
return (
<Col gapY="3">
{_.map(associations.groups, (assoc: Association, group: string) => (
<GroupWithChannels key={group} association={assoc} />
))}
</Col>
);
}
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 (
<Box
display="grid"
gridTemplateColumns="24px 24px 1fr 24px 24px"
gridTemplateRows="auto"
gridGap="2"
gridTemplateAreas="'arrow icon title graphToggle groupToggle'"
>
{Object.keys(joinedGroupGraphs).length > 0 && (
<Center
cursor="pointer"
onClick={() => setOpen((o) => !o)}
gridArea="arrow"
>
<Icon icon={open ? "ChevronSouth" : "ChevronEast"} />
</Center>
)}
<MetadataIcon
size="24px"
gridArea="icon"
metadata={association.metadata}
/>
<Box gridArea="title">
<Text>{metadata.title}</Text>
</Box>
<Box gridArea="groupToggle">
<StatelessToggleSwitchField selected={value} onChange={onChange} />
</Box>
{open &&
_.map(joinedGroupGraphs, (a: Association, graph: string) => (
<Channel key={graph} association={a} />
))}
</Box>
);
}
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 (
<>
<Center gridColumn="2">
<Icon icon={icon} />
</Center>
<Box gridColumn="3">
<Text> {metadata.title}</Text>
</Box>
<Box gridColumn="4">
<StatelessToggleSwitchField selected={value} onChange={onChange} />
</Box>
</>
);
}

View File

@ -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"
/>
<Col gapY="3">
<Text lineHeight="tall">
Activity
</Text>
<Text gray>
Set which groups will send you notifications.
</Text>
<GroupChannelPicker />
</Col>
<AsyncButton primary width="fit-content">
Save
</AsyncButton>