@oxy.so/services 11.6.0 → 11.9.0

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 (186) hide show
  1. package/android/src/main/java/so/oxy/security/OxyCallerPolicy.kt +18 -0
  2. package/lib/commonjs/assets/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
  3. package/lib/commonjs/assets/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
  4. package/lib/commonjs/assets/assets/fonts/icons/manifest.json +8 -5
  5. package/lib/commonjs/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
  6. package/lib/commonjs/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
  7. package/lib/commonjs/assets/fonts/icons/manifest.json +8 -5
  8. package/lib/commonjs/ui/components/fileManagement/FileViewer.js +2 -1
  9. package/lib/commonjs/ui/components/fileManagement/FileViewer.js.map +1 -1
  10. package/lib/commonjs/ui/components/supervision/PersonPicker.js +69 -0
  11. package/lib/commonjs/ui/components/supervision/PersonPicker.js.map +1 -0
  12. package/lib/commonjs/ui/components/supervision/presentation.js +23 -0
  13. package/lib/commonjs/ui/components/supervision/presentation.js.map +1 -0
  14. package/lib/commonjs/ui/hooks/queries/queryKeys.js +3 -1
  15. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  16. package/lib/commonjs/ui/hooks/queries/useFileQueries.js +39 -17
  17. package/lib/commonjs/ui/hooks/queries/useFileQueries.js.map +1 -1
  18. package/lib/commonjs/ui/hooks/queries/useSupervision.js +37 -0
  19. package/lib/commonjs/ui/hooks/queries/useSupervision.js.map +1 -0
  20. package/lib/commonjs/ui/icons/subsetGlyphMaps.js +3 -0
  21. package/lib/commonjs/ui/icons/subsetGlyphMaps.js.map +1 -1
  22. package/lib/commonjs/ui/index.js +13 -0
  23. package/lib/commonjs/ui/index.js.map +1 -1
  24. package/lib/commonjs/ui/navigation/routes.js +2 -0
  25. package/lib/commonjs/ui/navigation/routes.js.map +1 -1
  26. package/lib/commonjs/ui/navigation/surfaceRegistry.js +2 -2
  27. package/lib/commonjs/ui/navigation/surfaceRegistry.js.map +1 -1
  28. package/lib/commonjs/ui/screens/CreateAccountScreen.js +10 -2
  29. package/lib/commonjs/ui/screens/CreateAccountScreen.js.map +1 -1
  30. package/lib/commonjs/ui/screens/FileManagementScreen.js +102 -44
  31. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  32. package/lib/commonjs/ui/screens/FileTrashScreen.js +235 -0
  33. package/lib/commonjs/ui/screens/FileTrashScreen.js.map +1 -0
  34. package/lib/commonjs/ui/screens/ManageAccountScreen.js +7 -0
  35. package/lib/commonjs/ui/screens/ManageAccountScreen.js.map +1 -1
  36. package/lib/commonjs/ui/screens/SupervisionScreen.js +529 -0
  37. package/lib/commonjs/ui/screens/SupervisionScreen.js.map +1 -0
  38. package/lib/commonjs/ui/screens/fileManagement/trashActions.js +147 -0
  39. package/lib/commonjs/ui/screens/fileManagement/trashActions.js.map +1 -0
  40. package/lib/commonjs/ui/screens/index.js +7 -0
  41. package/lib/commonjs/ui/screens/index.js.map +1 -1
  42. package/lib/commonjs/ui/stores/assetStore.js +22 -7
  43. package/lib/commonjs/ui/stores/assetStore.js.map +1 -1
  44. package/lib/module/assets/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
  45. package/lib/module/assets/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
  46. package/lib/module/assets/assets/fonts/icons/manifest.json +8 -5
  47. package/lib/module/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
  48. package/lib/module/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
  49. package/lib/module/assets/fonts/icons/manifest.json +8 -5
  50. package/lib/module/ui/components/fileManagement/FileViewer.js +2 -1
  51. package/lib/module/ui/components/fileManagement/FileViewer.js.map +1 -1
  52. package/lib/module/ui/components/supervision/PersonPicker.js +65 -0
  53. package/lib/module/ui/components/supervision/PersonPicker.js.map +1 -0
  54. package/lib/module/ui/components/supervision/presentation.js +19 -0
  55. package/lib/module/ui/components/supervision/presentation.js.map +1 -0
  56. package/lib/module/ui/hooks/queries/queryKeys.js +3 -1
  57. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  58. package/lib/module/ui/hooks/queries/useFileQueries.js +38 -17
  59. package/lib/module/ui/hooks/queries/useFileQueries.js.map +1 -1
  60. package/lib/module/ui/hooks/queries/useSupervision.js +32 -0
  61. package/lib/module/ui/hooks/queries/useSupervision.js.map +1 -0
  62. package/lib/module/ui/icons/subsetGlyphMaps.js +3 -0
  63. package/lib/module/ui/icons/subsetGlyphMaps.js.map +1 -1
  64. package/lib/module/ui/index.js +1 -0
  65. package/lib/module/ui/index.js.map +1 -1
  66. package/lib/module/ui/navigation/routes.js +2 -0
  67. package/lib/module/ui/navigation/routes.js.map +1 -1
  68. package/lib/module/ui/navigation/surfaceRegistry.js +2 -2
  69. package/lib/module/ui/navigation/surfaceRegistry.js.map +1 -1
  70. package/lib/module/ui/screens/CreateAccountScreen.js +10 -2
  71. package/lib/module/ui/screens/CreateAccountScreen.js.map +1 -1
  72. package/lib/module/ui/screens/FileManagementScreen.js +102 -44
  73. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  74. package/lib/module/ui/screens/FileTrashScreen.js +232 -0
  75. package/lib/module/ui/screens/FileTrashScreen.js.map +1 -0
  76. package/lib/module/ui/screens/ManageAccountScreen.js +7 -0
  77. package/lib/module/ui/screens/ManageAccountScreen.js.map +1 -1
  78. package/lib/module/ui/screens/SupervisionScreen.js +525 -0
  79. package/lib/module/ui/screens/SupervisionScreen.js.map +1 -0
  80. package/lib/module/ui/screens/fileManagement/trashActions.js +138 -0
  81. package/lib/module/ui/screens/fileManagement/trashActions.js.map +1 -0
  82. package/lib/module/ui/screens/index.js +1 -0
  83. package/lib/module/ui/screens/index.js.map +1 -1
  84. package/lib/module/ui/stores/assetStore.js +22 -7
  85. package/lib/module/ui/stores/assetStore.js.map +1 -1
  86. package/lib/typescript/commonjs/ui/components/fileManagement/FileViewer.d.ts +3 -0
  87. package/lib/typescript/commonjs/ui/components/fileManagement/FileViewer.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/ui/components/supervision/PersonPicker.d.ts +6 -0
  89. package/lib/typescript/commonjs/ui/components/supervision/PersonPicker.d.ts.map +1 -0
  90. package/lib/typescript/commonjs/ui/components/supervision/presentation.d.ts +3 -0
  91. package/lib/typescript/commonjs/ui/components/supervision/presentation.d.ts.map +1 -0
  92. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +2 -0
  93. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts +22 -6
  95. package/lib/typescript/commonjs/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/hooks/queries/useSupervision.d.ts +475 -0
  97. package/lib/typescript/commonjs/ui/hooks/queries/useSupervision.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts +1 -1
  99. package/lib/typescript/commonjs/ui/icons/Ionicons.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  101. package/lib/typescript/commonjs/ui/icons/MaterialCommunityIcons.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/icons/subsetGlyphMaps.d.ts +3 -0
  103. package/lib/typescript/commonjs/ui/icons/subsetGlyphMaps.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/ui/index.d.ts +1 -0
  105. package/lib/typescript/commonjs/ui/index.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
  107. package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts +6 -0
  109. package/lib/typescript/commonjs/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/screens/FileTrashScreen.d.ts +14 -0
  113. package/lib/typescript/commonjs/ui/screens/FileTrashScreen.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/ui/screens/SupervisionScreen.d.ts +3 -0
  116. package/lib/typescript/commonjs/ui/screens/SupervisionScreen.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/ui/screens/fileManagement/trashActions.d.ts +66 -0
  118. package/lib/typescript/commonjs/ui/screens/fileManagement/trashActions.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/ui/screens/index.d.ts +1 -0
  120. package/lib/typescript/commonjs/ui/screens/index.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/ui/stores/assetStore.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/ui/utils/identityCrypto.d.ts +1 -0
  123. package/lib/typescript/commonjs/ui/utils/identityCrypto.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/components/fileManagement/FileViewer.d.ts +3 -0
  125. package/lib/typescript/module/ui/components/fileManagement/FileViewer.d.ts.map +1 -1
  126. package/lib/typescript/module/ui/components/supervision/PersonPicker.d.ts +6 -0
  127. package/lib/typescript/module/ui/components/supervision/PersonPicker.d.ts.map +1 -0
  128. package/lib/typescript/module/ui/components/supervision/presentation.d.ts +3 -0
  129. package/lib/typescript/module/ui/components/supervision/presentation.d.ts.map +1 -0
  130. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +2 -0
  131. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts +22 -6
  133. package/lib/typescript/module/ui/hooks/queries/useFileQueries.d.ts.map +1 -1
  134. package/lib/typescript/module/ui/hooks/queries/useSupervision.d.ts +475 -0
  135. package/lib/typescript/module/ui/hooks/queries/useSupervision.d.ts.map +1 -0
  136. package/lib/typescript/module/ui/icons/Ionicons.d.ts +1 -1
  137. package/lib/typescript/module/ui/icons/Ionicons.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts +1 -1
  139. package/lib/typescript/module/ui/icons/MaterialCommunityIcons.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/icons/subsetGlyphMaps.d.ts +3 -0
  141. package/lib/typescript/module/ui/icons/subsetGlyphMaps.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/index.d.ts +1 -0
  143. package/lib/typescript/module/ui/index.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
  145. package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
  146. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts +6 -0
  147. package/lib/typescript/module/ui/navigation/surfaceRegistry.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/screens/CreateAccountScreen.d.ts.map +1 -1
  149. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/screens/FileTrashScreen.d.ts +14 -0
  151. package/lib/typescript/module/ui/screens/FileTrashScreen.d.ts.map +1 -0
  152. package/lib/typescript/module/ui/screens/ManageAccountScreen.d.ts.map +1 -1
  153. package/lib/typescript/module/ui/screens/SupervisionScreen.d.ts +3 -0
  154. package/lib/typescript/module/ui/screens/SupervisionScreen.d.ts.map +1 -0
  155. package/lib/typescript/module/ui/screens/fileManagement/trashActions.d.ts +66 -0
  156. package/lib/typescript/module/ui/screens/fileManagement/trashActions.d.ts.map +1 -0
  157. package/lib/typescript/module/ui/screens/index.d.ts +1 -0
  158. package/lib/typescript/module/ui/screens/index.d.ts.map +1 -1
  159. package/lib/typescript/module/ui/stores/assetStore.d.ts.map +1 -1
  160. package/lib/typescript/module/ui/utils/identityCrypto.d.ts +1 -0
  161. package/lib/typescript/module/ui/utils/identityCrypto.d.ts.map +1 -1
  162. package/package.json +6 -21
  163. package/src/assets/fonts/icons/OxyServicesIonicons.ttf +0 -0
  164. package/src/assets/fonts/icons/OxyServicesMaterialCommunityIcons.ttf +0 -0
  165. package/src/assets/fonts/icons/manifest.json +8 -5
  166. package/src/ui/components/fileManagement/FileViewer.tsx +5 -1
  167. package/src/ui/components/supervision/PersonPicker.tsx +67 -0
  168. package/src/ui/components/supervision/presentation.ts +17 -0
  169. package/src/ui/hooks/queries/__tests__/fileQueries.test.ts +70 -0
  170. package/src/ui/hooks/queries/queryKeys.ts +2 -0
  171. package/src/ui/hooks/queries/useFileQueries.ts +45 -16
  172. package/src/ui/hooks/queries/useSupervision.ts +25 -0
  173. package/src/ui/icons/subsetGlyphMaps.ts +3 -0
  174. package/src/ui/index.ts +2 -0
  175. package/src/ui/navigation/__tests__/getSurfaceConfig.test.ts +9 -0
  176. package/src/ui/navigation/routes.ts +4 -0
  177. package/src/ui/navigation/surfaceRegistry.ts +5 -1
  178. package/src/ui/screens/CreateAccountScreen.tsx +15 -1
  179. package/src/ui/screens/FileManagementScreen.tsx +142 -49
  180. package/src/ui/screens/FileTrashScreen.tsx +228 -0
  181. package/src/ui/screens/ManageAccountScreen.tsx +10 -0
  182. package/src/ui/screens/SupervisionScreen.tsx +593 -0
  183. package/src/ui/screens/fileManagement/__tests__/trashActions.test.ts +177 -0
  184. package/src/ui/screens/fileManagement/trashActions.ts +161 -0
  185. package/src/ui/screens/index.ts +2 -0
  186. package/src/ui/stores/assetStore.ts +24 -13
@@ -9,6 +9,7 @@ export const ioniconsGlyphMap = {
9
9
  "arrow-back": 61735,
10
10
  "arrow-down": 61741,
11
11
  "arrow-forward": 61747,
12
+ "arrow-undo": 61759,
12
13
  "arrow-up": 61765,
13
14
  "at": 61771,
14
15
  "briefcase-outline": 61874,
@@ -199,6 +200,7 @@ export const materialCommunityIconsGlyphMap = {
199
200
  "code-tags": 983412,
200
201
  "cog": 984211,
201
202
  "cog-outline": 985275,
203
+ "contacts": 984779,
202
204
  "contain": 985662,
203
205
  "content-copy": 983439,
204
206
  "controller": 983732,
@@ -206,6 +208,7 @@ export const materialCommunityIconsGlyphMap = {
206
208
  "credit-card": 987119,
207
209
  "cube-outline": 983463,
208
210
  "database-outline": 988722,
211
+ "decimal": 987297,
209
212
  "delete": 983476,
210
213
  "delete-outline": 985575,
211
214
  "devices": 987056,
package/src/ui/index.ts CHANGED
@@ -57,3 +57,5 @@ export {
57
57
  extractErrorMessage,
58
58
  } from './utils/errorHandlers';
59
59
  export type { HandleAuthErrorOptions } from './utils/errorHandlers';
60
+
61
+ export { useSupervision, supervisionQueryKeys } from './hooks/queries/useSupervision';
@@ -32,6 +32,15 @@ describe('getSurfaceConfig presentation', () => {
32
32
  expect(config.frameSize).toEqual({ heightRatio: 0.9, maxWidth: 640 });
33
33
  });
34
34
 
35
+ it('hosts the file Trash as a morphing own-scroller sheet (it owns its list)', () => {
36
+ const config = getSurfaceConfig('FileTrash', {});
37
+ expect(config.presentation).toBe('sheet');
38
+ expect(config.scrollable).toBe(false);
39
+ expect(config.morph).toBe(true);
40
+ expect(config.stacks).toBe(false);
41
+ expect(config.header).toBe(true);
42
+ });
43
+
35
44
  it('morphs the surface for every route, the picker included', () => {
36
45
  // NAV-WITHIN: drilling into these swaps the content inside ONE surface,
37
46
  // which reshapes the panel instead of hard-cutting.
@@ -3,6 +3,7 @@ import type { BaseScreenProps } from '../types/navigation';
3
3
 
4
4
  // Define all available route names
5
5
  export type RouteName =
6
+ | 'Supervision'
6
7
  | 'ManageAccount' // Unified "Manage your Oxy Account" surface
7
8
  | 'AccountVerification'
8
9
  | 'PaymentGateway'
@@ -11,6 +12,7 @@ export type RouteName =
11
12
  | 'PrivacySettings'
12
13
  | 'SearchSettings'
13
14
  | 'FileManagement'
15
+ | 'FileTrash' // The person's trash: restore, or delete permanently (ADR 0043 D5)
14
16
  | 'HelpSupport'
15
17
  | 'LegalDocuments'
16
18
  | 'AppInfo'
@@ -71,6 +73,7 @@ export type RouteName =
71
73
  * confines the gap to one documented widening in {@link getScreenComponent}.
72
74
  */
73
75
  const screenComponents: Record<RouteName, ComponentType<never>> = {
76
+ Supervision: lazy(() => import('../screens/SupervisionScreen')),
74
77
  ManageAccount: lazy(() => import('../screens/ManageAccountScreen')),
75
78
  AccountVerification: lazy(() => import('../screens/AccountVerificationScreen')),
76
79
  PaymentGateway: lazy(() => import('../screens/PaymentGatewayScreen')),
@@ -79,6 +82,7 @@ const screenComponents: Record<RouteName, ComponentType<never>> = {
79
82
  PrivacySettings: lazy(() => import('../screens/PrivacySettingsScreen')),
80
83
  SearchSettings: lazy(() => import('../screens/SearchSettingsScreen')),
81
84
  FileManagement: lazy(() => import('../screens/FileManagementScreen')),
85
+ FileTrash: lazy(() => import('../screens/FileTrashScreen')),
82
86
  HelpSupport: lazy(() => import('../screens/HelpSupportScreen')),
83
87
  LegalDocuments: lazy(() => import('../screens/LegalDocumentsScreen')),
84
88
  AppInfo: lazy(() => import('../screens/AppInfoScreen')),
@@ -24,6 +24,7 @@ type SurfaceRoute<Props = Record<string, unknown>, Result = void> = {
24
24
  };
25
25
 
26
26
  export interface SurfaceRegistry {
27
+ Supervision: SurfaceRoute<{ approvalId?: string; childAccountId?: string }>;
27
28
  ManageAccount: SurfaceRoute;
28
29
  AccountVerification: SurfaceRoute;
29
30
  PaymentGateway: SurfaceRoute;
@@ -38,6 +39,8 @@ export interface SurfaceRegistry {
38
39
  * keep their callback behaviour and this result goes unread.
39
40
  */
40
41
  FileManagement: SurfaceRoute<Record<string, unknown>, FileMetadata>;
42
+ /** The person's trash (ADR 0043 D5), opened from the file manager. */
43
+ FileTrash: SurfaceRoute;
41
44
  HelpSupport: SurfaceRoute;
42
45
  LegalDocuments: SurfaceRoute;
43
46
  AppInfo: SurfaceRoute;
@@ -232,7 +235,7 @@ const isFileManagementImageOnlyPicker = (props: Record<string, unknown>): boolea
232
235
  * Routes whose screen owns its OWN scroll container — a FlatList /
233
236
  * VirtualizedList (`ConnectedApps`, `TrustLeaderboard`, `FollowersList` /
234
237
  * `FollowingList` via `UserListScreen`) or its own vertical `ScrollView`
235
- * (`FileManagement`). These MUST opt out of the Dialog's internal ScrollView
238
+ * (`FileManagement`, `FileTrash`). These MUST opt out of the Dialog's internal ScrollView
236
239
  * (`scrollable: false`) so there is exactly ONE scroll container — otherwise a
237
240
  * VirtualizedList nests inside a plain ScrollView (RN warning) or a nested
238
241
  * vertical scroll-in-scroll breaks windowing. Screens with only a HORIZONTAL
@@ -241,6 +244,7 @@ const isFileManagementImageOnlyPicker = (props: Record<string, unknown>): boolea
241
244
  */
242
245
  const OWN_SCROLL_CONTAINER_ROUTES: ReadonlySet<RouteName> = new Set<RouteName>([
243
246
  'FileManagement',
247
+ 'FileTrash',
244
248
  'ConnectedApps',
245
249
  'TrustLeaderboard',
246
250
  'FollowersList',
@@ -120,7 +120,12 @@ const ACCOUNT_CATEGORY_OPTIONS: readonly AccountCategoryId[] = SELECTABLE_ACCOUN
120
120
  * account via the `parentAccountId` prop. NOT the cryptographic Commons/DID
121
121
  * "identity" — that is a separate concept.
122
122
  */
123
- const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, parentAccountId }) => {
123
+ const CreateAccountScreen: React.FC<BaseScreenProps> = ({
124
+ onClose,
125
+ goBack,
126
+ parentAccountId,
127
+ navigate,
128
+ }) => {
124
129
  const bloomTheme = useTheme();
125
130
  const { oxyServices, createAccount, switchToAccount, user, canUsePrivateApi } = useOxy();
126
131
  const { t, locale } = useI18n();
@@ -399,6 +404,15 @@ const CreateAccountScreen: React.FC<BaseScreenProps> = ({ onClose, goBack, paren
399
404
 
400
405
  return (
401
406
  <View className="gap-space-16 px-screen-margin pt-space-16 pb-space-32">
407
+ {user?.kind === 'personal' && !parentId ? (
408
+ <SettingsListGroup title={t('supervision.title')}>
409
+ <SettingsListItem
410
+ title={t('supervision.setupOwn')}
411
+ description={t('supervision.setupOwnDescription')}
412
+ onPress={() => navigate?.('Supervision')}
413
+ />
414
+ </SettingsListGroup>
415
+ ) : null}
402
416
  {/* Account type — canonical grouped selection rows (checkmark on the chosen one) */}
403
417
  <SettingsListGroup title={t('accounts.create.typeSection') || 'Account type'}>
404
418
  {KIND_OPTIONS.map((option) => {
@@ -42,6 +42,12 @@ import { presentFileDetails } from '../components/fileManagement/FileDetailsModa
42
42
  import { UploadPreview } from '../components/fileManagement/UploadPreview';
43
43
  import { presentActionSheet } from '../components/surfaces/ActionSheetSurface';
44
44
  import { getErrorMessage } from './fileManagement/shared';
45
+ import {
46
+ deletePermanently,
47
+ errorStatus,
48
+ libraryRemovalFor,
49
+ removeFromLibrary,
50
+ } from './fileManagement/trashActions';
45
51
  import { useFileUploadState } from './fileManagement/hooks/useFileUploadState';
46
52
  import PhotoPickerView from './fileManagement/PhotoPickerSection';
47
53
  import FileListSection, { type FileListItem } from './fileManagement/FileListSection';
@@ -124,6 +130,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
124
130
  const invalidateFiles = useCallback(() => {
125
131
  queryClient.invalidateQueries({ queryKey: queryKeys.files.list(targetUserId) });
126
132
  }, [queryClient, targetUserId]);
133
+ // A removal changes the library AND the trash (`files.all` holds both).
134
+ const invalidateAllFiles = useCallback(() => {
135
+ queryClient.invalidateQueries({ queryKey: queryKeys.files.all });
136
+ }, [queryClient]);
127
137
 
128
138
  const { width: windowWidth } = useWindowDimensions();
129
139
  // Prefer an explicit sheet width from the router; fall back to the window
@@ -483,90 +493,142 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
483
493
  t,
484
494
  });
485
495
 
486
- const confirmFileDelete = useCallback(
487
- async (fileId: string, filename: string) => {
488
- const confirmed = await surfaces.confirm({
496
+ // Asks before deleting for good a file an application still uses (the API
497
+ // refuses it until the person says so — ADR 0043 D5).
498
+ const confirmStillUsed = useCallback(
499
+ (filename: string) => (uses: number) =>
500
+ surfaces.confirm({
489
501
  title: t('fileManagement.deleteFile') || 'Delete File',
490
- description: t('fileManagement.confirms.deleteFile', { filename }),
491
- confirmLabel: t('fileManagement.confirm') || 'Delete',
502
+ description: t('fileManagement.confirms.deleteForeverInUse', { filename, count: uses }),
503
+ confirmLabel: t('fileManagement.trashView.deleteForever'),
492
504
  cancelLabel: t('common.cancel') || 'Cancel',
493
505
  destructive: true,
494
- });
506
+ }),
507
+ [t],
508
+ );
509
+
510
+ /**
511
+ * Remove one file from the library. It goes to TRASH — no longer served,
512
+ * restorable from Trash (ADR 0043 D5). A `blocked` file cannot be trashed
513
+ * (it is served to nobody already), so removing it is the permanent delete.
514
+ */
515
+ const confirmFileDelete = useCallback(
516
+ async (fileId: string, filename: string) => {
517
+ const file = files.find((candidate) => candidate.id === fileId);
518
+ const removal = libraryRemovalFor({ status: file?.status });
519
+ const confirmed = await surfaces.confirm(
520
+ removal === 'trash'
521
+ ? {
522
+ title: t('fileManagement.moveToTrash'),
523
+ description: t('fileManagement.confirms.moveToTrash', { filename }),
524
+ confirmLabel: t('fileManagement.moveToTrash'),
525
+ cancelLabel: t('common.cancel') || 'Cancel',
526
+ }
527
+ : {
528
+ title: t('fileManagement.deleteFile') || 'Delete File',
529
+ description: t('fileManagement.confirms.deleteFile', { filename }),
530
+ confirmLabel: t('fileManagement.confirm') || 'Delete',
531
+ cancelLabel: t('common.cancel') || 'Cancel',
532
+ destructive: true,
533
+ },
534
+ );
495
535
  if (!confirmed) return;
496
536
 
497
537
  try {
498
538
  setDeletingId(fileId);
499
- await oxyServices.assets.delete(fileId);
500
-
501
- toast.success(t('fileManagement.toasts.deleteSuccess'));
539
+ if (removal === 'trash') {
540
+ await oxyServices.assets.trash(fileId);
541
+ toast.success(t('fileManagement.toasts.movedToTrash'));
542
+ } else {
543
+ const outcome = await deletePermanently(
544
+ oxyServices.assets,
545
+ fileId,
546
+ confirmStillUsed(filename),
547
+ );
548
+ if (outcome === 'cancelled') return;
549
+ if (outcome === 'deleted') toast.success(t('fileManagement.toasts.deleteSuccess'));
550
+ }
502
551
 
503
- // Remove from the query cache, then reconcile against the server.
552
+ // Remove from the library cache, then reconcile both lists (the file
553
+ // is now in the trash) against the server.
504
554
  removeFileFromCache(queryClient, targetUserId, fileId);
505
- invalidateFiles();
555
+ invalidateAllFiles();
506
556
  } catch (error: unknown) {
507
- // Provide specific error messages
508
- if (
509
- getErrorMessage(error)?.includes('File not found') ||
510
- getErrorMessage(error)?.includes('404')
511
- ) {
557
+ const status = errorStatus(error);
558
+ if (status === 404) {
512
559
  toast.error(t('fileManagement.toasts.fileNotFound'));
513
560
  // It's already gone server-side — reconcile the list.
514
- invalidateFiles();
515
- } else if (
516
- getErrorMessage(error)?.includes('permission') ||
517
- getErrorMessage(error)?.includes('403')
518
- ) {
561
+ invalidateAllFiles();
562
+ } else if (status === 403) {
519
563
  toast.error(t('fileManagement.toasts.noPermission'));
520
564
  } else {
521
- toast.error(getErrorMessage(error) || t('fileManagement.toasts.deleteFailed'));
565
+ toast.error(
566
+ getErrorMessage(error) ||
567
+ t(
568
+ removal === 'trash'
569
+ ? 'fileManagement.toasts.moveToTrashFailed'
570
+ : 'fileManagement.toasts.deleteFailed',
571
+ ),
572
+ );
522
573
  }
523
574
  } finally {
524
575
  setDeletingId(null);
525
576
  }
526
577
  },
527
- [oxyServices, queryClient, targetUserId, invalidateFiles, t],
578
+ [files, oxyServices, queryClient, targetUserId, invalidateAllFiles, confirmStillUsed, t],
528
579
  );
529
580
 
530
581
  const confirmBulkDelete = useCallback(async () => {
531
582
  if (selectedIds.size === 0) return;
532
583
 
533
584
  const confirmed = await surfaces.confirm({
534
- title: t('fileManagement.deleteFiles') || 'Delete Files',
535
- description: t('fileManagement.confirms.deleteFiles', { count: selectedIds.size }),
536
- confirmLabel: t('fileManagement.confirm') || 'Delete',
585
+ title: t('fileManagement.moveToTrash'),
586
+ description: t('fileManagement.confirms.moveFilesToTrash', { count: selectedIds.size }),
587
+ confirmLabel: t('fileManagement.moveToTrash'),
537
588
  cancelLabel: t('common.cancel') || 'Cancel',
538
- destructive: true,
539
589
  });
540
590
  if (!confirmed) return;
541
591
 
542
592
  try {
543
- const deletePromises = Array.from(selectedIds).map(async (fileId) => {
544
- try {
545
- await oxyServices.assets.delete(fileId);
546
- removeFileFromCache(queryClient, targetUserId, fileId);
547
- return { success: true, fileId };
548
- } catch (error: unknown) {
549
- return { success: false, fileId, error };
550
- }
551
- });
552
-
553
- const results = await Promise.allSettled(deletePromises);
554
- const successful = results.filter((r) => r.status === 'fulfilled' && r.value.success).length;
555
- const failed = results.length - successful;
593
+ const selected = Array.from(selectedIds).map(
594
+ (id) => files.find((file) => file.id === id) ?? { id, filename: '', status: undefined },
595
+ );
596
+ const result = await removeFromLibrary(oxyServices.assets, selected, (uses, filename) =>
597
+ confirmStillUsed(filename)(uses),
598
+ );
599
+ for (const fileId of [...result.trashed, ...result.deleted]) {
600
+ removeFileFromCache(queryClient, targetUserId, fileId);
601
+ }
556
602
 
557
- if (successful > 0) {
558
- toast.success(t('fileManagement.toasts.bulkDeleteSuccess', { count: successful }));
603
+ if (result.trashed.length > 0) {
604
+ toast.success(
605
+ t('fileManagement.toasts.bulkMovedToTrash', { count: result.trashed.length }),
606
+ );
559
607
  }
560
- if (failed > 0) {
561
- toast.error(t('fileManagement.toasts.bulkDeleteFailed', { count: failed }));
608
+ if (result.deleted.length > 0) {
609
+ toast.success(
610
+ t('fileManagement.toasts.bulkDeleteSuccess', { count: result.deleted.length }),
611
+ );
612
+ }
613
+ if (result.failed.length > 0) {
614
+ toast.error(t('fileManagement.toasts.bulkDeleteFailed', { count: result.failed.length }));
562
615
  }
563
616
 
564
617
  setSelectedIds(new Set());
565
- invalidateFiles();
618
+ invalidateAllFiles();
566
619
  } catch (error: unknown) {
567
620
  toast.error(getErrorMessage(error) || t('fileManagement.toasts.bulkDeleteError'));
568
621
  }
569
- }, [selectedIds, oxyServices, queryClient, targetUserId, invalidateFiles, t]);
622
+ }, [
623
+ selectedIds,
624
+ files,
625
+ oxyServices,
626
+ queryClient,
627
+ targetUserId,
628
+ invalidateAllFiles,
629
+ confirmStillUsed,
630
+ t,
631
+ ]);
570
632
 
571
633
  const handleBulkVisibilityChange = useCallback(
572
634
  async (visibility: 'private' | 'public' | 'unlisted') => {
@@ -912,7 +974,16 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
912
974
  <Ionicons name={getFileIcon(file.contentType)} size={20} color={colors.primary} />
913
975
  ) : undefined),
914
976
  title: file.filename,
915
- description: `${formatFileSize(file.length)} • ${new Date(file.uploadDate).toLocaleDateString()}`,
977
+ // "Not in use": no application references it. Informational only —
978
+ // the file is never removed for that (ADR 0043 D5). A file taken down
979
+ // after a content claim says so: it is served to nobody (D10).
980
+ description: `${formatFileSize(file.length)} • ${new Date(file.uploadDate).toLocaleDateString()}${
981
+ file.status === 'blocked'
982
+ ? ` • ${t('fileManagement.removedAfterClaim')}`
983
+ : file.inUse === false
984
+ ? ` • ${t('fileManagement.notInUse')}`
985
+ : ''
986
+ }`,
916
987
  onPress: () => {
917
988
  // Support selection in regular mode with long press or if already selecting
918
989
  if (!selectMode && selectedIds.size > 0) {
@@ -955,7 +1026,12 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
955
1026
  iconOnly
956
1027
  stopPropagation
957
1028
  loading={deletingId === file.id}
958
- accessibilityLabel={`Delete ${file.filename}`}
1029
+ accessibilityLabel={t(
1030
+ file.status === 'blocked'
1031
+ ? 'fileManagement.a11y.deleteFile'
1032
+ : 'fileManagement.a11y.moveToTrash',
1033
+ { name: file.filename },
1034
+ )}
959
1035
  onPress={() => confirmFileDelete(file.id, file.filename)}
960
1036
  icon={<Ionicons name="trash" size={18} color={colors.error} />}
961
1037
  />
@@ -974,6 +1050,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
974
1050
  thumbSourceFor,
975
1051
  selectMode,
976
1052
  selectedIds,
1053
+ t,
977
1054
  ]);
978
1055
 
979
1056
  // Browse-themed terminal load-failure state — DISTINCT from the empty state.
@@ -1122,7 +1199,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1122
1199
  },
1123
1200
  {
1124
1201
  key: 'delete',
1125
- label: t('fileManagement.delete', { count: selectedIds.size }),
1202
+ label: t('fileManagement.moveToTrashCount', { count: selectedIds.size }),
1126
1203
  onPress: confirmBulkDelete,
1127
1204
  disabled: false,
1128
1205
  },
@@ -1387,6 +1464,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1387
1464
  onClose={handleCloseFile}
1388
1465
  onDownload={handleFileDownload}
1389
1466
  onDelete={confirmFileDelete}
1467
+ deleteLabel={
1468
+ openedFile.status === 'blocked'
1469
+ ? t('fileManagement.deleteFile')
1470
+ : t('fileManagement.moveToTrash')
1471
+ }
1390
1472
  isOwner={user?.id === targetUserId}
1391
1473
  />
1392
1474
  );
@@ -1494,6 +1576,17 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1494
1576
  </View>
1495
1577
  }
1496
1578
  />
1579
+ {user?.id === targetUserId && !selectMode && navigate && (
1580
+ <Button
1581
+ appearance="subtle"
1582
+ tone="neutral"
1583
+ size="sm"
1584
+ iconOnly
1585
+ onPress={() => navigate('FileTrash', userId ? { userId } : undefined)}
1586
+ accessibilityLabel={t('fileManagement.a11y.openTrash')}
1587
+ icon={<Ionicons name="trash-outline" size={20} color={colors.text} />}
1588
+ />
1589
+ )}
1497
1590
  {user?.id === targetUserId && (!selectMode || allowUploadInSelectMode) && (
1498
1591
  <Button
1499
1592
  iconOnly
@@ -0,0 +1,228 @@
1
+ import type React from 'react';
2
+ import { useCallback, useMemo, useRef, useState } from 'react';
3
+ import { View, type ScrollView } from 'react-native';
4
+ import { Button } from '@oxy.so/bloom/button';
5
+ import { Text } from '@oxy.so/bloom/typography';
6
+ import { toast } from '@oxy.so/bloom/toast';
7
+ import { surfaces } from '@oxy.so/bloom/surfaces';
8
+ import { useTheme } from '@oxy.so/bloom/theme';
9
+ import { useQueryClient } from '@tanstack/react-query';
10
+ import type { BaseScreenProps } from '../types/navigation';
11
+ import Ionicons from '../icons/Ionicons';
12
+ import { useOxy } from '../context/OxyContext';
13
+ import { useI18n } from '../hooks/useI18n';
14
+ import { useSurfaceHeader } from '../hooks/useSurfaceHeader';
15
+ import { queryKeys } from '../hooks/queries/queryKeys';
16
+ import { removeFileFromCache, useUserFilesInfinite } from '../hooks/queries/useFileQueries';
17
+ import { formatFileSize, getFileIcon } from '../utils/fileManagement';
18
+ import FileListSection, { type FileListItem } from './fileManagement/FileListSection';
19
+ import FileLibraryError from './fileManagement/FileLibraryError';
20
+ import { getErrorMessage } from './fileManagement/shared';
21
+ import { deletePermanently, restoreFromTrash } from './fileManagement/trashActions';
22
+
23
+ /**
24
+ * The person's Trash (ADR 0043 D5): the files they moved to trash, most
25
+ * recently trashed first. Each can be restored — served again, back in the
26
+ * library — or deleted permanently, after a confirmation (and a second one when
27
+ * an app still uses it). An account's trash never expires, so nothing here
28
+ * disappears on its own; it still counts toward the account's storage.
29
+ */
30
+ const FileTrashScreen: React.FC<BaseScreenProps & { userId?: string }> = ({ userId }) => {
31
+ const { user, oxyServices } = useOxy();
32
+ const { t } = useI18n();
33
+ const { colors } = useTheme();
34
+ const queryClient = useQueryClient();
35
+ const ownerId = userId || user?.id;
36
+
37
+ const trashQuery = useUserFilesInfinite(ownerId, 'trash');
38
+ const files = useMemo(
39
+ () => trashQuery.data?.pages.flatMap((page) => page.files) ?? [],
40
+ [trashQuery.data],
41
+ );
42
+ const scrollViewRef = useRef<ScrollView | null>(null);
43
+ // The file an action is in flight for: its buttons show progress.
44
+ const [busy, setBusy] = useState<{ id: string; restoring: boolean } | null>(null);
45
+
46
+ // Both lists change: a restored file returns to the library, a deleted one
47
+ // leaves the trash. `files.all` covers both keys.
48
+ const refreshFiles = useCallback(() => {
49
+ queryClient.invalidateQueries({ queryKey: queryKeys.files.all });
50
+ }, [queryClient]);
51
+
52
+ const handleRestore = useCallback(
53
+ async (fileId: string) => {
54
+ setBusy({ id: fileId, restoring: true });
55
+ try {
56
+ const outcome = await restoreFromTrash(oxyServices.assets, fileId);
57
+ if (outcome === 'restored') {
58
+ toast.success(t('fileManagement.toasts.restored'));
59
+ } else {
60
+ toast.error(t('fileManagement.toasts.notInTrash'));
61
+ }
62
+ removeFileFromCache(queryClient, ownerId, fileId, 'trash');
63
+ refreshFiles();
64
+ } catch (error: unknown) {
65
+ toast.error(getErrorMessage(error) || t('fileManagement.toasts.restoreFailed'));
66
+ } finally {
67
+ setBusy(null);
68
+ }
69
+ },
70
+ [oxyServices, queryClient, ownerId, refreshFiles, t],
71
+ );
72
+
73
+ const handleDeleteForever = useCallback(
74
+ async (fileId: string, filename: string) => {
75
+ const confirmed = await surfaces.confirm({
76
+ title: t('fileManagement.trashView.deleteForever'),
77
+ description: t('fileManagement.confirms.deleteForever', { filename }),
78
+ confirmLabel: t('fileManagement.trashView.deleteForever'),
79
+ cancelLabel: t('common.cancel') || 'Cancel',
80
+ destructive: true,
81
+ });
82
+ if (!confirmed) return;
83
+
84
+ setBusy({ id: fileId, restoring: false });
85
+ try {
86
+ const outcome = await deletePermanently(oxyServices.assets, fileId, (uses) =>
87
+ surfaces.confirm({
88
+ title: t('fileManagement.trashView.deleteForever'),
89
+ description: t('fileManagement.confirms.deleteForeverInUse', {
90
+ filename,
91
+ count: uses,
92
+ }),
93
+ confirmLabel: t('fileManagement.trashView.deleteForever'),
94
+ cancelLabel: t('common.cancel') || 'Cancel',
95
+ destructive: true,
96
+ }),
97
+ );
98
+ if (outcome === 'cancelled') return;
99
+ if (outcome === 'deleted') toast.success(t('fileManagement.toasts.deletedForever'));
100
+ removeFileFromCache(queryClient, ownerId, fileId, 'trash');
101
+ refreshFiles();
102
+ } catch (error: unknown) {
103
+ toast.error(getErrorMessage(error) || t('fileManagement.toasts.deleteFailed'));
104
+ } finally {
105
+ setBusy(null);
106
+ }
107
+ },
108
+ [oxyServices, queryClient, ownerId, refreshFiles, t],
109
+ );
110
+
111
+ const items = useMemo<FileListItem[]>(
112
+ () =>
113
+ files.map((file) => {
114
+ const trashedOn = file.trashedAt ? new Date(file.trashedAt).toLocaleDateString() : null;
115
+ return {
116
+ id: file.id,
117
+ icon: <Ionicons name={getFileIcon(file.contentType)} size={20} color={colors.primary} />,
118
+ title: file.filename,
119
+ description: trashedOn
120
+ ? `${formatFileSize(file.length)} • ${t('fileManagement.trashView.trashedOn', { date: trashedOn })}`
121
+ : formatFileSize(file.length),
122
+ // A trashed file is not served, so there is nothing to open.
123
+ onPress: () => {},
124
+ rightElement: (
125
+ <View className="flex-row items-center gap-1.5 ml-3">
126
+ <Button
127
+ appearance="subtle"
128
+ tone="neutral"
129
+ size="sm"
130
+ iconOnly
131
+ stopPropagation
132
+ disabled={busy !== null && busy.id !== file.id}
133
+ loading={busy?.id === file.id && busy.restoring}
134
+ accessibilityLabel={t('fileManagement.trashView.a11y.restore', {
135
+ name: file.filename,
136
+ })}
137
+ onPress={() => handleRestore(file.id)}
138
+ icon={<Ionicons name="arrow-undo" size={18} color={colors.text} />}
139
+ />
140
+ <Button
141
+ appearance="subtle"
142
+ tone="danger"
143
+ size="sm"
144
+ iconOnly
145
+ stopPropagation
146
+ disabled={busy !== null && busy.id !== file.id}
147
+ loading={busy?.id === file.id && !busy.restoring}
148
+ accessibilityLabel={t('fileManagement.trashView.a11y.deleteForever', {
149
+ name: file.filename,
150
+ })}
151
+ onPress={() => handleDeleteForever(file.id, file.filename)}
152
+ icon={<Ionicons name="trash" size={18} color={colors.error} />}
153
+ />
154
+ </View>
155
+ ),
156
+ };
157
+ }),
158
+ [files, colors, busy, handleRestore, handleDeleteForever, t],
159
+ );
160
+
161
+ useSurfaceHeader({
162
+ title: t('fileManagement.trashView.title'),
163
+ subtitle: t('fileManagement.trashView.subtitle'),
164
+ largeTitle: false,
165
+ });
166
+
167
+ const hasLoadError = trashQuery.isError && files.length === 0;
168
+ const renderEmptyState = () =>
169
+ hasLoadError ? (
170
+ <FileLibraryError
171
+ title={t('fileManagement.trashView.loadError')}
172
+ description={t('fileManagement.loadError.description')}
173
+ retryLabel={t('fileManagement.retry')}
174
+ onRetry={() => trashQuery.refetch()}
175
+ iconColor={colors.error}
176
+ titleColor={colors.text}
177
+ descriptionColor={colors.textSecondary}
178
+ />
179
+ ) : trashQuery.isLoading || !ownerId ? null : (
180
+ <View className="items-center py-[40px] px-[24px]">
181
+ <Ionicons name="trash-outline" size={64} color={colors.textTertiary} />
182
+ <Text
183
+ className="text-[24px] font-bold mt-[16px] mb-[8px] text-center"
184
+ style={{ color: colors.text }}
185
+ >
186
+ {t('fileManagement.trashView.emptyTitle')}
187
+ </Text>
188
+ <Text
189
+ className="text-[16px] text-center leading-[24px]"
190
+ style={{ color: colors.textSecondary }}
191
+ >
192
+ {t('fileManagement.trashView.emptyDescription')}
193
+ </Text>
194
+ </View>
195
+ );
196
+
197
+ return (
198
+ <View className="flex-1">
199
+ {files.length > 0 && (
200
+ <Text
201
+ className="text-[13px] leading-[18px] mx-[16px] mt-[4px] mb-[12px]"
202
+ style={{ color: colors.textSecondary }}
203
+ >
204
+ {t('fileManagement.trashView.notice')}
205
+ </Text>
206
+ )}
207
+ <FileListSection
208
+ scrollViewRef={scrollViewRef}
209
+ filteredFiles={files}
210
+ searchQuery=""
211
+ items={items}
212
+ paging={{
213
+ loadingMore: trashQuery.isFetchingNextPage,
214
+ hasMore: trashQuery.hasNextPage ?? false,
215
+ }}
216
+ refreshing={trashQuery.isRefetching}
217
+ colors={colors}
218
+ t={t}
219
+ onRefresh={() => trashQuery.refetch()}
220
+ onLoadMore={() => trashQuery.fetchNextPage()}
221
+ onClearSearch={() => {}}
222
+ renderEmptyState={renderEmptyState}
223
+ />
224
+ </View>
225
+ );
226
+ };
227
+
228
+ export default FileTrashScreen;