first run: Prompt new users to open groups

For #165

This adds a purple bullet to the groups tile for users who have not yet opened groups. The popover will either show some text about groups or let them know that they've been invited to a group (in the case of onboarding via lure).
This commit is contained in:
Patrick O'Sullivan 2023-05-31 06:27:11 -05:00
parent eb5ff83e0e
commit f9105ff7b9
7 changed files with 279 additions and 44 deletions

188
ui/package-lock.json generated
View File

@ -18,8 +18,9 @@
"@radix-ui/react-portal": "^0.0.15",
"@radix-ui/react-radio-group": "^0.1.5",
"@radix-ui/react-toggle": "^0.0.10",
"@tanstack/react-query": "^4.29.5",
"@tanstack/react-query-persist-client": "^4.29.5",
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-query-devtools": "^4.29.12",
"@tanstack/react-query-persist-client": "^4.29.12",
"@tlon/sigil-js": "^1.4.4",
"@tloncorp/mock-http-api": "^1.2.0",
"@types/lodash": "^4.14.172",
@ -2033,21 +2034,36 @@
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
},
"node_modules/@tanstack/match-sorter-utils": {
"version": "8.8.4",
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz",
"integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==",
"dependencies": {
"remove-accents": "0.4.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kentcdodds"
}
},
"node_modules/@tanstack/query-core": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.7.tgz",
"integrity": "sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA==",
"version": "4.29.11",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.11.tgz",
"integrity": "sha512-8C+hF6SFAb/TlFZyS9FItgNwrw4PMa7YeX+KQYe2ZAiEz6uzg6yIr+QBzPkUwZ/L0bXvGd1sufTm3wotoz+GwQ==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-persist-client-core": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.29.7.tgz",
"integrity": "sha512-/QahvSq9/f8hetCsCd9MaOy6fAoPn0YDGDcl6TTobqdr9kHMgrM9laP9yKJFg2hm5/jIsrCMDO/iCnxBiUhrqw==",
"version": "4.29.11",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.29.11.tgz",
"integrity": "sha512-CSmMZchr+446r79NJ/pjD2yfjqNqFV7k8BnqOq4yTZvXsaQLEIn3tsaU45IsPgs4N7g9OBfPUPDdapSQvck2WQ==",
"dependencies": {
"@tanstack/query-core": "4.29.7"
"@tanstack/query-core": "4.29.11"
},
"funding": {
"type": "github",
@ -2055,11 +2071,11 @@
}
},
"node_modules/@tanstack/react-query": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.7.tgz",
"integrity": "sha512-ijBWEzAIo09fB1yd22slRZzprrZ5zMdWYzBnCg5qiXuFbH78uGN1qtGz8+Ed4MuhaPaYSD+hykn+QEKtQviEtg==",
"version": "4.29.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.12.tgz",
"integrity": "sha512-zhcN6+zF6cxprxhTHQajHGlvxgK8npnp9uLe9yaWhGc6sYcPWXzyO4raL4HomUzQOPzu3jLvkriJQ7BOrDM8vA==",
"dependencies": {
"@tanstack/query-core": "4.29.7",
"@tanstack/query-core": "4.29.11",
"use-sync-external-store": "^1.2.0"
},
"funding": {
@ -2080,19 +2096,38 @@
}
}
},
"node_modules/@tanstack/react-query-persist-client": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.29.7.tgz",
"integrity": "sha512-KYUeESnthjjcfakpAei9Cz5gsIm1uDAVHrKcIAoARQwksk4j0KAo9ieExoIhL9v4mpTOlE9GsuZ/y06ANmaVaQ==",
"node_modules/@tanstack/react-query-devtools": {
"version": "4.29.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.29.12.tgz",
"integrity": "sha512-ug4YGQhMhh6QI8/sWJhjXxuvdeehxf1cyxpTifGMH5qreQ5ECHT6vzqG/aKvADQDzqLBGrF0q4wTDnRRYvvtrA==",
"dependencies": {
"@tanstack/query-persist-client-core": "4.29.7"
"@tanstack/match-sorter-utils": "^8.7.0",
"superjson": "^1.10.0",
"use-sync-external-store": "^1.2.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "4.29.7"
"@tanstack/react-query": "4.29.12",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/react-query-persist-client": {
"version": "4.29.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.29.12.tgz",
"integrity": "sha512-rh6zZJB+3j8lr+YsEkVadnqmUELmqNFZQzGGsHS5col/YOjYsMe9ppqaUjIMJ2aXnFXye50sbe4KxHhSGoaNVw==",
"dependencies": {
"@tanstack/query-persist-client-core": "4.29.11"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "4.29.12"
}
},
"node_modules/@tlon/sigil-js": {
@ -3245,6 +3280,20 @@
"safe-buffer": "~5.1.1"
}
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/core-js": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz",
@ -5779,6 +5828,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-what": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.12.tgz",
"integrity": "sha512-w7JwFt3gIzbzpp+I//2Ov2UeTGgKM10PxJRD6yf7yGruT3415CdT8TDR6F9/mBAakZKsIaysQVVvxryctQUbnw==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/is-wsl": {
"version": "2.2.0",
"dev": true,
@ -7488,6 +7548,11 @@
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/remove-accents": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
"integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
},
"node_modules/rename-keys": {
"version": "1.2.0",
"license": "MIT",
@ -8019,6 +8084,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/superjson": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz",
"integrity": "sha512-0j+U70KUtP8+roVPbwfqkyQI7lBt7ETnuA7KXbTDX3mCKiD/4fXs2ldKSMdt0MCfpTwiMxo20yFU3vu6ewETpQ==",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"dev": true,
@ -10303,34 +10379,52 @@
"dev": true,
"requires": {}
},
"@tanstack/match-sorter-utils": {
"version": "8.8.4",
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.8.4.tgz",
"integrity": "sha512-rKH8LjZiszWEvmi01NR72QWZ8m4xmXre0OOwlRGnjU01Eqz/QnN+cqpty2PJ0efHblq09+KilvyR7lsbzmXVEw==",
"requires": {
"remove-accents": "0.4.2"
}
},
"@tanstack/query-core": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.7.tgz",
"integrity": "sha512-GXG4b5hV2Loir+h2G+RXhJdoZhJLnrBWsuLB2r0qBRyhWuXq9w/dWxzvpP89H0UARlH6Mr9DiVj4SMtpkF/aUA=="
"version": "4.29.11",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.11.tgz",
"integrity": "sha512-8C+hF6SFAb/TlFZyS9FItgNwrw4PMa7YeX+KQYe2ZAiEz6uzg6yIr+QBzPkUwZ/L0bXvGd1sufTm3wotoz+GwQ=="
},
"@tanstack/query-persist-client-core": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.29.7.tgz",
"integrity": "sha512-/QahvSq9/f8hetCsCd9MaOy6fAoPn0YDGDcl6TTobqdr9kHMgrM9laP9yKJFg2hm5/jIsrCMDO/iCnxBiUhrqw==",
"version": "4.29.11",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.29.11.tgz",
"integrity": "sha512-CSmMZchr+446r79NJ/pjD2yfjqNqFV7k8BnqOq4yTZvXsaQLEIn3tsaU45IsPgs4N7g9OBfPUPDdapSQvck2WQ==",
"requires": {
"@tanstack/query-core": "4.29.7"
"@tanstack/query-core": "4.29.11"
}
},
"@tanstack/react-query": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.7.tgz",
"integrity": "sha512-ijBWEzAIo09fB1yd22slRZzprrZ5zMdWYzBnCg5qiXuFbH78uGN1qtGz8+Ed4MuhaPaYSD+hykn+QEKtQviEtg==",
"version": "4.29.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.12.tgz",
"integrity": "sha512-zhcN6+zF6cxprxhTHQajHGlvxgK8npnp9uLe9yaWhGc6sYcPWXzyO4raL4HomUzQOPzu3jLvkriJQ7BOrDM8vA==",
"requires": {
"@tanstack/query-core": "4.29.7",
"@tanstack/query-core": "4.29.11",
"use-sync-external-store": "^1.2.0"
}
},
"@tanstack/react-query-devtools": {
"version": "4.29.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.29.12.tgz",
"integrity": "sha512-ug4YGQhMhh6QI8/sWJhjXxuvdeehxf1cyxpTifGMH5qreQ5ECHT6vzqG/aKvADQDzqLBGrF0q4wTDnRRYvvtrA==",
"requires": {
"@tanstack/match-sorter-utils": "^8.7.0",
"superjson": "^1.10.0",
"use-sync-external-store": "^1.2.0"
}
},
"@tanstack/react-query-persist-client": {
"version": "4.29.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.29.7.tgz",
"integrity": "sha512-KYUeESnthjjcfakpAei9Cz5gsIm1uDAVHrKcIAoARQwksk4j0KAo9ieExoIhL9v4mpTOlE9GsuZ/y06ANmaVaQ==",
"version": "4.29.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.29.12.tgz",
"integrity": "sha512-rh6zZJB+3j8lr+YsEkVadnqmUELmqNFZQzGGsHS5col/YOjYsMe9ppqaUjIMJ2aXnFXye50sbe4KxHhSGoaNVw==",
"requires": {
"@tanstack/query-persist-client-core": "4.29.7"
"@tanstack/query-persist-client-core": "4.29.11"
}
},
"@tlon/sigil-js": {
@ -11055,6 +11149,14 @@
"safe-buffer": "~5.1.1"
}
},
"copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"requires": {
"is-what": "^4.1.8"
}
},
"core-js": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz",
@ -12692,6 +12794,11 @@
"version": "0.1.0",
"dev": true
},
"is-what": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.12.tgz",
"integrity": "sha512-w7JwFt3gIzbzpp+I//2Ov2UeTGgKM10PxJRD6yf7yGruT3415CdT8TDR6F9/mBAakZKsIaysQVVvxryctQUbnw=="
},
"is-wsl": {
"version": "2.2.0",
"dev": true,
@ -13740,6 +13847,11 @@
"version": "3.2.0",
"dev": true
},
"remove-accents": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
"integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
},
"rename-keys": {
"version": "1.2.0"
},
@ -14069,6 +14181,14 @@
"version": "3.1.1",
"dev": true
},
"superjson": {
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-1.12.3.tgz",
"integrity": "sha512-0j+U70KUtP8+roVPbwfqkyQI7lBt7ETnuA7KXbTDX3mCKiD/4fXs2ldKSMdt0MCfpTwiMxo20yFU3vu6ewETpQ==",
"requires": {
"copy-anything": "^3.0.2"
}
},
"supports-color": {
"version": "5.5.0",
"dev": true,

View File

@ -28,8 +28,9 @@
"@radix-ui/react-portal": "^0.0.15",
"@radix-ui/react-radio-group": "^0.1.5",
"@radix-ui/react-toggle": "^0.0.10",
"@tanstack/react-query": "^4.29.5",
"@tanstack/react-query-persist-client": "^4.29.5",
"@tanstack/react-query": "^4.29.12",
"@tanstack/react-query-devtools": "^4.29.12",
"@tanstack/react-query-persist-client": "^4.29.12",
"@tlon/sigil-js": "^1.4.4",
"@tloncorp/mock-http-api": "^1.2.0",
"@types/lodash": "^4.14.172",

View File

@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import Mousetrap from 'mousetrap';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import {
BrowserRouter,
Switch,
@ -132,6 +133,7 @@ export function App() {
onReset={() => window.location.reload()}
>
<BrowserRouter basename={base}>
<ReactQueryDevtools initialIsOpen={false} />
<AppRoutes />
<Scheduler />
</BrowserRouter>

View File

@ -95,3 +95,17 @@ export function useSawSeamMutation() {
},
});
}
export function useHasInviteToGroup(): Skein | undefined {
const skeins = useSkeins();
if (!skeins.data) {
return undefined;
}
return skeins.data.find(
(skein) =>
skein.top.rope.desk === 'groups' &&
skein.top.con.some((con) => con === ' sent you an invite to ') &&
skein.unread
);
}

View File

@ -49,6 +49,12 @@ export interface SettingsState {
};
}
interface GroupsSettingsState extends SettingsState {
groups: {
hasBeenUsed: boolean;
};
}
export const useSettings = () => {
const { data, ...rest } = useReactQuerySubscription<
{ desk: SettingsState },
@ -72,6 +78,29 @@ export const useSettings = () => {
}, [rest, data]);
};
export const useGroupsSettings = () => {
const { data, ...rest } = useReactQuerySubscription<
{ desk: GroupsSettingsState },
SettingsEvent
>({
scry: `/desk/groups`,
scryApp: 'settings',
app: 'settings',
path: `/desk/groups`,
queryKey: ['settings', 'groups'],
});
return useMemo(() => {
if (!data) {
return { data: undefined, ...rest };
}
const { desk } = data;
return { data: desk, ...rest };
}, [rest, data]);
};
export function useDisplay(): SettingsState['display'] {
const { data, isLoading } = useSettings();
@ -291,3 +320,16 @@ export function useBrowserNotifications(browserId: string): boolean {
const browserSetting = getBrowserSetting(settings, browserId);
return browserSetting?.browserNotifications ?? false;
}
export function useGroupsHasBeenUsed() {
const { data, isLoading } = useGroupsSettings();
return useMemo(() => {
if (isLoading || data === undefined) {
return false;
}
const setting = data.groups.hasBeenUsed;
return setting;
}, [isLoading, data]);
}

View File

@ -1,6 +1,7 @@
import classNames from 'classnames';
import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { useDrag } from 'react-dnd';
import * as Popover from '@radix-ui/react-popover';
import { chadIsRunning } from '@urbit/api';
import { TileMenu } from './TileMenu';
import { Spinner } from '../components/Spinner';
@ -11,6 +12,8 @@ import { useTileColor } from './useTileColor';
import { usePike } from '../state/kiln';
import { Bullet } from '../components/icons/Bullet';
import { dragTypes } from './TileGrid';
import { useGroupsHasBeenUsed } from '@/state/settings';
import { useHasInviteToGroup } from '@/state/hark';
type TileProps = {
charge: ChargeWithDesk;
@ -23,6 +26,15 @@ export const Tile: FunctionComponent<TileProps> = ({
desk,
disabled = false,
}) => {
const [showGroupsPopover, setShowGroupsPopover] = useState(false);
const hasOpenedGroups = useGroupsHasBeenUsed();
const invite = useHasInviteToGroup();
const inviteGroupName =
invite &&
typeof invite.top.con[2] === 'object' &&
'emph' in invite.top.con[2]
? invite.top.con[2].emph
: 'a group';
const addRecentApp = useRecentsStore((state) => state.addRecentApp);
const { title, image, color, chad, href } = charge;
const pike = usePike(desk);
@ -66,6 +78,45 @@ export const Tile: FunctionComponent<TileProps> = ({
onAuxClick={() => addRecentApp(desk)}
>
<div>
{desk === 'groups' && !hasOpenedGroups && (
<Popover.Root
open={showGroupsPopover}
onOpenChange={(o) => setShowGroupsPopover(o)}
>
<div
className="absolute top-4 right-4 z-10 sm:top-6 sm:right-6"
onMouseOver={() => setShowGroupsPopover(true)}
onMouseOut={() => setShowGroupsPopover(false)}
>
<Popover.Trigger>
<>
<div className="absolute h-[42px] w-[42px] animate-pulse rounded-full bg-indigo opacity-10 sm:top-0 sm:right-0" />
<Bullet className="h-[42px] w-[42px] text-indigo" />
</>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
side="right"
sideOffset={16}
className="z-40 w-[216px] rounded-lg bg-indigo p-4"
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<p className="text-white">
{invite ? (
<>You have an invitation to join {inviteGroupName}.</>
) : (
<>
Open Groups to create, join, and accept invitations to
communities.
</>
)}
</p>
</Popover.Content>
</Popover.Portal>
</div>
</Popover.Root>
)}
<div className="absolute top-4 left-4 z-10 flex items-center sm:top-6 sm:left-6">
{pike?.zest === 'held' && !disabled && (
<Bullet className="h-4 w-4 text-orange-500 dark:text-black" />
@ -85,6 +136,7 @@ export const Tile: FunctionComponent<TileProps> = ({
</>
)}
</div>
{desk === 'groups' && !hasOpenedGroups ? null : (
<TileMenu
desk={desk}
chad={chad}
@ -92,6 +144,7 @@ export const Tile: FunctionComponent<TileProps> = ({
lightText={lightText}
className="absolute top-3 right-3 z-10 opacity-0 focus:opacity-100 group-hover:opacity-100 pointer-coarse:opacity-100 hover-none:opacity-100 sm:top-5 sm:right-5"
/>
)}
{title && (
<div
className="h4 absolute bottom-[8%] left-[5%] z-10 rounded-lg py-1 px-3 sm:bottom-7 sm:left-5"

View File

@ -102,6 +102,9 @@ const base = new Theme().addColors({
800: '#641E00',
900: '#3E1100',
},
indigo: {
DEFAULT: '#615FD3',
},
green: {
100: '#E6F5F0',
200: '#B3E2D1',