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}