mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {StyleProp, View, ViewStyle} from 'react-native'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import isEqual from 'lodash.isequal'
6
7import {useMyListsQuery} from '#/state/queries/my-lists'
8import {ThreadgateSetting} from '#/state/queries/threadgate'
9import {atoms as a, useTheme} from '#/alf'
10import {Button, ButtonText} from '#/components/Button'
11import * as Dialog from '#/components/Dialog'
12import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
13import {Text} from '#/components/Typography'
14
15interface ThreadgateEditorDialogProps {
16 control: Dialog.DialogControlProps
17 threadgate: ThreadgateSetting[]
18 onChange?: (v: ThreadgateSetting[]) => void
19 onConfirm?: (v: ThreadgateSetting[]) => void
20}
21
22export function ThreadgateEditorDialog({
23 control,
24 threadgate,
25 onChange,
26 onConfirm,
27}: ThreadgateEditorDialogProps) {
28 return (
29 <Dialog.Outer control={control}>
30 <Dialog.Handle />
31 <DialogContent
32 seedThreadgate={threadgate}
33 onChange={onChange}
34 onConfirm={onConfirm}
35 />
36 </Dialog.Outer>
37 )
38}
39
40function DialogContent({
41 seedThreadgate,
42 onChange,
43 onConfirm,
44}: {
45 seedThreadgate: ThreadgateSetting[]
46 onChange?: (v: ThreadgateSetting[]) => void
47 onConfirm?: (v: ThreadgateSetting[]) => void
48}) {
49 const {_} = useLingui()
50 const control = Dialog.useDialogContext()
51 const {data: lists} = useMyListsQuery('curate')
52 const [draft, setDraft] = React.useState(seedThreadgate)
53
54 const [prevSeedThreadgate, setPrevSeedThreadgate] =
55 React.useState(seedThreadgate)
56 if (seedThreadgate !== prevSeedThreadgate) {
57 // New data flowed from above (e.g. due to update coming through).
58 setPrevSeedThreadgate(seedThreadgate)
59 setDraft(seedThreadgate) // Reset draft.
60 }
61
62 function updateThreadgate(nextThreadgate: ThreadgateSetting[]) {
63 setDraft(nextThreadgate)
64 onChange?.(nextThreadgate)
65 }
66
67 const onPressEverybody = () => {
68 updateThreadgate([])
69 }
70
71 const onPressNobody = () => {
72 updateThreadgate([{type: 'nobody'}])
73 }
74
75 const onPressAudience = (setting: ThreadgateSetting) => {
76 // remove nobody
77 let newSelected: ThreadgateSetting[] = draft.filter(
78 v => v.type !== 'nobody',
79 )
80 // toggle
81 const i = newSelected.findIndex(v => isEqual(v, setting))
82 if (i === -1) {
83 newSelected.push(setting)
84 } else {
85 newSelected.splice(i, 1)
86 }
87 updateThreadgate(newSelected)
88 }
89
90 const doneLabel = onConfirm ? _(msg`Save`) : _(msg`Done`)
91 return (
92 <Dialog.ScrollableInner
93 label={_(msg`Choose who can reply`)}
94 style={[{maxWidth: 500}, a.w_full]}>
95 <View style={[a.flex_1, a.gap_md]}>
96 <Text style={[a.text_2xl, a.font_bold]}>
97 <Trans>Choose who can reply</Trans>
98 </Text>
99 <Text style={a.mt_xs}>
100 <Trans>Either choose "Everybody" or "Nobody"</Trans>
101 </Text>
102 <View style={[a.flex_row, a.gap_sm]}>
103 <Selectable
104 label={_(msg`Everybody`)}
105 isSelected={draft.length === 0}
106 onPress={onPressEverybody}
107 style={{flex: 1}}
108 />
109 <Selectable
110 label={_(msg`Nobody`)}
111 isSelected={!!draft.find(v => v.type === 'nobody')}
112 onPress={onPressNobody}
113 style={{flex: 1}}
114 />
115 </View>
116 <Text style={a.mt_md}>
117 <Trans>Or combine these options:</Trans>
118 </Text>
119 <View style={[a.gap_sm]}>
120 <Selectable
121 label={_(msg`Mentioned users`)}
122 isSelected={!!draft.find(v => v.type === 'mention')}
123 onPress={() => onPressAudience({type: 'mention'})}
124 />
125 <Selectable
126 label={_(msg`Followed users`)}
127 isSelected={!!draft.find(v => v.type === 'following')}
128 onPress={() => onPressAudience({type: 'following'})}
129 />
130 {lists && lists.length > 0
131 ? lists.map(list => (
132 <Selectable
133 key={list.uri}
134 label={_(msg`Users in "${list.name}"`)}
135 isSelected={
136 !!draft.find(v => v.type === 'list' && v.list === list.uri)
137 }
138 onPress={() =>
139 onPressAudience({type: 'list', list: list.uri})
140 }
141 />
142 ))
143 : // No loading states to avoid jumps for the common case (no lists)
144 null}
145 </View>
146 </View>
147 <Button
148 label={doneLabel}
149 onPress={() => {
150 control.close()
151 onConfirm?.(draft)
152 }}
153 onAccessibilityEscape={control.close}
154 color="primary"
155 size="medium"
156 variant="solid"
157 style={a.mt_xl}>
158 <ButtonText>{doneLabel}</ButtonText>
159 </Button>
160 <Dialog.Close />
161 </Dialog.ScrollableInner>
162 )
163}
164
165function Selectable({
166 label,
167 isSelected,
168 onPress,
169 style,
170}: {
171 label: string
172 isSelected: boolean
173 onPress: () => void
174 style?: StyleProp<ViewStyle>
175}) {
176 const t = useTheme()
177 return (
178 <Button
179 onPress={onPress}
180 label={label}
181 accessibilityHint="Select this option"
182 accessibilityRole="checkbox"
183 aria-checked={isSelected}
184 accessibilityState={{
185 checked: isSelected,
186 }}
187 style={a.flex_1}>
188 {({hovered, focused}) => (
189 <View
190 style={[
191 a.flex_1,
192 a.flex_row,
193 a.align_center,
194 a.justify_between,
195 a.rounded_sm,
196 a.p_md,
197 {height: 40}, // for consistency with checkmark icon visible or not
198 t.atoms.bg_contrast_50,
199 (hovered || focused) && t.atoms.bg_contrast_100,
200 isSelected && {
201 backgroundColor: t.palette.primary_100,
202 },
203 style,
204 ]}>
205 <Text style={[a.text_sm, isSelected && a.font_semibold]}>
206 {label}
207 </Text>
208 {isSelected ? (
209 <Check size="sm" fill={t.palette.primary_500} />
210 ) : (
211 <View />
212 )}
213 </View>
214 )}
215 </Button>
216 )
217}