@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.
- package/lib/commonjs/ui/components/OxyAuthChooser.js +8 -1
- package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +15 -2
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useFileQueries.js +1 -1
- package/lib/commonjs/ui/hooks/queries/useFileQueries.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +20 -10
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ManageAccountScreen.js +11 -3
- package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/trashActions.js +5 -2
- package/lib/commonjs/ui/screens/fileManagement/trashActions.js.map +1 -1
- package/lib/module/ui/components/OxyAuthChooser.js +8 -1
- package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +15 -2
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/queries/useFileQueries.js +1 -1
- package/lib/module/ui/hooks/queries/useFileQueries.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +20 -10
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/ManageAccountScreen.js +11 -3
- package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/trashActions.js +5 -2
- package/lib/module/ui/screens/fileManagement/trashActions.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/trashActions.d.ts +4 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/trashActions.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/trashActions.d.ts +4 -1
- package/lib/typescript/module/ui/screens/fileManagement/trashActions.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/ui/components/OxyAuthChooser.tsx +9 -1
- package/src/ui/context/OxyContext.tsx +11 -2
- package/src/ui/hooks/queries/useFileQueries.ts +4 -1
- package/src/ui/screens/FileManagementScreen.tsx +33 -21
- package/src/ui/screens/ManageAccountScreen.tsx +26 -17
- 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
|
-
|
|
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.
|
|
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' ||
|
|
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 =
|
|
157
|
+
filteredByMode = withMedia.filter((file) => file.contentType.startsWith('image/'));
|
|
155
158
|
} else if (viewMode === 'videos') {
|
|
156
|
-
filteredByMode =
|
|
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 =
|
|
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
|
|
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
|
-
(
|
|
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
|
-
:
|
|
962
|
-
? ` • ${t('fileManagement.
|
|
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
|
-
{
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
accounts.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
/**
|
|
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. */
|