@dxos/react-ui 0.1.23 → 0.1.24-next.6b1f434

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 (199) hide show
  1. package/dist/lib/browser/index.mjs +2428 -72
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/IdentityList/DeviceList.d.ts +7 -0
  5. package/dist/types/src/components/IdentityList/DeviceList.d.ts.map +1 -0
  6. package/dist/types/src/components/IdentityList/IdentityListItem.d.ts +7 -0
  7. package/dist/types/src/components/IdentityList/IdentityListItem.d.ts.map +1 -0
  8. package/dist/types/src/components/{IdentityListItem → IdentityList}/IdentityListItem.stories.d.ts +2 -1
  9. package/dist/types/src/components/IdentityList/IdentityListItem.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts +7 -0
  11. package/dist/types/src/components/IdentityList/SpaceMemberList.d.ts.map +1 -0
  12. package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts +9 -0
  13. package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts.map +1 -0
  14. package/dist/types/src/components/IdentityList/index.d.ts +5 -0
  15. package/dist/types/src/components/IdentityList/index.d.ts.map +1 -0
  16. package/dist/types/src/components/InvitationList/InvitationList.d.ts +7 -0
  17. package/dist/types/src/components/InvitationList/InvitationList.d.ts.map +1 -0
  18. package/dist/types/src/components/InvitationList/InvitationList.stories.d.ts +10 -0
  19. package/dist/types/src/components/InvitationList/InvitationList.stories.d.ts.map +1 -0
  20. package/dist/types/src/components/InvitationList/InvitationListContainer.d.ts +7 -0
  21. package/dist/types/src/components/InvitationList/InvitationListContainer.d.ts.map +1 -0
  22. package/dist/types/src/components/InvitationList/InvitationListItem.d.ts +9 -0
  23. package/dist/types/src/components/InvitationList/InvitationListItem.d.ts.map +1 -0
  24. package/dist/types/src/components/InvitationList/InvitationListProps.d.ts +4 -0
  25. package/dist/types/src/components/InvitationList/InvitationListProps.d.ts.map +1 -0
  26. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts +14 -0
  27. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.d.ts.map +1 -0
  28. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.stories.d.ts +9 -0
  29. package/dist/types/src/components/InvitationList/InvitationStatusAvatar.stories.d.ts.map +1 -0
  30. package/dist/types/src/components/InvitationList/index.d.ts +5 -0
  31. package/dist/types/src/components/InvitationList/index.d.ts.map +1 -0
  32. package/dist/types/src/components/PanelSeparator/PanelSeparator.d.ts +3 -0
  33. package/dist/types/src/components/PanelSeparator/PanelSeparator.d.ts.map +1 -0
  34. package/dist/types/src/components/PanelSeparator/index.d.ts +2 -0
  35. package/dist/types/src/components/PanelSeparator/index.d.ts.map +1 -0
  36. package/dist/types/src/components/SpaceList/SpaceListItem.d.ts +7 -0
  37. package/dist/types/src/components/SpaceList/SpaceListItem.d.ts.map +1 -0
  38. package/dist/types/src/components/SpaceList/index.d.ts +2 -0
  39. package/dist/types/src/components/SpaceList/index.d.ts.map +1 -0
  40. package/dist/types/src/components/index.d.ts +5 -0
  41. package/dist/types/src/components/index.d.ts.map +1 -0
  42. package/dist/types/src/composites/DevicesDialog/DevicesDialog.d.ts +6 -0
  43. package/dist/types/src/composites/DevicesDialog/DevicesDialog.d.ts.map +1 -0
  44. package/dist/types/src/composites/DevicesDialog/index.d.ts +2 -0
  45. package/dist/types/src/composites/DevicesDialog/index.d.ts.map +1 -0
  46. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts +6 -0
  47. package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts.map +1 -0
  48. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts +10 -0
  49. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts.map +1 -0
  50. package/dist/types/src/composites/JoinDialog/index.d.ts +2 -0
  51. package/dist/types/src/composites/JoinDialog/index.d.ts.map +1 -0
  52. package/dist/types/src/composites/Shell/Shell.d.ts +6 -0
  53. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -0
  54. package/dist/types/src/composites/Shell/index.d.ts +2 -0
  55. package/dist/types/src/composites/Shell/index.d.ts.map +1 -0
  56. package/dist/types/src/composites/SpaceDialog/SpaceDialog.d.ts +6 -0
  57. package/dist/types/src/composites/SpaceDialog/SpaceDialog.d.ts.map +1 -0
  58. package/dist/types/src/composites/SpaceDialog/index.d.ts +2 -0
  59. package/dist/types/src/composites/SpaceDialog/index.d.ts.map +1 -0
  60. package/dist/types/src/composites/index.d.ts +3 -0
  61. package/dist/types/src/composites/index.d.ts.map +1 -1
  62. package/dist/types/src/index.d.ts +1 -0
  63. package/dist/types/src/index.d.ts.map +1 -1
  64. package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts +14 -0
  65. package/dist/types/src/layouts/PanelAlertDialog/PanelAlertDialog.d.ts.map +1 -0
  66. package/dist/types/src/layouts/PanelAlertDialog/index.d.ts +2 -0
  67. package/dist/types/src/layouts/PanelAlertDialog/index.d.ts.map +1 -0
  68. package/dist/types/src/layouts/PanelDialog/PanelDialog.d.ts +14 -0
  69. package/dist/types/src/layouts/PanelDialog/PanelDialog.d.ts.map +1 -0
  70. package/dist/types/src/layouts/PanelDialog/index.d.ts +2 -0
  71. package/dist/types/src/layouts/PanelDialog/index.d.ts.map +1 -0
  72. package/dist/types/src/layouts/PanelSidebar/PanelSidebar.d.ts +2 -3
  73. package/dist/types/src/layouts/PanelSidebar/PanelSidebar.d.ts.map +1 -1
  74. package/dist/types/src/layouts/index.d.ts +2 -0
  75. package/dist/types/src/layouts/index.d.ts.map +1 -1
  76. package/dist/types/src/panels/DevicesPanel/DevicesPanel.d.ts +10 -0
  77. package/dist/types/src/panels/DevicesPanel/DevicesPanel.d.ts.map +1 -0
  78. package/dist/types/src/panels/DevicesPanel/index.d.ts +2 -0
  79. package/dist/types/src/panels/DevicesPanel/index.d.ts.map +1 -0
  80. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts +13 -0
  81. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -0
  82. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts +3 -0
  83. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts.map +1 -0
  84. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +54 -0
  85. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -0
  86. package/dist/types/src/panels/JoinPanel/index.d.ts +3 -0
  87. package/dist/types/src/panels/JoinPanel/index.d.ts.map +1 -0
  88. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts +7 -0
  89. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts.map +1 -0
  90. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts +15 -0
  91. package/dist/types/src/panels/JoinPanel/view-states/IdentityAdded.d.ts.map +1 -0
  92. package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts +6 -0
  93. package/dist/types/src/panels/JoinPanel/view-states/IdentityInput.d.ts.map +1 -0
  94. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts +7 -0
  95. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts.map +1 -0
  96. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts +10 -0
  97. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts.map +1 -0
  98. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts +7 -0
  99. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts.map +1 -0
  100. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts +6 -0
  101. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts.map +1 -0
  102. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts +6 -0
  103. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts.map +1 -0
  104. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts +12 -0
  105. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts.map +1 -0
  106. package/dist/types/src/panels/JoinPanel/view-states/index.d.ts +9 -0
  107. package/dist/types/src/panels/JoinPanel/view-states/index.d.ts.map +1 -0
  108. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts +18 -0
  109. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -0
  110. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts +9 -0
  111. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -0
  112. package/dist/types/src/panels/SpacePanel/index.d.ts +2 -0
  113. package/dist/types/src/panels/SpacePanel/index.d.ts.map +1 -0
  114. package/dist/types/src/panels/index.d.ts +3 -0
  115. package/dist/types/src/panels/index.d.ts.map +1 -1
  116. package/dist/types/src/styles/dialogStyles.d.ts +3 -0
  117. package/dist/types/src/styles/dialogStyles.d.ts.map +1 -0
  118. package/dist/types/src/styles/index.d.ts +2 -0
  119. package/dist/types/src/styles/index.d.ts.map +1 -1
  120. package/dist/types/src/styles/invitationStatusStyles.d.ts +20 -0
  121. package/dist/types/src/styles/invitationStatusStyles.d.ts.map +1 -0
  122. package/dist/types/src/styles/panelStyles.d.ts +2 -1
  123. package/dist/types/src/styles/panelStyles.d.ts.map +1 -1
  124. package/dist/types/src/translations/locales/en-US.d.ts +52 -0
  125. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  126. package/dist/types/src/util/index.d.ts +2 -0
  127. package/dist/types/src/util/index.d.ts.map +1 -0
  128. package/dist/types/src/util/invitationStatusValue.d.ts +8 -0
  129. package/dist/types/src/util/invitationStatusValue.d.ts.map +1 -0
  130. package/package.json +19 -11
  131. package/src/components/IdentityList/DeviceList.tsx +31 -0
  132. package/src/components/{IdentityListItem → IdentityList}/IdentityListItem.stories.tsx +0 -0
  133. package/src/components/{IdentityListItem → IdentityList}/IdentityListItem.tsx +18 -5
  134. package/src/components/IdentityList/SpaceMemberList.tsx +33 -0
  135. package/src/components/IdentityList/SpaceMemberListContainer.tsx +28 -0
  136. package/src/components/IdentityList/index.ts +8 -0
  137. package/src/components/InvitationList/InvitationList.stories.tsx +65 -0
  138. package/src/components/InvitationList/InvitationList.tsx +28 -0
  139. package/src/components/InvitationList/InvitationListContainer.tsx +27 -0
  140. package/src/components/InvitationList/InvitationListItem.tsx +106 -0
  141. package/src/components/InvitationList/InvitationListProps.ts +7 -0
  142. package/src/components/InvitationList/InvitationStatusAvatar.stories.tsx +52 -0
  143. package/src/components/InvitationList/InvitationStatusAvatar.tsx +80 -0
  144. package/src/components/InvitationList/index.ts +8 -0
  145. package/src/components/PanelSeparator/PanelSeparator.tsx +17 -0
  146. package/src/components/PanelSeparator/index.ts +5 -0
  147. package/src/components/SpaceList/SpaceListItem.tsx +32 -0
  148. package/src/components/SpaceList/index.ts +5 -0
  149. package/src/components/index.ts +8 -0
  150. package/src/composites/DevicesDialog/DevicesDialog.tsx +38 -0
  151. package/src/composites/DevicesDialog/index.ts +5 -0
  152. package/src/composites/JoinDialog/JoinDialog.stories.tsx +81 -0
  153. package/src/composites/JoinDialog/JoinDialog.tsx +33 -0
  154. package/src/composites/JoinDialog/index.ts +5 -0
  155. package/src/composites/Shell/Shell.tsx +92 -0
  156. package/src/composites/Shell/index.ts +5 -0
  157. package/src/composites/SpaceDialog/SpaceDialog.tsx +38 -0
  158. package/src/composites/SpaceDialog/index.ts +5 -0
  159. package/src/composites/index.ts +4 -0
  160. package/src/index.ts +1 -0
  161. package/src/layouts/PanelAlertDialog/PanelAlertDialog.tsx +40 -0
  162. package/src/layouts/PanelAlertDialog/index.ts +5 -0
  163. package/src/layouts/PanelDialog/PanelDialog.tsx +39 -0
  164. package/src/layouts/PanelDialog/index.ts +5 -0
  165. package/src/layouts/PanelPopover/PanelPopover.tsx +2 -2
  166. package/src/layouts/PanelSidebar/PanelSidebar.tsx +17 -25
  167. package/src/layouts/index.ts +2 -0
  168. package/src/panels/DevicesPanel/DevicesPanel.tsx +98 -0
  169. package/src/panels/DevicesPanel/index.ts +5 -0
  170. package/src/panels/JoinPanel/JoinHeading.tsx +85 -0
  171. package/src/panels/JoinPanel/JoinPanel.tsx +319 -0
  172. package/src/panels/JoinPanel/JoinPanelProps.ts +90 -0
  173. package/src/panels/JoinPanel/index.ts +6 -0
  174. package/src/panels/JoinPanel/view-states/AdditionMethodSelector.tsx +73 -0
  175. package/src/panels/JoinPanel/view-states/IdentityAdded.tsx +99 -0
  176. package/src/panels/JoinPanel/view-states/IdentityInput.tsx +79 -0
  177. package/src/panels/JoinPanel/view-states/IdentitySelector.tsx +101 -0
  178. package/src/panels/JoinPanel/view-states/InvitationAccepted.tsx +70 -0
  179. package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +142 -0
  180. package/src/panels/JoinPanel/view-states/InvitationInput.tsx +75 -0
  181. package/src/panels/JoinPanel/view-states/InvitationRescuer.tsx +98 -0
  182. package/src/panels/JoinPanel/view-states/ViewState.tsx +204 -0
  183. package/src/panels/JoinPanel/view-states/index.ts +12 -0
  184. package/src/panels/SpacePanel/SpacePanel.stories.tsx +53 -0
  185. package/src/panels/SpacePanel/SpacePanel.tsx +174 -0
  186. package/src/panels/SpacePanel/index.ts +5 -0
  187. package/src/panels/index.ts +3 -0
  188. package/src/styles/dialogStyles.ts +8 -0
  189. package/src/styles/index.ts +2 -0
  190. package/src/styles/invitationStatusStyles.ts +42 -0
  191. package/src/styles/panelStyles.ts +2 -1
  192. package/src/testing/ClientProvider.tsx +1 -1
  193. package/src/translations/locales/en-US.ts +53 -1
  194. package/src/util/index.ts +5 -0
  195. package/src/util/invitationStatusValue.ts +21 -0
  196. package/theme-extensions.js +11 -0
  197. package/dist/types/src/components/IdentityListItem/IdentityListItem.d.ts +0 -6
  198. package/dist/types/src/components/IdentityListItem/IdentityListItem.d.ts.map +0 -1
  199. package/dist/types/src/components/IdentityListItem/IdentityListItem.stories.d.ts.map +0 -1
@@ -0,0 +1,85 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+ import { ProhibitInset } from 'phosphor-react';
5
+ import React, { cloneElement, ForwardedRef, forwardRef } from 'react';
6
+
7
+ import { AuthenticatingInvitationObservable } from '@dxos/client';
8
+ import { useSpace } from '@dxos/react-client';
9
+ import { Avatar, Button, getSize, Heading, mx, Trans, useId, useTranslation } from '@dxos/react-components';
10
+
11
+ import { subduedSurface } from '../../styles';
12
+ import { JoinPanelMode } from './JoinPanelProps';
13
+
14
+ export interface JoinSpaceHeadingProps {
15
+ mode?: JoinPanelMode;
16
+ titleId: string;
17
+ invitation?: AuthenticatingInvitationObservable;
18
+ onExit?: () => void;
19
+ exitActionParent?: Parameters<typeof cloneElement>[0];
20
+ preventExit?: boolean;
21
+ }
22
+
23
+ export const JoinHeading = forwardRef(
24
+ (
25
+ { mode, titleId, invitation, onExit, exitActionParent, preventExit }: JoinSpaceHeadingProps,
26
+ ref: ForwardedRef<HTMLDivElement>
27
+ ) => {
28
+ const { t } = useTranslation('os');
29
+
30
+ const space = useSpace(invitation?.invitation?.spaceKey);
31
+ const name = mode === 'halo-only' ? '(Unknown identity)' : space?.properties.name ?? '(Space name not available)';
32
+ const nameId = useId(mode === 'halo-only' ? 'identityDisplayName' : 'spaceDisplayName');
33
+
34
+ const invitationKey =
35
+ mode === 'halo-only'
36
+ ? invitation?.invitation?.identityKey?.toHex()
37
+ : invitation?.invitation?.identityKey?.toHex();
38
+
39
+ const exitButton = (
40
+ <Button
41
+ compact
42
+ variant='ghost'
43
+ {...(onExit && { onClick: onExit })}
44
+ className='grow-0 shrink-0'
45
+ data-testid='join-exit'
46
+ >
47
+ <ProhibitInset className={getSize(5)} />
48
+ <span className='sr-only'>{t('exit label')}</span>
49
+ </Button>
50
+ );
51
+
52
+ return (
53
+ <div role='none' className={mx(subduedSurface, 'p-2 rounded-bs-md')} ref={ref}>
54
+ <div role='group' className='flex items-center gap-2'>
55
+ {invitationKey ? (
56
+ <Avatar fallbackValue={invitationKey} labelId={nameId} />
57
+ ) : (
58
+ <span role='none' className={mx(getSize(10), 'bg-neutral-300 dark:bg-neutral-700 rounded-full')} />
59
+ )}
60
+ <Heading level={1} className='font-body font-normal text-base grow' id={titleId}>
61
+ {invitation ? (
62
+ <Trans
63
+ {...{
64
+ defaults: t('joining heading'),
65
+ components: {
66
+ small: <span className='block leading-none mbe-1 font-system-medium text-sm' />,
67
+ large: <span className='block leading-none' id={nameId} />
68
+ },
69
+ values: { name }
70
+ }}
71
+ />
72
+ ) : (
73
+ <span className='block leading-none font-system-medium text-sm'>
74
+ {mode === 'halo-only' ? t('halo heading') : t('join space heading')}
75
+ </span>
76
+ )}
77
+ </Heading>
78
+ {!preventExit &&
79
+ mode !== 'halo-only' &&
80
+ (exitActionParent ? cloneElement(exitActionParent, {}, exitButton) : exitButton)}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+ );
@@ -0,0 +1,319 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+ import React, { useEffect, useReducer } from 'react';
5
+
6
+ import { InvitationEncoder } from '@dxos/client';
7
+ import { log } from '@dxos/log';
8
+ import { useClient, useIdentity } from '@dxos/react-client';
9
+ import { useId } from '@dxos/react-components';
10
+
11
+ import { JoinHeading } from './JoinHeading';
12
+ import { JoinAction, JoinPanelProps, JoinState } from './JoinPanelProps';
13
+ import {
14
+ AdditionMethodSelector,
15
+ IdentitySelector,
16
+ IdentityInput,
17
+ IdentityAdded,
18
+ InvitationAuthenticator,
19
+ InvitationRescuer,
20
+ InvitationInput,
21
+ InvitationAccepted
22
+ } from './view-states';
23
+
24
+ export const JoinPanel = ({
25
+ mode,
26
+ initialInvitationCode,
27
+ titleId: propsTitleId,
28
+ exitActionParent,
29
+ onExit,
30
+ doneActionParent,
31
+ onDone,
32
+ preventExit
33
+ }: JoinPanelProps) => {
34
+ const client = useClient();
35
+ const internalTitleId = useId('joinPanel__title');
36
+ const titleId = propsTitleId ?? internalTitleId;
37
+ const identity = useIdentity();
38
+
39
+ const availableIdentities = identity ? [identity] : [];
40
+
41
+ const reducer = (state: JoinState, action: JoinAction) => {
42
+ const nextState = { ...state };
43
+ switch (action.type) {
44
+ case 'added identity':
45
+ nextState.activeView = 'identity added';
46
+ nextState.selectedIdentity = action.identity;
47
+ break;
48
+ case 'add identity':
49
+ nextState.activeView = 'addition method selector';
50
+ break;
51
+ case 'select addition method':
52
+ if (action.method === 'accept device invitation') {
53
+ nextState.activeView = 'halo invitation acceptor';
54
+ if (state.unredeemedHaloInvitationCode) {
55
+ nextState.haloInvitation = client.halo.acceptInvitation(
56
+ InvitationEncoder.decode(state.unredeemedHaloInvitationCode)
57
+ );
58
+ nextState.unredeemedHaloInvitationCode = undefined;
59
+ nextState.haloViewState = 'invitation authenticator';
60
+ } else {
61
+ nextState.haloViewState = 'invitation input';
62
+ }
63
+ } else {
64
+ nextState.activeView = 'identity input';
65
+ nextState.additionMethod = action.method;
66
+ }
67
+ break;
68
+ case 'select identity':
69
+ nextState.selectedIdentity = action.identity;
70
+ nextState.activeView = 'space invitation acceptor';
71
+ if (state.unredeemedSpaceInvitationCode) {
72
+ nextState.spaceInvitation = client.echo.acceptInvitation(
73
+ InvitationEncoder.decode(state.unredeemedSpaceInvitationCode)
74
+ );
75
+ nextState.unredeemedSpaceInvitationCode = undefined;
76
+ nextState.spaceViewState = 'invitation authenticator';
77
+ } else {
78
+ nextState.spaceViewState = 'invitation input';
79
+ }
80
+ break;
81
+ case 'deselect identity':
82
+ nextState.selectedIdentity = undefined;
83
+ nextState.activeView = 'identity selector';
84
+ break;
85
+ case 'cancel invitation':
86
+ if (action.from === 'space' && state.spaceInvitation?.invitation) {
87
+ void state.spaceInvitation?.cancel();
88
+ }
89
+ if (action.from === 'halo' && state.haloInvitation?.invitation) {
90
+ void state.haloInvitation?.cancel();
91
+ }
92
+ break;
93
+ case 'authenticating invitation':
94
+ nextState[action.from === 'halo' ? 'haloInvitationAnnotation' : 'spaceInvitationAnnotation'] = 'authenticating';
95
+ break;
96
+ case 'connecting invitation':
97
+ nextState[action.from === 'halo' ? 'haloInvitation' : 'spaceInvitation'] = action.invitation;
98
+ // eslint-disable-next-line no-fallthrough
99
+ case 'cancelled invitation':
100
+ case 'fail invitation':
101
+ case 'timeout invitation':
102
+ nextState[action.from === 'halo' ? 'haloViewState' : 'spaceViewState'] = 'invitation rescuer';
103
+ break;
104
+ case 'connect invitation':
105
+ case 'authenticate invitation':
106
+ if (action.from === 'halo' && state.haloInvitationAnnotation === 'authenticating') {
107
+ nextState.haloInvitationAnnotation = 'authentication failed';
108
+ }
109
+ if (action.from === 'space' && state.spaceInvitationAnnotation === 'authenticating') {
110
+ nextState.spaceInvitationAnnotation = 'authentication failed';
111
+ }
112
+ nextState[action.from === 'halo' ? 'haloViewState' : 'spaceViewState'] = 'invitation authenticator';
113
+ break;
114
+ case 'accepted invitation':
115
+ nextState[action.from === 'halo' ? 'haloViewState' : 'spaceViewState'] = 'invitation accepted';
116
+ break;
117
+ case 'reset invitation':
118
+ nextState[action.from === 'halo' ? 'haloInvitation' : 'spaceInvitation'] = undefined;
119
+ nextState[action.from === 'halo' ? 'haloInvitationAnnotation' : 'spaceInvitationAnnotation'] = undefined;
120
+ nextState[action.from === 'halo' ? 'unredeemedHaloInvitationCode' : 'unredeemedSpaceInvitationCode'] =
121
+ undefined;
122
+ nextState[action.from === 'halo' ? 'haloViewState' : 'spaceViewState'] = 'invitation input';
123
+ break;
124
+ }
125
+ log('[join panel reducer]', { action, nextState });
126
+ return nextState;
127
+ };
128
+
129
+ const unredeemedHaloInvitationCode = mode === 'halo-only' ? initialInvitationCode : undefined;
130
+ const [joinState, dispatch] = useReducer(reducer, {
131
+ unredeemedHaloInvitationCode,
132
+ unredeemedSpaceInvitationCode: mode === 'halo-only' ? undefined : initialInvitationCode,
133
+ spaceInvitation: undefined,
134
+ haloInvitation: undefined,
135
+ activeView: availableIdentities.length > 0 ? 'identity selector' : 'addition method selector',
136
+ selectedIdentity: undefined,
137
+ additionMethod: undefined,
138
+ spaceViewState: 'invitation input',
139
+ haloViewState: 'invitation input'
140
+ });
141
+
142
+ useEffect(() => {
143
+ if (unredeemedHaloInvitationCode) {
144
+ dispatch({ type: 'select addition method', method: 'accept device invitation' });
145
+ }
146
+ }, [unredeemedHaloInvitationCode]);
147
+
148
+ useEffect(() => {
149
+ // TODO (thure): Validate if this is sufficiently synchronous for iOS to move focus. It might not be!
150
+ const attrValue =
151
+ joinState.activeView === 'identity input'
152
+ ? `${joinState.activeView}; ${joinState.additionMethod}`
153
+ : joinState.activeView === 'space invitation acceptor'
154
+ ? `${joinState.activeView}; ${joinState.spaceViewState}`
155
+ : joinState.activeView === 'halo invitation acceptor'
156
+ ? `${joinState.activeView}; ${joinState.haloViewState}`
157
+ : joinState.activeView;
158
+ log('[autofocus value]', { attrValue });
159
+ const $nextAutofocus: HTMLElement | null = document.querySelector(`[data-autofocus="${attrValue}"]`);
160
+ if ($nextAutofocus) {
161
+ $nextAutofocus.focus();
162
+ }
163
+ }, [joinState.activeView, joinState.spaceViewState, joinState.haloViewState]);
164
+
165
+ useEffect(() => {
166
+ return joinState.spaceInvitation?.subscribe({
167
+ onAuthenticating: () => dispatch({ type: 'authenticate invitation', from: 'space' }),
168
+ onCancelled: () => dispatch({ type: 'cancelled invitation', from: 'space' }),
169
+ onConnected: () => dispatch({ type: 'connect invitation', from: 'space' }),
170
+ onError: () => dispatch({ type: 'fail invitation', from: 'space' }),
171
+ onSuccess: () => dispatch({ type: 'accepted invitation', from: 'space' }),
172
+ onTimeout: () => dispatch({ type: 'timeout invitation', from: 'space' })
173
+ });
174
+ }, [joinState.spaceInvitation]);
175
+
176
+ useEffect(() => {
177
+ return joinState.haloInvitation?.subscribe({
178
+ onAuthenticating: () => dispatch({ type: 'authenticate invitation', from: 'halo' }),
179
+ onCancelled: () => dispatch({ type: 'cancelled invitation', from: 'halo' }),
180
+ onConnected: () => dispatch({ type: 'connect invitation', from: 'halo' }),
181
+ onError: () => dispatch({ type: 'fail invitation', from: 'halo' }),
182
+ onSuccess: () => dispatch({ type: 'accepted invitation', from: 'halo' }),
183
+ onTimeout: () => dispatch({ type: 'timeout invitation', from: 'halo' })
184
+ });
185
+ }, [joinState.haloInvitation]);
186
+
187
+ return (
188
+ <>
189
+ <JoinHeading
190
+ {...{ mode, titleId, invitation: joinState.spaceInvitation, onExit, exitActionParent, preventExit }}
191
+ />
192
+ <div role='none' className='is-full overflow-hidden'>
193
+ <div role='none' className='flex is-[1300%]' aria-live='polite'>
194
+ <IdentitySelector
195
+ {...{ dispatch, availableIdentities, active: joinState.activeView === 'identity selector' }}
196
+ />
197
+ <AdditionMethodSelector
198
+ {...{
199
+ dispatch,
200
+ availableIdentities,
201
+ active: joinState.activeView === 'addition method selector'
202
+ }}
203
+ />
204
+ <IdentityInput
205
+ {...{
206
+ dispatch,
207
+ active: joinState.activeView === 'identity input' && joinState.additionMethod === 'create identity',
208
+ method: 'create identity'
209
+ }}
210
+ />
211
+ <IdentityInput
212
+ {...{
213
+ dispatch,
214
+ active: joinState.activeView === 'identity input' && joinState.additionMethod === 'recover identity',
215
+ method: 'recover identity'
216
+ }}
217
+ />
218
+ <InvitationInput
219
+ {...{
220
+ dispatch,
221
+ activeInvitation: joinState.haloInvitation || true,
222
+ active:
223
+ joinState.activeView === 'halo invitation acceptor' && joinState.haloViewState === 'invitation input',
224
+ invitationType: 'halo'
225
+ }}
226
+ />
227
+ <InvitationRescuer
228
+ {...{
229
+ dispatch,
230
+ activeInvitation: joinState.haloInvitation || true,
231
+ active:
232
+ joinState.activeView === 'halo invitation acceptor' && joinState.haloViewState === 'invitation rescuer',
233
+ invitationType: 'halo'
234
+ }}
235
+ />
236
+ <InvitationAuthenticator
237
+ {...{
238
+ dispatch,
239
+ activeInvitation: joinState.haloInvitation || true,
240
+ active:
241
+ joinState.activeView === 'halo invitation acceptor' &&
242
+ joinState.haloViewState === 'invitation authenticator',
243
+ invitationType: 'halo',
244
+ ...(joinState.spaceInvitationAnnotation === 'authentication failed' && { failed: true })
245
+ }}
246
+ />
247
+ <InvitationAccepted
248
+ {...{
249
+ dispatch,
250
+ activeInvitation: joinState.haloInvitation || true,
251
+ active:
252
+ joinState.activeView === 'halo invitation acceptor' &&
253
+ joinState.haloViewState === 'invitation accepted',
254
+ invitationType: 'halo',
255
+ doneActionParent,
256
+ onDone
257
+ }}
258
+ />
259
+ <IdentityAdded
260
+ {...{
261
+ mode,
262
+ dispatch,
263
+ addedIdentity: joinState.selectedIdentity,
264
+ active: joinState.activeView === 'identity added',
265
+ doneActionParent,
266
+ onDone
267
+ }}
268
+ />
269
+ <InvitationInput
270
+ {...{
271
+ dispatch,
272
+ activeInvitation: joinState.spaceInvitation || true,
273
+ selectedIdentity: joinState.selectedIdentity,
274
+ active:
275
+ joinState.activeView === 'space invitation acceptor' && joinState.spaceViewState === 'invitation input',
276
+ invitationType: 'space'
277
+ }}
278
+ />
279
+ <InvitationRescuer
280
+ {...{
281
+ dispatch,
282
+ activeInvitation: joinState.spaceInvitation || true,
283
+ selectedIdentity: joinState.selectedIdentity,
284
+ active:
285
+ joinState.activeView === 'space invitation acceptor' &&
286
+ joinState.spaceViewState === 'invitation rescuer',
287
+ invitationType: 'space'
288
+ }}
289
+ />
290
+ <InvitationAuthenticator
291
+ {...{
292
+ dispatch,
293
+ activeInvitation: joinState.spaceInvitation || true,
294
+ selectedIdentity: joinState.selectedIdentity,
295
+ active:
296
+ joinState.activeView === 'space invitation acceptor' &&
297
+ joinState.spaceViewState === 'invitation authenticator',
298
+ invitationType: 'space',
299
+ ...(joinState.spaceInvitationAnnotation === 'authentication failed' && { failed: true })
300
+ }}
301
+ />
302
+ <InvitationAccepted
303
+ {...{
304
+ dispatch,
305
+ activeInvitation: joinState.spaceInvitation || true,
306
+ selectedIdentity: joinState.selectedIdentity,
307
+ active:
308
+ joinState.activeView === 'space invitation acceptor' &&
309
+ joinState.spaceViewState === 'invitation accepted',
310
+ invitationType: 'space',
311
+ doneActionParent,
312
+ onDone
313
+ }}
314
+ />
315
+ </div>
316
+ </div>
317
+ </>
318
+ );
319
+ };
@@ -0,0 +1,90 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { cloneElement, Dispatch } from 'react';
6
+
7
+ import type { AuthenticatingInvitationObservable, Profile } from '@dxos/client';
8
+ import { InvitationResult } from '@dxos/react-client';
9
+
10
+ export type JoinPanelMode = 'default' | 'halo-only';
11
+
12
+ export interface JoinPanelProps {
13
+ mode?: JoinPanelMode;
14
+ initialInvitationCode?: string;
15
+ titleId?: string;
16
+ exitActionParent?: Parameters<typeof cloneElement>[0];
17
+ onExit?: () => void;
18
+ preventExit?: boolean;
19
+ doneActionParent?: Parameters<typeof cloneElement>[0];
20
+ onDone?: (result: InvitationResult | null) => void;
21
+ parseInvitationCodeInput?: (invitationCodeInput: string) => string;
22
+ }
23
+
24
+ export interface IdentityAction {
25
+ type: 'select identity' | 'added identity';
26
+ identity: Profile;
27
+ }
28
+
29
+ export interface EmptyJoinAction {
30
+ type: 'deselect identity' | 'cancel addition' | 'add identity';
31
+ }
32
+
33
+ export interface EmptyInvitationAction {
34
+ type:
35
+ | 'authenticating invitation'
36
+ | 'authenticate invitation'
37
+ | 'cancel invitation'
38
+ | 'cancelled invitation'
39
+ | 'connect invitation'
40
+ | 'fail invitation'
41
+ | 'accepted invitation'
42
+ | 'timeout invitation'
43
+ | 'reset invitation';
44
+ from: 'space' | 'halo';
45
+ }
46
+
47
+ export interface InvitationCodeAction {
48
+ type: 'connecting invitation';
49
+ from: 'space' | 'halo';
50
+ invitation: AuthenticatingInvitationObservable;
51
+ }
52
+
53
+ export type InvitationAction = EmptyInvitationAction | InvitationCodeAction;
54
+
55
+ export interface AdditionMethodAction {
56
+ type: 'select addition method';
57
+ method: 'recover identity' | 'accept device invitation' | 'create identity';
58
+ }
59
+
60
+ export type JoinAction = IdentityAction | EmptyJoinAction | AdditionMethodAction | InvitationAction;
61
+
62
+ export type JoinDispatch = Dispatch<JoinAction>;
63
+
64
+ export type InvitationView =
65
+ | 'invitation input'
66
+ | 'invitation rescuer'
67
+ | 'invitation authenticator'
68
+ | 'invitation accepted';
69
+
70
+ export type JoinView =
71
+ | 'identity selector'
72
+ | 'addition method selector'
73
+ | 'identity input'
74
+ | 'identity added'
75
+ | 'space invitation acceptor'
76
+ | 'halo invitation acceptor';
77
+
78
+ export interface JoinState {
79
+ activeView: JoinView;
80
+ unredeemedSpaceInvitationCode?: string;
81
+ spaceInvitation?: AuthenticatingInvitationObservable;
82
+ spaceViewState: InvitationView;
83
+ spaceInvitationAnnotation?: 'authenticating' | 'authentication failed';
84
+ unredeemedHaloInvitationCode?: string;
85
+ haloInvitation?: AuthenticatingInvitationObservable;
86
+ haloViewState: InvitationView;
87
+ haloInvitationAnnotation?: 'authenticating' | 'authentication failed';
88
+ selectedIdentity?: Profile;
89
+ additionMethod?: AdditionMethodAction['method'];
90
+ }
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './JoinPanel';
6
+ export * from './JoinPanelProps';
@@ -0,0 +1,73 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, CaretRight, Plus, QrCode, Textbox } from 'phosphor-react';
6
+ import React from 'react';
7
+
8
+ import type { Profile } from '@dxos/client';
9
+ import { Button, CompoundButton, getSize, mx, useTranslation } from '@dxos/react-components';
10
+
11
+ import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
12
+
13
+ export interface AdditionMethodSelectorProps extends ViewStateProps {
14
+ availableIdentities: Profile[];
15
+ }
16
+
17
+ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps }: AdditionMethodSelectorProps) => {
18
+ const disabled = !viewStateProps.active;
19
+ const { dispatch } = viewStateProps;
20
+
21
+ const { t } = useTranslation('os');
22
+
23
+ const sharedButtonProps = {
24
+ disabled,
25
+ after: <CaretRight className={getSize(4)} weight='bold' />,
26
+ slots: { label: { className: 'text-sm' } }
27
+ };
28
+
29
+ return (
30
+ <ViewState {...viewStateProps}>
31
+ <ViewStateHeading>{t('addition method selector title')}</ViewStateHeading>
32
+ <div role='none' className='flex flex-col gap-1 grow'>
33
+ <CompoundButton
34
+ {...sharedButtonProps}
35
+ description={t('create identity description')}
36
+ before={<Plus className={getSize(6)} />}
37
+ onClick={() => dispatch({ type: 'select addition method', method: 'create identity' })}
38
+ data-autofocus='addition method selector'
39
+ data-testid='create-identity'
40
+ >
41
+ {t('create identity label')}
42
+ </CompoundButton>
43
+ <CompoundButton
44
+ {...sharedButtonProps}
45
+ description={t('join identity description')}
46
+ before={<QrCode className={getSize(6)} />}
47
+ onClick={() => dispatch({ type: 'select addition method', method: 'accept device invitation' })}
48
+ data-testid='join-identity'
49
+ >
50
+ {t('join identity label')}
51
+ </CompoundButton>
52
+ <CompoundButton
53
+ {...sharedButtonProps}
54
+ description={t('recover identity description')}
55
+ before={<Textbox className={getSize(6)} />}
56
+ onClick={() => dispatch({ type: 'select addition method', method: 'recover identity' })}
57
+ data-testid='recover-identity'
58
+ >
59
+ {t('recover identity label')}
60
+ </CompoundButton>
61
+ </div>
62
+ <Button
63
+ disabled={disabled || availableIdentities.length < 1}
64
+ onClick={() => dispatch({ type: 'deselect identity' })}
65
+ data-testid='deselect-identity'
66
+ >
67
+ <CaretLeft className={getSize(4)} weight='bold' />
68
+ <span className='grow'>{t('deselect identity label')}</span>
69
+ <CaretRight className={mx(getSize(4), 'invisible')} weight='bold' />
70
+ </Button>
71
+ </ViewState>
72
+ );
73
+ };
@@ -0,0 +1,99 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { CaretLeft, CaretRight, Check } from 'phosphor-react';
6
+ import React, { cloneElement } from 'react';
7
+
8
+ import type { Profile } from '@dxos/client';
9
+ import { InvitationResult } from '@dxos/react-client';
10
+ import { Avatar, Button, getSize, mx, useTranslation } from '@dxos/react-components';
11
+
12
+ import { JoinPanelMode } from '../JoinPanelProps';
13
+ import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
14
+
15
+ export interface IdentityAddedProps extends ViewStateProps, DoneProps {
16
+ mode?: JoinPanelMode;
17
+ addedIdentity?: Profile;
18
+ }
19
+
20
+ export interface DoneProps extends ViewStateProps {
21
+ doneActionParent?: Parameters<typeof cloneElement>[0];
22
+ onDone?: (result: InvitationResult | null) => void;
23
+ }
24
+
25
+ const Done = ({ onDone, doneActionParent, active }: DoneProps) => {
26
+ const disabled = !active;
27
+ const { t } = useTranslation('os');
28
+
29
+ const doneButton = (
30
+ <Button
31
+ {...(onDone && { onClick: () => onDone(null) })}
32
+ disabled={disabled}
33
+ className='grow flex items-center gap-2 pli-2'
34
+ data-autofocus='identity added'
35
+ data-testid='identity-added-done'
36
+ >
37
+ <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
38
+ <span className='grow'>{t('done label')}</span>
39
+ <Check weight='bold' className={getSize(4)} />
40
+ </Button>
41
+ );
42
+
43
+ return doneActionParent ? cloneElement(doneActionParent, {}, doneButton) : doneButton;
44
+ };
45
+
46
+ export const IdentityAdded = ({
47
+ mode,
48
+ addedIdentity,
49
+ onDone,
50
+ doneActionParent,
51
+ ...viewStateProps
52
+ }: IdentityAddedProps) => {
53
+ const disabled = !viewStateProps.active;
54
+ const { dispatch } = viewStateProps;
55
+ const { t } = useTranslation('os');
56
+
57
+ return (
58
+ <ViewState {...viewStateProps}>
59
+ <ViewStateHeading>{t('identity added label')}</ViewStateHeading>
60
+ <div role='none' className='grow flex flex-col items-center justify-center text-center gap-2'>
61
+ <Avatar
62
+ size={20}
63
+ fallbackValue={addedIdentity?.identityKey.toHex() ?? ''}
64
+ label={
65
+ <p className={mx('text-lg', !addedIdentity?.displayName && 'font-mono')}>
66
+ {addedIdentity?.displayName ?? addedIdentity?.identityKey.truncate() ?? ' '}
67
+ </p>
68
+ }
69
+ variant='circle'
70
+ status='active'
71
+ />
72
+ </div>
73
+ {mode === 'halo-only' ? (
74
+ <Done onDone={onDone} doneActionParent={doneActionParent} {...viewStateProps} />
75
+ ) : (
76
+ <div className='flex gap-2'>
77
+ <Button
78
+ disabled={disabled || !addedIdentity}
79
+ className='grow flex items-center gap-2 pli-2 order-2'
80
+ onClick={() => addedIdentity && dispatch({ type: 'select identity', identity: addedIdentity })}
81
+ data-autofocus='identity added'
82
+ >
83
+ <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
84
+ <span className='grow'>{t('continue label')}</span>
85
+ <CaretRight weight='bold' className={getSize(4)} />
86
+ </Button>
87
+ <Button
88
+ disabled={disabled}
89
+ onClick={() => dispatch({ type: 'deselect identity' })}
90
+ className='flex items-center gap-2 pis-2 pie-4'
91
+ >
92
+ <CaretLeft weight='bold' className={getSize(4)} />
93
+ <span>{t('deselect identity label')}</span>
94
+ </Button>
95
+ </div>
96
+ )}
97
+ </ViewState>
98
+ );
99
+ };