user.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. import { create } from 'zustand';
  2. import { Platform } from 'react-native';
  3. import { isObjectsEqual } from 'app/lib/util'
  4. import { useShallow } from 'zustand/react/shallow';
  5. /**
  6. * Signed-in profile as UNA returns it in page data (`data.user`). Only the fields the
  7. * app reads are listed; UNA may send more (hence the index signature).
  8. */
  9. export type CurrentUser = {
  10. id: number | string;
  11. display_name?: string;
  12. url?: string;
  13. avatar?: string;
  14. confirmed?: boolean | number;
  15. hash?: string;
  16. /** Unread notifications; kept in sync with `counters.bx_notifications`. */
  17. notifications?: number;
  18. notificationsTs?: number | string;
  19. counters?: { bx_notifications?: number; [module: string]: number | undefined };
  20. /** Account settings (module-specific; mutated in place by some screens). */
  21. settings?: Record<string, any>;
  22. /** Account menu from UNA. */
  23. menu?: { items: any[] };
  24. membership?: number | string;
  25. membership_name?: string;
  26. membership_icon?: string;
  27. membership_icon_url?: string;
  28. /** Active context (e.g. organization) id when acting on behalf of another profile. */
  29. current_context?: number | string;
  30. informer?: any[];
  31. operator?: boolean | number;
  32. moderator?: boolean | number;
  33. /** HTTP-like status of the user's page (503 = maintenance). */
  34. page_status?: number;
  35. badges?: any;
  36. profiles_count?: number;
  37. profiles_limit?: number;
  38. [key: string]: any;
  39. };
  40. /** `null`: not known yet (before seeding); `false`: guest; object: signed in. */
  41. export type CurrentUserState = CurrentUser | false | null;
  42. type CurrentUserStore = {
  43. currentUser: CurrentUserState;
  44. /** Merge a partial user into the current one (skipped when deep-equal); falsy sets guest/unknown. */
  45. setCurrentUser: (userUpdate: Partial<CurrentUser> | false | null) => void;
  46. };
  47. /**
  48. * Web: page JSON from the server is enough to render guest UI before Zustand is seeded.
  49. * Logged-in pages still wait for currentUser (seeded on the client from data.user).
  50. */
  51. export function isWebAuthReady(pageData: { user?: unknown } | null | undefined, currentUser: CurrentUserState): boolean {
  52. if (currentUser !== null) {
  53. return true;
  54. }
  55. if (Platform.OS !== 'web' || pageData == null) {
  56. return false;
  57. }
  58. return !pageData.user;
  59. }
  60. /**
  61. * Web-only: seed the global store from page props (call from useLayoutEffect in Root).
  62. * No-op on native (tabs bootstrap keeps currentUser === null).
  63. */
  64. export function seedCurrentUserFromPageData(pageData: { user?: CurrentUser | null } | null | undefined): void {
  65. if (Platform.OS !== 'web' || pageData == null || typeof window === 'undefined') {
  66. return;
  67. }
  68. if (useCurrentUserStore.getState().currentUser !== null) {
  69. return;
  70. }
  71. useCurrentUserStore.getState().setCurrentUser(pageData.user ? pageData.user : false);
  72. }
  73. export const useCurrentUserStore = create<CurrentUserStore>()((set, get) => ({
  74. currentUser: null, // Initial state
  75. // Setter function with deep comparison check using lodash's isEqual
  76. setCurrentUser: (userUpdate) => {
  77. const currentUser = get().currentUser;
  78. if (!userUpdate) {
  79. set({ currentUser: userUpdate as false | null });
  80. return;
  81. }
  82. const hasCounters = userUpdate.counters != null;
  83. const nextNotifications = hasCounters
  84. ? (userUpdate.counters!.bx_notifications ?? 0)
  85. : (userUpdate.notifications ?? (currentUser || undefined)?.notifications ?? 0);
  86. const updatedUser = {
  87. ...currentUser, // Copy the current user object
  88. ...userUpdate, // Merge the updates (e.g., notifications)
  89. notifications: nextNotifications,
  90. };
  91. // Only update if the objects are not deeply equal
  92. if (!isObjectsEqual(currentUser, updatedUser)) {
  93. set(() => ({ currentUser: updatedUser as CurrentUser }));
  94. }
  95. },
  96. }));
  97. export const useCurrentUser = () => {
  98. const currentUser = useCurrentUserStore((state) => state.currentUser);
  99. const setCurrentUser = useCurrentUserStore((state) => state.setCurrentUser);
  100. return { currentUser, setCurrentUser };
  101. };
  102. export const useCurrentUserNoCounters = () => {
  103. return useCurrentUserStore(
  104. useShallow((state) => {
  105. const u = state.currentUser;
  106. if (!u) return u;
  107. const { counters, notifications, ...brief } = u;
  108. return brief;
  109. })
  110. );
  111. };