be-components 7.4.3 → 7.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/ApiOverrides/index.js +0 -2
- package/lib/commonjs/ApiOverrides/index.js.map +1 -1
- package/lib/commonjs/NotificationManager/api/index.js +448 -0
- package/lib/commonjs/NotificationManager/api/index.js.map +1 -0
- package/lib/commonjs/NotificationManager/index.js +1159 -0
- package/lib/commonjs/NotificationManager/index.js.map +1 -0
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/types.d.js.map +1 -1
- package/lib/module/ApiOverrides/index.js +0 -2
- package/lib/module/ApiOverrides/index.js.map +1 -1
- package/lib/module/NotificationManager/api/index.js +441 -0
- package/lib/module/NotificationManager/api/index.js.map +1 -0
- package/lib/module/NotificationManager/index.js +1140 -0
- package/lib/module/NotificationManager/index.js.map +1 -0
- package/lib/module/index.js +3 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.d.js.map +1 -1
- package/lib/typescript/lib/commonjs/ApiOverrides/index.d.ts.map +1 -1
- package/lib/typescript/lib/commonjs/NotificationManager/api/index.d.ts +804 -0
- package/lib/typescript/lib/commonjs/NotificationManager/api/index.d.ts.map +1 -0
- package/lib/typescript/lib/commonjs/NotificationManager/index.d.ts +815 -0
- package/lib/typescript/lib/commonjs/NotificationManager/index.d.ts.map +1 -0
- package/lib/typescript/lib/commonjs/index.d.ts +803 -0
- package/lib/typescript/lib/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/lib/module/ApiOverrides/index.d.ts.map +1 -1
- package/lib/typescript/lib/module/NotificationManager/api/index.d.ts +803 -0
- package/lib/typescript/lib/module/NotificationManager/api/index.d.ts.map +1 -0
- package/lib/typescript/lib/module/NotificationManager/index.d.ts +16 -0
- package/lib/typescript/lib/module/NotificationManager/index.d.ts.map +1 -0
- package/lib/typescript/lib/module/index.d.ts +4 -1
- package/lib/typescript/lib/module/index.d.ts.map +1 -1
- package/lib/typescript/src/ApiOverrides/index.d.ts.map +1 -1
- package/lib/typescript/src/NotificationManager/api/index.d.ts +822 -0
- package/lib/typescript/src/NotificationManager/api/index.d.ts.map +1 -0
- package/lib/typescript/src/NotificationManager/index.d.ts +17 -0
- package/lib/typescript/src/NotificationManager/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ApiOverrides/index.ts +0 -2
- package/src/NotificationManager/api/index.ts +134 -0
- package/src/NotificationManager/index.tsx +894 -0
- package/src/index.tsx +6 -1
- package/src/types.d.ts +70 -0
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import { FlatList, TouchableOpacity, ActivityIndicator } from 'react-native';
|
|
3
|
+
import { View, Text, Button, TextInput } from '../Components/Themed';
|
|
4
|
+
import { useColors } from '../constants/useColors';
|
|
5
|
+
import { NotificationApi, NotificationHelpers } from './api';
|
|
6
|
+
import type { PlayerNotificationProps, MyPlayerProps, NotificationGroupProps, FocusPositionProps } from '../types';
|
|
7
|
+
import { Icons } from '../Components';
|
|
8
|
+
import DropDown from '../Components/Dropdown';
|
|
9
|
+
import { showConfirmAlert } from '../Components/ConfirmAlert';
|
|
10
|
+
|
|
11
|
+
type NotificationManagerProps = {
|
|
12
|
+
player_ids: string[];
|
|
13
|
+
me: MyPlayerProps;
|
|
14
|
+
notification_type?: 'order_notifications' | 'competition_notifications' | 'social_notifications';
|
|
15
|
+
default_path_name?: string;
|
|
16
|
+
onFocusPosition?:(pos:FocusPositionProps) => void,
|
|
17
|
+
default_params?: Record<string, string>;
|
|
18
|
+
onComplete?: (notification: PlayerNotificationProps) => void;
|
|
19
|
+
onClose?: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const notification_types = [
|
|
23
|
+
{ label: 'Order Notifications', value: 'order_notifications' },
|
|
24
|
+
{ label: 'Competition Notifications', value: 'competition_notifications' },
|
|
25
|
+
{ label: 'Social Notifications', value: 'social_notifications' }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const sections = ['header','top_row', 'preview', 'second_row']
|
|
29
|
+
|
|
30
|
+
const NotificationManager = ({
|
|
31
|
+
player_ids,
|
|
32
|
+
me,
|
|
33
|
+
notification_type,
|
|
34
|
+
default_path_name,
|
|
35
|
+
onFocusPosition,
|
|
36
|
+
default_params,
|
|
37
|
+
onComplete,
|
|
38
|
+
onClose
|
|
39
|
+
}: NotificationManagerProps) => {
|
|
40
|
+
const Colors = useColors();
|
|
41
|
+
const [loading, setLoading] = useState(false);
|
|
42
|
+
const [loadingGroups, setLoadingGroups] = useState(false);
|
|
43
|
+
const [notificationGroups, setNotificationGroups] = useState<NotificationGroupProps[]>([]);
|
|
44
|
+
const [selectedGroup, setSelectedGroup] = useState<NotificationGroupProps | null>(null);
|
|
45
|
+
const [showGroupsModal, setShowGroupsModal] = useState(false);
|
|
46
|
+
const [groupMemberIds, setGroupMemberIds] = useState<string[]>([]);
|
|
47
|
+
const [showEditCustomGroupModal, setShowEditCustomGroupModal] = useState(false);
|
|
48
|
+
const [customGroupCsv, setCustomGroupCsv] = useState('');
|
|
49
|
+
const [sendingProgress, setSendingProgress] = useState<{ current: number; total: number } | null>(null);
|
|
50
|
+
|
|
51
|
+
// Create a custom group from initial player_ids
|
|
52
|
+
const customGroup: NotificationGroupProps | null = player_ids.length > 0 ? {
|
|
53
|
+
notification_group_id: 'custom_group',
|
|
54
|
+
name: 'Custom Recipients',
|
|
55
|
+
description: `${player_ids.length} selected recipients`,
|
|
56
|
+
status: 'active',
|
|
57
|
+
grouping_function:'',
|
|
58
|
+
player_count: player_ids.length,
|
|
59
|
+
create_datetime: new Date().toISOString(),
|
|
60
|
+
last_update_datetime: new Date().toISOString()
|
|
61
|
+
} : null;
|
|
62
|
+
|
|
63
|
+
// Initialize selectedPath based on default_path_name if provided
|
|
64
|
+
const initialPath = default_path_name
|
|
65
|
+
? NotificationHelpers.app_paths.find(p => p.path_name === default_path_name) || NotificationHelpers.app_paths[0]
|
|
66
|
+
: NotificationHelpers.app_paths[0];
|
|
67
|
+
|
|
68
|
+
const [selectedPath, setSelectedPath] = useState<typeof NotificationHelpers.app_paths[0] | undefined>(initialPath);
|
|
69
|
+
const [customPathName, setCustomPathName] = useState('');
|
|
70
|
+
const [pathParams, setPathParams] = useState<Record<string, string>>(default_params || {});
|
|
71
|
+
const [pathSearch, setPathSearch] = useState('');
|
|
72
|
+
const [notification, setNotification] = useState<Partial<PlayerNotificationProps>>({
|
|
73
|
+
type: notification_type || 'order_notifications',
|
|
74
|
+
notify_body_type: 'custom',
|
|
75
|
+
title: '',
|
|
76
|
+
body: '',
|
|
77
|
+
status: 'pending',
|
|
78
|
+
expire_time: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days from now
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
startUp();
|
|
83
|
+
// Auto-select custom group if initial player_ids provided
|
|
84
|
+
if (customGroup) {
|
|
85
|
+
setSelectedGroup(customGroup);
|
|
86
|
+
setGroupMemberIds(player_ids);
|
|
87
|
+
}
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
// Initialize pathParams when selectedPath changes, merging with default_params
|
|
92
|
+
const mergedParams = { ...selectedPath?.params, ...(default_params || {}) };
|
|
93
|
+
setPathParams(mergedParams as any);
|
|
94
|
+
}, [selectedPath]);
|
|
95
|
+
|
|
96
|
+
const startUp = () => {
|
|
97
|
+
NotificationApi.setEnvironment();
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const loadNotificationGroups = async () => {
|
|
101
|
+
setLoadingGroups(true);
|
|
102
|
+
const groups = await NotificationApi.getActiveNotificationGroups();
|
|
103
|
+
setNotificationGroups(groups);
|
|
104
|
+
setLoadingGroups(false);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleSelectGroup = async (group_id: string) => {
|
|
108
|
+
setLoadingGroups(true);
|
|
109
|
+
|
|
110
|
+
// Check if it's the custom group
|
|
111
|
+
if (group_id === 'custom_group' && customGroup) {
|
|
112
|
+
setSelectedGroup(customGroup);
|
|
113
|
+
setGroupMemberIds(player_ids);
|
|
114
|
+
setLoadingGroups(false);
|
|
115
|
+
setShowGroupsModal(false);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const group = notificationGroups.find(g => g.notification_group_id === group_id);
|
|
120
|
+
setSelectedGroup(group || null);
|
|
121
|
+
|
|
122
|
+
// Fetch all members for this group
|
|
123
|
+
const members = await NotificationApi.getNotificationGroupMembersByGroup(group_id);
|
|
124
|
+
|
|
125
|
+
// Flatten all player_ids arrays from all member objects
|
|
126
|
+
const memberPlayerIds = members.flatMap(m => m.player_ids);
|
|
127
|
+
setGroupMemberIds(memberPlayerIds);
|
|
128
|
+
|
|
129
|
+
setLoadingGroups(false);
|
|
130
|
+
setShowGroupsModal(false);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleRemoveGroup = () => {
|
|
134
|
+
setSelectedGroup(null);
|
|
135
|
+
setGroupMemberIds([]);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleOpenEditCustomGroup = () => {
|
|
139
|
+
// Populate CSV with current groupMemberIds
|
|
140
|
+
setCustomGroupCsv(groupMemberIds.join(','));
|
|
141
|
+
setShowEditCustomGroupModal(true);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleSaveCustomGroup = () => {
|
|
145
|
+
// Parse CSV and update groupMemberIds
|
|
146
|
+
const ids = customGroupCsv
|
|
147
|
+
.split(',')
|
|
148
|
+
.map(id => id.trim())
|
|
149
|
+
.filter(id => id.length > 0);
|
|
150
|
+
|
|
151
|
+
setGroupMemberIds(ids);
|
|
152
|
+
|
|
153
|
+
// Update the custom group description
|
|
154
|
+
if (customGroup && selectedGroup?.notification_group_id === 'custom_group') {
|
|
155
|
+
const updatedGroup = {
|
|
156
|
+
...customGroup,
|
|
157
|
+
description: `${ids.length} selected recipients`,
|
|
158
|
+
player_count: ids.length
|
|
159
|
+
};
|
|
160
|
+
setSelectedGroup(updatedGroup);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setShowEditCustomGroupModal(false);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Filter and sort paths: primary first (alphabetically), then others (alphabetically), limited by search
|
|
167
|
+
const filteredPaths = useMemo(() => {
|
|
168
|
+
let filtered = NotificationHelpers.app_paths;
|
|
169
|
+
|
|
170
|
+
// Apply search filter
|
|
171
|
+
if (pathSearch) {
|
|
172
|
+
filtered = filtered.filter(path =>
|
|
173
|
+
path.label.toLowerCase().includes(pathSearch.toLowerCase()) ||
|
|
174
|
+
path.path_name.toLowerCase().includes(pathSearch.toLowerCase())
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Sort: primary paths first (alphabetically), then non-primary (alphabetically)
|
|
179
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
180
|
+
if (a.primary && !b.primary) return -1;
|
|
181
|
+
if (!a.primary && b.primary) return 1;
|
|
182
|
+
return a.label.localeCompare(b.label);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Limit to 15 results
|
|
186
|
+
return sorted.slice(0, 15);
|
|
187
|
+
}, [pathSearch]);
|
|
188
|
+
|
|
189
|
+
const isValid = (): string[] => {
|
|
190
|
+
const errors: string[] = [];
|
|
191
|
+
|
|
192
|
+
if (!notification.title || notification.title.trim() === '') {
|
|
193
|
+
errors.push('Notification title is required');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!notification.body || notification.body.trim() === '') {
|
|
197
|
+
errors.push('Notification body is required');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (groupMemberIds.length === 0) {
|
|
201
|
+
errors.push('At least one recipient is required');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check required params
|
|
205
|
+
if (selectedPath?.requiredParams && selectedPath.requiredParams.length > 0) {
|
|
206
|
+
selectedPath.requiredParams.forEach(paramKey => {
|
|
207
|
+
if (!pathParams[paramKey] || pathParams[paramKey].trim() === '') {
|
|
208
|
+
errors.push(`Required parameter "${paramKey}" is missing`);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check custom path name
|
|
214
|
+
if (selectedPath?.path_name === 'custom' && (!customPathName || customPathName.trim() === '')) {
|
|
215
|
+
errors.push('Custom path name is required');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return errors;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const confirmSendNotification = (options?:{ test:boolean }) => {
|
|
222
|
+
const errors = isValid();
|
|
223
|
+
|
|
224
|
+
if (errors.length > 0) {
|
|
225
|
+
const errorMessage = 'Please fix the following errors:\n\n' + errors.map((err, idx) => `${idx + 1}. ${err}`).join('\n');
|
|
226
|
+
alert(errorMessage);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
showConfirmAlert('Are you sure?', options?.test ? 'This will send a test notification to you only':'This will send a notification to all users selected',
|
|
231
|
+
() => handleSendNotification(options),
|
|
232
|
+
() => console.log('Done')
|
|
233
|
+
)
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleSendNotification = async (options?:{ test:boolean }) => {
|
|
237
|
+
// Use only group member IDs (no individual player_ids)
|
|
238
|
+
let player_ids_to_send = [...groupMemberIds];
|
|
239
|
+
|
|
240
|
+
if(options?.test){
|
|
241
|
+
//Change to the admin
|
|
242
|
+
player_ids_to_send = [me.player_id]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
setLoading(true);
|
|
246
|
+
try {
|
|
247
|
+
// Determine final path_name
|
|
248
|
+
const finalPathName = selectedPath?.path_name === 'custom' ? customPathName : selectedPath?.path_name;
|
|
249
|
+
|
|
250
|
+
// Construct the complete notification object with all required fields
|
|
251
|
+
const completeNotification: PlayerNotificationProps = {
|
|
252
|
+
player_notification_id: '', // Will be generated by backend
|
|
253
|
+
player_id: me.player_id,
|
|
254
|
+
type: notification.type as 'order_notifications' | 'competition_notifications' | 'social_notifications',
|
|
255
|
+
notify_body_type: notification.notify_body_type || 'custom',
|
|
256
|
+
title: notification.title!,
|
|
257
|
+
body: notification.body!,
|
|
258
|
+
options: {
|
|
259
|
+
id: Date.now().toString(),
|
|
260
|
+
body: notification.body!,
|
|
261
|
+
icon: 'default',
|
|
262
|
+
type: notification.type || 'order_notifications',
|
|
263
|
+
data: {
|
|
264
|
+
id: Date.now().toString(),
|
|
265
|
+
player_id: me.player_id,
|
|
266
|
+
pageStack: 'MainStack',
|
|
267
|
+
page: 'Notifications',
|
|
268
|
+
pageParams: {},
|
|
269
|
+
url: notification.link_override,
|
|
270
|
+
path_name: finalPathName,
|
|
271
|
+
params: pathParams
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
status: 'unread',
|
|
275
|
+
link_override: notification.link_override,
|
|
276
|
+
expire_time: 0,
|
|
277
|
+
create_datetime: new Date().toISOString(),
|
|
278
|
+
last_update_datetime: new Date().toISOString(),
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Split into batches of 500
|
|
282
|
+
const BATCH_SIZE = 500;
|
|
283
|
+
const batches: string[][] = [];
|
|
284
|
+
for (let i = 0; i < player_ids_to_send.length; i += BATCH_SIZE) {
|
|
285
|
+
batches.push(player_ids_to_send.slice(i, i + BATCH_SIZE));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Send notifications in batches
|
|
289
|
+
for (let i = 0; i < batches.length; i++) {
|
|
290
|
+
const batch = batches[i];
|
|
291
|
+
if (!batch) continue;
|
|
292
|
+
|
|
293
|
+
setSendingProgress({ current: i + 1, total: batches.length });
|
|
294
|
+
await NotificationApi.broadcastNotifications(
|
|
295
|
+
completeNotification,
|
|
296
|
+
batch
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
setSendingProgress(null);
|
|
301
|
+
|
|
302
|
+
if (onComplete) {
|
|
303
|
+
onComplete(completeNotification);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Only reset form if not a test send
|
|
307
|
+
if (!options?.test) {
|
|
308
|
+
setNotification({
|
|
309
|
+
type: 'order_notifications',
|
|
310
|
+
notify_body_type: 'custom',
|
|
311
|
+
title: '',
|
|
312
|
+
body: '',
|
|
313
|
+
status: 'pending',
|
|
314
|
+
expire_time: Date.now() + (7 * 24 * 60 * 60 * 1000),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const recipientCount = options?.test ? 1 : groupMemberIds.length;
|
|
319
|
+
const recipientMessage = options?.test ? 'Test notification sent to you!' : `Notification sent successfully to ${recipientCount} user${recipientCount !== 1 ? 's' : ''}!`;
|
|
320
|
+
alert(recipientMessage);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Error sending notification:', error);
|
|
323
|
+
alert('Failed to send notification. Please try again.');
|
|
324
|
+
}
|
|
325
|
+
setLoading(false);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
const renderSections = (data:{ item:string, index:number }) => {
|
|
330
|
+
switch(data.item){
|
|
331
|
+
case 'header':
|
|
332
|
+
const headerTotalRecipients = groupMemberIds.length;
|
|
333
|
+
return (
|
|
334
|
+
<View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
|
|
335
|
+
<View transparent style={{ flex:1 }}>
|
|
336
|
+
<Text theme='h1'>Send Notification</Text>
|
|
337
|
+
<Text theme='description' style={{ marginTop:3 }}>Sending to {headerTotalRecipients} users</Text>
|
|
338
|
+
</View>
|
|
339
|
+
{onClose && (
|
|
340
|
+
<Button transparent onPress={onClose}>
|
|
341
|
+
<Icons.CloseIcon size={16} color={Colors.text.h1} />
|
|
342
|
+
</Button>
|
|
343
|
+
)}
|
|
344
|
+
</View>
|
|
345
|
+
)
|
|
346
|
+
case 'preview':
|
|
347
|
+
return (
|
|
348
|
+
<View float style={{ margin: 5 }}>
|
|
349
|
+
<View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
|
|
350
|
+
<View transparent style={{ flex:1 }}>
|
|
351
|
+
<Text theme='h1'>Preview</Text>
|
|
352
|
+
<Text theme='description' style={{ marginTop:3 }}>What will this notification look like?</Text>
|
|
353
|
+
</View>
|
|
354
|
+
</View>
|
|
355
|
+
<View
|
|
356
|
+
float
|
|
357
|
+
style={{
|
|
358
|
+
padding: 15,
|
|
359
|
+
margin:10,
|
|
360
|
+
borderRadius: 8,
|
|
361
|
+
borderWidth: 1,
|
|
362
|
+
borderColor: Colors.borders.light,
|
|
363
|
+
}}
|
|
364
|
+
>
|
|
365
|
+
<View transparent style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
366
|
+
<View
|
|
367
|
+
style={{
|
|
368
|
+
width: 40,
|
|
369
|
+
height: 40,
|
|
370
|
+
borderRadius: 8,
|
|
371
|
+
justifyContent: 'center',
|
|
372
|
+
alignItems: 'center',
|
|
373
|
+
marginRight: 12,
|
|
374
|
+
}}
|
|
375
|
+
>
|
|
376
|
+
<Icons.NotificationIcon size={20} color={Colors.text.h1} />
|
|
377
|
+
</View>
|
|
378
|
+
<View transparent style={{ flex: 1 }}>
|
|
379
|
+
<Text theme="h1">
|
|
380
|
+
{notification.title || 'Notification Title'}
|
|
381
|
+
</Text>
|
|
382
|
+
<Text theme="description" style={{ marginTop: 4 }}>
|
|
383
|
+
{notification.body || 'Notification message will appear here'}
|
|
384
|
+
</Text>
|
|
385
|
+
</View>
|
|
386
|
+
</View>
|
|
387
|
+
</View>
|
|
388
|
+
</View>
|
|
389
|
+
)
|
|
390
|
+
case 'top_row':
|
|
391
|
+
const totalRecipients = groupMemberIds.length;
|
|
392
|
+
return (
|
|
393
|
+
<View style={{ flexDirection:'row', flexWrap:'wrap' }}>
|
|
394
|
+
<View
|
|
395
|
+
float
|
|
396
|
+
style={{
|
|
397
|
+
flexGrow:1,
|
|
398
|
+
minWidth:300,
|
|
399
|
+
padding:10,
|
|
400
|
+
margin:5
|
|
401
|
+
}}
|
|
402
|
+
>
|
|
403
|
+
<View transparent style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
|
|
404
|
+
<View transparent style={{ flex:1 }}>
|
|
405
|
+
<Text theme="h2">Recipients ({totalRecipients})</Text>
|
|
406
|
+
<Text theme="description" style={{ marginTop: 4 }}>
|
|
407
|
+
{totalRecipients === 0 ? 'No recipients selected' : 'Selected group'}
|
|
408
|
+
</Text>
|
|
409
|
+
</View>
|
|
410
|
+
<Button
|
|
411
|
+
type="action"
|
|
412
|
+
onPress={() => {
|
|
413
|
+
loadNotificationGroups();
|
|
414
|
+
setShowGroupsModal(true);
|
|
415
|
+
}}
|
|
416
|
+
style={{ padding: 8 }}
|
|
417
|
+
>
|
|
418
|
+
<Text theme="h1" color={Colors.text.white}>
|
|
419
|
+
Select Group
|
|
420
|
+
</Text>
|
|
421
|
+
</Button>
|
|
422
|
+
</View>
|
|
423
|
+
|
|
424
|
+
{/* Display Selected Group */}
|
|
425
|
+
{selectedGroup && (
|
|
426
|
+
<View
|
|
427
|
+
float
|
|
428
|
+
style={{
|
|
429
|
+
flexDirection: 'row',
|
|
430
|
+
alignItems: 'center',
|
|
431
|
+
justifyContent: 'space-between',
|
|
432
|
+
padding: 12,
|
|
433
|
+
borderRadius: 8,
|
|
434
|
+
marginTop: 5
|
|
435
|
+
}}
|
|
436
|
+
>
|
|
437
|
+
<View transparent style={{ flex: 1 }}>
|
|
438
|
+
<Text theme="h1">{selectedGroup.name}</Text>
|
|
439
|
+
<Text theme="description" style={{ marginTop: 2 }}>
|
|
440
|
+
{groupMemberIds.length} members
|
|
441
|
+
</Text>
|
|
442
|
+
</View>
|
|
443
|
+
<View transparent style={{ flexDirection: 'row' }}>
|
|
444
|
+
{selectedGroup.notification_group_id === 'custom_group' && (
|
|
445
|
+
<Button
|
|
446
|
+
type="action"
|
|
447
|
+
onPress={handleOpenEditCustomGroup}
|
|
448
|
+
style={{ padding: 8, marginRight: 8 }}
|
|
449
|
+
>
|
|
450
|
+
<Icons.EditIcon size={16} color={Colors.text.white} />
|
|
451
|
+
</Button>
|
|
452
|
+
)}
|
|
453
|
+
<Button
|
|
454
|
+
type="error"
|
|
455
|
+
onPress={handleRemoveGroup}
|
|
456
|
+
style={{ padding: 8 }}
|
|
457
|
+
>
|
|
458
|
+
<Icons.CloseIcon size={16} color={Colors.text.white} />
|
|
459
|
+
</Button>
|
|
460
|
+
</View>
|
|
461
|
+
</View>
|
|
462
|
+
)}
|
|
463
|
+
</View>
|
|
464
|
+
</View>
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
case 'second_row':
|
|
468
|
+
return (
|
|
469
|
+
<View style={{ flexDirection:'row', flexWrap:'wrap' }}>
|
|
470
|
+
<View float style={{ minWidth:300, flexGrow :1, margin:5 }}>
|
|
471
|
+
<View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
|
|
472
|
+
<View transparent style={{ flex:1 }}>
|
|
473
|
+
<Text theme='h1'>Notification Message</Text>
|
|
474
|
+
<Text theme='description' style={{ marginTop:3 }}>Update notification message below</Text>
|
|
475
|
+
</View>
|
|
476
|
+
</View>
|
|
477
|
+
<View transparent type='body'>
|
|
478
|
+
<View transparent style={{ padding:10 }}>
|
|
479
|
+
<Text theme="h2" style={{ marginBottom:10 }}>
|
|
480
|
+
Notification Title
|
|
481
|
+
</Text>
|
|
482
|
+
<TextInput
|
|
483
|
+
value={notification.title}
|
|
484
|
+
onFocusPosition={onFocusPosition}
|
|
485
|
+
placeholder="Enter notification title"
|
|
486
|
+
onChangeText={(title) => setNotification({ ...notification, title })}
|
|
487
|
+
|
|
488
|
+
/>
|
|
489
|
+
</View>
|
|
490
|
+
<View transparent style={{ padding:10 }}>
|
|
491
|
+
<Text theme="h2" style={{ marginBottom:10 }}>
|
|
492
|
+
Notification Body
|
|
493
|
+
</Text>
|
|
494
|
+
<TextInput
|
|
495
|
+
value={notification.body}
|
|
496
|
+
onFocusPosition={onFocusPosition}
|
|
497
|
+
placeholder="Enter notification message"
|
|
498
|
+
onChangeText={(body) => setNotification({ ...notification, body })}
|
|
499
|
+
multiline
|
|
500
|
+
numberOfLines={6}
|
|
501
|
+
style={{ padding: 12, borderRadius: 8, minHeight: 120 }}
|
|
502
|
+
/>
|
|
503
|
+
</View>
|
|
504
|
+
</View>
|
|
505
|
+
</View>
|
|
506
|
+
<View float style={{ flexGrow:1, minWidth:300, margin:5 }}>
|
|
507
|
+
<View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
|
|
508
|
+
<View transparent style={{ flex:1 }}>
|
|
509
|
+
<Text theme='h1'>Notification Settings</Text>
|
|
510
|
+
<Text theme='description' style={{ marginTop:3 }}>Manage settigns below</Text>
|
|
511
|
+
</View>
|
|
512
|
+
</View>
|
|
513
|
+
<View transparent type='body'>
|
|
514
|
+
<View type='row' style={{ padding:10 }}>
|
|
515
|
+
<Text theme="h2" style={{ flex:1 }}>
|
|
516
|
+
Notification Type
|
|
517
|
+
</Text>
|
|
518
|
+
<DropDown
|
|
519
|
+
selected_value={
|
|
520
|
+
notification_types.find((t) => t.value === notification.type)?.label ?? ''
|
|
521
|
+
}
|
|
522
|
+
dropdown_options={[
|
|
523
|
+
{ value: 'type', eligible_options: notification_types.map((t) => t.label) },
|
|
524
|
+
]}
|
|
525
|
+
onOptionSelect={(selected) => {
|
|
526
|
+
const type = notification_types.find((t) => t.label === selected)?.value;
|
|
527
|
+
setNotification({ ...notification, type: type as any });
|
|
528
|
+
}}
|
|
529
|
+
/>
|
|
530
|
+
</View>
|
|
531
|
+
<View transparent style={{ padding:10 }}>
|
|
532
|
+
<Text theme="h2" style={{ marginBottom:10 }}>
|
|
533
|
+
Link Override (Optional)
|
|
534
|
+
</Text>
|
|
535
|
+
<TextInput
|
|
536
|
+
value={notification.link_override || ''}
|
|
537
|
+
onFocusPosition={onFocusPosition}
|
|
538
|
+
placeholder="https://example.com"
|
|
539
|
+
onChangeText={(link_override) =>
|
|
540
|
+
setNotification({ ...notification, link_override })
|
|
541
|
+
}
|
|
542
|
+
style={{ padding: 12, borderRadius: 8 }}
|
|
543
|
+
/>
|
|
544
|
+
</View>
|
|
545
|
+
<View transparent style={{ padding:10 }}>
|
|
546
|
+
<Text theme="h2" style={{ marginBottom: 10 }}>
|
|
547
|
+
Deep Link Path
|
|
548
|
+
</Text>
|
|
549
|
+
|
|
550
|
+
{/* Selected Path Display */}
|
|
551
|
+
<View
|
|
552
|
+
type="header"
|
|
553
|
+
style={{
|
|
554
|
+
padding: 12,
|
|
555
|
+
borderRadius: 8,
|
|
556
|
+
marginBottom: 8,
|
|
557
|
+
flexDirection: 'row',
|
|
558
|
+
alignItems: 'center',
|
|
559
|
+
justifyContent: 'space-between'
|
|
560
|
+
}}
|
|
561
|
+
>
|
|
562
|
+
<View transparent style={{ flex: 1 }}>
|
|
563
|
+
<Text theme="h1" style={{ marginBottom: 2 }}>
|
|
564
|
+
{selectedPath?.label}
|
|
565
|
+
</Text>
|
|
566
|
+
<Text theme="description" style={{ fontSize: 12 }}>
|
|
567
|
+
{selectedPath?.path_name}
|
|
568
|
+
</Text>
|
|
569
|
+
</View>
|
|
570
|
+
{selectedPath?.primary && (
|
|
571
|
+
<View transparent style={{ paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4 }}>
|
|
572
|
+
<Text theme="description" color={Colors.text.white} style={{ fontSize: 10 }}>
|
|
573
|
+
Primary
|
|
574
|
+
</Text>
|
|
575
|
+
</View>
|
|
576
|
+
)}
|
|
577
|
+
</View>
|
|
578
|
+
|
|
579
|
+
{/* Search Box */}
|
|
580
|
+
<TextInput
|
|
581
|
+
value={pathSearch}
|
|
582
|
+
placeholder="Search paths to change..."
|
|
583
|
+
onFocusPosition={onFocusPosition}
|
|
584
|
+
onChangeText={setPathSearch}
|
|
585
|
+
style={{ padding: 12, borderRadius: 8, marginBottom: 8 }}
|
|
586
|
+
/>
|
|
587
|
+
|
|
588
|
+
{/* Path Results List */}
|
|
589
|
+
{pathSearch && (
|
|
590
|
+
<View
|
|
591
|
+
float
|
|
592
|
+
style={{
|
|
593
|
+
maxHeight: 300,
|
|
594
|
+
borderRadius: 8,
|
|
595
|
+
borderWidth: 1,
|
|
596
|
+
borderColor: Colors.borders.light,
|
|
597
|
+
}}
|
|
598
|
+
>
|
|
599
|
+
<FlatList
|
|
600
|
+
data={filteredPaths}
|
|
601
|
+
keyExtractor={(item) => item.path_name}
|
|
602
|
+
renderItem={({ item }) => (
|
|
603
|
+
<TouchableOpacity
|
|
604
|
+
onPress={() => {
|
|
605
|
+
setSelectedPath(item);
|
|
606
|
+
setPathSearch('');
|
|
607
|
+
}}
|
|
608
|
+
style={{
|
|
609
|
+
padding: 12,
|
|
610
|
+
borderBottomWidth: 1,
|
|
611
|
+
borderColor: Colors.borders.light,
|
|
612
|
+
}}
|
|
613
|
+
>
|
|
614
|
+
<View transparent style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
615
|
+
<View transparent style={{ flex: 1 }}>
|
|
616
|
+
<Text theme="h2" style={{ marginBottom: 2 }}>
|
|
617
|
+
{item.label}
|
|
618
|
+
</Text>
|
|
619
|
+
<Text theme="description" style={{ fontSize: 11 }}>
|
|
620
|
+
{item.path_name}
|
|
621
|
+
</Text>
|
|
622
|
+
</View>
|
|
623
|
+
{item.primary && (
|
|
624
|
+
<View transparent style={{ paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, marginLeft: 8 }}>
|
|
625
|
+
<Text theme="description" color={Colors.text.white} style={{ fontSize: 9 }}>
|
|
626
|
+
Primary
|
|
627
|
+
</Text>
|
|
628
|
+
</View>
|
|
629
|
+
)}
|
|
630
|
+
</View>
|
|
631
|
+
</TouchableOpacity>
|
|
632
|
+
)}
|
|
633
|
+
ListEmptyComponent={
|
|
634
|
+
<View style={{ padding: 20, alignItems: 'center' }}>
|
|
635
|
+
<Text theme="description">No paths found</Text>
|
|
636
|
+
</View>
|
|
637
|
+
}
|
|
638
|
+
/>
|
|
639
|
+
</View>
|
|
640
|
+
)}
|
|
641
|
+
</View>
|
|
642
|
+
{Object.keys(selectedPath?.params ?? {}).length > 0 && (
|
|
643
|
+
<View style={{ padding:10 }}>
|
|
644
|
+
<Text theme="h2" style={{ marginBottom: 8 }}>
|
|
645
|
+
Path Parameters
|
|
646
|
+
</Text>
|
|
647
|
+
<View type="header" style={{ padding: 12, borderRadius: 8 }}>
|
|
648
|
+
{Object.keys(selectedPath?.params ?? {}).map((paramKey) => {
|
|
649
|
+
const isRequired = selectedPath?.requiredParams.includes(paramKey);
|
|
650
|
+
return (
|
|
651
|
+
<View key={paramKey} transparent style={{ marginBottom: 12 }}>
|
|
652
|
+
<View transparent style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 4 }}>
|
|
653
|
+
<Text theme="description">
|
|
654
|
+
{paramKey}
|
|
655
|
+
</Text>
|
|
656
|
+
{isRequired ? (
|
|
657
|
+
<View transparent style={{ marginLeft: 4, paddingHorizontal: 6, paddingVertical: 2, backgroundColor: Colors.text.error, borderRadius: 4 }}>
|
|
658
|
+
<Text theme="description" color={Colors.text.white} style={{ fontSize: 10 }}>
|
|
659
|
+
Required
|
|
660
|
+
</Text>
|
|
661
|
+
</View>
|
|
662
|
+
) : (
|
|
663
|
+
<View transparent style={{ marginLeft: 4, paddingHorizontal: 6, paddingVertical: 2, backgroundColor: Colors.borders.light, borderRadius: 4 }}>
|
|
664
|
+
<Text theme="description" style={{ fontSize: 10 }}>
|
|
665
|
+
Optional
|
|
666
|
+
</Text>
|
|
667
|
+
</View>
|
|
668
|
+
)}
|
|
669
|
+
</View>
|
|
670
|
+
<TextInput
|
|
671
|
+
value={pathParams[paramKey] || ''}
|
|
672
|
+
onFocusPosition={onFocusPosition}
|
|
673
|
+
placeholder={`Enter ${paramKey}${isRequired ? ' (required)' : ' (optional)'}`}
|
|
674
|
+
onChangeText={(value) =>
|
|
675
|
+
setPathParams({ ...pathParams, [paramKey]: value })
|
|
676
|
+
}
|
|
677
|
+
style={{
|
|
678
|
+
padding: 10,
|
|
679
|
+
borderRadius: 8,
|
|
680
|
+
borderWidth: isRequired ? 2 : 1,
|
|
681
|
+
borderColor: isRequired && !pathParams[paramKey] ? Colors.text.error : Colors.borders.light
|
|
682
|
+
}}
|
|
683
|
+
/>
|
|
684
|
+
</View>
|
|
685
|
+
);
|
|
686
|
+
})}
|
|
687
|
+
</View>
|
|
688
|
+
</View>
|
|
689
|
+
)}
|
|
690
|
+
</View>
|
|
691
|
+
{selectedPath?.path_name === 'custom' && (
|
|
692
|
+
<View style={{ marginBottom: 20 }}>
|
|
693
|
+
<Text theme="h2" style={{ marginBottom: 8 }}>
|
|
694
|
+
Custom Path Name
|
|
695
|
+
</Text>
|
|
696
|
+
<TextInput
|
|
697
|
+
value={customPathName}
|
|
698
|
+
onFocusPosition={onFocusPosition}
|
|
699
|
+
placeholder="/your/custom/path"
|
|
700
|
+
onChangeText={setCustomPathName}
|
|
701
|
+
style={{ padding: 12, borderRadius: 8 }}
|
|
702
|
+
/>
|
|
703
|
+
</View>
|
|
704
|
+
)}
|
|
705
|
+
</View>
|
|
706
|
+
|
|
707
|
+
</View>
|
|
708
|
+
)
|
|
709
|
+
default: return <></>
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return (
|
|
714
|
+
<View
|
|
715
|
+
style={{ flex: 1 }}
|
|
716
|
+
>
|
|
717
|
+
<View transparent style={{ flex:1 }}>
|
|
718
|
+
<FlatList
|
|
719
|
+
data={sections}
|
|
720
|
+
keyExtractor={item => item}
|
|
721
|
+
key={'notification_manager_list'}
|
|
722
|
+
renderItem={renderSections}
|
|
723
|
+
/>
|
|
724
|
+
</View>
|
|
725
|
+
|
|
726
|
+
{/* Footer with Send Button */}
|
|
727
|
+
<View type='footer' style={{ flexDirection:'row', alignItems:'center', padding:10, borderBottomRightRadius:8, borderBottomLeftRadius:8 }}>
|
|
728
|
+
<Button
|
|
729
|
+
type="action"
|
|
730
|
+
disabled={loading}
|
|
731
|
+
loading={loading}
|
|
732
|
+
onPress={() => confirmSendNotification({ test: true })}
|
|
733
|
+
style={{
|
|
734
|
+
flex:1,
|
|
735
|
+
opacity: loading || isValid().length > 0 ? 0.5 : 1,
|
|
736
|
+
}}
|
|
737
|
+
>
|
|
738
|
+
<Text textAlign='center' theme="h1" color={Colors.text.white}>
|
|
739
|
+
{loading
|
|
740
|
+
? (sendingProgress ? `Sending ${sendingProgress.current}/${sendingProgress.total}` : 'Sending...')
|
|
741
|
+
: 'Send Test'}
|
|
742
|
+
</Text>
|
|
743
|
+
</Button>
|
|
744
|
+
<Button
|
|
745
|
+
type="success"
|
|
746
|
+
disabled={loading}
|
|
747
|
+
loading={loading}
|
|
748
|
+
onPress={() => confirmSendNotification()}
|
|
749
|
+
style={{
|
|
750
|
+
flex:2,
|
|
751
|
+
marginLeft:5,
|
|
752
|
+
opacity: loading || isValid().length > 0 ? 0.5 : 1,
|
|
753
|
+
}}
|
|
754
|
+
>
|
|
755
|
+
<Text textAlign='center' theme="h1" color={Colors.text.white}>
|
|
756
|
+
{loading
|
|
757
|
+
? (sendingProgress ? `Sending batch ${sendingProgress.current} of ${sendingProgress.total}` : 'Sending...')
|
|
758
|
+
: 'Send Notification'}
|
|
759
|
+
</Text>
|
|
760
|
+
</Button>
|
|
761
|
+
</View>
|
|
762
|
+
|
|
763
|
+
{/* Notification Groups Modal */}
|
|
764
|
+
{showGroupsModal && (
|
|
765
|
+
<View type='blur' style={{ position:'absolute', top:0, left:0, right:0, bottom:0, padding:20 }}>
|
|
766
|
+
<View float style={{ flex:1 }}>
|
|
767
|
+
<View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
|
|
768
|
+
<View transparent style={{ flex:1 }}>
|
|
769
|
+
<Text theme='h1'>Notification Groups</Text>
|
|
770
|
+
<Text theme='description' style={{ marginTop:3 }}>Select a group to send to</Text>
|
|
771
|
+
</View>
|
|
772
|
+
</View>
|
|
773
|
+
<View style={{flex:1}}>
|
|
774
|
+
{loadingGroups ? (
|
|
775
|
+
<View style={{ padding: 40, alignItems: 'center' }}>
|
|
776
|
+
<ActivityIndicator size="large" color={Colors.text.action} />
|
|
777
|
+
</View>
|
|
778
|
+
) : (
|
|
779
|
+
<FlatList
|
|
780
|
+
data={customGroup ? [customGroup, ...notificationGroups] : notificationGroups}
|
|
781
|
+
keyExtractor={(item) => item.notification_group_id}
|
|
782
|
+
renderItem={({ item }) => (
|
|
783
|
+
<TouchableOpacity
|
|
784
|
+
onPress={() => handleSelectGroup(item.notification_group_id)}
|
|
785
|
+
style={{
|
|
786
|
+
padding: 15,
|
|
787
|
+
borderBottomWidth: 1,
|
|
788
|
+
borderColor: Colors.borders.light,
|
|
789
|
+
backgroundColor: item.notification_group_id === 'custom_group' ? Colors.views.header : 'transparent'
|
|
790
|
+
}}
|
|
791
|
+
>
|
|
792
|
+
<View transparent>
|
|
793
|
+
<View transparent style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
794
|
+
<View transparent style={{ flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
|
795
|
+
<Text theme="h1">{item.name}</Text>
|
|
796
|
+
{item.notification_group_id === 'custom_group' && (
|
|
797
|
+
<View transparent style={{ marginLeft: 8, paddingHorizontal: 6, paddingVertical: 2, backgroundColor: Colors.buttons.background.action, borderRadius: 4 }}>
|
|
798
|
+
<Text theme="description" color={Colors.text.white} style={{ fontSize: 10 }}>
|
|
799
|
+
CUSTOM
|
|
800
|
+
</Text>
|
|
801
|
+
</View>
|
|
802
|
+
)}
|
|
803
|
+
</View>
|
|
804
|
+
{item.player_count !== undefined && (
|
|
805
|
+
<View transparent style={{ marginLeft: 8, paddingHorizontal: 8, paddingVertical: 4, backgroundColor: Colors.views.header, borderRadius: 4 }}>
|
|
806
|
+
<Text theme="description" style={{ fontSize: 12, fontWeight: '600' }}>
|
|
807
|
+
{item.player_count.toLocaleString()} users
|
|
808
|
+
</Text>
|
|
809
|
+
</View>
|
|
810
|
+
)}
|
|
811
|
+
</View>
|
|
812
|
+
{item.description && (
|
|
813
|
+
<Text theme="description" style={{ marginTop: 4 }}>
|
|
814
|
+
{item.description}
|
|
815
|
+
</Text>
|
|
816
|
+
)}
|
|
817
|
+
</View>
|
|
818
|
+
</TouchableOpacity>
|
|
819
|
+
)}
|
|
820
|
+
ListEmptyComponent={
|
|
821
|
+
<View style={{ padding: 40, alignItems: 'center' }}>
|
|
822
|
+
<Text theme="description">No groups found</Text>
|
|
823
|
+
</View>
|
|
824
|
+
}
|
|
825
|
+
/>
|
|
826
|
+
)}
|
|
827
|
+
</View>
|
|
828
|
+
<View type='footer' style={{ flexDirection:'row', alignItems:'center', padding:10, borderBottomRightRadius:8, borderBottomLeftRadius:8 }}>
|
|
829
|
+
<Button
|
|
830
|
+
style={{ flex:1 }}
|
|
831
|
+
type='close'
|
|
832
|
+
title='CLOSE'
|
|
833
|
+
onPress={() => setShowGroupsModal(false)}
|
|
834
|
+
/>
|
|
835
|
+
</View>
|
|
836
|
+
</View>
|
|
837
|
+
</View>
|
|
838
|
+
)}
|
|
839
|
+
|
|
840
|
+
{/* Edit Custom Group Modal */}
|
|
841
|
+
{showEditCustomGroupModal && (
|
|
842
|
+
<View type='blur' style={{ position:'absolute', top:0, left:0, right:0, bottom:0, padding:20 }}>
|
|
843
|
+
<View float style={{ maxHeight: '80%', minHeight: 400 }}>
|
|
844
|
+
<View type='header' style={{ flexDirection:'row', alignItems:'center', padding:10, borderTopRightRadius:8, borderTopLeftRadius:8 }}>
|
|
845
|
+
<View transparent style={{ flex:1 }}>
|
|
846
|
+
<Text theme='h1'>Edit Custom Recipients</Text>
|
|
847
|
+
<Text theme='description' style={{ marginTop:3 }}>Paste comma-separated player IDs</Text>
|
|
848
|
+
</View>
|
|
849
|
+
</View>
|
|
850
|
+
<View style={{ flex: 1, padding: 15 }}>
|
|
851
|
+
<Text theme="h2" style={{ marginBottom: 10 }}>
|
|
852
|
+
Player IDs (CSV format)
|
|
853
|
+
</Text>
|
|
854
|
+
<TextInput
|
|
855
|
+
value={customGroupCsv}
|
|
856
|
+
onFocusPosition={onFocusPosition}
|
|
857
|
+
placeholder="98,100,2485,etc"
|
|
858
|
+
onChangeText={setCustomGroupCsv}
|
|
859
|
+
multiline
|
|
860
|
+
numberOfLines={10}
|
|
861
|
+
style={{
|
|
862
|
+
padding: 12,
|
|
863
|
+
borderRadius: 8,
|
|
864
|
+
minHeight: 200,
|
|
865
|
+
textAlignVertical: 'top'
|
|
866
|
+
}}
|
|
867
|
+
/>
|
|
868
|
+
<Text theme="description" style={{ marginTop: 8 }}>
|
|
869
|
+
Current count: {customGroupCsv.split(',').filter(id => id.trim().length > 0).length} IDs
|
|
870
|
+
</Text>
|
|
871
|
+
</View>
|
|
872
|
+
<View type='footer' style={{ flexDirection:'row', alignItems:'center', padding:10, borderBottomRightRadius:8, borderBottomLeftRadius:8 }}>
|
|
873
|
+
<Button
|
|
874
|
+
style={{ flex:1, marginRight: 5 }}
|
|
875
|
+
type='close'
|
|
876
|
+
title='CANCEL'
|
|
877
|
+
onPress={() => setShowEditCustomGroupModal(false)}
|
|
878
|
+
/>
|
|
879
|
+
<Button
|
|
880
|
+
style={{ flex:1 }}
|
|
881
|
+
type='success'
|
|
882
|
+
title='SAVE'
|
|
883
|
+
onPress={handleSaveCustomGroup}
|
|
884
|
+
/>
|
|
885
|
+
</View>
|
|
886
|
+
</View>
|
|
887
|
+
</View>
|
|
888
|
+
)}
|
|
889
|
+
</View>
|
|
890
|
+
);
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
export default NotificationManager;
|
|
894
|
+
export { NotificationApi, NotificationHelpers };
|