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 = draft.filter(v => v.type !== 'nobody')
78 // toggle
79 const i = newSelected.findIndex(v => isEqual(v, setting))
80 if (i === -1) {
81 newSelected.push(setting)
82 } else {
83 newSelected.splice(i, 1)
84 }
85 updateThreadgate(newSelected)
86 }
87
88 const doneLabel = onConfirm ? _(msg`Save`) : _(msg`Done`)
89 return (
90 <Dialog.ScrollableInner
91 label={_(msg`Choose who can reply`)}
92 style={[{maxWidth: 500}, a.w_full]}>
93 <View style={[a.flex_1, a.gap_md]}>
94 <Text style={[a.text_2xl, a.font_bold]}>
95 <Trans>Choose who can reply</Trans>
96 </Text>
97 <Text style={a.mt_xs}>
98 <Trans>Either choose "Everybody" or "Nobody"</Trans>
99 </Text>
100 <View style={[a.flex_row, a.gap_sm]}>
101 <Selectable
102 label={_(msg`Everybody`)}
103 isSelected={draft.length === 0}
104 onPress={onPressEverybody}
105 style={{flex: 1}}
106 />
107 <Selectable
108 label={_(msg`Nobody`)}
109 isSelected={!!draft.find(v => v.type === 'nobody')}
110 onPress={onPressNobody}
111 style={{flex: 1}}
112 />
113 </View>
114 <Text style={a.mt_md}>
115 <Trans>Or combine these options:</Trans>
116 </Text>
117 <View style={[a.gap_sm]}>
118 <Selectable
119 label={_(msg`Mentioned users`)}
120 isSelected={!!draft.find(v => v.type === 'mention')}
121 onPress={() => onPressAudience({type: 'mention'})}
122 />
123 <Selectable
124 label={_(msg`Followed users`)}
125 isSelected={!!draft.find(v => v.type === 'following')}
126 onPress={() => onPressAudience({type: 'following'})}
127 />
128 {lists && lists.length > 0
129 ? lists.map(list => (
130 <Selectable
131 key={list.uri}
132 label={_(msg`Users in "${list.name}"`)}
133 isSelected={
134 !!draft.find(v => v.type === 'list' && v.list === list.uri)
135 }
136 onPress={() =>
137 onPressAudience({type: 'list', list: list.uri})
138 }
139 />
140 ))
141 : // No loading states to avoid jumps for the common case (no lists)
142 null}
143 </View>
144 </View>
145 <Button
146 label={doneLabel}
147 onPress={() => {
148 control.close()
149 onConfirm?.(draft)
150 }}
151 onAccessibilityEscape={control.close}
152 color="primary"
153 size="medium"
154 variant="solid"
155 style={a.mt_xl}>
156 <ButtonText>{doneLabel}</ButtonText>
157 </Button>
158 <Dialog.Close />
159 </Dialog.ScrollableInner>
160 )
161}
162
163function Selectable({
164 label,
165 isSelected,
166 onPress,
167 style,
168}: {
169 label: string
170 isSelected: boolean
171 onPress: () => void
172 style?: StyleProp<ViewStyle>
173}) {
174 const t = useTheme()
175 return (
176 <Button
177 onPress={onPress}
178 label={label}
179 accessibilityHint="Select this option"
180 accessibilityRole="checkbox"
181 aria-checked={isSelected}
182 accessibilityState={{
183 checked: isSelected,
184 }}
185 style={a.flex_1}>
186 {({hovered, focused}) => (
187 <View
188 style={[
189 a.flex_1,
190 a.flex_row,
191 a.align_center,
192 a.justify_between,
193 a.rounded_sm,
194 a.p_md,
195 {height: 40}, // for consistency with checkmark icon visible or not
196 t.atoms.bg_contrast_50,
197 (hovered || focused) && t.atoms.bg_contrast_100,
198 isSelected && {
199 backgroundColor:
200 t.name === 'light'
201 ? t.palette.primary_50
202 : t.palette.primary_975,
203 },
204 style,
205 ]}>
206 <Text style={[a.text_sm, isSelected && a.font_semibold]}>
207 {label}
208 </Text>
209 {isSelected ? (
210 <Check size="sm" fill={t.palette.primary_500} />
211 ) : (
212 <View />
213 )}
214 </View>
215 )}
216 </Button>
217 )
218}