mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 16:51:42 +03:00
Merge pull request #4782 from urbit/lf/notif-fixes
notifications: preferences screen, fix pagination
This commit is contained in:
commit
fe61b8b7a9
@ -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) {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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> => {
|
||||
|
@ -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()}
|
||||
onClick={onArchive}
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
Dismiss
|
||||
</StatelessAsyncAction>
|
||||
</>
|
||||
<StatelessAsyncAction
|
||||
name={time.toString()}
|
||||
borderRadius={1}
|
||||
onClick={onArchive}
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Icon lineHeight="24px" size={16} icon="X" />
|
||||
</StatelessAsyncAction>
|
||||
)}
|
||||
</Row>
|
||||
</Box>
|
||||
|
@ -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>
|
||||
}
|
||||
>
|
||||
Mark All Read
|
||||
</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
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user