From c0a13dc2855bbf1e78cc663bada6ac3217cd5cd7 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Fri, 28 Jun 2024 12:51:51 -0500 Subject: [PATCH 01/12] wip: getting recursive reads to work --- desk/app/activity.hoon | 91 ++++++++++++++++++++++++++----------- desk/lib/activity-json.hoon | 41 +++++++++-------- desk/sur/activity.hoon | 4 +- 3 files changed, 90 insertions(+), 46 deletions(-) diff --git a/desk/app/activity.hoon b/desk/app/activity.hoon index 7d9f6e367b..c69e0f5628 100644 --- a/desk/app/activity.hoon +++ b/desk/app/activity.hoon @@ -596,9 +596,12 @@ (give-update update ~) =? cor &(!importing notify (is-allowed inc)) (give %fact ~[/notifications /v0/notifications] activity-event+!>([time-id event])) - =. indices - =/ =stream:a (put:on-event:a stream:base time-id event) - (~(put by indices) [%base ~] [stream reads:base]) + :: we always update sources in order, so make sure base is last + =; co + =. indices.co + =/ =stream:a (put:on-event:a stream:base time-id event) + (~(put by indices) [%base ~] [stream reads:base]) + co ?+ -<.event (add-to-index source time-id event) %chan-init =/ group-src [%group group.event] @@ -659,7 +662,7 @@ =. indices (~(put by indices) source new) ?: importing cor ::NOTE deferred until end of migration - (refresh source) + (refresh-summary source) :: ++ refresh-all-summaries ^+ cor @@ -775,10 +778,9 @@ ++ read |= [=source:a action=read-action:a] ^+ cor - %+ update-reads source + =/ =index:a (get-index source) ?- -.action %event - |= =index:a ?> ?=(%event -.action) =/ events %+ murn @@ -786,34 +788,56 @@ |= [=time =event:a] ?. =(-.event event.action) ~ `[time event] - ?~ events index - =- index(items.reads -) - %+ put:on-read-items:a items.reads.index - [-<.events ~] + ?~ events cor + (read source [%item -<.events]) :: %item - |= =index:a - =- index(items.reads -) - %+ put:on-read-items:a items.reads.index - [id.action ~] + =/ new-read [id.action ~] + =/ read-items (put:on-read-items:a items.reads.index new-read) + =. cor (update-parents source ~[new-read]) + (update-index source index(items.reads read-items) &) :: - %all - |= =index:a - ?^ time.action index(reads [u.time.action ~]) - =/ latest=(unit [=time event:a]) - ::REVIEW is this taking the item from the correct end? lol - (ram:on-event:a stream.index) - index(reads [?~(latest now.bowl time.u.latest) ~]) + ?(%all %recursive) + =/ new-floor=time + ?^ time.action u.time.action + =/ latest=(unit [=time event:a]) + (ram:on-event:a stream.index) + ?~(latest now.bowl time.u.latest) + =/ new=index:a index(reads [new-floor ~]) + =/ subset + (lot:on-event:a stream.index `floor.reads.index `+(new-floor)) + =/ reads + %+ turn + (tap:on-event:a subset) + |= [=time-id:a *] + [time-id ~] + =. cor (update-parents source reads) + =/ children (get-children source) + =< (update-index source new &) + |- + :: only update children if we're in recursive mode + ?: ?=(%all -.action) cor + ?~ children cor + =/ =source:a i.children + =/ =index:a (get-index source) + =/ new=index:a index(reads [new-floor ~]) + (update-index source new &) == :: +++ update-parents + |= [=source:a items=(list [=time-id:a ~])] + =/ parents (get-parents source) + |- + ?~ parents cor + =/ parent-index (get-index i.parents) + =/ =read-items:a + (gas:on-read-items:a items.reads.parent-index items) + =. cor + (update-index i.parents parent-index(items.reads read-items) &) + $(parents t.parents) ++ get-index |= =source:a (~(gut by indices) source *index:a) -++ update-reads - |= [=source:a updater=$-(index:a index:a)] - ^+ cor - =/ new (updater (get-index source)) - (update-index source new &) ++ give-unreads |= =source:a ^+ cor @@ -838,6 +862,21 @@ ^+ cor =. allowed na (give-update [%allow-notifications na] ~) +++ get-parents + |= =source:a + ^- (list source:a) + ?: ?=(%base -.source) ~ + ?< ?=(%base -.source) + :- [%base ~] + ?+ -.source ~ + %channel ~[[%group group.source]] + %dm-thread ~[[%dm whom.source]] + :: + %thread + :~ [%channel channel.source group.source] + [%group group.source] + == + == ++ get-children |= =source:a ^- (list source:a) diff --git a/desk/lib/activity-json.hoon b/desk/lib/activity-json.hoon index 43928d83c5..5c8cd0ec94 100644 --- a/desk/lib/activity-json.hoon +++ b/desk/lib/activity-json.hoon @@ -417,23 +417,7 @@ allow-notifications/(su (perk %all %some %none ~)) == :: - ++ add - ^- $-(json incoming-event:a) - %- of - :~ post/post-event - reply/reply-event - chan-init/chan-init-event - dm-invite/whom - dm-post/dm-post-event - dm-reply/dm-reply-event - flag-post/flag-post-event - flag-reply/flag-reply-event - group-ask/group-event - group-join/group-event - group-kick/group-event - group-invite/group-event - group-role/group-role-event - == + ++ add incoming-event :: ++ adjust %- ot @@ -450,8 +434,10 @@ :: ++ read-action %- of - :~ all/ul - item/id + :~ item/id + all/(mu (se %ud)) + event/incoming-event + recursive/(mu (se %ud)) == :: +| %basics @@ -503,6 +489,23 @@ notify/bo == :: + ++ incoming-event + ^- $-(json incoming-event:a) + %- of + :~ post/post-event + reply/reply-event + chan-init/chan-init-event + dm-invite/whom + dm-post/dm-post-event + dm-reply/dm-reply-event + flag-post/flag-post-event + flag-reply/flag-reply-event + group-ask/group-event + group-join/group-event + group-kick/group-event + group-invite/group-event + group-role/group-role-event + == ++ chan-init-event %- ot :~ channel/nest:dejs:cj diff --git a/desk/sur/activity.hoon b/desk/sur/activity.hoon index 8285f8398a..a85a5c8283 100644 --- a/desk/sur/activity.hoon +++ b/desk/sur/activity.hoon @@ -41,12 +41,14 @@ :: :: $item: mark an individual activity as read, indexed by id :: $event: mark an individual activity as read, indexed by the event itself -:: $all: mark _everything_ as read for this source +:: $all: mark _everything_ as read for this source, but not children +:: $recursive: mark _everything_ as read for this source and children :: +$ read-action $% [%item id=time-id] [%event event=incoming-event] [%all time=(unit time)] + [%recursive time=(unit time)] == :: +| %updates From 6c1312c1ae712eb6994c1885011dc27c9180cb68 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Fri, 28 Jun 2024 19:05:22 -0500 Subject: [PATCH 02/12] activity: recursive read state --- apps/tlon-web/src/channels/ChannelActions.tsx | 27 +- apps/tlon-web/src/channels/ChannelHeader.tsx | 6 - apps/tlon-web/src/chat/ChatChannel.tsx | 8 +- apps/tlon-web/src/diary/DiaryChannel.tsx | 10 +- apps/tlon-web/src/diary/DiaryHeader.tsx | 8 +- apps/tlon-web/src/dms/DMOptions.tsx | 8 +- apps/tlon-web/src/groups/GroupActions.tsx | 11 + .../src/groups/GroupSidebar/ChannelList.tsx | 33 +- apps/tlon-web/src/heap/HeapHeader.tsx | 8 +- apps/tlon-web/src/logic/channel.ts | 8 +- .../logic/useDismissChannelNotifications.ts | 50 +- apps/tlon-web/src/state/activity.ts | 5 +- apps/tlon-web/src/state/chat/chat.ts | 8 +- apps/tlon-web/src/state/unreads.ts | 32 +- desk/app/activity.hoon | 446 ++++++++++-------- packages/shared/src/logic/utils.ts | 11 + packages/shared/src/urbit/activity.ts | 3 +- 17 files changed, 353 insertions(+), 329 deletions(-) diff --git a/apps/tlon-web/src/channels/ChannelActions.tsx b/apps/tlon-web/src/channels/ChannelActions.tsx index ab76e7f358..886d80113a 100644 --- a/apps/tlon-web/src/channels/ChannelActions.tsx +++ b/apps/tlon-web/src/channels/ChannelActions.tsx @@ -1,7 +1,7 @@ import { GroupChannel } from '@tloncorp/shared/dist/urbit/groups'; +import { getPrettyAppName } from '@tloncorp/shared/src/logic/utils'; import cn from 'classnames'; import React, { PropsWithChildren, useCallback, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router'; import ActionMenu, { Action } from '@/components/ActionMenu'; import useActiveTab, { useNavWithinTab } from '@/components/Sidebar/util'; @@ -13,32 +13,25 @@ import { useIsChannelHost } from '@/logic/channel'; import { Status } from '@/logic/status'; import { useIsMobile } from '@/logic/useMedia'; import { getFlagParts, nestToFlag } from '@/logic/utils'; +import { useMarkReadMutation } from '@/state/activity'; +import { useLeaveMutation } from '@/state/channel/channel'; import { useDeleteChannelMutation, useRouteGroup } from '@/state/groups'; import ChannelHostConnection from './ChannelHostConnection'; export type ChannelActionsProps = PropsWithChildren<{ nest: string; - prettyAppName: string; channel: GroupChannel | undefined; isAdmin: boolean | undefined; - leave: ({ nest }: { nest: string }) => Promise; className?: string; }>; const ChannelActions = React.memo( - ({ - nest, - prettyAppName, - channel, - isAdmin, - leave, - className, - children, - }: ChannelActionsProps) => { + ({ nest, channel, isAdmin, className, children }: ChannelActionsProps) => { const { navigate } = useNavWithinTab(); const isMobile = useIsMobile(); const [_app, flag] = nestToFlag(nest); + const prettyAppName = getPrettyAppName(_app); const groupFlag = useRouteGroup(); const { ship, name } = getFlagParts(groupFlag); const [isOpen, setIsOpen] = useState(false); @@ -47,7 +40,9 @@ const ChannelActions = React.memo( const [deleteStatus, setDeleteStatus] = useState('initial'); const [showNotifications, setShowNotifications] = useState(false); const isChannelHost = useIsChannelHost(flag); + const { mutate: markRead } = useMarkReadMutation(true); const { mutate: deleteChannelMutate } = useDeleteChannelMutation(); + const { mutate: leave } = useLeaveMutation(); const hasChildren = !!children; const activeTab = useActiveTab(); @@ -129,6 +124,14 @@ const ChannelActions = React.memo( }); } + actions.push({ + key: 'mark-read', + type: 'prominent', + onClick: () => + markRead({ source: { channel: { nest, group: groupFlag } } }), + content: 'Mark as Read', + }); + actions.push({ key: 'notifications', onClick: () => { diff --git a/apps/tlon-web/src/channels/ChannelHeader.tsx b/apps/tlon-web/src/channels/ChannelHeader.tsx index b8b4aed349..383af01b94 100644 --- a/apps/tlon-web/src/channels/ChannelHeader.tsx +++ b/apps/tlon-web/src/channels/ChannelHeader.tsx @@ -14,15 +14,11 @@ import ChannelTitleButton from './ChannelTitleButton'; export type ChannelHeaderProps = PropsWithChildren<{ groupFlag: string; nest: string; - prettyAppName: string; - leave: ({ nest }: { nest: string }) => Promise; }>; export default function ChannelHeader({ groupFlag, nest, - prettyAppName, - leave, children, }: ChannelHeaderProps) { const isMobile = useIsMobile(); @@ -31,10 +27,8 @@ export default function ChannelHeader({ const actionProps: ChannelActionsProps = { nest, - prettyAppName, channel, isAdmin, - leave, }; if (isMobile) { diff --git a/apps/tlon-web/src/chat/ChatChannel.tsx b/apps/tlon-web/src/chat/ChatChannel.tsx index b1895d1588..a5c92238a8 100644 --- a/apps/tlon-web/src/chat/ChatChannel.tsx +++ b/apps/tlon-web/src/chat/ChatChannel.tsx @@ -55,7 +55,6 @@ const ChatChannel = React.memo(({ title }: ViewProps) => { const inDmSearch = useMatch( `/dm/groups/${groupFlag}/channels/${nest}/search/*` ); - const { mutateAsync: leaveChat } = useLeaveMutation(); const { mutate: sendMessage } = useAddPostMutation(nest); const dropZoneId = `chat-input-dropzone-${chFlag}`; const { isDragging, isOver } = useDragAndDrop(dropZoneId); @@ -120,12 +119,7 @@ const ChatChannel = React.memo(({ title }: ViewProps) => { + { if (atBottom && hasNextPage) { @@ -130,11 +130,9 @@ function DiaryChannel({ title }: ViewProps) { }; }, [newNote, location, navigate]); - useDismissChannelNotifications({ - nest, - markRead, - isMarking, - }); + useEffect(() => { + markRead(); + }, []); const sortedNotes = notes .filter(([k, v]) => v !== null) diff --git a/apps/tlon-web/src/diary/DiaryHeader.tsx b/apps/tlon-web/src/diary/DiaryHeader.tsx index d66fd3c2d0..c773b27793 100644 --- a/apps/tlon-web/src/diary/DiaryHeader.tsx +++ b/apps/tlon-web/src/diary/DiaryHeader.tsx @@ -40,7 +40,6 @@ export default function DiaryHeader({ const isMobile = useIsMobile(); const { compatible } = useChannelCompatibility(nest); const settings = useDiarySettings(); - const { mutateAsync: leaveDiary } = useLeaveMutation(); const { mutate } = usePutEntryMutation({ bucket: 'diary', key: 'settings', @@ -110,12 +109,7 @@ export default function DiaryHeader({ ]; return ( - leaveDiary({ nest: `diary/${ch}` })} - > +
+
+ } > {channel.meta.title || nest} diff --git a/apps/tlon-web/src/heap/HeapHeader.tsx b/apps/tlon-web/src/heap/HeapHeader.tsx index 726a27eab8..f6d0893f15 100644 --- a/apps/tlon-web/src/heap/HeapHeader.tsx +++ b/apps/tlon-web/src/heap/HeapHeader.tsx @@ -58,7 +58,6 @@ const HeapHeader = React.memo( bucket: 'heaps', key: 'heapSettings', }); - const { mutateAsync: leave } = useLeaveMutation(); const setDisplayMode = (setting: DisplayMode) => { const newSettings = setChannelSetting( @@ -118,12 +117,7 @@ const HeapHeader = React.memo( ]; return ( - + {isMobile ? (
{ const source: Source = thread ? { diff --git a/apps/tlon-web/src/logic/useDismissChannelNotifications.ts b/apps/tlon-web/src/logic/useDismissChannelNotifications.ts index f92233f357..587d6135b9 100644 --- a/apps/tlon-web/src/logic/useDismissChannelNotifications.ts +++ b/apps/tlon-web/src/logic/useDismissChannelNotifications.ts @@ -1,61 +1,19 @@ import { useEffect } from 'react'; -import { useIsChannelUnread } from '@/logic/channel'; -import { useNotifications } from '@/notifications/useNotifications'; -import { useRouteGroup } from '@/state/groups'; -import { useSawRopeMutation } from '@/state/hark'; - -import { nestToFlag } from './utils'; - interface UseDismissChannelProps { nest: string; - markRead: (flag: string) => Promise | void; + markRead: () => Promise; isMarking: boolean; } export default function useDismissChannelNotifications({ - nest, markRead, isMarking, }: UseDismissChannelProps) { - const flag = useRouteGroup(); - const [, chFlag] = nestToFlag(nest); - const unread = useIsChannelUnread(nest); - const { notifications } = useNotifications(flag); - const { mutate: sawRopeMutation } = useSawRopeMutation(); - - /** - * TODO: Confirm expected behavior for navigating to a Channel with Unreads. - * - * Does clicking on an Unread Channel automatically scrollback to the - * last read message? Should it only be dismissed when reaching the end of - * new messages? - */ - // dismiss unread notifications while viewing channel useEffect(() => { - if (nest && unread && !isMarking) { + if (!isMarking) { // dismiss unread - markRead(chFlag); - // iterate bins, saw each rope - notifications.forEach((n) => { - n.skeins.forEach((b) => { - if ( - b.unread && - b.top?.rope.channel && - b.top.rope.channel.includes(chFlag) - ) { - sawRopeMutation({ rope: b.top.rope }); - } - }); - }); + markRead(); } - }, [ - nest, - chFlag, - markRead, - unread, - notifications, - sawRopeMutation, - isMarking, - ]); + }, [markRead, isMarking]); } diff --git a/apps/tlon-web/src/state/activity.ts b/apps/tlon-web/src/state/activity.ts index da77603b23..6da1ed6ae2 100644 --- a/apps/tlon-web/src/state/activity.ts +++ b/apps/tlon-web/src/state/activity.ts @@ -150,7 +150,7 @@ export function useActivityFirehose() { }, [eventQueue]); } -export function useMarkReadMutation() { +export function useMarkReadMutation(recursive = false) { const mutationFn = async (variables: { source: Source; action?: ReadAction; @@ -159,7 +159,8 @@ export function useMarkReadMutation() { activityAction({ read: { source: variables.source, - action: variables.action || { all: null }, + action: + variables.action || recursive ? { recursive: null } : { all: null }, }, }) ); diff --git a/apps/tlon-web/src/state/chat/chat.ts b/apps/tlon-web/src/state/chat/chat.ts index 5f1e890b7f..cc37b2fb4a 100644 --- a/apps/tlon-web/src/state/chat/chat.ts +++ b/apps/tlon-web/src/state/chat/chat.ts @@ -661,8 +661,12 @@ export function useDmIsPending(ship: string) { return pending.includes(ship); } -export function useMarkDmReadMutation(whom: string, thread?: MessageKey) { - const { mutateAsync, ...rest } = useMarkReadMutation(); +export function useMarkDmReadMutation( + whom: string, + thread?: MessageKey, + recursive = false +) { + const { mutateAsync, ...rest } = useMarkReadMutation(recursive); const markDmRead = useCallback(() => { const whomObj = whomIsDm(whom) ? { ship: whom } : { club: whom }; const source: Source = thread diff --git a/apps/tlon-web/src/state/unreads.ts b/apps/tlon-web/src/state/unreads.ts index 828fb2ec65..a88f2870fe 100644 --- a/apps/tlon-web/src/state/unreads.ts +++ b/apps/tlon-web/src/state/unreads.ts @@ -1,6 +1,7 @@ import { Activity, ActivitySummary, + stripSourcePrefix, } from '@tloncorp/shared/dist/urbit/activity'; import produce from 'immer'; import { useCallback, useMemo } from 'react'; @@ -438,35 +439,12 @@ export function useAllGroupUnreads() { export function useMarkAllGroupsRead() { const allGroupUnreads = useAllGroupUnreads(); const { read } = useUnreadsStore(); - const { mutate } = useMarkReadMutation(); + const { mutate } = useMarkReadMutation(true); const markAllRead = useCallback(() => { - allGroupUnreads.forEach(([sourceId, groupUnread]) => { - if (groupUnread.status === 'unread') { - read(sourceId); - mutate({ source: { group: sourceId } }); - } - - const groupId = sourceId.split('/').slice(1).join('/'); - - const unreadChildrenIds = Object.entries(groupUnread.children ?? {}) - .filter(([_, childUnread]) => childUnread.count > 0) - .map(([childId]) => childId); - - unreadChildrenIds.forEach((childId) => { - read(childId); - if (childId.startsWith('channel')) { - const channelId = childId.split('/').slice(1).join('/'); - mutate({ - source: { - channel: { - group: groupId, - nest: channelId, - }, - }, - }); - } - }); + allGroupUnreads.forEach(([sourceId]) => { + read(sourceId); + mutate({ source: { group: stripSourcePrefix(sourceId) } }); }); }, [allGroupUnreads, read, mutate]); diff --git a/desk/app/activity.hoon b/desk/app/activity.hoon index c69e0f5628..711348a794 100644 --- a/desk/app/activity.hoon +++ b/desk/app/activity.hoon @@ -79,16 +79,6 @@ ^+ cor (emit %pass /migrate %agent [our.bowl dap.bowl] %poke noun+!>(%migrate)) :: -++ migrate - =. importing & - =. indices (~(put by indices) [%base ~] [*stream:a *reads:a]) - =. cor set-chat-reads - =+ .^(=channels:c %gx (scry-path %channels /v2/channels/full/noun)) - =. cor (set-volumes channels) - =. cor (set-channel-reads channels) - =. cor refresh-all-summaries - cor(importing |) -:: ++ load |= =vase |^ ^+ cor @@ -100,7 +90,7 @@ =? old ?=(%2 -.old) (state-2-to-3 old) ?> ?=(%3 -.old) =. state old - refresh-all-summaries + sync-reads +$ versioned-state $%(state-3 state-2 state-1) +$ state-3 current-state +$ state-2 @@ -147,173 +137,6 @@ %+ welp /(scot %p our.bowl)/[dude]/(scot %da now.bowl) path -++ set-channel-reads - |= =channels:c - ^+ cor - =+ .^(=unreads:c %gx (scry-path %channels /v1/unreads/noun)) - =/ entries ~(tap by unreads) - =; events=(list [time incoming-event:a]) - |- - ?~ events cor - =. cor (%*(. add start-time -.i.events) +.i.events) - $(events t.events) - |- ^- (list [time incoming-event:a]) - ?~ entries ~ - =/ head i.entries - =* next $(entries t.entries) - =/ [=nest:c =unread:c] head - =/ channel (~(get by channels) nest) - ?~ channel next - =/ group group.perm.u.channel - =; events=(list [time incoming-event:a]) - (weld events next) - =/ posts=(list [time incoming-event:a]) - ?~ unread.unread ~ - %+ murn - (tab:on-posts:c posts.u.channel `(sub id.u.unread.unread 1) count.u.unread.unread) - |= [=time post=(unit post:c)] - ?~ post ~ - =/ key=message-key:a - :_ time - [author.u.post time] - =/ mention - (was-mentioned:ch-utils content.u.post our.bowl) - `[time %post key nest group content.u.post mention] - =/ replies=(list [time incoming-event:a]) - %- zing - %+ murn - ~(tap by threads.unread) - |= [=id-post:c [id=id-reply:c count=@ud]] - ^- (unit (list [time incoming-event:a])) - =/ post=(unit (unit post:c)) (get:on-posts:c posts.u.channel id-post) - ?~ post ~ - ?~ u.post ~ - %- some - %+ turn - (tab:on-replies:c replies.u.u.post `(sub id 1) count) - |= [=time =reply:c] - =/ key=message-key:a - :_ time - [author.reply time] - =/ parent=message-key:a - :_ id-post - [author.u.u.post id-post] - =/ mention - (was-mentioned:ch-utils content.reply our.bowl) - [time %reply key parent nest group content.reply mention] - =/ init-time - ?: &(=(posts ~) =(replies ~)) recency.unread - *@da - :- [init-time %chan-init nest group] - (welp posts replies) -++ set-chat-reads - ^+ cor - =+ .^(=unreads:ch %gx (scry-path %chat /unreads/noun)) - =+ .^ [dms=(map ship dm:ch) clubs=(map id:club:ch club:ch)] - %gx (scry-path %chat /full/noun) - == - =/ entries ~(tap by unreads) - =; events=(list [time incoming-event:a]) - |- - ?~ events cor - =. cor (%*(. add start-time -.i.events) +.i.events) - $(events t.events) - |- ^- (list [time incoming-event:a]) - ?~ entries ~ - =/ head i.entries - =* next $(entries t.entries) - =/ [=whom:ch =unread:unreads:ch] head - =/ =pact:ch - ?- -.whom - %ship pact:(~(gut by dms) p.whom *dm:ch) - %club pact:(~(gut by clubs) p.whom *club:ch) - == - =; events=(list [time incoming-event:a]) - (weld events next) - =/ writs=(list [time incoming-event:a]) - ?~ unread.unread ~ - %+ murn - (tab:on:writs:ch wit.pact `(sub time.u.unread.unread 1) count.u.unread.unread) - |= [=time =writ:ch] - =/ key=message-key:a [id.writ time] - =/ mention - (was-mentioned:ch-utils content.writ our.bowl) - `[time %dm-post key whom content.writ mention] - =/ replies=(list [time incoming-event:a]) - %- zing - %+ murn - ~(tap by threads.unread) - |= [parent=message-key:ch [key=message-key:ch count=@ud]] - ^- (unit (list [time incoming-event:a])) - =/ writ=(unit writ:ch) (get:on:writs:ch wit.pact time.parent) - ?~ writ ~ - %- some - %+ turn - (tab:on:replies:ch replies.u.writ `(sub time.key 1) count) - |= [=time =reply:ch] - =/ mention - (was-mentioned:ch-utils content.reply our.bowl) - [time %dm-reply key parent whom content.reply mention] - =/ init-time - ?: &(=(writs ~) =(replies ~)) recency.unread - *@da - :- [init-time %dm-invite whom] - (welp writs replies) -++ set-volumes - |= =channels:c - =+ .^(=volume:v %gx (scry-path %groups /volume/all/noun)) - :: set all existing channels to old default since new default is different - =^ checkers cor - =/ checkers=(map flag:g $-([ship nest:g] ?)) ~ - =/ entries ~(tap by channels) - |- - ?~ entries [checkers cor] - =/ [=nest:c =channel:c] i.entries - =* group group.perm.channel - =+ .^(exists=? %gx (scry-path %groups /exists/(scot %p p.group)/[q.group]/noun)) - ?. exists $(entries t.entries) - =^ can-read checkers - ?^ gate=(~(get by checkers) group) [u.gate checkers] - =/ =path - %+ scry-path %groups - /groups/(scot %p p.group)/[q.group]/can-read/noun - =/ test=$-([ship nest:g] ?) - => [path=path nest=nest:g ..zuse] ~+ - .^($-([ship nest] ?) %gx path) - [test (~(put by checkers) group test)] - =. cor - :: don't set channel default if group above it has setting - ?: (~(has by area.volume) group) cor - %+ adjust [%channel nest group] - ?: (can-read our.bowl nest) `(my [%post & |] ~) - `mute:a - $(entries t.entries) - :: set any overrides from previous volume settings - =. cor (adjust [%base ~] `(~(got by old-volumes:a) base.volume)) - =. cor - =/ entries ~(tap by chan.volume) - |- - ?~ entries cor - =/ [=nest:g =level:v] i.entries - =* next $(entries t.entries) - ?. ?=(?(%chat %diary %heap) -.nest) next - =/ channel (~(get by channels) nest) - ?~ channel next - ?~ can-read=(~(get by checkers) group.perm.u.channel) next - :: don't override previously set mute from channel migration - ?. (u.can-read our.bowl nest) next - =. cor - %+ adjust [%channel nest group.perm.u.channel] - `(~(got by old-volumes:a) level) - next - =/ entries ~(tap by area.volume) - |- - ?~ entries cor - =* head i.entries - =. cor - %+ adjust [%group -.head] - `(~(got by old-volumes:a) +.head) - $(entries t.entries) ++ poke |= [=mark =vase] ^+ cor @@ -335,7 +158,7 @@ ?- -.action %add (add +.action) %del (del +.action) - %read (read +.action) + %read (read source.action read-action.action |) %adjust (adjust +.action) %allow-notifications (allow +.action) == @@ -600,7 +423,7 @@ =; co =. indices.co =/ =stream:a (put:on-event:a stream:base time-id event) - (~(put by indices) [%base ~] [stream reads:base]) + (~(put by indices.co) [%base ~] [stream reads:base]) co ?+ -<.event (add-to-index source time-id event) %chan-init @@ -776,7 +599,7 @@ index(floor.reads u.new-floor) :: ++ read - |= [=source:a action=read-action:a] + |= [=source:a action=read-action:a from-parent=?] ^+ cor =/ =index:a (get-index source) ?- -.action @@ -789,7 +612,7 @@ ?. =(-.event event.action) ~ `[time event] ?~ events cor - (read source [%item -<.events]) + (read source [%item -<.events] |) :: %item =/ new-read [id.action ~] @@ -804,23 +627,24 @@ (ram:on-event:a stream.index) ?~(latest now.bowl time.u.latest) =/ new=index:a index(reads [new-floor ~]) - =/ subset - (lot:on-event:a stream.index `floor.reads.index `+(new-floor)) - =/ reads + ~& [source floor.reads.index new-floor] + =? cor !from-parent + %+ update-parents source %+ turn - (tap:on-event:a subset) + %- tap:on-event:a + %^ lot:on-event:a stream.index `floor.reads.index + ?:((gte new-floor floor.reads.index) `+(new-floor) ~) |= [=time-id:a *] [time-id ~] - =. cor (update-parents source reads) - =/ children (get-children source) - =< (update-index source new &) - |- - :: only update children if we're in recursive mode - ?: ?=(%all -.action) cor - ?~ children cor - =/ =source:a i.children - =/ =index:a (get-index source) - =/ new=index:a index(reads [new-floor ~]) + =. cor + =/ children (get-children source) + |- + :: only update children if we're in recursive mode + ?: ?=(%all -.action) cor + ?~ children cor + =/ =source:a i.children + =. cor (read source action &) + $(children t.children) (update-index source new &) == :: @@ -1020,6 +844,58 @@ == -- :: +:: previously each source had independent read states that did not get +:: synced across sources. we set out to rectify that here +:: +++ sync-reads + =/ oldest-floors=(map source:a time) ~ + =/ indexes + :: sort children first in order so we only have to make one pass + :: of summarization aka not repeatedly updating the same source + :: + %+ sort + ~(tap by indices) + |= [[asrc=source:a *] [bsrc=source:a *]] + (gth (get-order asrc) (get-order bsrc)) + |- + ?~ indexes cor + =/ [=source:a =index:a] i.indexes + =^ min-floors indices + =/ parents (get-parents source) + =/ floors=(map source:a time) ~ + |- + ?~ parents [floors indices] + =/ parent-index (get-index i.parents) + =/ parent-reads + :- floor.reads.parent-index + (uni:on-read-items:a items.reads.parent-index items.reads.index) + :: keep track of oldest child floor + =. floors + %+ ~(put by floors) i.parents + (min floor.reads.index (~(gut by oldest-floors) source now.bowl)) + :: update parents with aggregated reads and move floor if appropriate + =. indices (~(put by indices) i.parents index(reads parent-reads)) + $(parents t.parents) + =. oldest-floors (~(uni by oldest-floors) min-floors) + =. reads.index + :: if we have no children then the reads are accurate + ?~ min-floor=(~(get by oldest-floors) source) reads.index + :: if we have children, but our floor is oldest, then we're good + ?: (lth floor.reads.index u.min-floor) reads.index + :: otherwise, we need to adjust our reads + =; main-reads=read-items:a + [u.min-floor main-reads] + %+ gas:on-read-items:a items.reads.index + %+ murn + :: take all events between our floor and the oldest child floor + (tap:on-event:a (lot:on-event:a stream.index `u.min-floor `floor.reads.index)) + |= [=time =event:a] + :: ignore child events + ?: child.event ~ + `[time ~] + =. cor (update-index source index &) + $(indexes t.indexes) +:: :: at some time in the past, for clubs activity, %dm-post and %dm-reply events :: with bad message/parent identifiers (respectively) got pushed into our :: streams. for the %dm-reply case, this made it impossible for clients (that @@ -1147,4 +1023,182 @@ [time event(time.key u.reply-time, parent post-key)] +$ indexes (list [=source:a =index:a]) -- +:: +++ migrate + =. importing & + =. indices (~(put by indices) [%base ~] [*stream:a *reads:a]) + =. cor set-chat-reads + =+ .^(=channels:c %gx (scry-path %channels /v2/channels/full/noun)) + =. cor (set-volumes channels) + =. cor (set-channel-reads channels) + =. cor refresh-all-summaries + cor(importing |) +:: +++ set-channel-reads + |= =channels:c + ^+ cor + =+ .^(=unreads:c %gx (scry-path %channels /v1/unreads/noun)) + =/ entries ~(tap by unreads) + =; events=(list [time incoming-event:a]) + |- + ?~ events cor + =. cor (%*(. add start-time -.i.events) +.i.events) + $(events t.events) + |- ^- (list [time incoming-event:a]) + ?~ entries ~ + =/ head i.entries + =* next $(entries t.entries) + =/ [=nest:c =unread:c] head + =/ channel (~(get by channels) nest) + ?~ channel next + =/ group group.perm.u.channel + =; events=(list [time incoming-event:a]) + (weld events next) + =/ posts=(list [time incoming-event:a]) + ?~ unread.unread ~ + %+ murn + (tab:on-posts:c posts.u.channel `(sub id.u.unread.unread 1) count.u.unread.unread) + |= [=time post=(unit post:c)] + ?~ post ~ + =/ key=message-key:a + :_ time + [author.u.post time] + =/ mention + (was-mentioned:ch-utils content.u.post our.bowl) + `[time %post key nest group content.u.post mention] + =/ replies=(list [time incoming-event:a]) + %- zing + %+ murn + ~(tap by threads.unread) + |= [=id-post:c [id=id-reply:c count=@ud]] + ^- (unit (list [time incoming-event:a])) + =/ post=(unit (unit post:c)) (get:on-posts:c posts.u.channel id-post) + ?~ post ~ + ?~ u.post ~ + %- some + %+ turn + (tab:on-replies:c replies.u.u.post `(sub id 1) count) + |= [=time =reply:c] + =/ key=message-key:a + :_ time + [author.reply time] + =/ parent=message-key:a + :_ id-post + [author.u.u.post id-post] + =/ mention + (was-mentioned:ch-utils content.reply our.bowl) + [time %reply key parent nest group content.reply mention] + =/ init-time + ?: &(=(posts ~) =(replies ~)) recency.unread + *@da + :- [init-time %chan-init nest group] + (welp posts replies) +++ set-chat-reads + ^+ cor + =+ .^(=unreads:ch %gx (scry-path %chat /unreads/noun)) + =+ .^ [dms=(map ship dm:ch) clubs=(map id:club:ch club:ch)] + %gx (scry-path %chat /full/noun) + == + =/ entries ~(tap by unreads) + =; events=(list [time incoming-event:a]) + |- + ?~ events cor + =. cor (%*(. add start-time -.i.events) +.i.events) + $(events t.events) + |- ^- (list [time incoming-event:a]) + ?~ entries ~ + =/ head i.entries + =* next $(entries t.entries) + =/ [=whom:ch =unread:unreads:ch] head + =/ =pact:ch + ?- -.whom + %ship pact:(~(gut by dms) p.whom *dm:ch) + %club pact:(~(gut by clubs) p.whom *club:ch) + == + =; events=(list [time incoming-event:a]) + (weld events next) + =/ writs=(list [time incoming-event:a]) + ?~ unread.unread ~ + %+ murn + (tab:on:writs:ch wit.pact `(sub time.u.unread.unread 1) count.u.unread.unread) + |= [=time =writ:ch] + =/ key=message-key:a [id.writ time] + =/ mention + (was-mentioned:ch-utils content.writ our.bowl) + `[time %dm-post key whom content.writ mention] + =/ replies=(list [time incoming-event:a]) + %- zing + %+ murn + ~(tap by threads.unread) + |= [parent=message-key:ch [key=message-key:ch count=@ud]] + ^- (unit (list [time incoming-event:a])) + =/ writ=(unit writ:ch) (get:on:writs:ch wit.pact time.parent) + ?~ writ ~ + %- some + %+ turn + (tab:on:replies:ch replies.u.writ `(sub time.key 1) count) + |= [=time =reply:ch] + =/ mention + (was-mentioned:ch-utils content.reply our.bowl) + [time %dm-reply key parent whom content.reply mention] + =/ init-time + ?: &(=(writs ~) =(replies ~)) recency.unread + *@da + :- [init-time %dm-invite whom] + (welp writs replies) +++ set-volumes + |= =channels:c + =+ .^(=volume:v %gx (scry-path %groups /volume/all/noun)) + :: set all existing channels to old default since new default is different + =^ checkers cor + =/ checkers=(map flag:g $-([ship nest:g] ?)) ~ + =/ entries ~(tap by channels) + |- + ?~ entries [checkers cor] + =/ [=nest:c =channel:c] i.entries + =* group group.perm.channel + =+ .^(exists=? %gx (scry-path %groups /exists/(scot %p p.group)/[q.group]/noun)) + ?. exists $(entries t.entries) + =^ can-read checkers + ?^ gate=(~(get by checkers) group) [u.gate checkers] + =/ =path + %+ scry-path %groups + /groups/(scot %p p.group)/[q.group]/can-read/noun + =/ test=$-([ship nest:g] ?) + => [path=path nest=nest:g ..zuse] ~+ + .^($-([ship nest] ?) %gx path) + [test (~(put by checkers) group test)] + =. cor + :: don't set channel default if group above it has setting + ?: (~(has by area.volume) group) cor + %+ adjust [%channel nest group] + ?: (can-read our.bowl nest) `(my [%post & |] ~) + `mute:a + $(entries t.entries) + :: set any overrides from previous volume settings + =. cor (adjust [%base ~] `(~(got by old-volumes:a) base.volume)) + =. cor + =/ entries ~(tap by chan.volume) + |- + ?~ entries cor + =/ [=nest:g =level:v] i.entries + =* next $(entries t.entries) + ?. ?=(?(%chat %diary %heap) -.nest) next + =/ channel (~(get by channels) nest) + ?~ channel next + ?~ can-read=(~(get by checkers) group.perm.u.channel) next + :: don't override previously set mute from channel migration + ?. (u.can-read our.bowl nest) next + =. cor + %+ adjust [%channel nest group.perm.u.channel] + `(~(got by old-volumes:a) level) + next + =/ entries ~(tap by area.volume) + |- + ?~ entries cor + =* head i.entries + =. cor + %+ adjust [%group -.head] + `(~(got by old-volumes:a) +.head) + $(entries t.entries) -- diff --git a/packages/shared/src/logic/utils.ts b/packages/shared/src/logic/utils.ts index ac02917d28..be3dcf8704 100644 --- a/packages/shared/src/logic/utils.ts +++ b/packages/shared/src/logic/utils.ts @@ -31,6 +31,17 @@ export function isValidUrl(str?: string): boolean { return str ? !!URL_REGEX.test(str) : false; } +export function getPrettyAppName(kind: 'chat' | 'diary' | 'heap') { + switch (kind) { + case 'chat': + return 'Chat'; + case 'diary': + return 'Notebook'; + case 'heap': + return 'Gallery'; + } +} + export async function jsonFetch( info: RequestInfo, init?: RequestInit diff --git a/packages/shared/src/urbit/activity.ts b/packages/shared/src/urbit/activity.ts index 61db2bc11e..57f79acb15 100644 --- a/packages/shared/src/urbit/activity.ts +++ b/packages/shared/src/urbit/activity.ts @@ -200,7 +200,8 @@ export type VolumeMap = Partial>; export type ReadAction = | { event: ActivityIncomingEvent } | { item: string } - | { all: null }; + | { all: null } + | { recursive: null }; export interface ActivityReadAction { source: Source; From b6126a0718702e3a04f9753356b96d6d433fbd4e Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 2 Jul 2024 17:52:57 -0500 Subject: [PATCH 03/12] activity: put sync reads under test --- desk/app/activity.hoon | 76 ++++++--- desk/tests/app/activity.hoon | 298 +++++++++++++++++++++++++++++++++++ 2 files changed, 348 insertions(+), 26 deletions(-) create mode 100644 desk/tests/app/activity.hoon diff --git a/desk/app/activity.hoon b/desk/app/activity.hoon index 711348a794..4c2aab8920 100644 --- a/desk/app/activity.hoon +++ b/desk/app/activity.hoon @@ -156,7 +156,7 @@ %activity-action =+ !<(=action:a vase) ?- -.action - %add (add +.action) + %add (add-event +.action) %del (del +.action) %read (read source.action read-action.action |) %adjust (adjust +.action) @@ -402,7 +402,7 @@ =/ v1-cage=cage activity-update-1+!>(update) =. cor (give %fact v1-paths v1-cage) (give %fact v0-paths v0-cage) -++ add +++ add-event =/ start-time=time now.bowl |= inc=incoming-event:a ^+ cor @@ -596,7 +596,9 @@ ^- index:a =/ new-floor=(unit time) (find-floor index) ?~ new-floor index - index(floor.reads u.new-floor) + =/ new-reads=read-items:a + (lot:on-read-items:a items.reads.index new-floor ~) + index(reads [u.new-floor new-reads]) :: ++ read |= [=source:a action=read-action:a from-parent=?] @@ -620,7 +622,20 @@ =. cor (update-parents source ~[new-read]) (update-index source index(items.reads read-items) &) :: - ?(%all %recursive) + %all + =/ new-reads=(list [=time-id:a ~]) + %+ murn + %- tap:on-event:a + (lot:on-event:a stream.index `floor.reads.index ~) + |= [=time =event:a] + ?: child.event ~ + `[time ~] + =/ new index(items.reads (malt new-reads)) + =? cor !from-parent + (update-parents source new-reads) + (update-index source new &) + :: + %recursive =/ new-floor=time ?^ time.action u.time.action =/ latest=(unit [=time event:a]) @@ -639,8 +654,6 @@ =. cor =/ children (get-children source) |- - :: only update children if we're in recursive mode - ?: ?=(%all -.action) cor ?~ children cor =/ =source:a i.children =. cor (read source action &) @@ -756,10 +769,10 @@ %- ~(rep by child-map) |= [[=source:a as=activity-summary:a] sum=activity-summary:a] %= sum - count (^add count.sum count.as) + count (add count.sum count.as) notify |(notify.sum notify.as) newest (max newest.as newest.sum) - notify-count (^add notify-count.sum notify-count.as) + notify-count (add notify-count.sum notify-count.as) == =/ newest=time :(max newest.cs floor.reads top) =/ total @@ -849,17 +862,19 @@ :: ++ sync-reads =/ oldest-floors=(map source:a time) ~ - =/ indexes + =/ sources :: sort children first in order so we only have to make one pass :: of summarization aka not repeatedly updating the same source :: %+ sort - ~(tap by indices) - |= [[asrc=source:a *] [bsrc=source:a *]] + ~(tap in ~(key by indices)) + |= [asrc=source:a bsrc=source:a] (gth (get-order asrc) (get-order bsrc)) |- - ?~ indexes cor - =/ [=source:a =index:a] i.indexes + ?~ sources cor + =/ =source:a i.sources + =/ =index:a (~(got by indices) source) + =/ our-reads (get-reads stream.index ~ `floor.reads.index) =^ min-floors indices =/ parents (get-parents source) =/ floors=(map source:a time) ~ @@ -868,13 +883,15 @@ =/ parent-index (get-index i.parents) =/ parent-reads :- floor.reads.parent-index - (uni:on-read-items:a items.reads.parent-index items.reads.index) + %+ gas:on-read-items:a + (uni:on-read-items:a items.reads.parent-index items.reads.index) + our-reads :: keep track of oldest child floor =. floors %+ ~(put by floors) i.parents - (min floor.reads.index (~(gut by oldest-floors) source now.bowl)) + (min floor.reads.index (~(gut by oldest-floors) i.parents now.bowl)) :: update parents with aggregated reads and move floor if appropriate - =. indices (~(put by indices) i.parents index(reads parent-reads)) + =. indices (~(put by indices) i.parents parent-index(reads parent-reads)) $(parents t.parents) =. oldest-floors (~(uni by oldest-floors) min-floors) =. reads.index @@ -886,15 +903,22 @@ =; main-reads=read-items:a [u.min-floor main-reads] %+ gas:on-read-items:a items.reads.index - %+ murn - :: take all events between our floor and the oldest child floor - (tap:on-event:a (lot:on-event:a stream.index `u.min-floor `floor.reads.index)) - |= [=time =event:a] - :: ignore child events - ?: child.event ~ - `[time ~] + (get-reads stream.index `u.min-floor `floor.reads.index) =. cor (update-index source index &) - $(indexes t.indexes) + $(sources t.sources) +:: +++ get-reads + |= [=stream:a start=(unit time) end=(unit time)] + %+ murn + :: take all events between our floor and the oldest child floor + %- tap:on-event:a + %^ lot:on-event:a stream + ?~(start ~ `(sub u.start 1)) + ?~(end ~ `(add u.end 1)) + |= [=time =event:a] + :: ignore child events + ?: child.event ~ + `[time ~] :: :: at some time in the past, for clubs activity, %dm-post and %dm-reply events :: with bad message/parent identifiers (respectively) got pushed into our @@ -1042,7 +1066,7 @@ =; events=(list [time incoming-event:a]) |- ?~ events cor - =. cor (%*(. add start-time -.i.events) +.i.events) + =. cor (%*(. add-event start-time -.i.events) +.i.events) $(events t.events) |- ^- (list [time incoming-event:a]) ?~ entries ~ @@ -1103,7 +1127,7 @@ =; events=(list [time incoming-event:a]) |- ?~ events cor - =. cor (%*(. add start-time -.i.events) +.i.events) + =. cor (%*(. add-event start-time -.i.events) +.i.events) $(events t.events) |- ^- (list [time incoming-event:a]) ?~ entries ~ diff --git a/desk/tests/app/activity.hoon b/desk/tests/app/activity.hoon new file mode 100644 index 0000000000..78d0da04f8 --- /dev/null +++ b/desk/tests/app/activity.hoon @@ -0,0 +1,298 @@ +/- a=activity, g=groups, c=channels +/+ *test-agent +/= activity-agent /app/activity +|% +++ dap %activity +++ test-sync-reads-0 + (run-sync-reads pre-sync-state-0 post-sync-state-0 5) +:: +++ test-sync-reads-1 + (run-sync-reads pre-sync-state-1 post-sync-state-1 5) +:: +++ test-sync-reads-2 + (run-sync-reads pre-sync-state-2 post-sync-state-2 5) +:: +++ test-sync-reads-3 + (run-sync-reads pre-sync-state-3 post-sync-state-3 9) +++ run-sync-reads + |= [pre=indices:a post=indices:a count=@ud] + %- eval-mare + =/ m (mare ,~) + ^- form:m + ;< * bind:m (do-init dap activity-agent) + ;< * bind:m (jab-bowl |=(b=bowl b(our ~zod, src ~zod))) + ;< * bind:m (do-load activity-agent `!>([%3 %some pre ~ ~])) + ;< * bind:m (ex-equal !>(~(wyt by pre)) !>(count)) + ;< new=vase bind:m get-save + =/ want-indices post + =+ !<(new-state=current-state new) + =/ new-indices indices.new-state + (ex-equal !>(new-indices) !>(want-indices)) +:: ++$ current-state + $: %3 + allowed=notifications-allowed:a + =indices:a + =activity:a + =volume-settings:a + == ++$ index-pair [=source:a =index:a] ++$ source-set + $: thrd1=index-pair + thrd2=index-pair + chnl=index-pair + grp=index-pair + base=index-pair + == +:: in this case, we test when a child has unreads before any of the parents, +:: resulting in a large list of reads and a very early floor +++ state-0 + ^- source-set + =/ thrd1 (thread-source-1 flag nest i0) + =/ thrd2 (thread-source-2 flag nest i0) + =/ chnl (channel-source flag nest stream.index.thrd1 stream.index.thrd2 i0) + =/ grp (group-source flag stream.index.chnl) + :* thrd1 + thrd2 + chnl + grp + (base-source stream.index.grp) + == +++ pre-sync-state-0 + ^- indices:a + %- ~(gas by *indices:a) + => state-0 + .(base [base ~]) +++ post-sync-state-0 + ^- indices:a + =+ state-0 + :: only care about the index + =/ new-reads=reads:a + :- d-1 + %+ gas:on-read-items:a *read-items:a + :~ [d1 ~] + [d2 ~] + [d3 ~] + [d4 ~] + == + %- my + :~ thrd1 + thrd2 + chnl(reads.index new-reads) + grp(reads.index new-reads) + base(reads.index new-reads) + == +:: in this case, we test when a child has unreads later than any of the +:: parents, but all other children are read, resulting in a late floor +:: and no reads +++ state-1 + ^- source-set + =/ thrd1 (thread-source-2 flag nest i0) + =/ thrd2 (thread-source-3 flag nest i0) + =/ chnl (channel-source flag nest stream.index.thrd1 stream.index.thrd2 i0) + =/ grp (group-source flag stream.index.chnl) + :* thrd1 + thrd2 + chnl + grp + (base-source stream.index.grp) + == +++ pre-sync-state-1 + ^- indices:a + %- ~(gas by *indices:a) + => state-1 + .(base [base ~]) +++ post-sync-state-1 + ^- indices:a + =+ state-1 + %- my + :~ thrd1 + thrd2 + chnl(reads.index [d4 ~]) + grp(reads.index [d4 ~]) + base(reads.index [d4 ~]) + == +:: in this case we test a parent having mixed reads with children that +:: are all read, resulting in a floor up to the parent reads and one +:: read item that comes later +++ state-2 + ^- source-set + =/ thrd1 (thread-source-2 flag nest i0) + =/ thrd2 (thread-source-3 flag nest i0) + =. thrd2 thrd2(reads.index [d5 ~]) + =/ chnl (channel-source flag nest stream.index.thrd1 stream.index.thrd2 i0) + =. chnl chnl(reads.index [d3 ~]) + =/ grp (group-source flag stream.index.chnl) + :* thrd1 + thrd2 + chnl + grp + (base-source stream.index.grp) + == +++ pre-sync-state-2 + ^- indices:a + %- ~(gas by *indices:a) + => state-2 + .(base [base ~]) +++ post-sync-state-2 + ^- indices:a + =+ state-2 + =/ new-reads=reads:a [d3 (my [d5 ~] ~)] + %- my + :~ thrd1 + thrd2 + chnl(reads.index new-reads) + grp(reads.index new-reads) + base(reads.index new-reads) + == +:: in this case we test multiple parents one with mixed reads and one +:: with all reads +++ state-3 + =/ thrd1-1 (thread-source-1 flag nest i0) + =/ thrd1-2 (thread-source-2 flag nest i0) + =/ chnl1 (channel-source flag nest stream.index.thrd1-1 stream.index.thrd1-2 i0) + =/ grp1 (group-source flag stream.index.chnl1) + :: + =/ second-grp=flag:g [~dev %urbit] + =/ second-chnl=nest:c [%chat ~dev %lobby] + =/ thrd2-1 (thread-source-2 second-grp second-chnl i1) + =/ thrd2-2 (thread-source-3 second-grp second-chnl i1) + =/ chnl2 + (channel-source second-grp second-chnl stream.index.thrd2-1 stream.index.thrd2-2 i1) + =/ grp2 (group-source second-grp stream.index.chnl2) + :: + :* thrd1-1=thrd1-1 + thrd1-2=thrd1-2 + thrd2-1=thrd2-1 + thrd2-2=thrd2-2 + chnl1=chnl1 + chnl2=chnl2 + grp1=grp1 + grp2=grp2 + base=(base-source (uni:on-event:a stream.index.grp1 stream.index.grp2)) + == +++ pre-sync-state-3 + ^- indices:a + %- ~(gas by *indices:a) + => state-3 + .(base [base ~]) +++ post-sync-state-3 + ^- indices:a + =+ state-3 + =/ new-reads2=reads:a [(add d4 i1) ~] + =/ new-reads1=reads:a + :- d-1 + %+ gas:on-read-items:a *read-items:a + :~ [d1 ~] + [d2 ~] + [d3 ~] + [d4 ~] + == + =/ base-reads=reads:a + :- d-1 + %+ gas:on-read-items:a items.new-reads1 + :~ [(add d1 i1) ~] + [(add d2 i1) ~] + [(add d3 i1) ~] + [(add d4 i1) ~] + == + %- my + :~ thrd1-1 + thrd1-2 + thrd2-1 + thrd2-2 + chnl1(reads.index new-reads1) + chnl2(reads.index new-reads2) + grp1(reads.index new-reads1) + grp2(reads.index new-reads2) + base(reads.index base-reads) + == +:: base +++ base-source + |= group-stream=stream:a + ^- index-pair + :- [%base ~] + :_ [*@da ~] + (child-stream group-stream) +:: the group has no posts of its own only from children, it is "unread" +++ group-source + |= [=flag:g channel-stream=stream:a] + ^- index-pair + :- [%group flag] + :_ [*@da ~] + (child-stream channel-stream) +:: the channel only has two posts, and it is completely read +++ channel-source + |= [=flag:g =nest:c thrd1=stream:a thrd2=stream:a interval=@dr] + ^- index-pair + :- [%channel nest flag] + :_ [(add d4 interval) ~] + %+ uni:on-event:a (child-stream thrd1) + %+ gas:on-event:a + (child-stream thrd2) + =/ key1 (mod [[~dev d3] d3] interval) + =/ key2 (mod [[~dev d4] d4] interval) + :~ [time.key1 [[%post key1 nest flag *story:c |] | |]] + [time.key2 [[%post key2 nest flag *story:c |] | |]] + == +:: +:: this thread has two messages and is unread +++ thread-source-1 + %^ thread-source + [[~zod *@da] *@da] + [d-1 ~] + :~ [[~dev d5] d5] + [[~dev d0] d0] + == +:: this thread has two messages and is read +++ thread-source-2 + %^ thread-source + [[~zod d1] d1] + [d2 ~] + :~ [[~dev d1] d1] + [[~dev d2] d2] + == +++ thread-source-3 + %^ thread-source + [[~zod *@da] *@da] + [*@da ~] + ~[[[~dev d5] d5]] +++ thread-source + |= [parent=message-key:a =reads:a replies=(list message-key:a)] + |= [=flag:g =nest:c interval=@dr] + =. parent (mod parent interval) + ^- index-pair + :- [%thread parent nest flag] + :_ %= reads + floor (add floor.reads interval) + items + %+ gas:on-read-items:a *read-items:a + %+ turn + (tap:on-read-items:a items.reads) + |= [=time *] + [(add time interval) ~] + == + %+ gas:on-event:a *stream:a + (turn replies (curr create-reply-pair parent interval)) +++ create-reply-pair + |= [key=message-key:a parent=message-key:a interval=@dr] + =. key (mod key interval) + [time.key [[%reply key parent nest flag *story:c |] & |]] +++ mod + |= [key=message-key:a interval=@dr] + key(time (add time.key interval), q.id (add q.id.key interval)) +++ child-stream + |= =stream:a + (run:on-event:a stream |=(=event:a event(child &))) +++ i0 *@dr +++ i1 (add i0 ~s1) +++ d-1 (dec *@da) +++ d0 *@da +++ d1 (add *@da ~d1) +++ d2 (add *@da ~d2) +++ d3 (add *@da ~d3) +++ d4 (add *@da ~d4) +++ d5 (add *@da ~d5) +++ flag [~zod %test] +++ nest [%chat ~zod %chat] +-- \ No newline at end of file From 35fda473420cb0e8df5fb5ad6ae209e1b44bc504 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 3 Jul 2024 14:37:37 -0400 Subject: [PATCH 04/12] native: switch DM subtitle icon to ChannelTalk, presig @p --- packages/ui/src/components/ListItem/ChannelListItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index 309b828e15..2ef5f2d30c 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -19,7 +19,7 @@ export function ChannelListItem({ } & ListItemProps) { const unreadCount = model.unread?.count ?? 0; const title = utils.getChannelTitle(model); - const firstMemberId = model.members?.[0]?.contactId?.replace('~', '') ?? ''; + const firstMemberId = model.members?.[0]?.contactId ?? ''; const memberCount = model.members?.length ?? 0; const { subtitle, subtitleIcon } = useMemo(() => { @@ -31,7 +31,7 @@ export function ChannelListItem({ ] .filter((v) => !!v) .join(' '), - subtitleIcon: 'Profile', + subtitleIcon: 'ChannelTalk', } as const; } else { return { From 5f75aadbb63f19220fec39eb67fc3a2a9e5a0737 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 3 Jul 2024 15:07:47 -0400 Subject: [PATCH 05/12] native: add ChannelDM.svg, change in ChannelListItem --- packages/ui/src/assets/icons/ChannelDM.svg | 3 +++ packages/ui/src/assets/icons/index.ts | 1 + packages/ui/src/components/ListItem/ChannelListItem.tsx | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/assets/icons/ChannelDM.svg diff --git a/packages/ui/src/assets/icons/ChannelDM.svg b/packages/ui/src/assets/icons/ChannelDM.svg new file mode 100644 index 0000000000..1dad858109 --- /dev/null +++ b/packages/ui/src/assets/icons/ChannelDM.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/icons/index.ts b/packages/ui/src/assets/icons/index.ts index ca7a19b802..24ccdfba50 100644 --- a/packages/ui/src/assets/icons/index.ts +++ b/packages/ui/src/assets/icons/index.ts @@ -72,3 +72,4 @@ export { default as Settings } from './Settings.svg'; export { default as Stop } from './Stop.svg'; export { default as TBlock } from './TBlock.svg'; export { default as Mute } from './Mute.svg'; +export { default as ChannelDM } from './ChannelDM.svg'; diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index 2ef5f2d30c..03848d6aaf 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -31,7 +31,7 @@ export function ChannelListItem({ ] .filter((v) => !!v) .join(' '), - subtitleIcon: 'ChannelTalk', + subtitleIcon: 'ChannelDM', } as const; } else { return { From ab1cd1ea1fe958ee2a58aeef514488bc2caf2dd9 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 3 Jul 2024 15:14:16 -0400 Subject: [PATCH 06/12] native: add ChannelMultiDM.svg --- packages/ui/src/assets/icons/ChannelMultiDM.svg | 11 +++++++++++ packages/ui/src/assets/icons/index.ts | 1 + .../ui/src/components/ListItem/ChannelListItem.tsx | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/assets/icons/ChannelMultiDM.svg diff --git a/packages/ui/src/assets/icons/ChannelMultiDM.svg b/packages/ui/src/assets/icons/ChannelMultiDM.svg new file mode 100644 index 0000000000..9e5bc4e59a --- /dev/null +++ b/packages/ui/src/assets/icons/ChannelMultiDM.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/ui/src/assets/icons/index.ts b/packages/ui/src/assets/icons/index.ts index 24ccdfba50..950eda1e85 100644 --- a/packages/ui/src/assets/icons/index.ts +++ b/packages/ui/src/assets/icons/index.ts @@ -73,3 +73,4 @@ export { default as Stop } from './Stop.svg'; export { default as TBlock } from './TBlock.svg'; export { default as Mute } from './Mute.svg'; export { default as ChannelDM } from './ChannelDM.svg'; +export { default as ChannelMultiDM } from './ChannelMultiDM.svg'; diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index 03848d6aaf..66e15886b8 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -31,7 +31,7 @@ export function ChannelListItem({ ] .filter((v) => !!v) .join(' '), - subtitleIcon: 'ChannelDM', + subtitleIcon: memberCount > 2 ? 'ChannelMultiDM' : 'ChannelDM', } as const; } else { return { From 8a26dde2863e60d3978abc65a24804cfee7044ee Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Wed, 3 Jul 2024 14:30:57 -0500 Subject: [PATCH 07/12] native: update crashlytics setup --- apps/tlon-mobile/app.config.ts | 2 ++ apps/tlon-mobile/firebase.json | 5 +++++ apps/tlon-mobile/ios/Podfile | 4 ++-- apps/tlon-mobile/ios/Podfile.lock | 6 +++--- 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 apps/tlon-mobile/firebase.json diff --git a/apps/tlon-mobile/app.config.ts b/apps/tlon-mobile/app.config.ts index 0240d2a9e1..033be3c255 100644 --- a/apps/tlon-mobile/app.config.ts +++ b/apps/tlon-mobile/app.config.ts @@ -52,6 +52,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ runtimeVersion: '4.0.1', }, plugins: [ + '@react-native-firebase/app', + '@react-native-firebase/crashlytics', [ 'expo-image-picker', { diff --git a/apps/tlon-mobile/firebase.json b/apps/tlon-mobile/firebase.json new file mode 100644 index 0000000000..719329b38b --- /dev/null +++ b/apps/tlon-mobile/firebase.json @@ -0,0 +1,5 @@ +{ + "react-native": { + "crashlytics_debug_enabled": false + } +} diff --git a/apps/tlon-mobile/ios/Podfile b/apps/tlon-mobile/ios/Podfile index c6ad43c18b..d89eecb70b 100644 --- a/apps/tlon-mobile/ios/Podfile +++ b/apps/tlon-mobile/ios/Podfile @@ -42,8 +42,8 @@ def shared(podfile_properties) use_expo_modules! $config = use_native_modules! - use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] - use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] + use_frameworks! :linkage => :static + $RNFirebaseAsStaticFramework = true # Flags change depending on the env values. flags = get_default_flags() diff --git a/apps/tlon-mobile/ios/Podfile.lock b/apps/tlon-mobile/ios/Podfile.lock index cdb7935ab1..35f7dd6694 100644 --- a/apps/tlon-mobile/ios/Podfile.lock +++ b/apps/tlon-mobile/ios/Podfile.lock @@ -1808,8 +1808,8 @@ SPEC CHECKSUMS: RNCAsyncStorage: 618d03a5f52fbccb3d7010076bc54712844c18ef RNCClipboard: 090462274cc05b02628bd158baf6d73c3abe8441 RNDeviceInfo: db5c64a060e66e5db3102d041ebe3ef307a85120 - RNFBApp: 614f1621b49db54ebd258df8c45427370d8d84a2 - RNFBCrashlytics: c39e903af97cb426f36a10d3268fb0623a1ccddf + RNFBApp: 91311b27bc9a33e23b76a62825afd1635501018a + RNFBCrashlytics: c3219ef7a0c779f2428236215781c38e7892f6f9 RNGestureHandler: 28bdf9a766c081e603120f79e925b72817c751c6 RNReanimated: fb34efce9255966f5d71bd0fc65e14042c4b88a9 RNScreens: 2b73f5eb2ac5d94fbd61fa4be0bfebd345716825 @@ -1824,6 +1824,6 @@ SPEC CHECKSUMS: UMAppLoader: 5df85360d65cabaef544be5424ac64672e648482 Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 -PODFILE CHECKSUM: 2ba4689327c6a97c2a40ef1fe4918cbab90120c7 +PODFILE CHECKSUM: 0cb7a78e5777e69c86c1bf4bb5135fd660376dbe COCOAPODS: 1.15.2 From 530a5358a4aaa49870aff7e4c5a2c9e4ea7730bb Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Wed, 3 Jul 2024 14:56:04 -0500 Subject: [PATCH 08/12] activity: refactoring action change and adding clarification --- .../groups/GroupSidebar/ChannelList.test.tsx | 1 + apps/tlon-web/src/state/activity.ts | 3 +- apps/tlon-web/src/state/unreads.ts | 25 ++---- desk/app/activity.hoon | 87 ++++++++++--------- desk/app/channels.hoon | 4 +- desk/app/chat.hoon | 2 +- desk/lib/activity-json.hoon | 8 +- desk/sur/activity.hoon | 3 +- packages/shared/src/api/activityApi.ts | 8 +- packages/shared/src/urbit/activity.ts | 3 +- 10 files changed, 75 insertions(+), 69 deletions(-) diff --git a/apps/tlon-web/src/groups/GroupSidebar/ChannelList.test.tsx b/apps/tlon-web/src/groups/GroupSidebar/ChannelList.test.tsx index 53e2fe6b82..e6893cb531 100644 --- a/apps/tlon-web/src/groups/GroupSidebar/ChannelList.test.tsx +++ b/apps/tlon-web/src/groups/GroupSidebar/ChannelList.test.tsx @@ -14,6 +14,7 @@ fakeGroup.channels[fakeNest] = createChannel('Fake Channel'); const fakeVessel = fakeGroup.fleet['~hastuc-dibtux']; vi.mock('@/state/groups', () => ({ + useAmAdmin: () => true, useGroup: () => fakeGroup, useRouteGroup: () => fakeFlag, useGroupFlag: () => fakeFlag, diff --git a/apps/tlon-web/src/state/activity.ts b/apps/tlon-web/src/state/activity.ts index 6da1ed6ae2..162e7568f4 100644 --- a/apps/tlon-web/src/state/activity.ts +++ b/apps/tlon-web/src/state/activity.ts @@ -159,8 +159,7 @@ export function useMarkReadMutation(recursive = false) { activityAction({ read: { source: variables.source, - action: - variables.action || recursive ? { recursive: null } : { all: null }, + action: variables.action || { all: { time: null, deep: recursive } }, }, }) ); diff --git a/apps/tlon-web/src/state/unreads.ts b/apps/tlon-web/src/state/unreads.ts index a88f2870fe..e42f3b0c76 100644 --- a/apps/tlon-web/src/state/unreads.ts +++ b/apps/tlon-web/src/state/unreads.ts @@ -426,27 +426,18 @@ export function useCombinedChatUnreads(messagesFilter: SidebarFilter) { ); } -export function useAllGroupUnreads() { - const sources = useUnreadsStore(useCallback((s) => s.sources, [])); - return Object.entries(sources).filter( - ([key, source]) => - key.startsWith('group') && - source.combined.count > 0 && - source.combined.status === 'unread' - ); -} - export function useMarkAllGroupsRead() { - const allGroupUnreads = useAllGroupUnreads(); - const { read } = useUnreadsStore(); + const { read, sources } = useUnreadsStore(); const { mutate } = useMarkReadMutation(true); const markAllRead = useCallback(() => { - allGroupUnreads.forEach(([sourceId]) => { - read(sourceId); - mutate({ source: { group: stripSourcePrefix(sourceId) } }); - }); - }, [allGroupUnreads, read, mutate]); + Object.entries(sources) + .filter(([key]) => key.startsWith('group')) + .forEach(([sourceId]) => { + read(sourceId); + mutate({ source: { group: stripSourcePrefix(sourceId) } }); + }); + }, [sources, read, mutate]); return markAllRead; } diff --git a/desk/app/activity.hoon b/desk/app/activity.hoon index 2c34ebd907..6618f8dad3 100644 --- a/desk/app/activity.hoon +++ b/desk/app/activity.hoon @@ -157,7 +157,7 @@ =+ !<(=action:a vase) ?- -.action %add (add-event +.action) - %del (del +.action) + %del (del-source +.action) %read (read source.action read-action.action |) %adjust (adjust +.action) %allow-notifications (allow +.action) @@ -410,7 +410,7 @@ =/ t start-time |- ?. (has:on-event:a stream:base t) t - $(t (^add t ~s0..0001)) + $(t (add t ~s0..0001)) =/ notify notify:(get-volume inc) =/ =event:a [inc notify |] =/ =source:a (determine-source inc) @@ -419,7 +419,7 @@ (give-update update ~) =? cor &(!importing notify (is-allowed inc)) (give %fact ~[/notifications /v0/notifications] activity-event+!>([time-id event])) - :: we always update sources in order, so make sure base is last + :: we always update sources in order, so make sure base is processed last =; co =. indices.co =/ =stream:a (put:on-event:a stream:base time-id event) @@ -464,7 +464,7 @@ %dm-post-mention & %dm-reply-mention & == -++ del +++ del-source |= =source:a ^+ cor =. indices (~(del by indices) source) @@ -477,8 +477,8 @@ =/ =index:a (~(gut by indices) source *index:a) =/ new=_stream.index (put:on-event:a stream.index time-id event) - (update-index source index(stream new) |) -++ update-index + (refresh-index source index(stream new) |) +++ refresh-index |= [=source:a new=index:a new-floor=?] =? new new-floor (update-floor new) @@ -619,49 +619,57 @@ %item =/ new-read [id.action ~] =/ read-items (put:on-read-items:a items.reads.index new-read) - =. cor (update-parents source ~[new-read]) - (update-index source index(items.reads read-items) &) + =. cor (propagate-read-items source ~[new-read]) + (refresh-index source index(items.reads read-items) &) :: %all - =/ new-reads=(list [=time-id:a ~]) - %+ murn - %- tap:on-event:a - (lot:on-event:a stream.index `floor.reads.index ~) - |= [=time =event:a] - ?: child.event ~ - `[time ~] - =/ new index(items.reads (malt new-reads)) - =? cor !from-parent - (update-parents source new-reads) - (update-index source new &) - :: - %recursive - =/ new-floor=time + =/ new=index:a + :: if we're only marking at our level, then we only want to grab + :: read items from our level, because we can't move the floor in + :: case children have older unread items + ?: !deep.action + =- index(items.reads (malt -)) + %+ murn + %- tap:on-event:a + (lot:on-event:a stream.index `floor.reads.index ~) + |= [=time =event:a] + ?: child.event ~ + `[time ~] + :: otherwise, we can short circuit and just mark everything read, + :: because we're going to also mark all children read + =- index(reads [- ~]) ?^ time.action u.time.action =/ latest=(unit [=time event:a]) (ram:on-event:a stream.index) ?~(latest now.bowl time.u.latest) - =/ new=index:a index(reads [new-floor ~]) - ~& [source floor.reads.index new-floor] - =? cor !from-parent - %+ update-parents source - %+ turn - %- tap:on-event:a - %^ lot:on-event:a stream.index `floor.reads.index - ?:((gte new-floor floor.reads.index) `+(new-floor) ~) - |= [=time-id:a *] - [time-id ~] - =. cor + :: if we're marking deep then we need to recursively read all children + =? cor deep.action =/ children (get-children source) |- ?~ children cor =/ =source:a i.children =. cor (read source action &) $(children t.children) - (update-index source new &) + :: we need to refresh our own index to reflect new reads + =. cor (refresh-index source new &) + :: if this isn't a recursive read, we need to propagate the new read + :: items up the tree so that parents can keep accurate counts + =? cor !from-parent + %+ propagate-read-items source + :: if we're not marking deep, we already have the items to send up + ?: !deep.action (tap:on-read-items:a items.reads.new) + :: if not, we need to generate the new items based on the floor + :: we just came up with + %+ turn + %- tap:on-event:a + %^ lot:on-event:a stream.index `floor.reads.index + ?:((gte floor.reads.new floor.reads.index) `+(floor.reads.new) ~) + |= [=time-id:a *] + [time-id ~] + cor == :: -++ update-parents +++ propagate-read-items |= [=source:a items=(list [=time-id:a ~])] =/ parents (get-parents source) |- @@ -670,7 +678,7 @@ =/ =read-items:a (gas:on-read-items:a items.reads.parent-index items) =. cor - (update-index i.parents parent-index(items.reads read-items) &) + (refresh-index i.parents parent-index(items.reads read-items) &) $(parents t.parents) ++ get-index |= =source:a @@ -704,7 +712,8 @@ ^- (list source:a) ?: ?=(%base -.source) ~ ?< ?=(%base -.source) - :- [%base ~] + =- (snoc - [%base ~]) + ^- (list source:a) ?+ -.source ~ %channel ~[[%group group.source]] %dm-thread ~[[%dm whom.source]] @@ -904,7 +913,7 @@ [u.min-floor main-reads] %+ gas:on-read-items:a items.reads.index (get-reads stream.index `u.min-floor `floor.reads.index) - =. cor (update-index source index &) + =. cor (refresh-index source index &) $(sources t.sources) :: ++ get-reads @@ -975,7 +984,7 @@ volume-settings (~(del by volume-settings) old-source) == :: update source + index, if new key create new index - =. cor (update-index source index.i.indxs &) + =. cor (refresh-index source index.i.indxs &) $(indxs t.indxs) %+ weld (handle-dms u.club dms) diff --git a/desk/app/channels.hoon b/desk/app/channels.hoon index 8915a5c68d..9fcc168857 100644 --- a/desk/app/channels.hoon +++ b/desk/app/channels.hoon @@ -745,7 +745,7 @@ ^+ ca-core ?: =(author our.bowl) =/ =source [%channel nest group.perm.perm.channel] - (send ~[`action`[%read source [%all `now.bowl]]]) + (send ~[`action`[%read source [%all `now.bowl |]]]) =/ mention=? (was-mentioned:utils content our.bowl) =/ action [%add %post [[author id] id] nest group.perm.perm.channel content mention] @@ -756,7 +756,7 @@ =/ parent-key=message-key [[author id]:parent id.parent] ?: =(author our.bowl) =/ =source [%thread parent-key nest group.perm.perm.channel] - (send ~[`action`[%read source [%all `now.bowl]]]) + (send ~[`action`[%read source [%all `now.bowl |]]]) =/ mention=? (was-mentioned:utils content our.bowl) =/ in-replies %+ lien (tap:on-v-replies:c replies.parent) diff --git a/desk/app/chat.hoon b/desk/app/chat.hoon index 5a3f0fcbfc..5ac03241f7 100644 --- a/desk/app/chat.hoon +++ b/desk/app/chat.hoon @@ -785,7 +785,7 @@ =/ =source ?: ?=(%post -.concern) [%dm whom] [%dm-thread top.concern whom] - activity-action+!>(`action`[%read source [%all `now.bowl]]) + activity-action+!>(`action`[%read source [%all `now.bowl |]]) :- %activity-action !> ^- action :- %add diff --git a/desk/lib/activity-json.hoon b/desk/lib/activity-json.hoon index 5c8cd0ec94..20e3c85c68 100644 --- a/desk/lib/activity-json.hoon +++ b/desk/lib/activity-json.hoon @@ -435,9 +435,13 @@ ++ read-action %- of :~ item/id - all/(mu (se %ud)) + all/all-read event/incoming-event - recursive/(mu (se %ud)) + == + ++ all-read + %- ou + :~ time/(un (mu (se %ud))) + deep/(uf | bo) == :: +| %basics diff --git a/desk/sur/activity.hoon b/desk/sur/activity.hoon index a85a5c8283..ec8511e9fa 100644 --- a/desk/sur/activity.hoon +++ b/desk/sur/activity.hoon @@ -47,8 +47,7 @@ +$ read-action $% [%item id=time-id] [%event event=incoming-event] - [%all time=(unit time)] - [%recursive time=(unit time)] + [%all time=(unit time) deep=?] == :: +| %updates diff --git a/packages/shared/src/api/activityApi.ts b/packages/shared/src/api/activityApi.ts index aeae8c4d14..8d9f4a6194 100644 --- a/packages/shared/src/api/activityApi.ts +++ b/packages/shared/src/api/activityApi.ts @@ -401,7 +401,9 @@ export const readChannel = async (channel: db.Channel) => { source = { channel: { nest: channel.id, group: channel.groupId! } }; } - const action = activityAction({ read: { source, action: { all: null } } }); + const action = activityAction({ + read: { source, action: { all: { time: null, deep: false } } }, + }); logger.log(`reading channel ${channel.id}`, action); // simple retry logic to avoid failed read leading to lingering unread state @@ -472,7 +474,9 @@ export const readThread = async ({ }; } - const action = activityAction({ read: { source, action: { all: null } } }); + const action = activityAction({ + read: { source, action: { all: { time: null, deep: false } } }, + }); // simple retry logic to avoid failed read leading to lingering unread state return backOff(() => poke(action), { diff --git a/packages/shared/src/urbit/activity.ts b/packages/shared/src/urbit/activity.ts index 57f79acb15..201bb4797d 100644 --- a/packages/shared/src/urbit/activity.ts +++ b/packages/shared/src/urbit/activity.ts @@ -200,8 +200,7 @@ export type VolumeMap = Partial>; export type ReadAction = | { event: ActivityIncomingEvent } | { item: string } - | { all: null } - | { recursive: null }; + | { all: { time: string | null; deep: boolean } }; export interface ActivityReadAction { source: Source; From a09d7c2950be56dd0628554021e0a0681b3d914b Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Wed, 3 Jul 2024 15:34:56 -0500 Subject: [PATCH 09/12] activity: add agent description/preamble --- desk/app/activity.hoon | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/desk/app/activity.hoon b/desk/app/activity.hoon index 6618f8dad3..2938b92e5e 100644 --- a/desk/app/activity.hoon +++ b/desk/app/activity.hoon @@ -1,3 +1,42 @@ +:: activity: tracking what's happening and how we alert you +:: +:: the main goal of the agent is to keep track of every event you are +:: aware of, and which ones you've read and interacted with. by doing +:: this, we have a centralized place to keep your read state and +:: notifications/alerts in sync. +:: +:: at its core, activity is composed of a few key parts: +:: - events: things that happen in other agents +:: - sources: where the events happen or their parents +:: - streams: a collection of all events from a source and its +:: children. each stream represents a source's activity. +:: - reads: metadata about what's been read in this source and all +:: its children +:: - summaries: a summary of the activity in a source, used to +:: display badges, alerts, and counts about unread events +:: - volume settings: how should we badge/alert/notify you about +:: each event type +:: +:: this means that the streams form a tree structure. +:: - base: the root of the tree, where all events are stored +:: - group +:: - channel +:: - thread +:: - dm +:: - dm-thread +:: +:: with this structure that means that data flows upwards from the +:: leaves to the root, and that we can easily keep the read state +:: in sync by propagating read data up. similarly we can have a feed +:: of events at any point in the tree, because the children's events +:: are always included in the parent's stream. +:: +:: to make sure we stay in sync, we always process sources in leaf- +:: first order, aka threads/dm-threads first, then channels, then +:: groups, then dms, and then finally the base. this way we can +:: always be sure that we have the most up-to-date information about +:: the children and save ourselves from having to do extra work. +:: :: /- a=activity, c=channels, ch=chat, g=groups /+ default-agent, verb, dbug, ch-utils=channel-utils, v=volume From d75b9152279039497b33ef056c3f32d3e8b8174c Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Fri, 5 Jul 2024 10:07:44 -0500 Subject: [PATCH 10/12] native: fix reply references --- .../src/components/ContentReference/ChannelReference.tsx | 3 +++ .../components/ContentReference/ChatReferenceWrapper.tsx | 9 ++++++++- packages/ui/src/components/ContentReference/index.tsx | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/ContentReference/ChannelReference.tsx b/packages/ui/src/components/ContentReference/ChannelReference.tsx index cfb150f55a..5fc5f71d64 100644 --- a/packages/ui/src/components/ContentReference/ChannelReference.tsx +++ b/packages/ui/src/components/ContentReference/ChannelReference.tsx @@ -7,11 +7,13 @@ import ReferenceSkeleton from './ReferenceSkeleton'; export default function ChannelReference({ channelId, postId, + replyId, asAttachment = false, viewMode = 'chat', }: { channelId: string; postId: string; + replyId?: string; asAttachment?: boolean; viewMode?: PostViewMode; }) { @@ -24,6 +26,7 @@ export default function ChannelReference({ viewMode={viewMode} channelId={channelId} postId={postId} + replyId={replyId} /> ); } diff --git a/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx b/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx index fce6add58c..9d7dced36c 100644 --- a/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx +++ b/packages/ui/src/components/ContentReference/ChatReferenceWrapper.tsx @@ -7,16 +7,23 @@ import ReferenceSkeleton from './ReferenceSkeleton'; export default function ChatReferenceWrapper({ channelId, postId, + replyId, asAttachment = false, viewMode = 'chat', }: { channelId: string; postId: string; + replyId?: string; asAttachment?: boolean; viewMode?: PostViewMode; }) { const { usePost, useChannel } = useRequests(); - const { data: post, isError, error, isLoading } = usePost({ id: postId }); + const { + data: post, + isError, + error, + isLoading, + } = usePost({ id: replyId ? replyId : postId }); const { data: channel } = useChannel({ id: channelId }); const { onPressRef } = useNavigation(); diff --git a/packages/ui/src/components/ContentReference/index.tsx b/packages/ui/src/components/ContentReference/index.tsx index 2743ca56f8..a0e562c5b3 100644 --- a/packages/ui/src/components/ContentReference/index.tsx +++ b/packages/ui/src/components/ContentReference/index.tsx @@ -20,6 +20,7 @@ export default function ContentReference({ From 49a65346364b8d5f0bf8550f5f5eac0d73295589 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Fri, 5 Jul 2024 15:15:08 -0500 Subject: [PATCH 11/12] activity: clarifying 'all' read cases --- desk/app/activity.hoon | 59 +++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/desk/app/activity.hoon b/desk/app/activity.hoon index 2938b92e5e..7985a4ccfc 100644 --- a/desk/app/activity.hoon +++ b/desk/app/activity.hoon @@ -662,11 +662,13 @@ (refresh-index source index(items.reads read-items) &) :: %all - =/ new=index:a - :: if we're only marking at our level, then we only want to grab - :: read items from our level, because we can't move the floor in - :: case children have older unread items - ?: !deep.action + ?: !deep.action + =/ new=index:a + :: take every event between the floor and now, and put it into + :: the index's items.reads. this way, the floor can be moved + :: without "losing" any unreads, and the call to +refresh-index + :: below will clean up unnecessary items.reads entries. + :: =- index(items.reads (malt -)) %+ murn %- tap:on-event:a @@ -674,15 +676,26 @@ |= [=time =event:a] ?: child.event ~ `[time ~] - :: otherwise, we can short circuit and just mark everything read, - :: because we're going to also mark all children read + :: we need to refresh our own index to reflect new reads + =. cor (refresh-index source new &) + :: since we're not marking deep, we already have the items to + :: send up to parents + %+ propagate-read-items source + (tap:on-read-items:a items.reads.new) + :: + :: marking read "deeply" + :: + =/ new=index:a + :: we can short circuit and just mark everything read, because + :: we're going to also mark all children read =- index(reads [- ~]) ?^ time.action u.time.action =/ latest=(unit [=time event:a]) (ram:on-event:a stream.index) ?~(latest now.bowl time.u.latest) - :: if we're marking deep then we need to recursively read all children - =? cor deep.action + :: since we're marking deeply we need to recursively read all + :: children + =. cor =/ children (get-children source) |- ?~ children cor @@ -691,21 +704,19 @@ $(children t.children) :: we need to refresh our own index to reflect new reads =. cor (refresh-index source new &) - :: if this isn't a recursive read, we need to propagate the new read - :: items up the tree so that parents can keep accurate counts - =? cor !from-parent - %+ propagate-read-items source - :: if we're not marking deep, we already have the items to send up - ?: !deep.action (tap:on-read-items:a items.reads.new) - :: if not, we need to generate the new items based on the floor - :: we just came up with - %+ turn - %- tap:on-event:a - %^ lot:on-event:a stream.index `floor.reads.index - ?:((gte floor.reads.new floor.reads.index) `+(floor.reads.new) ~) - |= [=time-id:a *] - [time-id ~] - cor + :: if this isn't a recursive read (see 4 lines above), we need to + :: propagate the new read items up the tree so that parents can + :: keep accurate counts, otherwise we can no-op + ?: from-parent cor + %+ propagate-read-items source + :: if not, we need to generate the new items based on the floor + :: we just came up with + %+ turn + %- tap:on-event:a + %^ lot:on-event:a stream.index `floor.reads.index + ?:((gte floor.reads.new floor.reads.index) `+(floor.reads.new) ~) + |= [=time-id:a *] + [time-id ~] == :: ++ propagate-read-items From 609d27657d26d2dcb9ea09c38835f4ac26a51f21 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 5 Jul 2024 20:34:21 +0000 Subject: [PATCH 12/12] update glob: [skip actions] --- desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desk/desk.docket-0 b/desk/desk.docket-0 index fc705f4bd3..f4a3d80e48 100644 --- a/desk/desk.docket-0 +++ b/desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v3.52s3s.v4b9q.f51n6.09ib0.39bae.glob' 0v3.52s3s.v4b9q.f51n6.09ib0.39bae] + glob-http+['https://bootstrap.urbit.org/glob-0v3.ggj2b.7rlnr.v4hjo.vjf95.vb7pu.glob' 0v3.ggj2b.7rlnr.v4hjo.vjf95.vb7pu] base+'groups' version+[6 0 2] website+'https://tlon.io'