@dxos/shell 0.4.7-main.7b31910 → 0.4.7-main.83fe62c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/bundle/shell.mjs +69914 -27222
  2. package/dist/bundle/shell.mjs.map +1 -1
  3. package/dist/bundle/shell.umd.js +796 -80
  4. package/dist/bundle/shell.umd.js.map +1 -1
  5. package/dist/bundle/style.css +1 -1
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/react.mjs +881 -669
  8. package/dist/lib/browser/react.mjs.map +4 -4
  9. package/dist/types/src/components/EmojiPicker.d.ts +11 -0
  10. package/dist/types/src/components/EmojiPicker.d.ts.map +1 -0
  11. package/dist/types/src/components/EmojiPicker.js +79 -0
  12. package/dist/types/src/components/EmojiPicker.js.map +1 -0
  13. package/dist/types/src/components/HuePicker.d.ts +11 -0
  14. package/dist/types/src/components/HuePicker.d.ts.map +1 -0
  15. package/dist/types/src/components/HuePicker.js +56 -0
  16. package/dist/types/src/components/HuePicker.js.map +1 -0
  17. package/dist/types/src/components/IdentityList/IdentityListItem.d.ts.map +1 -1
  18. package/dist/types/src/components/IdentityList/IdentityListItem.js +5 -5
  19. package/dist/types/src/components/IdentityList/IdentityListItem.js.map +1 -1
  20. package/dist/types/src/components/InvitationList/InvitationListItem.d.ts.map +1 -1
  21. package/dist/types/src/components/InvitationList/InvitationListItem.js +17 -6
  22. package/dist/types/src/components/InvitationList/InvitationListItem.js.map +1 -1
  23. package/dist/types/src/components/SpaceListItem.d.ts.map +1 -1
  24. package/dist/types/src/components/SpaceListItem.js +2 -3
  25. package/dist/types/src/components/SpaceListItem.js.map +1 -1
  26. package/dist/types/src/components/index.d.ts +2 -0
  27. package/dist/types/src/components/index.d.ts.map +1 -1
  28. package/dist/types/src/components/index.js +2 -0
  29. package/dist/types/src/components/index.js.map +1 -1
  30. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
  31. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js +7 -6
  32. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js.map +1 -1
  33. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts.map +1 -1
  34. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js +3 -1
  35. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js.map +1 -1
  36. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.d.ts.map +1 -1
  37. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.js +12 -3
  38. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.js.map +1 -1
  39. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.d.ts.map +1 -1
  40. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js +2 -1
  41. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js.map +1 -1
  42. package/dist/types/src/panels/JoinPanel/steps/IdentityAdded.d.ts.map +1 -1
  43. package/dist/types/src/panels/JoinPanel/steps/IdentityAdded.js +8 -7
  44. package/dist/types/src/panels/JoinPanel/steps/IdentityAdded.js.map +1 -1
  45. package/dist/types/src/panels/JoinPanel/steps/InvitationAuthenticator.js +2 -2
  46. package/dist/types/src/panels/JoinPanel/steps/InvitationAuthenticator.js.map +1 -1
  47. package/dist/types/src/panels/Panels.stories.d.ts.map +1 -1
  48. package/dist/types/src/panels/Panels.stories.js +1 -3
  49. package/dist/types/src/panels/Panels.stories.js.map +1 -1
  50. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts.map +1 -1
  51. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js +18 -1
  52. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js.map +1 -1
  53. package/dist/types/src/steps/InvitationManager.d.ts.map +1 -1
  54. package/dist/types/src/steps/InvitationManager.js +5 -4
  55. package/dist/types/src/steps/InvitationManager.js.map +1 -1
  56. package/dist/types/src/stories/Invitations.e2e-stories.d.ts.map +1 -1
  57. package/dist/types/src/stories/Invitations.e2e-stories.js +3 -2
  58. package/dist/types/src/stories/Invitations.e2e-stories.js.map +1 -1
  59. package/dist/types/src/translations/locales/en-US.d.ts +21 -0
  60. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  61. package/dist/types/src/translations/locales/en-US.js +21 -0
  62. package/dist/types/src/translations/locales/en-US.js.map +1 -1
  63. package/dist/types/src/util/index.d.ts +0 -1
  64. package/dist/types/src/util/index.d.ts.map +1 -1
  65. package/dist/types/src/util/index.js +0 -1
  66. package/dist/types/src/util/index.js.map +1 -1
  67. package/package.json +24 -22
  68. package/src/components/EmojiPicker.tsx +82 -0
  69. package/src/components/HuePicker.tsx +87 -0
  70. package/src/components/IdentityList/IdentityListItem.tsx +9 -5
  71. package/src/components/InvitationList/InvitationListItem.tsx +27 -13
  72. package/src/components/SpaceListItem.tsx +4 -5
  73. package/src/components/index.ts +2 -0
  74. package/src/panels/IdentityPanel/IdentityPanel.tsx +6 -5
  75. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +3 -1
  76. package/src/panels/IdentityPanel/steps/ProfileForm.tsx +29 -7
  77. package/src/panels/IdentityPanel/useAgentHandlers.ts +2 -1
  78. package/src/panels/JoinPanel/steps/IdentityAdded.tsx +7 -7
  79. package/src/panels/JoinPanel/steps/InvitationAuthenticator.tsx +2 -2
  80. package/src/panels/Panels.stories.tsx +1 -3
  81. package/src/panels/SpacePanel/steps/SpaceManager.tsx +18 -1
  82. package/src/steps/InvitationManager.tsx +4 -3
  83. package/src/stories/Invitations.e2e-stories.tsx +3 -2
  84. package/src/translations/locales/en-US.ts +21 -0
  85. package/src/util/index.ts +0 -1
  86. package/dist/types/src/util/toEmoji.d.ts +0 -2
  87. package/dist/types/src/util/toEmoji.d.ts.map +0 -1
  88. package/dist/types/src/util/toEmoji.js +0 -26
  89. package/dist/types/src/util/toEmoji.js.map +0 -1
  90. package/src/util/toEmoji.ts +0 -22
@@ -0,0 +1,82 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+ import emojiData from '@emoji-mart/data';
5
+ import EmojiMart from '@emoji-mart/react';
6
+ import { ArrowCounterClockwise, CaretDown } from '@phosphor-icons/react';
7
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
8
+ import React, { type Dispatch, type SetStateAction, useState } from 'react';
9
+
10
+ import { Button, Popover, useTranslation, Tooltip, type ButtonProps } from '@dxos/react-ui';
11
+ import { getSize } from '@dxos/react-ui-theme';
12
+
13
+ export type EmojiPickerProps = {
14
+ disabled?: boolean;
15
+ defaultEmoji?: string;
16
+ emoji?: string;
17
+ onChangeEmoji?: Dispatch<SetStateAction<string>>;
18
+ onClickClear?: ButtonProps['onClick'];
19
+ };
20
+
21
+ export const EmojiPicker = ({ disabled, defaultEmoji, emoji, onChangeEmoji, onClickClear }: EmojiPickerProps) => {
22
+ const { t } = useTranslation('os');
23
+
24
+ const [emojiValue, setEmojiValue] = useControllableState<string>({
25
+ prop: emoji,
26
+ onChange: onChangeEmoji,
27
+ defaultProp: defaultEmoji,
28
+ });
29
+
30
+ const [emojiPickerOpen, setEmojiPickerOpen] = useState<boolean>(false);
31
+
32
+ return (
33
+ <>
34
+ <Popover.Root open={emojiPickerOpen} onOpenChange={setEmojiPickerOpen}>
35
+ <Popover.Trigger asChild>
36
+ <Button variant='ghost' classNames='gap-2 text-2xl plb-1' disabled={disabled}>
37
+ <span className='sr-only'>{t('select emoji label')}</span>
38
+ <span className='grow pis-14'>{emojiValue}</span>
39
+ <CaretDown className={getSize(4)} />
40
+ </Button>
41
+ </Popover.Trigger>
42
+ <Popover.Content
43
+ side='right'
44
+ onKeyDownCapture={(event) => {
45
+ if (event.key === 'Escape') {
46
+ event.stopPropagation();
47
+ setEmojiPickerOpen(false);
48
+ }
49
+ }}
50
+ >
51
+ <EmojiMart
52
+ data={emojiData}
53
+ onEmojiSelect={({ native }: { native?: string }) => {
54
+ if (native) {
55
+ setEmojiValue(native);
56
+ setEmojiPickerOpen(false);
57
+ }
58
+ }}
59
+ autoFocus={true}
60
+ maxFrequentRows={0}
61
+ noCountryFlags={true}
62
+ />
63
+ <Popover.Arrow />
64
+ </Popover.Content>
65
+ </Popover.Root>
66
+ <Tooltip.Root>
67
+ <Tooltip.Trigger asChild>
68
+ <Button variant='ghost' onClick={onClickClear} disabled={disabled}>
69
+ <span className='sr-only'>{t('clear label')}</span>
70
+ <ArrowCounterClockwise />
71
+ </Button>
72
+ </Tooltip.Trigger>
73
+ <Tooltip.Portal>
74
+ <Tooltip.Content style={{ zIndex: 70 }} side='right'>
75
+ {t('clear label')}
76
+ <Tooltip.Arrow />
77
+ </Tooltip.Content>
78
+ </Tooltip.Portal>
79
+ </Tooltip.Root>
80
+ </>
81
+ );
82
+ };
@@ -0,0 +1,87 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+ import { ArrowCounterClockwise, CaretDown, Check } from '@phosphor-icons/react';
5
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
6
+ import React, { type Dispatch, type SetStateAction } from 'react';
7
+
8
+ import { Button, useTranslation, Tooltip, DropdownMenu, useThemeContext, type ButtonProps } from '@dxos/react-ui';
9
+ import { getSize, hueTokenThemes } from '@dxos/react-ui-theme';
10
+
11
+ const HuePreview = ({ hue }: { hue: string }) => {
12
+ const { tx } = useThemeContext();
13
+ return (
14
+ <svg width={12} height={12} viewBox='0 0 12 12'>
15
+ <rect x={0} y={0} width={12} height={12} className={tx('hue.fill', 'select--hue__preview', { hue })} />
16
+ </svg>
17
+ );
18
+ };
19
+
20
+ export type HuePickerProps = {
21
+ disabled?: boolean;
22
+ defaultHue?: string;
23
+ hue?: string;
24
+ onChangeHue?: Dispatch<SetStateAction<string>>;
25
+ onClickClear?: ButtonProps['onClick'];
26
+ };
27
+
28
+ export const HuePicker = ({ disabled, hue, onChangeHue, defaultHue, onClickClear }: HuePickerProps) => {
29
+ const { t } = useTranslation('os');
30
+
31
+ const [hueValue, setHueValue] = useControllableState<string>({
32
+ prop: hue,
33
+ onChange: onChangeHue,
34
+ defaultProp: defaultHue,
35
+ });
36
+
37
+ return (
38
+ <>
39
+ <DropdownMenu.Root modal={false}>
40
+ <DropdownMenu.Trigger asChild>
41
+ <Button variant='ghost' classNames='gap-2 plb-1' disabled={disabled}>
42
+ <span className='sr-only'>{t('select hue label')}</span>
43
+ <div role='none' className='pis-14 grow flex items-center justify-center gap-2'>
44
+ <HuePreview hue={hueValue!} />
45
+ <span>{t(`${hueValue} label`)}</span>
46
+ </div>
47
+ <CaretDown className={getSize(4)} />
48
+ </Button>
49
+ </DropdownMenu.Trigger>
50
+ <DropdownMenu.Content side='right'>
51
+ <DropdownMenu.Viewport>
52
+ {Object.keys(hueTokenThemes).map((hue) => {
53
+ return (
54
+ <DropdownMenu.CheckboxItem
55
+ key={hue}
56
+ checked={hue === hueValue}
57
+ onCheckedChange={() => setHueValue(hue)}
58
+ >
59
+ <HuePreview hue={hue} />
60
+ <span className='grow'>{t(`${hue} label`)}</span>
61
+ <DropdownMenu.ItemIndicator>
62
+ <Check />
63
+ </DropdownMenu.ItemIndicator>
64
+ </DropdownMenu.CheckboxItem>
65
+ );
66
+ })}
67
+ </DropdownMenu.Viewport>
68
+ <DropdownMenu.Arrow />
69
+ </DropdownMenu.Content>
70
+ </DropdownMenu.Root>
71
+ <Tooltip.Root>
72
+ <Tooltip.Trigger asChild>
73
+ <Button variant='ghost' onClick={onClickClear} disabled={disabled}>
74
+ <span className='sr-only'>{t('clear label')}</span>
75
+ <ArrowCounterClockwise />
76
+ </Button>
77
+ </Tooltip.Trigger>
78
+ <Tooltip.Portal>
79
+ <Tooltip.Content style={{ zIndex: 70 }} side='right'>
80
+ {t('clear label')}
81
+ <Tooltip.Arrow />
82
+ </Tooltip.Content>
83
+ </Tooltip.Portal>
84
+ </Tooltip.Root>
85
+ </>
86
+ );
87
+ };
@@ -7,8 +7,9 @@ import React, { type ComponentPropsWithoutRef, forwardRef } from 'react';
7
7
  import { generateName } from '@dxos/display-name';
8
8
  import { SpaceMember } from '@dxos/react-client/echo';
9
9
  import { type Identity } from '@dxos/react-client/halo';
10
- import { ListItem, Avatar, useJdenticonHref, useId, type ThemedClassName } from '@dxos/react-ui';
10
+ import { ListItem, Avatar, useId, type ThemedClassName } from '@dxos/react-ui';
11
11
  import { mx } from '@dxos/react-ui-theme';
12
+ import { keyToFallback } from '@dxos/util';
12
13
 
13
14
  type IdentityListItemProps = {
14
15
  identity: Identity;
@@ -20,9 +21,8 @@ export const IdentityListItem = forwardRef<
20
21
  HTMLLIElement,
21
22
  ThemedClassName<ComponentPropsWithoutRef<'li'>> & IdentityListItemProps
22
23
  >(({ identity, presence, onClick, classNames, ...props }, forwardedRef) => {
23
- const fallbackValue = identity.identityKey.toHex();
24
+ const fallbackValue = keyToFallback(identity.identityKey);
24
25
  const labelId = useId('identityListItem__label');
25
- const jdenticon = useJdenticonHref(fallbackValue ?? '', 12);
26
26
  const displayName = identity.profile?.displayName ?? generateName(identity.identityKey.toHex());
27
27
  return (
28
28
  <ListItem.Root
@@ -33,9 +33,13 @@ export const IdentityListItem = forwardRef<
33
33
  labelId={labelId}
34
34
  ref={forwardedRef}
35
35
  >
36
- <Avatar.Root status={presence === SpaceMember.PresenceState.ONLINE ? 'active' : 'inactive'} labelId={labelId}>
36
+ <Avatar.Root
37
+ status={presence === SpaceMember.PresenceState.ONLINE ? 'active' : 'inactive'}
38
+ labelId={labelId}
39
+ hue={identity.profile?.data?.hue || fallbackValue.hue}
40
+ >
37
41
  <Avatar.Frame classNames='place-self-center'>
38
- <Avatar.Fallback href={jdenticon} />
42
+ <Avatar.Fallback text={identity.profile?.data?.emoji || fallbackValue.emoji} />
39
43
  </Avatar.Frame>
40
44
  <Avatar.Label classNames='text-sm truncate pli-2'>{displayName}</Avatar.Label>
41
45
  </Avatar.Root>
@@ -2,6 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
  import { X } from '@phosphor-icons/react';
5
+ import { formatDistanceToNow } from 'date-fns';
5
6
  import React, { type ComponentPropsWithoutRef, useCallback } from 'react';
6
7
 
7
8
  import {
@@ -21,9 +22,9 @@ import {
21
22
  type ThemedClassName,
22
23
  } from '@dxos/react-ui';
23
24
  import { focusRing, getSize, mx } from '@dxos/react-ui-theme';
25
+ import { hexToEmoji } from '@dxos/util';
24
26
 
25
27
  import { type SharedInvitationListProps } from './InvitationListProps';
26
- import { toEmoji } from '../../util';
27
28
  import { AuthCode } from '../AuthCode';
28
29
  import { CopyButtonIconOnly } from '../Clipboard';
29
30
 
@@ -111,6 +112,10 @@ export const InvitationListItemImpl = ({
111
112
 
112
113
  const invitationUrl = invitationCode && createInvitationUrl(invitationCode);
113
114
  const invitationId = invitation?.get().invitationId;
115
+ const invitationHasLifetime = invitation?.get().lifetime;
116
+ const invitationTimeLeft = invitation?.expiry
117
+ ? formatDistanceToNow(invitation?.expiry, { addSuffix: true })
118
+ : undefined;
114
119
 
115
120
  const avatarAnimation = [
116
121
  Invitation.State.INIT,
@@ -151,7 +156,7 @@ export const InvitationListItemImpl = ({
151
156
  <Avatar.Root {...avatarProps} animation={avatarAnimation} status={avatarStatus}>
152
157
  <Tooltip.Trigger asChild>
153
158
  <Avatar.Frame tabIndex={0} classNames={[focusRing, 'relative rounded-full place-self-center']}>
154
- <Avatar.Fallback text={toEmoji(invitationId)} />
159
+ <Avatar.Fallback text={hexToEmoji(invitationId)} />
155
160
  </Avatar.Frame>
156
161
  </Tooltip.Trigger>
157
162
  </Avatar.Root>
@@ -163,17 +168,26 @@ export const InvitationListItemImpl = ({
163
168
  </Tooltip.Portal>
164
169
  </Tooltip.Root>
165
170
  {showShare && invitationUrl ? (
166
- <>
167
- <Button
168
- variant='ghost'
169
- classNames='grow justify-start font-medium'
170
- onClick={() => send({ type: 'selectInvitation', invitation })}
171
- data-testid='show-qrcode'
172
- >
173
- <span>{t('open share panel label')}</span>
174
- </Button>
175
- <CopyButtonIconOnly variant='ghost' value={invitationUrl} />
176
- </>
171
+ <Tooltip.Root>
172
+ <>
173
+ <Tooltip.Trigger asChild>
174
+ <Button
175
+ variant='ghost'
176
+ classNames='grow justify-start font-medium'
177
+ onClick={() => send({ type: 'selectInvitation', invitation })}
178
+ data-testid='show-qrcode'
179
+ >
180
+ <span>{t('open share panel label')}</span>
181
+ </Button>
182
+ </Tooltip.Trigger>
183
+ <CopyButtonIconOnly variant='ghost' value={invitationUrl} />
184
+ </>
185
+ <Tooltip.Portal>
186
+ <Tooltip.Content side='left' classNames='z-[70]'>
187
+ {invitationHasLifetime && <span>Expires {invitationTimeLeft}</span>}
188
+ </Tooltip.Content>
189
+ </Tooltip.Portal>
190
+ </Tooltip.Root>
177
191
  ) : showAuthCode ? (
178
192
  <AuthCode code={authCode} classNames='grow' />
179
193
  ) : invitationStatus === Invitation.State.CONNECTING ? (
@@ -5,15 +5,14 @@
5
5
  import React, { type ForwardedRef, forwardRef } from 'react';
6
6
 
7
7
  import type { Space } from '@dxos/react-client/echo';
8
- import { Avatar, useId, useJdenticonHref } from '@dxos/react-ui';
8
+ import { Avatar, useId } from '@dxos/react-ui';
9
9
  import { mx } from '@dxos/react-ui-theme';
10
- import { humanize } from '@dxos/util';
10
+ import { humanize, keyToEmoji } from '@dxos/util';
11
11
 
12
12
  export const SpaceListItem = forwardRef(
13
13
  ({ space, onClick }: { space: Space; onClick?: () => void }, ref: ForwardedRef<HTMLLIElement>) => {
14
- const fallbackValue = space.key.toHex();
14
+ const fallbackValue = keyToEmoji(space.key);
15
15
  const labelId = useId('identityListItem__label');
16
- const jdenticon = useJdenticonHref(fallbackValue ?? '', 12);
17
16
  const displayName = space.properties.name ?? humanize(space.key.toHex());
18
17
 
19
18
  return (
@@ -25,7 +24,7 @@ export const SpaceListItem = forwardRef(
25
24
  >
26
25
  <Avatar.Root labelId={labelId}>
27
26
  <Avatar.Frame>
28
- <Avatar.Fallback href={jdenticon} />
27
+ <Avatar.Fallback text={fallbackValue} />
29
28
  </Avatar.Frame>
30
29
  <Avatar.Label classNames='text-sm truncate'>{displayName}</Avatar.Label>
31
30
  </Avatar.Root>
@@ -4,6 +4,8 @@
4
4
 
5
5
  export * from './Clipboard';
6
6
  export * from './CompoundButton';
7
+ export * from './EmojiPicker';
8
+ export * from './HuePicker';
7
9
  export * from './IdentityList';
8
10
  export * from './InvitationList';
9
11
  export * from './Panel';
@@ -9,7 +9,8 @@ import { useClient } from '@dxos/react-client';
9
9
  import { type Identity, useIdentity, useDevices, useHaloInvitations } from '@dxos/react-client/halo';
10
10
  import { useInvitationStatus } from '@dxos/react-client/invitations';
11
11
  import type { CancellableInvitationObservable } from '@dxos/react-client/invitations';
12
- import { Avatar, DensityProvider, useId, useJdenticonHref, useTranslation } from '@dxos/react-ui';
12
+ import { Avatar, DensityProvider, useId, useTranslation } from '@dxos/react-ui';
13
+ import { keyToFallback } from '@dxos/util';
13
14
 
14
15
  import {
15
16
  type IdentityPanelHeadingProps,
@@ -25,12 +26,12 @@ import { InvitationManager } from '../../steps';
25
26
  const viewStyles = 'pbs-1 pbe-3 pli-3';
26
27
 
27
28
  const IdentityHeading = ({ titleId, title, identity, onDone }: IdentityPanelHeadingProps) => {
28
- const fallbackHref = useJdenticonHref(identity.identityKey.toHex(), 12);
29
+ const fallbackValue = keyToFallback(identity.identityKey);
29
30
  return (
30
31
  <Heading titleId={titleId} title={title} corner={<CloseButton onDone={onDone} />}>
31
- <Avatar.Root size={12} variant='circle' status='active'>
32
+ <Avatar.Root size={12} variant='circle' status='active' hue={identity.profile?.data?.hue || fallbackValue.hue}>
32
33
  <Avatar.Frame classNames='block mbs-4 mbe-2 mli-auto chromatic-ignore'>
33
- <Avatar.Fallback href={fallbackHref} />
34
+ <Avatar.Fallback text={identity.profile?.data?.emoji || fallbackValue.emoji} />
34
35
  </Avatar.Frame>
35
36
  <Avatar.Label classNames='block text-center font-light text-xl' data-testid='identityHeading.displayName'>
36
37
  {identity.profile?.displayName ?? generateName(identity.identityKey.toHex())}
@@ -128,7 +129,7 @@ export const IdentityPanel = ({
128
129
  const invitations = useHaloInvitations();
129
130
  const agentProps = useAgentHandlers({ client, identity, invitations });
130
131
  if (!identity) {
131
- console.error('IdentityPanel rendered with no active identity.');
132
+ log.error('IdentityPanel rendered with no active identity.');
132
133
  return null;
133
134
  }
134
135
  const [identityState, identitySend, identityService] = useIdentityMachine(client);
@@ -5,6 +5,7 @@
5
5
  import { CaretRight, Devices, Plus, Power, UserGear, HardDrive } from '@phosphor-icons/react';
6
6
  import React, { useCallback } from 'react';
7
7
 
8
+ import { log } from '@dxos/log';
8
9
  import { useAgentHostingProviderClient, useClient } from '@dxos/react-client';
9
10
  import { useHaloInvitations } from '@dxos/react-client/halo';
10
11
  import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
@@ -24,12 +25,13 @@ export const IdentityActionChooser = (props: IdentityPanelStepProps) => {
24
25
  const onInvitationEvent = useCallback((invitation: Invitation) => {
25
26
  const invitationCode = InvitationEncoder.encode(invitation);
26
27
  if (invitation.state === Invitation.State.CONNECTING) {
27
- console.log(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
28
+ log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
28
29
  }
29
30
  }, []);
30
31
  const createInvitation = (e: React.MouseEvent) => {
31
32
  const testing = e.altKey && e.shiftKey;
32
33
  invitations.forEach((invitation) => invitation.cancel());
34
+ // TODO(nf): allow user to make invitations non-persistent?
33
35
  const invitation = client.halo.share(
34
36
  testing ? { type: Invitation.Type.MULTIUSE, authMethod: Invitation.AuthMethod.NONE } : undefined,
35
37
  );
@@ -8,8 +8,9 @@ import { type Event, type SingleOrArray } from 'xstate';
8
8
  import { log } from '@dxos/log';
9
9
  import { type Identity } from '@dxos/react-client/halo';
10
10
  import { useTranslation } from '@dxos/react-ui';
11
+ import { hexToEmoji, hexToHue } from '@dxos/util';
11
12
 
12
- import { Action, Actions, StepHeading, Input, useClipboardContext } from '../../../components';
13
+ import { Action, Actions, StepHeading, Input, useClipboardContext, EmojiPicker, HuePicker } from '../../../components';
13
14
  import { type IdentityPanelStepProps } from '../IdentityPanelProps';
14
15
  import { type IdentityEvent } from '../identityMachine';
15
16
 
@@ -36,13 +37,20 @@ export const ProfileForm = (props: ProfileFormProps) => {
36
37
  return <ProfileFormImpl {...props} onUpdateProfile={handleUpdateProfile} validationMessage={validationMessage} />;
37
38
  };
38
39
 
40
+ const getHueValue = (identity?: Identity) =>
41
+ identity?.profile?.data?.hue || hexToHue(identity?.identityKey.toHex() ?? '0');
42
+ const getEmojiValue = (identity?: Identity) =>
43
+ identity?.profile?.data?.emoji || hexToEmoji(identity?.identityKey.toHex() ?? '0');
44
+
39
45
  // TODO(zhenyasav): impl shouldn't need send()
40
46
  const ProfileFormImpl = (props: ProfileFormImplProps) => {
41
47
  const { active, identity, send, onUpdateProfile, validationMessage } = props;
42
48
  const profile = identity?.profile;
43
49
  const disabled = !active;
44
50
  const { t } = useTranslation('os');
45
- const [inputValue, setInputValue] = useState(profile?.displayName ?? '');
51
+ const [displayName, setDisplayName] = useState(profile?.displayName ?? '');
52
+ const [hue, setHue] = useState<string>(getHueValue(identity));
53
+ const [emoji, setEmoji] = useState<string>(getEmojiValue(identity));
46
54
  const { textValue, setTextValue } = useClipboardContext();
47
55
  const identityHex = identity?.identityKey.toHex();
48
56
  const copied = textValue === identityHex;
@@ -51,14 +59,28 @@ const ProfileFormImpl = (props: ProfileFormImplProps) => {
51
59
  <div role='none' className='grow flex flex-col justify-center'>
52
60
  <Input
53
61
  {...{ validationMessage }}
54
- label={<StepHeading>{t('display name input label')}</StepHeading>}
62
+ label={<StepHeading className='m-0'>{t('display name input label')}</StepHeading>}
55
63
  disabled={disabled}
56
64
  data-testid='display-name-input'
57
65
  placeholder={t('display name input placeholder')}
58
- value={inputValue}
59
- onChange={({ target: { value } }) => setInputValue(value)}
60
- onKeyDown={({ key }) => key === 'Enter' && onUpdateProfile?.({ displayName: inputValue })}
66
+ value={displayName}
67
+ onChange={({ target: { value } }) => setDisplayName(value)}
61
68
  />
69
+ <StepHeading className='mbe-2'>{t('emoji and color label')}</StepHeading>
70
+ <div role='none' className='grid grid-cols-[1fr_min-content] gap-y-2'>
71
+ <EmojiPicker
72
+ emoji={emoji}
73
+ onChangeEmoji={setEmoji}
74
+ disabled={disabled}
75
+ onClickClear={() => setEmoji(getEmojiValue(identity))}
76
+ />
77
+ <HuePicker
78
+ hue={hue}
79
+ onChangeHue={setHue}
80
+ disabled={disabled}
81
+ onClickClear={() => setHue(getHueValue(identity))}
82
+ />
83
+ </div>
62
84
  </div>
63
85
  <Actions>
64
86
  <Action
@@ -84,7 +106,7 @@ const ProfileFormImpl = (props: ProfileFormImplProps) => {
84
106
  <Action
85
107
  variant='primary'
86
108
  disabled={disabled}
87
- onClick={() => onUpdateProfile?.({ displayName: inputValue })}
109
+ onClick={() => onUpdateProfile?.({ displayName, data: { emoji, hue } })}
88
110
  data-testid='update-profile-form-continue'
89
111
  >
90
112
  {t('done label')}
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from 'react';
6
6
 
7
7
  import { type CancellableInvitation } from '@dxos/client-protocol';
8
8
  import { invariant } from '@dxos/invariant';
9
+ import { log } from '@dxos/log';
9
10
  import { useAgentHostingProviderClient, type Client } from '@dxos/react-client';
10
11
  import { type Identity } from '@dxos/react-client/halo';
11
12
  import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
@@ -46,7 +47,7 @@ export const useAgentHandlers = ({
46
47
  const handleAgentCreate = useCallback(async (invitation: Invitation) => {
47
48
  const invitationCode = InvitationEncoder.encode(invitation);
48
49
  if (invitation.state === Invitation.State.CONNECTING) {
49
- console.log(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
50
+ log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
50
51
  invariant(identity, 'Identity not found');
51
52
  if (agentHostingProviderClient) {
52
53
  try {
@@ -6,8 +6,9 @@ import React, { cloneElement } from 'react';
6
6
 
7
7
  import { generateName } from '@dxos/display-name';
8
8
  import type { Identity } from '@dxos/react-client/halo';
9
- import { Avatar, useId, useJdenticonHref, useTranslation } from '@dxos/react-ui';
9
+ import { Avatar, useId, useTranslation } from '@dxos/react-ui';
10
10
  import { mx } from '@dxos/react-ui-theme';
11
+ import { hexToFallback } from '@dxos/util';
11
12
 
12
13
  import { Action, Actions, StepHeading } from '../../../components';
13
14
  import { type JoinPanelMode, type JoinStepProps } from '../JoinPanelProps';
@@ -22,11 +23,10 @@ export const IdentityAdded = (props: IdentityAddedProps) => {
22
23
  const disabled = !active;
23
24
  const { t } = useTranslation('os');
24
25
 
25
- const fallbackValue = addedIdentity?.identityKey.toHex();
26
+ const addedIdentityHex = addedIdentity?.identityKey.toHex() ?? '0';
27
+ const fallbackValue = hexToFallback(addedIdentityHex);
26
28
  const labelId = useId('identityListItem__label');
27
- const jdenticon = useJdenticonHref(fallbackValue ?? '', 12);
28
- const displayName =
29
- addedIdentity?.profile?.displayName ?? (addedIdentity && generateName(addedIdentity?.identityKey.toHex()));
29
+ const displayName = addedIdentity?.profile?.displayName ?? (addedIdentity && generateName(addedIdentityHex));
30
30
 
31
31
  const doneAction = (
32
32
  <Action
@@ -43,9 +43,9 @@ export const IdentityAdded = (props: IdentityAddedProps) => {
43
43
  <>
44
44
  <StepHeading>{t('identity added label')}</StepHeading>
45
45
  <div role='none' className='grow flex flex-col items-center justify-center text-center gap-2'>
46
- <Avatar.Root status='active' labelId={labelId}>
46
+ <Avatar.Root status='active' labelId={labelId} hue={addedIdentity?.profile?.data?.hue || fallbackValue.hue}>
47
47
  <Avatar.Frame>
48
- <Avatar.Fallback href={jdenticon} />
48
+ <Avatar.Fallback text={addedIdentity?.profile?.data?.emoji || fallbackValue.emoji} />
49
49
  </Avatar.Frame>
50
50
  <Avatar.Label classNames={mx('text-lg truncate', !addedIdentity?.profile?.displayName && 'font-mono')}>
51
51
  {displayName}
@@ -5,9 +5,9 @@
5
5
  import React, { type ChangeEvent, useState } from 'react';
6
6
 
7
7
  import { Input, useTranslation } from '@dxos/react-ui';
8
+ import { hexToEmoji } from '@dxos/util';
8
9
 
9
10
  import { Actions, Action, Emoji, StepHeading } from '../../../components';
10
- import { toEmoji } from '../../../util';
11
11
  import { type JoinStepProps } from '../JoinPanelProps';
12
12
 
13
13
  const pinLength = 6;
@@ -45,7 +45,7 @@ export const InvitationAuthenticator = ({
45
45
  return (
46
46
  <>
47
47
  <div role='none' className='grow flex flex-col justify-center'>
48
- {invitationId && <Emoji text={toEmoji(invitationId)} />}
48
+ {invitationId && <Emoji text={hexToEmoji(invitationId)} />}
49
49
  <Input.Root
50
50
  {...(failed && {
51
51
  validationValence: 'error',
@@ -16,9 +16,7 @@ const getComponentNames = (module: any): string[] =>
16
16
  Object.getOwnPropertyNames(module).filter((name) => !(name === 'default' || name.startsWith('__')));
17
17
 
18
18
  const getComponents = (module: any): React.FC[] => {
19
- const components = getComponentNames(module).map((name) => module[name]);
20
- console.log(module);
21
- return components;
19
+ return getComponentNames(module).map((name) => module[name]);
22
20
  };
23
21
 
24
22
  const camelCaseToSpacedName = (camelCase: string) => {
@@ -5,6 +5,7 @@
5
5
  import { Placeholder, UserPlus, UsersThree } from '@phosphor-icons/react';
6
6
  import React, { type Dispatch, type SetStateAction, useCallback, useState } from 'react';
7
7
 
8
+ import { log } from '@dxos/log';
8
9
  import { useConfig } from '@dxos/react-client';
9
10
  import { useSpaceInvitations } from '@dxos/react-client/echo';
10
11
  import { type CancellableInvitationObservable, Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
@@ -45,7 +46,7 @@ export const SpaceManager = (props: SpaceManagerProps) => {
45
46
  const onInvitationEvent = useCallback((invitation: Invitation) => {
46
47
  const invitationCode = InvitationEncoder.encode(invitation);
47
48
  if (invitation.state === Invitation.State.CONNECTING) {
48
- console.log(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
49
+ log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
49
50
  }
50
51
  }, []);
51
52
 
@@ -80,6 +81,22 @@ export const SpaceManager = (props: SpaceManagerProps) => {
80
81
  }
81
82
  }, [space]),
82
83
  },
84
+ // inviteNonPersistent: {
85
+ // label: 'Invite one (nonpersistent)',
86
+ // description: 'Only one user may join. Invitation does not resume if client is restarted.',
87
+ // icon: UserPlus,
88
+ // onClick: useCallback(() => {
89
+ // const invitation = space.share?.({
90
+ // type: Invitation.Type.INTERACTIVE,
91
+ // authMethod: Invitation.AuthMethod.SHARED_SECRET,
92
+ // persistent: false,
93
+ // target,
94
+ // });
95
+ // if (invitation && config.values.runtime?.app?.env?.DX_ENVIRONMENT !== 'production') {
96
+ // invitation.subscribe(onInvitationEvent);
97
+ // }
98
+ // }, [space]),
99
+ // },
83
100
  };
84
101
 
85
102
  return <SpaceManagerImpl {...props} invitations={invitations} inviteActions={inviteActions} />;
@@ -9,6 +9,7 @@ import { QR } from 'react-qr-rounded';
9
9
  import { type InvitationStatus, Invitation } from '@dxos/react-client/invitations';
10
10
  import { useId, useTranslation } from '@dxos/react-ui';
11
11
  import { descriptionText, getSize, mx } from '@dxos/react-ui-theme';
12
+ import { hexToEmoji } from '@dxos/util';
12
13
 
13
14
  import { type StepProps } from './StepProps';
14
15
  import {
@@ -22,7 +23,7 @@ import {
22
23
  CopyButton,
23
24
  Centered,
24
25
  } from '../components';
25
- import { invitationStatusValue, toEmoji } from '../util';
26
+ import { invitationStatusValue } from '../util';
26
27
 
27
28
  export type InvitationManagerProps = StepProps &
28
29
  Partial<InvitationStatus> & {
@@ -44,13 +45,13 @@ export const InvitationManager = ({
44
45
  status,
45
46
  type,
46
47
  authCode,
47
- id,
48
+ id = '0',
48
49
  }: InvitationManagerProps) => {
49
50
  const { t } = useTranslation('os');
50
51
  const qrLabel = useId('invitation-manager__qr-code');
51
52
  const statusValue = type === Invitation.Type.MULTIUSE ? 0 : invitationStatusValue.get(status!) ?? 0;
52
53
  const showAuthCode = statusValue === 3;
53
- const emoji = toEmoji(id ?? '');
54
+ const emoji = hexToEmoji(id);
54
55
  const activeView = useMemo(() => {
55
56
  switch (true) {
56
57
  case statusValue === 5:
@@ -7,6 +7,7 @@ import '@dxosTheme';
7
7
  import { Laptop, Planet, Plus, PlusCircle, SignIn, QrCode, WifiHigh, WifiSlash } from '@phosphor-icons/react';
8
8
  import React, { useMemo, useState } from 'react';
9
9
 
10
+ import { log } from '@dxos/log';
10
11
  import { faker } from '@dxos/random';
11
12
  import { useClient } from '@dxos/react-client';
12
13
  import { type Space, type SpaceMember, SpaceProxy, useSpaces } from '@dxos/react-client/echo';
@@ -42,7 +43,7 @@ const Panel = ({ id, panel, setPanel }: { id: number; panel?: PanelType; setPane
42
43
  invitation.subscribe((invitation) => {
43
44
  const invitationCode = InvitationEncoder.encode(invitation);
44
45
  if (invitation.state === Invitation.State.CONNECTING) {
45
- console.log(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
46
+ log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
46
47
  }
47
48
  });
48
49
  };
@@ -126,7 +127,7 @@ const Invitations = (args: { id: number; count: number }) => {
126
127
  invitation.subscribe((invitation) => {
127
128
  const invitationCode = InvitationEncoder.encode(invitation);
128
129
  if (invitation.state === Invitation.State.CONNECTING) {
129
- console.log(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
130
+ log.info(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
130
131
  }
131
132
  });
132
133
  };