@dxos/plugin-space 0.6.8-main.046e6cf

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 (109) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +15 -0
  3. package/dist/lib/browser/chunk-DTVUOG2C.mjs +95 -0
  4. package/dist/lib/browser/chunk-DTVUOG2C.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-LZEGRS7H.mjs +35 -0
  6. package/dist/lib/browser/chunk-LZEGRS7H.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +2660 -0
  8. package/dist/lib/browser/index.mjs.map +7 -0
  9. package/dist/lib/browser/meta.json +1 -0
  10. package/dist/lib/browser/meta.mjs +13 -0
  11. package/dist/lib/browser/meta.mjs.map +7 -0
  12. package/dist/lib/browser/types/index.mjs +21 -0
  13. package/dist/lib/browser/types/index.mjs.map +7 -0
  14. package/dist/lib/node/chunk-6CNYF6YU.cjs +60 -0
  15. package/dist/lib/node/chunk-6CNYF6YU.cjs.map +7 -0
  16. package/dist/lib/node/chunk-CVZPI2P3.cjs +120 -0
  17. package/dist/lib/node/chunk-CVZPI2P3.cjs.map +7 -0
  18. package/dist/lib/node/index.cjs +2682 -0
  19. package/dist/lib/node/index.cjs.map +7 -0
  20. package/dist/lib/node/meta.cjs +34 -0
  21. package/dist/lib/node/meta.cjs.map +7 -0
  22. package/dist/lib/node/meta.json +1 -0
  23. package/dist/lib/node/types/index.cjs +43 -0
  24. package/dist/lib/node/types/index.cjs.map +7 -0
  25. package/dist/types/src/SpacePlugin.d.ts +27 -0
  26. package/dist/types/src/SpacePlugin.d.ts.map +1 -0
  27. package/dist/types/src/components/AwaitingObject.d.ts +5 -0
  28. package/dist/types/src/components/AwaitingObject.d.ts.map +1 -0
  29. package/dist/types/src/components/CollectionMain.d.ts +6 -0
  30. package/dist/types/src/components/CollectionMain.d.ts.map +1 -0
  31. package/dist/types/src/components/CollectionSection.d.ts +6 -0
  32. package/dist/types/src/components/CollectionSection.d.ts.map +1 -0
  33. package/dist/types/src/components/EmptySpace.d.ts +3 -0
  34. package/dist/types/src/components/EmptySpace.d.ts.map +1 -0
  35. package/dist/types/src/components/EmptyTree.d.ts +3 -0
  36. package/dist/types/src/components/EmptyTree.d.ts.map +1 -0
  37. package/dist/types/src/components/MenuFooter.d.ts +6 -0
  38. package/dist/types/src/components/MenuFooter.d.ts.map +1 -0
  39. package/dist/types/src/components/MissingObject.d.ts +5 -0
  40. package/dist/types/src/components/MissingObject.d.ts.map +1 -0
  41. package/dist/types/src/components/PersistenceStatus.d.ts +6 -0
  42. package/dist/types/src/components/PersistenceStatus.d.ts.map +1 -0
  43. package/dist/types/src/components/PopoverRenameObject.d.ts +6 -0
  44. package/dist/types/src/components/PopoverRenameObject.d.ts.map +1 -0
  45. package/dist/types/src/components/PopoverRenameSpace.d.ts +6 -0
  46. package/dist/types/src/components/PopoverRenameSpace.d.ts.map +1 -0
  47. package/dist/types/src/components/ShareSpaceButton.d.ts +8 -0
  48. package/dist/types/src/components/ShareSpaceButton.d.ts.map +1 -0
  49. package/dist/types/src/components/ShareSpaceButton.stories.d.ts +98 -0
  50. package/dist/types/src/components/ShareSpaceButton.stories.d.ts.map +1 -0
  51. package/dist/types/src/components/SpaceMain/SpaceMain.d.ts +10 -0
  52. package/dist/types/src/components/SpaceMain/SpaceMain.d.ts.map +1 -0
  53. package/dist/types/src/components/SpaceMain/SpaceMembersSection.d.ts +6 -0
  54. package/dist/types/src/components/SpaceMain/SpaceMembersSection.d.ts.map +1 -0
  55. package/dist/types/src/components/SpaceMain/index.d.ts +2 -0
  56. package/dist/types/src/components/SpaceMain/index.d.ts.map +1 -0
  57. package/dist/types/src/components/SpacePresence.d.ts +34 -0
  58. package/dist/types/src/components/SpacePresence.d.ts.map +1 -0
  59. package/dist/types/src/components/SpacePresence.stories.d.ts +97 -0
  60. package/dist/types/src/components/SpacePresence.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/SpaceSettings.d.ts +6 -0
  62. package/dist/types/src/components/SpaceSettings.d.ts.map +1 -0
  63. package/dist/types/src/components/index.d.ts +15 -0
  64. package/dist/types/src/components/index.d.ts.map +1 -0
  65. package/dist/types/src/index.d.ts +9 -0
  66. package/dist/types/src/index.d.ts.map +1 -0
  67. package/dist/types/src/meta.d.ts +26 -0
  68. package/dist/types/src/meta.d.ts.map +1 -0
  69. package/dist/types/src/translations.d.ts +83 -0
  70. package/dist/types/src/translations.d.ts.map +1 -0
  71. package/dist/types/src/types/collection.d.ts +18 -0
  72. package/dist/types/src/types/collection.d.ts.map +1 -0
  73. package/dist/types/src/types/index.d.ts +4 -0
  74. package/dist/types/src/types/index.d.ts.map +1 -0
  75. package/dist/types/src/types/thread.d.ts +225 -0
  76. package/dist/types/src/types/thread.d.ts.map +1 -0
  77. package/dist/types/src/types/types.d.ts +54 -0
  78. package/dist/types/src/types/types.d.ts.map +1 -0
  79. package/dist/types/src/util.d.ts +85 -0
  80. package/dist/types/src/util.d.ts.map +1 -0
  81. package/package.json +101 -0
  82. package/src/SpacePlugin.tsx +1234 -0
  83. package/src/components/AwaitingObject.tsx +118 -0
  84. package/src/components/CollectionMain.tsx +33 -0
  85. package/src/components/CollectionSection.tsx +20 -0
  86. package/src/components/EmptySpace.tsx +25 -0
  87. package/src/components/EmptyTree.tsx +25 -0
  88. package/src/components/MenuFooter.tsx +33 -0
  89. package/src/components/MissingObject.tsx +54 -0
  90. package/src/components/PersistenceStatus.tsx +87 -0
  91. package/src/components/PopoverRenameObject.tsx +54 -0
  92. package/src/components/PopoverRenameSpace.tsx +44 -0
  93. package/src/components/ShareSpaceButton.stories.tsx +23 -0
  94. package/src/components/ShareSpaceButton.tsx +27 -0
  95. package/src/components/SpaceMain/SpaceMain.tsx +81 -0
  96. package/src/components/SpaceMain/SpaceMembersSection.tsx +205 -0
  97. package/src/components/SpaceMain/index.ts +5 -0
  98. package/src/components/SpacePresence.stories.tsx +102 -0
  99. package/src/components/SpacePresence.tsx +244 -0
  100. package/src/components/SpaceSettings.tsx +34 -0
  101. package/src/components/index.ts +18 -0
  102. package/src/index.ts +15 -0
  103. package/src/meta.ts +31 -0
  104. package/src/translations.ts +92 -0
  105. package/src/types/collection.ts +16 -0
  106. package/src/types/index.ts +7 -0
  107. package/src/types/thread.ts +68 -0
  108. package/src/types/types.ts +81 -0
  109. package/src/util.tsx +642 -0
@@ -0,0 +1,205 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { CaretDown, Check, UserPlus, UsersThree } from '@phosphor-icons/react';
6
+ import React, { useCallback, useState } from 'react';
7
+
8
+ import { LayoutAction, useIntent } from '@dxos/app-framework';
9
+ import { type Space, useMembers, SpaceMember, useSpaceInvitations } from '@dxos/react-client/echo';
10
+ import { type CancellableInvitationObservable, InvitationEncoder } from '@dxos/react-client/invitations';
11
+ import { Invitation } from '@dxos/react-client/invitations';
12
+ import { Button, ButtonGroup, DropdownMenu, List, useTranslation } from '@dxos/react-ui';
13
+ import { descriptionText, getSize, mx } from '@dxos/react-ui-theme';
14
+ import { InvitationListItem, IdentityListItem } from '@dxos/shell/react';
15
+
16
+ import { SPACE_PLUGIN } from '../../meta';
17
+
18
+ // TODO(thure): Sync with shell?
19
+ const activeActionKeyStorageKey = 'dxos:react-shell/space-manager/active-action';
20
+
21
+ const Presence = SpaceMember.PresenceState;
22
+
23
+ const handleCreateInvitationUrl = (invitationCode: string) => `${origin}?spaceInvitationCode=${invitationCode}`;
24
+
25
+ const SpaceMemberList = ({ members }: { members: SpaceMember[] }) => {
26
+ return members.length > 0 ? (
27
+ <List classNames='col-start-2 col-end-5 gap-y-1 grid grid-cols-subgrid items-center'>
28
+ {members.map((member) => (
29
+ <IdentityListItem
30
+ classNames='contents'
31
+ key={member.identity.identityKey.toHex()}
32
+ identity={member.identity}
33
+ presence={member.presence}
34
+ />
35
+ ))}
36
+ </List>
37
+ ) : null;
38
+ };
39
+
40
+ export const SpaceMembersSection = ({ space }: { space: Space }) => {
41
+ const { t } = useTranslation(SPACE_PLUGIN);
42
+ const invitations = useSpaceInvitations(space.key);
43
+ const { dispatch } = useIntent();
44
+
45
+ const handleCloseDialog = () =>
46
+ dispatch({
47
+ action: LayoutAction.SET_LAYOUT,
48
+ data: { element: 'dialog', state: false },
49
+ });
50
+
51
+ const handleInvitationSelect = ({
52
+ invitation: invitationObservable,
53
+ }: {
54
+ invitation: CancellableInvitationObservable;
55
+ }) => {
56
+ const invitation = invitationObservable.get();
57
+ void dispatch({
58
+ action: LayoutAction.SET_LAYOUT,
59
+ data: {
60
+ element: 'dialog',
61
+ component: 'dxos.org/plugin/space/InvitationManagerDialog',
62
+ subject: {
63
+ invitationUrl: handleCreateInvitationUrl(InvitationEncoder.encode(invitation)),
64
+ send: handleCloseDialog,
65
+ status: invitation.state,
66
+ type: invitation.type,
67
+ authCode: invitation.authCode,
68
+ id: invitation.invitationId,
69
+ },
70
+ },
71
+ });
72
+ };
73
+
74
+ const inviteActions = {
75
+ inviteOne: {
76
+ label: t('invite one label', { ns: 'os' }),
77
+ description: t('invite one description', { ns: 'os' }),
78
+ icon: UserPlus,
79
+ onClick: useCallback(() => {
80
+ space.share?.({
81
+ type: Invitation.Type.INTERACTIVE,
82
+ authMethod: Invitation.AuthMethod.SHARED_SECRET,
83
+ });
84
+ }, [space]),
85
+ },
86
+ inviteMany: {
87
+ label: t('invite many label', { ns: 'os' }),
88
+ description: t('invite many description', { ns: 'os' }),
89
+ icon: UsersThree,
90
+ onClick: useCallback(() => {
91
+ space.share?.({
92
+ type: Invitation.Type.INTERACTIVE,
93
+ authMethod: Invitation.AuthMethod.NONE,
94
+ multiUse: true,
95
+ });
96
+ }, [space]),
97
+ },
98
+ };
99
+
100
+ const [activeActionKey, setInternalActiveActionKey] = useState<keyof typeof inviteActions>(
101
+ (localStorage.getItem(activeActionKeyStorageKey) as keyof typeof inviteActions) ?? 'inviteOne',
102
+ );
103
+ const setActiveActionKey = (nextKey: keyof typeof inviteActions) => {
104
+ setInternalActiveActionKey(nextKey);
105
+ localStorage.setItem(activeActionKeyStorageKey, nextKey);
106
+ };
107
+
108
+ const activeAction = inviteActions[activeActionKey as keyof typeof inviteActions] ?? {};
109
+
110
+ // TODO(thure): Simplify when Object.groupBy() is supported by Safari
111
+ // https://caniuse.com/mdn-javascript_builtins_object_groupby
112
+ const members = useMembers(space.key).reduce(
113
+ (acc: Record<SpaceMember.PresenceState, SpaceMember[]>, member) => {
114
+ acc[member.presence].push(member);
115
+ return acc;
116
+ },
117
+ {
118
+ [Presence.ONLINE]: [],
119
+ [Presence.OFFLINE]: [],
120
+ },
121
+ );
122
+
123
+ return (
124
+ <section className='mbe-4 col-span-3 grid gap-y-2 grid-cols-subgrid auto-rows-min'>
125
+ <h2 className='contents'>
126
+ <UsersThree weight='duotone' className={mx(getSize(5), 'place-self-center')} />
127
+ <span className='text-lg col-span-2'>{t('space members label')}</span>
128
+ </h2>
129
+ <h3 className='col-start-2 col-span-3 text-sm italic fg-description'>{t('invitations heading')}</h3>
130
+ {invitations.length > 0 && (
131
+ <List classNames='col-start-2 col-span-2 gap-y-2 grid grid-cols-[var(--rail-size)_1fr_var(--rail-action)_var(--rail-action)]'>
132
+ {invitations.map((invitation) => (
133
+ <InvitationListItem
134
+ reverseEffects
135
+ classNames='pis-0 pie-0 gap-0 col-span-4 grid grid-cols-subgrid'
136
+ key={invitation.get().invitationId}
137
+ invitation={invitation}
138
+ send={handleInvitationSelect}
139
+ createInvitationUrl={handleCreateInvitationUrl}
140
+ />
141
+ ))}
142
+ </List>
143
+ )}
144
+ <ButtonGroup classNames='col-start-2 col-end-4 grid grid-cols-[1fr_var(--rail-action)] place-self-grow gap-px'>
145
+ <Button classNames='gap-2' onClick={activeAction.onClick}>
146
+ <activeAction.icon className={getSize(5)} />
147
+ <span>{t(activeAction.label, { ns: 'os' })}</span>
148
+ </Button>
149
+ <DropdownMenu.Root>
150
+ <DropdownMenu.Trigger asChild>
151
+ <Button classNames='pli-0'>
152
+ <CaretDown className={getSize(4)} />
153
+ </Button>
154
+ </DropdownMenu.Trigger>
155
+ <DropdownMenu.Content>
156
+ <DropdownMenu.Viewport>
157
+ {Object.entries(inviteActions).map(([id, action]) => {
158
+ return (
159
+ <DropdownMenu.CheckboxItem
160
+ key={id}
161
+ aria-labelledby={`${id}__label`}
162
+ aria-describedby={`${id}__description`}
163
+ checked={activeActionKey === id}
164
+ onCheckedChange={(checked) => checked && setActiveActionKey(id as keyof typeof inviteActions)}
165
+ classNames='gap-2'
166
+ >
167
+ {action.icon && <action.icon className={getSize(5)} />}
168
+ <div role='none' className='flex-1 min-is-0 space-b-1'>
169
+ <p id={`${id}__label`}>{t(action.label, { ns: 'os' })}</p>
170
+ {action.description && (
171
+ <p id={`${id}__description`} className={descriptionText}>
172
+ {t(action.description, { ns: 'os' })}
173
+ </p>
174
+ )}
175
+ </div>
176
+ <DropdownMenu.ItemIndicator asChild>
177
+ <Check className={getSize(4)} />
178
+ </DropdownMenu.ItemIndicator>
179
+ </DropdownMenu.CheckboxItem>
180
+ );
181
+ })}
182
+ </DropdownMenu.Viewport>
183
+ <DropdownMenu.Arrow />
184
+ </DropdownMenu.Content>
185
+ </DropdownMenu.Root>
186
+ </ButtonGroup>
187
+ {members[Presence.ONLINE].length + members[Presence.OFFLINE].length < 1 ? (
188
+ <p className={mx(descriptionText, 'text-center is-full mlb-2')}>
189
+ {t('empty space members message', { ns: 'os' })}
190
+ </p>
191
+ ) : (
192
+ <>
193
+ <h3 className='col-start-2 col-end-5 text-sm italic fg-description'>
194
+ {t('active space members heading', { count: members[Presence.ONLINE].length })}
195
+ </h3>
196
+ <SpaceMemberList members={members[Presence.ONLINE]} />
197
+ <h3 className='col-start-2 col-end-5 text-sm italic fg-description'>
198
+ {t('inactive space members heading', { count: members[Presence.OFFLINE].length })}
199
+ </h3>
200
+ <SpaceMemberList members={members[Presence.OFFLINE]} />
201
+ </>
202
+ )}
203
+ </section>
204
+ );
205
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './SpaceMain';
@@ -0,0 +1,102 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import '@dxosTheme';
8
+
9
+ import { PublicKey } from '@dxos/keys';
10
+ import { HaloSpaceMember, SpaceMember } from '@dxos/react-client/echo';
11
+ import { Tooltip } from '@dxos/react-ui';
12
+ import { withTheme } from '@dxos/storybook-utils';
13
+
14
+ import { FullPresence, type MemberPresenceProps, SmallPresence, type Member } from './SpacePresence';
15
+ import translations from '../translations';
16
+
17
+ export default {
18
+ title: 'plugin-space/SpacePresence',
19
+ decorators: [withTheme],
20
+ parameters: { translations },
21
+ actions: { argTypesRegex: '^on.*' },
22
+ };
23
+
24
+ const nViewers = (n: number, currentlyAttended = true): Member[] =>
25
+ Array.from({ length: n }, () => ({
26
+ role: HaloSpaceMember.Role.ADMIN,
27
+ identity: { identityKey: PublicKey.random() },
28
+ presence: SpaceMember.PresenceState.ONLINE,
29
+ lastSeen: Date.now(),
30
+ currentlyAttended,
31
+ }));
32
+
33
+ export const Full = (props: MemberPresenceProps) => {
34
+ const p: MemberPresenceProps = {
35
+ ...props,
36
+ };
37
+
38
+ return (
39
+ <Tooltip.Provider>
40
+ <div className='p-4'>
41
+ <div className='p-3'>
42
+ <FullPresence members={nViewers(1)} {...p} />
43
+ </div>
44
+ <div className='p-3'>
45
+ <FullPresence members={nViewers(2)} {...p} />
46
+ </div>
47
+ <div className='p-3'>
48
+ <FullPresence members={nViewers(3)} {...p} />
49
+ </div>
50
+ <div className='p-3'>
51
+ <FullPresence members={nViewers(3, false)} {...p} />
52
+ </div>
53
+ <div className='p-3'>
54
+ <FullPresence members={nViewers(4)} {...p} />
55
+ </div>
56
+ <div className='p-3'>
57
+ <FullPresence members={nViewers(5)} {...p} />
58
+ </div>
59
+ <div className='p-3'>
60
+ <FullPresence members={nViewers(5, false)} {...p} />
61
+ </div>
62
+ <div className='p-3'>
63
+ <FullPresence members={nViewers(10)} {...p} />
64
+ </div>
65
+ <div className='p-3'>
66
+ <FullPresence members={nViewers(100)} {...p} />
67
+ </div>
68
+ </div>
69
+ </Tooltip.Provider>
70
+ );
71
+ };
72
+
73
+ export const Small = (props: MemberPresenceProps) => {
74
+ const p: MemberPresenceProps = {
75
+ ...props,
76
+ };
77
+
78
+ return (
79
+ <Tooltip.Provider>
80
+ <div className='p-4'>
81
+ <div className='p-3'>
82
+ <SmallPresence count={0} {...p} />
83
+ </div>
84
+ <div className='p-3'>
85
+ <SmallPresence count={1} {...p} />
86
+ </div>
87
+ <div className='p-3'>
88
+ <SmallPresence count={2} {...p} />
89
+ </div>
90
+ <div className='p-3'>
91
+ <SmallPresence count={3} {...p} />
92
+ </div>
93
+ <div className='p-3'>
94
+ <SmallPresence count={4} {...p} />
95
+ </div>
96
+ <div className='p-3'>
97
+ <SmallPresence count={5} {...p} />
98
+ </div>
99
+ </div>
100
+ </Tooltip.Provider>
101
+ );
102
+ };
@@ -0,0 +1,244 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useEffect, useState } from 'react';
6
+
7
+ import { usePlugin } from '@dxos/app-framework';
8
+ import { generateName } from '@dxos/display-name';
9
+ import { type Expando } from '@dxos/echo-schema';
10
+ import { PublicKey, useClient } from '@dxos/react-client';
11
+ import { getSpace, useMembers, type SpaceMember, fullyQualifiedId } from '@dxos/react-client/echo';
12
+ import { type Identity, useIdentity } from '@dxos/react-client/halo';
13
+ import {
14
+ Avatar,
15
+ AvatarGroup,
16
+ AvatarGroupItem,
17
+ type Size,
18
+ type ThemedClassName,
19
+ Tooltip,
20
+ useDensityContext,
21
+ useTranslation,
22
+ List,
23
+ ListItem,
24
+ useDefaultValue,
25
+ } from '@dxos/react-ui';
26
+ import { AttentionGlyphCloseButton } from '@dxos/react-ui-attention';
27
+ import { ComplexMap, keyToFallback } from '@dxos/util';
28
+
29
+ import { SPACE_PLUGIN } from '../meta';
30
+ import type { ObjectViewerProps, SpacePluginProvides } from '../types';
31
+
32
+ // TODO(thure): Get/derive these values from protocol
33
+ const REFRESH_INTERVAL = 5000;
34
+ const ACTIVITY_DURATION = 30_000;
35
+
36
+ // TODO(thure): This is chiefly meant to satisfy TS & provide an empty map after `deepSignal` interactions.
37
+ const noViewers = new ComplexMap<PublicKey, ObjectViewerProps>(PublicKey.hash);
38
+
39
+ // TODO(wittjosiah): Factor out?
40
+ const getName = (identity: Identity) => identity.profile?.displayName ?? generateName(identity.identityKey.toHex());
41
+
42
+ export const SpacePresence = ({ object, spaceKey }: { object: Expando; spaceKey?: PublicKey }) => {
43
+ const density = useDensityContext();
44
+ const spacePlugin = usePlugin<SpacePluginProvides>(SPACE_PLUGIN);
45
+ const client = useClient();
46
+ const identity = useIdentity();
47
+ const space = spaceKey ? client.spaces.get(spaceKey) : getSpace(object);
48
+ const spaceMembers = useMembers(space?.key);
49
+
50
+ const [_moment, setMoment] = useState(Date.now());
51
+
52
+ // NOTE(thure): This is necessary so Presence updates without any underlying data updating.
53
+ useEffect(() => {
54
+ const interval = setInterval(() => setMoment(Date.now()), REFRESH_INTERVAL);
55
+ return () => clearInterval(interval);
56
+ }, []);
57
+
58
+ const memberOnline = useCallback((member: SpaceMember) => member.presence === 1, []);
59
+ const memberIsNotSelf = useCallback(
60
+ (member: SpaceMember) => !identity?.identityKey.equals(member.identity.identityKey),
61
+ [identity?.identityKey],
62
+ );
63
+
64
+ // TODO(thure): Could it be a smell to return early when there are interactions with `deepSignal` later, since it
65
+ // prevents reactivity?
66
+ if (!identity || !spacePlugin || !space) {
67
+ return null;
68
+ }
69
+
70
+ const spaceState = spacePlugin.provides.space;
71
+ const currentObjectViewers = spaceState.viewersByObject[fullyQualifiedId(object)] ?? noViewers;
72
+
73
+ const membersForObject = spaceMembers
74
+ .filter((member) => memberOnline(member) && memberIsNotSelf(member))
75
+ .filter((member) => currentObjectViewers.has(member.identity.identityKey))
76
+ .map((member) => {
77
+ const objectView = currentObjectViewers.get(member.identity.identityKey);
78
+ const lastSeen = objectView?.lastSeen ?? -Infinity;
79
+ const currentlyAttended = objectView?.currentlyAttended ?? false;
80
+
81
+ return {
82
+ ...member,
83
+ currentlyAttended,
84
+ lastSeen,
85
+ };
86
+ })
87
+ .toSorted((a, b) => a.lastSeen - b.lastSeen);
88
+
89
+ return density === 'fine' ? (
90
+ <SmallPresence count={membersForObject.length} />
91
+ ) : (
92
+ <FullPresence members={membersForObject} />
93
+ );
94
+ };
95
+
96
+ export type Member = SpaceMember & {
97
+ /**
98
+ * Last time a member was seen on this object.
99
+ */
100
+ lastSeen: number;
101
+ currentlyAttended: boolean;
102
+ };
103
+
104
+ export type MemberPresenceProps = ThemedClassName<{
105
+ size?: Size;
106
+ members?: Member[];
107
+ showCount?: boolean;
108
+ onMemberClick?: (member: Member) => void;
109
+ }>;
110
+
111
+ export const FullPresence = (props: MemberPresenceProps) => {
112
+ const { size = 9, onMemberClick } = props;
113
+ const members = useDefaultValue(props.members, []);
114
+
115
+ if (members.length === 0) {
116
+ return null;
117
+ }
118
+
119
+ return (
120
+ <AvatarGroup.Root size={size} classNames='mbs-2 mie-4' data-testid='spacePlugin.presence'>
121
+ {members.slice(0, 3).map((member, i) => (
122
+ <Tooltip.Root key={member.identity.identityKey.toHex()}>
123
+ <Tooltip.Trigger>
124
+ <PrensenceAvatar
125
+ identity={member.identity}
126
+ group
127
+ match={member.currentlyAttended} // TODO(Zan): Match always true now we're showing 'members viewing current object'.
128
+ index={members.length - i}
129
+ onClick={() => onMemberClick?.(member)}
130
+ />
131
+ </Tooltip.Trigger>
132
+ <Tooltip.Portal>
133
+ <Tooltip.Content side='bottom'>
134
+ <span>{getName(member.identity)}</span>
135
+ <Tooltip.Arrow />
136
+ </Tooltip.Content>
137
+ </Tooltip.Portal>
138
+ </Tooltip.Root>
139
+ ))}
140
+
141
+ {members.length > 3 && (
142
+ <Tooltip.Root>
143
+ <Tooltip.Trigger>
144
+ <AvatarGroupItem.Root status='inactive'>
145
+ <Avatar.Frame style={{ zIndex: members.length - 4 }}>
146
+ {/* TODO(wittjosiah): Make text fit. */}
147
+ <Avatar.Fallback text={`+${members.length - 3}`} />
148
+ </Avatar.Frame>
149
+ </AvatarGroupItem.Root>
150
+ </Tooltip.Trigger>
151
+ <Tooltip.Portal>
152
+ <Tooltip.Content side='bottom'>
153
+ <Tooltip.Arrow />
154
+ <List classNames='max-h-56 overflow-y-auto'>
155
+ {members.map((member) => (
156
+ <ListItem.Root
157
+ key={member.identity.identityKey.toHex()}
158
+ classNames='flex gap-2 items-center cursor-pointer mbe-2'
159
+ onClick={() => onMemberClick?.(member)}
160
+ data-testid='identity-list-item'
161
+ >
162
+ {/* TODO(Zan): Match always true now we're showing 'members viewing current object'. */}
163
+ <PrensenceAvatar identity={member.identity} showName match={member.currentlyAttended} />
164
+ </ListItem.Root>
165
+ ))}
166
+ </List>
167
+ </Tooltip.Content>
168
+ </Tooltip.Portal>
169
+ </Tooltip.Root>
170
+ )}
171
+ </AvatarGroup.Root>
172
+ );
173
+ };
174
+
175
+ type PresenceAvatarProps = {
176
+ identity: Identity;
177
+ showName?: boolean;
178
+ match?: boolean;
179
+ group?: boolean;
180
+ index?: number;
181
+ onClick?: () => void;
182
+ };
183
+
184
+ const PrensenceAvatar = ({ identity, showName, match, group, index, onClick }: PresenceAvatarProps) => {
185
+ const Root = group ? AvatarGroupItem.Root : Avatar.Root;
186
+ const status = match ? 'current' : 'active';
187
+ const fallbackValue = keyToFallback(identity.identityKey);
188
+ return (
189
+ <Root status={status} hue={identity.profile?.data?.hue || fallbackValue.hue}>
190
+ <Avatar.Frame
191
+ data-testid='spacePlugin.presence.member'
192
+ data-status={status}
193
+ {...(index ? { style: { zIndex: index } } : {})}
194
+ onClick={() => onClick?.()}
195
+ >
196
+ <Avatar.Fallback text={identity.profile?.data?.emoji || fallbackValue.emoji} />
197
+ </Avatar.Frame>
198
+ {showName && <Avatar.Label classNames='text-sm truncate pli-2'>{getName(identity)}</Avatar.Label>}
199
+ </Root>
200
+ );
201
+ };
202
+
203
+ export const SmallPresenceLive = ({
204
+ viewers,
205
+ onCloseClick,
206
+ }: {
207
+ viewers?: ComplexMap<PublicKey, ObjectViewerProps>;
208
+ onCloseClick?: () => void;
209
+ }) => {
210
+ const [moment, setMoment] = useState(Date.now());
211
+
212
+ // NOTE(thure): This is necessary so Presence updates without any underlying data updating.
213
+ useEffect(() => {
214
+ const interval = setInterval(() => setMoment(Date.now()), REFRESH_INTERVAL);
215
+ return () => clearInterval(interval);
216
+ }, []);
217
+
218
+ const activeViewers = viewers
219
+ ? Array.from(viewers.values()).filter(({ lastSeen }) => moment - lastSeen < ACTIVITY_DURATION)
220
+ : [];
221
+
222
+ return <SmallPresence count={activeViewers.length} onCloseClick={onCloseClick} />;
223
+ };
224
+
225
+ export const SmallPresence = ({ count, onCloseClick }: { count: number; onCloseClick?: () => void }) => {
226
+ const { t } = useTranslation(SPACE_PLUGIN);
227
+ return (
228
+ <Tooltip.Root>
229
+ <Tooltip.Trigger asChild>
230
+ <AttentionGlyphCloseButton
231
+ presence={count > 1 ? 'many' : count === 1 ? 'one' : 'none'}
232
+ classNames='self-center mie-1'
233
+ onClick={onCloseClick}
234
+ />
235
+ </Tooltip.Trigger>
236
+ <Tooltip.Portal>
237
+ <Tooltip.Content side='bottom' classNames='z-[70]'>
238
+ <span>{t('presence label', { count })}</span>
239
+ <Tooltip.Arrow />
240
+ </Tooltip.Content>
241
+ </Tooltip.Portal>
242
+ </Tooltip.Root>
243
+ );
244
+ };
@@ -0,0 +1,34 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { useIntentDispatcher } from '@dxos/app-framework';
8
+ import { SettingsValue } from '@dxos/plugin-settings';
9
+ import { Input, useTranslation } from '@dxos/react-ui';
10
+
11
+ import { SpaceAction, SPACE_PLUGIN } from '../meta';
12
+ import { type SpaceSettingsProps } from '../types';
13
+
14
+ export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) => {
15
+ const { t } = useTranslation(SPACE_PLUGIN);
16
+ const dispatch = useIntentDispatcher();
17
+
18
+ return (
19
+ <>
20
+ <SettingsValue label={t('show hidden spaces label')}>
21
+ <Input.Switch
22
+ checked={settings.showHidden}
23
+ onCheckedChange={(checked) =>
24
+ dispatch({
25
+ plugin: SPACE_PLUGIN,
26
+ action: SpaceAction.TOGGLE_HIDDEN,
27
+ data: { state: !!checked },
28
+ })
29
+ }
30
+ />
31
+ </SettingsValue>
32
+ </>
33
+ );
34
+ };
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './AwaitingObject';
6
+ export * from './CollectionMain';
7
+ export * from './CollectionSection';
8
+ export * from './EmptySpace';
9
+ export * from './EmptyTree';
10
+ export * from './MenuFooter';
11
+ export * from './MissingObject';
12
+ export * from './PersistenceStatus';
13
+ export * from './PopoverRenameObject';
14
+ export * from './PopoverRenameSpace';
15
+ export * from './ShareSpaceButton';
16
+ export * from './SpaceMain';
17
+ export * from './SpacePresence';
18
+ export * from './SpaceSettings';
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { SpacePlugin } from './SpacePlugin';
6
+
7
+ export default SpacePlugin;
8
+
9
+ export * from './components';
10
+ export * from './meta';
11
+ export * from './types';
12
+ export * from './util';
13
+ export { default as translations } from './translations';
14
+
15
+ export * from './SpacePlugin';
package/src/meta.ts ADDED
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export const SPACE_PLUGIN = 'dxos.org/plugin/space';
6
+ export const SPACE_PLUGIN_SHORT_ID = 'space';
7
+
8
+ export default {
9
+ id: SPACE_PLUGIN,
10
+ shortId: SPACE_PLUGIN_SHORT_ID,
11
+ name: 'Spaces',
12
+ };
13
+
14
+ const SPACE_ACTION = `${SPACE_PLUGIN}/action`;
15
+ export enum SpaceAction {
16
+ CREATE = `${SPACE_ACTION}/create`,
17
+ JOIN = `${SPACE_ACTION}/join`,
18
+ SHARE = `${SPACE_ACTION}/share`,
19
+ LOCK = `${SPACE_ACTION}/lock`,
20
+ UNLOCK = `${SPACE_ACTION}/unlock`,
21
+ RENAME = `${SPACE_ACTION}/rename`,
22
+ OPEN = `${SPACE_ACTION}/open`,
23
+ CLOSE = `${SPACE_ACTION}/close`,
24
+ MIGRATE = `${SPACE_ACTION}/migrate`,
25
+ ADD_OBJECT = `${SPACE_ACTION}/add-object`,
26
+ REMOVE_OBJECT = `${SPACE_ACTION}/remove-object`,
27
+ RENAME_OBJECT = `${SPACE_ACTION}/rename-object`,
28
+ DUPLICATE_OBJECT = `${SPACE_ACTION}/duplicate-object`,
29
+ WAIT_FOR_OBJECT = `${SPACE_ACTION}/wait-for-object`,
30
+ TOGGLE_HIDDEN = `${SPACE_ACTION}/toggle-hidden`,
31
+ }