1
- import React from 'react' ;
1
+ import React , { useCallback , useEffect , useMemo , useState } from 'react' ;
2
2
import styled from 'styled-components' ;
3
3
import {
4
4
CordContext ,
5
5
Avatar as DefaultAvatar ,
6
6
presence ,
7
+ user ,
7
8
} from '@cord-sdk/react' ;
9
+ import { UserPlusIcon } from '@heroicons/react/24/outline' ;
10
+ import { LockClosedIcon , HashtagIcon } from '@heroicons/react/24/solid' ;
8
11
import type { ClientUserData } from '@cord-sdk/types' ;
9
12
import type { Channel } from 'src/client/consts/Channel' ;
10
13
import { Colors } from 'src/client/consts/Colors' ;
11
14
import { ActiveBadge } from 'src/client/components/ActiveBadge' ;
12
15
import { Name } from 'src/client/components/Name' ;
13
16
import { XIcon } from 'src/client/components/Buttons' ;
17
+ import { useAPIUpdateFetch } from 'src/client/hooks/useAPIFetch' ;
18
+ import { EVERYONE_ORG_ID } from 'src/client/consts/consts' ;
14
19
15
- interface UsersInChannelModalProps {
20
+ type UsersInChannelModalProps = {
16
21
onClose : ( ) => void ;
17
22
channel : Channel ;
18
23
users : ClientUserData [ ] ;
19
- }
24
+ } ;
20
25
21
26
export function UsersInChannelModal ( {
22
27
onClose,
@@ -27,41 +32,289 @@ export function UsersInChannelModal({
27
32
{ page : 'clack' } ,
28
33
{ exclude_durable : true , partial_match : true } ,
29
34
) ;
35
+ const [ showAddUsersModal , setShowAddUsersModal ] = React . useState ( false ) ;
36
+
37
+ return (
38
+ < >
39
+ < Modal $order = { 1 } >
40
+ < Box >
41
+ < Header >
42
+ < Heading >
43
+ { channel . org ? (
44
+ < LockClosedIcon
45
+ width = { 20 }
46
+ style = { { padding : '1px' , marginRight : '2px' } }
47
+ />
48
+ ) : (
49
+ < HashtagIcon width = { 20 } style = { { padding : '1px' } } />
50
+ ) }
51
+ { channel . id }
52
+ </ Heading >
53
+ < CloseButton onClick = { onClose } >
54
+ < XIcon />
55
+ </ CloseButton >
56
+ </ Header >
57
+ < UsersList >
58
+ { /* Show the Add People modal option if this is a private org
59
+ (public channels have an undefined channel.org, and are visible to
60
+ everyone in the clack_all org) */ }
61
+ { channel . org && (
62
+ < UserDetails onClick = { ( ) => setShowAddUsersModal ( true ) } >
63
+ < AddPeopleIconWrapper >
64
+ < UserPlusIcon
65
+ width = { 32 }
66
+ height = { 32 }
67
+ style = { {
68
+ backgroundColor : '#e8f5fa' ,
69
+ color : 'rgba(18,100,163,1)' ,
70
+ } }
71
+ />
72
+ </ AddPeopleIconWrapper >
73
+ < Name $variant = "main" > Add people</ Name >
74
+ </ UserDetails >
75
+ ) }
76
+ { users . map ( ( user ) => {
77
+ const isUserPresent = usersPresent ?. some (
78
+ ( presence ) => presence . id === user . id ,
79
+ ) ;
80
+ return (
81
+ < UserRow
82
+ key = { user . id }
83
+ org = { channel . org }
84
+ isUserPresent = { ! ! isUserPresent }
85
+ user = { user }
86
+ />
87
+ ) ;
88
+ } ) }
89
+ </ UsersList >
90
+ </ Box >
91
+ </ Modal >
92
+ { showAddUsersModal && (
93
+ < AddUsersToChannelModal
94
+ channel = { channel }
95
+ onClose = { ( ) => setShowAddUsersModal ( false ) }
96
+ existingUsers = { users . map ( ( u ) => u . id ) }
97
+ />
98
+ ) }
99
+ </ >
100
+ ) ;
101
+ }
102
+
103
+ function UserRow ( {
104
+ org,
105
+ isUserPresent,
106
+ user,
107
+ } : {
108
+ org : string | undefined ;
109
+ isUserPresent : boolean ;
110
+ user : ClientUserData ;
111
+ } ) {
30
112
const { userID : cordUserID } = React . useContext ( CordContext ) ;
113
+ const [ showDelete , setShowDelete ] = useState ( false ) ;
114
+
115
+ const update = useAPIUpdateFetch ( ) ;
31
116
32
117
return (
33
- < Modal >
118
+ < >
119
+ < UserDetails
120
+ key = { user . id }
121
+ onMouseEnter = { ( ) => setShowDelete ( true ) }
122
+ onMouseLeave = { ( ) => setShowDelete ( false ) }
123
+ >
124
+ < Avatar userId = { user . id } enableTooltip />
125
+ { /* //todo: fill short name values in db console? */ }
126
+ < Name $variant = "main" >
127
+ { user . shortName || user . name }
128
+ { cordUserID === user . id ? ' (you)' : '' }
129
+ </ Name >
130
+ < ActiveBadge $isActive = { isUserPresent } />
131
+ < Name $variant = "simple" > { user ?. name } </ Name >
132
+ { showDelete && org && (
133
+ // TODO: the org members API currently doesn't have subscriptions, so
134
+ // it looks like nothing's happened in the FE atm
135
+ < DeleteButton
136
+ onClick = { ( ) => {
137
+ void update ( `/channels/${ org } ` , 'DELETE' , {
138
+ userIDs : [ user . id ] ,
139
+ } ) ;
140
+ } }
141
+ >
142
+ Remove
143
+ </ DeleteButton >
144
+ ) }
145
+ </ UserDetails >
146
+ </ >
147
+ ) ;
148
+ }
149
+
150
+ type AddUsersToChannelModalProps = {
151
+ onClose : ( ) => void ;
152
+ channel : Channel ;
153
+ existingUsers : string [ ] ;
154
+ } ;
155
+
156
+ function AddUsersToChannelModal ( {
157
+ onClose,
158
+ existingUsers,
159
+ channel,
160
+ } : AddUsersToChannelModalProps ) {
161
+ const {
162
+ orgMembers : allOrgMembers ,
163
+ loading,
164
+ hasMore,
165
+ fetchMore,
166
+ } = user . useOrgMembers ( {
167
+ organizationID : EVERYONE_ORG_ID ,
168
+ } ) ;
169
+
170
+ useEffect ( ( ) => {
171
+ if ( ! loading && hasMore ) {
172
+ void fetchMore ( 50 ) ;
173
+ }
174
+ } , [ hasMore , loading , fetchMore ] ) ;
175
+
176
+ const addableUsers = useMemo ( ( ) => {
177
+ return allOrgMembers
178
+ . filter ( ( om ) => ! existingUsers . includes ( om . id ) )
179
+ . sort (
180
+ ( a , b ) =>
181
+ ( a . shortName ?? a . name ?? 'Unknown' ) ?. localeCompare (
182
+ b . shortName ?? b . name ?? 'Unknown' ,
183
+ ) ,
184
+ ) ;
185
+ } , [ allOrgMembers , existingUsers ] ) ;
186
+
187
+ const [ usersToAdd , setUsersToAdd ] = useState < string [ ] > ( [ ] ) ;
188
+
189
+ const update = useAPIUpdateFetch ( ) ;
190
+
191
+ // TODO: the org members API currently doesn't have subscriptions, so
192
+ // it looks like nothing's happened in the FE atm
193
+ const addUsers = useCallback ( ( ) => {
194
+ void update ( `/channels/${ channel . org } ` , 'PUT' , {
195
+ userIDs : usersToAdd ,
196
+ } ) . then ( ( ) => onClose ( ) ) ;
197
+ } , [ channel . org , onClose , update , usersToAdd ] ) ;
198
+
199
+ return (
200
+ // This is a modal stacked on top of another modal
201
+ < Modal $order = { 2 } >
34
202
< Box >
35
203
< Header >
36
- < Heading > # { channel . id } </ Heading >
204
+ < Heading >
205
+ Add people to{ ' ' }
206
+ { channel . org ? (
207
+ < LockClosedIcon
208
+ width = { 20 }
209
+ style = { { padding : '1px' , marginRight : '2px' } }
210
+ />
211
+ ) : (
212
+ < HashtagIcon width = { 20 } style = { { padding : '1px' } } />
213
+ ) } { ' ' }
214
+ { channel . id }
215
+ </ Heading >
37
216
< CloseButton onClick = { onClose } >
38
217
< XIcon />
39
218
</ CloseButton >
40
219
</ Header >
41
-
42
220
< UsersList >
43
- { users . map ( ( user ) => {
44
- const isUserPresent = usersPresent ?. some (
45
- ( presence ) => presence . id === user . id ,
46
- ) ;
221
+ { addableUsers . map ( ( user ) => {
47
222
return (
48
- < UserDetails key = { user . id } >
49
- < Avatar userId = { user . id } enableTooltip />
50
- { /* //todo: fill short name values in db console? */ }
51
- < Name $variant = "main" >
52
- { user . shortName || user . name }
53
- { cordUserID === user . id ? ' (you)' : '' }
54
- </ Name >
55
- < ActiveBadge $isActive = { ! ! isUserPresent } />
56
- < Name $variant = "simple" > { user ?. name } </ Name >
57
- </ UserDetails >
223
+ < Label key = { user . id } >
224
+ < UserDetails >
225
+ < Checkbox
226
+ id = { `add-${ user . id } ` }
227
+ type = "checkbox"
228
+ value = { user . id }
229
+ onChange = { ( e ) => {
230
+ if ( e . target . checked ) {
231
+ setUsersToAdd ( ( prevState ) => [ ...prevState , user . id ] ) ;
232
+ } else {
233
+ setUsersToAdd ( ( prevState ) =>
234
+ prevState . filter ( ( u ) => u !== user . id ) ,
235
+ ) ;
236
+ }
237
+ } }
238
+ />
239
+ < Avatar userId = { user . id } enableTooltip />
240
+ < Name $variant = "main" > { user . shortName || user . name } </ Name >
241
+ < Name $variant = "simple" > { user . name } </ Name >
242
+ </ UserDetails >
243
+ </ Label >
58
244
) ;
59
245
} ) }
60
246
</ UsersList >
247
+ < Footer >
248
+ < AddButton onClick = { addUsers } > Add</ AddButton >
249
+ </ Footer >
61
250
</ Box >
62
251
</ Modal >
63
252
) ;
64
253
}
254
+ const AddButton = styled . button ( {
255
+ border : 'none' ,
256
+ borderRadius : '4px' ,
257
+ backgroundColor : '#007a5a' ,
258
+ color : '#ffffff' ,
259
+ padding : '0 12px 1px' ,
260
+ fontSize : '15px' ,
261
+ height : '36px' ,
262
+ minWidth : '80px' ,
263
+ boxShadow : 'none' ,
264
+ fontWeight : '700' ,
265
+ transition : 'all 80ms linear' ,
266
+ cursor : 'pointer' ,
267
+ '&:hover' : {
268
+ background : '#148567' ,
269
+ boxShadow : '0 1px 4px #0000004d' ,
270
+ } ,
271
+ } ) ;
272
+
273
+ const DeleteButton = styled . button ( {
274
+ backgroundColor : 'rgba(224,30,90)' ,
275
+ border : 'none' ,
276
+ borderRadius : '4px' ,
277
+ boxShadow : 'none' ,
278
+ color : '#ffffff' ,
279
+ cursor : 'pointer' ,
280
+ fontSize : '15px' ,
281
+ fontWeight : '700' ,
282
+ height : '36px' ,
283
+ marginLeft : 'auto' ,
284
+ minWidth : '80px' ,
285
+ padding : '0 12px 1px' ,
286
+ transition : 'all 80ms linear' ,
287
+ '&:hover' : {
288
+ background : '#e23067' ,
289
+ boxShadow : '0 1px 4px #0000004d' ,
290
+ } ,
291
+ } ) ;
292
+
293
+ const AddPeopleIconWrapper = styled . div ( {
294
+ alignItems : 'center' ,
295
+ backgroundColor : '#e8f5fa' ,
296
+ display : 'flex' ,
297
+ height : '36px' ,
298
+ justifyContent : 'center' ,
299
+ width : '36px' ,
300
+ } ) ;
301
+
302
+ const Checkbox = styled . input ( {
303
+ marginRight : '16px' ,
304
+ cursor : 'pointer' ,
305
+ } ) ;
306
+
307
+ const Label = styled . label ( {
308
+ cursor : 'pointer' ,
309
+ } ) ;
310
+
311
+ const Footer = styled . div ( {
312
+ display : 'flex' ,
313
+ justifyContent : 'space-between' ,
314
+ backgroundColor : 'transparent' ,
315
+ padding : '24px 24px' ,
316
+ borderTop : `1px solid ${ Colors . gray_light } ` ,
317
+ } ) ;
65
318
66
319
const Avatar = styled ( DefaultAvatar ) `
67
320
grid-area: avatar;
@@ -71,16 +324,16 @@ const Avatar = styled(DefaultAvatar)`
71
324
}
72
325
` ;
73
326
74
- const Modal = styled . div ( {
327
+ const Modal = styled . div < { $order : number } > ( ( { $order } ) => ( {
75
328
position : 'absolute' ,
76
329
height : '100vh' ,
77
330
inset : 0 ,
78
331
backgroundColor : 'rgba(0, 0, 0, 0.4)' ,
79
- zIndex : 999 ,
332
+ zIndex : $order * 999 ,
80
333
display : 'flex' ,
81
334
alignItems : 'center' ,
82
335
justifyContent : 'center' ,
83
- } ) ;
336
+ } ) ) ;
84
337
85
338
const Box = styled . div ( {
86
339
backgroundColor : 'white' ,
@@ -101,6 +354,7 @@ const Header = styled.div({
101
354
} ) ;
102
355
103
356
const Heading = styled . h2 ( {
357
+ display : 'flex' ,
104
358
marginTop : 0 ,
105
359
} ) ;
106
360
@@ -120,6 +374,7 @@ const UserDetails = styled.div({
120
374
} ,
121
375
// todo: update once we have profile details like role
122
376
alignItems : 'center' ,
377
+ cursor : 'pointer' ,
123
378
} ) ;
124
379
125
380
const CloseButton = styled . button ( {
0 commit comments