@oxy.so/services 12.9.0 → 12.10.1

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 (45) hide show
  1. package/lib/commonjs/ui/components/OxyAuthChooser.js +8 -1
  2. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  3. package/lib/commonjs/ui/context/OxyContext.js +15 -2
  4. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  5. package/lib/commonjs/ui/hooks/queries/useFileQueries.js +1 -1
  6. package/lib/commonjs/ui/hooks/queries/useFileQueries.js.map +1 -1
  7. package/lib/commonjs/ui/screens/FileManagementScreen.js +20 -10
  8. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  9. package/lib/commonjs/ui/screens/ManageAccountScreen.js +11 -3
  10. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  11. package/lib/commonjs/ui/screens/fileManagement/trashActions.js +5 -2
  12. package/lib/commonjs/ui/screens/fileManagement/trashActions.js.map +1 -1
  13. package/lib/module/ui/components/OxyAuthChooser.js +8 -1
  14. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  15. package/lib/module/ui/context/OxyContext.js +15 -2
  16. package/lib/module/ui/context/OxyContext.js.map +1 -1
  17. package/lib/module/ui/hooks/queries/useFileQueries.js +1 -1
  18. package/lib/module/ui/hooks/queries/useFileQueries.js.map +1 -1
  19. package/lib/module/ui/screens/FileManagementScreen.js +20 -10
  20. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  21. package/lib/module/ui/screens/ManageAccountScreen.js +11 -3
  22. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  23. package/lib/module/ui/screens/fileManagement/trashActions.js +5 -2
  24. package/lib/module/ui/screens/fileManagement/trashActions.js.map +1 -1
  25. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/ui/screens/fileManagement/trashActions.d.ts +4 -1
  31. package/lib/typescript/commonjs/ui/screens/fileManagement/trashActions.d.ts.map +1 -1
  32. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  33. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  34. package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
  35. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  36. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  37. package/lib/typescript/module/ui/screens/fileManagement/trashActions.d.ts +4 -1
  38. package/lib/typescript/module/ui/screens/fileManagement/trashActions.d.ts.map +1 -1
  39. package/package.json +5 -5
  40. package/src/ui/components/OxyAuthChooser.tsx +9 -1
  41. package/src/ui/context/OxyContext.tsx +11 -2
  42. package/src/ui/hooks/queries/useFileQueries.ts +4 -1
  43. package/src/ui/screens/FileManagementScreen.tsx +33 -21
  44. package/src/ui/screens/ManageAccountScreen.tsx +26 -17
  45. package/src/ui/screens/fileManagement/trashActions.ts +5 -2
@@ -41,6 +41,7 @@ import { surfaces } from '@oxy.so/bloom/surfaces';
41
41
  import { useTheme } from '@oxy.so/bloom/theme';
42
42
  import { getNormalizedUserHandle, type User } from '@oxy.so/core';
43
43
  import { useQueryClient } from '@tanstack/react-query';
44
+ import type { LogoutResult } from '../context/oxyContextTypes';
44
45
  import { useOxy } from '../context/oxyRuntimeContext';
45
46
  import { useDeviceSwitcher } from '../hooks/useDeviceSwitcher';
46
47
  import { useI18n } from '../hooks/useI18n';
@@ -257,7 +258,14 @@ const OxyAuthChooser: React.FC<OxyAuthChooserProps> = ({ onComplete }) => {
257
258
  */
258
259
  const handleSignOut = useCallback(async () => {
259
260
  if (!controller) return;
260
- const outcome = await controller.runDeviceMutation(() => logout());
261
+ let outcome: { ran: true; value: LogoutResult } | { ran: false };
262
+ try {
263
+ outcome = await controller.runDeviceMutation(() => logout());
264
+ } catch {
265
+ // `runDeviceMutation` rethrows; the caller is `void handleSignOut()`.
266
+ toast.error(t('common.errors.signOutFailed'));
267
+ return;
268
+ }
261
269
  if (!outcome.ran) return;
262
270
  if (outcome.value.status === 'failed') {
263
271
  toast.error(t('common.errors.signOutFailed'));
@@ -999,8 +999,8 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
999
999
  if (host) {
1000
1000
  accountDialogHostRef.current = null;
1001
1001
  setAccountDialogOpen(false);
1002
- host.goBack();
1003
- return;
1002
+ // A host that is already gone cannot pop; fall through to the detached surface.
1003
+ if (host.goBack()) return;
1004
1004
  }
1005
1005
  accountDialogSurfaceRef.current?.dismiss();
1006
1006
  }, []);
@@ -1161,7 +1161,16 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
1161
1161
  accountDialogControllerRef.current?.setView(nextView);
1162
1162
 
1163
1163
  // Its own detached surface is already open → just re-point the view above.
1164
+ // A route surface (e.g. ManageAccount) may sit ON TOP of it, so re-pointing the
1165
+ // view is not enough: bring the AccountDialog frame back to the front too.
1164
1166
  if (accountDialogSurfaceRef.current) {
1167
+ const top = topRouteSurface();
1168
+ if (top) {
1169
+ accountDialogHostRef.current = top;
1170
+ top.navigate('AccountDialog', { initialView: nextView });
1171
+ } else {
1172
+ accountDialogSurfaceRef.current.navigate('AccountDialog', { initialView: nextView });
1173
+ }
1165
1174
  setAccountDialogOpen(true);
1166
1175
  return;
1167
1176
  }
@@ -60,7 +60,10 @@ export function mapRawFileToMetadata(raw: RawUserFile): FileMetadata {
60
60
  chunkSize: 0,
61
61
  uploadDate: raw.createdAt ?? '',
62
62
  ...(typeof raw.inUse === 'boolean' ? { inUse: raw.inUse } : {}),
63
- ...(raw.status === 'active' || raw.status === 'trash' || raw.status === 'blocked'
63
+ ...(raw.status === 'active' ||
64
+ raw.status === 'trash' ||
65
+ raw.status === 'blocked' ||
66
+ raw.status === 'lost'
64
67
  ? { status: raw.status }
65
68
  : {}),
66
69
  ...(typeof raw.trashedAt === 'string' ? { trashedAt: raw.trashedAt } : {}),
@@ -150,10 +150,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
150
150
  // Derived filtered and sorted files (avoid setState loops)
151
151
  const filteredFiles = useMemo(() => {
152
152
  let filteredByMode = files;
153
+ // A lost file (ADR 0043 D13) has no media to show: it is listed under
154
+ // "all" and "documents" only, as lost, never as a broken thumbnail.
155
+ const withMedia = files.filter((file) => file.status !== 'lost');
153
156
  if (viewMode === 'photos') {
154
- filteredByMode = files.filter((file) => file.contentType.startsWith('image/'));
157
+ filteredByMode = withMedia.filter((file) => file.contentType.startsWith('image/'));
155
158
  } else if (viewMode === 'videos') {
156
- filteredByMode = files.filter((file) => file.contentType.startsWith('video/'));
159
+ filteredByMode = withMedia.filter((file) => file.contentType.startsWith('video/'));
157
160
  } else if (viewMode === 'documents') {
158
161
  filteredByMode = files.filter(
159
162
  (file) =>
@@ -167,7 +170,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
167
170
  file.contentType.includes('powerpoint'),
168
171
  );
169
172
  } else if (viewMode === 'audio') {
170
- filteredByMode = files.filter((file) => file.contentType.startsWith('audio/'));
173
+ filteredByMode = withMedia.filter((file) => file.contentType.startsWith('audio/'));
171
174
  }
172
175
 
173
176
  let filtered = filteredByMode;
@@ -721,6 +724,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
721
724
  toggleSelect(file);
722
725
  return;
723
726
  }
727
+ // Nothing to open: its bytes are gone (ADR 0043 D13). The row says so.
728
+ if (file.status === 'lost') return;
724
729
  try {
725
730
  setLoadingFileContent(true);
726
731
  setOpenedFile(file);
@@ -865,7 +870,8 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
865
870
  const isImage = file.contentType.startsWith('image/');
866
871
  const isPDF = file.contentType.includes('pdf');
867
872
  const isVideo = file.contentType.startsWith('video/');
868
- const hasPreview = isImage || isPDF || isVideo;
873
+ const isLost = file.status === 'lost';
874
+ const hasPreview = !isLost && (isImage || isPDF || isVideo);
869
875
  const isSelected = selectedIds.has(file.id);
870
876
 
871
877
  // Create icon for preview thumbnails (36x36)
@@ -948,7 +954,9 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
948
954
  id: file.id,
949
955
  icon:
950
956
  fileIcon ??
951
- (!hasPreview ? (
957
+ (isLost ? (
958
+ <Ionicons name="cloud-offline-outline" size={20} color={colors.error} />
959
+ ) : !hasPreview ? (
952
960
  <Ionicons name={getFileIcon(file.contentType)} size={20} color={colors.primary} />
953
961
  ) : undefined),
954
962
  title: file.filename,
@@ -958,9 +966,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
958
966
  description: `${formatFileSize(file.length)} • ${new Date(file.uploadDate).toLocaleDateString()}${
959
967
  file.status === 'blocked'
960
968
  ? ` • ${t('fileManagement.removedAfterClaim')}`
961
- : file.inUse === false
962
- ? ` • ${t('fileManagement.notInUse')}`
963
- : ''
969
+ : isLost
970
+ ? ` • ${t('fileManagement.contentLost')}`
971
+ : file.inUse === false
972
+ ? ` • ${t('fileManagement.notInUse')}`
973
+ : ''
964
974
  }`,
965
975
  onPress: () => {
966
976
  // Support selection in regular mode with long press or if already selecting
@@ -975,7 +985,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
975
985
  rightElement:
976
986
  !selectMode && selectedIds.size === 0 ? (
977
987
  <View className="flex-row items-center gap-1.5 ml-3">
978
- {isImage || isVideo || file.contentType.includes('pdf') ? (
988
+ {hasPreview ? (
979
989
  <Button
980
990
  appearance="subtle"
981
991
  tone="neutral"
@@ -987,16 +997,18 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
987
997
  icon={<Ionicons name="eye" size={18} color={colors.text} />}
988
998
  />
989
999
  ) : null}
990
- <Button
991
- appearance="subtle"
992
- tone="neutral"
993
- size="sm"
994
- iconOnly
995
- stopPropagation
996
- accessibilityLabel={`Download ${file.filename}`}
997
- onPress={() => handleFileDownload(file.id, file.filename)}
998
- icon={<Ionicons name="download" size={18} color={colors.text} />}
999
- />
1000
+ {isLost ? null : (
1001
+ <Button
1002
+ appearance="subtle"
1003
+ tone="neutral"
1004
+ size="sm"
1005
+ iconOnly
1006
+ stopPropagation
1007
+ accessibilityLabel={`Download ${file.filename}`}
1008
+ onPress={() => handleFileDownload(file.id, file.filename)}
1009
+ icon={<Ionicons name="download" size={18} color={colors.text} />}
1010
+ />
1011
+ )}
1000
1012
  <Button
1001
1013
  appearance="subtle"
1002
1014
  tone="danger"
@@ -1005,7 +1017,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1005
1017
  stopPropagation
1006
1018
  loading={deletingId === file.id}
1007
1019
  accessibilityLabel={t(
1008
- file.status === 'blocked'
1020
+ libraryRemovalFor(file) === 'delete'
1009
1021
  ? 'fileManagement.a11y.deleteFile'
1010
1022
  : 'fileManagement.a11y.moveToTrash',
1011
1023
  { name: file.filename },
@@ -1308,7 +1320,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1308
1320
  onDownload={handleFileDownload}
1309
1321
  onDelete={confirmFileDelete}
1310
1322
  deleteLabel={
1311
- openedFile.status === 'blocked'
1323
+ libraryRemovalFor(openedFile) === 'delete'
1312
1324
  ? t('fileManagement.deleteFile')
1313
1325
  : t('fileManagement.moveToTrash')
1314
1326
  }
@@ -61,6 +61,7 @@ const ManageAccountScreen: React.FC<ManageAccountScreenProps> = ({
61
61
  openAvatarPicker,
62
62
  accounts,
63
63
  openAccountDialog,
64
+ accountDialogController,
64
65
  hasIdentity,
65
66
  } = useOxy();
66
67
 
@@ -105,7 +106,12 @@ const ManageAccountScreen: React.FC<ManageAccountScreenProps> = ({
105
106
  }
106
107
  setSigningOut(true);
107
108
  try {
108
- await logout();
109
+ const result = await logout();
110
+ if (result?.status === 'failed') {
111
+ loggerUtil.warn('Sign out failed', { component: 'ManageAccountScreen' }, result.error);
112
+ toast.error(t('common.errors.signOutFailed') || 'Failed to sign out');
113
+ return;
114
+ }
109
115
  toast.success(t('common.actions.signedOut') || 'Signed out');
110
116
  onClose?.();
111
117
  } catch (error) {
@@ -360,22 +366,25 @@ const ManageAccountScreen: React.FC<ManageAccountScreenProps> = ({
360
366
  {/* Accounts (unified account graph) */}
361
367
  {accounts.length > 0 || isAuthenticated ? (
362
368
  <GroupedSection title={t('accountCenter.sections.accounts') || 'Accounts'}>
363
- <GroupedSectionItem
364
- icon={<SettingsIcon name="account-switch" color={bloomTheme.colors.info} />}
365
- title={t('accounts.manage.switch.title') || 'Switch account'}
366
- description={
367
- accounts.length > 0
368
- ? // One account is "1 account", not "1 accounts".
369
- t(
370
- accounts.length === 1
371
- ? 'accounts.manage.switch.countOne'
372
- : 'accounts.manage.switch.count',
373
- { count: accounts.length },
374
- )
375
- : t('accounts.manage.switch.empty') || 'Accounts you own or share'
376
- }
377
- onPress={() => openAccountDialog('accounts')}
378
- />
369
+ {/* No controller means the dialog can never open (identity-bound session). */}
370
+ {accountDialogController ? (
371
+ <GroupedSectionItem
372
+ icon={<SettingsIcon name="account-switch" color={bloomTheme.colors.info} />}
373
+ title={t('accounts.manage.switch.title') || 'Switch account'}
374
+ description={
375
+ accounts.length > 0
376
+ ? // One account is "1 account", not "1 accounts".
377
+ t(
378
+ accounts.length === 1
379
+ ? 'accounts.manage.switch.countOne'
380
+ : 'accounts.manage.switch.count',
381
+ { count: accounts.length },
382
+ )
383
+ : t('accounts.manage.switch.empty') || 'Accounts you own or share'
384
+ }
385
+ onPress={() => openAccountDialog('accounts')}
386
+ />
387
+ ) : null}
379
388
  <GroupedSectionItem
380
389
  icon={<SettingsIcon name="account-plus" color={bloomTheme.colors.primary} />}
381
390
  title={t('accounts.create.title') || 'Create account'}
@@ -25,9 +25,12 @@ export interface TrashAssetsApi {
25
25
  /** What removing a file from the library does to it. */
26
26
  export type LibraryRemoval = 'trash' | 'delete';
27
27
 
28
- /** A `blocked` file is deleted (it cannot go to trash); every other is trashed. */
28
+ /**
29
+ * A `blocked` file is deleted (it cannot go to trash), and so is a `lost` one
30
+ * (ADR 0043 D13: nothing is left to restore); every other is trashed.
31
+ */
29
32
  export function libraryRemovalFor(file: Pick<FileMetadata, 'status'>): LibraryRemoval {
30
- return file.status === 'blocked' ? 'delete' : 'trash';
33
+ return file.status === 'blocked' || file.status === 'lost' ? 'delete' : 'trash';
31
34
  }
32
35
 
33
36
  /** The HTTP status of a failed asset call, when it carries one. */