@messenger-box/slack-ui-browser 10.0.3-alpha.176

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 (61) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +21 -0
  3. package/lib/components/Home/Channels.js +62 -0
  4. package/lib/components/Home/Channels.js.map +1 -0
  5. package/lib/components/Home/DirectChannels.js +92 -0
  6. package/lib/components/Home/DirectChannels.js.map +1 -0
  7. package/lib/components/Home/InviteMembers.js +70 -0
  8. package/lib/components/Home/InviteMembers.js.map +1 -0
  9. package/lib/components/Home/Teams.js +62 -0
  10. package/lib/components/Home/Teams.js.map +1 -0
  11. package/lib/components/Home/TopCommonSlider.js +35 -0
  12. package/lib/components/Home/TopCommonSlider.js.map +1 -0
  13. package/lib/compute.js +223 -0
  14. package/lib/compute.js.map +1 -0
  15. package/lib/constants/routes.js +63 -0
  16. package/lib/constants/routes.js.map +1 -0
  17. package/lib/hooks/useOptimizedChannelsQueries.js +107 -0
  18. package/lib/hooks/useOptimizedChannelsQueries.js.map +1 -0
  19. package/lib/hooks/useRouteState.js +193 -0
  20. package/lib/hooks/useRouteState.js.map +1 -0
  21. package/lib/index.js +1 -0
  22. package/lib/index.js.map +1 -0
  23. package/lib/machines/routeMachine.js +804 -0
  24. package/lib/machines/routeMachine.js.map +1 -0
  25. package/lib/module.js +3 -0
  26. package/lib/module.js.map +1 -0
  27. package/lib/queries/slackuiQueries.js +144 -0
  28. package/lib/queries/slackuiQueries.js.map +1 -0
  29. package/lib/routes.json +260 -0
  30. package/lib/screens/Home/HomeScreen.js +664 -0
  31. package/lib/screens/Home/HomeScreen.js.map +1 -0
  32. package/lib/screens/Home/index.js +1 -0
  33. package/lib/screens/Home/index.js.map +1 -0
  34. package/package.json +52 -0
  35. package/rollup.config.mjs +41 -0
  36. package/src/components/Home/Channels.tsx +135 -0
  37. package/src/components/Home/DirectChannels.tsx +185 -0
  38. package/src/components/Home/InviteMembers.tsx +134 -0
  39. package/src/components/Home/Teams.tsx +129 -0
  40. package/src/components/Home/TopCommonSlider.tsx +70 -0
  41. package/src/components/Home/index.ts +5 -0
  42. package/src/components/index.ts +1 -0
  43. package/src/compute.ts +156 -0
  44. package/src/constants/index.ts +1 -0
  45. package/src/constants/routes.ts +92 -0
  46. package/src/hooks/index.ts +3 -0
  47. package/src/hooks/useOptimizedChannelsQueries.ts +165 -0
  48. package/src/hooks/useRouteState.ts +253 -0
  49. package/src/icons.ts +137 -0
  50. package/src/index.ts +11 -0
  51. package/src/machines/index.ts +9 -0
  52. package/src/machines/routeMachine.ts +682 -0
  53. package/src/module.tsx +6 -0
  54. package/src/queries/index.ts +1 -0
  55. package/src/queries/slackuiQueries.ts +227 -0
  56. package/src/screens/Home/HomeScreen.tsx +1308 -0
  57. package/src/screens/Home/index.ts +4 -0
  58. package/src/screens/NewChannel/NewChannelScreen.tsx +188 -0
  59. package/src/screens/NewChannel/index.ts +2 -0
  60. package/src/screens/index.ts +1 -0
  61. package/tsconfig.json +21 -0
@@ -0,0 +1,1308 @@
1
+ import React, { createContext, useMemo, useState, useCallback, useEffect } from 'react';
2
+ import { useSelector } from 'react-redux';
3
+ import { userSelector } from '@adminide-stack/user-auth0-client';
4
+ import { useLocation, useNavigate, useParams, Outlet } from '@remix-run/react';
5
+ import { RoomType, ApplicationRoles } from 'common';
6
+ import {
7
+ FiEdit, FiHash, FiLock, FiUser, FiX, FiMessageSquare,
8
+ FiUsers, FiUserPlus, FiMail, FiShare2, FiBell, FiBookmark, FiSearch,
9
+ FiAlertCircle, FiCheck
10
+ } from '../../icons';
11
+ import { FiFile } from '@react-icons/all-files/fi/FiFile.js';
12
+ import { TopCommonSlider, Teams, Channels, DirectChannels, InviteMembers } from '../../components/Home';
13
+ import { useOptimizedChannelsQueries } from '../../hooks/useOptimizedChannelsQueries';
14
+ import { useRouteState } from '../../hooks/useRouteState';
15
+ import { slackUiRoutePaths, SLACK_UI_ROUTES } from '../../constants/routes';
16
+ import { RouteState } from '../../machines/routeMachine';
17
+ import {
18
+ useAddChannelMutation,
19
+ useAddDirectChannelMutation,
20
+ useCreateTeamMutation,
21
+ useSendOrganizationInvitationMutation,
22
+ useGetOrganizationMembersQuery,
23
+ useOrganizationSharableLinkQuery,
24
+ useGetOrganizationDetailQuery,
25
+ } from '../../queries/slackuiQueries';
26
+
27
+ // Create a context to control refetching across child components
28
+ export const RefetchContext = createContext({
29
+ shouldRefetch: false,
30
+ setRefetchStatus: (status: boolean) => {},
31
+ });
32
+
33
+ // Create a context to share optimized channel data across child components
34
+ export const ChannelsDataContext = createContext<{
35
+ channelsData: any[];
36
+ directChannelsData: any[];
37
+ loading: boolean;
38
+ error: any;
39
+ refetchChannels: () => void;
40
+ hasChannels: boolean;
41
+ hasDirectChannels: boolean;
42
+ }>({
43
+ channelsData: [],
44
+ directChannelsData: [],
45
+ loading: false,
46
+ error: null,
47
+ refetchChannels: () => {},
48
+ hasChannels: false,
49
+ hasDirectChannels: false,
50
+ });
51
+
52
+ const HomeScreen: React.FC = () => {
53
+ const currentUser: any = useSelector(userSelector);
54
+ const location = useLocation();
55
+ const navigate = useNavigate();
56
+ const [shouldRefetch, setShouldRefetch] = useState(false);
57
+ const orgSlug = location.pathname.split('/')[2];
58
+
59
+ // Use optimized channels queries hook
60
+ const {
61
+ channelsData,
62
+ directChannelsData,
63
+ loading,
64
+ error,
65
+ refetchChannels,
66
+ hasChannels,
67
+ hasDirectChannels,
68
+ } = useOptimizedChannelsQueries({
69
+ orgName: orgSlug,
70
+ enabled: !!orgSlug,
71
+ });
72
+
73
+ // Memoize the refetch context value
74
+ const refetchContextValue = useMemo(
75
+ () => ({
76
+ shouldRefetch,
77
+ setRefetchStatus: setShouldRefetch,
78
+ }),
79
+ [shouldRefetch],
80
+ );
81
+
82
+ // Memoize the channels data context value
83
+ const channelsDataContextValue = useMemo(
84
+ () => ({
85
+ channelsData,
86
+ directChannelsData,
87
+ loading,
88
+ error,
89
+ refetchChannels,
90
+ hasChannels,
91
+ hasDirectChannels,
92
+ }),
93
+ [channelsData, directChannelsData, loading, error, refetchChannels, hasChannels, hasDirectChannels],
94
+ );
95
+
96
+ // Refetch on mount
97
+ useEffect(() => {
98
+ if (orgSlug) {
99
+ refetchChannels();
100
+ }
101
+ }, [orgSlug]);
102
+
103
+ // Navigate to new message/channel creation
104
+ const handleNewMessage = useCallback(() => {
105
+ navigate(slackUiRoutePaths.messageNew(orgSlug));
106
+ }, [navigate, orgSlug]);
107
+
108
+ // Loading state
109
+ if (!currentUser) {
110
+ return (
111
+ <div className="flex items-center justify-center h-full bg-white">
112
+ <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ if (!orgSlug) {
118
+ return (
119
+ <div className="flex items-center justify-center h-full bg-white">
120
+ <p className="text-gray-500">No Organization found</p>
121
+ </div>
122
+ );
123
+ }
124
+
125
+ return (
126
+ <RefetchContext.Provider value={refetchContextValue}>
127
+ <ChannelsDataContext.Provider value={channelsDataContextValue}>
128
+ <div className="flex h-full bg-white">
129
+ {/* Left Sidebar - Channels List */}
130
+ <div className="w-72 flex-shrink-0 border-r border-gray-200 flex flex-col h-full overflow-hidden">
131
+ {/* Sidebar Header */}
132
+ <div className="p-4 border-b border-gray-200">
133
+ <h2 className="text-lg font-semibold text-gray-900 truncate">
134
+ {orgSlug}
135
+ </h2>
136
+ </div>
137
+
138
+ {/* Scrollable Sidebar Content */}
139
+ <div className="flex-1 overflow-y-auto">
140
+ {/* Top Common Slider */}
141
+ <TopCommonSlider />
142
+
143
+ <hr className="border-gray-200 my-2" />
144
+
145
+ {/* Teams Section */}
146
+ <Teams
147
+ teams={[]}
148
+ loading={false}
149
+ error={null}
150
+ onRefetch={() => {}}
151
+ />
152
+
153
+ <hr className="border-gray-200 my-2" />
154
+
155
+ {/* Channels Section */}
156
+ <Channels
157
+ channels={channelsData}
158
+ loading={loading}
159
+ error={error}
160
+ onRefetch={refetchChannels}
161
+ currentUser={currentUser}
162
+ />
163
+
164
+ <hr className="border-gray-200 my-2" />
165
+
166
+ {/* Direct Messages Section */}
167
+ <DirectChannels
168
+ directChannels={directChannelsData}
169
+ loading={loading}
170
+ error={error}
171
+ onRefetch={refetchChannels}
172
+ currentUser={currentUser}
173
+ />
174
+
175
+ <hr className="border-gray-200 my-2" />
176
+
177
+ {/* Invite Members Section */}
178
+ <InviteMembers
179
+ organizationName={orgSlug}
180
+ />
181
+ </div>
182
+
183
+ {/* Floating Action Button in Sidebar */}
184
+ <div className="p-4 border-t border-gray-200">
185
+ <button
186
+ onClick={handleNewMessage}
187
+ className="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
188
+ >
189
+ <FiEdit className="w-4 h-4" />
190
+ <span>New Message</span>
191
+ </button>
192
+ </div>
193
+ </div>
194
+
195
+ {/* Right Panel - Message/Content Area */}
196
+ <div className="flex-1 flex flex-col h-full overflow-hidden">
197
+ <MessagePanel
198
+ orgSlug={orgSlug}
199
+ currentUser={currentUser}
200
+ channelsData={channelsData}
201
+ directChannelsData={directChannelsData}
202
+ />
203
+ </div>
204
+ </div>
205
+ </ChannelsDataContext.Provider>
206
+ </RefetchContext.Provider>
207
+ );
208
+ };
209
+
210
+ // Message Panel Component - handles route-based content
211
+ interface MessagePanelProps {
212
+ orgSlug: string;
213
+ currentUser: any;
214
+ channelsData: any[];
215
+ directChannelsData: any[];
216
+ }
217
+
218
+ const MessagePanel: React.FC<MessagePanelProps> = ({ orgSlug, currentUser, channelsData, directChannelsData }) => {
219
+ // Use XState-based route state management
220
+ const routeState = useRouteState(orgSlug);
221
+
222
+ const {
223
+ currentState,
224
+ channelName,
225
+ teamName,
226
+ isHome,
227
+ isChannelView,
228
+ isChannelNew,
229
+ isMessageView,
230
+ isMessageNew,
231
+ isTeamView,
232
+ isTeamNew,
233
+ isThreads,
234
+ isActivity,
235
+ isDrafts,
236
+ isSaved,
237
+ isFiles,
238
+ isSearch,
239
+ isInvite,
240
+ isInviteContacts,
241
+ isInviteEmail,
242
+ isInviteLink,
243
+ navigateToHome,
244
+ navigateToChannel,
245
+ navigateToChannelNew,
246
+ navigateToMessage,
247
+ navigateToMessageNew,
248
+ } = routeState;
249
+
250
+ // Find the selected channel/dm
251
+ const selectedChannel = useMemo(() => {
252
+ if (isChannelView && channelName) {
253
+ return channelsData.find((ch: any) => ch.title === channelName || ch.name === channelName);
254
+ }
255
+ if (isMessageView && channelName) {
256
+ return directChannelsData.find((dm: any) =>
257
+ dm.title === channelName ||
258
+ dm.name === channelName ||
259
+ dm.displayName?.includes(channelName)
260
+ );
261
+ }
262
+ return null;
263
+ }, [isChannelView, isMessageView, channelName, channelsData, directChannelsData]);
264
+
265
+ // New Channel Screen
266
+ if (isChannelNew) {
267
+ return (
268
+ <NewChannelPanel
269
+ orgSlug={orgSlug}
270
+ onClose={navigateToHome}
271
+ />
272
+ );
273
+ }
274
+
275
+ // New Message Screen
276
+ if (isMessageNew) {
277
+ return (
278
+ <NewMessagePanel
279
+ orgSlug={orgSlug}
280
+ currentUser={currentUser}
281
+ onClose={navigateToHome}
282
+ />
283
+ );
284
+ }
285
+
286
+ // New Team Screen
287
+ if (isTeamNew) {
288
+ return (
289
+ <GenericPanel
290
+ title="Create a Team"
291
+ icon={<FiUsers className="w-12 h-12 text-blue-600" />}
292
+ description="Teams help organize groups of people working together"
293
+ orgSlug={orgSlug}
294
+ onClose={navigateToHome}
295
+ >
296
+ <NewTeamForm orgSlug={orgSlug} onClose={navigateToHome} />
297
+ </GenericPanel>
298
+ );
299
+ }
300
+
301
+ // Team view
302
+ if (isTeamView && teamName) {
303
+ return (
304
+ <GenericPanel
305
+ title={`Team: ${teamName}`}
306
+ icon={<FiUsers className="w-12 h-12 text-blue-600" />}
307
+ description="Team details and members"
308
+ orgSlug={orgSlug}
309
+ />
310
+ );
311
+ }
312
+
313
+ // Threads view
314
+ if (isThreads) {
315
+ return (
316
+ <GenericPanel
317
+ title="Threads"
318
+ icon={<FiMessageSquare className="w-12 h-12 text-blue-600" />}
319
+ description="Your thread replies will appear here"
320
+ orgSlug={orgSlug}
321
+ />
322
+ );
323
+ }
324
+
325
+ // Invite Contacts view
326
+ if (isInviteContacts) {
327
+ return (
328
+ <GenericPanel
329
+ title="Invite from Contacts"
330
+ icon={<FiUserPlus className="w-12 h-12 text-blue-600" />}
331
+ description="Select contacts to invite to your workspace"
332
+ orgSlug={orgSlug}
333
+ onClose={() => routeState.navigateToInvite()}
334
+ >
335
+ <InviteContactsForm orgSlug={orgSlug} />
336
+ </GenericPanel>
337
+ );
338
+ }
339
+
340
+ // Invite Email view
341
+ if (isInviteEmail) {
342
+ return (
343
+ <GenericPanel
344
+ title="Invite by Email"
345
+ icon={<FiMail className="w-12 h-12 text-blue-600" />}
346
+ description="Send email invitations to join your workspace"
347
+ orgSlug={orgSlug}
348
+ onClose={() => routeState.navigateToInvite()}
349
+ >
350
+ <InviteEmailForm orgSlug={orgSlug} />
351
+ </GenericPanel>
352
+ );
353
+ }
354
+
355
+ // Invite Link view
356
+ if (isInviteLink) {
357
+ return (
358
+ <GenericPanel
359
+ title="Invite Link"
360
+ icon={<FiShare2 className="w-12 h-12 text-blue-600" />}
361
+ description="Share this link to invite people to your workspace"
362
+ orgSlug={orgSlug}
363
+ onClose={() => routeState.navigateToInvite()}
364
+ >
365
+ <InviteLinkForm orgSlug={orgSlug} />
366
+ </GenericPanel>
367
+ );
368
+ }
369
+
370
+ // Invite view (main)
371
+ if (isInvite) {
372
+ return (
373
+ <InvitePanel orgSlug={orgSlug} routeState={routeState} />
374
+ );
375
+ }
376
+
377
+ // Activity view
378
+ if (isActivity) {
379
+ return (
380
+ <GenericPanel
381
+ title="Activity"
382
+ icon={<FiBell className="w-12 h-12 text-blue-600" />}
383
+ description="Your recent activity and notifications"
384
+ orgSlug={orgSlug}
385
+ />
386
+ );
387
+ }
388
+
389
+ // Drafts view
390
+ if (isDrafts) {
391
+ return (
392
+ <GenericPanel
393
+ title="Drafts"
394
+ icon={<FiEdit className="w-12 h-12 text-blue-600" />}
395
+ description="Your unsent message drafts"
396
+ orgSlug={orgSlug}
397
+ />
398
+ );
399
+ }
400
+
401
+ // Saved view
402
+ if (isSaved) {
403
+ return (
404
+ <GenericPanel
405
+ title="Saved Items"
406
+ icon={<FiBookmark className="w-12 h-12 text-blue-600" />}
407
+ description="Messages and files you've saved for later"
408
+ orgSlug={orgSlug}
409
+ />
410
+ );
411
+ }
412
+
413
+ // Files view
414
+ if (isFiles) {
415
+ return (
416
+ <GenericPanel
417
+ title="Files"
418
+ icon={<FiFile className="w-12 h-12 text-blue-600" />}
419
+ description="Files shared in your workspace"
420
+ orgSlug={orgSlug}
421
+ />
422
+ );
423
+ }
424
+
425
+ // Search view
426
+ if (isSearch) {
427
+ return (
428
+ <GenericPanel
429
+ title="Search"
430
+ icon={<FiSearch className="w-12 h-12 text-blue-600" />}
431
+ description="Search messages, files, and channels"
432
+ orgSlug={orgSlug}
433
+ >
434
+ <SearchForm orgSlug={orgSlug} />
435
+ </GenericPanel>
436
+ );
437
+ }
438
+
439
+ // Channel or DM selected - show messaging view
440
+ if (selectedChannel) {
441
+ return (
442
+ <ChannelMessagingView
443
+ channel={selectedChannel}
444
+ currentUser={currentUser}
445
+ isDirectMessage={isMessageView}
446
+ />
447
+ );
448
+ }
449
+
450
+ // Default - Welcome screen
451
+ return (
452
+ <div className="flex-1 flex flex-col items-center justify-center text-gray-500 p-8">
453
+ <div className="w-24 h-24 bg-blue-100 rounded-full flex items-center justify-center mb-6">
454
+ <FiMessageSquare className="w-12 h-12 text-blue-600" />
455
+ </div>
456
+ <h3 className="text-xl font-semibold text-gray-900 mb-2">Welcome to {orgSlug}</h3>
457
+ <p className="text-center text-gray-600 mb-6 max-w-md">
458
+ Select a channel or direct message from the sidebar to start chatting,
459
+ or create a new conversation.
460
+ </p>
461
+ <div className="flex gap-3">
462
+ <button
463
+ onClick={navigateToChannelNew}
464
+ className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
465
+ >
466
+ <FiHash className="w-4 h-4" />
467
+ <span>Create Channel</span>
468
+ </button>
469
+ <button
470
+ onClick={navigateToMessageNew}
471
+ className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
472
+ >
473
+ <FiEdit className="w-4 h-4" />
474
+ <span>New Message</span>
475
+ </button>
476
+ </div>
477
+ </div>
478
+ );
479
+ };
480
+
481
+ // New Channel Panel
482
+ interface NewChannelPanelProps {
483
+ orgSlug: string;
484
+ onClose: () => void;
485
+ onSuccess?: () => void;
486
+ }
487
+
488
+ const NewChannelPanel: React.FC<NewChannelPanelProps> = ({ orgSlug, onClose, onSuccess }) => {
489
+ const [channelName, setChannelName] = useState('');
490
+ const [description, setDescription] = useState('');
491
+ const [isPrivate, setIsPrivate] = useState(false);
492
+ const [error, setError] = useState<string | null>(null);
493
+ const navigate = useNavigate();
494
+
495
+ const [createChannel, { loading: isLoading }] = useAddChannelMutation({
496
+ onCompleted: (data: any) => {
497
+ if (data?.createChannel) {
498
+ const createdChannel = data.createChannel;
499
+ // Navigate to the new channel
500
+ navigate(slackUiRoutePaths.channel(orgSlug, createdChannel.title || createdChannel.name));
501
+ onSuccess?.();
502
+ }
503
+ },
504
+ onError: (err: any) => {
505
+ console.error('Error creating channel:', err);
506
+ setError(err.message || 'Failed to create channel. Please try again.');
507
+ },
508
+ });
509
+
510
+ const handleSubmit = async (e: React.FormEvent) => {
511
+ e.preventDefault();
512
+ if (!channelName.trim()) return;
513
+
514
+ setError(null);
515
+
516
+ createChannel({
517
+ variables: {
518
+ name: channelName.toLowerCase().replace(/\s+/g, '-'),
519
+ description: description || undefined,
520
+ type: isPrivate ? RoomType.Private : RoomType.Public,
521
+ },
522
+ });
523
+ };
524
+
525
+ return (
526
+ <div className="flex-1 flex flex-col h-full">
527
+ {/* Header */}
528
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
529
+ <h2 className="text-lg font-semibold text-gray-900">Create a Channel</h2>
530
+ <button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full">
531
+ <FiX className="w-5 h-5 text-gray-500" />
532
+ </button>
533
+ </div>
534
+
535
+ {/* Form */}
536
+ <form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-6 space-y-6">
537
+ {/* Channel Type */}
538
+ <div className="space-y-3">
539
+ <label className="block text-sm font-medium text-gray-700">Channel Type</label>
540
+ <div className="flex gap-4">
541
+ <button
542
+ type="button"
543
+ onClick={() => setIsPrivate(false)}
544
+ className={`flex-1 flex items-center gap-3 p-4 rounded-lg border-2 transition-colors ${
545
+ !isPrivate ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'
546
+ }`}
547
+ >
548
+ <FiHash className={`w-5 h-5 ${!isPrivate ? 'text-blue-600' : 'text-gray-400'}`} />
549
+ <div className="text-left">
550
+ <div className={`font-medium ${!isPrivate ? 'text-blue-700' : 'text-gray-700'}`}>Public</div>
551
+ <div className="text-xs text-gray-500">Anyone can join</div>
552
+ </div>
553
+ </button>
554
+ <button
555
+ type="button"
556
+ onClick={() => setIsPrivate(true)}
557
+ className={`flex-1 flex items-center gap-3 p-4 rounded-lg border-2 transition-colors ${
558
+ isPrivate ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300'
559
+ }`}
560
+ >
561
+ <FiLock className={`w-5 h-5 ${isPrivate ? 'text-blue-600' : 'text-gray-400'}`} />
562
+ <div className="text-left">
563
+ <div className={`font-medium ${isPrivate ? 'text-blue-700' : 'text-gray-700'}`}>Private</div>
564
+ <div className="text-xs text-gray-500">Invite only</div>
565
+ </div>
566
+ </button>
567
+ </div>
568
+ </div>
569
+
570
+ {/* Channel Name */}
571
+ <div className="space-y-2">
572
+ <label className="block text-sm font-medium text-gray-700">Name</label>
573
+ <div className="relative">
574
+ <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
575
+ {isPrivate ? <FiLock className="w-4 h-4" /> : <FiHash className="w-4 h-4" />}
576
+ </span>
577
+ <input
578
+ type="text"
579
+ value={channelName}
580
+ onChange={(e) => setChannelName(e.target.value.toLowerCase().replace(/\s+/g, '-'))}
581
+ placeholder="e.g. marketing"
582
+ className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
583
+ />
584
+ </div>
585
+ </div>
586
+
587
+ {/* Description */}
588
+ <div className="space-y-2">
589
+ <label className="block text-sm font-medium text-gray-700">
590
+ Description <span className="text-gray-400">(optional)</span>
591
+ </label>
592
+ <textarea
593
+ value={description}
594
+ onChange={(e) => setDescription(e.target.value)}
595
+ placeholder="What's this channel about?"
596
+ rows={3}
597
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
598
+ />
599
+ </div>
600
+
601
+ {/* Error Message */}
602
+ {error && (
603
+ <div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
604
+ <FiAlertCircle className="w-5 h-5 flex-shrink-0" />
605
+ <span className="text-sm">{error}</span>
606
+ </div>
607
+ )}
608
+
609
+ {/* Submit */}
610
+ <button
611
+ type="submit"
612
+ disabled={!channelName.trim() || isLoading}
613
+ className="w-full py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
614
+ >
615
+ {isLoading ? 'Creating...' : 'Create Channel'}
616
+ </button>
617
+ </form>
618
+ </div>
619
+ );
620
+ };
621
+
622
+ // New Message Panel
623
+ interface NewMessagePanelProps {
624
+ orgSlug: string;
625
+ currentUser: any;
626
+ onClose: () => void;
627
+ onSuccess?: () => void;
628
+ }
629
+
630
+ const NewMessagePanel: React.FC<NewMessagePanelProps> = ({ orgSlug, currentUser, onClose, onSuccess }) => {
631
+ const [searchQuery, setSearchQuery] = useState('');
632
+ const [selectedUsers, setSelectedUsers] = useState<any[]>([]);
633
+ const [error, setError] = useState<string | null>(null);
634
+ const navigate = useNavigate();
635
+
636
+ // Fetch organization members for user search
637
+ const { data: membersData, loading: membersLoading } = useGetOrganizationMembersQuery({
638
+ variables: { orgName: orgSlug },
639
+ skip: !orgSlug,
640
+ });
641
+
642
+ const [createDirectChannel, { loading: isLoading }] = useAddDirectChannelMutation({
643
+ onCompleted: (data: any) => {
644
+ if (data?.createDirectChannel) {
645
+ const channel = data.createDirectChannel;
646
+ // Navigate to the direct message
647
+ navigate(slackUiRoutePaths.message(orgSlug, channel.title || channel.name || channel.id));
648
+ onSuccess?.();
649
+ }
650
+ },
651
+ onError: (err: any) => {
652
+ console.error('Error creating direct channel:', err);
653
+ setError(err.message || 'Failed to start conversation. Please try again.');
654
+ },
655
+ });
656
+
657
+ // Filter members based on search query
658
+ const filteredMembers = useMemo(() => {
659
+ if (!membersData?.getOrganizationMembers?.data) return [];
660
+ const members = membersData.getOrganizationMembers.data;
661
+
662
+ // Filter out current user and already selected users
663
+ const availableMembers = members.filter((member: any) =>
664
+ member.id !== currentUser?.id &&
665
+ !selectedUsers.some(u => u.id === member.id)
666
+ );
667
+
668
+ if (!searchQuery.trim()) return availableMembers;
669
+
670
+ const query = searchQuery.toLowerCase();
671
+ return availableMembers.filter((member: any) =>
672
+ member.username?.toLowerCase().includes(query) ||
673
+ member.email?.toLowerCase().includes(query) ||
674
+ member.name?.toLowerCase().includes(query)
675
+ );
676
+ }, [membersData, searchQuery, selectedUsers, currentUser]);
677
+
678
+ const handleSelectUser = (user: any) => {
679
+ setSelectedUsers([...selectedUsers, user]);
680
+ setSearchQuery('');
681
+ };
682
+
683
+ const handleStartConversation = () => {
684
+ if (selectedUsers.length === 0) return;
685
+
686
+ setError(null);
687
+
688
+ // Get receiver IDs
689
+ const receiverIds = selectedUsers.map(u => u.id);
690
+
691
+ // Create display name from usernames
692
+ const displayName = selectedUsers.length === 1
693
+ ? selectedUsers[0].username
694
+ : selectedUsers.map(u => u.username).join(', ');
695
+
696
+ createDirectChannel({
697
+ variables: {
698
+ receiver: receiverIds,
699
+ displayName: displayName,
700
+ channelOptions: { schemeAdmin: false },
701
+ },
702
+ });
703
+ };
704
+
705
+ return (
706
+ <div className="flex-1 flex flex-col h-full">
707
+ {/* Header */}
708
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
709
+ <h2 className="text-lg font-semibold text-gray-900">New Message</h2>
710
+ <button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full">
711
+ <FiX className="w-5 h-5 text-gray-500" />
712
+ </button>
713
+ </div>
714
+
715
+ {/* To Field */}
716
+ <div className="px-6 py-4 border-b border-gray-200">
717
+ <div className="flex items-center gap-2">
718
+ <span className="text-gray-500">To:</span>
719
+ <div className="flex-1 flex flex-wrap gap-2">
720
+ {selectedUsers.map((user, idx) => (
721
+ <span key={idx} className="inline-flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 rounded-full text-sm">
722
+ {user.username || user.email}
723
+ <button onClick={() => setSelectedUsers(selectedUsers.filter((_, i) => i !== idx))}>
724
+ <FiX className="w-3 h-3" />
725
+ </button>
726
+ </span>
727
+ ))}
728
+ <input
729
+ type="text"
730
+ value={searchQuery}
731
+ onChange={(e) => setSearchQuery(e.target.value)}
732
+ placeholder="Search for people..."
733
+ className="flex-1 min-w-[200px] outline-none"
734
+ />
735
+ </div>
736
+ </div>
737
+ </div>
738
+
739
+ {/* User Search Results */}
740
+ <div className="flex-1 overflow-y-auto p-6">
741
+ {membersLoading ? (
742
+ <div className="flex items-center justify-center py-8">
743
+ <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
744
+ </div>
745
+ ) : filteredMembers.length > 0 ? (
746
+ <div className="space-y-2">
747
+ {filteredMembers.map((member: any) => (
748
+ <button
749
+ key={member.id}
750
+ onClick={() => handleSelectUser(member)}
751
+ className="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100 transition-colors text-left"
752
+ >
753
+ <div className="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center">
754
+ <FiUser className="w-5 h-5 text-gray-600" />
755
+ </div>
756
+ <div>
757
+ <div className="font-medium text-gray-900">{member.username || member.name}</div>
758
+ {member.email && (
759
+ <div className="text-sm text-gray-500">{member.email}</div>
760
+ )}
761
+ </div>
762
+ </button>
763
+ ))}
764
+ </div>
765
+ ) : searchQuery ? (
766
+ <div className="text-center text-gray-500 py-8">
767
+ <FiUser className="w-12 h-12 mx-auto mb-3 text-gray-300" />
768
+ <p>No members found matching "{searchQuery}"</p>
769
+ </div>
770
+ ) : (
771
+ <div className="text-center text-gray-500 py-8">
772
+ <FiUser className="w-12 h-12 mx-auto mb-3 text-gray-300" />
773
+ <p>Search for people to start a conversation</p>
774
+ </div>
775
+ )}
776
+
777
+ {/* Error Message */}
778
+ {error && (
779
+ <div className="mt-4 flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
780
+ <FiAlertCircle className="w-5 h-5 flex-shrink-0" />
781
+ <span className="text-sm">{error}</span>
782
+ </div>
783
+ )}
784
+ </div>
785
+
786
+ {/* Start Button */}
787
+ {selectedUsers.length > 0 && (
788
+ <div className="p-4 border-t border-gray-200">
789
+ <button
790
+ onClick={handleStartConversation}
791
+ disabled={isLoading}
792
+ className={`w-full py-3 rounded-lg font-medium transition-colors ${
793
+ isLoading
794
+ ? 'bg-gray-400 text-gray-200 cursor-not-allowed'
795
+ : 'bg-blue-600 text-white hover:bg-blue-700'
796
+ }`}
797
+ >
798
+ {isLoading ? 'Creating...' : 'Start Conversation'}
799
+ </button>
800
+ </div>
801
+ )}
802
+ </div>
803
+ );
804
+ };
805
+
806
+ // Generic Panel Component - reusable panel with header
807
+ interface GenericPanelProps {
808
+ title: string;
809
+ icon: React.ReactNode;
810
+ description: string;
811
+ orgSlug: string;
812
+ onClose?: () => void;
813
+ children?: React.ReactNode;
814
+ }
815
+
816
+ const GenericPanel: React.FC<GenericPanelProps> = ({ title, icon, description, orgSlug, onClose, children }) => {
817
+ return (
818
+ <div className="flex-1 flex flex-col h-full">
819
+ {/* Header */}
820
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
821
+ <h2 className="text-lg font-semibold text-gray-900">{title}</h2>
822
+ {onClose && (
823
+ <button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full">
824
+ <FiX className="w-5 h-5 text-gray-500" />
825
+ </button>
826
+ )}
827
+ </div>
828
+
829
+ {/* Content */}
830
+ <div className="flex-1 overflow-y-auto p-6">
831
+ {children ? (
832
+ children
833
+ ) : (
834
+ <div className="flex flex-col items-center justify-center h-full text-center">
835
+ <div className="w-24 h-24 bg-blue-100 rounded-full flex items-center justify-center mb-6">
836
+ {icon}
837
+ </div>
838
+ <h3 className="text-xl font-semibold text-gray-900 mb-2">{title}</h3>
839
+ <p className="text-gray-600 max-w-md">{description}</p>
840
+ </div>
841
+ )}
842
+ </div>
843
+ </div>
844
+ );
845
+ };
846
+
847
+ // Invite Panel Component
848
+ interface InvitePanelProps {
849
+ orgSlug: string;
850
+ routeState: ReturnType<typeof useRouteState>;
851
+ }
852
+
853
+ const InvitePanel: React.FC<InvitePanelProps> = ({ orgSlug, routeState }) => {
854
+ return (
855
+ <div className="flex-1 flex flex-col h-full">
856
+ {/* Header */}
857
+ <div className="flex items-center px-6 py-4 border-b border-gray-200">
858
+ <h2 className="text-lg font-semibold text-gray-900">Invite People</h2>
859
+ </div>
860
+
861
+ {/* Content */}
862
+ <div className="flex-1 overflow-y-auto p-6">
863
+ <div className="max-w-md mx-auto space-y-4">
864
+ <p className="text-gray-600 text-center mb-6">
865
+ Choose how you'd like to invite people to {orgSlug}
866
+ </p>
867
+
868
+ <button
869
+ onClick={() => routeState.navigateToInviteContacts()}
870
+ className="w-full flex items-center gap-4 p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
871
+ >
872
+ <div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
873
+ <FiUserPlus className="w-5 h-5 text-blue-600" />
874
+ </div>
875
+ <div className="text-left">
876
+ <div className="font-medium text-gray-900">From Contacts</div>
877
+ <div className="text-sm text-gray-500">Invite from your contact list</div>
878
+ </div>
879
+ </button>
880
+
881
+ <button
882
+ onClick={() => routeState.navigateToInviteEmail()}
883
+ className="w-full flex items-center gap-4 p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
884
+ >
885
+ <div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
886
+ <FiMail className="w-5 h-5 text-green-600" />
887
+ </div>
888
+ <div className="text-left">
889
+ <div className="font-medium text-gray-900">By Email</div>
890
+ <div className="text-sm text-gray-500">Send email invitations</div>
891
+ </div>
892
+ </button>
893
+
894
+ <button
895
+ onClick={() => routeState.navigateToInviteLink()}
896
+ className="w-full flex items-center gap-4 p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
897
+ >
898
+ <div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
899
+ <FiShare2 className="w-5 h-5 text-purple-600" />
900
+ </div>
901
+ <div className="text-left">
902
+ <div className="font-medium text-gray-900">Share Link</div>
903
+ <div className="text-sm text-gray-500">Copy and share an invite link</div>
904
+ </div>
905
+ </button>
906
+ </div>
907
+ </div>
908
+ </div>
909
+ );
910
+ };
911
+
912
+ // New Team Form Component
913
+ interface NewTeamFormProps {
914
+ orgSlug: string;
915
+ onClose: () => void;
916
+ }
917
+
918
+ const NewTeamForm: React.FC<NewTeamFormProps> = ({ orgSlug, onClose }) => {
919
+ const [teamName, setTeamName] = useState('');
920
+ const [description, setDescription] = useState('');
921
+ const [formError, setFormError] = useState<string | null>(null);
922
+ const navigate = useNavigate();
923
+
924
+ const [createTeam, { loading, error }] = useCreateTeamMutation({
925
+ context: {
926
+ headers: {
927
+ orgname: orgSlug,
928
+ },
929
+ },
930
+ onCompleted: (data) => {
931
+ if (data?.createTeam) {
932
+ const teamSlug = data.createTeam.name || teamName.toLowerCase().replace(/\s+/g, '-');
933
+ navigate(slackUiRoutePaths.team(orgSlug, teamSlug));
934
+ }
935
+ },
936
+ onError: (err) => {
937
+ console.error('Error creating team:', err);
938
+ setFormError(err.message || 'Failed to create team. Please try again.');
939
+ },
940
+ });
941
+
942
+ const handleSubmit = async (e: React.FormEvent) => {
943
+ e.preventDefault();
944
+ if (!teamName.trim()) return;
945
+
946
+ setFormError(null);
947
+ createTeam({
948
+ variables: {
949
+ orgName: orgSlug,
950
+ request: {
951
+ name: teamName.trim().toLowerCase().replace(/\s+/g, '-'),
952
+ title: teamName.trim(),
953
+ description: description.trim() || undefined,
954
+ },
955
+ },
956
+ });
957
+ };
958
+
959
+ return (
960
+ <form onSubmit={handleSubmit} className="space-y-6">
961
+ <div className="space-y-2">
962
+ <label className="block text-sm font-medium text-gray-700">Team Name</label>
963
+ <input
964
+ type="text"
965
+ value={teamName}
966
+ onChange={(e) => setTeamName(e.target.value)}
967
+ placeholder="e.g. Engineering"
968
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
969
+ />
970
+ </div>
971
+
972
+ <div className="space-y-2">
973
+ <label className="block text-sm font-medium text-gray-700">
974
+ Description <span className="text-gray-400">(optional)</span>
975
+ </label>
976
+ <textarea
977
+ value={description}
978
+ onChange={(e) => setDescription(e.target.value)}
979
+ placeholder="What's this team about?"
980
+ rows={3}
981
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
982
+ />
983
+ </div>
984
+
985
+ {/* Error Message */}
986
+ {(formError || error) && (
987
+ <div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
988
+ <FiAlertCircle className="w-5 h-5 flex-shrink-0" />
989
+ <span className="text-sm">{formError || error?.message || 'An error occurred'}</span>
990
+ </div>
991
+ )}
992
+
993
+ <button
994
+ type="submit"
995
+ disabled={!teamName.trim() || loading}
996
+ className={`w-full py-3 rounded-lg font-medium transition-colors ${
997
+ loading
998
+ ? 'bg-gray-400 text-gray-200 cursor-not-allowed'
999
+ : 'bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed'
1000
+ }`}
1001
+ >
1002
+ {loading ? 'Creating...' : 'Create Team'}
1003
+ </button>
1004
+ </form>
1005
+ );
1006
+ };
1007
+
1008
+ // Invite Contacts Form Component
1009
+ interface InviteContactsFormProps {
1010
+ orgSlug: string;
1011
+ }
1012
+
1013
+ const InviteContactsForm: React.FC<InviteContactsFormProps> = ({ orgSlug }) => {
1014
+ const [searchQuery, setSearchQuery] = useState('');
1015
+
1016
+ return (
1017
+ <div className="space-y-4">
1018
+ <input
1019
+ type="text"
1020
+ value={searchQuery}
1021
+ onChange={(e) => setSearchQuery(e.target.value)}
1022
+ placeholder="Search contacts..."
1023
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1024
+ />
1025
+ <div className="text-center text-gray-500 py-8">
1026
+ <FiUsers className="w-12 h-12 mx-auto mb-3 text-gray-300" />
1027
+ <p>Search for contacts to invite</p>
1028
+ </div>
1029
+ </div>
1030
+ );
1031
+ };
1032
+
1033
+ // Invite Email Form Component
1034
+ interface InviteEmailFormProps {
1035
+ orgSlug: string;
1036
+ }
1037
+
1038
+ const InviteEmailForm: React.FC<InviteEmailFormProps> = ({ orgSlug }) => {
1039
+ const [email, setEmail] = useState('');
1040
+ const [emails, setEmails] = useState<string[]>([]);
1041
+ const [formError, setFormError] = useState<string | null>(null);
1042
+ const [successMessage, setSuccessMessage] = useState<string | null>(null);
1043
+
1044
+ // Fetch org details to get the org ID
1045
+ const { data: orgData } = useGetOrganizationDetailQuery({
1046
+ variables: { where: { name: orgSlug } },
1047
+ skip: !orgSlug,
1048
+ });
1049
+ const orgId = orgData?.getOrganizationDetail?.id;
1050
+
1051
+ const [sendInvitation, { loading, error }] = useSendOrganizationInvitationMutation({
1052
+ onCompleted: (data) => {
1053
+ if (data?.sendOrganizationInvitation) {
1054
+ setSuccessMessage(`Invitation${emails.length > 1 ? 's' : ''} sent successfully!`);
1055
+ setEmails([]);
1056
+ setEmail('');
1057
+ // Clear success message after 3 seconds
1058
+ setTimeout(() => setSuccessMessage(null), 3000);
1059
+ }
1060
+ },
1061
+ onError: (err) => {
1062
+ console.error('Error sending invitation:', err);
1063
+ setFormError(err.message || 'Failed to send invitation. Please try again.');
1064
+ },
1065
+ });
1066
+
1067
+ const handleAddEmail = () => {
1068
+ const trimmedEmail = email.trim().toLowerCase();
1069
+ if (trimmedEmail && !emails.includes(trimmedEmail) && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) {
1070
+ setEmails([...emails, trimmedEmail]);
1071
+ setEmail('');
1072
+ setFormError(null);
1073
+ } else if (trimmedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmedEmail)) {
1074
+ setFormError('Please enter a valid email address');
1075
+ }
1076
+ };
1077
+
1078
+ const handleSendInvitations = async () => {
1079
+ if (emails.length === 0) return;
1080
+
1081
+ if (!orgId) {
1082
+ setFormError('Organization not found. Please try again.');
1083
+ return;
1084
+ }
1085
+
1086
+ setFormError(null);
1087
+ setSuccessMessage(null);
1088
+
1089
+ // Send invitations for each email
1090
+ for (const emailAddr of emails) {
1091
+ await sendInvitation({
1092
+ variables: {
1093
+ request: {
1094
+ email: emailAddr,
1095
+ orgId: orgId,
1096
+ orgName: orgSlug,
1097
+ role: ApplicationRoles.Member,
1098
+ },
1099
+ },
1100
+ });
1101
+ }
1102
+ };
1103
+
1104
+ return (
1105
+ <div className="space-y-4">
1106
+ <div className="flex gap-2">
1107
+ <input
1108
+ type="email"
1109
+ value={email}
1110
+ onChange={(e) => setEmail(e.target.value)}
1111
+ placeholder="Enter email address"
1112
+ className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1113
+ onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleAddEmail())}
1114
+ />
1115
+ <button
1116
+ type="button"
1117
+ onClick={handleAddEmail}
1118
+ className="px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1119
+ >
1120
+ Add
1121
+ </button>
1122
+ </div>
1123
+
1124
+ {emails.length > 0 && (
1125
+ <div className="flex flex-wrap gap-2">
1126
+ {emails.map((e, idx) => (
1127
+ <span key={idx} className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm">
1128
+ {e}
1129
+ <button onClick={() => setEmails(emails.filter((_, i) => i !== idx))}>
1130
+ <FiX className="w-3 h-3" />
1131
+ </button>
1132
+ </span>
1133
+ ))}
1134
+ </div>
1135
+ )}
1136
+
1137
+ {/* Error Message */}
1138
+ {(formError || error) && (
1139
+ <div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700">
1140
+ <FiAlertCircle className="w-5 h-5 flex-shrink-0" />
1141
+ <span className="text-sm">{formError || error?.message || 'An error occurred'}</span>
1142
+ </div>
1143
+ )}
1144
+
1145
+ {/* Success Message */}
1146
+ {successMessage && (
1147
+ <div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700">
1148
+ <FiCheck className="w-5 h-5 flex-shrink-0" />
1149
+ <span className="text-sm">{successMessage}</span>
1150
+ </div>
1151
+ )}
1152
+
1153
+ <button
1154
+ type="button"
1155
+ onClick={handleSendInvitations}
1156
+ disabled={emails.length === 0 || loading}
1157
+ className={`w-full py-3 rounded-lg font-medium transition-colors ${
1158
+ loading
1159
+ ? 'bg-gray-400 text-gray-200 cursor-not-allowed'
1160
+ : 'bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed'
1161
+ }`}
1162
+ >
1163
+ {loading ? 'Sending...' : 'Send Invitations'}
1164
+ </button>
1165
+ </div>
1166
+ );
1167
+ };
1168
+
1169
+ // Invite Link Form Component
1170
+ interface InviteLinkFormProps {
1171
+ orgSlug: string;
1172
+ }
1173
+
1174
+ const InviteLinkForm: React.FC<InviteLinkFormProps> = ({ orgSlug }) => {
1175
+ const inviteLink = `${window.location.origin}/o/${orgSlug}/join`;
1176
+ const [copied, setCopied] = useState(false);
1177
+
1178
+ const handleCopy = () => {
1179
+ navigator.clipboard.writeText(inviteLink);
1180
+ setCopied(true);
1181
+ setTimeout(() => setCopied(false), 2000);
1182
+ };
1183
+
1184
+ return (
1185
+ <div className="space-y-4">
1186
+ <p className="text-gray-600 text-sm">
1187
+ Share this link with anyone you'd like to invite to {orgSlug}
1188
+ </p>
1189
+
1190
+ <div className="flex gap-2">
1191
+ <input
1192
+ type="text"
1193
+ value={inviteLink}
1194
+ readOnly
1195
+ className="flex-1 px-4 py-3 border border-gray-300 rounded-lg bg-gray-50 text-gray-600"
1196
+ />
1197
+ <button
1198
+ type="button"
1199
+ onClick={handleCopy}
1200
+ className="px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
1201
+ >
1202
+ {copied ? 'Copied!' : 'Copy'}
1203
+ </button>
1204
+ </div>
1205
+ </div>
1206
+ );
1207
+ };
1208
+
1209
+ // Search Form Component
1210
+ interface SearchFormProps {
1211
+ orgSlug: string;
1212
+ }
1213
+
1214
+ const SearchForm: React.FC<SearchFormProps> = ({ orgSlug }) => {
1215
+ const [searchQuery, setSearchQuery] = useState('');
1216
+
1217
+ return (
1218
+ <div className="space-y-4">
1219
+ <input
1220
+ type="text"
1221
+ value={searchQuery}
1222
+ onChange={(e) => setSearchQuery(e.target.value)}
1223
+ placeholder="Search messages, files, and channels..."
1224
+ className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1225
+ />
1226
+ <div className="text-center text-gray-500 py-8">
1227
+ <FiSearch className="w-12 h-12 mx-auto mb-3 text-gray-300" />
1228
+ <p>Enter a search term to find messages, files, and channels</p>
1229
+ </div>
1230
+ </div>
1231
+ );
1232
+ };
1233
+
1234
+ // Channel Messaging View
1235
+ interface ChannelMessagingViewProps {
1236
+ channel: any;
1237
+ currentUser: any;
1238
+ isDirectMessage: boolean;
1239
+ }
1240
+
1241
+ const ChannelMessagingView: React.FC<ChannelMessagingViewProps> = ({ channel, currentUser, isDirectMessage }) => {
1242
+ const [message, setMessage] = useState('');
1243
+
1244
+ const handleSendMessage = (e: React.FormEvent) => {
1245
+ e.preventDefault();
1246
+ if (!message.trim()) return;
1247
+ // TODO: Call sendMessage mutation
1248
+ console.log('Sending message:', message);
1249
+ setMessage('');
1250
+ };
1251
+
1252
+ const channelTitle = channel?.title || channel?.name || channel?.displayName || 'Channel';
1253
+
1254
+ return (
1255
+ <div className="flex-1 flex flex-col h-full">
1256
+ {/* Channel Header */}
1257
+ <div className="flex items-center px-6 py-4 border-b border-gray-200">
1258
+ <div className="flex items-center gap-3">
1259
+ {isDirectMessage ? (
1260
+ <div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center">
1261
+ <FiUser className="w-4 h-4 text-gray-600" />
1262
+ </div>
1263
+ ) : (
1264
+ <span className="text-gray-400">
1265
+ {channel?.type === 'PRIVATE' ? <FiLock className="w-5 h-5" /> : <FiHash className="w-5 h-5" />}
1266
+ </span>
1267
+ )}
1268
+ <div>
1269
+ <h2 className="font-semibold text-gray-900">{channelTitle}</h2>
1270
+ {channel?.description && (
1271
+ <p className="text-sm text-gray-500 truncate">{channel.description}</p>
1272
+ )}
1273
+ </div>
1274
+ </div>
1275
+ </div>
1276
+
1277
+ {/* Messages Area */}
1278
+ <div className="flex-1 overflow-y-auto p-6">
1279
+ <div className="text-center text-gray-500 py-8">
1280
+ <p className="text-sm">This is the start of your conversation in <strong>{channelTitle}</strong></p>
1281
+ </div>
1282
+ {/* Messages will be rendered here */}
1283
+ </div>
1284
+
1285
+ {/* Message Input */}
1286
+ <form onSubmit={handleSendMessage} className="p-4 border-t border-gray-200">
1287
+ <div className="flex items-center gap-3">
1288
+ <input
1289
+ type="text"
1290
+ value={message}
1291
+ onChange={(e) => setMessage(e.target.value)}
1292
+ placeholder={`Message ${isDirectMessage ? '' : '#'}${channelTitle}`}
1293
+ className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
1294
+ />
1295
+ <button
1296
+ type="submit"
1297
+ disabled={!message.trim()}
1298
+ className="px-6 py-3 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
1299
+ >
1300
+ Send
1301
+ </button>
1302
+ </div>
1303
+ </form>
1304
+ </div>
1305
+ );
1306
+ };
1307
+
1308
+ export default React.memo(HomeScreen);