@oxyhq/services 5.13.11 → 5.13.15

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 (96) hide show
  1. package/lib/commonjs/core/HttpClient.js +79 -0
  2. package/lib/commonjs/core/HttpClient.js.map +1 -1
  3. package/lib/commonjs/core/OxyServices.js +159 -0
  4. package/lib/commonjs/core/OxyServices.js.map +1 -1
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/ui/context/OxyContext.js +1 -47
  7. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  8. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +239 -1
  9. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
  10. package/lib/commonjs/utils/apiUtils.js +0 -14
  11. package/lib/commonjs/utils/apiUtils.js.map +1 -1
  12. package/lib/commonjs/utils/asyncUtils.js +0 -20
  13. package/lib/commonjs/utils/asyncUtils.js.map +1 -1
  14. package/lib/module/core/HttpClient.js +79 -0
  15. package/lib/module/core/HttpClient.js.map +1 -1
  16. package/lib/module/core/OxyServices.js +159 -0
  17. package/lib/module/core/OxyServices.js.map +1 -1
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/ui/context/OxyContext.js +1 -47
  20. package/lib/module/ui/context/OxyContext.js.map +1 -1
  21. package/lib/module/ui/navigation/types.js.map +1 -1
  22. package/lib/module/ui/screens/PrivacySettingsScreen.js +241 -3
  23. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
  24. package/lib/module/utils/apiUtils.js +0 -13
  25. package/lib/module/utils/apiUtils.js.map +1 -1
  26. package/lib/module/utils/asyncUtils.js +0 -20
  27. package/lib/module/utils/asyncUtils.js.map +1 -1
  28. package/lib/typescript/core/HttpClient.d.ts +46 -0
  29. package/lib/typescript/core/HttpClient.d.ts.map +1 -1
  30. package/lib/typescript/core/OxyServices.d.ts +65 -1
  31. package/lib/typescript/core/OxyServices.d.ts.map +1 -1
  32. package/lib/typescript/index.d.ts +1 -1
  33. package/lib/typescript/index.d.ts.map +1 -1
  34. package/lib/typescript/models/interfaces.d.ts +26 -0
  35. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  36. package/lib/typescript/ui/context/OxyContext.d.ts +0 -6
  37. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  38. package/lib/typescript/ui/navigation/types.d.ts +0 -1
  39. package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
  40. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  41. package/lib/typescript/utils/apiUtils.d.ts +0 -7
  42. package/lib/typescript/utils/apiUtils.d.ts.map +1 -1
  43. package/lib/typescript/utils/asyncUtils.d.ts +0 -11
  44. package/lib/typescript/utils/asyncUtils.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/core/HttpClient.ts +69 -0
  47. package/src/core/OxyServices.ts +174 -1
  48. package/src/index.ts +4 -0
  49. package/src/models/interfaces.ts +28 -0
  50. package/src/ui/context/OxyContext.tsx +0 -54
  51. package/src/ui/navigation/types.ts +0 -1
  52. package/src/ui/screens/PrivacySettingsScreen.tsx +240 -2
  53. package/src/utils/apiUtils.ts +0 -14
  54. package/src/utils/asyncUtils.ts +0 -20
  55. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js +0 -192
  56. package/lib/commonjs/ui/screens/internal/SignInPasswordStep.js.map +0 -1
  57. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js +0 -142
  58. package/lib/commonjs/ui/screens/internal/SignInUsernameStep.js.map +0 -1
  59. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js +0 -113
  60. package/lib/commonjs/ui/screens/internal/SignUpIdentityStep.js.map +0 -1
  61. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js +0 -132
  62. package/lib/commonjs/ui/screens/internal/SignUpSecurityStep.js.map +0 -1
  63. package/lib/commonjs/ui/screens/internal/SignUpSummaryStep.js +0 -83
  64. package/lib/commonjs/ui/screens/internal/SignUpSummaryStep.js.map +0 -1
  65. package/lib/commonjs/ui/screens/internal/SignUpWelcomeStep.js +0 -58
  66. package/lib/commonjs/ui/screens/internal/SignUpWelcomeStep.js.map +0 -1
  67. package/lib/module/ui/screens/internal/SignInPasswordStep.js +0 -186
  68. package/lib/module/ui/screens/internal/SignInPasswordStep.js.map +0 -1
  69. package/lib/module/ui/screens/internal/SignInUsernameStep.js +0 -136
  70. package/lib/module/ui/screens/internal/SignInUsernameStep.js.map +0 -1
  71. package/lib/module/ui/screens/internal/SignUpIdentityStep.js +0 -108
  72. package/lib/module/ui/screens/internal/SignUpIdentityStep.js.map +0 -1
  73. package/lib/module/ui/screens/internal/SignUpSecurityStep.js +0 -127
  74. package/lib/module/ui/screens/internal/SignUpSecurityStep.js.map +0 -1
  75. package/lib/module/ui/screens/internal/SignUpSummaryStep.js +0 -78
  76. package/lib/module/ui/screens/internal/SignUpSummaryStep.js.map +0 -1
  77. package/lib/module/ui/screens/internal/SignUpWelcomeStep.js +0 -53
  78. package/lib/module/ui/screens/internal/SignUpWelcomeStep.js.map +0 -1
  79. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts +0 -28
  80. package/lib/typescript/ui/screens/internal/SignInPasswordStep.d.ts.map +0 -1
  81. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts +0 -25
  82. package/lib/typescript/ui/screens/internal/SignInUsernameStep.d.ts.map +0 -1
  83. package/lib/typescript/ui/screens/internal/SignUpIdentityStep.d.ts +0 -20
  84. package/lib/typescript/ui/screens/internal/SignUpIdentityStep.d.ts.map +0 -1
  85. package/lib/typescript/ui/screens/internal/SignUpSecurityStep.d.ts +0 -24
  86. package/lib/typescript/ui/screens/internal/SignUpSecurityStep.d.ts.map +0 -1
  87. package/lib/typescript/ui/screens/internal/SignUpSummaryStep.d.ts +0 -15
  88. package/lib/typescript/ui/screens/internal/SignUpSummaryStep.d.ts.map +0 -1
  89. package/lib/typescript/ui/screens/internal/SignUpWelcomeStep.d.ts +0 -13
  90. package/lib/typescript/ui/screens/internal/SignUpWelcomeStep.d.ts.map +0 -1
  91. package/src/ui/screens/internal/SignInPasswordStep.tsx +0 -184
  92. package/src/ui/screens/internal/SignInUsernameStep.tsx +0 -145
  93. package/src/ui/screens/internal/SignUpIdentityStep.tsx +0 -112
  94. package/src/ui/screens/internal/SignUpSecurityStep.tsx +0 -132
  95. package/src/ui/screens/internal/SignUpSummaryStep.tsx +0 -66
  96. package/src/ui/screens/internal/SignUpWelcomeStep.tsx +0 -52
@@ -64,7 +64,9 @@ import type {
64
64
  Notification,
65
65
  AssetInitResponse,
66
66
  AssetUrlResponse,
67
- AssetVariant
67
+ AssetVariant,
68
+ BlockedUser,
69
+ RestrictedUser
68
70
  } from '../models/interfaces';
69
71
  import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
70
72
  import type { LanguageMetadata } from '../utils/languageUtils';
@@ -838,6 +840,177 @@ export class OxyServices {
838
840
  }
839
841
  }
840
842
 
843
+ // ============================================================================
844
+ // BLOCKED USERS METHODS
845
+ // ============================================================================
846
+
847
+ /**
848
+ * Get list of blocked users
849
+ * @returns Array of blocked users
850
+ */
851
+ async getBlockedUsers(): Promise<BlockedUser[]> {
852
+ try {
853
+ return await this.makeRequest<BlockedUser[]>('GET', '/api/privacy/blocked', undefined, {
854
+ cache: true,
855
+ cacheTTL: 1 * 60 * 1000, // 1 minute cache
856
+ });
857
+ } catch (error) {
858
+ throw this.handleError(error);
859
+ }
860
+ }
861
+
862
+ /**
863
+ * Block a user
864
+ * @param userId - The user ID to block
865
+ * @returns Success message
866
+ */
867
+ async blockUser(userId: string): Promise<{ message: string }> {
868
+ try {
869
+ if (!userId) {
870
+ throw new Error('User ID is required');
871
+ }
872
+ return await this.makeRequest<{ message: string }>('POST', `/api/privacy/blocked/${userId}`, undefined, {
873
+ cache: false,
874
+ });
875
+ } catch (error) {
876
+ throw this.handleError(error);
877
+ }
878
+ }
879
+
880
+ /**
881
+ * Unblock a user
882
+ * @param userId - The user ID to unblock
883
+ * @returns Success message
884
+ */
885
+ async unblockUser(userId: string): Promise<{ message: string }> {
886
+ try {
887
+ if (!userId) {
888
+ throw new Error('User ID is required');
889
+ }
890
+ return await this.makeRequest<{ message: string }>('DELETE', `/api/privacy/blocked/${userId}`, undefined, {
891
+ cache: false,
892
+ });
893
+ } catch (error) {
894
+ throw this.handleError(error);
895
+ }
896
+ }
897
+
898
+ /**
899
+ * Extract user ID from blocked/restricted user object
900
+ * @private
901
+ */
902
+ private extractUserId(userIdField: string | { _id: string; username?: string; avatar?: string }): string {
903
+ return typeof userIdField === 'string' ? userIdField : userIdField._id;
904
+ }
905
+
906
+ /**
907
+ * Check if a user is in a list (blocked or restricted)
908
+ * @private
909
+ */
910
+ private async isUserInList<T extends BlockedUser | RestrictedUser>(
911
+ userId: string,
912
+ getUserList: () => Promise<T[]>,
913
+ getIdField: (item: T) => string | { _id: string; username?: string; avatar?: string }
914
+ ): Promise<boolean> {
915
+ try {
916
+ if (!userId) {
917
+ return false;
918
+ }
919
+ const users = await getUserList();
920
+ return users.some(item => {
921
+ const itemId = this.extractUserId(getIdField(item));
922
+ return itemId === userId;
923
+ });
924
+ } catch (error) {
925
+ // If there's an error, assume not in list to avoid breaking functionality
926
+ if (__DEV__) {
927
+ console.warn('Error checking user list:', error);
928
+ }
929
+ return false;
930
+ }
931
+ }
932
+
933
+ /**
934
+ * Check if a user is blocked
935
+ * @param userId - The user ID to check
936
+ * @returns True if the user is blocked, false otherwise
937
+ */
938
+ async isUserBlocked(userId: string): Promise<boolean> {
939
+ return this.isUserInList(
940
+ userId,
941
+ () => this.getBlockedUsers(),
942
+ (block) => block.blockedId
943
+ );
944
+ }
945
+
946
+ // ============================================================================
947
+ // RESTRICTED USERS METHODS
948
+ // ============================================================================
949
+
950
+ /**
951
+ * Get list of restricted users
952
+ * @returns Array of restricted users
953
+ */
954
+ async getRestrictedUsers(): Promise<RestrictedUser[]> {
955
+ try {
956
+ return await this.makeRequest<RestrictedUser[]>('GET', '/api/privacy/restricted', undefined, {
957
+ cache: true,
958
+ cacheTTL: 1 * 60 * 1000, // 1 minute cache
959
+ });
960
+ } catch (error) {
961
+ throw this.handleError(error);
962
+ }
963
+ }
964
+
965
+ /**
966
+ * Restrict a user (limit their interactions without fully blocking)
967
+ * @param userId - The user ID to restrict
968
+ * @returns Success message
969
+ */
970
+ async restrictUser(userId: string): Promise<{ message: string }> {
971
+ try {
972
+ if (!userId) {
973
+ throw new Error('User ID is required');
974
+ }
975
+ return await this.makeRequest<{ message: string }>('POST', `/api/privacy/restricted/${userId}`, undefined, {
976
+ cache: false,
977
+ });
978
+ } catch (error) {
979
+ throw this.handleError(error);
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Unrestrict a user
985
+ * @param userId - The user ID to unrestrict
986
+ * @returns Success message
987
+ */
988
+ async unrestrictUser(userId: string): Promise<{ message: string }> {
989
+ try {
990
+ if (!userId) {
991
+ throw new Error('User ID is required');
992
+ }
993
+ return await this.makeRequest<{ message: string }>('DELETE', `/api/privacy/restricted/${userId}`, undefined, {
994
+ cache: false,
995
+ });
996
+ } catch (error) {
997
+ throw this.handleError(error);
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Check if a user is restricted
1003
+ * @param userId - The user ID to check
1004
+ * @returns True if the user is restricted, false otherwise
1005
+ */
1006
+ async isUserRestricted(userId: string): Promise<boolean> {
1007
+ return this.isUserInList(
1008
+ userId,
1009
+ () => this.getRestrictedUsers(),
1010
+ (restrict) => restrict.restrictedId
1011
+ );
1012
+ }
1013
+
841
1014
  /**
842
1015
  * Request account verification
843
1016
  */
package/src/index.ts CHANGED
@@ -65,6 +65,10 @@ export type {
65
65
  DeviceSessionsResponse,
66
66
  DeviceSessionLogoutResponse,
67
67
  UpdateDeviceNameResponse,
68
+ // Blocked users
69
+ BlockedUser,
70
+ // Restricted users
71
+ RestrictedUser,
68
72
  // Central Asset Service types
69
73
  FileVisibility,
70
74
  AssetLink,
@@ -90,6 +90,34 @@ export interface Transaction {
90
90
  // Add other transaction fields as needed
91
91
  }
92
92
 
93
+ export interface BlockedUser {
94
+ _id?: string;
95
+ blockedId: string | {
96
+ _id: string;
97
+ username: string;
98
+ avatar?: string;
99
+ };
100
+ userId: string;
101
+ createdAt?: string;
102
+ blockedAt?: string;
103
+ username?: string;
104
+ avatar?: string;
105
+ }
106
+
107
+ export interface RestrictedUser {
108
+ _id?: string;
109
+ restrictedId: string | {
110
+ _id: string;
111
+ username: string;
112
+ avatar?: string;
113
+ };
114
+ userId: string;
115
+ createdAt?: string;
116
+ restrictedAt?: string;
117
+ username?: string;
118
+ avatar?: string;
119
+ }
120
+
93
121
  export interface TransferFundsRequest {
94
122
  fromUserId: string;
95
123
  toUserId: string;
@@ -1,6 +1,5 @@
1
1
  import type React from 'react';
2
2
  import { createContext, useContext, useEffect, useCallback, useMemo, useRef, useState, type ReactNode } from 'react';
3
- import type { UseFollowHook } from '../hooks/useFollow.types';
4
3
  import { OxyServices } from '../../core';
5
4
  import type { User, ApiError } from '../../models/interfaces';
6
5
  import type { SessionLoginResponse, ClientSession, MinimalUserData } from '../../models/session';
@@ -15,9 +14,6 @@ import { getLanguageMetadata, getLanguageName, getNativeLanguageName, normalizeL
15
14
  import type { LanguageMetadata } from '../../utils/languageUtils';
16
15
 
17
16
  // Define the context shape
18
- // NOTE: We intentionally avoid importing useFollow here to prevent a require cycle.
19
- // If consumers relied on `const { useFollow } = useOxy()`, we provide a lazy proxy below.
20
-
21
17
  export interface OxyContextState {
22
18
  // Authentication state
23
19
  user: User | null; // Current active user (loaded from server)
@@ -62,34 +58,8 @@ export interface OxyContextState {
62
58
  // Methods to directly control the bottom sheet
63
59
  showBottomSheet?: (screenOrConfig?: RouteName | string | { screen: RouteName | string; props?: Record<string, any> }) => void;
64
60
  hideBottomSheet?: () => void;
65
-
66
- /**
67
- * (Deprecated) useFollow hook access via context. Prefer: import { useFollow } from '@oxyhq/services';
68
- * Kept for backward compatibility; implemented as a lazy dynamic require to avoid circular dependency.
69
- */
70
- useFollow: UseFollowHook; // Back-compat; prefer direct import
71
61
  }
72
62
 
73
- // Empty follow hook fallback
74
- const createEmptyFollowHook = (): UseFollowHook => {
75
- const emptyResult = {
76
- isFollowing: false,
77
- isLoading: false,
78
- error: null,
79
- toggleFollow: async () => { },
80
- setFollowStatus: () => { },
81
- fetchStatus: async () => { },
82
- clearError: () => { },
83
- followerCount: null,
84
- followingCount: null,
85
- isLoadingCounts: false,
86
- fetchUserCounts: async () => { },
87
- setFollowerCount: () => { },
88
- setFollowingCount: () => { },
89
- };
90
- return () => emptyResult;
91
- };
92
-
93
63
  // Create the context with default values
94
64
  const OxyContext = createContext<OxyContextState | null>(null);
95
65
 
@@ -903,29 +873,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
903
873
  }, [logout]),
904
874
  });
905
875
 
906
- // Context value - optimized to prevent unnecessary re-renders
907
- // Lazy proxy to load the hook only when accessed, breaking the static import cycle.
908
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
909
- const useFollowProxy: UseFollowHook = (userId?: string | string[]) => {
910
- try {
911
- // Dynamically require to avoid top-level cycle
912
- // eslint-disable-next-line @typescript-eslint/no-var-requires
913
- const mod = require('../hooks/useFollow');
914
- if (mod && typeof mod.useFollow === 'function') {
915
- return mod.useFollow(userId);
916
- }
917
- if (__DEV__) {
918
- console.warn('useFollow module did not export a function as expected');
919
- }
920
- return createEmptyFollowHook()(userId);
921
- } catch (e) {
922
- if (__DEV__) {
923
- console.warn('Failed to dynamically load useFollow hook:', e);
924
- }
925
- return createEmptyFollowHook()(userId);
926
- }
927
- };
928
-
929
876
  // Compute language metadata from currentLanguage
930
877
  const languageMetadata = useMemo(() => getLanguageMetadata(currentLanguage), [currentLanguage]);
931
878
  const languageName = useMemo(() => getLanguageName(currentLanguage), [currentLanguage]);
@@ -960,7 +907,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
960
907
  bottomSheetRef,
961
908
  showBottomSheet,
962
909
  hideBottomSheet,
963
- useFollow: useFollowProxy,
964
910
  }), [
965
911
  user?.id, // Only depend on user ID, not the entire user object
966
912
  minimalUser?.id,
@@ -80,7 +80,6 @@ export interface OxyProviderProps {
80
80
  /**
81
81
  * @internal
82
82
  * Reference to the bottom sheet component (for internal use only)
83
- * @deprecated External bottom sheet ref is no longer required as OxyProvider handles the bottom sheet internally
84
83
  * @hidden
85
84
  */
86
85
  bottomSheetRef?: React.RefObject<BottomSheetController | null>;
@@ -6,12 +6,14 @@ import {
6
6
  ScrollView,
7
7
  Switch,
8
8
  ActivityIndicator,
9
+ TouchableOpacity,
9
10
  } from 'react-native';
10
11
  import type { BaseScreenProps } from '../navigation/types';
11
12
  import { useOxy } from '../context/OxyContext';
12
13
  import { toast } from '../../lib/sonner';
13
- import { Header, Section } from '../components';
14
+ import { Header, Section, Avatar } from '../components';
14
15
  import { useI18n } from '../hooks/useI18n';
16
+ import type { BlockedUser, RestrictedUser } from '../../models/interfaces';
15
17
 
16
18
  interface PrivacySettings {
17
19
  isPrivateAccount: boolean;
@@ -67,8 +69,11 @@ const PrivacySettingsScreen: React.FC<BaseScreenProps> = ({
67
69
  });
68
70
  const [isLoading, setIsLoading] = useState(true);
69
71
  const [isSaving, setIsSaving] = useState(false);
72
+ const [blockedUsers, setBlockedUsers] = useState<BlockedUser[]>([]);
73
+ const [restrictedUsers, setRestrictedUsers] = useState<RestrictedUser[]>([]);
74
+ const [isLoadingUsers, setIsLoadingUsers] = useState(false);
70
75
 
71
- // Load settings
76
+ // Load settings and users
72
77
  useEffect(() => {
73
78
  const loadSettings = async () => {
74
79
  try {
@@ -90,6 +95,28 @@ const PrivacySettingsScreen: React.FC<BaseScreenProps> = ({
90
95
  loadSettings();
91
96
  }, [user?.id, oxyServices, t]);
92
97
 
98
+ // Load blocked and restricted users
99
+ useEffect(() => {
100
+ const loadUsers = async () => {
101
+ if (!oxyServices) return;
102
+ try {
103
+ setIsLoadingUsers(true);
104
+ const [blocked, restricted] = await Promise.all([
105
+ oxyServices.getBlockedUsers(),
106
+ oxyServices.getRestrictedUsers(),
107
+ ]);
108
+ setBlockedUsers(blocked);
109
+ setRestrictedUsers(restricted);
110
+ } catch (error) {
111
+ console.error('Failed to load blocked/restricted users:', error);
112
+ } finally {
113
+ setIsLoadingUsers(false);
114
+ }
115
+ };
116
+
117
+ loadUsers();
118
+ }, [oxyServices]);
119
+
93
120
  const updateSetting = useCallback(async (key: keyof PrivacySettings, value: boolean) => {
94
121
  try {
95
122
  setIsSaving(true);
@@ -110,6 +137,113 @@ const PrivacySettingsScreen: React.FC<BaseScreenProps> = ({
110
137
  }
111
138
  }, [settings, user?.id, oxyServices, t]);
112
139
 
140
+ const handleUnblock = useCallback(async (userId: string) => {
141
+ if (!oxyServices) return;
142
+ try {
143
+ await oxyServices.unblockUser(userId);
144
+ setBlockedUsers(prev => prev.filter(u => {
145
+ const id = typeof u.blockedId === 'string' ? u.blockedId : u.blockedId._id;
146
+ return id !== userId;
147
+ }));
148
+ toast.success(t('privacySettings.userUnblocked') || 'User unblocked');
149
+ } catch (error) {
150
+ console.error('Failed to unblock user:', error);
151
+ toast.error(t('privacySettings.unblockError') || 'Failed to unblock user');
152
+ }
153
+ }, [oxyServices, t]);
154
+
155
+ const handleUnrestrict = useCallback(async (userId: string) => {
156
+ if (!oxyServices) return;
157
+ try {
158
+ await oxyServices.unrestrictUser(userId);
159
+ setRestrictedUsers(prev => prev.filter(u => {
160
+ const id = typeof u.restrictedId === 'string' ? u.restrictedId : u.restrictedId._id;
161
+ return id !== userId;
162
+ }));
163
+ toast.success(t('privacySettings.userUnrestricted') || 'User unrestricted');
164
+ } catch (error) {
165
+ console.error('Failed to unrestrict user:', error);
166
+ toast.error(t('privacySettings.unrestrictError') || 'Failed to unrestrict user');
167
+ }
168
+ }, [oxyServices, t]);
169
+
170
+ // Helper to extract user info from blocked/restricted objects
171
+ const extractUserInfo = useCallback((
172
+ item: BlockedUser | RestrictedUser,
173
+ idField: 'blockedId' | 'restrictedId'
174
+ ) => {
175
+ let userIdField: string | { _id: string; username?: string; avatar?: string };
176
+ let username: string;
177
+ let avatar: string | undefined;
178
+
179
+ if (idField === 'blockedId' && 'blockedId' in item) {
180
+ userIdField = item.blockedId;
181
+ username = typeof item.blockedId === 'string'
182
+ ? (item.username || 'Unknown')
183
+ : (item.blockedId.username || 'Unknown');
184
+ avatar = typeof item.blockedId === 'string' ? item.avatar : item.blockedId.avatar;
185
+ } else if (idField === 'restrictedId' && 'restrictedId' in item) {
186
+ userIdField = item.restrictedId;
187
+ username = typeof item.restrictedId === 'string'
188
+ ? (item.username || 'Unknown')
189
+ : (item.restrictedId.username || 'Unknown');
190
+ avatar = typeof item.restrictedId === 'string' ? item.avatar : item.restrictedId.avatar;
191
+ } else {
192
+ // Fallback (should not happen)
193
+ return { userId: '', username: 'Unknown', avatar: undefined };
194
+ }
195
+
196
+ const userId = typeof userIdField === 'string' ? userIdField : userIdField._id;
197
+ return { userId, username, avatar };
198
+ }, []);
199
+
200
+ // Reusable user list item component
201
+ const UserListItem: React.FC<{
202
+ item: BlockedUser | RestrictedUser;
203
+ idField: 'blockedId' | 'restrictedId';
204
+ onAction: (userId: string) => void;
205
+ actionLabel: string;
206
+ actionColor: string;
207
+ subtitle?: string;
208
+ }> = ({ item, idField, onAction, actionLabel, actionColor, subtitle }) => {
209
+ const { userId, username, avatar } = extractUserInfo(item, idField);
210
+ // Convert avatar file ID to URI if needed
211
+ const avatarUri = avatar && oxyServices
212
+ ? oxyServices.getFileDownloadUrl(avatar, 'thumb')
213
+ : undefined;
214
+
215
+ return (
216
+ <View style={[styles.userRow, { borderBottomColor: themeStyles.borderColor }]}>
217
+ <View style={styles.userInfo}>
218
+ <Avatar
219
+ uri={avatarUri}
220
+ name={username}
221
+ size={40}
222
+ theme={theme}
223
+ />
224
+ <View style={styles.userDetails}>
225
+ <Text style={[styles.username, { color: themeStyles.textColor }]}>
226
+ {username}
227
+ </Text>
228
+ {subtitle && (
229
+ <Text style={[styles.userSubtext, { color: themeStyles.mutedTextColor }]}>
230
+ {subtitle}
231
+ </Text>
232
+ )}
233
+ </View>
234
+ </View>
235
+ <TouchableOpacity
236
+ onPress={() => onAction(userId)}
237
+ style={[styles.actionButton, { backgroundColor: themeStyles.secondaryBackgroundColor }]}
238
+ >
239
+ <Text style={[styles.actionButtonText, { color: actionColor }]}>
240
+ {actionLabel}
241
+ </Text>
242
+ </TouchableOpacity>
243
+ </View>
244
+ );
245
+ };
246
+
113
247
  const themeStyles = useMemo(() => {
114
248
  const isDarkTheme = theme === 'dark';
115
249
  return {
@@ -288,6 +422,65 @@ const PrivacySettingsScreen: React.FC<BaseScreenProps> = ({
288
422
  onValueChange={(value) => updateSetting('blockScreenshots', value)}
289
423
  />
290
424
  </Section>
425
+
426
+ {/* Blocked Users */}
427
+ <Section title={t('privacySettings.sections.blockedUsers') || 'BLOCKED USERS'} theme={theme}>
428
+ {isLoadingUsers ? (
429
+ <View style={styles.loadingUsersContainer}>
430
+ <ActivityIndicator size="small" color={themeStyles.textColor} />
431
+ </View>
432
+ ) : blockedUsers.length === 0 ? (
433
+ <View style={styles.emptyContainer}>
434
+ <Text style={[styles.emptyText, { color: themeStyles.mutedTextColor }]}>
435
+ {t('privacySettings.noBlockedUsers') || 'No blocked users'}
436
+ </Text>
437
+ </View>
438
+ ) : (
439
+ blockedUsers.map((blocked) => {
440
+ const { userId } = extractUserInfo(blocked, 'blockedId');
441
+ return (
442
+ <UserListItem
443
+ key={userId}
444
+ item={blocked}
445
+ idField="blockedId"
446
+ onAction={handleUnblock}
447
+ actionLabel={t('privacySettings.unblock') || 'Unblock'}
448
+ actionColor="#FF3B30"
449
+ />
450
+ );
451
+ })
452
+ )}
453
+ </Section>
454
+
455
+ {/* Restricted Users */}
456
+ <Section title={t('privacySettings.sections.restrictedUsers') || 'RESTRICTED USERS'} theme={theme}>
457
+ {isLoadingUsers ? (
458
+ <View style={styles.loadingUsersContainer}>
459
+ <ActivityIndicator size="small" color={themeStyles.textColor} />
460
+ </View>
461
+ ) : restrictedUsers.length === 0 ? (
462
+ <View style={styles.emptyContainer}>
463
+ <Text style={[styles.emptyText, { color: themeStyles.mutedTextColor }]}>
464
+ {t('privacySettings.noRestrictedUsers') || 'No restricted users'}
465
+ </Text>
466
+ </View>
467
+ ) : (
468
+ restrictedUsers.map((restricted) => {
469
+ const { userId } = extractUserInfo(restricted, 'restrictedId');
470
+ return (
471
+ <UserListItem
472
+ key={userId}
473
+ item={restricted}
474
+ idField="restrictedId"
475
+ onAction={handleUnrestrict}
476
+ actionLabel={t('privacySettings.unrestrict') || 'Unrestrict'}
477
+ actionColor="#007AFF"
478
+ subtitle={t('privacySettings.restrictedDescription') || 'Limited interactions'}
479
+ />
480
+ );
481
+ })
482
+ )}
483
+ </Section>
291
484
  </ScrollView>
292
485
  </View>
293
486
  );
@@ -326,6 +519,51 @@ const styles = StyleSheet.create({
326
519
  fontSize: 14,
327
520
  opacity: 0.7,
328
521
  },
522
+ loadingUsersContainer: {
523
+ paddingVertical: 20,
524
+ alignItems: 'center',
525
+ },
526
+ emptyContainer: {
527
+ paddingVertical: 20,
528
+ alignItems: 'center',
529
+ },
530
+ emptyText: {
531
+ fontSize: 14,
532
+ },
533
+ userRow: {
534
+ flexDirection: 'row',
535
+ justifyContent: 'space-between',
536
+ alignItems: 'center',
537
+ paddingVertical: 12,
538
+ borderBottomWidth: 1,
539
+ },
540
+ userInfo: {
541
+ flexDirection: 'row',
542
+ alignItems: 'center',
543
+ flex: 1,
544
+ marginRight: 12,
545
+ },
546
+ userDetails: {
547
+ marginLeft: 12,
548
+ flex: 1,
549
+ },
550
+ username: {
551
+ fontSize: 16,
552
+ fontWeight: '500',
553
+ marginBottom: 2,
554
+ },
555
+ userSubtext: {
556
+ fontSize: 13,
557
+ },
558
+ actionButton: {
559
+ paddingHorizontal: 16,
560
+ paddingVertical: 8,
561
+ borderRadius: 8,
562
+ },
563
+ actionButtonText: {
564
+ fontSize: 14,
565
+ fontWeight: '600',
566
+ },
329
567
  });
330
568
 
331
569
  export default React.memo(PrivacySettingsScreen);
@@ -70,20 +70,6 @@ export interface ErrorResponse {
70
70
  details?: any;
71
71
  }
72
72
 
73
- /**
74
- * Validate required parameters
75
- * @param params Object to validate
76
- * @param requiredKeys Array of required keys
77
- * @throws Error if any required key is missing
78
- */
79
- export function validateRequiredParams(params: Record<string, any>, requiredKeys: string[]): void {
80
- const missing = requiredKeys.filter(key => params[key] === undefined || params[key] === null);
81
-
82
- if (missing.length > 0) {
83
- throw new Error(`Missing required parameters: ${missing.join(', ')}`);
84
- }
85
- }
86
-
87
73
  /**
88
74
  * Safe JSON parsing with error handling
89
75
  * @param data Data to parse
@@ -196,26 +196,6 @@ export async function withTimeout<T>(
196
196
  return Promise.race([operation, timeoutPromise]);
197
197
  }
198
198
 
199
- /**
200
- * Cache async operation results
201
- * @deprecated Use TTLCache from '../utils/cache' instead
202
- * This function is kept for backward compatibility but will be removed in a future version
203
- */
204
- export function createAsyncCache<T>(
205
- ttl: number = 5 * 60 * 1000 // 5 minutes default
206
- ) {
207
- // Re-export from centralized cache utility
208
- const cache = new TTLCache<T>(ttl);
209
- registerCacheForCleanup(cache);
210
-
211
- return {
212
- get: (key: string): T | null => cache.get(key),
213
- set: (key: string, data: T): void => cache.set(key, data),
214
- clear: (): void => cache.clear(),
215
- delete: (key: string): boolean => cache.delete(key),
216
- };
217
- }
218
-
219
199
  /**
220
200
  * Execute async operation with loading state
221
201
  */