@oxyhq/services 22.4.2 → 22.6.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.
- package/LICENSE +661 -21
- package/README.md +1 -1
- package/lib/commonjs/index.js +20 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/client.js +1 -1
- package/lib/commonjs/ui/components/FontLoader.js +18 -145
- package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
- package/lib/commonjs/ui/components/FontLoader.native.js +88 -0
- package/lib/commonjs/ui/components/FontLoader.native.js.map +1 -0
- package/lib/commonjs/ui/components/OxyProvider.js +1 -1
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js +16 -27
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/styles.js +1 -225
- package/lib/commonjs/ui/components/fileManagement/styles.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +5 -2
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/useOxyAccountGraph.js +7 -0
- package/lib/commonjs/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +14 -5
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/userCache.js +286 -0
- package/lib/commonjs/ui/hooks/queries/userCache.js.map +1 -0
- package/lib/commonjs/ui/hooks/useAvatarPicker.js +23 -8
- package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js +7 -21
- package/lib/commonjs/ui/hooks/useFileDownloadUrl.js.map +1 -1
- package/lib/commonjs/ui/hooks/useResolvedFileUrls.js +163 -0
- package/lib/commonjs/ui/hooks/useResolvedFileUrls.js.map +1 -0
- package/lib/commonjs/ui/index.js +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +51 -22
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js +216 -203
- package/lib/commonjs/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js +26 -9
- package/lib/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/commonjs/ui/screens/fileManagement/photoGridLayout.js +44 -0
- package/lib/commonjs/ui/screens/fileManagement/photoGridLayout.js.map +1 -0
- package/lib/commonjs/ui/session/tokenTransport.js +21 -10
- package/lib/commonjs/ui/session/tokenTransport.js.map +1 -1
- package/lib/commonjs/ui/utils/fileManagement.js +0 -40
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/index.js +7 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/client.js +1 -1
- package/lib/module/ui/client.js.map +1 -1
- package/lib/module/ui/components/FontLoader.js +18 -146
- package/lib/module/ui/components/FontLoader.js.map +1 -1
- package/lib/module/ui/components/FontLoader.native.js +83 -0
- package/lib/module/ui/components/FontLoader.native.js.map +1 -0
- package/lib/module/ui/components/OxyProvider.js +1 -1
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js +16 -27
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/module/ui/components/fileManagement/styles.js +0 -224
- package/lib/module/ui/components/fileManagement/styles.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +5 -2
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/useOxyAccountGraph.js +7 -0
- package/lib/module/ui/context/useOxyAccountGraph.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +14 -5
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/queries/userCache.js +282 -0
- package/lib/module/ui/hooks/queries/userCache.js.map +1 -0
- package/lib/module/ui/hooks/useAvatarPicker.js +23 -8
- package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -1
- package/lib/module/ui/hooks/useFileDownloadUrl.js +7 -21
- package/lib/module/ui/hooks/useFileDownloadUrl.js.map +1 -1
- package/lib/module/ui/hooks/useResolvedFileUrls.js +157 -0
- package/lib/module/ui/hooks/useResolvedFileUrls.js.map +1 -0
- package/lib/module/ui/index.js +1 -1
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +52 -23
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js +218 -205
- package/lib/module/ui/screens/fileManagement/PhotoPickerSection.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js +26 -9
- package/lib/module/ui/screens/fileManagement/hooks/useFileUploadState.js.map +1 -1
- package/lib/module/ui/screens/fileManagement/photoGridLayout.js +39 -0
- package/lib/module/ui/screens/fileManagement/photoGridLayout.js.map +1 -0
- package/lib/module/ui/session/tokenTransport.js +21 -10
- package/lib/module/ui/session/tokenTransport.js.map +1 -1
- package/lib/module/ui/utils/fileManagement.js +0 -39
- 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/FontLoader.d.ts +14 -5
- package/lib/typescript/commonjs/ui/components/FontLoader.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/FontLoader.native.d.ts +13 -0
- package/lib/typescript/commonjs/ui/components/FontLoader.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/fileManagement/styles.d.ts +0 -218
- package/lib/typescript/commonjs/ui/components/fileManagement/styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts +9 -0
- package/lib/typescript/commonjs/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts +96 -0
- package/lib/typescript/commonjs/ui/hooks/queries/userCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts +3 -2
- package/lib/typescript/commonjs/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useResolvedFileUrls.d.ts +48 -0
- package/lib/typescript/commonjs/ui/hooks/useResolvedFileUrls.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/photoGridLayout.d.ts +37 -0
- package/lib/typescript/commonjs/ui/screens/fileManagement/photoGridLayout.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts +15 -8
- package/lib/typescript/commonjs/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts +4 -11
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/client.d.ts +1 -1
- package/lib/typescript/module/ui/client.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FontLoader.d.ts +14 -5
- package/lib/typescript/module/ui/components/FontLoader.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FontLoader.native.d.ts +13 -0
- package/lib/typescript/module/ui/components/FontLoader.native.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/fileManagement/FileDetailsModal.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/fileManagement/styles.d.ts +0 -218
- package/lib/typescript/module/ui/components/fileManagement/styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/useOxyAccountGraph.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts +9 -0
- package/lib/typescript/module/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/userCache.d.ts +96 -0
- package/lib/typescript/module/ui/hooks/queries/userCache.d.ts.map +1 -0
- package/lib/typescript/module/ui/hooks/useAvatarPicker.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts +3 -2
- package/lib/typescript/module/ui/hooks/useFileDownloadUrl.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useResolvedFileUrls.d.ts +48 -0
- package/lib/typescript/module/ui/hooks/useResolvedFileUrls.d.ts.map +1 -0
- package/lib/typescript/module/ui/index.d.ts +1 -1
- package/lib/typescript/module/ui/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/PhotoPickerSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/photoGridLayout.d.ts +37 -0
- package/lib/typescript/module/ui/screens/fileManagement/photoGridLayout.d.ts.map +1 -0
- package/lib/typescript/module/ui/session/tokenTransport.d.ts +15 -8
- package/lib/typescript/module/ui/session/tokenTransport.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/fileManagement.d.ts +4 -11
- package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
- package/package.json +6 -11
- package/src/index.ts +8 -0
- package/src/ui/components/FontLoader.native.tsx +88 -0
- package/src/ui/components/FontLoader.tsx +16 -151
- package/src/ui/components/fileManagement/FileDetailsModal.tsx +17 -18
- package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
- package/src/ui/components/fileManagement/styles.ts +0 -225
- package/src/ui/context/hooks/useAuthOperations.ts +5 -2
- package/src/ui/context/useOxyAccountGraph.ts +5 -0
- package/src/ui/hooks/queries/__tests__/userCache.test.ts +361 -0
- package/src/ui/hooks/queries/useAccountQueries.ts +14 -5
- package/src/ui/hooks/queries/userCache.ts +308 -0
- package/src/ui/hooks/useAvatarPicker.ts +27 -7
- package/src/ui/hooks/useFileDownloadUrl.ts +7 -23
- package/src/ui/hooks/useResolvedFileUrls.ts +170 -0
- package/src/ui/screens/FileManagementScreen.tsx +55 -27
- package/src/ui/screens/__tests__/photoGridLayout.test.ts +57 -0
- package/src/ui/screens/fileManagement/PhotoPickerSection.tsx +215 -213
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +27 -3
- package/src/ui/screens/fileManagement/photoGridLayout.ts +45 -0
- package/src/ui/session/__tests__/tokenTransport.test.ts +25 -10
- package/src/ui/session/tokenTransport.ts +21 -10
- package/src/ui/utils/fileManagement.ts +3 -46
- package/lib/typescript/types/expo-vector-icons.d.ts +0 -22
- package/src/types/expo-vector-icons.d.ts +0 -22
|
@@ -382,43 +382,6 @@ export const fileManagementStyles = StyleSheet.create({
|
|
|
382
382
|
flex: 1,
|
|
383
383
|
padding: 16,
|
|
384
384
|
},
|
|
385
|
-
fileDetailCard: {
|
|
386
|
-
padding: 18,
|
|
387
|
-
borderRadius: 14,
|
|
388
|
-
borderWidth: 1,
|
|
389
|
-
alignItems: 'center',
|
|
390
|
-
},
|
|
391
|
-
fileDetailIcon: {
|
|
392
|
-
marginBottom: 16,
|
|
393
|
-
},
|
|
394
|
-
fileDetailName: {
|
|
395
|
-
fontSize: 20,
|
|
396
|
-
fontWeight: 'bold',
|
|
397
|
-
textAlign: 'center',
|
|
398
|
-
marginBottom: 24,
|
|
399
|
-
},
|
|
400
|
-
fileDetailInfo: {
|
|
401
|
-
width: '100%',
|
|
402
|
-
marginBottom: 32,
|
|
403
|
-
},
|
|
404
|
-
detailRow: {
|
|
405
|
-
flexDirection: 'row',
|
|
406
|
-
justifyContent: 'space-between',
|
|
407
|
-
alignItems: 'flex-start',
|
|
408
|
-
marginBottom: 12,
|
|
409
|
-
flexWrap: 'wrap',
|
|
410
|
-
},
|
|
411
|
-
detailLabel: {
|
|
412
|
-
fontSize: 16,
|
|
413
|
-
fontWeight: '500',
|
|
414
|
-
flex: 1,
|
|
415
|
-
minWidth: 100,
|
|
416
|
-
},
|
|
417
|
-
detailValue: {
|
|
418
|
-
fontSize: 16,
|
|
419
|
-
flex: 2,
|
|
420
|
-
textAlign: 'right',
|
|
421
|
-
},
|
|
422
385
|
modalActions: {
|
|
423
386
|
flexDirection: 'row',
|
|
424
387
|
gap: 12,
|
|
@@ -858,191 +821,3 @@ export const fileManagementStyles = StyleSheet.create({
|
|
|
858
821
|
borderRadius: 8,
|
|
859
822
|
},
|
|
860
823
|
});
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Styles for the dedicated photo picker view used when `selectMode + image-only`
|
|
864
|
-
* is active (e.g. the avatar picker context). This view is intentionally
|
|
865
|
-
* flagship-grade: black backdrop, translucent header, edge-to-edge grid.
|
|
866
|
-
*
|
|
867
|
-
* Kept in a separate StyleSheet so the rest of `fileManagementStyles` does not
|
|
868
|
-
* carry a black-background bias when used elsewhere.
|
|
869
|
-
*/
|
|
870
|
-
export const photoPickerStyles = StyleSheet.create({
|
|
871
|
-
root: {
|
|
872
|
-
flex: 1,
|
|
873
|
-
backgroundColor: '#000000',
|
|
874
|
-
},
|
|
875
|
-
headerSpacer: {
|
|
876
|
-
// Reserved for the absolute header so content does not sit underneath it.
|
|
877
|
-
backgroundColor: '#000000',
|
|
878
|
-
},
|
|
879
|
-
header: {
|
|
880
|
-
position: 'absolute',
|
|
881
|
-
top: 0,
|
|
882
|
-
left: 0,
|
|
883
|
-
right: 0,
|
|
884
|
-
backgroundColor: 'rgba(0,0,0,0.92)',
|
|
885
|
-
flexDirection: 'row',
|
|
886
|
-
alignItems: 'center',
|
|
887
|
-
justifyContent: 'space-between',
|
|
888
|
-
paddingHorizontal: 12,
|
|
889
|
-
zIndex: 30,
|
|
890
|
-
},
|
|
891
|
-
headerRow: {
|
|
892
|
-
flexDirection: 'row',
|
|
893
|
-
alignItems: 'center',
|
|
894
|
-
justifyContent: 'space-between',
|
|
895
|
-
width: '100%',
|
|
896
|
-
// Material 3 top app bar height — leaves a generous tap target for
|
|
897
|
-
// the Cancel / Upload pills without crowding the title.
|
|
898
|
-
height: 56,
|
|
899
|
-
},
|
|
900
|
-
headerSide: {
|
|
901
|
-
flexBasis: 0,
|
|
902
|
-
flexGrow: 1,
|
|
903
|
-
flexDirection: 'row',
|
|
904
|
-
alignItems: 'center',
|
|
905
|
-
},
|
|
906
|
-
headerSideRight: {
|
|
907
|
-
justifyContent: 'flex-end',
|
|
908
|
-
},
|
|
909
|
-
headerSideLeft: {
|
|
910
|
-
justifyContent: 'flex-start',
|
|
911
|
-
},
|
|
912
|
-
headerTitle: {
|
|
913
|
-
fontSize: 17,
|
|
914
|
-
fontWeight: '600',
|
|
915
|
-
color: '#FFFFFF',
|
|
916
|
-
letterSpacing: -0.2,
|
|
917
|
-
textAlign: 'center',
|
|
918
|
-
},
|
|
919
|
-
headerCancel: {
|
|
920
|
-
paddingHorizontal: 6,
|
|
921
|
-
paddingVertical: 8,
|
|
922
|
-
minHeight: 36,
|
|
923
|
-
minWidth: 44,
|
|
924
|
-
justifyContent: 'center',
|
|
925
|
-
},
|
|
926
|
-
headerCancelText: {
|
|
927
|
-
fontSize: 17,
|
|
928
|
-
fontWeight: '500',
|
|
929
|
-
color: '#FFFFFF',
|
|
930
|
-
},
|
|
931
|
-
headerPrimaryPill: {
|
|
932
|
-
flexDirection: 'row',
|
|
933
|
-
alignItems: 'center',
|
|
934
|
-
gap: 6,
|
|
935
|
-
paddingHorizontal: 14,
|
|
936
|
-
paddingVertical: 8,
|
|
937
|
-
borderRadius: 999,
|
|
938
|
-
minHeight: 36,
|
|
939
|
-
},
|
|
940
|
-
headerPrimaryPillIconOnly: {
|
|
941
|
-
paddingHorizontal: 8,
|
|
942
|
-
width: 36,
|
|
943
|
-
justifyContent: 'center',
|
|
944
|
-
},
|
|
945
|
-
headerPrimaryText: {
|
|
946
|
-
color: '#FFFFFF',
|
|
947
|
-
fontSize: 15,
|
|
948
|
-
fontWeight: '600',
|
|
949
|
-
},
|
|
950
|
-
headerProgressBarTrack: {
|
|
951
|
-
position: 'absolute',
|
|
952
|
-
left: 0,
|
|
953
|
-
right: 0,
|
|
954
|
-
bottom: 0,
|
|
955
|
-
height: 2,
|
|
956
|
-
backgroundColor: 'rgba(255,255,255,0.12)',
|
|
957
|
-
},
|
|
958
|
-
headerProgressBarFill: {
|
|
959
|
-
height: '100%',
|
|
960
|
-
},
|
|
961
|
-
grid: {
|
|
962
|
-
flex: 1,
|
|
963
|
-
},
|
|
964
|
-
gridContent: {
|
|
965
|
-
// Top inset is set dynamically (header height + safe area).
|
|
966
|
-
paddingBottom: 24,
|
|
967
|
-
},
|
|
968
|
-
cellWrapper: {
|
|
969
|
-
position: 'relative',
|
|
970
|
-
// width/height set dynamically per device
|
|
971
|
-
},
|
|
972
|
-
cellInner: {
|
|
973
|
-
flex: 1,
|
|
974
|
-
borderRadius: 8,
|
|
975
|
-
overflow: 'hidden',
|
|
976
|
-
backgroundColor: '#111111',
|
|
977
|
-
},
|
|
978
|
-
cellImage: {
|
|
979
|
-
width: '100%',
|
|
980
|
-
height: '100%',
|
|
981
|
-
},
|
|
982
|
-
cellRing: {
|
|
983
|
-
position: 'absolute',
|
|
984
|
-
top: 0,
|
|
985
|
-
left: 0,
|
|
986
|
-
right: 0,
|
|
987
|
-
bottom: 0,
|
|
988
|
-
borderRadius: 8,
|
|
989
|
-
borderWidth: 3,
|
|
990
|
-
},
|
|
991
|
-
cellDim: {
|
|
992
|
-
opacity: 0.6,
|
|
993
|
-
},
|
|
994
|
-
cellBadge: {
|
|
995
|
-
position: 'absolute',
|
|
996
|
-
top: 6,
|
|
997
|
-
right: 6,
|
|
998
|
-
minWidth: 22,
|
|
999
|
-
height: 22,
|
|
1000
|
-
paddingHorizontal: 6,
|
|
1001
|
-
borderRadius: 11,
|
|
1002
|
-
alignItems: 'center',
|
|
1003
|
-
justifyContent: 'center',
|
|
1004
|
-
},
|
|
1005
|
-
cellBadgeText: {
|
|
1006
|
-
color: '#FFFFFF',
|
|
1007
|
-
fontSize: 12,
|
|
1008
|
-
fontWeight: '700',
|
|
1009
|
-
lineHeight: 14,
|
|
1010
|
-
},
|
|
1011
|
-
empty: {
|
|
1012
|
-
flex: 1,
|
|
1013
|
-
alignItems: 'center',
|
|
1014
|
-
justifyContent: 'center',
|
|
1015
|
-
paddingHorizontal: 32,
|
|
1016
|
-
},
|
|
1017
|
-
emptyIconWrap: {
|
|
1018
|
-
opacity: 0.3,
|
|
1019
|
-
marginBottom: 16,
|
|
1020
|
-
},
|
|
1021
|
-
emptyTitle: {
|
|
1022
|
-
fontSize: 17,
|
|
1023
|
-
fontWeight: '600',
|
|
1024
|
-
textAlign: 'center',
|
|
1025
|
-
marginBottom: 6,
|
|
1026
|
-
},
|
|
1027
|
-
emptySubtitle: {
|
|
1028
|
-
fontSize: 14,
|
|
1029
|
-
fontWeight: '400',
|
|
1030
|
-
textAlign: 'center',
|
|
1031
|
-
opacity: 0.7,
|
|
1032
|
-
marginBottom: 24,
|
|
1033
|
-
},
|
|
1034
|
-
emptyCta: {
|
|
1035
|
-
flexDirection: 'row',
|
|
1036
|
-
alignItems: 'center',
|
|
1037
|
-
gap: 8,
|
|
1038
|
-
paddingHorizontal: 22,
|
|
1039
|
-
paddingVertical: 12,
|
|
1040
|
-
borderRadius: 999,
|
|
1041
|
-
},
|
|
1042
|
-
emptyCtaText: {
|
|
1043
|
-
color: '#FFFFFF',
|
|
1044
|
-
fontSize: 15,
|
|
1045
|
-
fontWeight: '600',
|
|
1046
|
-
},
|
|
1047
|
-
});
|
|
1048
|
-
|
|
@@ -170,8 +170,11 @@ export const useAuthOperations = ({
|
|
|
170
170
|
logger?.('Failed to register sign-in into device session set', registrationError);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
//
|
|
174
|
-
|
|
173
|
+
// Hydrate the full user from the bearer (`GET /users/me`) — `verifyChallenge`
|
|
174
|
+
// has already planted the access token for this session, so the bearer IS
|
|
175
|
+
// the session identity. Avoids a second, session-id-keyed source of truth
|
|
176
|
+
// (`GET /session/user/:sessionId`) that can disagree with the bearer.
|
|
177
|
+
fullUser = await oxyServices.getCurrentUser();
|
|
175
178
|
|
|
176
179
|
// Fetch device sessions
|
|
177
180
|
let allDeviceSessions: ClientSession[] = [];
|
|
@@ -5,6 +5,7 @@ import { logger as loggerUtil } from '@oxyhq/core';
|
|
|
5
5
|
import { useAuthStore } from '../stores/authStore';
|
|
6
6
|
import { isUnauthorizedStatus } from './oxyContextHelpers';
|
|
7
7
|
import { resetSessionScopedStores } from '../stores/resetSessionScopedStores';
|
|
8
|
+
import { ASSET_DOWNLOAD_URLS_QUERY_KEY } from '../hooks/useResolvedFileUrls';
|
|
8
9
|
import type { CommitInput } from './oxyContextTypes';
|
|
9
10
|
|
|
10
11
|
interface UseOxyAccountGraphParams {
|
|
@@ -63,6 +64,10 @@ export function useOxyAccountGraph({
|
|
|
63
64
|
|
|
64
65
|
const runPostAccountSwitchSideEffects = useCallback(async (): Promise<void> => {
|
|
65
66
|
resetSessionScopedStores();
|
|
67
|
+
// Scoped media tokens are per-viewer — drop any cached URLs immediately so
|
|
68
|
+
// `keepPreviousData`-style placeholders cannot flash the prior account's
|
|
69
|
+
// private thumbnails while the new bearer mint lands.
|
|
70
|
+
queryClient.removeQueries({ queryKey: [ASSET_DOWNLOAD_URLS_QUERY_KEY] });
|
|
66
71
|
await refreshAccounts();
|
|
67
72
|
queryClient.invalidateQueries();
|
|
68
73
|
}, [refreshAccounts, queryClient]);
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* userCache — canonical merge-upsert into the SDK user query cache.
|
|
3
|
+
*
|
|
4
|
+
* Covers the merge semantics that eliminate the "sparse source strips a field an
|
|
5
|
+
* authoritative fetch stored" class of bug:
|
|
6
|
+
* - cold slot seeds the full object STALE (so react-query refetches)
|
|
7
|
+
* - a sparse user merged over a full entry KEEPS relationship / createdAt / etc.
|
|
8
|
+
* - null / empty / undefined incoming fields never strip or degrade
|
|
9
|
+
* - nested name / _count / relationship merge field-by-field
|
|
10
|
+
* - anti-degradation (empty username, 'Unknown user', null avatar)
|
|
11
|
+
* - both cache keys written; by-username is viewer-scoped and case-insensitive
|
|
12
|
+
* - batch upsert; viewer id defaults from the auth store
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
16
|
+
import { upsertCachedUser, upsertCachedUsers } from '../userCache';
|
|
17
|
+
import type { CacheableUser } from '../userCache';
|
|
18
|
+
import { queryKeys } from '../queryKeys';
|
|
19
|
+
import { useAuthStore } from '../../../stores/authStore';
|
|
20
|
+
|
|
21
|
+
function makeClient(): QueryClient {
|
|
22
|
+
return new QueryClient({
|
|
23
|
+
defaultOptions: { queries: { retry: false } },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Read the by-id cache entry. */
|
|
28
|
+
function readById(qc: QueryClient, id: string): CacheableUser | undefined {
|
|
29
|
+
return qc.getQueryData<CacheableUser>(queryKeys.users.detail(id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Read the viewer-scoped by-username cache entry. */
|
|
33
|
+
function readByUsername(qc: QueryClient, username: string, viewerId: string): CacheableUser | undefined {
|
|
34
|
+
return qc.getQueryData<CacheableUser>(queryKeys.users.byUsername(username, viewerId));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
// Default to anonymous viewer unless a test sets one.
|
|
39
|
+
useAuthStore.setState({ user: null });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('upsertCachedUser — cold slot', () => {
|
|
43
|
+
it('seeds the full object and marks it STALE (updatedAt: 0) under both keys', () => {
|
|
44
|
+
const qc = makeClient();
|
|
45
|
+
const user: CacheableUser = {
|
|
46
|
+
id: 'u1',
|
|
47
|
+
username: 'Alice',
|
|
48
|
+
name: { displayName: 'Alice A' },
|
|
49
|
+
avatar: 'file_a',
|
|
50
|
+
createdAt: '2020-01-01T00:00:00Z',
|
|
51
|
+
_count: { followers: 10, following: 5 },
|
|
52
|
+
relationship: { isFollowing: true, followsYou: false },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
upsertCachedUser(qc, user, 'viewer-1');
|
|
56
|
+
|
|
57
|
+
// by-id
|
|
58
|
+
expect(readById(qc, 'u1')).toMatchObject({ id: 'u1', username: 'Alice', avatar: 'file_a' });
|
|
59
|
+
expect(qc.getQueryState(queryKeys.users.detail('u1'))?.dataUpdatedAt).toBe(0);
|
|
60
|
+
|
|
61
|
+
// by-username (viewer-scoped, normalized to lowercase)
|
|
62
|
+
const uname = readByUsername(qc, 'alice', 'viewer-1');
|
|
63
|
+
expect(uname).toMatchObject({ id: 'u1', relationship: { isFollowing: true } });
|
|
64
|
+
expect(qc.getQueryState(queryKeys.users.byUsername('alice', 'viewer-1'))?.dataUpdatedAt).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('resolves the id from _id when id is absent', () => {
|
|
68
|
+
const qc = makeClient();
|
|
69
|
+
upsertCachedUser(qc, { _id: 'mongo1', username: 'bob' }, '');
|
|
70
|
+
expect(readById(qc, 'mongo1')).toMatchObject({ id: 'mongo1', username: 'bob' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('is a no-op when no id can be resolved', () => {
|
|
74
|
+
const qc = makeClient();
|
|
75
|
+
upsertCachedUser(qc, { username: 'ghost' }, '');
|
|
76
|
+
expect(qc.getQueryCache().getAll()).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('normalizes a plain-string name into { displayName }', () => {
|
|
80
|
+
const qc = makeClient();
|
|
81
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', name: 'Alice Display' }, '');
|
|
82
|
+
expect(readById(qc, 'u1')?.name).toEqual({ displayName: 'Alice Display' });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('upsertCachedUser — merge over an existing full entry', () => {
|
|
87
|
+
const full: CacheableUser = {
|
|
88
|
+
id: 'u1',
|
|
89
|
+
username: 'alice',
|
|
90
|
+
name: { displayName: 'Alice A', first: 'Alice' },
|
|
91
|
+
avatar: 'file_a',
|
|
92
|
+
verified: true,
|
|
93
|
+
badges: ['og'],
|
|
94
|
+
createdAt: '2020-01-01T00:00:00Z',
|
|
95
|
+
_count: { followers: 10, following: 5 },
|
|
96
|
+
relationship: { isFollowing: true, followsYou: true },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function seedFull(qc: QueryClient): void {
|
|
100
|
+
// Represents an authoritative single-profile fetch already in cache (fresh).
|
|
101
|
+
qc.setQueryData(queryKeys.users.detail('u1'), full);
|
|
102
|
+
qc.setQueryData(queryKeys.users.byUsername('alice', 'viewer-1'), full);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
it('keeps relationship + createdAt + _count when a SPARSE feed user is upserted', () => {
|
|
106
|
+
const qc = makeClient();
|
|
107
|
+
seedFull(qc);
|
|
108
|
+
|
|
109
|
+
// A feed author: no relationship, no createdAt, no counts.
|
|
110
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', name: { displayName: 'Alice A' }, avatar: 'file_a' }, 'viewer-1');
|
|
111
|
+
|
|
112
|
+
const byId = readById(qc, 'u1');
|
|
113
|
+
expect(byId?.createdAt).toBe('2020-01-01T00:00:00Z');
|
|
114
|
+
expect(byId?._count).toEqual({ followers: 10, following: 5 });
|
|
115
|
+
expect(byId?.verified).toBe(true);
|
|
116
|
+
|
|
117
|
+
const byName = readByUsername(qc, 'alice', 'viewer-1');
|
|
118
|
+
expect(byName?.relationship).toEqual({ isFollowing: true, followsYou: true });
|
|
119
|
+
expect(byName?.createdAt).toBe('2020-01-01T00:00:00Z');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does NOT mark an existing entry stale', () => {
|
|
123
|
+
const qc = makeClient();
|
|
124
|
+
seedFull(qc);
|
|
125
|
+
const before = qc.getQueryState(queryKeys.users.detail('u1'))?.dataUpdatedAt;
|
|
126
|
+
expect(before).not.toBe(0);
|
|
127
|
+
|
|
128
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice' }, 'viewer-1');
|
|
129
|
+
|
|
130
|
+
const after = qc.getQueryState(queryKeys.users.detail('u1'))?.dataUpdatedAt;
|
|
131
|
+
expect(after).toBe(before); // freshness preserved, not reset to 0
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('null / empty / undefined incoming fields never strip an existing field', () => {
|
|
135
|
+
const qc = makeClient();
|
|
136
|
+
seedFull(qc);
|
|
137
|
+
|
|
138
|
+
upsertCachedUser(
|
|
139
|
+
qc,
|
|
140
|
+
{ id: 'u1', username: '', avatar: null, createdAt: '', _count: undefined, relationship: null },
|
|
141
|
+
'viewer-1',
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const byId = readById(qc, 'u1');
|
|
145
|
+
expect(byId?.username).toBe('alice');
|
|
146
|
+
expect(byId?.avatar).toBe('file_a');
|
|
147
|
+
expect(byId?.createdAt).toBe('2020-01-01T00:00:00Z');
|
|
148
|
+
expect(byId?._count).toEqual({ followers: 10, following: 5 });
|
|
149
|
+
|
|
150
|
+
const byName = readByUsername(qc, 'alice', 'viewer-1');
|
|
151
|
+
expect(byName?.relationship).toEqual({ isFollowing: true, followsYou: true });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('updates the fields the incoming user DOES carry', () => {
|
|
155
|
+
const qc = makeClient();
|
|
156
|
+
seedFull(qc);
|
|
157
|
+
|
|
158
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', bio: 'new bio', avatar: 'file_new' }, 'viewer-1');
|
|
159
|
+
|
|
160
|
+
const byId = readById(qc, 'u1');
|
|
161
|
+
expect(byId?.bio).toBe('new bio');
|
|
162
|
+
expect(byId?.avatar).toBe('file_new');
|
|
163
|
+
// untouched fields survive
|
|
164
|
+
expect(byId?.createdAt).toBe('2020-01-01T00:00:00Z');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('a defined boolean/number (verified:false, _count.followers:0) DOES override', () => {
|
|
168
|
+
const qc = makeClient();
|
|
169
|
+
seedFull(qc);
|
|
170
|
+
|
|
171
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', verified: false, _count: { followers: 0 } }, 'viewer-1');
|
|
172
|
+
|
|
173
|
+
const byId = readById(qc, 'u1');
|
|
174
|
+
expect(byId?.verified).toBe(false);
|
|
175
|
+
// partial _count overrides only followers; following kept from existing
|
|
176
|
+
expect(byId?._count).toEqual({ followers: 0, following: 5 });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('upsertCachedUser — nested merge', () => {
|
|
181
|
+
it('a partial name never replaces a fuller name; displayName upgrades', () => {
|
|
182
|
+
const qc = makeClient();
|
|
183
|
+
qc.setQueryData(queryKeys.users.detail('u1'), {
|
|
184
|
+
id: 'u1',
|
|
185
|
+
username: 'alice',
|
|
186
|
+
name: { first: 'Alice', last: 'Adams', displayName: 'Alice A' },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', name: { displayName: 'Alice Adams' } }, '');
|
|
190
|
+
|
|
191
|
+
expect(readById(qc, 'u1')?.name).toEqual({
|
|
192
|
+
first: 'Alice',
|
|
193
|
+
last: 'Adams',
|
|
194
|
+
displayName: 'Alice Adams',
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('a partial relationship never nulls out the other field', () => {
|
|
199
|
+
const qc = makeClient();
|
|
200
|
+
qc.setQueryData(queryKeys.users.byUsername('alice', 'v1'), {
|
|
201
|
+
id: 'u1',
|
|
202
|
+
username: 'alice',
|
|
203
|
+
relationship: { isFollowing: true, followsYou: true },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', relationship: { followsYou: false } }, 'v1');
|
|
207
|
+
|
|
208
|
+
expect(readByUsername(qc, 'alice', 'v1')?.relationship).toEqual({ isFollowing: true, followsYou: false });
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('upsertCachedUser — anti-degradation', () => {
|
|
213
|
+
it('never overwrites a good displayName with the "Unknown user" sentinel', () => {
|
|
214
|
+
const qc = makeClient();
|
|
215
|
+
qc.setQueryData(queryKeys.users.detail('u1'), { id: 'u1', username: 'alice', name: { displayName: 'Alice A' } });
|
|
216
|
+
|
|
217
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', name: { displayName: 'Unknown user' } }, '');
|
|
218
|
+
|
|
219
|
+
expect(readById(qc, 'u1')?.name).toEqual({ displayName: 'Alice A' });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('never overwrites a good username with an empty one', () => {
|
|
223
|
+
const qc = makeClient();
|
|
224
|
+
qc.setQueryData(queryKeys.users.detail('u1'), { id: 'u1', username: 'alice' });
|
|
225
|
+
|
|
226
|
+
upsertCachedUser(qc, { id: 'u1', username: ' ' }, '');
|
|
227
|
+
|
|
228
|
+
expect(readById(qc, 'u1')?.username).toBe('alice');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('never nulls out a good avatar', () => {
|
|
232
|
+
const qc = makeClient();
|
|
233
|
+
qc.setQueryData(queryKeys.users.detail('u1'), { id: 'u1', username: 'alice', avatar: 'file_a' });
|
|
234
|
+
|
|
235
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', avatar: null }, '');
|
|
236
|
+
|
|
237
|
+
expect(readById(qc, 'u1')?.avatar).toBe('file_a');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('upgrades a degraded existing displayName when a real one arrives', () => {
|
|
241
|
+
const qc = makeClient();
|
|
242
|
+
qc.setQueryData(queryKeys.users.detail('u1'), { id: 'u1', username: 'alice', name: { displayName: 'Unknown user' } });
|
|
243
|
+
|
|
244
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', name: { displayName: 'Alice A' } }, '');
|
|
245
|
+
|
|
246
|
+
expect(readById(qc, 'u1')?.name).toEqual({ displayName: 'Alice A' });
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('upsertCachedUser — keys & viewer scoping', () => {
|
|
251
|
+
it('writes ONLY the by-id key when no username is present', () => {
|
|
252
|
+
const qc = makeClient();
|
|
253
|
+
upsertCachedUser(qc, { id: 'u1' }, 'v1');
|
|
254
|
+
expect(readById(qc, 'u1')).toBeDefined();
|
|
255
|
+
// no username-derived entries
|
|
256
|
+
const usernameEntries = qc
|
|
257
|
+
.getQueryCache()
|
|
258
|
+
.getAll()
|
|
259
|
+
.filter((q) => (q.queryKey as unknown[]).includes('username'));
|
|
260
|
+
expect(usernameEntries).toHaveLength(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('viewer-scopes the by-username key (different viewers -> different entries)', () => {
|
|
264
|
+
const qc = makeClient();
|
|
265
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', relationship: { isFollowing: true, followsYou: false } }, 'viewerA');
|
|
266
|
+
|
|
267
|
+
expect(readByUsername(qc, 'alice', 'viewerA')).toBeDefined();
|
|
268
|
+
expect(readByUsername(qc, 'alice', 'viewerB')).toBeUndefined();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('never stores viewer-relative relationship on the viewer-independent by-id key', () => {
|
|
272
|
+
const qc = makeClient();
|
|
273
|
+
upsertCachedUser(
|
|
274
|
+
qc,
|
|
275
|
+
{ id: 'u1', username: 'alice', relationship: { isFollowing: true, followsYou: true } },
|
|
276
|
+
'viewerA',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
expect(readById(qc, 'u1')?.relationship).toBeUndefined();
|
|
280
|
+
expect(readByUsername(qc, 'alice', 'viewerA')?.relationship).toEqual({
|
|
281
|
+
isFollowing: true,
|
|
282
|
+
followsYou: true,
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('strips a stale relationship from an existing by-id entry on merge', () => {
|
|
287
|
+
const qc = makeClient();
|
|
288
|
+
qc.setQueryData(queryKeys.users.detail('u1'), {
|
|
289
|
+
id: 'u1',
|
|
290
|
+
username: 'alice',
|
|
291
|
+
relationship: { isFollowing: true, followsYou: true },
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice', avatar: 'file_a' }, 'viewerA');
|
|
295
|
+
|
|
296
|
+
expect(readById(qc, 'u1')?.relationship).toBeUndefined();
|
|
297
|
+
expect(readById(qc, 'u1')?.avatar).toBe('file_a');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('is case-insensitive on username (byUsername normalization)', () => {
|
|
301
|
+
const qc = makeClient();
|
|
302
|
+
upsertCachedUser(qc, { id: 'u1', username: 'AlIcE' }, 'v1');
|
|
303
|
+
// read with any casing resolves the same entry
|
|
304
|
+
expect(readByUsername(qc, 'alice', 'v1')).toMatchObject({ id: 'u1' });
|
|
305
|
+
expect(readByUsername(qc, 'ALICE', 'v1')).toMatchObject({ id: 'u1' });
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('defaults the viewer id from the auth store when omitted', () => {
|
|
309
|
+
const qc = makeClient();
|
|
310
|
+
useAuthStore.setState({ user: { id: 'store-viewer', username: 'me', name: { displayName: 'Me' }, publicKey: 'pk' } });
|
|
311
|
+
|
|
312
|
+
upsertCachedUser(qc, { id: 'u1', username: 'alice' });
|
|
313
|
+
|
|
314
|
+
expect(readByUsername(qc, 'alice', 'store-viewer')).toMatchObject({ id: 'u1' });
|
|
315
|
+
expect(readByUsername(qc, 'alice', '')).toBeUndefined();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('upsertCachedUsers — batch', () => {
|
|
320
|
+
it('upserts every user under both keys', () => {
|
|
321
|
+
const qc = makeClient();
|
|
322
|
+
upsertCachedUsers(
|
|
323
|
+
qc,
|
|
324
|
+
[
|
|
325
|
+
{ id: 'u1', username: 'alice' },
|
|
326
|
+
{ id: 'u2', username: 'bob' },
|
|
327
|
+
],
|
|
328
|
+
'v1',
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(readById(qc, 'u1')).toMatchObject({ username: 'alice' });
|
|
332
|
+
expect(readById(qc, 'u2')).toMatchObject({ username: 'bob' });
|
|
333
|
+
expect(readByUsername(qc, 'alice', 'v1')).toBeDefined();
|
|
334
|
+
expect(readByUsername(qc, 'bob', 'v1')).toBeDefined();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('accumulates fields when the same user appears twice with different slices', () => {
|
|
338
|
+
const qc = makeClient();
|
|
339
|
+
// Seed a fresh (non-cold) authoritative entry first so both batch items merge.
|
|
340
|
+
qc.setQueryData(queryKeys.users.detail('u1'), { id: 'u1', username: 'alice' });
|
|
341
|
+
upsertCachedUsers(
|
|
342
|
+
qc,
|
|
343
|
+
[
|
|
344
|
+
{ id: 'u1', username: 'alice', avatar: 'file_a' },
|
|
345
|
+
{ id: 'u1', username: 'alice', bio: 'hello' },
|
|
346
|
+
],
|
|
347
|
+
'v1',
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const byId = readById(qc, 'u1');
|
|
351
|
+
expect(byId?.avatar).toBe('file_a');
|
|
352
|
+
expect(byId?.bio).toBe('hello');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('ignores null / empty input', () => {
|
|
356
|
+
const qc = makeClient();
|
|
357
|
+
upsertCachedUsers(qc, null, 'v1');
|
|
358
|
+
upsertCachedUsers(qc, [], 'v1');
|
|
359
|
+
expect(qc.getQueryCache().getAll()).toHaveLength(0);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -50,6 +50,15 @@ export const useUserProfiles = (sessionIds: string[], options?: { enabled?: bool
|
|
|
50
50
|
/**
|
|
51
51
|
* Get current authenticated user.
|
|
52
52
|
*
|
|
53
|
+
* Hydrates from `GET /users/me` (the bearer identifies the session) rather than
|
|
54
|
+
* `GET /session/user/:activeSessionId`. Post zero-cookie cutover the bearer is
|
|
55
|
+
* the single source of session truth, so keying the CURRENT user off a session
|
|
56
|
+
* id in the URL was a second, independently-updated source that could disagree
|
|
57
|
+
* with the bearer during an account switch — the request fired under the
|
|
58
|
+
* previous account's token against the new account's session id and 404'd. The
|
|
59
|
+
* query KEY stays scoped to the active account so the cache re-scopes (and
|
|
60
|
+
* refetches) on a switch; only the URL stopped carrying a session id.
|
|
61
|
+
*
|
|
53
62
|
* The store-mirror effect must NOT overwrite the in-memory user while a
|
|
54
63
|
* write mutation is in flight — otherwise a stale background refetch
|
|
55
64
|
* landing between optimistic update and server-confirmed update would
|
|
@@ -67,10 +76,7 @@ export const useCurrentUser = (options?: { enabled?: boolean }) => {
|
|
|
67
76
|
const query = useQuery({
|
|
68
77
|
queryKey: queryKeys.accounts.current(activeSessionId),
|
|
69
78
|
queryFn: async () => {
|
|
70
|
-
|
|
71
|
-
throw new Error('No active session');
|
|
72
|
-
}
|
|
73
|
-
return await oxyServices.getUserBySession(activeSessionId);
|
|
79
|
+
return await oxyServices.getCurrentUser();
|
|
74
80
|
},
|
|
75
81
|
enabled: (options?.enabled !== false) && isAuthenticated && !!activeSessionId,
|
|
76
82
|
staleTime: 1 * 60 * 1000, // 1 minute for current user
|
|
@@ -200,7 +206,10 @@ export const useUserByUsername = (username: string | null, options?: { enabled?:
|
|
|
200
206
|
if (!username) {
|
|
201
207
|
throw new Error('Username is required');
|
|
202
208
|
}
|
|
203
|
-
|
|
209
|
+
// Match queryKeys.users.byUsername normalization so the cache key and
|
|
210
|
+
// the API request agree (case-insensitive local handles).
|
|
211
|
+
const normalizedUsername = username.trim().toLowerCase();
|
|
212
|
+
return await oxyServices.getProfileByUsername(normalizedUsername);
|
|
204
213
|
},
|
|
205
214
|
enabled: (options?.enabled !== false) && !!username,
|
|
206
215
|
staleTime: 5 * 60 * 1000,
|