@oxyhq/services 5.11.8 → 5.11.10

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 (199) hide show
  1. package/lib/commonjs/core/OxyServices.js +104 -10
  2. package/lib/commonjs/core/OxyServices.js.map +1 -1
  3. package/lib/commonjs/ui/components/AnimationExample.js +213 -0
  4. package/lib/commonjs/ui/components/AnimationExample.js.map +1 -0
  5. package/lib/commonjs/ui/components/FollowButton.js +58 -47
  6. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  7. package/lib/commonjs/ui/components/GroupedItem.js +2 -1
  8. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
  9. package/lib/commonjs/ui/components/GroupedSection.js +3 -0
  10. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
  11. package/lib/commonjs/ui/components/Header.js +25 -11
  12. package/lib/commonjs/ui/components/Header.js.map +1 -1
  13. package/lib/commonjs/ui/components/OxyProvider.js +69 -33
  14. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  15. package/lib/commonjs/ui/components/ProfileCard.js +5 -1
  16. package/lib/commonjs/ui/components/ProfileCard.js.map +1 -1
  17. package/lib/commonjs/ui/components/index.js +0 -7
  18. package/lib/commonjs/ui/components/index.js.map +1 -1
  19. package/lib/commonjs/ui/components/internal/TextField.js +8 -4
  20. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
  21. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js +161 -0
  22. package/lib/commonjs/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -0
  23. package/lib/commonjs/ui/context/OxyContext.js +97 -38
  24. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  25. package/lib/commonjs/ui/hooks/useFollow.types.js +2 -0
  26. package/lib/commonjs/ui/hooks/useFollow.types.js.map +1 -0
  27. package/lib/commonjs/ui/navigation/OxyRouter.js +10 -0
  28. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  29. package/lib/commonjs/ui/screens/AccountCenterScreen.js +26 -14
  30. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +3 -3
  32. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +64 -15
  34. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  35. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +4 -4
  36. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/FeedbackScreen.js +72 -75
  38. package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -1
  39. package/lib/commonjs/ui/screens/FileManagementScreen.js +286 -126
  40. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  41. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +322 -0
  42. package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -0
  43. package/lib/commonjs/ui/screens/ProfileScreen.js +1 -1
  44. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  45. package/lib/commonjs/ui/screens/SessionManagementScreen.js +176 -174
  46. package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
  47. package/lib/commonjs/ui/screens/SignInScreen.js +43 -52
  48. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  49. package/lib/commonjs/ui/screens/SignUpScreen.js +6 -4
  50. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  51. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +386 -0
  52. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -0
  53. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +25 -15
  54. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  55. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +16 -9
  56. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  57. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +1 -1
  58. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  59. package/lib/commonjs/ui/styles/authStyles.js +1 -1
  60. package/lib/commonjs/ui/styles/authStyles.js.map +1 -1
  61. package/lib/module/core/OxyServices.js +103 -9
  62. package/lib/module/core/OxyServices.js.map +1 -1
  63. package/lib/module/ui/components/AnimationExample.js +209 -0
  64. package/lib/module/ui/components/AnimationExample.js.map +1 -0
  65. package/lib/module/ui/components/FollowButton.js +58 -47
  66. package/lib/module/ui/components/FollowButton.js.map +1 -1
  67. package/lib/module/ui/components/GroupedItem.js +2 -1
  68. package/lib/module/ui/components/GroupedItem.js.map +1 -1
  69. package/lib/module/ui/components/GroupedSection.js +3 -0
  70. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  71. package/lib/module/ui/components/Header.js +25 -11
  72. package/lib/module/ui/components/Header.js.map +1 -1
  73. package/lib/module/ui/components/OxyProvider.js +70 -34
  74. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  75. package/lib/module/ui/components/ProfileCard.js +5 -1
  76. package/lib/module/ui/components/ProfileCard.js.map +1 -1
  77. package/lib/module/ui/components/index.js +0 -1
  78. package/lib/module/ui/components/index.js.map +1 -1
  79. package/lib/module/ui/components/internal/TextField.js +8 -4
  80. package/lib/module/ui/components/internal/TextField.js.map +1 -1
  81. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js +156 -0
  82. package/lib/module/ui/components/photogrid/JustifiedPhotoGrid.js.map +1 -0
  83. package/lib/module/ui/context/OxyContext.js +97 -39
  84. package/lib/module/ui/context/OxyContext.js.map +1 -1
  85. package/lib/module/ui/hooks/useFollow.types.js +2 -0
  86. package/lib/module/ui/hooks/useFollow.types.js.map +1 -0
  87. package/lib/module/ui/navigation/OxyRouter.js +10 -0
  88. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  89. package/lib/module/ui/screens/AccountCenterScreen.js +12 -1
  90. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  91. package/lib/module/ui/screens/AccountOverviewScreen.js +3 -3
  92. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  93. package/lib/module/ui/screens/AccountSettingsScreen.js +64 -15
  94. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  95. package/lib/module/ui/screens/AccountSwitcherScreen.js +4 -4
  96. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  97. package/lib/module/ui/screens/FeedbackScreen.js +72 -75
  98. package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
  99. package/lib/module/ui/screens/FileManagementScreen.js +285 -125
  100. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  101. package/lib/module/ui/screens/LanguageSelectorScreen.js +319 -0
  102. package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -0
  103. package/lib/module/ui/screens/ProfileScreen.js +1 -1
  104. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  105. package/lib/module/ui/screens/SessionManagementScreen.js +177 -175
  106. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
  107. package/lib/module/ui/screens/SignInScreen.js +44 -53
  108. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  109. package/lib/module/ui/screens/SignUpScreen.js +6 -4
  110. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  111. package/lib/module/ui/screens/WelcomeNewUserScreen.js +382 -0
  112. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -0
  113. package/lib/module/ui/screens/internal/SignInPasswordStep.js +23 -14
  114. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  115. package/lib/module/ui/screens/internal/SignInUsernameStep.js +15 -9
  116. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  117. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +1 -1
  118. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  119. package/lib/module/ui/styles/authStyles.js +1 -1
  120. package/lib/module/ui/styles/authStyles.js.map +1 -1
  121. package/lib/typescript/core/OxyServices.d.ts +95 -4
  122. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  123. package/lib/typescript/models/interfaces.d.ts +1 -5
  124. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  125. package/lib/typescript/models/session.d.ts +1 -4
  126. package/lib/typescript/models/session.d.ts.map +1 -1
  127. package/lib/typescript/ui/components/AnimationExample.d.ts +4 -0
  128. package/lib/typescript/ui/components/AnimationExample.d.ts.map +1 -0
  129. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  130. package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -1
  131. package/lib/typescript/ui/components/Header.d.ts +9 -0
  132. package/lib/typescript/ui/components/Header.d.ts.map +1 -1
  133. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  134. package/lib/typescript/ui/components/ProfileCard.d.ts +1 -3
  135. package/lib/typescript/ui/components/ProfileCard.d.ts.map +1 -1
  136. package/lib/typescript/ui/components/index.d.ts +0 -1
  137. package/lib/typescript/ui/components/index.d.ts.map +1 -1
  138. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
  139. package/lib/typescript/ui/components/photogrid/JustifiedPhotoGrid.d.ts +27 -0
  140. package/lib/typescript/ui/components/photogrid/JustifiedPhotoGrid.d.ts.map +1 -0
  141. package/lib/typescript/ui/context/OxyContext.d.ts +6 -2
  142. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  143. package/lib/typescript/ui/hooks/useFollow.types.d.ts +33 -0
  144. package/lib/typescript/ui/hooks/useFollow.types.d.ts.map +1 -0
  145. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  146. package/lib/typescript/ui/navigation/types.d.ts +5 -0
  147. package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
  148. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  149. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  150. package/lib/typescript/ui/screens/FeedbackScreen.d.ts.map +1 -1
  151. package/lib/typescript/ui/screens/FileManagementScreen.d.ts +18 -1
  152. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  153. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts +7 -0
  154. package/lib/typescript/ui/screens/LanguageSelectorScreen.d.ts.map +1 -0
  155. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  156. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
  157. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  158. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  159. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts +13 -0
  160. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -0
  161. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts +5 -5
  162. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
  163. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts +4 -4
  164. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts.map +1 -1
  165. package/lib/typescript/ui/styles/authStyles.d.ts +1 -1
  166. package/package.json +10 -2
  167. package/src/core/OxyServices.ts +107 -13
  168. package/src/models/interfaces.ts +2 -5
  169. package/src/models/session.ts +1 -4
  170. package/src/ui/components/AnimationExample.tsx +194 -0
  171. package/src/ui/components/FollowButton.tsx +65 -45
  172. package/src/ui/components/GroupedItem.tsx +1 -0
  173. package/src/ui/components/GroupedSection.tsx +1 -1
  174. package/src/ui/components/Header.tsx +36 -12
  175. package/src/ui/components/OxyProvider.tsx +66 -32
  176. package/src/ui/components/ProfileCard.tsx +6 -8
  177. package/src/ui/components/index.ts +0 -1
  178. package/src/ui/components/internal/TextField.tsx +12 -6
  179. package/src/ui/components/photogrid/JustifiedPhotoGrid.tsx +158 -0
  180. package/src/ui/context/OxyContext.tsx +84 -54
  181. package/src/ui/hooks/useFollow.types.ts +33 -0
  182. package/src/ui/navigation/OxyRouter.tsx +10 -0
  183. package/src/ui/navigation/types.ts +6 -0
  184. package/src/ui/screens/AccountCenterScreen.tsx +13 -7
  185. package/src/ui/screens/AccountOverviewScreen.tsx +3 -3
  186. package/src/ui/screens/AccountSettingsScreen.tsx +65 -13
  187. package/src/ui/screens/AccountSwitcherScreen.tsx +4 -4
  188. package/src/ui/screens/FeedbackScreen.tsx +57 -80
  189. package/src/ui/screens/FileManagementScreen.tsx +278 -175
  190. package/src/ui/screens/LanguageSelectorScreen.tsx +322 -0
  191. package/src/ui/screens/ProfileScreen.tsx +6 -1
  192. package/src/ui/screens/SessionManagementScreen.tsx +148 -151
  193. package/src/ui/screens/SignInScreen.tsx +43 -62
  194. package/src/ui/screens/SignUpScreen.tsx +3 -5
  195. package/src/ui/screens/WelcomeNewUserScreen.tsx +272 -0
  196. package/src/ui/screens/internal/SignInPasswordStep.tsx +28 -13
  197. package/src/ui/screens/internal/SignInUsernameStep.tsx +21 -11
  198. package/src/ui/screens/karma/KarmaCenterScreen.tsx +1 -1
  199. package/src/ui/styles/authStyles.ts +1 -1
@@ -16,6 +16,8 @@ import {
16
16
  type NativeSyntheticEvent,
17
17
  type TargetedEvent,
18
18
  type TextInputFocusEventData,
19
+ type FocusEvent,
20
+ type BlurEvent,
19
21
  } from 'react-native';
20
22
  import { Ionicons } from '@expo/vector-icons';
21
23
  import Svg, { Path } from 'react-native-svg';
@@ -304,16 +306,20 @@ const TextField = forwardRef<TextInput, TextFieldProps>(({
304
306
  }, [formatValue, inputMask, customMask]);
305
307
 
306
308
  // Handle focus
307
- const handleFocus = useCallback((event: NativeSyntheticEvent<TextInputFocusEventData>) => {
309
+ const handleFocus = useCallback((event: FocusEvent) => {
308
310
  if (disabled) return;
309
311
  setFocused(true);
310
- onFocus?.(event);
312
+ // Convert FocusEvent to the expected type for parent callback
313
+ const syntheticEvent = event as any;
314
+ onFocus?.(syntheticEvent);
311
315
  }, [disabled, onFocus]);
312
316
 
313
317
  // Handle blur
314
- const handleBlur = useCallback((event: NativeSyntheticEvent<TextInputFocusEventData>) => {
318
+ const handleBlur = useCallback((event: BlurEvent) => {
315
319
  setFocused(false);
316
- onBlur?.(event);
320
+ // Convert BlurEvent to the expected type for parent callback
321
+ const syntheticEvent = event as any;
322
+ onBlur?.(syntheticEvent);
317
323
  }, [onBlur]);
318
324
 
319
325
  // Handle mouse events
@@ -421,7 +427,7 @@ const TextField = forwardRef<TextInput, TextFieldProps>(({
421
427
  position: 'relative',
422
428
  ...Platform.select({
423
429
  web: {
424
- outlineStyle: 'none',
430
+ outlineStyle: undefined,
425
431
  outlineWidth: 0,
426
432
  outlineOffset: 0,
427
433
  },
@@ -441,7 +447,7 @@ const TextField = forwardRef<TextInput, TextFieldProps>(({
441
447
  ...Platform.select({
442
448
  web: {
443
449
  border: 'none',
444
- outlineStyle: 'none',
450
+ outlineStyle: undefined,
445
451
  outlineWidth: 0,
446
452
  outlineOffset: 0,
447
453
  boxShadow: 'none',
@@ -0,0 +1,158 @@
1
+ import React, { useEffect, useMemo, useState, useCallback } from 'react';
2
+ import { View, Text, LayoutChangeEvent } from 'react-native';
3
+ import type { FileMetadata } from '../../../models/interfaces';
4
+ // Using plain React Native styles (nativewind not installed in this repo)
5
+
6
+ export interface JustifiedPhotoGridProps {
7
+ photos: FileMetadata[];
8
+ photoDimensions: { [key: string]: { width: number; height: number } };
9
+ loadPhotoDimensions: (photos: FileMetadata[]) => Promise<void>;
10
+ createJustifiedRows: (photos: FileMetadata[], containerWidth: number) => FileMetadata[][];
11
+ renderJustifiedPhotoItem: (photo: FileMetadata, width: number, height: number, isLast: boolean) => React.ReactElement;
12
+ renderSimplePhotoItem: (photo: FileMetadata, index: number) => React.ReactElement;
13
+ textColor: string;
14
+ /**
15
+ * Full available width from parent. If omitted, component will measure itself and adapt responsively.
16
+ */
17
+ containerWidth?: number; // optional; will auto-measure if not provided
18
+ gap?: number;
19
+ minRowHeight?: number;
20
+ maxRowHeight?: number;
21
+ dateFormatLocale?: string;
22
+ }
23
+
24
+ /**
25
+ * Responsive justified photo grid that stretches to the provided containerWidth.
26
+ * Uses flex rows with proportional children widths instead of absolute pixel widths so it always fills.
27
+ */
28
+ const JustifiedPhotoGrid: React.FC<JustifiedPhotoGridProps> = ({
29
+ photos,
30
+ photoDimensions,
31
+ loadPhotoDimensions,
32
+ createJustifiedRows,
33
+ renderJustifiedPhotoItem,
34
+ textColor,
35
+ containerWidth: explicitWidth,
36
+ gap = 4,
37
+ minRowHeight = 100,
38
+ maxRowHeight = 300,
39
+ dateFormatLocale = 'en-US',
40
+ }) => {
41
+ // Responsive width measurement if not explicitly provided
42
+ const [measuredWidth, setMeasuredWidth] = useState<number | null>(null);
43
+ const effectiveWidth = explicitWidth ?? measuredWidth ?? 0; // 0 until measured
44
+
45
+ const onLayoutContainer = useCallback((e: LayoutChangeEvent) => {
46
+ if (explicitWidth) return; // ignore if controlled
47
+ const w = e.nativeEvent.layout.width;
48
+ setMeasuredWidth(prev => (prev === w ? prev : w));
49
+ }, [explicitWidth]);
50
+ // Ensure dimensions are loaded for displayed photos
51
+ useEffect(() => {
52
+ loadPhotoDimensions(photos);
53
+ // eslint-disable-next-line react-hooks/exhaustive-deps
54
+ }, [photos.map(p => p.id).join(',')]);
55
+
56
+ // Group photos by date first
57
+ const photosByDate = useMemo(() => {
58
+ return photos.reduce((groups: { [key: string]: FileMetadata[] }, photo) => {
59
+ const date = new Date(photo.uploadDate).toDateString();
60
+ if (!groups[date]) groups[date] = [];
61
+ groups[date].push(photo);
62
+ return groups;
63
+ }, {} as { [key: string]: FileMetadata[] });
64
+ }, [photos]);
65
+
66
+ const sortedDates = useMemo(() => Object.keys(photosByDate).sort((a, b) => new Date(b).getTime() - new Date(a).getTime()), [photosByDate]);
67
+
68
+ // Track measured width of each date section (may differ if parent applies horizontal padding/margins)
69
+ const [dateWidths, setDateWidths] = useState<Record<string, number>>({});
70
+ const onLayoutDate = useCallback((date: string, width: number) => {
71
+ setDateWidths(prev => (prev[date] === width ? prev : { ...prev, [date]: width }));
72
+ }, []);
73
+
74
+ return (
75
+ <View style={{ width: '100%' }} onLayout={onLayoutContainer}>
76
+ {/* If width not yet known (uncontrolled), avoid rendering to prevent layout jump */}
77
+ {effectiveWidth === 0 && !explicitWidth ? null : (
78
+ <>
79
+ {sortedDates.map((date: string) => {
80
+ const dayPhotos = photosByDate[date];
81
+ // createJustifiedRows should build rows such that the "ideal" height (availableWidth / totalAspect) stays within min/max.
82
+ // We pass the effective container width.
83
+ const dateWidth = dateWidths[date] ?? effectiveWidth; // fallback to overall width until measured
84
+ const rows = dateWidth > 0 ? createJustifiedRows(dayPhotos, dateWidth) : [];
85
+ return (
86
+ <View
87
+ key={date}
88
+ style={{ marginBottom: 24, width: '100%' }}
89
+ onLayout={e => onLayoutDate(date, e.nativeEvent.layout.width)}
90
+ >
91
+ <Text style={{ fontSize: 14, fontWeight: '600', marginBottom: 12, color: textColor }}>
92
+ {new Date(date).toLocaleDateString(dateFormatLocale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}
93
+ </Text>
94
+ <View style={{ width: '100%' }}>
95
+ {rows.map((row, rowIndex) => {
96
+ // Compute total aspect ratios using loaded dimensions (fallback 4/3)
97
+ const aspects = row.map(p => {
98
+ const dims = photoDimensions[p.id];
99
+ return dims ? dims.width / dims.height : 4 / 3;
100
+ });
101
+ const totalAspect = aspects.reduce((a, b) => a + b, 0);
102
+ const gapsTotal = gap * (row.length - 1);
103
+ const availableWidth = dateWidth - gapsTotal;
104
+ // Ideal height that perfectly fills width when preserving aspect ratios
105
+ const idealHeight = availableWidth / totalAspect;
106
+ // We rely on row construction keeping idealHeight within min/max bounds; if not, clamp but then distribute leftover/overflow.
107
+ let rowHeight = idealHeight;
108
+ let widthAdjustment = 0; // difference to distribute if clamped
109
+ if (idealHeight < minRowHeight) {
110
+ rowHeight = minRowHeight;
111
+ widthAdjustment = availableWidth - rowHeight * totalAspect; // negative means overflow
112
+ } else if (idealHeight > maxRowHeight) {
113
+ rowHeight = maxRowHeight;
114
+ widthAdjustment = availableWidth - rowHeight * totalAspect;
115
+ }
116
+
117
+ // Pre-compute widths maintaining aspect ratios
118
+ let widths = aspects.map(ar => ar * rowHeight);
119
+ // If we have widthAdjustment (due to clamping) distribute proportionally so row still fills exactly
120
+ if (widthAdjustment !== 0) {
121
+ const widthSum = widths.reduce((a, b) => a + b, 0);
122
+ widths = widths.map(w => w + (w / widthSum) * widthAdjustment);
123
+ }
124
+
125
+ // To combat rounding issues, adjust last item width to fill precisely
126
+ const widthSumRounded = widths.reduce((a, b) => a + b, 0);
127
+ const roundingDiff = availableWidth - widthSumRounded;
128
+ if (Math.abs(roundingDiff) > 0.5) {
129
+ widths[widths.length - 1] += roundingDiff; // minimal correction
130
+ }
131
+
132
+ return (
133
+ <View key={rowIndex} style={{ flexDirection: 'row', width: '100%', marginBottom: 4 }}>
134
+ {row.map((p, i) => {
135
+ const photoWidth = widths[i];
136
+ return (
137
+ <View
138
+ key={p.id}
139
+ style={{ width: photoWidth, height: rowHeight, marginRight: i === row.length - 1 ? 0 : gap }}
140
+ >
141
+ {renderJustifiedPhotoItem(p, photoWidth, rowHeight, i === row.length - 1)}
142
+ </View>
143
+ );
144
+ })}
145
+ </View>
146
+ );
147
+ })}
148
+ </View>
149
+ </View>
150
+ );
151
+ })}
152
+ </>
153
+ )}
154
+ </View>
155
+ );
156
+ };
157
+
158
+ export default React.memo(JustifiedPhotoGrid);
@@ -1,4 +1,5 @@
1
1
  import React, { createContext, useContext, useEffect, useCallback, type ReactNode, useMemo, useRef, useState } from 'react';
2
+ import type { UseFollowHook } from '../hooks/useFollow.types';
2
3
  import { View, Text } from 'react-native';
3
4
  import { OxyServices } from '../../core';
4
5
  import type { User, ApiError } from '../../models/interfaces';
@@ -9,8 +10,8 @@ import { toast } from '../../lib/sonner';
9
10
  import { useAuthStore } from '../stores/authStore';
10
11
 
11
12
  // Define the context shape
12
-
13
- import { useFollow as baseUseFollow } from '../hooks/useFollow';
13
+ // NOTE: We intentionally avoid importing useFollow here to prevent a require cycle.
14
+ // If consumers relied on `const { useFollow } = useOxy()`, we provide a lazy proxy below.
14
15
 
15
16
  export interface OxyContextState {
16
17
  // Authentication state
@@ -22,6 +23,9 @@ export interface OxyContextState {
22
23
  isLoading: boolean;
23
24
  error: string | null;
24
25
 
26
+ // Language state
27
+ currentLanguage: string;
28
+
25
29
  // Auth methods
26
30
  login: (username: string, password: string, deviceName?: string) => Promise<User>;
27
31
  logout: (targetSessionId?: string) => Promise<void>;
@@ -33,6 +37,9 @@ export interface OxyContextState {
33
37
  removeSession: (sessionId: string) => Promise<void>;
34
38
  refreshSessions: () => Promise<void>;
35
39
 
40
+ // Language methods
41
+ setLanguage: (languageId: string) => Promise<void>;
42
+
36
43
  // Device management methods
37
44
  getDeviceSessions: () => Promise<any[]>;
38
45
  logoutAllDeviceSessions: () => Promise<void>;
@@ -47,9 +54,10 @@ export interface OxyContextState {
47
54
  hideBottomSheet?: () => void;
48
55
 
49
56
  /**
50
- * useFollow hook, exposed for convenience so you can do const { useFollow } = useOxy();
57
+ * (Deprecated) useFollow hook access via context. Prefer: import { useFollow } from '@oxyhq/services';
58
+ * Kept for backward compatibility; implemented as a lazy dynamic require to avoid circular dependency.
51
59
  */
52
- useFollow: any;
60
+ useFollow: UseFollowHook; // Back-compat; prefer direct import
53
61
  }
54
62
 
55
63
  // Create the context with default values
@@ -122,6 +130,7 @@ const getStorage = async (): Promise<StorageInterface> => {
122
130
  // Storage keys for sessions
123
131
  const getStorageKeys = (prefix = 'oxy_session') => ({
124
132
  activeSessionId: `${prefix}_active_session_id`, // Only store the active session ID
133
+ language: `${prefix}_language`, // Store the selected language
125
134
  });
126
135
 
127
136
  export const OxyProvider: React.FC<OxyContextProviderProps> = ({
@@ -162,6 +171,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
162
171
  const [sessions, setSessions] = React.useState<ClientSession[]>([]);
163
172
  const [activeSessionId, setActiveSessionId] = React.useState<string | null>(null);
164
173
  const [storage, setStorage] = React.useState<StorageInterface | null>(null);
174
+ const [currentLanguage, setCurrentLanguage] = React.useState<string>('en');
165
175
  // Add a new state to track token restoration
166
176
  const [tokenReady, setTokenReady] = React.useState(false);
167
177
 
@@ -185,88 +195,66 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
185
195
  const platformStorage = await getStorage();
186
196
  setStorage(platformStorage);
187
197
  } catch (error) {
188
- console.error('Failed to initialize storage:', error);
198
+ console.error('Init storage failed', error);
189
199
  useAuthStore.setState({ error: 'Failed to initialize storage' });
190
200
  }
191
201
  };
192
-
193
202
  initStorage();
194
203
  }, []);
195
204
 
196
- // Effect to initialize authentication state - only store session ID
205
+ // Initialize authentication state
197
206
  useEffect(() => {
198
207
  const initAuth = async () => {
199
208
  if (!storage) return;
200
-
201
209
  useAuthStore.setState({ isLoading: true });
202
210
  try {
203
- // Only load the active session ID from storage
204
- const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
205
-
206
- console.log('Auth - activeSessionId:', storedActiveSessionId);
207
- console.log('Auth - storage available:', !!storage);
208
- console.log('Auth - oxyServices available:', !!oxyServices);
211
+ setTokenReady(false);
212
+
213
+ // Load saved language preference
214
+ const savedLanguage = await storage.getItem(keys.language);
215
+ if (savedLanguage) {
216
+ setCurrentLanguage(savedLanguage);
217
+ }
209
218
 
219
+ // Try to restore active session from storage
220
+ const storedActiveSessionId = await storage.getItem(keys.activeSessionId);
210
221
  if (storedActiveSessionId) {
211
- // Validate the stored session with the backend
212
222
  try {
213
- const validation = await oxyServices.validateSession(storedActiveSessionId, {
214
- useHeaderValidation: true
215
- });
216
-
223
+ const validation = await oxyServices.validateSession(storedActiveSessionId, { useHeaderValidation: true });
217
224
  if (validation.valid) {
218
- console.log('Auth - session validated successfully');
219
225
  setActiveSessionId(storedActiveSessionId);
220
-
221
- // Get access token for API calls
222
226
  await oxyServices.getTokenBySession(storedActiveSessionId);
223
-
224
- // Load full user data from backend
225
227
  const fullUser = await oxyServices.getUserBySession(storedActiveSessionId);
226
228
  loginSuccess(fullUser);
227
- setMinimalUser({
228
- id: fullUser.id,
229
- username: fullUser.username,
230
- avatar: fullUser.avatar
231
- });
232
-
233
- // Load sessions from backend
229
+ setMinimalUser({ id: fullUser.id, username: fullUser.username, avatar: fullUser.avatar });
234
230
  const serverSessions = await oxyServices.getSessionsBySessionId(storedActiveSessionId);
235
- const clientSessions: ClientSession[] = serverSessions.map(serverSession => ({
236
- sessionId: serverSession.sessionId,
237
- deviceId: serverSession.deviceId,
238
- expiresAt: serverSession.expiresAt || new Date().toISOString(),
239
- lastActive: serverSession.lastActive || new Date().toISOString(),
240
- userId: serverSession.userId || fullUser.id
231
+ const clientSessions: ClientSession[] = serverSessions.map(s => ({
232
+ sessionId: s.sessionId,
233
+ deviceId: s.deviceId,
234
+ expiresAt: s.expiresAt || new Date().toISOString(),
235
+ lastActive: s.lastActive || new Date().toISOString(),
236
+ userId: s.userId || fullUser.id
241
237
  }));
242
238
  setSessions(clientSessions);
243
-
244
- if (onAuthStateChange) {
245
- onAuthStateChange(fullUser);
246
- }
239
+ onAuthStateChange?.(fullUser);
247
240
  } else {
248
- console.log('Auth - session invalid, clearing storage');
249
241
  await clearAllStorage();
250
242
  }
251
- } catch (error) {
252
- console.error('Auth - session validation error:', error);
243
+ } catch (e) {
244
+ console.error('Session validation error', e);
253
245
  await clearAllStorage();
254
246
  }
255
- } else {
256
- console.log('Auth - no stored session found, user needs to login');
257
247
  }
258
- } catch (err) {
259
- console.error('Auth initialization error:', err);
248
+ setTokenReady(true);
249
+ } catch (e) {
250
+ console.error('Auth init error', e);
260
251
  await clearAllStorage();
261
252
  } finally {
262
253
  useAuthStore.setState({ isLoading: false });
263
254
  }
264
255
  };
265
-
266
- if (storage) {
267
- initAuth();
268
- }
269
- }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, setMinimalUser, clearAllStorage]);
256
+ initAuth();
257
+ }, [storage, oxyServices, keys, onAuthStateChange, loginSuccess, clearAllStorage]);
270
258
 
271
259
 
272
260
 
@@ -630,6 +618,26 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
630
618
  }
631
619
  }, [activeSessionId, oxyServices]);
632
620
 
621
+ // Language management method
622
+ const setLanguage = useCallback(async (languageId: string): Promise<void> => {
623
+ if (!storage) throw new Error('Storage not initialized');
624
+
625
+ try {
626
+ // Save language preference
627
+ await storage.setItem(keys.language, languageId);
628
+ setCurrentLanguage(languageId);
629
+
630
+ console.log(`Language changed to ${languageId}`);
631
+
632
+ // TODO: Here you can add any additional logic needed for app-wide language updates
633
+ // such as updating i18n configuration, refreshing translations, etc.
634
+
635
+ } catch (error) {
636
+ console.error('Error saving language preference:', error);
637
+ throw error;
638
+ }
639
+ }, [storage, keys.language]);
640
+
633
641
  // Bottom sheet control methods
634
642
  const showBottomSheet = useCallback((screenOrConfig?: string | { screen: string; props?: Record<string, any> }) => {
635
643
  console.log('showBottomSheet called with:', screenOrConfig);
@@ -691,6 +699,24 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
691
699
  });
692
700
 
693
701
  // Context value - optimized to prevent unnecessary re-renders
702
+ // Lazy proxy to load the hook only when accessed, breaking the static import cycle.
703
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
704
+ const useFollowProxy: UseFollowHook = (userId?: string | string[]) => {
705
+ try {
706
+ // Dynamically require to avoid top-level cycle
707
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
708
+ const mod = require('../hooks/useFollow');
709
+ if (mod && typeof mod.useFollow === 'function') {
710
+ return mod.useFollow(userId);
711
+ }
712
+ console.warn('useFollow module did not export a function as expected');
713
+ return { isFollowing: false, isLoading: false, error: null, toggleFollow: async () => { }, setFollowStatus: () => { }, fetchStatus: async () => { }, clearError: () => { }, followerCount: null, followingCount: null, isLoadingCounts: false, fetchUserCounts: async () => { }, setFollowerCount: () => { }, setFollowingCount: () => { } } as any;
714
+ } catch (e) {
715
+ console.warn('Failed to dynamically load useFollow hook:', e);
716
+ return { isFollowing: false, isLoading: false, error: null, toggleFollow: async () => { }, setFollowStatus: () => { }, fetchStatus: async () => { }, clearError: () => { }, followerCount: null, followingCount: null, isLoadingCounts: false, fetchUserCounts: async () => { }, setFollowerCount: () => { }, setFollowingCount: () => { } } as any;
717
+ }
718
+ };
719
+
694
720
  const contextValue: OxyContextState = useMemo(() => ({
695
721
  user,
696
722
  minimalUser,
@@ -699,6 +725,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
699
725
  isAuthenticated,
700
726
  isLoading,
701
727
  error,
728
+ currentLanguage,
702
729
  login,
703
730
  logout,
704
731
  logoutAll,
@@ -706,6 +733,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
706
733
  switchSession,
707
734
  removeSession,
708
735
  refreshSessions,
736
+ setLanguage,
709
737
  getDeviceSessions,
710
738
  logoutAllDeviceSessions,
711
739
  updateDeviceName,
@@ -713,7 +741,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
713
741
  bottomSheetRef,
714
742
  showBottomSheet,
715
743
  hideBottomSheet,
716
- useFollow: baseUseFollow,
744
+ useFollow: useFollowProxy,
717
745
  }), [
718
746
  user?.id, // Only depend on user ID, not the entire user object
719
747
  minimalUser?.id,
@@ -722,6 +750,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
722
750
  isAuthenticated,
723
751
  isLoading,
724
752
  error,
753
+ currentLanguage,
725
754
  login,
726
755
  logout,
727
756
  logoutAll,
@@ -729,6 +758,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
729
758
  switchSession,
730
759
  removeSession,
731
760
  refreshSessions,
761
+ setLanguage,
732
762
  getDeviceSessions,
733
763
  logoutAllDeviceSessions,
734
764
  updateDeviceName,
@@ -0,0 +1,33 @@
1
+ // Type-only definition for the useFollow hook to allow context exposure without runtime import cycles.
2
+ // Expand this as needed to better reflect the real return type.
3
+
4
+ export type SingleFollowResult = {
5
+ isFollowing: boolean;
6
+ isLoading: boolean;
7
+ error: string | null;
8
+ toggleFollow: () => Promise<void>;
9
+ setFollowStatus: (following: boolean) => void;
10
+ fetchStatus: () => Promise<void>;
11
+ clearError: () => void;
12
+ followerCount: number | null;
13
+ followingCount: number | null;
14
+ isLoadingCounts: boolean;
15
+ fetchUserCounts: () => Promise<void>;
16
+ setFollowerCount: (count: number) => void;
17
+ setFollowingCount: (count: number) => void;
18
+ };
19
+
20
+ export type MultiFollowResult = {
21
+ followData: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }>;
22
+ toggleFollowForUser: (userId: string) => Promise<void>;
23
+ setFollowStatusForUser: (userId: string, following: boolean) => void;
24
+ fetchStatusForUser: (userId: string) => Promise<void>;
25
+ fetchAllStatuses: () => Promise<void>;
26
+ clearErrorForUser: (userId: string) => void;
27
+ isAnyLoading: boolean;
28
+ hasAnyError: boolean;
29
+ allFollowing: boolean;
30
+ allNotFollowing: boolean;
31
+ };
32
+
33
+ export type UseFollowHook = (userId?: string | string[]) => SingleFollowResult | MultiFollowResult;
@@ -25,6 +25,8 @@ import UserLinksScreen from '../screens/UserLinksScreen';
25
25
  import FileManagementScreen from '../screens/FileManagementScreen';
26
26
  import RecoverAccountScreen from '../screens/RecoverAccountScreen';
27
27
  import PaymentGatewayScreen from '../screens/PaymentGatewayScreen';
28
+ import WelcomeNewUserScreen from '../screens/WelcomeNewUserScreen';
29
+ import LanguageSelectorScreen from '../screens/LanguageSelectorScreen';
28
30
 
29
31
  // Import types
30
32
  import type { OxyRouterProps, RouteConfig } from './types';
@@ -115,6 +117,14 @@ const routes: Record<string, RouteConfig> = {
115
117
  component: PaymentGatewayScreen,
116
118
  snapPoints: ['60%', '90%'],
117
119
  },
120
+ WelcomeNewUser: {
121
+ component: WelcomeNewUserScreen,
122
+ snapPoints: ['65%', '90%'],
123
+ },
124
+ LanguageSelector: {
125
+ component: LanguageSelectorScreen,
126
+ snapPoints: ['70%', '100%'],
127
+ },
118
128
  };
119
129
 
120
130
  const OxyRouter: React.FC<OxyRouterProps> = ({
@@ -1,6 +1,7 @@
1
1
  import type { OxyServices } from '../../core';
2
2
  import type { User } from '../../models/interfaces';
3
3
  import type { ComponentType, ReactNode } from 'react';
4
+ import type { QueryClient } from '@tanstack/react-query';
4
5
 
5
6
  /**
6
7
  * Base props for all screens in the Oxy UI system
@@ -138,4 +139,9 @@ export interface OxyProviderProps {
138
139
  * @default true
139
140
  */
140
141
  showInternalToaster?: boolean;
142
+
143
+ /**
144
+ * Optional QueryClient instance for React Query. If not provided, a sensible default is created.
145
+ */
146
+ queryClient?: QueryClient;
141
147
  }
@@ -16,13 +16,11 @@ import { toast } from '../../lib/sonner';
16
16
  import { confirmAction } from '../utils/confirmAction';
17
17
  import { Ionicons } from '@expo/vector-icons';
18
18
  import { fontFamilies } from '../styles/fonts';
19
- import {
20
- ProfileCard,
21
- Section,
22
- QuickActions,
23
- GroupedSection,
24
- GroupedItem
25
- } from '../components';
19
+ import ProfileCard from '../components/ProfileCard';
20
+ import Section from '../components/Section';
21
+ import QuickActions from '../components/QuickActions';
22
+ import GroupedSection from '../components/GroupedSection';
23
+ import GroupedItem from '../components/GroupedItem';
26
24
 
27
25
  const AccountCenterScreen: React.FC<BaseScreenProps> = ({
28
26
  onClose,
@@ -218,6 +216,14 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
218
216
  subtitle: 'Manage notification settings',
219
217
  onPress: () => toast.info('Notifications feature coming soon!'),
220
218
  }] : []),
219
+ {
220
+ id: 'language',
221
+ icon: 'language',
222
+ iconColor: '#32D74B',
223
+ title: 'Language',
224
+ subtitle: 'Choose your preferred language',
225
+ onPress: () => navigate('LanguageSelector'),
226
+ },
221
227
  {
222
228
  id: 'help',
223
229
  icon: 'help-circle',
@@ -192,7 +192,7 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
192
192
  <>
193
193
  <View style={styles.userIcon}>
194
194
  <Avatar
195
- uri={user?.avatar?.url}
195
+ uri={user?.avatar ? oxyServices.getFileDownloadUrl(user.avatar as string, 'thumb') : undefined}
196
196
  name={user?.name?.full}
197
197
  size={40}
198
198
  theme={theme}
@@ -301,8 +301,8 @@ const AccountOverviewScreen: React.FC<BaseScreenProps> = ({
301
301
  customContent: (
302
302
  <>
303
303
  <View style={styles.userIcon}>
304
- {account.avatar?.url ? (
305
- <Image source={{ uri: account.avatar.url }} style={styles.accountAvatarImage} />
304
+ {account.avatar ? (
305
+ <Image source={{ uri: oxyServices.getFileStreamUrl(account.avatar as string) }} style={styles.accountAvatarImage} />
306
306
  ) : (
307
307
  <View style={styles.accountAvatarFallback}>
308
308
  <Text style={styles.accountAvatarText}>