Merge pull request #3915 from urbit/lf/hark-qa-fixes

hark: lazier notifications loading, final bugfixes
This commit is contained in:
matildepark 2020-11-11 22:27:04 -05:00 committed by GitHub
commit 118e7d2c8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 198 additions and 80 deletions

View File

@ -48,7 +48,7 @@
(on-watch:def path) (on-watch:def path)
:_ this :_ this
=; =cage =; =cage
[%give %fact ~[/updates] cage]~ [%give %fact ~ cage]~
:- %hark-group-hook-update :- %hark-group-hook-update
!> ^- update:hook !> ^- update:hook
[%initial watching] [%initial watching]

View File

@ -15,7 +15,7 @@
=notifications:store =notifications:store
archive=notifications:store archive=notifications:store
last-seen=@da last-seen=@da
dnd=? dnd=_|
== ==
+$ inflated-state +$ inflated-state
$: state-0 $: state-0
@ -25,6 +25,7 @@
:: albeit expensively :: albeit expensively
+$ cache +$ cache
$: unread-count=@ud $: unread-count=@ud
graph-unreads=(map resource @ud)
~ ~
== ==
:: ::
@ -71,14 +72,17 @@
^- update:store ^- update:store
:- %more :- %more
^- (list update:store) ^- (list update:store)
:- [%graph-unreads graph-unreads]
:+ [%set-dnd dnd] :+ [%set-dnd dnd]
[%count unread-count] [%count unread-count]
%+ weld %+ weld
%+ turn %+ turn
(tap-nonempty archive) %+ scag 5
(tap-nonempty:ha archive)
(timebox-update &) (timebox-update &)
%+ turn %+ turn
(tap-nonempty notifications) %+ scag 5
(tap-nonempty:ha notifications)
(timebox-update |) (timebox-update |)
:: ::
++ timebox-update ++ timebox-update
@ -86,12 +90,6 @@
|= [time=@da =timebox:store] |= [time=@da =timebox:store]
^- update:store ^- update:store
[%timebox time archived ~(tap by timebox)] [%timebox time archived ~(tap by timebox)]
::
++ tap-nonempty
|= =notifications:store
^- (list [@da timebox:store])
%+ skip (tap:orm notifications)
|=([@da =timebox:store] =(0 ~(wyt by timebox)))
-- --
:: ::
++ on-peek ++ on-peek
@ -104,11 +102,13 @@
(slav %ud i.t.t.path) (slav %ud i.t.t.path)
=/ length=@ud =/ length=@ud
(slav %ud i.t.t.t.path) (slav %ud i.t.t.t.path)
:^ ~ ~ %noun :^ ~ ~ %hark-update
!> ^- update:store !> ^- update:store
:- %more :- %more
%+ turn %+ turn
(scag length (slag offset (tap:orm notifications))) %+ scag length
%+ slag offset
(tap-nonempty:ha notifications)
|= [time=@da =timebox:store] |= [time=@da =timebox:store]
^- update:store ^- update:store
:^ %timebox time %.n :^ %timebox time %.n
@ -154,6 +154,7 @@
(~(put by timebox) index new) (~(put by timebox) index new)
:- (give:ha [/updates]~ %added last-seen index new) :- (give:ha [/updates]~ %added last-seen index new)
%_ state %_ state
+ ?~(existing-notif (upd-unreads:ha index %.n) +.state)
notifications (put:orm notifications last-seen new-timebox) notifications (put:orm notifications last-seen new-timebox)
unread-count ?~(existing-notif +(unread-count) unread-count) unread-count ?~(existing-notif +(unread-count) unread-count)
== ==
@ -169,7 +170,7 @@
(~(del by timebox) index) (~(del by timebox) index)
:- (give:ha [/updates]~ %archive time index) :- (give:ha [/updates]~ %archive time index)
%_ state %_ state
unread-count ?.(read.notification (dec unread-count) unread-count) + ?.(read.notification (upd-unreads:ha index %.y) +.state)
:: ::
notifications notifications
(put:orm notifications time new-timebox) (put:orm notifications time new-timebox)
@ -186,6 +187,7 @@
^- (quip card _state) ^- (quip card _state)
:- (give:ha [/updates]~ %read time index) :- (give:ha [/updates]~ %read time index)
%_ state %_ state
+ (upd-unreads:ha index %.y)
unread-count (dec unread-count) unread-count (dec unread-count)
notifications (change-read-status:ha time index %.y) notifications (change-read-status:ha time index %.y)
== ==
@ -195,6 +197,7 @@
^- (quip card _state) ^- (quip card _state)
:- (give:ha [/updates]~ %unread time index) :- (give:ha [/updates]~ %unread time index)
%_ state %_ state
+ (upd-unreads:ha index %.n)
unread-count +(unread-count) unread-count +(unread-count)
notifications (change-read-status:ha time index %.n) notifications (change-read-status:ha time index %.n)
== ==
@ -231,6 +234,12 @@
|_ =bowl:gall |_ =bowl:gall
+* met ~(. metadata bowl) +* met ~(. metadata bowl)
:: ::
++ tap-nonempty
|= =notifications:store
^- (list [@da timebox:store])
%+ skip (tap:orm notifications)
|=([@da =timebox:store] =(0 ~(wyt by timebox)))
::
++ merge-notification ++ merge-notification
|= [existing=notification:store new=notification:store] |= [existing=notification:store new=notification:store]
^- notification:store ^- notification:store
@ -292,22 +301,37 @@
^- (list card) ^- (list card)
[%give %fact paths [%hark-update !>(update)]]~ [%give %fact paths [%hark-update !>(update)]]~
:: ::
++ upd-unreads
|= [=index:store read=?]
^+ +.state
=/ f=$-(@ @)
?: read
dec
|=(a=@ +(a))
=. unread-count (f unread-count)
?. ?=(%graph -.index)
+.state
=/ curr-unread=@ud
(~(gut by graph-unreads) graph.index 0)
+.state(graph-unreads (~(put by graph-unreads) graph.index (f curr-unread)))
::
++ inflate-cache ++ inflate-cache
|= state-0 |= state-0
^- cache ^+ +.state
:_ ~ =/ nots=(list [p=@da =timebox:store])
%+ roll
(tap:orm notifications) (tap:orm notifications)
|= [[time=@da =timebox:store] out=@ud] |- =* outer $
=/ unreads ~(tap by timebox) ?~ nots
|- +.state
?~ unreads out =/ unreads ~(tap by timebox.i.nots)
|- =* inner $
?~ unreads
outer(nots t.nots)
=* notification q.i.unreads =* notification q.i.unreads
=* index p.i.unreads
?: read.notification ?: read.notification
out inner(unreads t.unreads)
%_ $ =. +.state
unreads t.unreads (upd-unreads index read.notification)
:: inner(unreads t.unreads)
out +(out)
==
-- --

View File

@ -78,12 +78,22 @@
%timebox (timebox +.upd) %timebox (timebox +.upd)
%set-dnd b+dnd.upd %set-dnd b+dnd.upd
%count (numb count.upd) %count (numb count.upd)
%graph-unreads (graph-unreads map.upd)
%more (more +.upd) %more (more +.upd)
:: ::
?(%archive %read %unread) ?(%archive %read %unread)
(notif-ref +.upd) (notif-ref +.upd)
== ==
:: ::
++ graph-unreads
|= =(map resource @ud)
^- json
%- pairs
%+ turn
~(tap by map)
|= [rid=resource unread=@ud]
(enjs-path:resource rid)^(numb unread)
::
++ added ++ added
|= [tim=@da idx=^index not=^notification] |= [tim=@da idx=^index not=^notification]
^- json ^- json

View File

@ -46,5 +46,6 @@
[%added time=@da =index =notification] [%added time=@da =index =notification]
[%timebox time=@da archived=? =(list [index notification])] [%timebox time=@da archived=? =(list [index notification])]
[%count count=@ud] [%count count=@ud]
[%graph-unreads =(map resource @ud)]
== ==
-- --

View File

@ -149,10 +149,21 @@ export class HarkApi extends BaseApi<StoreState> {
}); });
} }
getMore() {
const offset = this.store.state.notifications.size;
const count = 10;
return this.getSubset(offset,count);
}
async getSubset(offset:number, count:number) {
const data = await this.scry("hark-store", `/recent/${offset}/${count}`);
this.store.handleEvent({ data });
}
async getTimeSubset(start?: Date, end?: Date) { async getTimeSubset(start?: Date, end?: Date) {
const s = start ? dateToDa(start) : "-"; const s = start ? dateToDa(start) : "-";
const e = end ? dateToDa(end) : "-"; const e = end ? dateToDa(end) : "-";
const result = await this.scry("hark-hook", `/time-subset/${s}/${e}`); const result = await this.scry("hark-hook", `/recent/${s}/${e}`);
this.store.handleEvent({ this.store.handleEvent({
data: result, data: result,
}); });

View File

@ -23,7 +23,6 @@ export const HarkReducer = (json: any, state: HarkState) => {
} }
const graphHookData = _.get(json, "hark-graph-hook-update", false); const graphHookData = _.get(json, "hark-graph-hook-update", false);
if (graphHookData) { if (graphHookData) {
console.log(graphHookData);
graphInitial(graphHookData, state); graphInitial(graphHookData, state);
graphIgnore(graphHookData, state); graphIgnore(graphHookData, state);
graphListen(graphHookData, state); graphListen(graphHookData, state);
@ -133,6 +132,7 @@ function graphWatchSelf(json: any, state: HarkState) {
} }
function reduce(data: any, state: HarkState) { function reduce(data: any, state: HarkState) {
console.log(data);
unread(data, state); unread(data, state);
read(data, state); read(data, state);
archive(data, state); archive(data, state);
@ -141,6 +141,14 @@ function reduce(data: any, state: HarkState) {
dnd(data, state); dnd(data, state);
count(data, state); count(data, state);
added(data, state); added(data, state);
graphUnreads(data, state);
}
function graphUnreads(json: any, state: HarkState) {
const data = _.get(json, 'graph-unreads');
if(data) {
state.graphUnreads = data;
}
} }
function added(json: any, state: HarkState) { function added(json: any, state: HarkState) {
@ -158,6 +166,10 @@ function added(json: any, state: HarkState) {
} else { } else {
state.notifications.set(time, [...timebox, { index, notification }]); state.notifications.set(time, [...timebox, { index, notification }]);
state.notificationsCount++; state.notificationsCount++;
if('graph' in index) {
const curr = state.graphUnreads[index.graph.graph] || 0;
state.graphUnreads[index.graph.graph] = curr+1;
}
} }
} }
} }
@ -243,6 +255,11 @@ function read(json: any, state: HarkState) {
if (data) { if (data) {
const { time, index } = data; const { time, index } = data;
state.notificationsCount--; state.notificationsCount--;
if('graph' in index) {
const curr = state.graphUnreads[index.graph.graph] || 0;
state.graphUnreads[index.graph.graph] = curr-1;
}
setRead(time, index, true, state); setRead(time, index, true, state);
} }
} }
@ -252,6 +269,10 @@ function unread(json: any, state: HarkState) {
if (data) { if (data) {
const { time, index } = data; const { time, index } = data;
state.notificationsCount++; state.notificationsCount++;
if('graph' in index) {
const curr = state.graphUnreads[index.graph.graph] || 0;
state.graphUnreads[index.graph.graph] = curr+1;
}
setRead(time, index, false, state); setRead(time, index, false, state);
} }
} }

View File

@ -106,7 +106,8 @@ export default class GlobalStore extends BaseStore<StoreState> {
mentions: false, mentions: false,
watching: [], watching: [],
}, },
notificationsCount: 0 notificationsCount: 0,
graphUnreads: {}
}; };
} }

View File

@ -65,4 +65,5 @@ export interface StoreState {
notificationsChatConfig: string[]; notificationsChatConfig: string[];
notificationsCount: number, notificationsCount: number,
doNotDisturb: boolean; doNotDisturb: boolean;
graphUnreads: Record<string, number>;
} }

View File

@ -86,7 +86,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
unreadMarkerRef, unreadMarkerRef,
history, history,
api, api,
highlighted highlighted,
fontSize
} = this.props; } = this.props;
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1); const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
@ -118,7 +119,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
history, history,
api, api,
scrollWindow, scrollWindow,
highlighted highlighted,
fontSize
}; };
const unreadContainerStyle = { const unreadContainerStyle = {
@ -131,7 +133,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
width='100%' width='100%'
display='flex' display='flex'
flexWrap='wrap' flexWrap='wrap'
pt={renderSigil ? 3 : 0} pt={this.props.pt ? this.props.pt : renderSigil ? 3 : 0}
pr={3} pr={3}
pb={isLastMessage ? 3 : 0} pb={isLastMessage ? 3 : 0}
ref={this.divRef} ref={this.divRef}
@ -170,7 +172,7 @@ interface MessageProps {
export class MessageWithSigil extends PureComponent<MessageProps> { export class MessageWithSigil extends PureComponent<MessageProps> {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
render() { render() {
const { const {
msg, msg,
@ -184,14 +186,15 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
measure, measure,
api, api,
history, history,
scrollWindow scrollWindow,
fontSize
} = this.props; } = this.props;
const datestamp = moment.unix(msg.when / 1000).format(DATESTAMP_FORMAT); const datestamp = moment.unix(msg.when / 1000).format(DATESTAMP_FORMAT);
const contact = msg.author in contacts ? contacts[msg.author] : false; const contact = msg.author in contacts ? contacts[msg.author] : false;
const showNickname = !hideNicknames && contact && contact.nickname; const showNickname = !hideNicknames && contact && contact.nickname;
const name = showNickname ? contact.nickname : cite(msg.author); const name = showNickname ? contact.nickname : cite(msg.author);
const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF' const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF'
const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken'; const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken';
let nameSpan = null; let nameSpan = null;
@ -245,7 +248,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text> <Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
<Text gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text> <Text gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
</Box> </Box>
<Box fontSize='14px'><MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} /></Box> <Box fontSize={fontSize ? fontSize : '14px'}><MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} fontSize={fontSize} /></Box>
</Box> </Box>
</> </>
); );
@ -261,12 +264,12 @@ export const MessageWithoutSigil = ({ timestamp, msg, remoteContentPolicy, measu
</> </>
); );
export const MessageContent = ({ content, remoteContentPolicy, measure }) => { export const MessageContent = ({ content, remoteContentPolicy, measure, fontSize }) => {
if ('code' in content) { if ('code' in content) {
return <CodeContent content={content} />; return <CodeContent content={content} />;
} else if ('url' in content) { } else if ('url' in content) {
return ( return (
<Text fontSize='14px' lineHeight="tall" color='black'> <Text fontSize={fontSize ? fontSize : '14px'} lineHeight="tall" color='black'>
<RemoteContent <RemoteContent
url={content.url} url={content.url}
remoteContentPolicy={remoteContentPolicy} remoteContentPolicy={remoteContentPolicy}
@ -282,13 +285,13 @@ export const MessageContent = ({ content, remoteContentPolicy, measure }) => {
); );
} else if ('me' in content) { } else if ('me' in content) {
return ( return (
<Text fontStyle='italic' fontSize='14px' lineHeight='tall' color='black'> <Text fontStyle='italic' fontSize={fontSize ? fontSize : '14px'} lineHeight='tall' color='black'>
{content.me} {content.me}
</Text> </Text>
); );
} }
else if ('text' in content) { else if ('text' in content) {
return <TextContent content={content} />; return <TextContent fontSize={fontSize} content={content} />;
} else { } else {
return null; return null;
} }

View File

@ -16,7 +16,6 @@ export default class CodeContent extends Component {
p='1' p='1'
my='0' my='0'
borderRadius='1' borderRadius='1'
fontSize='14px'
overflow='auto' overflow='auto'
maxHeight='10em' maxHeight='10em'
maxWidth='100%' maxWidth='100%'
@ -35,7 +34,6 @@ export default class CodeContent extends Component {
my='0' my='0'
p='1' p='1'
borderRadius='1' borderRadius='1'
fontSize='14px'
overflow='auto' overflow='auto'
maxHeight='10em' maxHeight='10em'
maxWidth='100%' maxWidth='100%'

View File

@ -26,13 +26,12 @@ const DISABLED_INLINE_TOKENS = [
const renderers = { const renderers = {
inlineCode: ({language, value}) => { inlineCode: ({language, value}) => {
return <Text mono fontSize='14px' backgroundColor='washedGray' style={{ whiteSpace: 'preWrap'}}>{value}</Text> return <Text mono backgroundColor='washedGray' style={{ whiteSpace: 'preWrap'}}>{value}</Text>
}, },
code: ({language, value}) => { code: ({language, value}) => {
return <Text return <Text
p='1' p='1'
className='clamp-message' className='clamp-message'
fontSize='14px'
display='block' display='block'
borderRadius='1' borderRadius='1'
mono mono
@ -84,7 +83,7 @@ export default class TextContent extends Component {
&& (urbitOb.isValidPatp(group[2]) // valid patp? && (urbitOb.isValidPatp(group[2]) // valid patp?
&& (group[0] === content.text))) { // entire message is room name? && (group[0] === content.text))) { // entire message is room name?
return ( return (
<Text fontSize='14px' color='black' lineHeight="tall"> <Text fontSize={props.fontSize ? props.fontSize : '14px'} color='black' lineHeight="tall">
<Link <Link
className="bb b--black b--white-d mono" className="bb b--black b--white-d mono"
to={'/~landscape/join/' + group.input}> to={'/~landscape/join/' + group.input}>
@ -94,7 +93,7 @@ export default class TextContent extends Component {
); );
} else { } else {
return ( return (
<Text color='black' fontSize='14px' lineHeight="tall" style={{ overflowWrap: 'break-word' }}> <Text color='black' fontSize={props.fontSize ? props.fontSize : '14px'} lineHeight="tall" style={{ overflowWrap: 'break-word' }}>
<MessageMarkdown source={content.text} /> <MessageMarkdown source={content.text} />
</Text> </Text>
); );

View File

@ -83,6 +83,8 @@ export function ChatNotification(props: {
isLastRead={false} isLastRead={false}
group={group} group={group}
contacts={groupContacts} contacts={groupContacts}
fontSize='0'
pt='2'
/> />
</Link> </Link>
); );

View File

@ -51,8 +51,8 @@ function describeNotification(description: string, plural: boolean) {
} }
const GraphUrl = ({ url, title }) => ( const GraphUrl = ({ url, title }) => (
<Box borderRadius="1" p="2" bg="washedGray"> <Box borderRadius="2" p="2" bg="scales.black05">
<Anchor target="_blank" color="gray" href={url}> <Anchor underline={false} target="_blank" color="black" href={url}>
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" /> <Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
{title} {title}
</Anchor> </Anchor>
@ -140,7 +140,7 @@ const GraphNode = ({
return ( return (
<Link to={nodeUrl}> <Link to={nodeUrl}>
<Row gapX="2" py="2"> <Row gapX="2" pt="2">
<Col>{img}</Col> <Col>{img}</Col>
<Col alignItems="flex-start"> <Col alignItems="flex-start">
<Row <Row

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import React, { useEffect, useCallback } from "react";
import f from "lodash/fp"; import f from "lodash/fp";
import _ from "lodash"; import _ from "lodash";
import { Icon, Col, Row, Box, Text, Anchor } from "@tlon/indigo-react"; import { Icon, Col, Row, Box, Text, Anchor } from "@tlon/indigo-react";
@ -69,13 +69,20 @@ export default function Inbox(props: {
f.values f.values
)(notifications); )(notifications);
const onScroll = useCallback((e) => {
let container = e.target;
if(!props.showArchive && (container.scrollHeight - container.scrollTop === container.clientHeight)) {
api.hark.getMore();
}
}, [api]);
const incomingGroups = Object.values(invites?.['contacts'] || {}); const incomingGroups = Object.values(invites?.['contacts'] || {});
const getKeyByValue = (object, value) => { const getKeyByValue = (object, value) => {
return Object.keys(object).find(key => object[key] === value); return Object.keys(object).find(key => object[key] === value);
}; };
const acceptInvite = (invite) => { const acceptInvite = (invite) => {
const resource = { const resource = {
ship: `~${invite.resource.ship}`, ship: `~${invite.resource.ship}`,
name: invite.resource.name name: invite.resource.name
@ -86,11 +93,9 @@ const acceptInvite = (invite) => {
}; };
return ( return (
<Col overflowY="auto" flexGrow="1"> <Col onScroll={onScroll} overflowY="auto" flexGrow="1" minHeight='0' flexShrink='0'>
{incomingGroups.map((invite) => ( {incomingGroups.map((invite) => (
<Box <Box
height='100%'
width='100%'
bg='white' bg='white'
p='3' p='3'
fontSize='0'> fontSize='0'>

View File

@ -87,9 +87,9 @@ function NotificationWrapper(props: {
const changeMuteDesc = isMuted ? "Unmute" : "Mute"; const changeMuteDesc = isMuted ? "Unmute" : "Mute";
return ( return (
<Row alignItems="center" justifyContent="space-between"> <Row alignItems="top" justifyContent="space-between">
{children} {children}
<Row gapX="2" p="2" alignItems="center"> <Row gapX="2" p="2" pt='3' alignItems="top">
<StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute} backgroundColor="transparent"> <StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute} backgroundColor="transparent">
{changeMuteDesc} {changeMuteDesc}
</StatelessAsyncAction> </StatelessAsyncAction>

View File

@ -53,7 +53,7 @@ export default function NotificationsScreen(props: any) {
const { view } = routeProps.match.params; const { view } = routeProps.match.params;
return ( return (
<Body> <Body>
<Col height="100%"> <Col height="100%" minHeight='0' overflowY='scroll'>
<Row <Row
p="3" p="3"
alignItems="center" alignItems="center"

View File

@ -0,0 +1,36 @@
import React, { ReactNode, useState, useEffect, useCallback } from "react";
import {
StatelessToggleSwitchField as Toggle,
LoadingSpinner,
Text
} from "@tlon/indigo-react";
import { useFormikContext } from "formik";
import { useStatelessAsyncClickable } from "~/logic/lib/useStatelessAsyncClickable";
interface AsyncToggleProps {
name?: string;
onClick: (e: React.MouseEvent) => Promise<void>;
}
export function StatelessAsyncToggle({
onClick,
name = "",
...rest
}: AsyncToggleProps & Parameters<typeof Toggle>[0]) {
const {
onClick: handleClick,
buttonState: state,
} = useStatelessAsyncClickable(onClick, name);
return state === "error" ? (
<Text mr="2">Error</Text>
) : state === "loading" ? (
<LoadingSpinner mr="2" foreground={"white"} background="gray" />
) : state === "success" ? (
<Text mr="2">Done</Text>
) : (
<Toggle onClick={handleClick} {...rest} />
);
}

View File

@ -26,7 +26,7 @@ const StatusBar = (props) => {
<StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}> <StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}>
{ !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) && { !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) &&
(<Box display="block" right="-5px" top="-5px" position="absolute" > (<Box display="block" right="-8px" top="-8px" position="absolute" >
<Icon color="blue" icon="Bullet" /> <Icon color="blue" icon="Bullet" />
</Box> </Box>
)} )}

View File

@ -20,6 +20,7 @@ export function StatusBarItem({
color="washedGray" color="washedGray"
bg="white" bg="white"
px={2} px={2}
overflow='visible'
{...props} {...props}
> >
{children} {children}

View File

@ -9,9 +9,9 @@ import {
Col, Col,
Label, Label,
Button, Button,
LoadingSpinner,
BaseLabel
} from "@tlon/indigo-react"; } from "@tlon/indigo-react";
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import { FormError } from "~/views/components/FormError";
import { Group, GroupPolicy } from "~/types/group-update"; import { Group, GroupPolicy } from "~/types/group-update";
import { Enc } from "~/types/noun"; import { Enc } from "~/types/noun";
import { Association } from "~/types/metadata-update"; import { Association } from "~/types/metadata-update";
@ -24,6 +24,7 @@ import { useHistory } from "react-router-dom";
import { uxToHex } from "~/logic/lib/util"; import { uxToHex } from "~/logic/lib/util";
import { FormikOnBlur } from "~/views/components/FormikOnBlur"; import { FormikOnBlur } from "~/views/components/FormikOnBlur";
import {GroupNotificationsConfig} from "~/types"; import {GroupNotificationsConfig} from "~/types";
import {StatelessAsyncToggle} from "~/views/components/StatelessAsyncToggle";
function DeleteGroup(props: { function DeleteGroup(props: {
owner: boolean; owner: boolean;
@ -42,7 +43,7 @@ function DeleteGroup(props: {
const action = props.owner ? "Delete" : "Leave"; const action = props.owner ? "Delete" : "Leave";
const description = props.owner const description = props.owner
? "Permanently delete this group. (All current members will no longer see this group.)" ? "Permanently delete this group. (All current members will no longer see this group.)"
: "Leave this group. You can rejoin if it is an open group, or if you are reinvited"; : "You can rejoin if it is an open group, or if you are reinvited";
return ( return (
<Col> <Col>
@ -50,7 +51,7 @@ function DeleteGroup(props: {
<Label gray mt="2"> <Label gray mt="2">
{description} {description}
</Label> </Label>
<StatelessAsyncButton onClick={onDelete} mt={2} destructive> <StatelessAsyncButton onClick={onDelete} mt={2} destructive={props.owner}>
{action} this group {action} this group
</StatelessAsyncButton> </StatelessAsyncButton>
</Col> </Col>
@ -71,27 +72,27 @@ export function GroupPersonalSettings(props: {
const watching = props.notificationsGroupConfig.findIndex(g => g === groupPath) !== -1; const watching = props.notificationsGroupConfig.findIndex(g => g === groupPath) !== -1;
const initialValues: FormSchema = { const onClick = async () => {
watching const func = !watching ? 'listenGroup' : 'ignoreGroup';
};
const onSubmit = async (values: FormSchema) => {
if(values.watching === watching) {
return;
}
const func = values.watching ? 'listenGroup' : 'ignoreGroup';
await props.api.hark[func](groupPath); await props.api.hark[func](groupPath);
}; };
const owner = (props.group?.tags?.role?.admin.has(window.ship) || false);
return ( return (
<Col gapY="4"> <Col gapY="4">
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}> <BaseLabel
<Toggle htmlFor="asyncToggle"
id="watching" display="flex"
label="Notify me on group activity" cursor="pointer"
caption="Send me notifications when this group changes" >
/> <StatelessAsyncToggle selected={watching} onClick={onClick} />
</FormikOnBlur> <Col>
<DeleteGroup association={props.association} owner api={props.api} /> <Label>Notify me on group activity</Label>
<Label mt="2" gray>Send me notifications when this group changes</Label>
</Col>
</BaseLabel>
<DeleteGroup association={props.association} owner={owner} api={props.api} />
</Col> </Col>
); );
} }

View File

@ -42,9 +42,13 @@ export function useChat(
export function useGraphModule( export function useGraphModule(
graphKeys: Set<string>, graphKeys: Set<string>,
graphs: Graphs, graphs: Graphs,
graphUnreads: Record<string, number>
): SidebarAppConfig { ): SidebarAppConfig {
const getStatus = useCallback( const getStatus = useCallback(
(s: string) => { (s: string) => {
if((graphUnreads[s] || 0) > 0) {
return 'unread';
}
const [, , host, name] = s.split("/"); const [, , host, name] = s.split("/");
const graphKey = `${host.slice(1)}/${name}`; const graphKey = `${host.slice(1)}/${name}`;

View File

@ -43,7 +43,7 @@ interface SkeletonProps {
export function Skeleton(props: SkeletonProps) { export function Skeleton(props: SkeletonProps) {
const chatConfig = useChat(props.inbox, props.chatSynced); const chatConfig = useChat(props.inbox, props.chatSynced);
const graphConfig = useGraphModule(props.graphKeys, props.graphs); const graphConfig = useGraphModule(props.graphKeys, props.graphs, props.graphUnreads);
const config = useMemo( const config = useMemo(
() => ({ () => ({
graph: graphConfig, graph: graphConfig,