mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at react-sdui 6.3 kB view raw
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}