@oxyhq/services 5.10.15 → 5.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/README.md +604 -127
  2. package/lib/commonjs/assets/assets/icons/OxyServices.tsx +2 -2
  3. package/lib/commonjs/assets/assets/illustrations/HighFive.tsx +1 -1
  4. package/lib/commonjs/assets/icons/OxyServices.js +0 -2
  5. package/lib/commonjs/assets/icons/OxyServices.js.map +1 -1
  6. package/lib/commonjs/assets/illustrations/HighFive.js +0 -2
  7. package/lib/commonjs/assets/illustrations/HighFive.js.map +1 -1
  8. package/lib/commonjs/core/OxyServices.js +244 -26
  9. package/lib/commonjs/core/OxyServices.js.map +1 -1
  10. package/lib/commonjs/core/index.js +7 -0
  11. package/lib/commonjs/core/index.js.map +1 -1
  12. package/lib/commonjs/index.js +93 -0
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/commonjs/lib/sonner-safe.js +2 -0
  15. package/lib/commonjs/lib/sonner-safe.js.map +1 -1
  16. package/lib/commonjs/lib/sonner.js +2 -0
  17. package/lib/commonjs/lib/sonner.js.map +1 -1
  18. package/lib/commonjs/node/index.js +7 -0
  19. package/lib/commonjs/node/index.js.map +1 -1
  20. package/lib/commonjs/ui/components/Avatar.js +0 -2
  21. package/lib/commonjs/ui/components/Avatar.js.map +1 -1
  22. package/lib/commonjs/ui/components/FollowButton.js +4 -4
  23. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  24. package/lib/commonjs/ui/components/FontLoader.js +3 -2
  25. package/lib/commonjs/ui/components/FontLoader.js.map +1 -1
  26. package/lib/commonjs/ui/components/GroupedItem.js +7 -4
  27. package/lib/commonjs/ui/components/GroupedItem.js.map +1 -1
  28. package/lib/commonjs/ui/components/GroupedSection.js +1 -1
  29. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
  30. package/lib/commonjs/ui/components/Header.js +0 -1
  31. package/lib/commonjs/ui/components/Header.js.map +1 -1
  32. package/lib/commonjs/ui/components/OxyLogo.js +0 -2
  33. package/lib/commonjs/ui/components/OxyLogo.js.map +1 -1
  34. package/lib/commonjs/ui/components/OxyPayButton.js +4 -5
  35. package/lib/commonjs/ui/components/OxyPayButton.js.map +1 -1
  36. package/lib/commonjs/ui/components/OxyProvider.js +4 -3
  37. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  38. package/lib/commonjs/ui/components/OxySignInButton.js +0 -1
  39. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  40. package/lib/commonjs/ui/components/ProfileCard.js +0 -1
  41. package/lib/commonjs/ui/components/ProfileCard.js.map +1 -1
  42. package/lib/commonjs/ui/components/QuickActions.js +0 -2
  43. package/lib/commonjs/ui/components/QuickActions.js.map +1 -1
  44. package/lib/commonjs/ui/components/Section.js +0 -1
  45. package/lib/commonjs/ui/components/Section.js.map +1 -1
  46. package/lib/commonjs/ui/components/SectionTitle.js +0 -2
  47. package/lib/commonjs/ui/components/SectionTitle.js.map +1 -1
  48. package/lib/commonjs/ui/components/icon/FAIRWalletIcon.js +0 -2
  49. package/lib/commonjs/ui/components/icon/FAIRWalletIcon.js.map +1 -1
  50. package/lib/commonjs/ui/components/icon/OxyIcon.js +0 -2
  51. package/lib/commonjs/ui/components/icon/OxyIcon.js.map +1 -1
  52. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js +0 -2
  53. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
  54. package/lib/commonjs/ui/components/internal/PinInput.js +4 -3
  55. package/lib/commonjs/ui/components/internal/PinInput.js.map +1 -1
  56. package/lib/commonjs/ui/components/internal/TextField.js +3 -3
  57. package/lib/commonjs/ui/components/internal/TextField.js.map +1 -1
  58. package/lib/commonjs/ui/hooks/useAssets.js +263 -0
  59. package/lib/commonjs/ui/hooks/useAssets.js.map +1 -0
  60. package/lib/commonjs/ui/navigation/OxyRouter.js +1 -2
  61. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  62. package/lib/commonjs/ui/screens/AccountCenterScreen.js +0 -2
  63. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  64. package/lib/commonjs/ui/screens/AccountManagementDemo.js +0 -1
  65. package/lib/commonjs/ui/screens/AccountManagementDemo.js.map +1 -1
  66. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +2 -2
  67. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  68. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +5 -6
  69. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  70. package/lib/commonjs/ui/screens/AppInfoScreen.js +1 -2
  71. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  72. package/lib/commonjs/ui/screens/FileManagementScreen.js +631 -430
  73. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  74. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js +2 -2
  75. package/lib/commonjs/ui/screens/PaymentGatewayScreen.js.map +1 -1
  76. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js +1 -2
  77. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  78. package/lib/commonjs/ui/screens/ProfileScreen.js +1 -2
  79. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  80. package/lib/commonjs/ui/screens/RecoverAccountScreen.js +1 -2
  81. package/lib/commonjs/ui/screens/RecoverAccountScreen.js.map +1 -1
  82. package/lib/commonjs/ui/screens/SessionManagementScreen.js +1 -2
  83. package/lib/commonjs/ui/screens/SessionManagementScreen.js.map +1 -1
  84. package/lib/commonjs/ui/screens/SignInScreen.js +1 -2
  85. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  86. package/lib/commonjs/ui/screens/UserLinksScreen.js +0 -2
  87. package/lib/commonjs/ui/screens/UserLinksScreen.js.map +1 -1
  88. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +1 -2
  89. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  90. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +1 -2
  91. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  92. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js +1 -2
  93. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js.map +1 -1
  94. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js +1 -2
  95. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js.map +1 -1
  96. package/lib/commonjs/ui/screens/internal/SignUpSummaryStep.js +0 -1
  97. package/lib/commonjs/ui/screens/internal/SignUpSummaryStep.js.map +1 -1
  98. package/lib/commonjs/ui/screens/internal/SignUpWelcomeStep.js +0 -1
  99. package/lib/commonjs/ui/screens/internal/SignUpWelcomeStep.js.map +1 -1
  100. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js +0 -2
  101. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js.map +1 -1
  102. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +1 -2
  103. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  104. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js +1 -2
  105. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -1
  106. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js +0 -2
  107. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  108. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js +1 -2
  109. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js.map +1 -1
  110. package/lib/commonjs/ui/stores/assetStore.js +225 -0
  111. package/lib/commonjs/ui/stores/assetStore.js.map +1 -0
  112. package/lib/commonjs/ui/stores/authStore.js +1 -1
  113. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  114. package/lib/commonjs/ui/stores/fileStore.js +153 -0
  115. package/lib/commonjs/ui/stores/fileStore.js.map +1 -0
  116. package/lib/commonjs/ui/stores/followStore.js +2 -2
  117. package/lib/commonjs/ui/stores/followStore.js.map +1 -1
  118. package/lib/commonjs/utils/asyncUtils.js +1 -1
  119. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  120. package/lib/commonjs/utils/errorUtils.js +19 -11
  121. package/lib/commonjs/utils/errorUtils.js.map +1 -1
  122. package/lib/commonjs/utils/hookUtils.js +1 -1
  123. package/lib/commonjs/utils/hookUtils.js.map +1 -1
  124. package/lib/commonjs/utils/loggerUtils.js.map +1 -1
  125. package/lib/commonjs/utils/validationUtils.js +2 -2
  126. package/lib/commonjs/utils/validationUtils.js.map +1 -1
  127. package/lib/module/assets/assets/icons/OxyServices.tsx +2 -2
  128. package/lib/module/assets/assets/illustrations/HighFive.tsx +1 -1
  129. package/lib/module/assets/icons/OxyServices.js +0 -1
  130. package/lib/module/assets/icons/OxyServices.js.map +1 -1
  131. package/lib/module/assets/illustrations/HighFive.js +0 -1
  132. package/lib/module/assets/illustrations/HighFive.js.map +1 -1
  133. package/lib/module/core/OxyServices.js +243 -25
  134. package/lib/module/core/OxyServices.js.map +1 -1
  135. package/lib/module/core/index.js +1 -1
  136. package/lib/module/core/index.js.map +1 -1
  137. package/lib/module/index.js +3 -1
  138. package/lib/module/index.js.map +1 -1
  139. package/lib/module/lib/sonner-safe.js +2 -0
  140. package/lib/module/lib/sonner-safe.js.map +1 -1
  141. package/lib/module/lib/sonner.js +3 -0
  142. package/lib/module/lib/sonner.js.map +1 -1
  143. package/lib/module/node/index.js +2 -2
  144. package/lib/module/node/index.js.map +1 -1
  145. package/lib/module/ui/components/Avatar.js +0 -1
  146. package/lib/module/ui/components/Avatar.js.map +1 -1
  147. package/lib/module/ui/components/FollowButton.js +4 -3
  148. package/lib/module/ui/components/FollowButton.js.map +1 -1
  149. package/lib/module/ui/components/FontLoader.js +3 -2
  150. package/lib/module/ui/components/FontLoader.js.map +1 -1
  151. package/lib/module/ui/components/GroupedItem.js +7 -3
  152. package/lib/module/ui/components/GroupedItem.js.map +1 -1
  153. package/lib/module/ui/components/GroupedSection.js +1 -1
  154. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  155. package/lib/module/ui/components/Header.js +0 -1
  156. package/lib/module/ui/components/Header.js.map +1 -1
  157. package/lib/module/ui/components/OxyLogo.js +0 -1
  158. package/lib/module/ui/components/OxyLogo.js.map +1 -1
  159. package/lib/module/ui/components/OxyPayButton.js +4 -4
  160. package/lib/module/ui/components/OxyPayButton.js.map +1 -1
  161. package/lib/module/ui/components/OxyProvider.js +4 -2
  162. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  163. package/lib/module/ui/components/OxySignInButton.js +0 -1
  164. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  165. package/lib/module/ui/components/ProfileCard.js +0 -1
  166. package/lib/module/ui/components/ProfileCard.js.map +1 -1
  167. package/lib/module/ui/components/QuickActions.js +0 -1
  168. package/lib/module/ui/components/QuickActions.js.map +1 -1
  169. package/lib/module/ui/components/Section.js +0 -1
  170. package/lib/module/ui/components/Section.js.map +1 -1
  171. package/lib/module/ui/components/SectionTitle.js +0 -1
  172. package/lib/module/ui/components/SectionTitle.js.map +1 -1
  173. package/lib/module/ui/components/icon/FAIRWalletIcon.js +0 -1
  174. package/lib/module/ui/components/icon/FAIRWalletIcon.js.map +1 -1
  175. package/lib/module/ui/components/icon/OxyIcon.js +0 -1
  176. package/lib/module/ui/components/icon/OxyIcon.js.map +1 -1
  177. package/lib/module/ui/components/internal/GroupedPillButtons.js +0 -1
  178. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  179. package/lib/module/ui/components/internal/PinInput.js +4 -2
  180. package/lib/module/ui/components/internal/PinInput.js.map +1 -1
  181. package/lib/module/ui/components/internal/TextField.js +3 -3
  182. package/lib/module/ui/components/internal/TextField.js.map +1 -1
  183. package/lib/module/ui/context/OxyContext.js.map +1 -1
  184. package/lib/module/ui/hooks/useAssets.js +257 -0
  185. package/lib/module/ui/hooks/useAssets.js.map +1 -0
  186. package/lib/module/ui/navigation/OxyRouter.js +1 -1
  187. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  188. package/lib/module/ui/screens/AccountCenterScreen.js +0 -1
  189. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  190. package/lib/module/ui/screens/AccountManagementDemo.js +0 -1
  191. package/lib/module/ui/screens/AccountManagementDemo.js.map +1 -1
  192. package/lib/module/ui/screens/AccountSettingsScreen.js +2 -2
  193. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  194. package/lib/module/ui/screens/AccountSwitcherScreen.js +5 -5
  195. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  196. package/lib/module/ui/screens/AppInfoScreen.js +1 -1
  197. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  198. package/lib/module/ui/screens/FileManagementScreen.js +633 -432
  199. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  200. package/lib/module/ui/screens/PaymentGatewayScreen.js +1 -1
  201. package/lib/module/ui/screens/PaymentGatewayScreen.js.map +1 -1
  202. package/lib/module/ui/screens/PremiumSubscriptionScreen.js +1 -1
  203. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  204. package/lib/module/ui/screens/ProfileScreen.js +1 -1
  205. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  206. package/lib/module/ui/screens/RecoverAccountScreen.js +1 -1
  207. package/lib/module/ui/screens/RecoverAccountScreen.js.map +1 -1
  208. package/lib/module/ui/screens/SessionManagementScreen.js +1 -1
  209. package/lib/module/ui/screens/SessionManagementScreen.js.map +1 -1
  210. package/lib/module/ui/screens/SignInScreen.js +1 -1
  211. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  212. package/lib/module/ui/screens/UserLinksScreen.js +0 -1
  213. package/lib/module/ui/screens/UserLinksScreen.js.map +1 -1
  214. package/lib/module/ui/screens/internal/SignInPasswordStep.js +1 -1
  215. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +1 -1
  216. package/lib/module/ui/screens/internal/SignInUsernameStep.js +1 -1
  217. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +1 -1
  218. package/lib/module/ui/screens/internal/SignUpIdentityStep.js +1 -1
  219. package/lib/module/ui/screens/internal/SignUpIdentityStep.js.map +1 -1
  220. package/lib/module/ui/screens/internal/SignUpSecurityStep.js +1 -1
  221. package/lib/module/ui/screens/internal/SignUpSecurityStep.js.map +1 -1
  222. package/lib/module/ui/screens/internal/SignUpSummaryStep.js +0 -1
  223. package/lib/module/ui/screens/internal/SignUpSummaryStep.js.map +1 -1
  224. package/lib/module/ui/screens/internal/SignUpWelcomeStep.js +0 -1
  225. package/lib/module/ui/screens/internal/SignUpWelcomeStep.js.map +1 -1
  226. package/lib/module/ui/screens/karma/KarmaAboutScreen.js +0 -1
  227. package/lib/module/ui/screens/karma/KarmaAboutScreen.js.map +1 -1
  228. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +1 -1
  229. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  230. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js +1 -1
  231. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -1
  232. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js +0 -1
  233. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  234. package/lib/module/ui/screens/karma/KarmaRulesScreen.js +1 -1
  235. package/lib/module/ui/screens/karma/KarmaRulesScreen.js.map +1 -1
  236. package/lib/module/ui/stores/assetStore.js +212 -0
  237. package/lib/module/ui/stores/assetStore.js.map +1 -0
  238. package/lib/module/ui/stores/authStore.js +1 -1
  239. package/lib/module/ui/stores/authStore.js.map +1 -1
  240. package/lib/module/ui/stores/fileStore.js +145 -0
  241. package/lib/module/ui/stores/fileStore.js.map +1 -0
  242. package/lib/module/ui/stores/followStore.js +2 -2
  243. package/lib/module/ui/stores/followStore.js.map +1 -1
  244. package/lib/module/ui/styles/fonts.js.map +1 -1
  245. package/lib/module/ui/styles/theme.js.map +1 -1
  246. package/lib/module/utils/asyncUtils.js +1 -1
  247. package/lib/module/utils/asyncUtils.js.map +1 -1
  248. package/lib/module/utils/errorUtils.js +19 -11
  249. package/lib/module/utils/errorUtils.js.map +1 -1
  250. package/lib/module/utils/hookUtils.js +1 -1
  251. package/lib/module/utils/hookUtils.js.map +1 -1
  252. package/lib/module/utils/loggerUtils.js.map +1 -1
  253. package/lib/module/utils/validationUtils.js +2 -2
  254. package/lib/module/utils/validationUtils.js.map +1 -1
  255. package/lib/typescript/assets/icons/OxyServices.d.ts +2 -2
  256. package/lib/typescript/assets/icons/OxyServices.d.ts.map +1 -1
  257. package/lib/typescript/assets/illustrations/HighFive.d.ts +1 -1
  258. package/lib/typescript/assets/illustrations/HighFive.d.ts.map +1 -1
  259. package/lib/typescript/core/OxyServices.d.ts +69 -12
  260. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  261. package/lib/typescript/core/index.d.ts +1 -1
  262. package/lib/typescript/core/index.d.ts.map +1 -1
  263. package/lib/typescript/index.d.ts +4 -2
  264. package/lib/typescript/index.d.ts.map +1 -1
  265. package/lib/typescript/lib/sonner-safe.d.ts +2 -1
  266. package/lib/typescript/lib/sonner-safe.d.ts.map +1 -1
  267. package/lib/typescript/lib/sonner.d.ts +11 -3
  268. package/lib/typescript/lib/sonner.d.ts.map +1 -1
  269. package/lib/typescript/models/interfaces.d.ts +105 -6
  270. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  271. package/lib/typescript/node/index.d.ts +2 -2
  272. package/lib/typescript/node/index.d.ts.map +1 -1
  273. package/lib/typescript/types/expo-vector-icons.d.ts +8 -1
  274. package/lib/typescript/types/express.d.ts +22 -3
  275. package/lib/typescript/ui/components/Avatar.d.ts +2 -2
  276. package/lib/typescript/ui/components/Avatar.d.ts.map +1 -1
  277. package/lib/typescript/ui/components/FollowButton.d.ts +2 -2
  278. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  279. package/lib/typescript/ui/components/FontLoader.d.ts +1 -1
  280. package/lib/typescript/ui/components/FontLoader.d.ts.map +1 -1
  281. package/lib/typescript/ui/components/GroupedItem.d.ts +2 -1
  282. package/lib/typescript/ui/components/GroupedItem.d.ts.map +1 -1
  283. package/lib/typescript/ui/components/GroupedSection.d.ts +2 -1
  284. package/lib/typescript/ui/components/GroupedSection.d.ts.map +1 -1
  285. package/lib/typescript/ui/components/Header.d.ts +1 -1
  286. package/lib/typescript/ui/components/Header.d.ts.map +1 -1
  287. package/lib/typescript/ui/components/OxyLogo.d.ts +2 -2
  288. package/lib/typescript/ui/components/OxyLogo.d.ts.map +1 -1
  289. package/lib/typescript/ui/components/OxyPayButton.d.ts +2 -2
  290. package/lib/typescript/ui/components/OxyPayButton.d.ts.map +1 -1
  291. package/lib/typescript/ui/components/OxyProvider.d.ts +2 -2
  292. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  293. package/lib/typescript/ui/components/OxySignInButton.d.ts +2 -2
  294. package/lib/typescript/ui/components/OxySignInButton.d.ts.map +1 -1
  295. package/lib/typescript/ui/components/ProfileCard.d.ts +1 -1
  296. package/lib/typescript/ui/components/ProfileCard.d.ts.map +1 -1
  297. package/lib/typescript/ui/components/QuickActions.d.ts +1 -1
  298. package/lib/typescript/ui/components/QuickActions.d.ts.map +1 -1
  299. package/lib/typescript/ui/components/Section.d.ts +1 -1
  300. package/lib/typescript/ui/components/Section.d.ts.map +1 -1
  301. package/lib/typescript/ui/components/SectionTitle.d.ts +1 -1
  302. package/lib/typescript/ui/components/SectionTitle.d.ts.map +1 -1
  303. package/lib/typescript/ui/components/icon/FAIRWalletIcon.d.ts +1 -1
  304. package/lib/typescript/ui/components/icon/FAIRWalletIcon.d.ts.map +1 -1
  305. package/lib/typescript/ui/components/icon/OxyIcon.d.ts +1 -1
  306. package/lib/typescript/ui/components/icon/OxyIcon.d.ts.map +1 -1
  307. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts +1 -1
  308. package/lib/typescript/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  309. package/lib/typescript/ui/components/internal/PinInput.d.ts +1 -1
  310. package/lib/typescript/ui/components/internal/PinInput.d.ts.map +1 -1
  311. package/lib/typescript/ui/components/internal/TextField.d.ts +1 -1
  312. package/lib/typescript/ui/components/internal/TextField.d.ts.map +1 -1
  313. package/lib/typescript/ui/context/OxyContext.d.ts +3 -3
  314. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  315. package/lib/typescript/ui/hooks/useAssets.d.ts +35 -0
  316. package/lib/typescript/ui/hooks/useAssets.d.ts.map +1 -0
  317. package/lib/typescript/ui/navigation/OxyRouter.d.ts +2 -2
  318. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  319. package/lib/typescript/ui/navigation/types.d.ts +7 -7
  320. package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
  321. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts +2 -2
  322. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  323. package/lib/typescript/ui/screens/AccountManagementDemo.d.ts +1 -1
  324. package/lib/typescript/ui/screens/AccountManagementDemo.d.ts.map +1 -1
  325. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts +1 -1
  326. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  327. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts +1 -1
  328. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  329. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts +2 -2
  330. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  331. package/lib/typescript/ui/screens/AppInfoScreen.d.ts +2 -2
  332. package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
  333. package/lib/typescript/ui/screens/FeedbackScreen.d.ts +1 -1
  334. package/lib/typescript/ui/screens/FeedbackScreen.d.ts.map +1 -1
  335. package/lib/typescript/ui/screens/FileManagementScreen.d.ts +1 -1
  336. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  337. package/lib/typescript/ui/screens/PaymentGatewayScreen.d.ts +2 -2
  338. package/lib/typescript/ui/screens/PaymentGatewayScreen.d.ts.map +1 -1
  339. package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts +2 -2
  340. package/lib/typescript/ui/screens/PremiumSubscriptionScreen.d.ts.map +1 -1
  341. package/lib/typescript/ui/screens/ProfileScreen.d.ts +2 -2
  342. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  343. package/lib/typescript/ui/screens/RecoverAccountScreen.d.ts +1 -1
  344. package/lib/typescript/ui/screens/RecoverAccountScreen.d.ts.map +1 -1
  345. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts +2 -2
  346. package/lib/typescript/ui/screens/SessionManagementScreen.d.ts.map +1 -1
  347. package/lib/typescript/ui/screens/SignInScreen.d.ts +2 -2
  348. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  349. package/lib/typescript/ui/screens/SignUpScreen.d.ts +1 -1
  350. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  351. package/lib/typescript/ui/screens/UserLinksScreen.d.ts +2 -2
  352. package/lib/typescript/ui/screens/UserLinksScreen.d.ts.map +1 -1
  353. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts +1 -1
  354. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +1 -1
  355. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts +1 -1
  356. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts.map +1 -1
  357. package/lib/typescript/ui/screens/internal/SignUpIdentityStep.d.ts +1 -1
  358. package/lib/typescript/ui/screens/internal/SignUpIdentityStep.d.ts.map +1 -1
  359. package/lib/typescript/ui/screens/internal/SignUpSecurityStep.d.ts +1 -1
  360. package/lib/typescript/ui/screens/internal/SignUpSecurityStep.d.ts.map +1 -1
  361. package/lib/typescript/ui/screens/internal/SignUpSummaryStep.d.ts +1 -1
  362. package/lib/typescript/ui/screens/internal/SignUpSummaryStep.d.ts.map +1 -1
  363. package/lib/typescript/ui/screens/internal/SignUpWelcomeStep.d.ts +1 -1
  364. package/lib/typescript/ui/screens/internal/SignUpWelcomeStep.d.ts.map +1 -1
  365. package/lib/typescript/ui/screens/karma/KarmaAboutScreen.d.ts +2 -2
  366. package/lib/typescript/ui/screens/karma/KarmaAboutScreen.d.ts.map +1 -1
  367. package/lib/typescript/ui/screens/karma/KarmaCenterScreen.d.ts +2 -2
  368. package/lib/typescript/ui/screens/karma/KarmaCenterScreen.d.ts.map +1 -1
  369. package/lib/typescript/ui/screens/karma/KarmaFAQScreen.d.ts +1 -1
  370. package/lib/typescript/ui/screens/karma/KarmaFAQScreen.d.ts.map +1 -1
  371. package/lib/typescript/ui/screens/karma/KarmaLeaderboardScreen.d.ts +2 -2
  372. package/lib/typescript/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +1 -1
  373. package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts +2 -2
  374. package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts.map +1 -1
  375. package/lib/typescript/ui/screens/karma/KarmaRulesScreen.d.ts +2 -2
  376. package/lib/typescript/ui/screens/karma/KarmaRulesScreen.d.ts.map +1 -1
  377. package/lib/typescript/ui/stores/assetStore.d.ts +54 -0
  378. package/lib/typescript/ui/stores/assetStore.d.ts.map +1 -0
  379. package/lib/typescript/ui/stores/fileStore.d.ts +31 -0
  380. package/lib/typescript/ui/stores/fileStore.d.ts.map +1 -0
  381. package/lib/typescript/ui/stores/followStore.d.ts +1 -1
  382. package/lib/typescript/ui/stores/followStore.d.ts.map +1 -1
  383. package/lib/typescript/ui/styles/fonts.d.ts +1 -1
  384. package/lib/typescript/ui/styles/fonts.d.ts.map +1 -1
  385. package/lib/typescript/ui/styles/theme.d.ts +1 -1
  386. package/lib/typescript/ui/styles/theme.d.ts.map +1 -1
  387. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  388. package/lib/typescript/utils/errorUtils.d.ts +5 -5
  389. package/lib/typescript/utils/errorUtils.d.ts.map +1 -1
  390. package/lib/typescript/utils/hookUtils.d.ts +4 -4
  391. package/lib/typescript/utils/hookUtils.d.ts.map +1 -1
  392. package/lib/typescript/utils/loggerUtils.d.ts +18 -18
  393. package/lib/typescript/utils/loggerUtils.d.ts.map +1 -1
  394. package/lib/typescript/utils/validationUtils.d.ts +6 -6
  395. package/lib/typescript/utils/validationUtils.d.ts.map +1 -1
  396. package/package.json +149 -143
  397. package/src/assets/icons/OxyServices.tsx +2 -2
  398. package/src/assets/illustrations/HighFive.tsx +1 -1
  399. package/src/core/OxyServices.ts +268 -41
  400. package/src/core/__tests__/OxyServices.test.ts +180 -0
  401. package/src/core/index.ts +1 -1
  402. package/src/index.ts +16 -2
  403. package/src/lib/sonner-safe.ts +4 -1
  404. package/src/lib/sonner.ts +19 -2
  405. package/src/models/interfaces.ts +117 -6
  406. package/src/node/index.ts +2 -2
  407. package/src/types/expo-vector-icons.d.ts +8 -1
  408. package/src/types/express.d.ts +22 -3
  409. package/src/ui/components/Avatar.tsx +2 -2
  410. package/src/ui/components/FollowButton.tsx +10 -8
  411. package/src/ui/components/FontLoader.tsx +5 -3
  412. package/src/ui/components/GroupedItem.tsx +12 -2
  413. package/src/ui/components/GroupedSection.tsx +3 -1
  414. package/src/ui/components/Header.tsx +1 -1
  415. package/src/ui/components/OxyLogo.tsx +2 -2
  416. package/src/ui/components/OxyPayButton.tsx +6 -5
  417. package/src/ui/components/OxyProvider.tsx +7 -4
  418. package/src/ui/components/OxySignInButton.tsx +2 -2
  419. package/src/ui/components/ProfileCard.tsx +1 -1
  420. package/src/ui/components/QuickActions.tsx +1 -1
  421. package/src/ui/components/Section.tsx +1 -1
  422. package/src/ui/components/SectionTitle.tsx +1 -1
  423. package/src/ui/components/icon/FAIRWalletIcon.tsx +1 -1
  424. package/src/ui/components/icon/OxyIcon.tsx +1 -1
  425. package/src/ui/components/internal/GroupedPillButtons.tsx +1 -1
  426. package/src/ui/components/internal/PinInput.tsx +3 -2
  427. package/src/ui/components/internal/TextField.tsx +9 -11
  428. package/src/ui/context/OxyContext.tsx +3 -3
  429. package/src/ui/hooks/useAssets.ts +306 -0
  430. package/src/ui/navigation/OxyRouter.tsx +3 -2
  431. package/src/ui/navigation/types.ts +8 -8
  432. package/src/ui/screens/AccountCenterScreen.tsx +2 -2
  433. package/src/ui/screens/AccountManagementDemo.tsx +1 -1
  434. package/src/ui/screens/AccountOverviewScreen.tsx +1 -1
  435. package/src/ui/screens/AccountSettingsScreen.tsx +3 -3
  436. package/src/ui/screens/AccountSwitcherScreen.tsx +9 -8
  437. package/src/ui/screens/AppInfoScreen.tsx +3 -2
  438. package/src/ui/screens/FeedbackScreen.tsx +1 -1
  439. package/src/ui/screens/FileManagementScreen.tsx +619 -494
  440. package/src/ui/screens/PaymentGatewayScreen.tsx +3 -2
  441. package/src/ui/screens/PremiumSubscriptionScreen.tsx +3 -2
  442. package/src/ui/screens/ProfileScreen.tsx +3 -2
  443. package/src/ui/screens/RecoverAccountScreen.tsx +3 -2
  444. package/src/ui/screens/SessionManagementScreen.tsx +4 -3
  445. package/src/ui/screens/SignInScreen.tsx +3 -2
  446. package/src/ui/screens/SignUpScreen.tsx +1 -1
  447. package/src/ui/screens/UserLinksScreen.tsx +2 -2
  448. package/src/ui/screens/internal/SignInPasswordStep.tsx +3 -2
  449. package/src/ui/screens/internal/SignInUsernameStep.tsx +3 -2
  450. package/src/ui/screens/internal/SignUpIdentityStep.tsx +3 -2
  451. package/src/ui/screens/internal/SignUpSecurityStep.tsx +3 -2
  452. package/src/ui/screens/internal/SignUpSummaryStep.tsx +1 -1
  453. package/src/ui/screens/internal/SignUpWelcomeStep.tsx +1 -1
  454. package/src/ui/screens/karma/KarmaAboutScreen.tsx +2 -2
  455. package/src/ui/screens/karma/KarmaCenterScreen.tsx +3 -2
  456. package/src/ui/screens/karma/KarmaFAQScreen.tsx +1 -1
  457. package/src/ui/screens/karma/KarmaLeaderboardScreen.tsx +3 -2
  458. package/src/ui/screens/karma/KarmaRewardsScreen.tsx +2 -2
  459. package/src/ui/screens/karma/KarmaRulesScreen.tsx +3 -2
  460. package/src/ui/stores/assetStore.ts +281 -0
  461. package/src/ui/stores/authStore.ts +1 -1
  462. package/src/ui/stores/fileStore.ts +118 -0
  463. package/src/ui/stores/followStore.ts +4 -4
  464. package/src/ui/styles/fonts.ts +1 -1
  465. package/src/ui/styles/theme.ts +1 -1
  466. package/src/utils/__tests__/validationUtils.test.ts +236 -0
  467. package/src/utils/asyncUtils.ts +4 -4
  468. package/src/utils/errorUtils.ts +35 -23
  469. package/src/utils/hookUtils.ts +7 -7
  470. package/src/utils/loggerUtils.ts +18 -18
  471. package/src/utils/validationUtils.ts +8 -8
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
1
+ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
2
2
  import {
3
3
  View,
4
4
  Text,
@@ -11,39 +11,26 @@ import {
11
11
  Dimensions,
12
12
  Modal,
13
13
  TextInput,
14
- Image,
14
+ Image, // kept for Image.getSize only
15
15
  } from 'react-native';
16
- import { BaseScreenProps } from '../navigation/types';
16
+ import { Image as ExpoImage } from 'expo-image';
17
+ import type { BaseScreenProps } from '../navigation/types';
17
18
  import { useOxy } from '../context/OxyContext';
18
19
  import { fontFamilies } from '../styles/fonts';
19
20
  import { toast } from '../../lib/sonner';
20
21
  import { Ionicons } from '@expo/vector-icons';
21
- import { FileMetadata } from '../../models/interfaces';
22
+ import type { FileMetadata } from '../../models/interfaces';
23
+ import { useFileStore, useFiles, useUploading as useUploadingStore, useUploadAggregateProgress, useDeleting as useDeletingStore } from '../stores/fileStore';
24
+ import Header from '../components/Header';
25
+ import { GroupedSection } from '../components';
22
26
 
23
27
  interface FileManagementScreenProps extends BaseScreenProps {
24
28
  userId?: string;
25
29
  }
26
30
 
27
31
  // Add this helper function near the top (after imports):
28
- async function uploadFileRaw(file: File | Blob, userId: string) {
29
- const fileName = (file as any).name || 'upload.bin';
30
- const mimeType = (file as any).type || 'application/octet-stream';
31
-
32
- const res = await fetch('/api/files/upload-raw', {
33
- method: 'POST',
34
- headers: {
35
- 'Content-Type': mimeType,
36
- 'X-File-Name': encodeURIComponent(fileName),
37
- 'X-User-Id': userId,
38
- },
39
- body: file,
40
- credentials: 'include', // if you use cookies/session
41
- });
42
-
43
- if (!res.ok) {
44
- throw new Error(await res.text());
45
- }
46
- return await res.json();
32
+ async function uploadFileRaw(file: File | Blob, userId: string, oxyServices: any) {
33
+ return await oxyServices.uploadRawFile(file);
47
34
  }
48
35
 
49
36
  const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
@@ -67,12 +54,12 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
67
54
  console.log('[FileManagementScreen] Available content width:', availableContentWidth);
68
55
  console.log('[FileManagementScreen] Spacing fix applied: 4px uniform gap both horizontal and vertical');
69
56
  }, [containerWidth]);
70
- const [files, setFiles] = useState<FileMetadata[]>([]);
57
+ const files = useFiles();
58
+ const uploading = useUploadingStore();
59
+ const uploadProgress = useUploadAggregateProgress();
60
+ const deleting = useDeletingStore();
71
61
  const [loading, setLoading] = useState(true);
72
62
  const [refreshing, setRefreshing] = useState(false);
73
- const [uploading, setUploading] = useState(false);
74
- const [uploadProgress, setUploadProgress] = useState<{ current: number, total: number } | null>(null);
75
- const [deleting, setDeleting] = useState<string | null>(null);
76
63
  const [selectedFile, setSelectedFile] = useState<FileMetadata | null>(null);
77
64
  const [showFileDetails, setShowFileDetails] = useState(false);
78
65
  const [openedFile, setOpenedFile] = useState<FileMetadata | null>(null);
@@ -81,11 +68,74 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
81
68
  const [showFileDetailsInViewer, setShowFileDetailsInViewer] = useState(false);
82
69
  const [viewMode, setViewMode] = useState<'all' | 'photos'>('all');
83
70
  const [searchQuery, setSearchQuery] = useState('');
84
- const [filteredFiles, setFilteredFiles] = useState<FileMetadata[]>([]);
71
+ // Derived filtered files (avoid setState loops)
72
+ const filteredFiles = useMemo(() => {
73
+ let filteredByMode = files;
74
+ if (viewMode === 'photos') {
75
+ filteredByMode = files.filter(file => file.contentType.startsWith('image/'));
76
+ }
77
+ if (!searchQuery.trim()) {
78
+ return filteredByMode;
79
+ }
80
+ const query = searchQuery.toLowerCase();
81
+ return filteredByMode.filter(file =>
82
+ file.filename.toLowerCase().includes(query) ||
83
+ file.contentType.toLowerCase().includes(query) ||
84
+ (file.metadata?.description && file.metadata.description.toLowerCase().includes(query))
85
+ );
86
+ }, [files, searchQuery, viewMode]);
85
87
  const [isDragging, setIsDragging] = useState(false);
86
88
  const [photoDimensions, setPhotoDimensions] = useState<{ [key: string]: { width: number, height: number } }>({});
87
89
  const [loadingDimensions, setLoadingDimensions] = useState(false);
88
90
  const [hoveredPreview, setHoveredPreview] = useState<string | null>(null);
91
+ const uploadStartRef = useRef<number | null>(null);
92
+ const MIN_BANNER_MS = 600;
93
+ const endUpload = useCallback(() => {
94
+ const started = uploadStartRef.current;
95
+ const elapsed = started ? Date.now() - started : MIN_BANNER_MS;
96
+ const remaining = elapsed < MIN_BANNER_MS ? MIN_BANNER_MS - elapsed : 0;
97
+ setTimeout(() => {
98
+ useFileStore.getState().setUploading(false);
99
+ uploadStartRef.current = null;
100
+ }, remaining);
101
+ }, []);
102
+
103
+ // Helper to safely request a thumbnail variant only for image mime types.
104
+ // Prevents backend warnings: "Variant thumb not supported for mime application/pdf".
105
+ const getSafeDownloadUrl = useCallback(
106
+ (file: FileMetadata, variant: string = 'thumb') => {
107
+ const isImage = file.contentType.startsWith('image/');
108
+ const isVideo = file.contentType.startsWith('video/');
109
+
110
+ // Prefer explicit variant key if variants metadata present
111
+ if (file.variants && file.variants.length > 0) {
112
+ // For videos, try 'poster' regardless of requested variant
113
+ if (isVideo) {
114
+ const poster = file.variants.find(v => v.type === 'poster');
115
+ if (poster) return oxyServices.getFileDownloadUrl(file.id, 'poster');
116
+ }
117
+ if (isImage) {
118
+ const desired = file.variants.find(v => v.type === variant);
119
+ if (desired) return oxyServices.getFileDownloadUrl(file.id, variant);
120
+ }
121
+ }
122
+
123
+ if (isImage) {
124
+ return oxyServices.getFileDownloadUrl(file.id, variant);
125
+ }
126
+ if (isVideo) {
127
+ // Fallback to poster if backend supports implicit generation
128
+ try {
129
+ return oxyServices.getFileDownloadUrl(file.id, 'poster');
130
+ } catch {
131
+ return oxyServices.getFileDownloadUrl(file.id);
132
+ }
133
+ }
134
+ // Other mime types: no variant
135
+ return oxyServices.getFileDownloadUrl(file.id);
136
+ },
137
+ [oxyServices]
138
+ );
89
139
 
90
140
  // Memoize theme-related calculations to prevent unnecessary recalculations
91
141
  const themeStyles = useMemo(() => {
@@ -108,18 +158,33 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
108
158
 
109
159
  const targetUserId = userId || user?.id;
110
160
 
111
- const loadFiles = useCallback(async (isRefresh = false) => {
161
+ const storeSetUploading = useFileStore(s => s.setUploading);
162
+ const storeSetUploadProgress = useFileStore(s => s.setUploadProgress);
163
+ const storeSetDeleting = useFileStore(s => s.setDeleting);
164
+
165
+ const loadFiles = useCallback(async (mode: 'initial' | 'refresh' | 'silent' = 'initial') => {
112
166
  if (!targetUserId) return;
113
167
 
114
168
  try {
115
- if (isRefresh) {
169
+ if (mode === 'refresh') {
116
170
  setRefreshing(true);
117
- } else {
171
+ } else if (mode === 'initial') {
118
172
  setLoading(true);
119
173
  }
120
174
 
121
- const response = await oxyServices.listUserFiles(targetUserId);
122
- setFiles(response.files || []);
175
+ const response = await oxyServices.listUserFiles();
176
+ const assets: FileMetadata[] = (response.files || []).map((f: any) => ({
177
+ id: f.id,
178
+ filename: f.originalName || f.sha256,
179
+ contentType: f.mime,
180
+ length: f.size,
181
+ chunkSize: 0,
182
+ uploadDate: f.createdAt,
183
+ metadata: f.metadata || {},
184
+ variants: f.variants || [],
185
+ }));
186
+ // Merge to preserve existing order & allow incremental updates
187
+ useFileStore.getState().setFiles(assets, { merge: true });
123
188
  } catch (error: any) {
124
189
  console.error('Failed to load files:', error);
125
190
  toast.error(error.message || 'Failed to load files');
@@ -129,28 +194,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
129
194
  }
130
195
  }, [targetUserId, oxyServices]);
131
196
 
132
- // Filter files based on search query and view mode
133
- useEffect(() => {
134
- let filteredByMode = files;
135
-
136
- // Filter by view mode first
137
- if (viewMode === 'photos') {
138
- filteredByMode = files.filter(file => file.contentType.startsWith('image/'));
139
- }
140
-
141
- // Then filter by search query
142
- if (!searchQuery.trim()) {
143
- setFilteredFiles(filteredByMode);
144
- } else {
145
- const query = searchQuery.toLowerCase();
146
- const filtered = filteredByMode.filter(file =>
147
- file.filename.toLowerCase().includes(query) ||
148
- file.contentType.toLowerCase().includes(query) ||
149
- (file.metadata?.description && file.metadata.description.toLowerCase().includes(query))
150
- );
151
- setFilteredFiles(filtered);
152
- }
153
- }, [files, searchQuery, viewMode]);
197
+ // (removed effect; filteredFiles is memoized)
154
198
 
155
199
  // Load photo dimensions for justified grid
156
200
  const loadPhotoDimensions = useCallback(async (photos: FileMetadata[]) => {
@@ -172,7 +216,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
172
216
  await Promise.all(
173
217
  photosToLoad.map(async (photo) => {
174
218
  try {
175
- const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
219
+ const downloadUrl = getSafeDownloadUrl(photo, 'thumb');
176
220
 
177
221
  if (Platform.OS === 'web') {
178
222
  const img = new (window as any).Image();
@@ -249,7 +293,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
249
293
  if (selectedFiles.length === 0) return;
250
294
  if (!targetUserId) return; // Guard clause to ensure userId is defined
251
295
  try {
252
- setUploadProgress({ current: 0, total: selectedFiles.length });
296
+ storeSetUploadProgress({ current: 0, total: selectedFiles.length });
253
297
  const maxSize = 50 * 1024 * 1024; // 50MB
254
298
  const oversizedFiles = selectedFiles.filter(file => file.size > maxSize);
255
299
  if (oversizedFiles.length > 0) {
@@ -261,9 +305,42 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
261
305
  let failureCount = 0;
262
306
  const errors: string[] = [];
263
307
  for (let i = 0; i < selectedFiles.length; i++) {
264
- setUploadProgress({ current: i + 1, total: selectedFiles.length });
308
+ storeSetUploadProgress({ current: i + 1, total: selectedFiles.length });
265
309
  try {
266
- await uploadFileRaw(selectedFiles[i], targetUserId);
310
+ const raw = selectedFiles[i];
311
+ const optimisticId = `temp-${Date.now()}-${i}`;
312
+ const optimisticFile: FileMetadata = {
313
+ id: optimisticId,
314
+ filename: raw.name,
315
+ contentType: raw.type || 'application/octet-stream',
316
+ length: raw.size,
317
+ chunkSize: 0,
318
+ uploadDate: new Date().toISOString(),
319
+ metadata: { uploading: true },
320
+ variants: [],
321
+ };
322
+ useFileStore.getState().addFile(optimisticFile, { prepend: true });
323
+ const result = await uploadFileRaw(raw, targetUserId, oxyServices);
324
+ // Attempt to refresh file list incrementally – fetch single file metadata if API allows
325
+ if (result?.file || result?.files?.[0]) {
326
+ const f = result.file || result.files[0];
327
+ const merged: FileMetadata = {
328
+ id: f.id,
329
+ filename: f.originalName || f.sha256 || raw.name,
330
+ contentType: f.mime || raw.type || 'application/octet-stream',
331
+ length: f.size || raw.size,
332
+ chunkSize: 0,
333
+ uploadDate: f.createdAt || new Date().toISOString(),
334
+ metadata: f.metadata || {},
335
+ variants: f.variants || [],
336
+ };
337
+ // Remove optimistic then add real
338
+ useFileStore.getState().removeFile(optimisticId);
339
+ useFileStore.getState().addFile(merged, { prepend: true });
340
+ } else {
341
+ // Fallback: will reconcile on later list refresh
342
+ useFileStore.getState().updateFile(optimisticId, { metadata: { uploading: false } as any });
343
+ }
267
344
  successCount++;
268
345
  } catch (error: any) {
269
346
  failureCount++;
@@ -277,21 +354,21 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
277
354
  const errorMessage = `${failureCount} file(s) failed to upload${errors.length > 0 ? ':\n' + errors.slice(0, 3).join('\n') + (errors.length > 3 ? '\n...' : '') : ''}`;
278
355
  toast.error(errorMessage);
279
356
  }
280
- setTimeout(async () => {
281
- await loadFiles();
282
- }, 500);
357
+ // Silent background refresh to ensure metadata/variants updated
358
+ setTimeout(() => { loadFiles('silent'); }, 1200);
283
359
  } catch (error: any) {
284
360
  console.error('Upload error:', error);
285
361
  toast.error(error.message || 'Failed to upload files');
286
362
  } finally {
287
- setUploadProgress(null);
363
+ storeSetUploadProgress(null);
288
364
  }
289
365
  };
290
366
 
291
367
  const handleFileUpload = async () => {
292
368
  try {
293
- setUploading(true);
294
- setUploadProgress(null);
369
+ uploadStartRef.current = Date.now();
370
+ storeSetUploading(true);
371
+ storeSetUploadProgress(null);
295
372
 
296
373
  if (Platform.OS === 'web') {
297
374
  // Web file picker implementation
@@ -299,10 +376,26 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
299
376
  input.type = 'file';
300
377
  input.multiple = true;
301
378
  input.accept = '*/*';
379
+ // Fallback: if the user cancels the dialog (no onchange fires or 0 files), hide banner
380
+ const cancellationTimer = setTimeout(() => {
381
+ const state = useFileStore.getState();
382
+ if (state.uploading && uploadStartRef.current && !state.uploadProgress) {
383
+ // No selection happened; treat as cancel
384
+ endUpload();
385
+ }
386
+ }, 1500); // allow enough time for user to pick
302
387
 
303
388
  input.onchange = async (e: any) => {
304
- const selectedFiles = Array.from(e.target.files) as File[];
389
+ clearTimeout(cancellationTimer);
390
+ const selectedFiles = Array.from(e.target.files || []) as File[];
391
+ if (selectedFiles.length === 0) {
392
+ // User explicitly canceled (some browsers still fire onchange with empty list)
393
+ endUpload();
394
+ return;
395
+ }
396
+ storeSetUploadProgress({ current: 0, total: selectedFiles.length });
305
397
  await processFileUploads(selectedFiles);
398
+ endUpload();
306
399
  };
307
400
 
308
401
  input.click();
@@ -320,8 +413,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
320
413
  } catch (error: any) {
321
414
  toast.error(error.message || 'Failed to upload file');
322
415
  } finally {
323
- setUploading(false);
324
- setUploadProgress(null);
416
+ // IMPORTANT: Do NOT call endUpload here.
417
+ // We only want to hide the banner after the actual upload(s) complete.
418
+ // The input.onchange handler invokes processFileUploads then calls endUpload().
419
+ // Calling endUpload here caused the banner to disappear while files were still uploading.
420
+ storeSetUploadProgress(null); // keep clearing any stale progress
325
421
  }
326
422
  };
327
423
 
@@ -338,7 +434,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
338
434
  console.log('Deleting file:', { fileId, filename });
339
435
  console.log('Target user ID:', targetUserId);
340
436
  console.log('Current user ID:', user?.id);
341
- setDeleting(fileId);
437
+ storeSetDeleting(fileId);
342
438
 
343
439
  const result = await oxyServices.deleteFile(fileId);
344
440
  console.log('Delete result:', result);
@@ -346,9 +442,10 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
346
442
  toast.success('File deleted successfully');
347
443
 
348
444
  // Reload files after successful deletion
349
- setTimeout(async () => {
350
- await loadFiles();
351
- }, 500);
445
+ // Optimistic remove
446
+ useFileStore.getState().removeFile(fileId);
447
+ // Silent background reconcile
448
+ setTimeout(() => loadFiles('silent'), 800);
352
449
  } catch (error: any) {
353
450
  console.error('Delete error:', error);
354
451
  console.error('Error details:', error.response?.data || error.message);
@@ -357,16 +454,14 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
357
454
  if (error.message?.includes('File not found') || error.message?.includes('404')) {
358
455
  toast.error('File not found. It may have already been deleted.');
359
456
  // Still reload files to refresh the list
360
- setTimeout(async () => {
361
- await loadFiles();
362
- }, 500);
457
+ setTimeout(() => loadFiles('silent'), 800);
363
458
  } else if (error.message?.includes('permission') || error.message?.includes('403')) {
364
459
  toast.error('You do not have permission to delete this file.');
365
460
  } else {
366
461
  toast.error(error.message || 'Failed to delete file');
367
462
  }
368
463
  } finally {
369
- setDeleting(null);
464
+ storeSetDeleting(null);
370
465
  }
371
466
  };
372
467
 
@@ -378,6 +473,13 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
378
473
  }
379
474
  };
380
475
 
476
+ const handleDragEnter = (e: any) => {
477
+ if (Platform.OS === 'web' && user?.id === targetUserId) {
478
+ e.preventDefault();
479
+ setIsDragging(true);
480
+ }
481
+ };
482
+
381
483
  const handleDragLeave = (e: any) => {
382
484
  if (Platform.OS === 'web') {
383
485
  e.preventDefault();
@@ -385,19 +487,54 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
385
487
  }
386
488
  };
387
489
 
490
+ // Global drag listeners (web) to catch drags outside component bounds
491
+ useEffect(() => {
492
+ if (Platform.OS !== 'web' || user?.id !== targetUserId) return;
493
+ const onDocDragEnter = (e: any) => {
494
+ if (e?.dataTransfer?.types?.includes('Files')) setIsDragging(true);
495
+ };
496
+ const onDocDragOver = (e: any) => {
497
+ if (e?.dataTransfer?.types?.includes('Files')) {
498
+ e.preventDefault();
499
+ setIsDragging(true);
500
+ }
501
+ };
502
+ const onDocDrop = (e: any) => {
503
+ if (e?.dataTransfer?.files?.length) {
504
+ e.preventDefault();
505
+ setIsDragging(false);
506
+ }
507
+ };
508
+ const onDocDragLeave = (e: any) => {
509
+ if (!e.relatedTarget && e.screenX === 0 && e.screenY === 0) setIsDragging(false);
510
+ };
511
+ document.addEventListener('dragenter', onDocDragEnter);
512
+ document.addEventListener('dragover', onDocDragOver);
513
+ document.addEventListener('drop', onDocDrop);
514
+ document.addEventListener('dragleave', onDocDragLeave);
515
+ return () => {
516
+ document.removeEventListener('dragenter', onDocDragEnter);
517
+ document.removeEventListener('dragover', onDocDragOver);
518
+ document.removeEventListener('drop', onDocDrop);
519
+ document.removeEventListener('dragleave', onDocDragLeave);
520
+ };
521
+ }, [user?.id, targetUserId]);
522
+
388
523
  const handleDrop = async (e: any) => {
389
524
  if (Platform.OS === 'web' && user?.id === targetUserId) {
390
525
  e.preventDefault();
391
526
  setIsDragging(false);
392
- setUploading(true);
527
+ uploadStartRef.current = Date.now();
528
+ storeSetUploading(true);
393
529
 
394
530
  try {
395
531
  const files = Array.from(e.dataTransfer.files) as File[];
532
+ if (files.length > 0) storeSetUploadProgress({ current: 0, total: files.length });
396
533
  await processFileUploads(files);
397
534
  } catch (error: any) {
398
535
  toast.error(error.message || 'Failed to upload files');
399
536
  } finally {
400
- setUploading(false);
537
+ endUpload();
401
538
  }
402
539
  }
403
540
  };
@@ -455,7 +592,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
455
592
  const k = 1024;
456
593
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
457
594
  const i = Math.floor(Math.log(bytes) / Math.log(k));
458
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
595
+ return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
459
596
  };
460
597
 
461
598
  const getFileIcon = (contentType: string): string => {
@@ -532,7 +669,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
532
669
  };
533
670
 
534
671
  const renderSimplePhotoItem = useCallback((photo: FileMetadata, index: number) => {
535
- const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
672
+ const downloadUrl = getSafeDownloadUrl(photo, 'thumb');
536
673
 
537
674
  // Calculate photo item width based on actual container size from bottom sheet
538
675
  let itemsPerRow = 3; // Default for mobile
@@ -560,45 +697,24 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
560
697
  activeOpacity={0.8}
561
698
  >
562
699
  <View style={styles.simplePhotoContainer}>
563
- {Platform.OS === 'web' ? (
564
- <img
565
- src={downloadUrl}
566
- alt={photo.filename}
567
- style={{
568
- width: '100%',
569
- height: '100%',
570
- objectFit: 'cover',
571
- borderRadius: 8,
572
- transition: 'transform 0.2s ease',
573
- }}
574
- loading="lazy"
575
- onError={(e) => {
576
- console.error('Photo failed to load:', e);
577
- }}
578
- onMouseEnter={(e) => {
579
- e.currentTarget.style.transform = 'scale(1.05)';
580
- }}
581
- onMouseLeave={(e) => {
582
- e.currentTarget.style.transform = 'scale(1)';
583
- }}
584
- />
585
- ) : (
586
- <Image
587
- source={{ uri: downloadUrl }}
588
- style={styles.simplePhotoImage}
589
- resizeMode="cover"
590
- onError={(e) => {
591
- console.error('Photo failed to load:', e);
592
- }}
593
- />
594
- )}
700
+ <ExpoImage
701
+ source={{ uri: downloadUrl }}
702
+ style={styles.simplePhotoImage}
703
+ contentFit="cover"
704
+ transition={120}
705
+ cachePolicy="memory-disk"
706
+ onError={(e: any) => {
707
+ console.error('Photo failed to load:', (e as any)?.nativeEvent ?? e);
708
+ }}
709
+ accessibilityLabel={photo.filename}
710
+ />
595
711
  </View>
596
712
  </TouchableOpacity>
597
713
  );
598
714
  }, [oxyServices, containerWidth]);
599
715
 
600
716
  const renderJustifiedPhotoItem = useCallback((photo: FileMetadata, width: number, height: number, isLast: boolean) => {
601
- const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
717
+ const downloadUrl = getSafeDownloadUrl(photo, 'thumb');
602
718
 
603
719
  return (
604
720
  <TouchableOpacity
@@ -614,50 +730,32 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
614
730
  activeOpacity={0.8}
615
731
  >
616
732
  <View style={styles.justifiedPhotoContainer}>
617
- {Platform.OS === 'web' ? (
618
- <img
619
- src={downloadUrl}
620
- alt={photo.filename}
621
- style={{
622
- width: '100%',
623
- height: '100%',
624
- objectFit: 'cover',
625
- borderRadius: 6,
626
- transition: 'transform 0.2s ease, box-shadow 0.2s ease',
627
- }}
628
- loading="lazy"
629
- onError={(e) => {
630
- console.error('Photo failed to load:', e);
631
- }}
632
- onMouseEnter={(e) => {
633
- e.currentTarget.style.transform = 'scale(1.02)';
634
- e.currentTarget.style.boxShadow = '0 8px 25px rgba(0,0,0,0.15)';
635
- e.currentTarget.style.zIndex = '10';
636
- }}
637
- onMouseLeave={(e) => {
638
- e.currentTarget.style.transform = 'scale(1)';
639
- e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
640
- e.currentTarget.style.zIndex = '1';
641
- }}
642
- />
643
- ) : (
644
- <Image
645
- source={{ uri: downloadUrl }}
646
- style={styles.justifiedPhotoImage}
647
- resizeMode="cover"
648
- onError={(e) => {
649
- console.error('Photo failed to load:', e);
650
- }}
651
- />
652
- )}
733
+ <ExpoImage
734
+ source={{ uri: downloadUrl }}
735
+ style={styles.justifiedPhotoImage}
736
+ contentFit="cover"
737
+ transition={120}
738
+ cachePolicy="memory-disk"
739
+ onError={(e: any) => {
740
+ console.error('Photo failed to load:', (e as any)?.nativeEvent ?? e);
741
+ }}
742
+ accessibilityLabel={photo.filename}
743
+ />
653
744
  </View>
654
745
  </TouchableOpacity>
655
746
  );
656
747
  }, [oxyServices]);
657
748
 
749
+ // Run initial load once per targetUserId change to avoid accidental loops
750
+ const lastLoadedFor = useRef<string | undefined>(undefined);
658
751
  useEffect(() => {
659
- loadFiles();
660
- }, [loadFiles]);
752
+ const key = targetUserId || 'anonymous';
753
+ if (lastLoadedFor.current !== key) {
754
+ lastLoadedFor.current = key;
755
+ loadFiles('initial');
756
+ }
757
+ // eslint-disable-next-line react-hooks/exhaustive-deps
758
+ }, [targetUserId]);
661
759
 
662
760
  const renderFileItem = (file: FileMetadata) => {
663
761
  const isImage = file.contentType.startsWith('image/');
@@ -687,37 +785,17 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
687
785
  })}
688
786
  >
689
787
  {isImage && (
690
- Platform.OS === 'web' ? (
691
- <img
692
- src={oxyServices.getFileDownloadUrl(file.id)}
693
- style={{
694
- width: '100%',
695
- height: '100%',
696
- objectFit: 'cover',
697
- borderRadius: 8,
698
- transition: 'transform 0.2s ease',
699
- transform: hoveredPreview === file.id ? 'scale(1.05)' : 'scale(1)',
700
- }}
701
- onError={(e) => {
702
- // Show fallback icon if image fails to load
703
- e.currentTarget.style.display = 'none';
704
- const fallbackElement = e.currentTarget.parentElement?.querySelector('[data-fallback="true"]');
705
- if (fallbackElement) {
706
- (fallbackElement as HTMLElement).style.display = 'flex';
707
- }
708
- }}
709
- />
710
- ) : (
711
- <Image
712
- source={{ uri: oxyServices.getFileDownloadUrl(file.id) }}
713
- style={styles.previewImage}
714
- resizeMode="cover"
715
- onError={() => {
716
- // For React Native, you might want to set an error state
717
- console.warn('Failed to load image preview for file:', file.id);
718
- }}
719
- />
720
- )
788
+ <ExpoImage
789
+ source={{ uri: getSafeDownloadUrl(file, 'thumb') }}
790
+ style={styles.previewImage}
791
+ contentFit="cover"
792
+ transition={120}
793
+ cachePolicy="memory-disk"
794
+ onError={(_: any) => {
795
+ console.warn('Failed to load image preview.');
796
+ }}
797
+ accessibilityLabel={file.filename}
798
+ />
721
799
  )}
722
800
  {isPDF && (
723
801
  <View style={styles.pdfPreview}>
@@ -726,9 +804,21 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
726
804
  </View>
727
805
  )}
728
806
  {isVideo && (
729
- <View style={styles.videoPreview}>
730
- <Ionicons name="play-circle" size={32} color={themeStyles.primaryColor} />
731
- <Text style={[styles.videoLabel, { color: themeStyles.primaryColor }]}>VIDEO</Text>
807
+ <View style={styles.videoPreviewWrapper}>
808
+ <ExpoImage
809
+ source={{ uri: getSafeDownloadUrl(file, 'thumb') }}
810
+ style={styles.videoPosterImage}
811
+ contentFit="cover"
812
+ transition={120}
813
+ cachePolicy="memory-disk"
814
+ onError={(_: any) => {
815
+ // If thumbnail not available, we still show icon overlay
816
+ }}
817
+ accessibilityLabel={file.filename + ' video thumbnail'}
818
+ />
819
+ <View style={styles.videoOverlay}>
820
+ <Ionicons name="play" size={24} color="#FFFFFF" />
821
+ </View>
732
822
  </View>
733
823
  )}
734
824
  {/* Fallback icon (hidden by default for images) */}
@@ -816,8 +906,69 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
816
906
  );
817
907
  };
818
908
 
909
+ // GroupedSection-based file items (for 'all' view) replacing legacy flat list look
910
+ const groupedFileItems = useMemo(() => {
911
+ return filteredFiles
912
+ .filter(f => true) // placeholder for future filtering
913
+ .sort((a, b) => new Date(b.uploadDate).getTime() - new Date(a.uploadDate).getTime())
914
+ .map((file) => {
915
+ const isImage = file.contentType.startsWith('image/');
916
+ const isVideo = file.contentType.startsWith('video/');
917
+ const hasPreview = isImage || isVideo;
918
+ const previewUrl = hasPreview ? (isVideo ? getSafeDownloadUrl(file, 'poster') : getSafeDownloadUrl(file, 'thumb')) : undefined;
919
+ return {
920
+ id: file.id,
921
+ image: previewUrl,
922
+ imageSize: 44,
923
+ icon: !previewUrl ? getFileIcon(file.contentType) : undefined,
924
+ iconColor: themeStyles.primaryColor,
925
+ title: file.filename,
926
+ subtitle: `${formatFileSize(file.length)} • ${new Date(file.uploadDate).toLocaleDateString()}`,
927
+ theme: theme as 'light' | 'dark',
928
+ onPress: () => handleFileOpen(file),
929
+ showChevron: false,
930
+ dense: true,
931
+ multiRow: !!file.metadata?.description,
932
+ customContent: (
933
+ <View style={styles.groupedActions}>
934
+ {(isImage || isVideo || file.contentType.includes('pdf')) && (
935
+ <TouchableOpacity
936
+ style={[styles.groupedActionBtn, { backgroundColor: themeStyles.isDarkTheme ? '#333333' : '#F0F0F0' }]}
937
+ onPress={() => handleFileOpen(file)}
938
+ >
939
+ <Ionicons name="eye" size={18} color={themeStyles.primaryColor} />
940
+ </TouchableOpacity>
941
+ )}
942
+ <TouchableOpacity
943
+ style={[styles.groupedActionBtn, { backgroundColor: themeStyles.isDarkTheme ? '#333333' : '#F0F0F0' }]}
944
+ onPress={() => handleFileDownload(file.id, file.filename)}
945
+ >
946
+ <Ionicons name="download" size={18} color={themeStyles.primaryColor} />
947
+ </TouchableOpacity>
948
+ <TouchableOpacity
949
+ style={[styles.groupedActionBtn, { backgroundColor: themeStyles.isDarkTheme ? '#400000' : '#FFEBEE' }]}
950
+ onPress={() => handleFileDelete(file.id, file.filename)}
951
+ disabled={deleting === file.id}
952
+ >
953
+ {deleting === file.id ? (
954
+ <ActivityIndicator size="small" color={themeStyles.dangerColor} />
955
+ ) : (
956
+ <Ionicons name="trash" size={18} color={themeStyles.dangerColor} />
957
+ )}
958
+ </TouchableOpacity>
959
+ </View>
960
+ ),
961
+ customContentBelow: file.metadata?.description ? (
962
+ <Text style={[styles.groupedDescription, { color: themeStyles.isDarkTheme ? '#AAAAAA' : '#666666' }]} numberOfLines={2}>
963
+ {file.metadata.description}
964
+ </Text>
965
+ ) : undefined,
966
+ } as any; // GroupedSectionItem shape
967
+ });
968
+ }, [filteredFiles, theme, themeStyles, deleting, handleFileDownload, handleFileDelete, handleFileOpen, getSafeDownloadUrl]);
969
+
819
970
  const renderPhotoItem = (photo: FileMetadata, index: number) => {
820
- const downloadUrl = oxyServices.getFileDownloadUrl(photo.id);
971
+ const downloadUrl = getSafeDownloadUrl(photo, 'thumb');
821
972
 
822
973
  // Calculate photo item width based on actual container size from bottom sheet
823
974
  let itemsPerRow = 3; // Default for mobile
@@ -844,39 +995,17 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
844
995
  activeOpacity={0.8}
845
996
  >
846
997
  <View style={styles.photoContainer}>
847
- {Platform.OS === 'web' ? (
848
- <img
849
- src={downloadUrl}
850
- alt={photo.filename}
851
- style={{
852
- width: '100%',
853
- height: '100%',
854
- objectFit: 'cover',
855
- borderRadius: 8,
856
- transition: 'transform 0.2s ease',
857
- }}
858
- loading="lazy"
859
- onError={(e) => {
860
- console.error('Photo failed to load:', e);
861
- // Could replace with placeholder image
862
- }}
863
- onMouseEnter={(e) => {
864
- e.currentTarget.style.transform = 'scale(1.02)';
865
- }}
866
- onMouseLeave={(e) => {
867
- e.currentTarget.style.transform = 'scale(1)';
868
- }}
869
- />
870
- ) : (
871
- <Image
872
- source={{ uri: downloadUrl }}
873
- style={styles.photoImage}
874
- resizeMode="cover"
875
- onError={(e) => {
876
- console.error('Photo failed to load:', e);
877
- }}
878
- />
879
- )}
998
+ <ExpoImage
999
+ source={{ uri: downloadUrl }}
1000
+ style={styles.photoImage}
1001
+ contentFit="cover"
1002
+ transition={120}
1003
+ cachePolicy="memory-disk"
1004
+ onError={(_: any) => {
1005
+ console.warn('Failed to load image preview for photo:', photo.id);
1006
+ }}
1007
+ accessibilityLabel={photo.filename}
1008
+ />
880
1009
  </View>
881
1010
  </TouchableOpacity>
882
1011
  );
@@ -890,12 +1019,11 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
890
1019
  <View style={styles.emptyState}>
891
1020
  <Ionicons name="images-outline" size={64} color={themeStyles.isDarkTheme ? '#666666' : '#CCCCCC'} />
892
1021
  <Text style={[styles.emptyStateTitle, { color: themeStyles.textColor }]}>No Photos Yet</Text>
893
- <Text style={[styles.emptyStateDescription, { color: themeStyles.isDarkTheme ? '#BBBBBB' : '#666666' }]}>
894
- {user?.id === targetUserId
1022
+ <Text style={[styles.emptyStateDescription, { color: themeStyles.isDarkTheme ? '#BBBBBB' : '#666666' }]}> {
1023
+ user?.id === targetUserId
895
1024
  ? `Upload photos to get started. You can select multiple photos at once${Platform.OS === 'web' ? ' or drag & drop them here.' : '.'}`
896
1025
  : "This user hasn't uploaded any photos yet"
897
- }
898
- </Text>
1026
+ } </Text>
899
1027
  {user?.id === targetUserId && (
900
1028
  <TouchableOpacity
901
1029
  style={[styles.emptyStateButton, { backgroundColor: themeStyles.primaryColor }]}
@@ -923,7 +1051,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
923
1051
  refreshControl={
924
1052
  <RefreshControl
925
1053
  refreshing={refreshing}
926
- onRefresh={() => loadFiles(true)}
1054
+ onRefresh={() => loadFiles('refresh')}
927
1055
  tintColor={themeStyles.primaryColor}
928
1056
  />
929
1057
  }
@@ -932,9 +1060,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
932
1060
  {loadingDimensions && (
933
1061
  <View style={styles.dimensionsLoadingIndicator}>
934
1062
  <ActivityIndicator size="small" color={themeStyles.primaryColor} />
935
- <Text style={[styles.dimensionsLoadingText, { color: themeStyles.isDarkTheme ? '#BBBBBB' : '#666666' }]}>
936
- Loading photo layout...
937
- </Text>
1063
+ <Text style={[styles.dimensionsLoadingText, { color: themeStyles.isDarkTheme ? '#BBBBBB' : '#666666' }]}>Loading photo layout...</Text>
938
1064
  </View>
939
1065
  )}
940
1066
 
@@ -983,15 +1109,17 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
983
1109
  photoDimensions: { [key: string]: { width: number, height: number } };
984
1110
  loadPhotoDimensions: (photos: FileMetadata[]) => Promise<void>;
985
1111
  createJustifiedRows: (photos: FileMetadata[], containerWidth: number) => FileMetadata[][];
986
- renderJustifiedPhotoItem: (photo: FileMetadata, width: number, height: number, isLast: boolean) => JSX.Element;
987
- renderSimplePhotoItem: (photo: FileMetadata, index: number) => JSX.Element;
1112
+ renderJustifiedPhotoItem: (photo: FileMetadata, width: number, height: number, isLast: boolean) => React.ReactElement;
1113
+ renderSimplePhotoItem: (photo: FileMetadata, index: number) => React.ReactElement;
988
1114
  textColor: string;
989
1115
  containerWidth: number;
990
1116
  }) => {
991
1117
  // Load dimensions for new photos
992
1118
  React.useEffect(() => {
993
1119
  loadPhotoDimensions(photos);
994
- }, [photos.map(p => p.id).join(','), loadPhotoDimensions]);
1120
+ // Depend only on photo IDs to avoid re-running from dimension state changes
1121
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1122
+ }, [photos.map(p => p.id).join(',')]);
995
1123
 
996
1124
  // Group photos by date
997
1125
  const photosByDate = React.useMemo(() => {
@@ -1377,34 +1505,17 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1377
1505
  </View>
1378
1506
  ) : isImage && fileContent ? (
1379
1507
  <View style={styles.imageContainer}>
1380
- {Platform.OS === 'web' ? (
1381
- <img
1382
- src={fileContent}
1383
- alt={openedFile.filename}
1384
- style={{
1385
- maxWidth: '100%',
1386
- maxHeight: '80vh',
1387
- objectFit: 'contain',
1388
- borderRadius: 8,
1389
- }}
1390
- onError={(e) => {
1391
- console.error('Image failed to load:', e);
1392
- }}
1393
- />
1394
- ) : (
1395
- <Image
1396
- source={{ uri: fileContent }}
1397
- style={{
1398
- width: '100%',
1399
- height: 400,
1400
- resizeMode: 'contain',
1401
- borderRadius: 8,
1402
- }}
1403
- onError={(e) => {
1404
- console.error('Image failed to load:', e);
1405
- }}
1406
- />
1407
- )}
1508
+ <ExpoImage
1509
+ source={{ uri: fileContent }}
1510
+ style={{ width: '100%', height: 400, borderRadius: 8 }}
1511
+ contentFit="contain"
1512
+ transition={120}
1513
+ cachePolicy="memory-disk"
1514
+ onError={(e: any) => {
1515
+ console.error('Image failed to load:', (e as any)?.nativeEvent ?? e);
1516
+ }}
1517
+ accessibilityLabel={openedFile.filename}
1518
+ />
1408
1519
  </View>
1409
1520
  ) : isText && fileContent ? (
1410
1521
  <View style={[styles.textContainer, { backgroundColor: themeStyles.secondaryBackgroundColor, borderColor }]}>
@@ -1543,160 +1654,82 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1543
1654
  <View
1544
1655
  style={[
1545
1656
  styles.container,
1546
- { backgroundColor },
1547
1657
  isDragging && Platform.OS === 'web' && styles.dragOverlay
1548
1658
  ]}
1549
1659
  {...(Platform.OS === 'web' && user?.id === targetUserId ? {
1550
1660
  onDragOver: handleDragOver,
1661
+ onDragEnter: handleDragEnter,
1551
1662
  onDragLeave: handleDragLeave,
1552
1663
  onDrop: handleDrop,
1553
1664
  } : {})}
1554
1665
  >
1555
- {/* Header */}
1556
- <View style={[
1557
- styles.header,
1558
- {
1559
- borderBottomColor: borderColor,
1560
- backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
1561
- shadowColor: '#000000',
1562
- shadowOffset: {
1563
- width: 0,
1564
- height: 2,
1565
- },
1566
- shadowOpacity: themeStyles.isDarkTheme ? 0.3 : 0.1,
1567
- shadowRadius: 8,
1568
- elevation: 4,
1569
- ...Platform.select({
1570
- web: {
1571
- boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
1572
- },
1573
- default: {},
1574
- }),
1575
- }
1576
- ]}>
1577
- <TouchableOpacity
1578
- style={[
1579
- styles.backButton,
1580
- {
1581
- backgroundColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#F8F9FA',
1582
- borderRadius: 12,
1583
- }
1584
- ]}
1585
- onPress={onClose || goBack}
1586
- >
1587
- <Ionicons name="arrow-back" size={22} color={themeStyles.textColor} />
1588
- </TouchableOpacity>
1589
-
1590
- <View style={styles.headerTitleContainer}>
1591
- <Text style={[styles.headerTitle, { color: themeStyles.textColor }]}>
1592
- {viewMode === 'photos' ? 'Photos' : 'File Management'}
1593
- </Text>
1594
- <Text style={[styles.headerSubtitle, { color: themeStyles.isDarkTheme ? '#AAAAAA' : '#666666' }]}>
1595
- {filteredFiles.length} {filteredFiles.length === 1 ? 'item' : 'items'}
1596
- </Text>
1597
- </View>
1598
-
1599
- <View style={styles.headerActions}>
1600
- {/* View Mode Toggle */}
1601
- <View style={[
1602
- styles.viewModeToggle,
1603
- {
1604
- backgroundColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#F8F9FA',
1605
- borderWidth: 1,
1606
- borderColor: themeStyles.isDarkTheme ? '#3A3A3A' : '#E8E9EA',
1607
- shadowColor: '#000000',
1608
- shadowOffset: {
1609
- width: 0,
1610
- height: 1,
1611
- },
1612
- shadowOpacity: themeStyles.isDarkTheme ? 0.3 : 0.05,
1613
- shadowRadius: 4,
1614
- elevation: 2,
1615
- }
1616
- ]}>
1617
- <TouchableOpacity
1618
- style={[
1619
- styles.viewModeButton,
1620
- viewMode === 'all' && {
1621
- backgroundColor: themeStyles.primaryColor,
1622
- shadowColor: themeStyles.primaryColor,
1623
- shadowOffset: {
1624
- width: 0,
1625
- height: 2,
1626
- },
1627
- shadowOpacity: 0.3,
1628
- shadowRadius: 4,
1629
- elevation: 3,
1630
- }
1631
- ]}
1632
- onPress={() => setViewMode('all')}
1633
- >
1634
- <Ionicons
1635
- name="folder"
1636
- size={18}
1637
- color={viewMode === 'all' ? '#FFFFFF' : themeStyles.textColor}
1638
- />
1639
- </TouchableOpacity>
1640
- <TouchableOpacity
1641
- style={[
1642
- styles.viewModeButton,
1643
- viewMode === 'photos' && {
1644
- backgroundColor: themeStyles.primaryColor,
1645
- shadowColor: themeStyles.primaryColor,
1646
- shadowOffset: {
1647
- width: 0,
1648
- height: 2,
1649
- },
1650
- shadowOpacity: 0.3,
1651
- shadowRadius: 4,
1652
- elevation: 3,
1653
- }
1654
- ]}
1655
- onPress={() => setViewMode('photos')}
1656
- >
1657
- <Ionicons
1658
- name="images"
1659
- size={18}
1660
- color={viewMode === 'photos' ? '#FFFFFF' : themeStyles.textColor}
1661
- />
1662
- </TouchableOpacity>
1663
- </View>
1664
-
1665
- {user?.id === targetUserId && (
1666
- <TouchableOpacity
1667
- style={[
1668
- styles.uploadButton,
1669
- {
1670
- backgroundColor: themeStyles.primaryColor,
1671
- shadowColor: themeStyles.primaryColor,
1672
- shadowOffset: {
1673
- width: 0,
1674
- height: 3,
1675
- },
1676
- shadowOpacity: 0.4,
1677
- shadowRadius: 6,
1678
- elevation: 5,
1679
- transform: uploading ? [{ scale: 0.95 }] : [{ scale: 1 }],
1680
- }
1681
- ]}
1682
- onPress={handleFileUpload}
1683
- disabled={uploading}
1684
- >
1685
- {uploading ? (
1686
- <View style={styles.uploadProgress}>
1687
- <ActivityIndicator size="small" color="#FFFFFF" />
1688
- {uploadProgress && (
1689
- <Text style={styles.uploadProgressText}>
1690
- {uploadProgress.current}/{uploadProgress.total}
1691
- </Text>
1692
- )}
1693
- </View>
1694
- ) : (
1695
- <Ionicons name="add" size={26} color="#FFFFFF" />
1696
- )}
1697
- </TouchableOpacity>
1698
- )}
1666
+ <Header
1667
+ title={viewMode === 'photos' ? 'Photos' : 'File Management'}
1668
+ subtitle={`${filteredFiles.length} ${filteredFiles.length === 1 ? 'item' : 'items'}`}
1669
+ onBack={onClose || goBack}
1670
+ theme={theme}
1671
+ showBackButton
1672
+ variant="minimal"
1673
+ elevation="none"
1674
+ titleAlignment="left"
1675
+ />
1676
+
1677
+ <View style={styles.controlsBar}>
1678
+ <View style={[
1679
+ styles.viewModeToggle,
1680
+ {
1681
+ backgroundColor: themeStyles.isDarkTheme ? '#181818' : '#FFFFFF',
1682
+ borderWidth: 1,
1683
+ borderColor: themeStyles.isDarkTheme ? '#2A2A2A' : '#E8E9EA',
1684
+ }
1685
+ ]}>
1686
+ <TouchableOpacity
1687
+ style={[
1688
+ styles.viewModeButton,
1689
+ viewMode === 'all' && { backgroundColor: themeStyles.primaryColor }
1690
+ ]}
1691
+ onPress={() => setViewMode('all')}
1692
+ >
1693
+ <Ionicons
1694
+ name="folder"
1695
+ size={18}
1696
+ color={viewMode === 'all' ? '#FFFFFF' : themeStyles.textColor}
1697
+ />
1698
+ </TouchableOpacity>
1699
+ <TouchableOpacity
1700
+ style={[
1701
+ styles.viewModeButton,
1702
+ viewMode === 'photos' && { backgroundColor: themeStyles.primaryColor }
1703
+ ]}
1704
+ onPress={() => setViewMode('photos')}
1705
+ >
1706
+ <Ionicons
1707
+ name="images"
1708
+ size={18}
1709
+ color={viewMode === 'photos' ? '#FFFFFF' : themeStyles.textColor}
1710
+ />
1711
+ </TouchableOpacity>
1699
1712
  </View>
1713
+ {user?.id === targetUserId && (
1714
+ <TouchableOpacity
1715
+ style={[styles.uploadButton, { backgroundColor: themeStyles.primaryColor }]}
1716
+ onPress={handleFileUpload}
1717
+ disabled={uploading}
1718
+ >
1719
+ {uploading ? (
1720
+ <View style={styles.uploadProgress}>
1721
+ <ActivityIndicator size="small" color="#FFFFFF" />
1722
+ {uploadProgress && (
1723
+ <Text style={styles.uploadProgressText}>
1724
+ {uploadProgress.current}/{uploadProgress.total}
1725
+ </Text>
1726
+ )}
1727
+ </View>
1728
+ ) : (
1729
+ <Ionicons name="add" size={22} color="#FFFFFF" />
1730
+ )}
1731
+ </TouchableOpacity>
1732
+ )}
1700
1733
  </View>
1701
1734
 
1702
1735
  {/* Search Bar */}
@@ -1706,14 +1739,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1706
1739
  {
1707
1740
  backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
1708
1741
  borderColor: themeStyles.isDarkTheme ? '#3A3A3A' : '#E8E9EA',
1709
- shadowColor: '#000000',
1710
- shadowOffset: {
1711
- width: 0,
1712
- height: 1,
1713
- },
1714
- shadowOpacity: themeStyles.isDarkTheme ? 0.2 : 0.05,
1715
- shadowRadius: 4,
1716
- elevation: 2,
1717
1742
  }
1718
1743
  ]}>
1719
1744
  <Ionicons name="search" size={22} color={themeStyles.isDarkTheme ? '#888888' : '#666666'} />
@@ -1742,14 +1767,6 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1742
1767
  {
1743
1768
  backgroundColor: themeStyles.isDarkTheme ? '#1A1A1A' : '#FFFFFF',
1744
1769
  borderColor: themeStyles.isDarkTheme ? '#3A3A3A' : '#E8E9EA',
1745
- shadowColor: '#000000',
1746
- shadowOffset: {
1747
- width: 0,
1748
- height: 1,
1749
- },
1750
- shadowOpacity: themeStyles.isDarkTheme ? 0.2 : 0.05,
1751
- shadowRadius: 4,
1752
- elevation: 2,
1753
1770
  }
1754
1771
  ]}>
1755
1772
  <View style={styles.statItem}>
@@ -1787,7 +1804,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1787
1804
  refreshControl={
1788
1805
  <RefreshControl
1789
1806
  refreshing={refreshing}
1790
- onRefresh={() => loadFiles(true)}
1807
+ onRefresh={() => loadFiles('refresh')}
1791
1808
  tintColor={themeStyles.primaryColor}
1792
1809
  />
1793
1810
  }
@@ -1808,15 +1825,28 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1808
1825
  </TouchableOpacity>
1809
1826
  </View>
1810
1827
  ) : filteredFiles.length === 0 ? renderEmptyState() : (
1811
- <>
1812
- {filteredFiles.map(renderFileItem)}
1813
- </>
1828
+ <GroupedSection items={groupedFileItems} theme={theme as 'light' | 'dark'} />
1814
1829
  )}
1815
1830
  </ScrollView>
1816
1831
  )}
1817
1832
 
1818
1833
  {renderFileDetailsModal()}
1819
1834
 
1835
+ {/* Uploading banner overlay */}
1836
+ {uploading && (
1837
+ <View pointerEvents="none" style={styles.uploadBannerContainer}>
1838
+ <View style={[styles.uploadBanner, { backgroundColor: themeStyles.isDarkTheme ? '#222831EE' : '#FFFFFFEE', borderColor: themeStyles.borderColor }]}>
1839
+ <Ionicons name="cloud-upload" size={18} color={themeStyles.primaryColor} />
1840
+ <Text style={[styles.uploadBannerText, { color: themeStyles.textColor }]}>Uploading{uploadProgress ? ` ${uploadProgress.current}/${uploadProgress.total}` : '...'}</Text>
1841
+ <View style={styles.uploadBannerDots}>
1842
+ {[0, 1, 2].map(i => (
1843
+ <View key={i} style={[styles.dot, { opacity: ((Date.now() / 400 + i) % 3) < 1 ? 1 : 0.25 }]} />
1844
+ ))}
1845
+ </View>
1846
+ </View>
1847
+ </View>
1848
+ )}
1849
+
1820
1850
  {/* Drag and Drop Overlay */}
1821
1851
  {isDragging && Platform.OS === 'web' && (
1822
1852
  <View style={styles.dragDropOverlay}>
@@ -1840,9 +1870,9 @@ const styles = StyleSheet.create({
1840
1870
  flex: 1,
1841
1871
  },
1842
1872
  dragOverlay: {
1843
- backgroundColor: 'rgba(0, 122, 255, 0.1)',
1844
- borderWidth: 2,
1845
- borderColor: '#007AFF',
1873
+ backgroundColor: 'rgba(0, 122, 255, 0.06)',
1874
+ borderWidth: 1,
1875
+ borderColor: '#66AFFF',
1846
1876
  borderStyle: 'dashed',
1847
1877
  },
1848
1878
  centerContent: {
@@ -1853,8 +1883,8 @@ const styles = StyleSheet.create({
1853
1883
  flexDirection: 'row',
1854
1884
  alignItems: 'center',
1855
1885
  justifyContent: 'space-between',
1856
- paddingHorizontal: 24,
1857
- paddingVertical: 20,
1886
+ paddingHorizontal: 16,
1887
+ paddingVertical: 12,
1858
1888
  borderBottomWidth: 1,
1859
1889
  position: 'relative',
1860
1890
  },
@@ -1903,16 +1933,53 @@ const styles = StyleSheet.create({
1903
1933
  fontWeight: '600',
1904
1934
  marginTop: 2,
1905
1935
  },
1936
+ uploadBannerContainer: {
1937
+ position: 'absolute',
1938
+ top: 72, // below header
1939
+ left: 0,
1940
+ right: 0,
1941
+ alignItems: 'center',
1942
+ zIndex: 50,
1943
+ },
1944
+ uploadBanner: {
1945
+ flexDirection: 'row',
1946
+ alignItems: 'center',
1947
+ paddingHorizontal: 14,
1948
+ paddingVertical: 8,
1949
+ borderRadius: 24,
1950
+ gap: 8,
1951
+ borderWidth: 1,
1952
+ shadowColor: '#000',
1953
+ shadowOpacity: 0.1,
1954
+ shadowRadius: 6,
1955
+ shadowOffset: { width: 0, height: 2 },
1956
+ elevation: 2,
1957
+ },
1958
+ uploadBannerText: {
1959
+ fontSize: 13,
1960
+ fontWeight: '500',
1961
+ },
1962
+ uploadBannerDots: {
1963
+ flexDirection: 'row',
1964
+ gap: 4,
1965
+ marginLeft: 2,
1966
+ },
1967
+ dot: {
1968
+ width: 6,
1969
+ height: 6,
1970
+ borderRadius: 3,
1971
+ backgroundColor: '#007AFF',
1972
+ },
1906
1973
  searchContainer: {
1907
1974
  flexDirection: 'row',
1908
1975
  alignItems: 'center',
1909
- paddingHorizontal: 20,
1910
- paddingVertical: 16,
1911
- marginHorizontal: 24,
1912
- marginTop: 20,
1913
- borderRadius: 16,
1976
+ paddingHorizontal: 14,
1977
+ paddingVertical: 10,
1978
+ marginHorizontal: 16,
1979
+ marginTop: 12,
1980
+ borderRadius: 10,
1914
1981
  borderWidth: 1,
1915
- gap: 16,
1982
+ gap: 10,
1916
1983
  },
1917
1984
  searchInput: {
1918
1985
  flex: 1,
@@ -1931,11 +1998,11 @@ const styles = StyleSheet.create({
1931
1998
  },
1932
1999
  statsContainer: {
1933
2000
  flexDirection: 'row',
1934
- paddingHorizontal: 24,
1935
- paddingVertical: 20,
1936
- marginHorizontal: 24,
1937
- marginTop: 20,
1938
- borderRadius: 16,
2001
+ paddingHorizontal: 14,
2002
+ paddingVertical: 10,
2003
+ marginHorizontal: 16,
2004
+ marginTop: 12,
2005
+ borderRadius: 10,
1939
2006
  borderWidth: 1,
1940
2007
  },
1941
2008
  statItem: {
@@ -1944,31 +2011,31 @@ const styles = StyleSheet.create({
1944
2011
  paddingVertical: 4,
1945
2012
  },
1946
2013
  statValue: {
1947
- fontSize: 28,
2014
+ fontSize: 20,
1948
2015
  fontWeight: '800',
1949
2016
  fontFamily: fontFamilies.phuduBold,
1950
2017
  letterSpacing: -0.5,
1951
- lineHeight: 32,
2018
+ lineHeight: 24,
1952
2019
  },
1953
2020
  statLabel: {
1954
- fontSize: 15,
2021
+ fontSize: 12,
1955
2022
  fontWeight: '500',
1956
2023
  fontFamily: fontFamilies.phuduMedium,
1957
- marginTop: 4,
1958
- letterSpacing: 0.3,
2024
+ marginTop: 2,
2025
+ letterSpacing: 0.2,
1959
2026
  },
1960
2027
  scrollView: {
1961
2028
  flex: 1,
1962
2029
  },
1963
2030
  scrollContainer: {
1964
- padding: 20,
2031
+ padding: 12,
1965
2032
  },
1966
2033
  fileItem: {
1967
2034
  flexDirection: 'row',
1968
2035
  alignItems: 'center',
1969
- padding: 16,
1970
- marginBottom: 12,
1971
- borderRadius: 12,
2036
+ padding: 10,
2037
+ marginBottom: 8,
2038
+ borderRadius: 10,
1972
2039
  borderWidth: 1,
1973
2040
  },
1974
2041
  fileContent: {
@@ -1984,15 +2051,15 @@ const styles = StyleSheet.create({
1984
2051
  marginRight: 12,
1985
2052
  },
1986
2053
  filePreviewContainer: {
1987
- width: 60,
1988
- height: 60,
1989
- marginRight: 12,
2054
+ width: 52,
2055
+ height: 52,
2056
+ marginRight: 10,
1990
2057
  },
1991
2058
  filePreview: {
1992
2059
  width: '100%',
1993
2060
  height: '100%',
1994
2061
  borderRadius: 8,
1995
- backgroundColor: '#F5F5F5',
2062
+ backgroundColor: 'transparent',
1996
2063
  alignItems: 'center',
1997
2064
  justifyContent: 'center',
1998
2065
  overflow: 'hidden',
@@ -2035,7 +2102,7 @@ const styles = StyleSheet.create({
2035
2102
  bottom: 0,
2036
2103
  alignItems: 'center',
2037
2104
  justifyContent: 'center',
2038
- backgroundColor: '#F5F5F5',
2105
+ backgroundColor: 'transparent',
2039
2106
  borderRadius: 8,
2040
2107
  },
2041
2108
  previewOverlay: {
@@ -2044,10 +2111,56 @@ const styles = StyleSheet.create({
2044
2111
  left: 0,
2045
2112
  right: 0,
2046
2113
  bottom: 0,
2047
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
2114
+ backgroundColor: 'rgba(0, 0, 0, 0.25)',
2115
+ alignItems: 'center',
2116
+ justifyContent: 'center',
2117
+ borderRadius: 8,
2118
+ },
2119
+ groupedActions: {
2120
+ flexDirection: 'row',
2121
+ alignItems: 'center',
2122
+ gap: 6,
2123
+ marginLeft: 12,
2124
+ },
2125
+ groupedActionBtn: {
2126
+ width: 34,
2127
+ height: 34,
2128
+ borderRadius: 8,
2048
2129
  alignItems: 'center',
2049
2130
  justifyContent: 'center',
2131
+ },
2132
+ groupedDescription: {
2133
+ fontSize: 12,
2134
+ lineHeight: 16,
2135
+ marginTop: 6,
2136
+ },
2137
+ videoPreviewWrapper: {
2138
+ width: '100%',
2139
+ height: '100%',
2050
2140
  borderRadius: 8,
2141
+ overflow: 'hidden',
2142
+ backgroundColor: '#000000',
2143
+ alignItems: 'center',
2144
+ justifyContent: 'center',
2145
+ },
2146
+ videoPosterImage: {
2147
+ position: 'absolute',
2148
+ top: 0,
2149
+ left: 0,
2150
+ right: 0,
2151
+ bottom: 0,
2152
+ width: '100%',
2153
+ height: '100%',
2154
+ },
2155
+ videoOverlay: {
2156
+ position: 'absolute',
2157
+ top: 0,
2158
+ left: 0,
2159
+ right: 0,
2160
+ bottom: 0,
2161
+ alignItems: 'center',
2162
+ justifyContent: 'center',
2163
+ backgroundColor: 'rgba(0,0,0,0.25)',
2051
2164
  },
2052
2165
  fileInfo: {
2053
2166
  flex: 1,
@@ -2070,16 +2183,19 @@ const styles = StyleSheet.create({
2070
2183
  gap: 8,
2071
2184
  },
2072
2185
  actionButton: {
2073
- width: 40,
2074
- height: 40,
2075
- borderRadius: 20,
2186
+ width: 36,
2187
+ height: 36,
2188
+ borderRadius: 18,
2076
2189
  alignItems: 'center',
2077
2190
  justifyContent: 'center',
2191
+ borderWidth: 1,
2192
+ borderColor: '#E0E0E0',
2193
+ backgroundColor: 'transparent',
2078
2194
  },
2079
2195
  emptyState: {
2080
2196
  alignItems: 'center',
2081
- paddingVertical: 60,
2082
- paddingHorizontal: 40,
2197
+ paddingVertical: 40,
2198
+ paddingHorizontal: 24,
2083
2199
  },
2084
2200
  emptyStateTitle: {
2085
2201
  fontSize: 24,
@@ -2120,8 +2236,8 @@ const styles = StyleSheet.create({
2120
2236
  flexDirection: 'row',
2121
2237
  alignItems: 'center',
2122
2238
  justifyContent: 'space-between',
2123
- paddingHorizontal: 20,
2124
- paddingVertical: 16,
2239
+ paddingHorizontal: 16,
2240
+ paddingVertical: 12,
2125
2241
  borderBottomWidth: 1,
2126
2242
  },
2127
2243
  modalCloseButton: {
@@ -2137,11 +2253,11 @@ const styles = StyleSheet.create({
2137
2253
  },
2138
2254
  modalContent: {
2139
2255
  flex: 1,
2140
- padding: 20,
2256
+ padding: 16,
2141
2257
  },
2142
2258
  fileDetailCard: {
2143
- padding: 24,
2144
- borderRadius: 16,
2259
+ padding: 18,
2260
+ borderRadius: 14,
2145
2261
  borderWidth: 1,
2146
2262
  alignItems: 'center',
2147
2263
  },
@@ -2204,25 +2320,25 @@ const styles = StyleSheet.create({
2204
2320
  left: 0,
2205
2321
  right: 0,
2206
2322
  bottom: 0,
2207
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
2323
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
2208
2324
  justifyContent: 'center',
2209
2325
  alignItems: 'center',
2210
2326
  zIndex: 1000,
2211
2327
  },
2212
2328
  dragDropContent: {
2213
2329
  alignItems: 'center',
2214
- backgroundColor: 'rgba(255, 255, 255, 0.9)',
2215
- padding: 40,
2216
- borderRadius: 20,
2217
- borderWidth: 3,
2218
- borderColor: '#007AFF',
2330
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
2331
+ padding: 20,
2332
+ borderRadius: 14,
2333
+ borderWidth: 1,
2334
+ borderColor: '#66AFFF',
2219
2335
  borderStyle: 'dashed',
2220
2336
  },
2221
2337
  dragDropTitle: {
2222
- fontSize: 24,
2338
+ fontSize: 20,
2223
2339
  fontWeight: 'bold',
2224
- marginTop: 16,
2225
- marginBottom: 8,
2340
+ marginTop: 12,
2341
+ marginBottom: 6,
2226
2342
  },
2227
2343
  dragDropSubtitle: {
2228
2344
  fontSize: 16,
@@ -2236,8 +2352,8 @@ const styles = StyleSheet.create({
2236
2352
  fileViewerHeader: {
2237
2353
  flexDirection: 'row',
2238
2354
  alignItems: 'center',
2239
- paddingHorizontal: 20,
2240
- paddingVertical: 16,
2355
+ paddingHorizontal: 16,
2356
+ paddingVertical: 12,
2241
2357
  borderBottomWidth: 1,
2242
2358
  },
2243
2359
  fileViewerTitleContainer: {
@@ -2265,7 +2381,7 @@ const styles = StyleSheet.create({
2265
2381
  },
2266
2382
  fileViewerContentContainer: {
2267
2383
  flexGrow: 1,
2268
- padding: 20,
2384
+ padding: 14,
2269
2385
  },
2270
2386
  fileViewerLoading: {
2271
2387
  flex: 1,
@@ -2283,10 +2399,10 @@ const styles = StyleSheet.create({
2283
2399
  },
2284
2400
  textContainer: {
2285
2401
  flex: 1,
2286
- borderRadius: 12,
2402
+ borderRadius: 10,
2287
2403
  borderWidth: 1,
2288
- padding: 16,
2289
- minHeight: 200,
2404
+ padding: 12,
2405
+ minHeight: 180,
2290
2406
  maxHeight: '80%',
2291
2407
  },
2292
2408
  textContent: {
@@ -2298,8 +2414,8 @@ const styles = StyleSheet.create({
2298
2414
  flex: 1,
2299
2415
  justifyContent: 'center',
2300
2416
  alignItems: 'center',
2301
- paddingVertical: 60,
2302
- paddingHorizontal: 40,
2417
+ paddingVertical: 40,
2418
+ paddingHorizontal: 24,
2303
2419
  },
2304
2420
  unsupportedFileTitle: {
2305
2421
  fontSize: 24,
@@ -2318,9 +2434,9 @@ const styles = StyleSheet.create({
2318
2434
  downloadButtonLarge: {
2319
2435
  flexDirection: 'row',
2320
2436
  alignItems: 'center',
2321
- paddingHorizontal: 24,
2322
- paddingVertical: 16,
2323
- borderRadius: 24,
2437
+ paddingHorizontal: 18,
2438
+ paddingVertical: 12,
2439
+ borderRadius: 20,
2324
2440
  gap: 8,
2325
2441
  },
2326
2442
  downloadButtonText: {
@@ -2347,10 +2463,10 @@ const styles = StyleSheet.create({
2347
2463
 
2348
2464
  // File Details in Viewer styles
2349
2465
  fileDetailsSection: {
2350
- margin: 16,
2466
+ margin: 12,
2351
2467
  marginTop: 0,
2352
- padding: 20,
2353
- borderRadius: 12,
2468
+ padding: 14,
2469
+ borderRadius: 10,
2354
2470
  borderWidth: 1,
2355
2471
  },
2356
2472
  fileDetailsSectionTitle: {
@@ -2394,6 +2510,15 @@ const styles = StyleSheet.create({
2394
2510
  alignItems: 'center',
2395
2511
  gap: 16,
2396
2512
  },
2513
+ controlsBar: {
2514
+ flexDirection: 'row',
2515
+ alignItems: 'center',
2516
+ justifyContent: 'space-between',
2517
+ paddingHorizontal: 16,
2518
+ paddingTop: 8,
2519
+ paddingBottom: 4,
2520
+ gap: 12,
2521
+ },
2397
2522
  viewModeToggle: {
2398
2523
  flexDirection: 'row',
2399
2524
  borderRadius: 24,
@@ -2412,17 +2537,17 @@ const styles = StyleSheet.create({
2412
2537
 
2413
2538
  // Photo Grid styles
2414
2539
  photoScrollContainer: {
2415
- padding: 16,
2540
+ padding: 10,
2416
2541
  },
2417
2542
  photoDateSection: {
2418
- marginBottom: 24,
2543
+ marginBottom: 16,
2419
2544
  },
2420
2545
  photoDateHeader: {
2421
- fontSize: 18,
2546
+ fontSize: 16,
2422
2547
  fontWeight: '600',
2423
2548
  fontFamily: fontFamilies.phuduSemiBold,
2424
- marginBottom: 12,
2425
- paddingHorizontal: 4,
2549
+ marginBottom: 8,
2550
+ paddingHorizontal: 2,
2426
2551
  },
2427
2552
  photoGrid: {
2428
2553
  flexDirection: 'row',
@@ -2475,7 +2600,7 @@ const styles = StyleSheet.create({
2475
2600
  position: 'relative',
2476
2601
  borderRadius: 6,
2477
2602
  overflow: 'hidden',
2478
- backgroundColor: '#F5F5F5',
2603
+ backgroundColor: 'transparent',
2479
2604
  },
2480
2605
  justifiedPhotoImage: {
2481
2606
  width: '100%',
@@ -2487,7 +2612,7 @@ const styles = StyleSheet.create({
2487
2612
  simplePhotoItem: {
2488
2613
  borderRadius: 8,
2489
2614
  overflow: 'hidden',
2490
- backgroundColor: '#F5F5F5',
2615
+ backgroundColor: 'transparent',
2491
2616
  },
2492
2617
  simplePhotoContainer: {
2493
2618
  width: '100%',