diff --git a/README.md b/README.md index f0f5e5b2f8..aa545b517a 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,6 @@ Gallery is a versatile repository for collective knowledge and references you want to share or remember. Within Groups, you can use Gallery to collect links, images, media, and even random musings. -## Learn more about Landscape, Tlon, and Urbit - -- [Learn more about Tlon →](https://tlon.io) -- [Learn more about Urbit →](https://urbit.org) - --- ## Developer documentation diff --git a/desk/app/chat.hoon b/desk/app/chat.hoon index 3154dbaf1f..6986ae1859 100644 --- a/desk/app/chat.hoon +++ b/desk/app/chat.hoon @@ -126,6 +126,11 @@ =. cor restore-missing-subs =. cor (emit %pass ca-area:ca-core:cor %agent [our.bowl dap.bowl] %poke %recheck-all-perms !>(0)) =. cor (emit %pass ca-area:ca-core:cor %agent [our.bowl dap.bowl] %poke %leave-old-channels !>(0)) + =. cor + %+ roll ~(tap in `(set @p)`(~(run in ~(key by chats)) head)) + |= [=ship cr=_cor] + ?: =(ship our.bowl) cr + (watch-epic:cr ship &) ?: =(okay:c cool) cor :: =? cor bad (emit (keep !>(old))) %- (note:wood %ver leaf/"New Epic" ~) @@ -303,10 +308,15 @@ (emit %pass /groups %agent [our.bowl %groups] %watch /groups) :: ++ watch-epic - |= her=ship + |= [her=ship leave=?] ^+ cor =/ =wire /epic =/ =dock [her dap.bowl] + ?: leave + %- emil + :~ [%pass wire %agent [her dap.bowl] %leave ~] + [%pass wire %agent [her dap.bowl] %watch /epic] + == ?: (~(has by wex.bowl) [wire dock]) cor (emit %pass wire %agent [her dap.bowl] %watch /epic) @@ -837,15 +847,13 @@ ^+ cor ?+ -.sign cor %kick - (watch-epic src.bowl) + (watch-epic src.bowl |) :: %fact ?. =(%epic p.cage.sign) %- (note:wood %odd leaf/"!!! weird fact on /epic" ~) cor =+ !<(=epic:e q.cage.sign) - ?. =(epic okay:c) :: is now our guy - cor %+ roll ~(tap by chats) |= [[=flag:g =chat:c] out=_cor] ?. =(src.bowl p.flag) @@ -1628,7 +1636,7 @@ ca-core :: %watch-ack - =. net.chat [%sub src.bowl & %chi ~] + =? net.chat ?=(%sub -.net.chat) net.chat(load ?=(~ p.sign)) ?~ p.sign ca-core %- (slog leaf/"Failed subscription" u.p.sign) :: =. gone & @@ -1656,7 +1664,7 @@ ca-core %- (note:wood %ver leaf/"took lev epic: {}" ~) =. saga.net.chat lev/~ - =. cor (watch-epic p.flag) + =. cor (watch-epic p.flag |) ca-core :: ++ ca-make-chi @@ -1754,12 +1762,17 @@ |= j=join:c ^+ ca-core ?> |(=(p.group.j src.bowl) =(src.bowl our.bowl)) - =. chats (~(put by chats) chan.j *chat:c) + =| =chat:c + =. net.chat + ?: =(our.bowl p.chan.j) [%pub ~] + [%sub p.chan.j | %chi ~] + =. chats (~(put by chats) chan.j chat) =. ca-core (ca-abed chan.j) =. last-read.remark.chat now.bowl =. group.perm.chat group.j =. cor (give-brief flag/flag ca-brief) - ca-sub + =. cor (watch-epic p.flag &) + ca-core :: ++ ca-leave =/ =dock [p.flag dap.bowl] diff --git a/desk/app/diary.hoon b/desk/app/diary.hoon index b2c645adc9..8d2fd59816 100644 --- a/desk/app/diary.hoon +++ b/desk/app/diary.hoon @@ -880,7 +880,7 @@ di-core :: %watch-ack - =. net.diary [%sub src.bowl & [%chi ~]] + =? net.diary ?=(%sub -.net.diary) net.diary(load ?=(~ p.sign)) ?~ p.sign di-core %- (slog leaf/"Failed subscription" u.p.sign) :: =. gone & @@ -971,13 +971,17 @@ |= j=join:d ^+ di-core ?> |(=(p.group.j src.bowl) =(src.bowl our.bowl)) - =. shelf (~(put by shelf) chan.j *diary:d) + =| diary:d + =. net.diary + ?: =(our.bowl p.chan.j) [%pub ~] + [%sub p.chan.j | %chi ~] + =. shelf (~(put by shelf) chan.j diary) =. di-core (di-abed chan.j) =. group.perm.diary group.j =. last-read.remark.diary now.bowl =. cor (give-brief flag di-brief) =. cor (watch-epic p.flag &) - di-sub + di-core :: ++ di-leave =/ =dock [p.flag dap.bowl] diff --git a/desk/app/heap.hoon b/desk/app/heap.hoon index 2752e12f58..cb1ebe90d8 100644 --- a/desk/app/heap.hoon +++ b/desk/app/heap.hoon @@ -223,6 +223,11 @@ =. cor restore-missing-subs =. cor (emit %pass he-area:he-core:cor %agent [our.bowl dap.bowl] %poke %recheck-all-perms !>(0)) =. cor (emit %pass he-area:he-core:cor %agent [our.bowl dap.bowl] %poke %leave-old-channels !>(0)) + =. cor + %+ roll ~(tap in `(set @p)`(~(run in ~(key by stash)) head)) + |= [=ship cr=_cor] + ?: =(ship our.bowl) cr + (watch-epic:cr ship &) ?: =(okay:h cool) cor :: speak the good news =. cor (emil (drop load:epos)) @@ -374,10 +379,15 @@ == == ++ watch-epic - |= her=ship + |= [her=ship leave=?] ^+ cor =/ =wire /epic =/ =dock [her dap.bowl] + ?: leave + %- emil + :~ [%pass wire %agent [her dap.bowl] %leave ~] + [%pass wire %agent [her dap.bowl] %watch /epic] + == ?: (~(has by wex.bowl) [wire dock]) cor (emit %pass wire %agent [her dap.bowl] %watch /epic) @@ -387,20 +397,18 @@ ^+ cor ?+ -.sign cor %kick - (watch-epic src.bowl) + (watch-epic src.bowl |) :: %fact ?. =(%epic p.cage.sign) ~& '!!! weird fact on /epic' cor =+ !<(=epic:e q.cage.sign) - ?. =(epic okay:h) - cor - ~& >> "good news everyone!" %+ roll ~(tap by stash) |= [[=flag:g =heap:h] out=_cor] - ?> =(src.bowl p.flag) + ?. =(src.bowl p.flag) out he-abet:(he-take-epic:(he-abed:he-core:out flag) epic) + :: %watch-ack %. cor ?~ p.sign same @@ -821,7 +829,7 @@ he-core ~& make-lev/flag =. saga.net.heap lev+~ - =. cor (watch-epic p.flag) + =. cor (watch-epic p.flag |) he-core :: ++ he-make-chi @@ -841,7 +849,7 @@ he-core :: %watch-ack - =. net.heap [%sub src.bowl & [%chi ~]] + =? net.heap ?=(%sub -.net.heap) net.heap(load ?=(~ p.sign)) ?~ p.sign he-core %- (slog leaf/"Failed subscription" u.p.sign) :: =. gone & @@ -943,12 +951,17 @@ |= j=join:h ^+ he-core ?> |(=(p.group.j src.bowl) =(src.bowl our.bowl)) - =. stash (~(put by stash) chan.j *heap:h) + =| =heap:h + =. net.heap + ?: =(our.bowl p.chan.j) [%pub ~] + [%sub p.chan.j | %chi ~] + =. stash (~(put by stash) chan.j heap) =. he-core (he-abed chan.j) =. group.perm.heap group.j =. last-read.remark.heap now.bowl =. cor (give-brief flag he-brief) - he-sub + =. cor (watch-epic p.flag &) + he-core :: ++ he-leave =/ =dock [p.flag dap.bowl] diff --git a/desk/desk.docket-0 b/desk/desk.docket-0 index 4fe8fc7f5f..ae0321bade 100644 --- a/desk/desk.docket-0 +++ b/desk/desk.docket-0 @@ -2,9 +2,9 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Groups is a decentralized platform that integrates with Talk, Notebook, and Gallery for a full, communal suite of tools.' color+0xef.f0f4 image+'https://bootstrap.urbit.org/icon-groups.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0vpkb66.7906k.fhl40.sfbag.3h35r.glob' 0vpkb66.7906k.fhl40.sfbag.3h35r] + glob-http+['https://bootstrap.urbit.org/glob-0v3.nm7sd.0feba.nn9oj.4osu9.cebao.glob' 0v3.nm7sd.0feba.nn9oj.4osu9.cebao] base+'groups' - version+[4 8 1] + version+[4 9 0] website+'https://tlon.io' license+'MIT' == diff --git a/talk/desk.docket-0 b/talk/desk.docket-0 index 4ae8d399fc..20967b7672 100644 --- a/talk/desk.docket-0 +++ b/talk/desk.docket-0 @@ -2,9 +2,9 @@ info+'Send encrypted direct messages to one or many friends. Talk is a simple chat tool for catching up, getting work done, and everything in between.' color+0x10.5ec7 image+'https://bootstrap.urbit.org/icon-talk.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v7.lnq57.22fup.bdspn.9v0b5.gotem.glob' 0v7.lnq57.22fup.bdspn.9v0b5.gotem] + glob-http+['https://bootstrap.urbit.org/glob-0v2.1rv9j.2hplp.ef5ff.ndd4n.4b36s.glob' 0v2.1rv9j.2hplp.ef5ff.ndd4n.4b36s] base+'talk' - version+[4 8 1] + version+[4 9 0] website+'https://tlon.io' license+'MIT' == diff --git a/ui/E2E.md b/ui/E2E.md new file mode 100644 index 0000000000..d1b22f87fe --- /dev/null +++ b/ui/E2E.md @@ -0,0 +1,41 @@ +# UI Tests + +The ui app contains a suite of automated e2e tests that run in CI and can be run locally. + +The main test script is located in `rube/index.ts` and uses [Playwright](https://playwright.dev) to run. + +The script: + +1. fetches fake ships piers from GCS +2. downloads the urbit binaries for the local architecture +3. boots the ships +4. executes the tests against the current ui code version +5. kills spawned processes + +Test specs are in the `e2e/` directory. + +## Running locally + +After installing this project's dependencies with `npm install`, + +``` +npm run e2e +``` + +### Debugging tests + +``` +npm run e2e:debug +``` + +Currently, tests utilize two fake ships, `~zod` and `~bus`. + + +To debug only tests from one of the ships, use for example + + +``` +npm run e2e:debug:bus +``` + +The script will kill all processes on exit, but in some cases a localhost may be left running and generate an error when trying to run again. If this happens, find it with `ps aux | grep localhost` and kill from cmd line. \ No newline at end of file diff --git a/ui/e2e/001-create-group.spec.ts b/ui/e2e/001-create-group.spec.ts index 7612eb1952..d3e10fa1fa 100644 --- a/ui/e2e/001-create-group.spec.ts +++ b/ui/e2e/001-create-group.spec.ts @@ -4,7 +4,7 @@ test('Create a group', async ({ page }) => { test.skip(process.env.SHIP === '~zod', 'skip on ~zod'); test.skip(process.env.APP === 'chat', 'skip on talk'); await page.goto(''); - await page.getByText('Some Good Groups').waitFor(); + await page.getByRole('link', { name: 'Create Group' }).waitFor(); await page.getByRole('link', { name: 'Create Group' }).click(); await page.getByPlaceholder('e.g. Urbit Fan Club').click(); await page.getByPlaceholder('e.g. Urbit Fan Club').fill('Bus Club'); diff --git a/ui/e2e/005-accept-group-invite.spec.ts b/ui/e2e/005-accept-group-invite.spec.ts index 651a68aef9..6f12301705 100644 --- a/ui/e2e/005-accept-group-invite.spec.ts +++ b/ui/e2e/005-accept-group-invite.spec.ts @@ -4,8 +4,14 @@ test('accept group invite', async ({ page }) => { test.skip(process.env.SHIP === '~bus', 'skip on ~bus'); test.skip(process.env.APP === 'chat', 'skip on talk'); await page.goto(''); - await page.getByText('Pending Invites').waitFor(); - await page.getByRole('button', { name: 'Join Group' }).first().click(); + await page + .getByTestId('group-invite') + .filter({ hasText: 'Bus Club' }) + .waitFor(); + const groupInvite = page + .getByTestId('group-invite') + .filter({ hasText: 'Bus Club' }); + await groupInvite.getByRole('button', { name: 'Accept' }).first().click(); await page.getByText('Join This Group').waitFor(); await page.getByRole('button', { name: 'Join Group' }).first().click(); await page.getByText('bus chat').first().waitFor(); diff --git a/ui/rube/index.ts b/ui/rube/index.ts index a61a424344..11eeb9ff7b 100644 --- a/ui/rube/index.ts +++ b/ui/rube/index.ts @@ -29,8 +29,8 @@ const ships: Record< } > = { zod: { - url: 'https://bootstrap.urbit.org/rube-zod4.tgz', - savePath: path.join(__dirname, 'rube-zod4.tgz'), + url: 'https://bootstrap.urbit.org/rube-zod5.tgz', + savePath: path.join(__dirname, 'rube-zod5.tgz'), extractPath: path.join(__dirname, 'zod'), ship: 'zod', code: 'lidlut-tabwed-pillex-ridrup', @@ -38,8 +38,8 @@ const ships: Record< loopbackPort: '', }, bus: { - url: 'https://bootstrap.urbit.org/rube-bus4.tgz', - savePath: path.join(__dirname, 'rube-bus4.tgz'), + url: 'https://bootstrap.urbit.org/rube-bus5.tgz', + savePath: path.join(__dirname, 'rube-bus5.tgz'), extractPath: path.join(__dirname, 'bus'), ship: 'bus', code: 'riddec-bicrym-ridlev-pocsef', @@ -502,7 +502,10 @@ const shipsAreReadyForTests = async () => { return true; } - console.log(`~${ship.ship} is not ready`); + console.log(`~${ship.ship} is not ready`, { + groups: json.groups.hash, + talk: json.talk.hash, + }); return false; }) diff --git a/ui/src/app.tsx b/ui/src/app.tsx index aa6a842024..4ea573598d 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -85,7 +85,11 @@ import Dialog from '@/components/Dialog'; import useIsStandaloneMode from '@/logic/useIsStandaloneMode'; import EmojiPicker from '@/components/EmojiPicker'; import SettingsDialog from '@/components/Settings/SettingsDialog'; -import { captureAnalyticsEvent, captureError } from '@/logic/analytics'; +import { + ANALYTICS_DEFAULT_PROPERTIES, + captureAnalyticsEvent, + captureError, +} from '@/logic/analytics'; import GroupChannel from '@/groups/GroupChannel'; import PrivacyNotice from '@/groups/PrivacyNotice'; import ActivityModal, { ActivityChecker } from '@/components/ActivityModal'; @@ -804,7 +808,7 @@ function RoutedApp() { useEffect(() => { if (posthog && analyticsId !== '' && logActivity) { - posthog.identify(analyticsId); + posthog.identify(analyticsId, ANALYTICS_DEFAULT_PROPERTIES); } }, [posthog, analyticsId, logActivity]); diff --git a/ui/src/components/ActivityModal.tsx b/ui/src/components/ActivityModal.tsx index d7afd48a66..120deda189 100644 --- a/ui/src/components/ActivityModal.tsx +++ b/ui/src/components/ActivityModal.tsx @@ -10,7 +10,10 @@ import { import { useGroups } from '@/state/groups'; import React, { useEffect } from 'react'; import { isHosted } from '@/logic/utils'; -import { analyticsClient } from '@/logic/analytics'; +import { + ANALYTICS_DEFAULT_PROPERTIES, + analyticsClient, +} from '@/logic/analytics'; import { PrivacyContents } from '@/groups/PrivacyNotice'; import Dialog from './Dialog'; @@ -24,7 +27,9 @@ export function ActivityChecker() { useEffect(() => { // manage analytics opt-in/out based on settings if (analyticsClient.has_opted_out_capturing() && logActivity) { - analyticsClient.opt_in_capturing(); + analyticsClient.opt_in_capturing({ + capture_properties: ANALYTICS_DEFAULT_PROPERTIES, + }); } else if (analyticsClient.has_opted_in_capturing() && !logActivity) { analyticsClient.opt_out_capturing(); } diff --git a/ui/src/components/References/WritBaseReference.tsx b/ui/src/components/References/WritBaseReference.tsx index e7d7ba0197..f411d79685 100644 --- a/ui/src/components/References/WritBaseReference.tsx +++ b/ui/src/components/References/WritBaseReference.tsx @@ -58,6 +58,10 @@ function WritBaseReference({ } const handleOpenReferenceClick = () => { + // We have nowhere to navigate to if we haven't yet loaded group information + if (!preview?.group?.flag) { + return; + } if (!group) { navigate(`/gangs/${groupFlag}?type=chat&nest=${nest}&id=${time}`, { state: { backgroundLocation: location }, diff --git a/ui/src/components/Sidebar/Sidebar.tsx b/ui/src/components/Sidebar/Sidebar.tsx index 78785e562b..1b27d0dd9c 100644 --- a/ui/src/components/Sidebar/Sidebar.tsx +++ b/ui/src/components/Sidebar/Sidebar.tsx @@ -275,7 +275,7 @@ export default function Sidebar() { {!sortedGroups.length && !isLoading && (
- Check out Discovery above to find new groups + Check out Discover above to find new groups in your network or view group invites.
)} diff --git a/ui/src/diary/DiaryCommentField.tsx b/ui/src/diary/DiaryCommentField.tsx index fee3f8779d..2f0ea3a79a 100644 --- a/ui/src/diary/DiaryCommentField.tsx +++ b/ui/src/diary/DiaryCommentField.tsx @@ -17,6 +17,7 @@ import useGroupPrivacy from '@/logic/useGroupPrivacy'; import { captureGroupsAnalyticsEvent } from '@/logic/analytics'; import { useChannelCompatibility } from '@/logic/channel'; import Tooltip from '@/components/Tooltip'; +import { useChatInputFocus } from '@/logic/ChatInputFocusContext'; interface DiaryCommentFieldProps { flag: string; @@ -44,6 +45,7 @@ export default function DiaryCommentField({ const { mutateAsync: addQuip } = useAddQuipMutation(); const { privacy } = useGroupPrivacy(groupFlag); const { compatible, text } = useChannelCompatibility(`diary/${chFlag}`); + const { handleFocus, handleBlur, isChatInputFocused } = useChatInputFocus(); /** * This handles submission for new Curios; for edits, see EditCurioForm @@ -152,6 +154,30 @@ export default function DiaryCommentField({ quipReply, ]); + useEffect(() => { + if (messageEditor && !messageEditor.isDestroyed) { + if (!isChatInputFocused && messageEditor.isFocused) { + handleFocus(); + } + + if (isChatInputFocused && !messageEditor.isFocused) { + handleBlur(); + } + } + + return () => { + if (isChatInputFocused) { + handleBlur(); + } + }; + }, [ + isChatInputFocused, + messageEditor, + messageEditor?.isFocused, + handleFocus, + handleBlur, + ]); + const onClick = useCallback( () => messageEditor && onSubmit(messageEditor), [messageEditor, onSubmit] diff --git a/ui/src/diary/DiaryNote.tsx b/ui/src/diary/DiaryNote.tsx index d39c0eadbe..1d70ba2e64 100644 --- a/ui/src/diary/DiaryNote.tsx +++ b/ui/src/diary/DiaryNote.tsx @@ -38,6 +38,8 @@ import { useChannelCompatibility, useChannelIsJoined } from '@/logic/channel'; import { useGroupsAnalyticsEvent } from '@/logic/useAnalyticsEvent'; import { ViewProps } from '@/types/groups'; import { useConnectivityCheck } from '@/state/vitals'; +import { useIsMobile } from '@/logic/useMedia'; +import { useChatInputFocus } from '@/logic/ChatInputFocusContext'; import DiaryComment, { DiaryCommentProps } from './DiaryComment'; import DiaryCommentField from './DiaryCommentField'; import DiaryContent from './DiaryContent/DiaryContent'; @@ -117,6 +119,9 @@ export default function DiaryNote({ title }: ViewProps) { const brief = useDiaryBrief(chFlag); const sort = useDiaryCommentSortMode(chFlag); const perms = useDiaryPerms(chFlag); + const isMobile = useIsMobile(); + const { isChatInputFocused } = useChatInputFocus(); + const shouldApplyPaddingBottom = isMobile && !isChatInputFocused; const { compatible } = useChannelCompatibility(nest); const { mutateAsync: joinDiary } = useJoinDiaryMutation(); const joinChannel = useCallback(async () => { @@ -180,6 +185,9 @@ export default function DiaryNote({ title }: ViewProps) { if (!note.essay || status === 'loading') { return ( { + if (messageEditor && !messageEditor.isDestroyed) { + if (!isChatInputFocused && messageEditor.isFocused && comment) { + handleFocus(); + } + + if (isChatInputFocused && !messageEditor.isFocused && comment) { + handleBlur(); + } + } + + return () => { + if (isChatInputFocused) { + handleBlur(); + } + }; + }, [ + comment, + isChatInputFocused, + messageEditor, + messageEditor?.isFocused, + handleFocus, + handleBlur, + ]); + const onClick = useCallback( () => messageEditor && onSubmit(messageEditor), [messageEditor, onSubmit] @@ -209,67 +235,61 @@ export default function HeapTextInput({ // TODO: Set a sane length limit for comments return ( - <> +
messageEditor.commands.focus()} + > + {chatInfo.blocks.length > 0 ? ( +
+ Attached: + {chatInfo.blocks.length} reference + {chatInfo.blocks.length === 1 ? '' : 's'} + +
+ ) : null}
messageEditor.commands.focus()} + className={cn( + 'w-full', + comment ? 'flex flex-row items-end' : 'relative flex h-full' + )} > - {chatInfo.blocks.length > 0 ? ( -
- Attached: - {chatInfo.blocks.length} reference - {chatInfo.blocks.length === 1 ? '' : 's'} + tag, only style + // the fake placeholder when the field is empty + messageEditor.getText() === '' ? 'text-gray-400' : '' + )} + /> + {!sendDisabled ? ( + -
+ ) : null} -
- tag, only style - // the fake placeholder when the field is empty - messageEditor.getText() === '' ? 'text-gray-400' : '' - )} - /> - {!sendDisabled ? ( - - - - ) : null} -
- {isMobile && messageEditor.isFocused ? ( - - ) : null} - +
); } diff --git a/ui/src/logic/DragAndDropContext.tsx b/ui/src/logic/DragAndDropContext.tsx index 36e65b5981..3539de5474 100644 --- a/ui/src/logic/DragAndDropContext.tsx +++ b/ui/src/logic/DragAndDropContext.tsx @@ -162,9 +162,9 @@ export function useDragAndDrop(targetId: string) { const handleDropWithTarget = useCallback( (e: DragEvent) => { - handleDrop(e, targetId); + handleDrop(e, currentTargetId); }, - [handleDrop, targetId] + [handleDrop, currentTargetId] ); useEffect(() => { diff --git a/ui/src/logic/analytics.ts b/ui/src/logic/analytics.ts index 6821c36f1d..b8c2566708 100644 --- a/ui/src/logic/analytics.ts +++ b/ui/src/logic/analytics.ts @@ -50,6 +50,30 @@ posthog.init(import.meta.env.VITE_POSTHOG_KEY, { export const analyticsClient = posthog; +export const ANALYTICS_DEFAULT_PROPERTIES: Properties = { + // The following default properties stop PostHog from auto-logging the URL, + // which can inadvertently reveal private info on Urbit + $current_url: null, + $pathname: null, + $set_once: null, + $host: null, + $referrer: null, + $initial_current_url: null, + $initial_referrer_url: null, + $referring_domain: null, + $initial_referring_domain: null, + $unset: [ + 'initial_referrer_url', + 'initial_referring_domain', + 'initial_current_url', + 'current_url', + 'pathname', + 'host', + 'referrer', + 'referring_domain', + ], +}; + // Once someone is opted in this will fire no matter what so we need // additional guarding here to prevent accidentally capturing data. export const captureAnalyticsEvent = ( @@ -64,27 +88,7 @@ export const captureAnalyticsEvent = ( log('Attempting to capture analytics event', name); const captureProperties: Properties = { ...(properties || {}), - // The following default properties stop PostHog from auto-logging the URL, - // which can inadvertently reveal private info on Urbit - $current_url: null, - $pathname: null, - $set_once: null, - $host: null, - $referrer: null, - $initial_current_url: null, - $initial_referrer_url: null, - $referring_domain: null, - $initial_referring_domain: null, - $unset: [ - 'initial_referrer_url', - 'initial_referring_domain', - 'initial_current_url', - 'current_url', - 'pathname', - 'host', - 'referrer', - 'referring_domain', - ], + ...ANALYTICS_DEFAULT_PROPERTIES, }; posthog.capture(name, captureProperties, { diff --git a/ui/src/logic/useReactQuerySubscription.tsx b/ui/src/logic/useReactQuerySubscription.tsx index 0b75c21362..be0a2dd3a0 100644 --- a/ui/src/logic/useReactQuerySubscription.tsx +++ b/ui/src/logic/useReactQuerySubscription.tsx @@ -16,6 +16,7 @@ export default function useReactQuerySubscription({ scry, scryApp = app, priority = 3, + onEvent, options, }: { queryKey: QueryKey; @@ -24,6 +25,7 @@ export default function useReactQuerySubscription({ scry: string; scryApp?: string; priority?: number; + onEvent?: (data: Event) => void; options?: UseQueryOptions; }) { const queryClient = useQueryClient(); @@ -50,9 +52,9 @@ export default function useReactQuerySubscription({ api.subscribe({ app, path, - event: invalidate.current, + event: onEvent ? onEvent : invalidate.current, }); - }, [app, path, queryClient, queryKey]); + }, [app, path, queryClient, queryKey, onEvent]); return useQuery(queryKey, fetchData, { staleTime: 60 * 1000, diff --git a/ui/src/notifications/Notification.tsx b/ui/src/notifications/Notification.tsx index df60c871ff..44972f6ec4 100644 --- a/ui/src/notifications/Notification.tsx +++ b/ui/src/notifications/Notification.tsx @@ -258,7 +258,10 @@ export default function Notification({ {inviteBool ? (
{avatar}
-
+
{topLine}
{bin.top && ( diff --git a/ui/src/state/chat/chat.ts b/ui/src/state/chat/chat.ts index 06d4ff5e7b..ece6d6f74b 100644 --- a/ui/src/state/chat/chat.ts +++ b/ui/src/state/chat/chat.ts @@ -743,7 +743,13 @@ export const useChatState = createState( }); get().batchSet((draft) => { - const chat = { perms, saga: null }; + let chat = draft.chats[whom]; + if (chat) { + chat.perms = perms; + } else { + chat = { perms, saga: null }; + } + draft.chats[whom] = chat; }); }, 1); diff --git a/ui/src/state/groups/groups.ts b/ui/src/state/groups/groups.ts index 958eaa23b2..37ecb8c7db 100644 --- a/ui/src/state/groups/groups.ts +++ b/ui/src/state/groups/groups.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { useParams } from 'react-router'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; import create from 'zustand'; import { MutationFunction, @@ -248,18 +248,41 @@ const defGang = { export function useGangs() { const queryClient = useQueryClient(); + const queryKey = ['gangs']; + + const invalidate = useRef( + _.debounce( + () => { + queryClient.invalidateQueries(queryKey); + }, + 300, + { leading: true, trailing: true } + ) + ); + const { data, ...rest } = useReactQuerySubscription({ - queryKey: ['gangs'], + queryKey, app: 'groups', path: `/gangs/updates`, scry: `/gangs`, + onEvent: (event) => { + // right now for long group joins, the gang is initially removed but no fact about the corresponding created + // group is emitted until the join completes. This is a blunt hack to ensure our view of existing groups remains up to date. + // Once this is fixed, we should remove this and use the default useReactQuerySubscription event handler + const currGangCount = Object.keys( + queryClient.getQueryData(queryKey) || {} + ).length; + const newGangCount = Object.keys(event || {}).length; + const gangWasRemoved = + currGangCount && newGangCount && newGangCount < currGangCount; + if (gangWasRemoved) { + queryClient.invalidateQueries(['groups']); + } + + invalidate.current(); + }, options: { refetchOnMount: false, - onSuccess: () => { - // TEMPORARY: right now for long group joins, the gang is initially removed but no fact about the corresponding created - // group is emitted until the join completes. This is a blunt hack to ensure our view of existing groups remains up to date - queryClient.invalidateQueries(['groups']); - }, }, }); diff --git a/ui/src/state/storage/upload.ts b/ui/src/state/storage/upload.ts index ec31bd79e7..261bec2080 100644 --- a/ui/src/state/storage/upload.ts +++ b/ui/src/state/storage/upload.ts @@ -22,10 +22,12 @@ function prefixEndpoint(endpoint: string) { } function imageSize(url: string) { - const size = getImageSize(url).then<[number, number]>(({ width, height }) => [ - width, - height, - ]); + const size = getImageSize(url) + .then<[number, number]>(({ width, height }) => [width, height]) + .catch((e) => { + console.log('failed to get image size', { e }); + return undefined; + }); return size; } @@ -169,12 +171,17 @@ export const useFileStore = create((set, get) => ({ return ''; }); updateStatus(uploader, key, 'success'); - imageSize(fileUrl).then((s) => - updateFile(uploader, key, { - size: s, - url: fileUrl, - }) - ); + imageSize(fileUrl) + .then((s) => + updateFile(uploader, key, { + size: s, + url: fileUrl, + }) + ) + .catch((e) => { + console.log('failed to get image size', { e }); + return ''; + }); }) .catch((error: any) => { updateStatus( @@ -217,12 +224,17 @@ export const useFileStore = create((set, get) => ({ .then(() => { const fileUrl = url.split('?')[0]; updateStatus(uploader, key, 'success'); - imageSize(fileUrl).then((s) => - updateFile(uploader, key, { - size: s, - url: fileUrl, - }) - ); + imageSize(fileUrl) + .then((s) => + updateFile(uploader, key, { + size: s, + url: fileUrl, + }) + ) + .catch((e) => { + console.log('failed to get image size', { e }); + return ''; + }); }) .catch((error: any) => { updateStatus(