mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at patch-expo-modules-core 377 lines 12 kB view raw
1import React from 'react' 2import {Keyboard, View} from 'react-native' 3import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6 7import {logger} from '#/logger' 8import {isNative} from '#/platform/detection' 9import { 10 usePreferencesQuery, 11 useRemoveMutedWordMutation, 12 useUpsertMutedWordsMutation, 13} from '#/state/queries/preferences' 14import { 15 atoms as a, 16 native, 17 useBreakpoints, 18 useTheme, 19 ViewStyleProp, 20 web, 21} from '#/alf' 22import {Button, ButtonIcon, ButtonText} from '#/components/Button' 23import * as Dialog from '#/components/Dialog' 24import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 25import {Divider} from '#/components/Divider' 26import * as Toggle from '#/components/forms/Toggle' 27import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 28import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText' 29import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 30import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 31import {KeyboardPadding} from '#/components/KeyboardPadding' 32import {Loader} from '#/components/Loader' 33import * as Prompt from '#/components/Prompt' 34import {Text} from '#/components/Typography' 35 36export function MutedWordsDialog() { 37 const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() 38 return ( 39 <Dialog.Outer control={control}> 40 <Dialog.Handle /> 41 <MutedWordsInner /> 42 </Dialog.Outer> 43 ) 44} 45 46function MutedWordsInner() { 47 const t = useTheme() 48 const {_} = useLingui() 49 const {gtMobile} = useBreakpoints() 50 const { 51 isLoading: isPreferencesLoading, 52 data: preferences, 53 error: preferencesError, 54 } = usePreferencesQuery() 55 const {isPending, mutateAsync: addMutedWord} = useUpsertMutedWordsMutation() 56 const [field, setField] = React.useState('') 57 const [options, setOptions] = React.useState(['content']) 58 const [error, setError] = React.useState('') 59 60 const submit = React.useCallback(async () => { 61 const sanitizedValue = sanitizeMutedWordValue(field) 62 const targets = ['tag', options.includes('content') && 'content'].filter( 63 Boolean, 64 ) as AppBskyActorDefs.MutedWord['targets'] 65 66 if (!sanitizedValue || !targets.length) { 67 setField('') 68 setError(_(msg`Please enter a valid word, tag, or phrase to mute`)) 69 return 70 } 71 72 try { 73 // send raw value and rely on SDK as sanitization source of truth 74 await addMutedWord([{value: field, targets}]) 75 setField('') 76 } catch (e: any) { 77 logger.error(`Failed to save muted word`, {message: e.message}) 78 setError(e.message) 79 } 80 }, [_, field, options, addMutedWord, setField]) 81 82 return ( 83 <Dialog.ScrollableInner label={_(msg`Manage your muted words and tags`)}> 84 <View onTouchStart={Keyboard.dismiss}> 85 <Text 86 style={[a.text_md, a.font_bold, a.pb_sm, t.atoms.text_contrast_high]}> 87 <Trans>Add muted words and tags</Trans> 88 </Text> 89 <Text style={[a.pb_lg, a.leading_snug, t.atoms.text_contrast_medium]}> 90 <Trans> 91 Posts can be muted based on their text, their tags, or both. 92 </Trans> 93 </Text> 94 95 <View style={[a.pb_lg]}> 96 <Dialog.Input 97 autoCorrect={false} 98 autoCapitalize="none" 99 autoComplete="off" 100 label={_(msg`Enter a word or tag`)} 101 placeholder={_(msg`Enter a word or tag`)} 102 value={field} 103 onChangeText={value => { 104 if (error) { 105 setError('') 106 } 107 setField(value) 108 }} 109 onSubmitEditing={submit} 110 /> 111 112 <Toggle.Group 113 label={_(msg`Toggle between muted word options.`)} 114 type="radio" 115 values={options} 116 onChange={setOptions}> 117 <View 118 style={[ 119 a.pt_sm, 120 a.py_sm, 121 a.flex_row, 122 a.align_center, 123 a.gap_sm, 124 a.flex_wrap, 125 ]}> 126 <Toggle.Item 127 label={_(msg`Mute this word in post text and tags`)} 128 name="content" 129 style={[a.flex_1, !gtMobile && [a.w_full, a.flex_0]]}> 130 <TargetToggle> 131 <View style={[a.flex_row, a.align_center, a.gap_sm]}> 132 <Toggle.Radio /> 133 <Toggle.LabelText> 134 <Trans>Mute in text & tags</Trans> 135 </Toggle.LabelText> 136 </View> 137 <PageText size="sm" /> 138 </TargetToggle> 139 </Toggle.Item> 140 141 <Toggle.Item 142 label={_(msg`Mute this word in tags only`)} 143 name="tag" 144 style={[a.flex_1, !gtMobile && [a.w_full, a.flex_0]]}> 145 <TargetToggle> 146 <View style={[a.flex_row, a.align_center, a.gap_sm]}> 147 <Toggle.Radio /> 148 <Toggle.LabelText> 149 <Trans>Mute in tags only</Trans> 150 </Toggle.LabelText> 151 </View> 152 <Hashtag size="sm" /> 153 </TargetToggle> 154 </Toggle.Item> 155 156 <Button 157 disabled={isPending || !field} 158 label={_(msg`Add mute word for configured settings`)} 159 size="small" 160 color="primary" 161 variant="solid" 162 style={[!gtMobile && [a.w_full, a.flex_0]]} 163 onPress={submit}> 164 <ButtonText> 165 <Trans>Add</Trans> 166 </ButtonText> 167 <ButtonIcon icon={isPending ? Loader : Plus} /> 168 </Button> 169 </View> 170 </Toggle.Group> 171 172 {error && ( 173 <View 174 style={[ 175 a.mb_lg, 176 a.flex_row, 177 a.rounded_sm, 178 a.p_md, 179 a.mb_xs, 180 t.atoms.bg_contrast_25, 181 { 182 backgroundColor: t.palette.negative_400, 183 }, 184 ]}> 185 <Text 186 style={[ 187 a.italic, 188 {color: t.palette.white}, 189 native({marginTop: 2}), 190 ]}> 191 {error} 192 </Text> 193 </View> 194 )} 195 196 <Text 197 style={[ 198 a.pt_xs, 199 a.text_sm, 200 a.italic, 201 a.leading_snug, 202 t.atoms.text_contrast_medium, 203 ]}> 204 <Trans> 205 We recommend avoiding common words that appear in many posts, 206 since it can result in no posts being shown. 207 </Trans> 208 </Text> 209 </View> 210 211 <Divider /> 212 213 <View style={[a.pt_2xl]}> 214 <Text 215 style={[ 216 a.text_md, 217 a.font_bold, 218 a.pb_md, 219 t.atoms.text_contrast_high, 220 ]}> 221 <Trans>Your muted words</Trans> 222 </Text> 223 224 {isPreferencesLoading ? ( 225 <Loader /> 226 ) : preferencesError || !preferences ? ( 227 <View 228 style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}> 229 <Text style={[a.italic, t.atoms.text_contrast_high]}> 230 <Trans> 231 We're sorry, but we weren't able to load your muted words at 232 this time. Please try again. 233 </Trans> 234 </Text> 235 </View> 236 ) : preferences.moderationPrefs.mutedWords.length ? ( 237 [...preferences.moderationPrefs.mutedWords] 238 .reverse() 239 .map((word, i) => ( 240 <MutedWordRow 241 key={word.value + i} 242 word={word} 243 style={[i % 2 === 0 && t.atoms.bg_contrast_25]} 244 /> 245 )) 246 ) : ( 247 <View 248 style={[a.py_md, a.px_lg, a.rounded_md, t.atoms.bg_contrast_25]}> 249 <Text style={[a.italic, t.atoms.text_contrast_high]}> 250 <Trans>You haven't muted any words or tags yet</Trans> 251 </Text> 252 </View> 253 )} 254 </View> 255 256 {isNative && <View style={{height: 20}} />} 257 </View> 258 259 <Dialog.Close /> 260 <KeyboardPadding maxHeight={100} /> 261 </Dialog.ScrollableInner> 262 ) 263} 264 265function MutedWordRow({ 266 style, 267 word, 268}: ViewStyleProp & {word: AppBskyActorDefs.MutedWord}) { 269 const t = useTheme() 270 const {_} = useLingui() 271 const {isPending, mutateAsync: removeMutedWord} = useRemoveMutedWordMutation() 272 const control = Prompt.usePromptControl() 273 274 const remove = React.useCallback(async () => { 275 control.close() 276 removeMutedWord(word) 277 }, [removeMutedWord, word, control]) 278 279 return ( 280 <> 281 <Prompt.Basic 282 control={control} 283 title={_(msg`Are you sure?`)} 284 description={_( 285 msg`This will delete ${word.value} from your muted words. You can always add it back later.`, 286 )} 287 onConfirm={remove} 288 confirmButtonCta={_(msg`Remove`)} 289 confirmButtonColor="negative" 290 /> 291 292 <View 293 style={[ 294 a.py_md, 295 a.px_lg, 296 a.flex_row, 297 a.align_center, 298 a.justify_between, 299 a.rounded_md, 300 a.gap_md, 301 style, 302 ]}> 303 <Text 304 style={[ 305 a.flex_1, 306 a.leading_snug, 307 a.w_full, 308 a.font_bold, 309 t.atoms.text_contrast_high, 310 web({ 311 overflowWrap: 'break-word', 312 wordBreak: 'break-word', 313 }), 314 ]}> 315 {word.value} 316 </Text> 317 318 <View style={[a.flex_row, a.align_center, a.justify_end, a.gap_sm]}> 319 {word.targets.map(target => ( 320 <View 321 key={target} 322 style={[a.py_xs, a.px_sm, a.rounded_sm, t.atoms.bg_contrast_100]}> 323 <Text 324 style={[a.text_xs, a.font_bold, t.atoms.text_contrast_medium]}> 325 {target === 'content' ? _(msg`text`) : _(msg`tag`)} 326 </Text> 327 </View> 328 ))} 329 330 <Button 331 label={_(msg`Remove mute word from your list`)} 332 size="tiny" 333 shape="round" 334 variant="ghost" 335 color="secondary" 336 onPress={() => control.open()} 337 style={[a.ml_sm]}> 338 <ButtonIcon icon={isPending ? Loader : X} /> 339 </Button> 340 </View> 341 </View> 342 </> 343 ) 344} 345 346function TargetToggle({children}: React.PropsWithChildren<{}>) { 347 const t = useTheme() 348 const ctx = Toggle.useItemContext() 349 const {gtMobile} = useBreakpoints() 350 return ( 351 <View 352 style={[ 353 a.flex_row, 354 a.align_center, 355 a.justify_between, 356 a.gap_xs, 357 a.flex_1, 358 a.py_sm, 359 a.px_sm, 360 gtMobile && a.px_md, 361 a.rounded_sm, 362 t.atoms.bg_contrast_50, 363 (ctx.hovered || ctx.focused) && t.atoms.bg_contrast_100, 364 ctx.selected && [ 365 { 366 backgroundColor: 367 t.name === 'light' ? t.palette.primary_50 : t.palette.primary_975, 368 }, 369 ], 370 ctx.disabled && { 371 opacity: 0.8, 372 }, 373 ]}> 374 {children} 375 </View> 376 ) 377}