@oxyhq/services 6.6.0 → 6.6.2
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/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/Avatar.js +12 -3
- package/lib/commonjs/ui/components/Avatar.js.map +1 -1
- package/lib/commonjs/ui/components/ProfileCard.js +1 -1
- package/lib/commonjs/ui/components/ProfileCard.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +2 -3
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAssets.js +0 -2
- package/lib/commonjs/ui/hooks/useAssets.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAvatarPicker.js +3 -2
- package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js +26 -15
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js.map +1 -1
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +2 -2
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/accountStore.js +8 -39
- package/lib/commonjs/ui/stores/accountStore.js.map +1 -1
- package/lib/commonjs/ui/utils/avatarUtils.js +1 -26
- package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
- package/lib/commonjs/ui/utils/fileManagement.js +2 -2
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/Avatar.js +13 -4
- package/lib/module/ui/components/Avatar.js.map +1 -1
- package/lib/module/ui/components/ProfileCard.js +1 -1
- package/lib/module/ui/components/ProfileCard.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +2 -3
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/useAssets.js +0 -2
- package/lib/module/ui/hooks/useAssets.js.map +1 -1
- package/lib/module/ui/hooks/useAvatarPicker.js +4 -2
- package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/module/ui/hooks/useFileDownloadUrl.js +26 -15
- package/lib/module/ui/hooks/useFileDownloadUrl.js.map +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/module/ui/stores/accountStore.js +2 -33
- package/lib/module/ui/stores/accountStore.js.map +1 -1
- package/lib/module/ui/utils/avatarUtils.js +1 -25
- package/lib/module/ui/utils/avatarUtils.js.map +1 -1
- package/lib/module/ui/utils/fileManagement.js +1 -1
- package/lib/module/ui/utils/fileManagement.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/Avatar.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAssets.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts +6 -3
- package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/stores/accountStore.d.ts +2 -9
- package/lib/typescript/commonjs/ui/stores/accountStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts +0 -10
- package/lib/typescript/commonjs/ui/utils/avatarUtils.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/Avatar.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAssets.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts +6 -3
- package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
- package/lib/typescript/module/ui/stores/accountStore.d.ts +2 -9
- package/lib/typescript/module/ui/stores/accountStore.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/avatarUtils.d.ts +0 -10
- package/lib/typescript/module/ui/utils/avatarUtils.d.ts.map +1 -1
- package/package.json +7 -8
- package/src/index.ts +2 -0
- package/src/ui/components/Avatar.tsx +12 -3
- package/src/ui/components/ProfileCard.tsx +1 -1
- package/src/ui/hooks/mutations/useAccountMutations.ts +4 -5
- package/src/ui/hooks/useAssets.ts +1 -2
- package/src/ui/hooks/useAvatarPicker.ts +5 -3
- package/src/ui/hooks/useFileDownloadUrl.ts +33 -24
- package/src/ui/screens/WelcomeNewUserScreen.tsx +1 -1
- package/src/ui/stores/accountStore.ts +15 -47
- package/src/ui/utils/avatarUtils.ts +1 -31
- package/src/ui/utils/fileManagement.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAssets.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useAssets.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,KAAK,EAGL,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAKrB,eAAO,MAAM,mBAAmB,GAAI,UAAU,WAAW,SAExD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;mBA6BZ,IAAI,aACC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAC7B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"useAssets.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useAssets.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EACL,KAAK,EAGL,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAKrB,eAAO,MAAM,mBAAmB,GAAI,UAAU,WAAW,SAExD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;mBA6BZ,IAAI,aACC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAC7B,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;oBAoDb,MAAM,OACV,MAAM,cACC,MAAM,YACR,MAAM,KACf,OAAO,CAAC,IAAI,CAAC;sBAsCL,MAAM,OACV,MAAM,cACC,MAAM,YACR,MAAM,KACf,OAAO,CAAC,IAAI,CAAC;sBA2BL,MAAM,YACL,MAAM,cACJ,MAAM,KACjB,OAAO,CAAC,MAAM,CAAC;wBAc2B,MAAM,KAAG,OAAO,CAAC,KAAK,CAAC;2BAmBzD,MAAM,UACR,OAAO,KACb,OAAO,CAAC,IAAI,CAAC;uBAoB4B,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;2BAgBlB,MAAM;;;;;;;CAuCvD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAvatarPicker.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useAvatarPicker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"useAvatarPicker.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useAvatarPicker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAItD,UAAU,sBAAsB;IAC9B,WAAW,EAAE,WAAW,CAAC;IACzB,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,WAAW,CAAC;IACzB,eAAe,EAAE,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;CAC3F;AAED,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,eAAe,EACf,eAAe,EACf,WAAW,EACX,eAAe,GAChB,EAAE,sBAAsB;;EAiCxB"}
|
|
@@ -12,8 +12,11 @@ export interface UseFileDownloadUrlResult {
|
|
|
12
12
|
/**
|
|
13
13
|
* Hook to resolve a file's download URL asynchronously.
|
|
14
14
|
*
|
|
15
|
-
* Prefers `
|
|
16
|
-
*
|
|
15
|
+
* Prefers the provided `oxyServices` instance, falls back to the module-level
|
|
16
|
+
* singleton set via `setOxyFileUrlInstance`.
|
|
17
|
+
*
|
|
18
|
+
* Uses `getFileDownloadUrlAsync` first, falling back to the synchronous
|
|
19
|
+
* `getFileDownloadUrl` if the async call fails.
|
|
17
20
|
*/
|
|
18
|
-
export declare const useFileDownloadUrl: (
|
|
21
|
+
export declare const useFileDownloadUrl: (fileIdOrServices?: string | OxyServices | null, fileIdOrOptions?: string | UseFileDownloadUrlOptions | null, maybeOptions?: UseFileDownloadUrlOptions) => UseFileDownloadUrlResult;
|
|
19
22
|
//# sourceMappingURL=useFileDownloadUrl.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFileDownloadUrl.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useFileDownloadUrl.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C,eAAO,MAAM,qBAAqB,GAAI,UAAU,WAAW,SAE1D,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED
|
|
1
|
+
{"version":3,"file":"useFileDownloadUrl.d.ts","sourceRoot":"","sources":["../../../../../src/ui/hooks/useFileDownloadUrl.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C,eAAO,MAAM,qBAAqB,GAAI,UAAU,WAAW,SAE1D,CAAC;AAEF,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC7B,mBAAmB,MAAM,GAAG,WAAW,GAAG,IAAI,EAC9C,kBAAkB,MAAM,GAAG,yBAAyB,GAAG,IAAI,EAC3D,eAAe,yBAAyB,KACvC,wBA6FF,CAAC"}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import type { OxyServices } from '@oxyhq/core';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
userId?: string;
|
|
5
|
-
username: string;
|
|
6
|
-
displayName: string;
|
|
7
|
-
avatar?: string;
|
|
8
|
-
avatarUrl?: string;
|
|
9
|
-
}
|
|
2
|
+
import type { QuickAccount } from '@oxyhq/core';
|
|
3
|
+
export type { QuickAccount };
|
|
10
4
|
interface AccountState {
|
|
11
5
|
accounts: Record<string, QuickAccount>;
|
|
12
6
|
accountOrder: string[];
|
|
@@ -30,5 +24,4 @@ export declare const useAccounts: () => QuickAccount[];
|
|
|
30
24
|
export declare const useAccountLoading: () => boolean;
|
|
31
25
|
export declare const useAccountError: () => string | null;
|
|
32
26
|
export declare const useAccountLoadingSession: (sessionId: string) => boolean;
|
|
33
|
-
export {};
|
|
34
27
|
//# sourceMappingURL=accountStore.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accountStore.d.ts","sourceRoot":"","sources":["../../../../../src/ui/stores/accountStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,
|
|
1
|
+
{"version":3,"file":"accountStore.d.ts","sourceRoot":"","sources":["../../../../../src/ui/stores/accountStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B,UAAU,YAAY;IAElB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACvC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,YAAY,EAAE,CAAC;IAG9B,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAG/B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAGrB,WAAW,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IAChD,UAAU,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAC5C,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,gBAAgB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAG9C,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IAGjE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAGzC,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,CAAC,EAAE,YAAY,EAAE,EAAE,aAAa,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAG5I,KAAK,EAAE,MAAM,IAAI,CAAC;CACrB;AAYD,eAAO,MAAM,eAAe,2EAwMzB,CAAC;AAGJ,eAAO,MAAM,WAAW,QAAO,YAAY,EAE1C,CAAC;AAEF,eAAO,MAAM,iBAAiB,eAAwC,CAAC;AACvE,eAAO,MAAM,eAAe,qBAAsC,CAAC;AACnE,eAAO,MAAM,wBAAwB,GAAI,WAAW,MAAM,YACE,CAAC"}
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import type { OxyServices, User } from '@oxyhq/core';
|
|
2
2
|
import { QueryClient } from '@tanstack/react-query';
|
|
3
|
-
/**
|
|
4
|
-
* Updates file visibility to public for avatar use.
|
|
5
|
-
* Handles errors gracefully, only logging non-404 errors.
|
|
6
|
-
*
|
|
7
|
-
* @param fileId - The file ID to update visibility for
|
|
8
|
-
* @param oxyServices - OxyServices instance
|
|
9
|
-
* @param contextName - Optional context name for logging
|
|
10
|
-
* @returns Promise that resolves when visibility is updated (or skipped)
|
|
11
|
-
*/
|
|
12
|
-
export declare function updateAvatarVisibility(fileId: string | undefined, oxyServices: OxyServices, contextName?: string): Promise<void>;
|
|
13
3
|
/**
|
|
14
4
|
* Refreshes avatar in accountStore with cache-busted URL to force image reload.
|
|
15
5
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"avatarUtils.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/avatarUtils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGpD
|
|
1
|
+
{"version":3,"file":"avatarUtils.d.ts","sourceRoot":"","sources":["../../../../../src/ui/utils/avatarUtils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAGrD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGpD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,WAAW,GACvB,IAAI,CAON;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,EACtB,WAAW,EAAE,WAAW,EACxB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,WAAW,EAAE,WAAW,EACxB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAChC,OAAO,CAAC,IAAI,CAAC,CA2Bf"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/services",
|
|
3
|
-
"version": "6.6.
|
|
3
|
+
"version": "6.6.2",
|
|
4
4
|
"description": "OxyHQ Expo/React Native SDK — UI components, screens, and native features",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -51,15 +51,14 @@
|
|
|
51
51
|
"author": "OxyHQ",
|
|
52
52
|
"license": "MIT",
|
|
53
53
|
"engines": {
|
|
54
|
-
"node": ">=18.0.0"
|
|
55
|
-
"npm": ">=9.0.0"
|
|
54
|
+
"node": ">=18.0.0"
|
|
56
55
|
},
|
|
57
56
|
"homepage": "https://oxy.so",
|
|
58
57
|
"scripts": {
|
|
59
58
|
"typescript": "tsc --skipLibCheck --noEmit",
|
|
60
59
|
"lint": "biome lint --error-on-warnings ./src",
|
|
61
|
-
"build": "
|
|
62
|
-
"build:js": "
|
|
60
|
+
"build": "bun x react-native-builder-bob build && bun run copy-assets && bun run copy-dts && bun run delete-dts.js && bun run delete-debug-view",
|
|
61
|
+
"build:js": "bun x react-native-builder-bob build --target commonjs && bun x react-native-builder-bob build --target module && bun run copy-assets",
|
|
63
62
|
"test": "jest --passWithNoTests",
|
|
64
63
|
"test:watch": "jest --watch",
|
|
65
64
|
"test:coverage": "jest --coverage",
|
|
@@ -67,8 +66,8 @@
|
|
|
67
66
|
"copy-dts": "copyfiles -u 1 \"src/**/*.d.ts\" lib/typescript",
|
|
68
67
|
"delete-debug-view": "rm -rf ./lib/commonjs/components/bottomSheetDebugView ./lib/module/components/bottomSheetDebugView ./lib/typescript/components/bottomSheetDebugView || true",
|
|
69
68
|
"delete-dts.js": "find ./lib/commonjs -name '*.d.js*' -delete && find ./lib/module -name '*.d.js*' -delete",
|
|
70
|
-
"release": "rm -rf lib &&
|
|
71
|
-
"prepublishOnly": "
|
|
69
|
+
"release": "rm -rf lib && bun run build && release-it",
|
|
70
|
+
"prepublishOnly": "bun run typescript && bun run test && bun run build"
|
|
72
71
|
},
|
|
73
72
|
"dependencies": {
|
|
74
73
|
"@lottiefiles/dotlottie-react": "^0.13.5",
|
|
@@ -79,7 +78,7 @@
|
|
|
79
78
|
"@types/react": "*",
|
|
80
79
|
"copyfiles": "^2.4.1",
|
|
81
80
|
"expo-blur": "~55.0.8",
|
|
82
|
-
"expo-checkbox": "~55.0.
|
|
81
|
+
"expo-checkbox": "~55.0.3",
|
|
83
82
|
"expo-crypto": "~55.0.8",
|
|
84
83
|
"expo-print": "~55.0.8",
|
|
85
84
|
"expo-secure-store": "~55.0.8",
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,8 @@ export { useFileFiltering } from './ui/hooks/useFileFiltering';
|
|
|
92
92
|
export type { ViewMode, SortBy, SortOrder } from './ui/hooks/useFileFiltering';
|
|
93
93
|
|
|
94
94
|
// UI components
|
|
95
|
+
export { default as Avatar } from './ui/components/Avatar';
|
|
96
|
+
export type { AvatarProps } from './ui/components/Avatar';
|
|
95
97
|
export { OxySignInButton } from './ui/components/OxySignInButton';
|
|
96
98
|
export { OxyLogo, FollowButton } from './ui';
|
|
97
99
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { memo, useMemo } from 'react';
|
|
2
|
+
import { memo, useMemo, useState, useEffect } from 'react';
|
|
3
3
|
import { View, Text, Image, StyleSheet, type StyleProp, type ViewStyle, type ImageStyle, type TextStyle, ActivityIndicator, Platform } from 'react-native';
|
|
4
4
|
import { useThemeColors } from '../styles';
|
|
5
5
|
import { fontFamilies } from '../styles/fonts';
|
|
@@ -148,6 +148,14 @@ const Avatar: React.FC<AvatarProps> = ({
|
|
|
148
148
|
isLoading = false,
|
|
149
149
|
}) => {
|
|
150
150
|
const colors = useThemeColors(theme);
|
|
151
|
+
const [imageError, setImageError] = useState(false);
|
|
152
|
+
|
|
153
|
+
// Reset error state when uri changes
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (uri) {
|
|
156
|
+
setImageError(false);
|
|
157
|
+
}
|
|
158
|
+
}, [uri]);
|
|
151
159
|
|
|
152
160
|
const displayText = useMemo(
|
|
153
161
|
() => text || (name ? getInitials(name) : ''),
|
|
@@ -221,8 +229,8 @@ const Avatar: React.FC<AvatarProps> = ({
|
|
|
221
229
|
);
|
|
222
230
|
}
|
|
223
231
|
|
|
224
|
-
// Image avatar
|
|
225
|
-
if (uri) {
|
|
232
|
+
// Image avatar (with fallback to initials on error)
|
|
233
|
+
if (uri && !imageError) {
|
|
226
234
|
return (
|
|
227
235
|
<View
|
|
228
236
|
style={[
|
|
@@ -236,6 +244,7 @@ const Avatar: React.FC<AvatarProps> = ({
|
|
|
236
244
|
source={{ uri }}
|
|
237
245
|
style={[styles.image, containerStyle, imageStyle]}
|
|
238
246
|
resizeMode="cover"
|
|
247
|
+
onError={() => setImageError(true)}
|
|
239
248
|
/>
|
|
240
249
|
</View>
|
|
241
250
|
);
|
|
@@ -37,7 +37,7 @@ const ProfileCard: React.FC<ProfileCardProps> = ({
|
|
|
37
37
|
const secondaryBackgroundColor = themeStyles.secondaryBackgroundColor;
|
|
38
38
|
const primaryColor = '#0066CC';
|
|
39
39
|
|
|
40
|
-
const avatarUrl = useFileDownloadUrl(user?.avatar, { variant: 'thumb' }).url || undefined;
|
|
40
|
+
const avatarUrl = useFileDownloadUrl(oxyServices, user?.avatar, { variant: 'thumb' }).url || undefined;
|
|
41
41
|
|
|
42
42
|
return (
|
|
43
43
|
<View style={styles.headerSection}>
|
|
@@ -91,12 +91,11 @@ export const useUploadAvatar = () => {
|
|
|
91
91
|
return useMutation({
|
|
92
92
|
mutationFn: async (file: { uri: string; type?: string; name?: string; size?: number }) => {
|
|
93
93
|
return authenticatedApiCall<User>(oxyServices, activeSessionId, async () => {
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const fileId = uploadResult?.file?.id || uploadResult?.id || uploadResult;
|
|
94
|
+
const uploadResult = await oxyServices.assetUpload(file, 'public');
|
|
95
|
+
const fileId = uploadResult?.file?.id;
|
|
97
96
|
|
|
98
97
|
if (!fileId || typeof fileId !== 'string') {
|
|
99
|
-
throw new Error('
|
|
98
|
+
throw new Error('Upload succeeded but response did not contain a file ID');
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
// Update profile with file ID
|
|
@@ -336,7 +335,7 @@ export const useUploadFile = () => {
|
|
|
336
335
|
return authenticatedApiCall<UploadResult>(
|
|
337
336
|
oxyServices,
|
|
338
337
|
activeSessionId,
|
|
339
|
-
() => oxyServices.assetUpload(file
|
|
338
|
+
() => oxyServices.assetUpload(file, visibility, metadata, onProgress)
|
|
340
339
|
);
|
|
341
340
|
},
|
|
342
341
|
});
|
|
@@ -58,8 +58,7 @@ export const useAssets = () => {
|
|
|
58
58
|
clearErrors();
|
|
59
59
|
setUploading(true);
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
const result = await oxyInstance.assetUpload(file as any, undefined, metadata);
|
|
61
|
+
const result = await oxyInstance.assetUpload(file, undefined, metadata);
|
|
63
62
|
|
|
64
63
|
// Update progress with final status
|
|
65
64
|
if (result?.file) {
|
|
@@ -12,7 +12,8 @@ import { translate } from '@oxyhq/core';
|
|
|
12
12
|
import type { QueryClient } from '@tanstack/react-query';
|
|
13
13
|
import { toast } from '../../lib/sonner';
|
|
14
14
|
import type { RouteName } from '../navigation/routes';
|
|
15
|
-
import { updateAvatarVisibility
|
|
15
|
+
import { updateAvatarVisibility } from '@oxyhq/core';
|
|
16
|
+
import { updateProfileWithAvatar } from '../utils/avatarUtils';
|
|
16
17
|
|
|
17
18
|
interface UseAvatarPickerOptions {
|
|
18
19
|
oxyServices: OxyServices;
|
|
@@ -51,8 +52,9 @@ export function useAvatarPicker({
|
|
|
51
52
|
queryClient
|
|
52
53
|
);
|
|
53
54
|
toast.success(translate(currentLanguage ?? undefined, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
|
|
54
|
-
} catch (e:
|
|
55
|
-
|
|
55
|
+
} catch (e: unknown) {
|
|
56
|
+
const message = e instanceof Error ? e.message : undefined;
|
|
57
|
+
toast.error(message || translate(currentLanguage ?? undefined, 'editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
|
|
56
58
|
}
|
|
57
59
|
},
|
|
58
60
|
},
|
|
@@ -21,13 +21,34 @@ export interface UseFileDownloadUrlResult {
|
|
|
21
21
|
/**
|
|
22
22
|
* Hook to resolve a file's download URL asynchronously.
|
|
23
23
|
*
|
|
24
|
-
* Prefers `
|
|
25
|
-
*
|
|
24
|
+
* Prefers the provided `oxyServices` instance, falls back to the module-level
|
|
25
|
+
* singleton set via `setOxyFileUrlInstance`.
|
|
26
|
+
*
|
|
27
|
+
* Uses `getFileDownloadUrlAsync` first, falling back to the synchronous
|
|
28
|
+
* `getFileDownloadUrl` if the async call fails.
|
|
26
29
|
*/
|
|
27
30
|
export const useFileDownloadUrl = (
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
fileIdOrServices?: string | OxyServices | null,
|
|
32
|
+
fileIdOrOptions?: string | UseFileDownloadUrlOptions | null,
|
|
33
|
+
maybeOptions?: UseFileDownloadUrlOptions
|
|
30
34
|
): UseFileDownloadUrlResult => {
|
|
35
|
+
// Support two call signatures:
|
|
36
|
+
// 1. useFileDownloadUrl(oxyServices, fileId, options) — preferred
|
|
37
|
+
// 2. useFileDownloadUrl(fileId, options) — legacy (uses singleton)
|
|
38
|
+
let services: OxyServices | null;
|
|
39
|
+
let fileId: string | null | undefined;
|
|
40
|
+
let options: UseFileDownloadUrlOptions | undefined;
|
|
41
|
+
|
|
42
|
+
if (fileIdOrServices instanceof OxyServices) {
|
|
43
|
+
services = fileIdOrServices;
|
|
44
|
+
fileId = typeof fileIdOrOptions === 'string' ? fileIdOrOptions : null;
|
|
45
|
+
options = maybeOptions;
|
|
46
|
+
} else {
|
|
47
|
+
services = oxyInstance;
|
|
48
|
+
fileId = typeof fileIdOrServices === 'string' ? fileIdOrServices : null;
|
|
49
|
+
options = typeof fileIdOrOptions === 'object' && fileIdOrOptions !== null ? fileIdOrOptions as UseFileDownloadUrlOptions : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
const [url, setUrl] = useState<string | null>(null);
|
|
32
53
|
const [loading, setLoading] = useState(false);
|
|
33
54
|
const [error, setError] = useState<Error | null>(null);
|
|
@@ -40,8 +61,7 @@ export const useFileDownloadUrl = (
|
|
|
40
61
|
return;
|
|
41
62
|
}
|
|
42
63
|
|
|
43
|
-
if (!
|
|
44
|
-
// Fail silently but don't crash the UI – caller can decide what to do with null URL.
|
|
64
|
+
if (!services) {
|
|
45
65
|
setUrl(null);
|
|
46
66
|
setLoading(false);
|
|
47
67
|
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
@@ -49,40 +69,33 @@ export const useFileDownloadUrl = (
|
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
let cancelled = false;
|
|
72
|
+
const instance = services;
|
|
52
73
|
|
|
53
74
|
const load = async () => {
|
|
54
75
|
setLoading(true);
|
|
55
76
|
setError(null);
|
|
56
77
|
|
|
57
|
-
// Store instance in local variable for TypeScript null checking
|
|
58
|
-
const instance = oxyInstance;
|
|
59
|
-
if (!instance) {
|
|
60
|
-
setLoading(false);
|
|
61
|
-
setError(new Error('OxyServices instance not configured for useFileDownloadUrl'));
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
78
|
try {
|
|
66
79
|
const { variant, expiresIn } = options || {};
|
|
67
80
|
let resolvedUrl: string | null = null;
|
|
68
81
|
|
|
69
82
|
if (typeof instance.getFileDownloadUrlAsync === 'function') {
|
|
70
|
-
resolvedUrl = await instance.getFileDownloadUrlAsync(fileId
|
|
83
|
+
resolvedUrl = await instance.getFileDownloadUrlAsync(fileId!, variant, expiresIn);
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
if (!resolvedUrl && typeof instance.getFileDownloadUrl === 'function') {
|
|
74
|
-
resolvedUrl = instance.getFileDownloadUrl(fileId
|
|
87
|
+
resolvedUrl = instance.getFileDownloadUrl(fileId!, variant, expiresIn);
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
if (!cancelled) {
|
|
78
91
|
setUrl(resolvedUrl || null);
|
|
79
92
|
}
|
|
80
|
-
} catch (err:
|
|
93
|
+
} catch (err: unknown) {
|
|
81
94
|
// Fallback to sync URL on error where possible
|
|
82
95
|
try {
|
|
83
96
|
if (typeof instance.getFileDownloadUrl === 'function') {
|
|
84
97
|
const { variant, expiresIn } = options || {};
|
|
85
|
-
const fallbackUrl = instance.getFileDownloadUrl(fileId
|
|
98
|
+
const fallbackUrl = instance.getFileDownloadUrl(fileId!, variant, expiresIn);
|
|
86
99
|
if (!cancelled) {
|
|
87
100
|
setUrl(fallbackUrl || null);
|
|
88
101
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -90,7 +103,7 @@ export const useFileDownloadUrl = (
|
|
|
90
103
|
return;
|
|
91
104
|
}
|
|
92
105
|
} catch {
|
|
93
|
-
// ignore secondary failure
|
|
106
|
+
// ignore secondary failure
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
if (!cancelled) {
|
|
@@ -108,11 +121,7 @@ export const useFileDownloadUrl = (
|
|
|
108
121
|
return () => {
|
|
109
122
|
cancelled = true;
|
|
110
123
|
};
|
|
111
|
-
}, [fileId, options?.variant, options?.expiresIn]);
|
|
124
|
+
}, [fileId, services, options?.variant, options?.expiresIn]);
|
|
112
125
|
|
|
113
126
|
return { url, loading, error };
|
|
114
127
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
@@ -13,7 +13,7 @@ import GroupedPillButtons from '../components/internal/GroupedPillButtons';
|
|
|
13
13
|
import { useI18n } from '../hooks/useI18n';
|
|
14
14
|
import { useOxy } from '../context/OxyContext';
|
|
15
15
|
import { useUpdateProfile } from '../hooks/mutations/useAccountMutations';
|
|
16
|
-
import { updateAvatarVisibility } from '
|
|
16
|
+
import { updateAvatarVisibility } from '@oxyhq/core';
|
|
17
17
|
|
|
18
18
|
const GAP = 12;
|
|
19
19
|
const INNER_GAP = 8;
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { shallow } from 'zustand/shallow';
|
|
3
3
|
import type { OxyServices } from '@oxyhq/core';
|
|
4
|
+
import { buildAccountsArray, createQuickAccount } from '@oxyhq/core';
|
|
5
|
+
import type { QuickAccount } from '@oxyhq/core';
|
|
4
6
|
|
|
5
|
-
export
|
|
6
|
-
sessionId: string;
|
|
7
|
-
userId?: string; // User ID for deduplication
|
|
8
|
-
username: string;
|
|
9
|
-
displayName: string;
|
|
10
|
-
avatar?: string;
|
|
11
|
-
avatarUrl?: string; // Cached avatar URL to prevent recalculation
|
|
12
|
-
}
|
|
7
|
+
export type { QuickAccount };
|
|
13
8
|
|
|
14
9
|
interface AccountState {
|
|
15
10
|
// Account data
|
|
@@ -54,38 +49,6 @@ const initialState = {
|
|
|
54
49
|
error: null,
|
|
55
50
|
};
|
|
56
51
|
|
|
57
|
-
// Helper: Build accounts array from accounts map and order
|
|
58
|
-
const buildAccountsArray = (accounts: Record<string, QuickAccount>, order: string[]): QuickAccount[] => {
|
|
59
|
-
const result: QuickAccount[] = [];
|
|
60
|
-
for (const id of order) {
|
|
61
|
-
const account = accounts[id];
|
|
62
|
-
if (account) result.push(account);
|
|
63
|
-
}
|
|
64
|
-
return result;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Helper: Create QuickAccount from user data
|
|
68
|
-
const createQuickAccount = (sessionId: string, userData: any, existingAccount?: QuickAccount, oxyServices?: OxyServices): QuickAccount => {
|
|
69
|
-
const displayName = userData.name?.full || userData.name?.first || userData.username || 'Account';
|
|
70
|
-
const userId = userData.id || userData._id?.toString();
|
|
71
|
-
|
|
72
|
-
// Preserve existing avatarUrl if avatar hasn't changed (prevents image reload)
|
|
73
|
-
let avatarUrl: string | undefined;
|
|
74
|
-
if (existingAccount && existingAccount.avatar === userData.avatar && existingAccount.avatarUrl) {
|
|
75
|
-
avatarUrl = existingAccount.avatarUrl; // Reuse existing URL
|
|
76
|
-
} else if (userData.avatar && oxyServices) {
|
|
77
|
-
avatarUrl = oxyServices.getFileDownloadUrl(userData.avatar, 'thumb');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
sessionId,
|
|
82
|
-
userId,
|
|
83
|
-
username: userData.username || '',
|
|
84
|
-
displayName,
|
|
85
|
-
avatar: userData.avatar,
|
|
86
|
-
avatarUrl,
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
52
|
|
|
90
53
|
export const useAccountStore = create<AccountState>((set, get) => ({
|
|
91
54
|
...initialState,
|
|
@@ -116,7 +79,7 @@ export const useAccountStore = create<AccountState>((set, get) => ({
|
|
|
116
79
|
existing.avatarUrl === newAccount.avatarUrl;
|
|
117
80
|
});
|
|
118
81
|
|
|
119
|
-
if (sameAccounts) return {} as
|
|
82
|
+
if (sameAccounts) return {} as Partial<AccountState>;
|
|
120
83
|
|
|
121
84
|
return { accounts: accountMap, accountOrder: order, accountsArray };
|
|
122
85
|
}),
|
|
@@ -127,7 +90,7 @@ export const useAccountStore = create<AccountState>((set, get) => ({
|
|
|
127
90
|
// Update existing
|
|
128
91
|
const existing = state.accounts[account.sessionId];
|
|
129
92
|
if (existing.avatar === account.avatar && existing.avatarUrl === account.avatarUrl) {
|
|
130
|
-
return {} as
|
|
93
|
+
return {} as Partial<AccountState>; // No change
|
|
131
94
|
}
|
|
132
95
|
const newAccounts = { ...state.accounts, [account.sessionId]: account };
|
|
133
96
|
return {
|
|
@@ -147,11 +110,11 @@ export const useAccountStore = create<AccountState>((set, get) => ({
|
|
|
147
110
|
|
|
148
111
|
updateAccount: (sessionId, updates) => set((state) => {
|
|
149
112
|
const existing = state.accounts[sessionId];
|
|
150
|
-
if (!existing) return {} as
|
|
113
|
+
if (!existing) return {} as Partial<AccountState>;
|
|
151
114
|
|
|
152
115
|
const updated = { ...existing, ...updates };
|
|
153
116
|
if (existing.avatar === updated.avatar && existing.avatarUrl === updated.avatarUrl) {
|
|
154
|
-
return {} as
|
|
117
|
+
return {} as Partial<AccountState>; // No change
|
|
155
118
|
}
|
|
156
119
|
|
|
157
120
|
const newAccounts = { ...state.accounts, [sessionId]: updated };
|
|
@@ -162,7 +125,7 @@ export const useAccountStore = create<AccountState>((set, get) => ({
|
|
|
162
125
|
}),
|
|
163
126
|
|
|
164
127
|
removeAccount: (sessionId) => set((state) => {
|
|
165
|
-
if (!state.accounts[sessionId]) return {} as
|
|
128
|
+
if (!state.accounts[sessionId]) return {} as Partial<AccountState>;
|
|
166
129
|
|
|
167
130
|
const { [sessionId]: _removed, ...rest } = state.accounts;
|
|
168
131
|
const newOrder = state.accountOrder.filter(id => id !== sessionId);
|
|
@@ -175,7 +138,7 @@ export const useAccountStore = create<AccountState>((set, get) => ({
|
|
|
175
138
|
}),
|
|
176
139
|
|
|
177
140
|
moveAccountToTop: (sessionId) => set((state) => {
|
|
178
|
-
if (!state.accounts[sessionId]) return {} as
|
|
141
|
+
if (!state.accounts[sessionId]) return {} as Partial<AccountState>;
|
|
179
142
|
|
|
180
143
|
const filtered = state.accountOrder.filter(id => id !== sessionId);
|
|
181
144
|
const newOrder = [sessionId, ...filtered];
|
|
@@ -242,7 +205,12 @@ export const useAccountStore = create<AccountState>((set, get) => ({
|
|
|
242
205
|
for (const { sessionId, user: userData } of batchResults) {
|
|
243
206
|
if (userData && !accountMap.has(sessionId)) {
|
|
244
207
|
const existing = existingMap.get(sessionId);
|
|
245
|
-
accountMap.set(sessionId, createQuickAccount(
|
|
208
|
+
accountMap.set(sessionId, createQuickAccount(
|
|
209
|
+
sessionId,
|
|
210
|
+
userData,
|
|
211
|
+
existing,
|
|
212
|
+
(fileId, variant) => oxyServices.getFileDownloadUrl(fileId, variant)
|
|
213
|
+
));
|
|
246
214
|
}
|
|
247
215
|
}
|
|
248
216
|
|
|
@@ -5,38 +5,9 @@ import { useAuthStore } from '../stores/authStore';
|
|
|
5
5
|
import { QueryClient } from '@tanstack/react-query';
|
|
6
6
|
import { queryKeys, invalidateUserQueries, invalidateAccountQueries } from '../hooks/queries/queryKeys';
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Updates file visibility to public for avatar use.
|
|
10
|
-
* Handles errors gracefully, only logging non-404 errors.
|
|
11
|
-
*
|
|
12
|
-
* @param fileId - The file ID to update visibility for
|
|
13
|
-
* @param oxyServices - OxyServices instance
|
|
14
|
-
* @param contextName - Optional context name for logging
|
|
15
|
-
* @returns Promise that resolves when visibility is updated (or skipped)
|
|
16
|
-
*/
|
|
17
|
-
export async function updateAvatarVisibility(
|
|
18
|
-
fileId: string | undefined,
|
|
19
|
-
oxyServices: OxyServices,
|
|
20
|
-
contextName: string = 'AvatarUtils'
|
|
21
|
-
): Promise<void> {
|
|
22
|
-
// Skip if temporary asset ID or no file ID
|
|
23
|
-
if (!fileId || fileId.startsWith('temp-')) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
await oxyServices.assetUpdateVisibility(fileId, 'public');
|
|
29
|
-
// Visibility update is logged by the API
|
|
30
|
-
} catch (visError: any) {
|
|
31
|
-
// Silently handle errors - 404 means asset doesn't exist yet (which is OK)
|
|
32
|
-
// Other errors are logged by the API, so no need to log here
|
|
33
|
-
// Function continues gracefully regardless of visibility update success
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
8
|
/**
|
|
38
9
|
* Refreshes avatar in accountStore with cache-busted URL to force image reload.
|
|
39
|
-
*
|
|
10
|
+
*
|
|
40
11
|
* @param sessionId - The session ID for the account to update
|
|
41
12
|
* @param avatarFileId - The new avatar file ID
|
|
42
13
|
* @param oxyServices - OxyServices instance to generate download URL
|
|
@@ -99,4 +70,3 @@ export async function updateProfileWithAvatar(
|
|
|
99
70
|
|
|
100
71
|
return data;
|
|
101
72
|
}
|
|
102
|
-
|
|
@@ -3,7 +3,7 @@ import type { FileMetadata } from '@oxyhq/core';
|
|
|
3
3
|
import { File as ExpoFile } from 'expo-file-system';
|
|
4
4
|
import { toast } from '../../lib/sonner';
|
|
5
5
|
import type { RouteName } from '../navigation/routes';
|
|
6
|
-
import { updateAvatarVisibility } from '
|
|
6
|
+
import { updateAvatarVisibility } from '@oxyhq/core';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Format file size in bytes to human-readable string
|