diff --git a/pkg/arvo/app/hark-group-hook.hoon b/pkg/arvo/app/hark-group-hook.hoon index 62a7fb5f3..4b685c938 100644 --- a/pkg/arvo/app/hark-group-hook.hoon +++ b/pkg/arvo/app/hark-group-hook.hoon @@ -48,7 +48,7 @@ (on-watch:def path) :_ this =; =cage - [%give %fact ~[/updates] cage]~ + [%give %fact ~ cage]~ :- %hark-group-hook-update !> ^- update:hook [%initial watching] diff --git a/pkg/arvo/app/hark-store.hoon b/pkg/arvo/app/hark-store.hoon index f1d3905ac..d16060b52 100644 --- a/pkg/arvo/app/hark-store.hoon +++ b/pkg/arvo/app/hark-store.hoon @@ -15,7 +15,7 @@ =notifications:store archive=notifications:store last-seen=@da - dnd=? + dnd=_| == +$ inflated-state $: state-0 @@ -25,6 +25,7 @@ :: albeit expensively +$ cache $: unread-count=@ud + graph-unreads=(map resource @ud) ~ == :: @@ -71,14 +72,17 @@ ^- update:store :- %more ^- (list update:store) + :- [%graph-unreads graph-unreads] :+ [%set-dnd dnd] [%count unread-count] %+ weld %+ turn - (tap-nonempty archive) + %+ scag 5 + (tap-nonempty:ha archive) (timebox-update &) %+ turn - (tap-nonempty notifications) + %+ scag 5 + (tap-nonempty:ha notifications) (timebox-update |) :: ++ timebox-update @@ -86,12 +90,6 @@ |= [time=@da =timebox:store] ^- update:store [%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 @@ -104,11 +102,13 @@ (slav %ud i.t.t.path) =/ length=@ud (slav %ud i.t.t.t.path) - :^ ~ ~ %noun + :^ ~ ~ %hark-update !> ^- update:store :- %more %+ turn - (scag length (slag offset (tap:orm notifications))) + %+ scag length + %+ slag offset + (tap-nonempty:ha notifications) |= [time=@da =timebox:store] ^- update:store :^ %timebox time %.n @@ -154,6 +154,7 @@ (~(put by timebox) index new) :- (give:ha [/updates]~ %added last-seen index new) %_ state + + ?~(existing-notif (upd-unreads:ha index %.n) +.state) notifications (put:orm notifications last-seen new-timebox) unread-count ?~(existing-notif +(unread-count) unread-count) == @@ -169,7 +170,7 @@ (~(del by timebox) index) :- (give:ha [/updates]~ %archive time index) %_ state - unread-count ?.(read.notification (dec unread-count) unread-count) + + ?.(read.notification (upd-unreads:ha index %.y) +.state) :: notifications (put:orm notifications time new-timebox) @@ -186,6 +187,7 @@ ^- (quip card _state) :- (give:ha [/updates]~ %read time index) %_ state + + (upd-unreads:ha index %.y) unread-count (dec unread-count) notifications (change-read-status:ha time index %.y) == @@ -195,6 +197,7 @@ ^- (quip card _state) :- (give:ha [/updates]~ %unread time index) %_ state + + (upd-unreads:ha index %.n) unread-count +(unread-count) notifications (change-read-status:ha time index %.n) == @@ -231,6 +234,12 @@ |_ =bowl:gall +* met ~(. metadata bowl) :: +++ tap-nonempty + |= =notifications:store + ^- (list [@da timebox:store]) + %+ skip (tap:orm notifications) + |=([@da =timebox:store] =(0 ~(wyt by timebox))) +:: ++ merge-notification |= [existing=notification:store new=notification:store] ^- notification:store @@ -292,22 +301,37 @@ ^- (list card) [%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 |= state-0 - ^- cache - :_ ~ - %+ roll + ^+ +.state + =/ nots=(list [p=@da =timebox:store]) (tap:orm notifications) - |= [[time=@da =timebox:store] out=@ud] - =/ unreads ~(tap by timebox) - |- - ?~ unreads out + |- =* outer $ + ?~ nots + +.state + =/ unreads ~(tap by timebox.i.nots) + |- =* inner $ + ?~ unreads + outer(nots t.nots) =* notification q.i.unreads + =* index p.i.unreads ?: read.notification - out - %_ $ - unreads t.unreads - :: - out +(out) - == + inner(unreads t.unreads) + =. +.state + (upd-unreads index read.notification) + inner(unreads t.unreads) -- diff --git a/pkg/arvo/lib/hark/store.hoon b/pkg/arvo/lib/hark/store.hoon index 214d42de7..16f520e79 100644 --- a/pkg/arvo/lib/hark/store.hoon +++ b/pkg/arvo/lib/hark/store.hoon @@ -78,12 +78,22 @@ %timebox (timebox +.upd) %set-dnd b+dnd.upd %count (numb count.upd) + %graph-unreads (graph-unreads map.upd) %more (more +.upd) :: ?(%archive %read %unread) (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 |= [tim=@da idx=^index not=^notification] ^- json diff --git a/pkg/arvo/sur/hark-store.hoon b/pkg/arvo/sur/hark-store.hoon index 4379612eb..72ee3fed6 100644 --- a/pkg/arvo/sur/hark-store.hoon +++ b/pkg/arvo/sur/hark-store.hoon @@ -46,5 +46,6 @@ [%added time=@da =index =notification] [%timebox time=@da archived=? =(list [index notification])] [%count count=@ud] + [%graph-unreads =(map resource @ud)] == -- diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index c9899da0b..d15ca9e10 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -149,10 +149,21 @@ export class HarkApi extends BaseApi { }); } + 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) { const s = start ? dateToDa(start) : "-"; 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({ data: result, }); diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 7acadd8ad..d1c4a21ef 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -23,7 +23,6 @@ export const HarkReducer = (json: any, state: HarkState) => { } const graphHookData = _.get(json, "hark-graph-hook-update", false); if (graphHookData) { - console.log(graphHookData); graphInitial(graphHookData, state); graphIgnore(graphHookData, state); graphListen(graphHookData, state); @@ -133,6 +132,7 @@ function graphWatchSelf(json: any, state: HarkState) { } function reduce(data: any, state: HarkState) { + console.log(data); unread(data, state); read(data, state); archive(data, state); @@ -141,6 +141,14 @@ function reduce(data: any, state: HarkState) { dnd(data, state); count(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) { @@ -158,6 +166,10 @@ function added(json: any, state: HarkState) { } else { state.notifications.set(time, [...timebox, { index, notification }]); 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) { const { time, index } = data; 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); } } @@ -252,6 +269,10 @@ function unread(json: any, state: HarkState) { if (data) { const { time, index } = data; 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); } } diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 03382aafb..dd3526953 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -106,7 +106,8 @@ export default class GlobalStore extends BaseStore { mentions: false, watching: [], }, - notificationsCount: 0 + notificationsCount: 0, + graphUnreads: {} }; } diff --git a/pkg/interface/src/logic/store/type.ts b/pkg/interface/src/logic/store/type.ts index 75a5c5a7d..405e7cfbc 100644 --- a/pkg/interface/src/logic/store/type.ts +++ b/pkg/interface/src/logic/store/type.ts @@ -65,4 +65,5 @@ export interface StoreState { notificationsChatConfig: string[]; notificationsCount: number, doNotDisturb: boolean; + graphUnreads: Record; } diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 1ba25a969..4299d39bc 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -86,7 +86,8 @@ export default class ChatMessage extends Component { unreadMarkerRef, history, api, - highlighted + highlighted, + fontSize } = this.props; const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1); @@ -118,7 +119,8 @@ export default class ChatMessage extends Component { history, api, scrollWindow, - highlighted + highlighted, + fontSize }; const unreadContainerStyle = { @@ -131,7 +133,7 @@ export default class ChatMessage extends Component { width='100%' display='flex' flexWrap='wrap' - pt={renderSigil ? 3 : 0} + pt={this.props.pt ? this.props.pt : renderSigil ? 3 : 0} pr={3} pb={isLastMessage ? 3 : 0} ref={this.divRef} @@ -170,7 +172,7 @@ interface MessageProps { export class MessageWithSigil extends PureComponent { isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - + render() { const { msg, @@ -184,14 +186,15 @@ export class MessageWithSigil extends PureComponent { measure, api, history, - scrollWindow + scrollWindow, + fontSize } = this.props; const datestamp = moment.unix(msg.when / 1000).format(DATESTAMP_FORMAT); const contact = msg.author in contacts ? contacts[msg.author] : false; const showNickname = !hideNicknames && contact && contact.nickname; 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'; let nameSpan = null; @@ -245,7 +248,7 @@ export class MessageWithSigil extends PureComponent { {timestamp} {datestamp} - + ); @@ -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) { return ; } else if ('url' in content) { return ( - + { ); } else if ('me' in content) { return ( - + {content.me} ); } else if ('text' in content) { - return ; + return ; } else { return null; } diff --git a/pkg/interface/src/views/apps/chat/components/content/code.js b/pkg/interface/src/views/apps/chat/components/content/code.js index 356c5c778..49808a394 100644 --- a/pkg/interface/src/views/apps/chat/components/content/code.js +++ b/pkg/interface/src/views/apps/chat/components/content/code.js @@ -16,7 +16,6 @@ export default class CodeContent extends Component { p='1' my='0' borderRadius='1' - fontSize='14px' overflow='auto' maxHeight='10em' maxWidth='100%' @@ -35,7 +34,6 @@ export default class CodeContent extends Component { my='0' p='1' borderRadius='1' - fontSize='14px' overflow='auto' maxHeight='10em' maxWidth='100%' diff --git a/pkg/interface/src/views/apps/chat/components/content/text.js b/pkg/interface/src/views/apps/chat/components/content/text.js index 03a5034f7..d578f6925 100644 --- a/pkg/interface/src/views/apps/chat/components/content/text.js +++ b/pkg/interface/src/views/apps/chat/components/content/text.js @@ -26,13 +26,12 @@ const DISABLED_INLINE_TOKENS = [ const renderers = { inlineCode: ({language, value}) => { - return {value} + return {value} }, code: ({language, value}) => { return + @@ -94,7 +93,7 @@ export default class TextContent extends Component { ); } else { return ( - + ); diff --git a/pkg/interface/src/views/apps/notifications/chat.tsx b/pkg/interface/src/views/apps/notifications/chat.tsx index 8c8bee0bb..31075707e 100644 --- a/pkg/interface/src/views/apps/notifications/chat.tsx +++ b/pkg/interface/src/views/apps/notifications/chat.tsx @@ -83,6 +83,8 @@ export function ChatNotification(props: { isLastRead={false} group={group} contacts={groupContacts} + fontSize='0' + pt='2' /> ); diff --git a/pkg/interface/src/views/apps/notifications/graph.tsx b/pkg/interface/src/views/apps/notifications/graph.tsx index e7f68ef9c..7f5b9c7a7 100644 --- a/pkg/interface/src/views/apps/notifications/graph.tsx +++ b/pkg/interface/src/views/apps/notifications/graph.tsx @@ -51,8 +51,8 @@ function describeNotification(description: string, plural: boolean) { } const GraphUrl = ({ url, title }) => ( - - + + {title} @@ -140,7 +140,7 @@ const GraphNode = ({ return ( - + {img} { + let container = e.target; + if(!props.showArchive && (container.scrollHeight - container.scrollTop === container.clientHeight)) { + api.hark.getMore(); + } + }, [api]); + const incomingGroups = Object.values(invites?.['contacts'] || {}); - const getKeyByValue = (object, value) => { + const getKeyByValue = (object, value) => { return Object.keys(object).find(key => object[key] === value); }; -const acceptInvite = (invite) => { + const acceptInvite = (invite) => { const resource = { ship: `~${invite.resource.ship}`, name: invite.resource.name @@ -86,11 +93,9 @@ const acceptInvite = (invite) => { }; return ( - + {incomingGroups.map((invite) => ( diff --git a/pkg/interface/src/views/apps/notifications/notification.tsx b/pkg/interface/src/views/apps/notifications/notification.tsx index 517d823eb..f62a48745 100644 --- a/pkg/interface/src/views/apps/notifications/notification.tsx +++ b/pkg/interface/src/views/apps/notifications/notification.tsx @@ -87,9 +87,9 @@ function NotificationWrapper(props: { const changeMuteDesc = isMuted ? "Unmute" : "Mute"; return ( - + {children} - + {changeMuteDesc} diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index a91421c8c..0c7302c3a 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -53,7 +53,7 @@ export default function NotificationsScreen(props: any) { const { view } = routeProps.match.params; return ( - + Promise; +} + +export function StatelessAsyncToggle({ + onClick, + name = "", + ...rest +}: AsyncToggleProps & Parameters[0]) { + const { + onClick: handleClick, + buttonState: state, + } = useStatelessAsyncClickable(onClick, name); + + return state === "error" ? ( + Error + ) : state === "loading" ? ( + + ) : state === "success" ? ( + Done + ) : ( + + ); +} diff --git a/pkg/interface/src/views/components/StatusBar.js b/pkg/interface/src/views/components/StatusBar.js index b26890ab1..0e1190f99 100644 --- a/pkg/interface/src/views/components/StatusBar.js +++ b/pkg/interface/src/views/components/StatusBar.js @@ -26,7 +26,7 @@ const StatusBar = (props) => { props.api.local.setOmnibox()}> { !props.doNotDisturb && (props.notificationsCount > 0 || invites.length > 0) && - ( + ( )} diff --git a/pkg/interface/src/views/components/StatusBarItem.tsx b/pkg/interface/src/views/components/StatusBarItem.tsx index 34a76e8a3..49fe634aa 100644 --- a/pkg/interface/src/views/components/StatusBarItem.tsx +++ b/pkg/interface/src/views/components/StatusBarItem.tsx @@ -20,6 +20,7 @@ export function StatusBarItem({ color="washedGray" bg="white" px={2} + overflow='visible' {...props} > {children} diff --git a/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx b/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx index 33be942d7..9b036ec45 100644 --- a/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx @@ -9,9 +9,9 @@ import { Col, Label, Button, + LoadingSpinner, + BaseLabel } 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 { Enc } from "~/types/noun"; import { Association } from "~/types/metadata-update"; @@ -24,6 +24,7 @@ import { useHistory } from "react-router-dom"; import { uxToHex } from "~/logic/lib/util"; import { FormikOnBlur } from "~/views/components/FormikOnBlur"; import {GroupNotificationsConfig} from "~/types"; +import {StatelessAsyncToggle} from "~/views/components/StatelessAsyncToggle"; function DeleteGroup(props: { owner: boolean; @@ -42,7 +43,7 @@ function DeleteGroup(props: { const action = props.owner ? "Delete" : "Leave"; const description = props.owner ? "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 ( @@ -50,7 +51,7 @@ function DeleteGroup(props: { - + {action} this group @@ -71,27 +72,27 @@ export function GroupPersonalSettings(props: { const watching = props.notificationsGroupConfig.findIndex(g => g === groupPath) !== -1; - const initialValues: FormSchema = { - watching - }; - const onSubmit = async (values: FormSchema) => { - if(values.watching === watching) { - return; - } - const func = values.watching ? 'listenGroup' : 'ignoreGroup'; + const onClick = async () => { + const func = !watching ? 'listenGroup' : 'ignoreGroup'; await props.api.hark[func](groupPath); }; + const owner = (props.group?.tags?.role?.admin.has(window.ship) || false); + return ( - - - - + + + + + + + + ); } diff --git a/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx b/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx index 691098239..1dfd29080 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx @@ -42,9 +42,13 @@ export function useChat( export function useGraphModule( graphKeys: Set, graphs: Graphs, + graphUnreads: Record ): SidebarAppConfig { const getStatus = useCallback( (s: string) => { + if((graphUnreads[s] || 0) > 0) { + return 'unread'; + } const [, , host, name] = s.split("/"); const graphKey = `${host.slice(1)}/${name}`; diff --git a/pkg/interface/src/views/landscape/components/Skeleton.tsx b/pkg/interface/src/views/landscape/components/Skeleton.tsx index 99d9cbaa7..e1e052f54 100644 --- a/pkg/interface/src/views/landscape/components/Skeleton.tsx +++ b/pkg/interface/src/views/landscape/components/Skeleton.tsx @@ -43,7 +43,7 @@ interface SkeletonProps { export function Skeleton(props: SkeletonProps) { 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( () => ({ graph: graphConfig,