diff options
Diffstat (limited to 'src/view/com/modals')
31 files changed, 714 insertions, 266 deletions
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx index 812a36f45..7ec8268be 100644 --- a/src/view/com/modals/AddAppPasswords.tsx +++ b/src/view/com/modals/AddAppPasswords.tsx @@ -72,10 +72,10 @@ export function Component({}: {}) { const onCopy = React.useCallback(() => { if (appPassword) { Clipboard.setString(appPassword) - Toast.show('Copied to clipboard') + Toast.show(_(msg`Copied to clipboard`)) setWasCopied(true) } - }, [appPassword]) + }, [appPassword, _]) const onDone = React.useCallback(() => { closeModal() @@ -85,7 +85,9 @@ export function Component({}: {}) { // if name is all whitespace, we don't allow it if (!name || !name.trim()) { Toast.show( - 'Please enter a name for your app password. All spaces is not allowed.', + _( + msg`Please enter a name for your app password. All spaces is not allowed.`, + ), 'times', ) return @@ -93,14 +95,14 @@ export function Component({}: {}) { // if name is too short (under 4 chars), we don't allow it if (name.length < 4) { Toast.show( - 'App Password names must be at least 4 characters long.', + _(msg`App Password names must be at least 4 characters long.`), 'times', ) return } if (passwords?.find(p => p.name === name)) { - Toast.show('This name is already in use', 'times') + Toast.show(_(msg`This name is already in use`), 'times') return } @@ -109,11 +111,11 @@ export function Component({}: {}) { if (newPassword) { setAppPassword(newPassword.password) } else { - Toast.show('Failed to create app password.', 'times') + Toast.show(_(msg`Failed to create app password.`), 'times') // TODO: better error handling (?) } } catch (e) { - Toast.show('Failed to create app password.', 'times') + Toast.show(_(msg`Failed to create app password.`), 'times') logger.error('Failed to create app password', {error: e}) } } @@ -127,7 +129,9 @@ export function Component({}: {}) { setName(text) } else { Toast.show( - 'App Password names can only contain letters, numbers, spaces, dashes, and underscores.', + _( + msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`, + ), ) } } @@ -158,7 +162,7 @@ export function Component({}: {}) { style={[styles.input, pal.text]} onChangeText={_onChangeText} value={name} - placeholder="Enter a name for this App Password" + placeholder={_(msg`Enter a name for this App Password`)} placeholderTextColor={pal.colors.textLight} autoCorrect={false} autoComplete="off" @@ -175,7 +179,7 @@ export function Component({}: {}) { onEndEditing={createAppPassword} accessible={true} accessibilityLabel={_(msg`Name`)} - accessibilityHint="Input name for app password" + accessibilityHint={_(msg`Input name for app password`)} /> </View> ) : ( @@ -184,7 +188,7 @@ export function Component({}: {}) { onPress={onCopy} accessibilityRole="button" accessibilityLabel={_(msg`Copy`)} - accessibilityHint="Copies app password"> + accessibilityHint={_(msg`Copies app password`)}> <Text type="2xl-bold" style={[pal.text]}> {appPassword} </Text> @@ -221,7 +225,7 @@ export function Component({}: {}) { <View style={styles.btnContainer}> <Button type="primary" - label={!appPassword ? 'Create App Password' : 'Done'} + label={!appPassword ? _(msg`Create App Password`) : _(msg`Done`)} style={styles.btn} labelStyle={styles.btnLabel} onPress={!appPassword ? createAppPassword : onDone} diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index a2e918317..5156511d6 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -1,14 +1,12 @@ import React, {useMemo, useCallback, useState} from 'react' import { ImageStyle, - KeyboardAvoidingView, - ScrollView, StyleSheet, - TextInput, TouchableOpacity, View, useWindowDimensions, } from 'react-native' +import {ScrollView, TextInput} from './util' import {Image} from 'expo-image' import {usePalette} from 'lib/hooks/usePalette' import {gradients, s} from 'lib/styles' @@ -17,13 +15,13 @@ import {MAX_ALT_TEXT} from 'lib/constants' import {useTheme} from 'lib/ThemeContext' import {Text} from '../util/text/Text' import LinearGradient from 'react-native-linear-gradient' -import {isAndroid, isWeb} from 'platform/detection' +import {isWeb} from 'platform/detection' import {ImageModel} from 'state/models/media/image' import {useLingui} from '@lingui/react' import {Trans, msg} from '@lingui/macro' import {useModalControls} from '#/state/modals' -export const snapPoints = ['fullscreen'] +export const snapPoints = ['100%'] interface Props { image: ImageModel @@ -54,102 +52,86 @@ export function Component({image}: Props) { } }, [image, windim]) + const onUpdate = useCallback( + (v: string) => { + v = enforceLen(v, MAX_ALT_TEXT) + setAltText(v) + image.setAltText(v) + }, + [setAltText, image], + ) + const onPressSave = useCallback(() => { image.setAltText(altText) closeModal() }, [closeModal, image, altText]) - const onPressCancel = () => { - closeModal() - } - return ( - <KeyboardAvoidingView - behavior={isAndroid ? 'height' : 'padding'} - style={[pal.view, styles.container]}> - <ScrollView - testID="altTextImageModal" - style={styles.scrollContainer} - keyboardShouldPersistTaps="always" - nativeID="imageAltText"> - <View style={styles.scrollInner}> - <View style={[pal.viewLight, styles.imageContainer]}> - <Image - testID="selectedPhotoImage" - style={imageStyles} - source={{ - uri: image.cropped?.path ?? image.path, - }} - contentFit="contain" - accessible={true} - accessibilityIgnoresInvertColors - /> - </View> - <TextInput - testID="altTextImageInput" - style={[styles.textArea, pal.border, pal.text]} - keyboardAppearance={theme.colorScheme} - multiline - placeholder="Add alt text" - placeholderTextColor={pal.colors.textLight} - value={altText} - onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))} - accessibilityLabel={_(msg`Image alt text`)} - accessibilityHint="" - accessibilityLabelledBy="imageAltText" - autoFocus + <ScrollView + testID="altTextImageModal" + style={[pal.view, styles.scrollContainer]} + keyboardShouldPersistTaps="always" + nativeID="imageAltText"> + <View style={styles.scrollInner}> + <View style={[pal.viewLight, styles.imageContainer]}> + <Image + testID="selectedPhotoImage" + style={imageStyles} + source={{ + uri: image.cropped?.path ?? image.path, + }} + contentFit="contain" + accessible={true} + accessibilityIgnoresInvertColors /> - <View style={styles.buttonControls}> - <TouchableOpacity - testID="altTextImageSaveBtn" - onPress={onPressSave} - accessibilityLabel={_(msg`Save alt text`)} - accessibilityHint={`Saves alt text, which reads: ${altText}`} - accessibilityRole="button"> - <LinearGradient - colors={[gradients.blueLight.start, gradients.blueLight.end]} - start={{x: 0, y: 0}} - end={{x: 1, y: 1}} - style={[styles.button]}> - <Text type="button-lg" style={[s.white, s.bold]}> - <Trans>Save</Trans> - </Text> - </LinearGradient> - </TouchableOpacity> - <TouchableOpacity - testID="altTextImageCancelBtn" - onPress={onPressCancel} - accessibilityRole="button" - accessibilityLabel={_(msg`Cancel add image alt text`)} - accessibilityHint="" - onAccessibilityEscape={onPressCancel}> - <View style={[styles.button]}> - <Text type="button-lg" style={[pal.textLight]}> - <Trans>Cancel</Trans> - </Text> - </View> - </TouchableOpacity> - </View> </View> - </ScrollView> - </KeyboardAvoidingView> + <TextInput + testID="altTextImageInput" + style={[styles.textArea, pal.border, pal.text]} + keyboardAppearance={theme.colorScheme} + multiline + placeholder={_(msg`Add alt text`)} + placeholderTextColor={pal.colors.textLight} + value={altText} + onChangeText={onUpdate} + accessibilityLabel={_(msg`Image alt text`)} + accessibilityHint="" + accessibilityLabelledBy="imageAltText" + autoFocus + /> + <View style={styles.buttonControls}> + <TouchableOpacity + testID="altTextImageSaveBtn" + onPress={onPressSave} + accessibilityLabel={_(msg`Save alt text`)} + accessibilityHint="" + accessibilityRole="button"> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[styles.button]}> + <Text type="button-lg" style={[s.white, s.bold]}> + <Trans>Done</Trans> + </Text> + </LinearGradient> + </TouchableOpacity> + </View> + </View> + </ScrollView> ) } const styles = StyleSheet.create({ - container: { - flex: 1, - height: '100%', - width: '100%', - paddingVertical: isWeb ? 0 : 18, - }, scrollContainer: { flex: 1, height: '100%', paddingHorizontal: isWeb ? 0 : 12, + paddingVertical: isWeb ? 0 : 24, }, scrollInner: { gap: 12, + paddingTop: isWeb ? 0 : 12, }, imageContainer: { borderRadius: 8, @@ -173,5 +155,6 @@ const styles = StyleSheet.create({ }, buttonControls: { gap: 8, + paddingBottom: isWeb ? 0 : 50, }, }) diff --git a/src/view/com/modals/AppealLabel.tsx b/src/view/com/modals/AppealLabel.tsx index edc6f4cd0..b0aaaf625 100644 --- a/src/view/com/modals/AppealLabel.tsx +++ b/src/view/com/modals/AppealLabel.tsx @@ -38,14 +38,14 @@ export function Component(props: ReportComponentProps) { ? 'com.atproto.repo.strongRef' : 'com.atproto.admin.defs#repoRef' await getAgent().createModerationReport({ - reasonType: ComAtprotoModerationDefs.REASONOTHER, + reasonType: ComAtprotoModerationDefs.REASONAPPEAL, subject: { $type, ...props, }, reason: details, }) - Toast.show("We'll look into your appeal promptly.") + Toast.show(_(msg`We'll look into your appeal promptly.`)) } finally { closeModal() } diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx index c78f06ed4..5ebc61137 100644 --- a/src/view/com/modals/BirthDateSettings.tsx +++ b/src/view/com/modals/BirthDateSettings.tsx @@ -23,7 +23,7 @@ import { } from '#/state/queries/preferences' import {logger} from '#/logger' -export const snapPoints = ['50%'] +export const snapPoints = ['50%', '90%'] function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { const pal = usePalette('default') @@ -63,6 +63,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { <View> <DateInput + handleAsUTC testID="birthdayInput" value={date} onChange={setDate} @@ -70,7 +71,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { buttonStyle={[pal.border, styles.dateInputButton]} buttonLabelType="lg" accessibilityLabel={_(msg`Birthday`)} - accessibilityHint="Enter your birth date" + accessibilityHint={_(msg`Enter your birth date`)} accessibilityLabelledBy="birthDate" /> </View> diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx index 44b102fa0..c5672bc81 100644 --- a/src/view/com/modals/ChangeEmail.tsx +++ b/src/view/com/modals/ChangeEmail.tsx @@ -38,7 +38,7 @@ export function Component() { const onRequestChange = async () => { if (email === currentAccount?.email) { - setError('Enter your new email above') + setError(_(msg`Enter your new email above`)) return } setError('') @@ -53,7 +53,7 @@ export function Component() { email: email.trim(), emailConfirmed: false, }) - Toast.show('Email updated') + Toast.show(_(msg`Email updated`)) setStage(Stages.Done) } } catch (e) { @@ -85,7 +85,7 @@ export function Component() { email: email.trim(), emailConfirmed: false, }) - Toast.show('Email updated') + Toast.show(_(msg`Email updated`)) setStage(Stages.Done) } catch (e) { setError(cleanError(String(e))) diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx index 31f6d6ea7..e578fa7da 100644 --- a/src/view/com/modals/ChangeHandle.tsx +++ b/src/view/com/modals/ChangeHandle.tsx @@ -147,7 +147,7 @@ export function Inner({ onPress={onPressCancel} accessibilityRole="button" accessibilityLabel={_(msg`Cancel change handle`)} - accessibilityHint="Exits handle change process" + accessibilityHint={_(msg`Exits handle change process`)} onAccessibilityEscape={onPressCancel}> <Text type="lg" style={pal.textLight}> Cancel @@ -168,7 +168,7 @@ export function Inner({ onPress={onPressSave} accessibilityRole="button" accessibilityLabel={_(msg`Save handle change`)} - accessibilityHint={`Saves handle change to ${handle}`}> + accessibilityHint={_(msg`Saves handle change to ${handle}`)}> <Text type="2xl-medium" style={pal.link}> <Trans>Save</Trans> </Text> @@ -263,14 +263,16 @@ function ProvidedHandleForm({ editable={!isProcessing} accessible={true} accessibilityLabel={_(msg`Handle`)} - accessibilityHint="Sets Bluesky username" + accessibilityHint={_(msg`Sets Bluesky username`)} /> </View> <Text type="md" style={[pal.textLight, s.pl10, s.pt10]}> - <Trans>Your full handle will be</Trans>{' '} - <Text type="md-bold" style={pal.textLight}> - @{createFullHandle(handle, userDomain)} - </Text> + <Trans> + Your full handle will be{' '} + <Text type="md-bold" style={pal.textLight}> + @{createFullHandle(handle, userDomain)} + </Text> + </Trans> </Text> <TouchableOpacity onPress={onToggleCustom} diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index 5e869f396..307897fb8 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -12,7 +12,7 @@ import {cleanError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useLingui} from '@lingui/react' -import {msg} from '@lingui/macro' +import {Trans, msg} from '@lingui/macro' import type {ConfirmModal} from '#/state/modals' import {useModalControls} from '#/state/modals' @@ -72,10 +72,10 @@ export function Component({ onPress={onPress} style={[styles.btn, confirmBtnStyle]} accessibilityRole="button" - accessibilityLabel={_(msg`Confirm`)} + accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))} accessibilityHint=""> <Text style={[s.white, s.bold, s.f18]}> - {confirmBtnText ?? 'Confirm'} + {confirmBtnText ?? <Trans context="action">Confirm</Trans>} </Text> </TouchableOpacity> )} @@ -85,10 +85,10 @@ export function Component({ onPress={onPressCancel} style={[styles.btnCancel, s.mt10]} accessibilityRole="button" - accessibilityLabel={_(msg`Cancel`)} + accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))} accessibilityHint=""> <Text type="button-lg" style={pal.textLight}> - {cancelBtnText ?? 'Cancel'} + {cancelBtnText ?? <Trans context="action">Cancel</Trans>} </Text> </TouchableOpacity> )} diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index 8b42e1b1d..d681fbf0b 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -104,6 +104,7 @@ export function Component({}: {}) { function AdultContentEnabledPref() { const pal = usePalette('default') + const {_} = useLingui() const {data: preferences} = usePreferencesQuery() const {mutate, variables} = usePreferencesSetAdultContentMutation() const {openModal} = useModalControls() @@ -121,36 +122,44 @@ function AdultContentEnabledPref() { enabled: !(variables?.enabled ?? preferences?.adultContentEnabled), }) } catch (e) { - Toast.show('There was an issue syncing your preferences with the server') + Toast.show( + _(msg`There was an issue syncing your preferences with the server`), + ) logger.error('Failed to update preferences with server', {error: e}) } - }, [variables, preferences, mutate]) + }, [variables, preferences, mutate, _]) return ( <View style={s.mb10}> {isIOS ? ( preferences?.adultContentEnabled ? null : ( <Text type="md" style={pal.textLight}> - Adult content can only be enabled via the Web at{' '} - <TextLink - style={pal.link} - href="https://bsky.app" - text="bsky.app" - /> - . + <Trans> + Adult content can only be enabled via the Web at{' '} + <TextLink + style={pal.link} + href="https://bsky.app" + text="bsky.app" + /> + . + </Trans> </Text> ) ) : typeof preferences?.birthDate === 'undefined' ? ( <View style={[pal.viewLight, styles.agePrompt]}> <Text type="md" style={[pal.text, {flex: 1}]}> - Confirm your age to enable adult content. + <Trans>Confirm your age to enable adult content.</Trans> </Text> - <Button type="primary" label="Set Age" onPress={onSetAge} /> + <Button + type="primary" + label={_(msg({message: 'Set Age', context: 'action'}))} + onPress={onSetAge} + /> </View> ) : (preferences.userAge || 0) >= 18 ? ( <ToggleButton type="default-light" - label="Enable Adult Content" + label={_(msg`Enable Adult Content`)} isSelected={variables?.enabled ?? preferences?.adultContentEnabled} onPress={onToggleAdultContent} style={styles.toggleBtn} @@ -158,9 +167,13 @@ function AdultContentEnabledPref() { ) : ( <View style={[pal.viewLight, styles.agePrompt]}> <Text type="md" style={[pal.text, {flex: 1}]}> - You must be 18 or older to enable adult content. + <Trans>You must be 18 or older to enable adult content.</Trans> </Text> - <Button type="primary" label="Set Age" onPress={onSetAge} /> + <Button + type="primary" + label={_(msg({message: 'Set Age', context: 'action'}))} + onPress={onSetAge} + /> </View> )} </View> @@ -203,7 +216,7 @@ function ContentLabelPref({ {disabled || !visibility ? ( <Text type="sm-bold" style={pal.textLight}> - Hide + <Trans context="action">Hide</Trans> </Text> ) : ( <SelectGroup @@ -223,12 +236,14 @@ interface SelectGroupProps { } function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) { + const {_} = useLingui() + return ( <View style={styles.selectableBtns}> <SelectableBtn current={current} value="hide" - label="Hide" + label={_(msg`Hide`)} left onChange={onChange} labelGroup={labelGroup} @@ -236,14 +251,14 @@ function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) { <SelectableBtn current={current} value="warn" - label="Warn" + label={_(msg`Warn`)} onChange={onChange} labelGroup={labelGroup} /> <SelectableBtn current={current} value="ignore" - label="Show" + label={_(msg`Show`)} right onChange={onChange} labelGroup={labelGroup} @@ -273,6 +288,8 @@ function SelectableBtn({ }: SelectableBtnProps) { const pal = usePalette('default') const palPrimary = usePalette('inverted') + const {_} = useLingui() + return ( <Pressable style={[ @@ -285,7 +302,9 @@ function SelectableBtn({ onPress={() => onChange(value)} accessibilityRole="button" accessibilityLabel={value} - accessibilityHint={`Set ${value} for ${labelGroup} content moderation policy`}> + accessibilityHint={_( + msg`Set ${value} for ${labelGroup} content moderation policy`, + )}> <Text style={current === value ? palPrimary.text : pal.text}> {label} </Text> diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx index 8d13cdf2f..0e11fcffd 100644 --- a/src/view/com/modals/CreateOrEditList.tsx +++ b/src/view/com/modals/CreateOrEditList.tsx @@ -8,7 +8,11 @@ import { TouchableOpacity, View, } from 'react-native' -import {AppBskyGraphDefs} from '@atproto/api' +import { + AppBskyGraphDefs, + AppBskyRichtextFacet, + RichText as RichTextAPI, +} from '@atproto/api' import LinearGradient from 'react-native-linear-gradient' import {Image as RNImage} from 'react-native-image-crop-picker' import {Text} from '../util/text/Text' @@ -30,6 +34,9 @@ import { useListCreateMutation, useListMetadataMutation, } from '#/state/queries/list' +import {richTextToString} from '#/lib/strings/rich-text-helpers' +import {shortenLinks} from '#/lib/strings/rich-text-manip' +import {getAgent} from '#/state/session' const MAX_NAME = 64 // todo const MAX_DESCRIPTION = 300 // todo @@ -65,16 +72,45 @@ export function Component({ return 'app.bsky.graph.defs#curatelist' }, [list, purpose]) const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist' - const purposeLabel = isCurateList ? 'User' : 'Moderation' const [isProcessing, setProcessing] = useState<boolean>(false) const [name, setName] = useState<string>(list?.name || '') - const [description, setDescription] = useState<string>( - list?.description || '', - ) + + const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => { + const text = list?.description + const facets = list?.descriptionFacets + + if (!text || !facets) { + return new RichTextAPI({text: text || ''}) + } + + // We want to be working with a blank state here, so let's get the + // serialized version and turn it back into a RichText + const serialized = richTextToString(new RichTextAPI({text, facets}), false) + + const richText = new RichTextAPI({text: serialized}) + richText.detectFacetsWithoutResolution() + + return richText + }) + const graphemeLength = useMemo(() => { + return shortenLinks(descriptionRt).graphemeLength + }, [descriptionRt]) + const isDescriptionOver = graphemeLength > MAX_DESCRIPTION + const [avatar, setAvatar] = useState<string | undefined>(list?.avatar) const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() + const onDescriptionChange = useCallback( + (newText: string) => { + const richText = new RichTextAPI({text: newText}) + richText.detectFacetsWithoutResolution() + + setDescriptionRt(richText) + }, + [setDescriptionRt], + ) + const onPressCancel = useCallback(() => { closeModal() }, [closeModal]) @@ -106,7 +142,7 @@ export function Component({ } const nameTrimmed = name.trim() if (!nameTrimmed) { - setError('Name is required') + setError(_(msg`Name is required`)) return } setProcessing(true) @@ -114,30 +150,61 @@ export function Component({ setError('') } try { + let richText = new RichTextAPI( + {text: descriptionRt.text.trimEnd()}, + {cleanNewlines: true}, + ) + + await richText.detectFacets(getAgent()) + richText = shortenLinks(richText) + + // filter out any mention facets that didn't map to a user + richText.facets = richText.facets?.filter(facet => { + const mention = facet.features.find(feature => + AppBskyRichtextFacet.isMention(feature), + ) + if (mention && !mention.did) { + return false + } + return true + }) + if (list) { await listMetadataMutation.mutateAsync({ uri: list.uri, name: nameTrimmed, - description: description.trim(), + description: richText.text, + descriptionFacets: richText.facets, avatar: newAvatar, }) - Toast.show(`${purposeLabel} list updated`) + Toast.show( + isCurateList + ? _(msg`User list updated`) + : _(msg`Moderation list updated`), + ) onSave?.(list.uri) } else { const res = await listCreateMutation.mutateAsync({ purpose: activePurpose, name, - description, + description: richText.text, + descriptionFacets: richText.facets, avatar: newAvatar, }) - Toast.show(`${purposeLabel} list created`) + Toast.show( + isCurateList + ? _(msg`User list created`) + : _(msg`Moderation list created`), + ) onSave?.(res.uri) } closeModal() } catch (e: any) { if (isNetworkError(e)) { setError( - 'Failed to create the list. Check your internet connection and try again.', + _( + msg`Failed to create the list. Check your internet connection and try again.`, + ), ) } else { setError(cleanError(e)) @@ -153,13 +220,13 @@ export function Component({ closeModal, activePurpose, isCurateList, - purposeLabel, name, - description, + descriptionRt, newAvatar, list, listMetadataMutation, listCreateMutation, + _, ]) return ( @@ -173,9 +240,17 @@ export function Component({ ]} testID="createOrEditListModal"> <Text style={[styles.title, pal.text]}> - <Trans> - {list ? 'Edit' : 'New'} {purposeLabel} List - </Trans> + {isCurateList ? ( + list ? ( + <Trans>Edit User List</Trans> + ) : ( + <Trans>New User List</Trans> + ) + ) : list ? ( + <Trans>Edit Moderation List</Trans> + ) : ( + <Trans>New Moderation List</Trans> + )} </Text> {error !== '' && ( <View style={styles.errorContainer}> @@ -195,14 +270,18 @@ export function Component({ </View> <View style={styles.form}> <View> - <Text style={[styles.label, pal.text]} nativeID="list-name"> - <Trans>List Name</Trans> - </Text> + <View style={styles.labelWrapper}> + <Text style={[styles.label, pal.text]} nativeID="list-name"> + <Trans>List Name</Trans> + </Text> + </View> <TextInput testID="editNameInput" style={[styles.textInput, pal.border, pal.text]} placeholder={ - isCurateList ? 'e.g. Great Posters' : 'e.g. Spammers' + isCurateList + ? _(msg`e.g. Great Posters`) + : _(msg`e.g. Spammers`) } placeholderTextColor={colors.gray4} value={name} @@ -214,22 +293,30 @@ export function Component({ /> </View> <View style={s.pb10}> - <Text style={[styles.label, pal.text]} nativeID="list-description"> - <Trans>Description</Trans> - </Text> + <View style={styles.labelWrapper}> + <Text + style={[styles.label, pal.text]} + nativeID="list-description"> + <Trans>Description</Trans> + </Text> + <Text + style={[!isDescriptionOver ? pal.textLight : s.red3, s.f13]}> + {graphemeLength}/{MAX_DESCRIPTION} + </Text> + </View> <TextInput testID="editDescriptionInput" style={[styles.textArea, pal.border, pal.text]} placeholder={ isCurateList - ? 'e.g. The posters who never miss.' - : 'e.g. Users that repeatedly reply with ads.' + ? _(msg`e.g. The posters who never miss.`) + : _(msg`e.g. Users that repeatedly reply with ads.`) } placeholderTextColor={colors.gray4} keyboardAppearance={theme.colorScheme} multiline - value={description} - onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} + value={descriptionRt.text} + onChangeText={onDescriptionChange} accessible={true} accessibilityLabel={_(msg`Description`)} accessibilityHint="" @@ -243,7 +330,8 @@ export function Component({ ) : ( <TouchableOpacity testID="saveBtn" - style={s.mt10} + style={[s.mt10, isDescriptionOver && s.dimmed]} + disabled={isDescriptionOver} onPress={onPressSave} accessibilityRole="button" accessibilityLabel={_(msg`Save`)} @@ -252,9 +340,9 @@ export function Component({ colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} end={{x: 1, y: 1}} - style={[styles.btn]}> + style={styles.btn}> <Text style={[s.white, s.bold]}> - <Trans>Save</Trans> + <Trans context="action">Save</Trans> </Text> </LinearGradient> </TouchableOpacity> @@ -269,7 +357,7 @@ export function Component({ onAccessibilityEscape={onPressCancel}> <View style={[styles.btn]}> <Text style={[s.black, s.bold, pal.text]}> - <Trans>Cancel</Trans> + <Trans context="action">Cancel</Trans> </Text> </View> </TouchableOpacity> @@ -286,12 +374,18 @@ const styles = StyleSheet.create({ fontSize: 24, marginBottom: 18, }, - label: { - fontWeight: 'bold', + labelWrapper: { + flexDirection: 'row', + gap: 8, + alignItems: 'center', + justifyContent: 'space-between', paddingHorizontal: 4, paddingBottom: 4, marginTop: 20, }, + label: { + fontWeight: 'bold', + }, form: { paddingHorizontal: 6, }, diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index ee16d46b3..945d7bc89 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -62,7 +62,7 @@ export function Component({}: {}) { password, token, }) - Toast.show('Your account has been deleted') + Toast.show(_(msg`Your account has been deleted`)) resetToTab('HomeTab') removeAccount(currentAccount) clearCurrentAccount() @@ -125,7 +125,9 @@ export function Component({}: {}) { onPress={onPressSendEmail} accessibilityRole="button" accessibilityLabel={_(msg`Send email`)} - accessibilityHint="Sends email with confirmation code for account deletion"> + accessibilityHint={_( + msg`Sends email with confirmation code for account deletion`, + )}> <LinearGradient colors={[ gradients.blueLight.start, @@ -135,7 +137,7 @@ export function Component({}: {}) { end={{x: 1, y: 1}} style={[styles.btn]}> <Text type="button-lg" style={[s.white, s.bold]}> - <Trans>Send Email</Trans> + <Trans context="action">Send Email</Trans> </Text> </LinearGradient> </TouchableOpacity> @@ -147,7 +149,7 @@ export function Component({}: {}) { accessibilityHint="" onAccessibilityEscape={onCancel}> <Text type="button-lg" style={pal.textLight}> - <Trans>Cancel</Trans> + <Trans context="action">Cancel</Trans> </Text> </TouchableOpacity> </> @@ -158,7 +160,7 @@ export function Component({}: {}) { {/* TODO: Update this label to be more concise */} <Text type="lg" - style={styles.description} + style={[pal.text, styles.description]} nativeID="confirmationCode"> <Trans> Check your inbox for an email with the confirmation code to @@ -174,9 +176,14 @@ export function Component({}: {}) { onChangeText={setConfirmCode} accessibilityLabelledBy="confirmationCode" accessibilityLabel={_(msg`Confirmation code`)} - accessibilityHint="Input confirmation code for account deletion" + accessibilityHint={_( + msg`Input confirmation code for account deletion`, + )} /> - <Text type="lg" style={styles.description} nativeID="password"> + <Text + type="lg" + style={[pal.text, styles.description]} + nativeID="password"> <Trans>Please enter your password as well:</Trans> </Text> <TextInput @@ -189,7 +196,7 @@ export function Component({}: {}) { onChangeText={setPassword} accessibilityLabelledBy="password" accessibilityLabel={_(msg`Password`)} - accessibilityHint="Input password for account deletion" + accessibilityHint={_(msg`Input password for account deletion`)} /> {error ? ( <View style={styles.mt20}> @@ -220,7 +227,7 @@ export function Component({}: {}) { accessibilityHint="Exits account deletion process" onAccessibilityEscape={onCancel}> <Text type="button-lg" style={pal.textLight}> - <Trans>Cancel</Trans> + <Trans context="action">Cancel</Trans> </Text> </TouchableOpacity> </> diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx index 753907472..3b35ffee2 100644 --- a/src/view/com/modals/EditImage.tsx +++ b/src/view/com/modals/EditImage.tsx @@ -112,16 +112,16 @@ export const Component = observer(function EditImageImpl({ // }, { name: 'flip' as const, - label: 'Flip horizontal', + label: _(msg`Flip horizontal`), onPress: onFlipHorizontal, }, { name: 'flip' as const, - label: 'Flip vertically', + label: _(msg`Flip vertically`), onPress: onFlipVertical, }, ], - [onFlipHorizontal, onFlipVertical], + [onFlipHorizontal, onFlipVertical, _], ) useEffect(() => { @@ -284,7 +284,7 @@ export const Component = observer(function EditImageImpl({ size={label?.startsWith('Flip') ? 22 : 24} style={[ pal.text, - label === 'Flip vertically' + label === _(msg`Flip vertically`) ? styles.flipVertical : undefined, ]} @@ -330,7 +330,7 @@ export const Component = observer(function EditImageImpl({ end={{x: 1, y: 1}} style={[styles.btn]}> <Text type="xl-medium" style={s.white}> - <Trans>Done</Trans> + <Trans context="action">Done</Trans> </Text> </LinearGradient> </Pressable> diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index e044f8c0e..dd8ac9ae7 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -125,7 +125,7 @@ export function Component({ newUserAvatar, newUserBanner, }) - Toast.show('Profile updated') + Toast.show(_(msg`Profile updated`)) onUpdate?.() closeModal() } catch (e: any) { @@ -142,6 +142,7 @@ export function Component({ newUserAvatar, newUserBanner, setImageError, + _, ]) return ( @@ -181,7 +182,7 @@ export function Component({ <TextInput testID="editProfileDisplayNameInput" style={[styles.textInput, pal.border, pal.text]} - placeholder="e.g. Alice Roberts" + placeholder={_(msg`e.g. Alice Roberts`)} placeholderTextColor={colors.gray4} value={displayName} onChangeText={v => @@ -189,7 +190,7 @@ export function Component({ } accessible={true} accessibilityLabel={_(msg`Display name`)} - accessibilityHint="Edit your display name" + accessibilityHint={_(msg`Edit your display name`)} /> </View> <View style={s.pb10}> @@ -199,7 +200,7 @@ export function Component({ <TextInput testID="editProfileDescriptionInput" style={[styles.textArea, pal.border, pal.text]} - placeholder="e.g. Artist, dog-lover, and avid reader." + placeholder={_(msg`e.g. Artist, dog-lover, and avid reader.`)} placeholderTextColor={colors.gray4} keyboardAppearance={theme.colorScheme} multiline @@ -207,7 +208,7 @@ export function Component({ onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} accessible={true} accessibilityLabel={_(msg`Description`)} - accessibilityHint="Edit your profile description" + accessibilityHint={_(msg`Edit your profile description`)} /> </View> {updateMutation.isPending ? ( @@ -221,7 +222,7 @@ export function Component({ onPress={onPressSave} accessibilityRole="button" accessibilityLabel={_(msg`Save`)} - accessibilityHint="Saves any changes to your profile"> + accessibilityHint={_(msg`Saves any changes to your profile`)}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} diff --git a/src/view/com/modals/EmbedConsent.tsx b/src/view/com/modals/EmbedConsent.tsx new file mode 100644 index 000000000..04104c52e --- /dev/null +++ b/src/view/com/modals/EmbedConsent.tsx @@ -0,0 +1,153 @@ +import React from 'react' +import {StyleSheet, TouchableOpacity, View} from 'react-native' +import LinearGradient from 'react-native-linear-gradient' +import {s, colors, gradients} from 'lib/styles' +import {Text} from '../util/text/Text' +import {ScrollView} from './util' +import {usePalette} from 'lib/hooks/usePalette' +import { + EmbedPlayerSource, + embedPlayerSources, + externalEmbedLabels, +} from '#/lib/strings/embed-player' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useModalControls} from '#/state/modals' +import {useSetExternalEmbedPref} from '#/state/preferences/external-embeds-prefs' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' + +export const snapPoints = [450] + +export function Component({ + onAccept, + source, +}: { + onAccept: () => void + source: EmbedPlayerSource +}) { + const pal = usePalette('default') + const {closeModal} = useModalControls() + const {_} = useLingui() + const setExternalEmbedPref = useSetExternalEmbedPref() + const {isMobile} = useWebMediaQueries() + + const onShowAllPress = React.useCallback(() => { + for (const key of embedPlayerSources) { + setExternalEmbedPref(key, 'show') + } + onAccept() + closeModal() + }, [closeModal, onAccept, setExternalEmbedPref]) + + const onShowPress = React.useCallback(() => { + setExternalEmbedPref(source, 'show') + onAccept() + closeModal() + }, [closeModal, onAccept, setExternalEmbedPref, source]) + + const onHidePress = React.useCallback(() => { + setExternalEmbedPref(source, 'hide') + closeModal() + }, [closeModal, setExternalEmbedPref, source]) + + return ( + <ScrollView + testID="embedConsentModal" + style={[ + s.flex1, + pal.view, + isMobile + ? {paddingHorizontal: 20, paddingTop: 10} + : {paddingHorizontal: 30}, + ]}> + <Text style={[pal.text, styles.title]}> + <Trans>External Media</Trans> + </Text> + + <Text style={pal.text}> + <Trans> + This content is hosted by {externalEmbedLabels[source]}. Do you want + to enable external media? + </Trans> + </Text> + <View style={[s.mt10]} /> + <Text style={pal.textLight}> + <Trans> + External media may allow websites to collect information about you and + your device. No information is sent or requested until you press the + "play" button. + </Trans> + </Text> + <View style={[s.mt20]} /> + <TouchableOpacity + testID="enableAllBtn" + onPress={onShowAllPress} + accessibilityRole="button" + accessibilityLabel={_( + msg`Show embeds from ${externalEmbedLabels[source]}`, + )} + accessibilityHint="" + onAccessibilityEscape={closeModal}> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[styles.btn]}> + <Text style={[s.white, s.bold, s.f18]}> + <Trans>Enable External Media</Trans> + </Text> + </LinearGradient> + </TouchableOpacity> + <View style={[s.mt10]} /> + <TouchableOpacity + testID="enableSourceBtn" + onPress={onShowPress} + accessibilityRole="button" + accessibilityLabel={_( + msg`Never load embeds from ${externalEmbedLabels[source]}`, + )} + accessibilityHint="" + onAccessibilityEscape={closeModal}> + <View style={[styles.btn, pal.btn]}> + <Text style={[pal.text, s.bold, s.f18]}> + <Trans>Enable {externalEmbedLabels[source]} only</Trans> + </Text> + </View> + </TouchableOpacity> + <View style={[s.mt10]} /> + <TouchableOpacity + testID="disableSourceBtn" + onPress={onHidePress} + accessibilityRole="button" + accessibilityLabel={_( + msg`Never load embeds from ${externalEmbedLabels[source]}`, + )} + accessibilityHint="" + onAccessibilityEscape={closeModal}> + <View style={[styles.btn, pal.btn]}> + <Text style={[pal.text, s.bold, s.f18]}> + <Trans>No thanks</Trans> + </Text> + </View> + </TouchableOpacity> + </ScrollView> + ) +} + +const styles = StyleSheet.create({ + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + marginBottom: 12, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + borderRadius: 32, + padding: 14, + backgroundColor: colors.gray1, + }, +}) diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx new file mode 100644 index 000000000..86bb46ca8 --- /dev/null +++ b/src/view/com/modals/InAppBrowserConsent.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' + +import {s} from 'lib/styles' +import {Text} from '../util/text/Text' +import {Button} from '../util/forms/Button' +import {ScrollView} from './util' +import {usePalette} from 'lib/hooks/usePalette' + +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useModalControls} from '#/state/modals' +import { + useOpenLink, + useSetInAppBrowser, +} from '#/state/preferences/in-app-browser' + +export const snapPoints = [350] + +export function Component({href}: {href: string}) { + const pal = usePalette('default') + const {closeModal} = useModalControls() + const {_} = useLingui() + const setInAppBrowser = useSetInAppBrowser() + const openLink = useOpenLink() + + const onUseIAB = React.useCallback(() => { + setInAppBrowser(true) + closeModal() + openLink(href, true) + }, [closeModal, setInAppBrowser, href, openLink]) + + const onUseLinking = React.useCallback(() => { + setInAppBrowser(false) + closeModal() + openLink(href, false) + }, [closeModal, setInAppBrowser, href, openLink]) + + return ( + <ScrollView + testID="inAppBrowserConsentModal" + style={[s.flex1, pal.view, {paddingHorizontal: 20, paddingTop: 10}]}> + <Text style={[pal.text, styles.title]}> + <Trans>How should we open this link?</Trans> + </Text> + <Text style={pal.text}> + <Trans> + Your choice will be saved, but can be changed later in settings. + </Trans> + </Text> + <View style={[styles.btnContainer]}> + <Button + testID="confirmBtn" + type="inverted" + onPress={onUseIAB} + accessibilityLabel={_(msg`Use in-app browser`)} + accessibilityHint="" + label={_(msg`Use in-app browser`)} + labelContainerStyle={{justifyContent: 'center', padding: 8}} + labelStyle={[s.f18]} + /> + <Button + testID="confirmBtn" + type="inverted" + onPress={onUseLinking} + accessibilityLabel={_(msg`Use my default browser`)} + accessibilityHint="" + label={_(msg`Use my default browser`)} + labelContainerStyle={{justifyContent: 'center', padding: 8}} + labelStyle={[s.f18]} + /> + <Button + testID="cancelBtn" + type="default" + onPress={() => { + closeModal() + }} + accessibilityLabel={_(msg`Cancel`)} + accessibilityHint="" + label="Cancel" + labelContainerStyle={{justifyContent: 'center', padding: 8}} + labelStyle={[s.f18]} + /> + </View> + </ScrollView> + ) +} + +const styles = StyleSheet.create({ + title: { + textAlign: 'center', + fontWeight: 'bold', + fontSize: 24, + marginBottom: 12, + }, + btnContainer: { + marginTop: 20, + flexDirection: 'column', + justifyContent: 'center', + rowGap: 10, + }, +}) diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index 0ebb545cf..c0318df01 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -18,7 +18,7 @@ import {ScrollView} from './util' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import {Trans} from '@lingui/macro' +import {Trans, msg} from '@lingui/macro' import {cleanError} from 'lib/strings/errors' import {useModalControls} from '#/state/modals' import {useInvitesState, useInvitesAPI} from '#/state/invites' @@ -30,6 +30,7 @@ import { useInviteCodesQuery, InviteCodesQueryResponse, } from '#/state/queries/invites' +import {useLingui} from '@lingui/react' export const snapPoints = ['70%'] @@ -49,6 +50,7 @@ export function Component() { export function Inner({invites}: {invites: InviteCodesQueryResponse}) { const pal = usePalette('default') + const {_} = useLingui() const {closeModal} = useModalControls() const {isTabletOrDesktop} = useWebMediaQueries() @@ -75,7 +77,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) { ]}> <Button type="primary" - label="Done" + label={_(msg`Done`)} style={styles.btn} labelStyle={styles.btnLabel} onPress={onClose} @@ -118,7 +120,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) { <Button testID="closeBtn" type="primary" - label="Done" + label={_(msg`Done`)} style={styles.btn} labelStyle={styles.btnLabel} onPress={onClose} @@ -140,15 +142,16 @@ function InviteCode({ invites: InviteCodesQueryResponse }) { const pal = usePalette('default') + const {_} = useLingui() const invitesState = useInvitesState() const {setInviteCopied} = useInvitesAPI() const uses = invite.uses const onPress = React.useCallback(() => { Clipboard.setString(invite.code) - Toast.show('Copied to clipboard') + Toast.show(_(msg`Copied to clipboard`)) setInviteCopied(invite.code) - }, [setInviteCopied, invite]) + }, [setInviteCopied, invite, _]) return ( <View @@ -163,10 +166,10 @@ function InviteCode({ accessibilityRole="button" accessibilityLabel={ invites.available.length === 1 - ? 'Invite codes: 1 available' - : `Invite codes: ${invites.available.length} available` + ? _(msg`Invite codes: 1 available`) + : _(msg`Invite codes: ${invites.available.length} available`) } - accessibilityHint="Opens list of invite codes"> + accessibilityHint={_(msg`Opens list of invite codes`)}> <Text testID={`${testID}-code`} type={used ? 'md' : 'md-bold'} diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx index 39e6cc3e6..81fdc7285 100644 --- a/src/view/com/modals/LinkWarning.tsx +++ b/src/view/com/modals/LinkWarning.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Linking, SafeAreaView, StyleSheet, View} from 'react-native' +import {SafeAreaView, StyleSheet, View} from 'react-native' import {ScrollView} from './util' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' @@ -12,6 +12,7 @@ import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' +import {useOpenLink} from '#/state/preferences/in-app-browser' export const snapPoints = ['50%'] @@ -21,10 +22,11 @@ export function Component({text, href}: {text: string; href: string}) { const {isMobile} = useWebMediaQueries() const {_} = useLingui() const potentiallyMisleading = isPossiblyAUrl(text) + const openLink = useOpenLink() const onPressVisit = () => { closeModal() - Linking.openURL(href) + openLink(href) } return ( diff --git a/src/view/com/modals/ListAddRemoveUsers.tsx b/src/view/com/modals/ListAddRemoveUsers.tsx index 14e16d6bf..27c33f806 100644 --- a/src/view/com/modals/ListAddRemoveUsers.tsx +++ b/src/view/com/modals/ListAddRemoveUsers.tsx @@ -67,7 +67,7 @@ export function Component({ <TextInput testID="searchInput" style={[styles.searchInput, pal.border, pal.text]} - placeholder="Search for users" + placeholder={_(msg`Search for users`)} placeholderTextColor={pal.colors.textLight} value={query} onChangeText={setQuery} @@ -85,7 +85,7 @@ export function Component({ onPress={onPressCancelSearch} accessibilityRole="button" accessibilityLabel={_(msg`Cancel search`)} - accessibilityHint="Exits inputting search query" + accessibilityHint={_(msg`Exits inputting search query`)} onAccessibilityEscape={onPressCancelSearch} hitSlop={HITSLOP_20}> <FontAwesomeIcon @@ -141,7 +141,7 @@ export function Component({ }} accessibilityLabel={_(msg`Done`)} accessibilityHint="" - label="Done" + label={_(msg({message: 'Done', context: 'action'}))} labelContainerStyle={{justifyContent: 'center', padding: 4}} labelStyle={[s.f18]} /> diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 2aac20dac..7f814d971 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -38,6 +38,8 @@ import * as VerifyEmailModal from './VerifyEmail' import * as ChangeEmailModal from './ChangeEmail' import * as SwitchAccountModal from './SwitchAccount' import * as LinkWarningModal from './LinkWarning' +import * as EmbedConsentModal from './EmbedConsent' +import * as InAppBrowserConsentModal from './InAppBrowserConsent' const DEFAULT_SNAPPOINTS = ['90%'] const HANDLE_HEIGHT = 24 @@ -176,6 +178,12 @@ export function ModalsContainer() { } else if (activeModal?.name === 'link-warning') { snapPoints = LinkWarningModal.snapPoints element = <LinkWarningModal.Component {...activeModal} /> + } else if (activeModal?.name === 'embed-consent') { + snapPoints = EmbedConsentModal.snapPoints + element = <EmbedConsentModal.Component {...activeModal} /> + } else if (activeModal?.name === 'in-app-browser-consent') { + snapPoints = InAppBrowserConsentModal.snapPoints + element = <InAppBrowserConsentModal.Component {...activeModal} /> } else { return null } diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 12138f54d..d79663746 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -3,6 +3,7 @@ import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useModals, useModalControls} from '#/state/modals' import type {Modal as ModalIface} from '#/state/modals' @@ -34,9 +35,11 @@ import * as BirthDateSettingsModal from './BirthDateSettings' import * as VerifyEmailModal from './VerifyEmail' import * as ChangeEmailModal from './ChangeEmail' import * as LinkWarningModal from './LinkWarning' +import * as EmbedConsentModal from './EmbedConsent' export function ModalsContainer() { const {isModalActive, activeModals} = useModals() + useWebBodyScrollLock(isModalActive) if (!isModalActive) { return null @@ -62,7 +65,11 @@ function Modal({modal}: {modal: ModalIface}) { } const onPressMask = () => { - if (modal.name === 'crop-image' || modal.name === 'edit-image') { + if ( + modal.name === 'crop-image' || + modal.name === 'edit-image' || + modal.name === 'alt-text-image' + ) { return // dont close on mask presses during crop } closeModal() @@ -129,6 +136,8 @@ function Modal({modal}: {modal: ModalIface}) { element = <ChangeEmailModal.Component /> } else if (modal.name === 'link-warning') { element = <LinkWarningModal.Component {...modal} /> + } else if (modal.name === 'embed-consent') { + element = <EmbedConsentModal.Component {...modal} /> } else { return null } @@ -159,7 +168,8 @@ function Modal({modal}: {modal: ModalIface}) { const styles = StyleSheet.create({ mask: { - position: 'absolute', + // @ts-ignore + position: 'fixed', top: 0, left: 0, width: '100%', diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx index c117023d4..ba7f76db1 100644 --- a/src/view/com/modals/ModerationDetails.tsx +++ b/src/view/com/modals/ModerationDetails.tsx @@ -10,6 +10,8 @@ import {isWeb} from 'platform/detection' import {listUriToHref} from 'lib/strings/url-helpers' import {Button} from '../util/forms/Button' import {useModalControls} from '#/state/modals' +import {useLingui} from '@lingui/react' +import {Trans, msg} from '@lingui/macro' export const snapPoints = [300] @@ -23,19 +25,21 @@ export function Component({ const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() const pal = usePalette('default') + const {_} = useLingui() let name let description if (!moderation.cause) { - name = 'Content Warning' - description = - 'Moderator has chosen to set a general warning on the content.' + name = _(msg`Content Warning`) + description = _( + msg`Moderator has chosen to set a general warning on the content.`, + ) } else if (moderation.cause.type === 'blocking') { if (moderation.cause.source.type === 'list') { const list = moderation.cause.source.list - name = 'User Blocked by List' + name = _(msg`User Blocked by List`) description = ( - <> + <Trans> This user is included in the{' '} <TextLink type="2xl" @@ -44,25 +48,30 @@ export function Component({ style={pal.link} />{' '} list which you have blocked. - </> + </Trans> ) } else { - name = 'User Blocked' - description = 'You have blocked this user. You cannot view their content.' + name = _(msg`User Blocked`) + description = _( + msg`You have blocked this user. You cannot view their content.`, + ) } } else if (moderation.cause.type === 'blocked-by') { - name = 'User Blocks You' - description = 'This user has blocked you. You cannot view their content.' + name = _(msg`User Blocks You`) + description = _( + msg`This user has blocked you. You cannot view their content.`, + ) } else if (moderation.cause.type === 'block-other') { - name = 'Content Not Available' - description = - 'This content is not available because one of the users involved has blocked the other.' + name = _(msg`Content Not Available`) + description = _( + msg`This content is not available because one of the users involved has blocked the other.`, + ) } else if (moderation.cause.type === 'muted') { if (moderation.cause.source.type === 'list') { const list = moderation.cause.source.list - name = <>Account Muted by List</> + name = _(msg`Account Muted by List`) description = ( - <> + <Trans> This user is included the{' '} <TextLink type="2xl" @@ -71,11 +80,11 @@ export function Component({ style={pal.link} />{' '} list which you have muted. - </> + </Trans> ) } else { - name = 'Account Muted' - description = 'You have muted this user.' + name = _(msg`Account Muted`) + description = _(msg`You have muted this user.`) } } else { name = moderation.cause.labelDef.strings[context].en.name diff --git a/src/view/com/modals/ProfilePreview.tsx b/src/view/com/modals/ProfilePreview.tsx index edfbf6a82..77e68db70 100644 --- a/src/view/com/modals/ProfilePreview.tsx +++ b/src/view/com/modals/ProfilePreview.tsx @@ -14,11 +14,14 @@ import {ErrorScreen} from '../util/error/ErrorScreen' import {CenteredView} from '../util/Views' import {cleanError} from '#/lib/strings/errors' import {useProfileShadow} from '#/state/cache/profile-shadow' +import {Trans, msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' export const snapPoints = [520, '100%'] export function Component({did}: {did: string}) { const pal = usePalette('default') + const {_} = useLingui() const moderationOpts = useModerationOpts() const { data: profile, @@ -43,7 +46,7 @@ export function Component({did}: {did: string}) { if (profileError) { return ( <ErrorScreen - title="Oops!" + title={_(msg`Oops!`)} message={cleanError(profileError)} onPressTryAgain={refetchProfile} /> @@ -55,8 +58,8 @@ export function Component({did}: {did: string}) { // should never happen return ( <ErrorScreen - title="Oops!" - message="Something went wrong and we're not sure what." + title={_(msg`Oops!`)} + message={_(msg`Something went wrong and we're not sure what.`)} onPressTryAgain={refetchProfile} /> ) @@ -104,7 +107,7 @@ function ComponentLoaded({ <> <InfoCircleIcon size={21} style={pal.textLight} /> <ThemedText type="xl" fg="light"> - Swipe up to see more + <Trans>Swipe up to see more</Trans> </ThemedText> </> )} diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx index a72da29b4..6e4881adc 100644 --- a/src/view/com/modals/Repost.tsx +++ b/src/view/com/modals/Repost.tsx @@ -37,11 +37,23 @@ export function Component({ style={[styles.actionBtn]} onPress={onRepost} accessibilityRole="button" - accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'} - accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}> + accessibilityLabel={ + isReposted + ? _(msg`Undo repost`) + : _(msg({message: `Repost`, context: 'action'})) + } + accessibilityHint={ + isReposted + ? _(msg`Remove repost`) + : _(msg({message: `Repost`, context: 'action'})) + }> <RepostIcon strokeWidth={2} size={24} style={s.blue3} /> <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> - <Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans> + {!isReposted ? ( + <Trans context="action">Repost</Trans> + ) : ( + <Trans>Undo repost</Trans> + )} </Text> </TouchableOpacity> <TouchableOpacity @@ -49,11 +61,13 @@ export function Component({ style={[styles.actionBtn]} onPress={onQuote} accessibilityRole="button" - accessibilityLabel={_(msg`Quote post`)} + accessibilityLabel={_( + msg({message: `Quote post`, context: 'action'}), + )} accessibilityHint=""> <FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} /> <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> - <Trans>Quote Post</Trans> + <Trans context="action">Quote Post</Trans> </Text> </TouchableOpacity> </View> diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx index 092dd2d32..779a9e71b 100644 --- a/src/view/com/modals/SelfLabel.tsx +++ b/src/view/com/modals/SelfLabel.tsx @@ -92,7 +92,7 @@ export function Component({ testID="sexualLabelBtn" selected={selected.includes('sexual')} left - label="Suggestive" + label={_(msg`Suggestive`)} onSelect={() => toggleAdultLabel('sexual')} accessibilityHint="" style={s.flex1} @@ -100,7 +100,7 @@ export function Component({ <SelectableBtn testID="nudityLabelBtn" selected={selected.includes('nudity')} - label="Nudity" + label={_(msg`Nudity`)} onSelect={() => toggleAdultLabel('nudity')} accessibilityHint="" style={s.flex1} @@ -108,7 +108,7 @@ export function Component({ <SelectableBtn testID="pornLabelBtn" selected={selected.includes('porn')} - label="Porn" + label={_(msg`Porn`)} right onSelect={() => toggleAdultLabel('porn')} accessibilityHint="" @@ -154,7 +154,7 @@ export function Component({ accessibilityLabel={_(msg`Confirm`)} accessibilityHint=""> <Text style={[s.white, s.bold, s.f18]}> - <Trans>Done</Trans> + <Trans context="action">Done</Trans> </Text> </TouchableOpacity> </View> diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx index b30293859..550dffa1c 100644 --- a/src/view/com/modals/ServerInput.tsx +++ b/src/view/com/modals/ServerInput.tsx @@ -101,7 +101,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) { onChangeText={setCustomUrl} accessibilityLabel={_(msg`Custom domain`)} // TODO: Simplify this wording further to be understandable by everyone - accessibilityHint="Use your domain as your Bluesky client service provider" + accessibilityHint={_( + msg`Use your domain as your Bluesky client service provider`, + )} /> <TouchableOpacity testID="customServerSelectBtn" @@ -110,7 +112,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) { accessibilityRole="button" accessibilityLabel={`Confirm service. ${ customUrl === '' - ? 'Button disabled. Input custom domain to proceed.' + ? _(msg`Button disabled. Input custom domain to proceed.`) : '' }`} accessibilityHint="" diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx index 37691e717..c034c4b52 100644 --- a/src/view/com/modals/SwitchAccount.tsx +++ b/src/view/com/modals/SwitchAccount.tsx @@ -62,7 +62,9 @@ function SwitchAccountCard({account}: {account: SessionAccount}) { onPress={isSwitchingAccounts ? undefined : onPressSignout} accessibilityRole="button" accessibilityLabel={_(msg`Sign out`)} - accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}> + accessibilityHint={_( + msg`Signs ${profile?.displayName} out of Bluesky`, + )}> <Text type="lg" style={pal.link}> <Trans>Sign out</Trans> </Text> @@ -92,8 +94,8 @@ function SwitchAccountCard({account}: {account: SessionAccount}) { isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account) } accessibilityRole="button" - accessibilityLabel={`Switch to ${account.handle}`} - accessibilityHint="Switches the account you are logged in to"> + accessibilityLabel={_(msg`Switch to ${account.handle}`)} + accessibilityHint={_(msg`Switches the account you are logged in to`)}> {contents} </TouchableOpacity> ) diff --git a/src/view/com/modals/Threadgate.tsx b/src/view/com/modals/Threadgate.tsx index 0deef185b..0e49fc2f3 100644 --- a/src/view/com/modals/Threadgate.tsx +++ b/src/view/com/modals/Threadgate.tsx @@ -126,10 +126,10 @@ export function Component({ }} style={styles.btn} accessibilityRole="button" - accessibilityLabel={_(msg`Done`)} + accessibilityLabel={_(msg({message: `Done`, context: 'action'}))} accessibilityHint=""> <Text style={[s.white, s.bold, s.f18]}> - <Trans>Done</Trans> + <Trans context="action">Done</Trans> </Text> </TouchableOpacity> </View> diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx index c51f862cc..23adbe1a8 100644 --- a/src/view/com/modals/UserAddRemoveLists.tsx +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -76,10 +76,10 @@ export function Component({ type="default" onPress={onPressDone} style={styles.footerBtn} - accessibilityLabel={_(msg`Done`)} + accessibilityLabel={_(msg({message: `Done`, context: 'action'}))} accessibilityHint="" onAccessibilityEscape={onPressDone} - label="Done" + label={_(msg({message: `Done`, context: 'action'}))} /> </View> </View> @@ -175,12 +175,22 @@ function ListItem({ {sanitizeDisplayName(list.name)} </Text> <Text type="md" style={[pal.textLight]} numberOfLines={1}> - {list.purpose === 'app.bsky.graph.defs#curatelist' && 'User list '} - {list.purpose === 'app.bsky.graph.defs#modlist' && 'Moderation list '} - by{' '} - {list.creator.did === currentAccount?.did - ? 'you' - : sanitizeHandle(list.creator.handle, '@')} + {list.purpose === 'app.bsky.graph.defs#curatelist' && + (list.creator.did === currentAccount?.did ? ( + <Trans>User list by you</Trans> + ) : ( + <Trans> + User list by {sanitizeHandle(list.creator.handle, '@')} + </Trans> + ))} + {list.purpose === 'app.bsky.graph.defs#modlist' && + (list.creator.did === currentAccount?.did ? ( + <Trans>Moderation list by you</Trans> + ) : ( + <Trans> + Moderation list by {sanitizeHandle(list.creator.handle, '@')} + </Trans> + ))} </Text> </View> <View> diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx index 4f2b1aadf..30a57afc5 100644 --- a/src/view/com/modals/VerifyEmail.tsx +++ b/src/view/com/modals/VerifyEmail.tsx @@ -75,7 +75,7 @@ export function Component({showReminder}: {showReminder?: boolean}) { token: confirmationCode.trim(), }) updateCurrentAccount({emailConfirmed: true}) - Toast.show('Email verified') + Toast.show(_(msg`Email verified`)) closeModal() } catch (e) { setError(cleanError(String(e))) @@ -97,9 +97,15 @@ export function Component({showReminder}: {showReminder?: boolean}) { {stage === Stages.Reminder && <ReminderIllustration />} <View style={styles.titleSection}> <Text type="title-lg" style={[pal.text, styles.title]}> - {stage === Stages.Reminder ? 'Please Verify Your Email' : ''} - {stage === Stages.ConfirmCode ? 'Enter Confirmation Code' : ''} - {stage === Stages.Email ? 'Verify Your Email' : ''} + {stage === Stages.Reminder ? ( + <Trans>Please Verify Your Email</Trans> + ) : stage === Stages.Email ? ( + <Trans>Verify Your Email</Trans> + ) : stage === Stages.ConfirmCode ? ( + <Trans>Enter Confirmation Code</Trans> + ) : ( + '' + )} </Text> </View> @@ -133,7 +139,7 @@ export function Component({showReminder}: {showReminder?: boolean}) { size={16} /> <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}> - {currentAccount?.email || '(no email)'} + {currentAccount?.email || _(msg`(no email)`)} </Text> </View> <Pressable @@ -182,7 +188,7 @@ export function Component({showReminder}: {showReminder?: boolean}) { onPress={() => setStage(Stages.Email)} accessibilityLabel={_(msg`Get Started`)} accessibilityHint="" - label="Get Started" + label={_(msg`Get Started`)} labelContainerStyle={{justifyContent: 'center', padding: 4}} labelStyle={[s.f18]} /> @@ -195,7 +201,7 @@ export function Component({showReminder}: {showReminder?: boolean}) { onPress={onSendEmail} accessibilityLabel={_(msg`Send Confirmation Email`)} accessibilityHint="" - label="Send Confirmation Email" + label={_(msg`Send Confirmation Email`)} labelContainerStyle={{ justifyContent: 'center', padding: 4, @@ -207,7 +213,7 @@ export function Component({showReminder}: {showReminder?: boolean}) { type="default" accessibilityLabel={_(msg`I have a code`)} accessibilityHint="" - label="I have a confirmation code" + label={_(msg`I have a confirmation code`)} labelContainerStyle={{ justifyContent: 'center', padding: 4, @@ -224,7 +230,7 @@ export function Component({showReminder}: {showReminder?: boolean}) { onPress={onConfirm} accessibilityLabel={_(msg`Confirm`)} accessibilityHint="" - label="Confirm" + label={_(msg`Confirm`)} labelContainerStyle={{justifyContent: 'center', padding: 4}} labelStyle={[s.f18]} /> @@ -236,10 +242,16 @@ export function Component({showReminder}: {showReminder?: boolean}) { closeModal() }} accessibilityLabel={ - stage === Stages.Reminder ? 'Not right now' : 'Cancel' + stage === Stages.Reminder + ? _(msg`Not right now`) + : _(msg`Cancel`) } accessibilityHint="" - label={stage === Stages.Reminder ? 'Not right now' : 'Cancel'} + label={ + stage === Stages.Reminder + ? _(msg`Not right now`) + : _(msg`Cancel`) + } labelContainerStyle={{justifyContent: 'center', padding: 4}} labelStyle={[s.f18]} /> diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx index a31545c0a..263dd27a2 100644 --- a/src/view/com/modals/Waitlist.tsx +++ b/src/view/com/modals/Waitlist.tsx @@ -48,7 +48,7 @@ export function Component({}: {}) { } else { setError( resBody.error || - 'Something went wrong. Check your email and try again.', + _(msg`Something went wrong. Check your email and try again.`), ) } } catch (e: any) { @@ -75,7 +75,7 @@ export function Component({}: {}) { </Text> <TextInput style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]} - placeholder="Enter your email" + placeholder={_(msg`Enter your email`)} placeholderTextColor={pal.textLight.color} autoCapitalize="none" autoCorrect={false} @@ -86,7 +86,9 @@ export function Component({}: {}) { enterKeyHint="done" accessible={true} accessibilityLabel={_(msg`Email`)} - accessibilityHint="Input your email to get on the Bluesky waitlist" + accessibilityHint={_( + msg`Input your email to get on the Bluesky waitlist`, + )} /> {error ? ( <View style={s.mt10}> @@ -114,7 +116,9 @@ export function Component({}: {}) { <TouchableOpacity onPress={onPressSignup} accessibilityRole="button" - accessibilityHint={`Confirms signing up ${email} to the waitlist`}> + accessibilityHint={_( + msg`Confirms signing up ${email} to the waitlist`, + )}> <LinearGradient colors={[gradients.blueLight.start, gradients.blueLight.end]} start={{x: 0, y: 0}} @@ -130,7 +134,9 @@ export function Component({}: {}) { onPress={onCancel} accessibilityRole="button" accessibilityLabel={_(msg`Cancel waitlist signup`)} - accessibilityHint={`Exits signing up for waitlist with ${email}`} + accessibilityHint={_( + msg`Exits signing up for waitlist with ${email}`, + )} onAccessibilityEscape={onCancel}> <Text type="button-lg" style={pal.textLight}> <Trans>Cancel</Trans> diff --git a/src/view/com/modals/report/InputIssueDetails.tsx b/src/view/com/modals/report/InputIssueDetails.tsx index 2f701b799..2bc86f75e 100644 --- a/src/view/com/modals/report/InputIssueDetails.tsx +++ b/src/view/com/modals/report/InputIssueDetails.tsx @@ -42,7 +42,8 @@ export function InputIssueDetails({ accessibilityHint="Add more details to your report"> <FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} /> <Text style={[pal.text, s.f18, pal.link]}> - <Trans> Back</Trans> + {' '} + <Trans>Back</Trans> </Text> </TouchableOpacity> <View style={[pal.btn, styles.detailsInputContainer]}> diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx index 60c3f06b7..afd0d417d 100644 --- a/src/view/com/modals/report/Modal.tsx +++ b/src/view/com/modals/report/Modal.tsx @@ -44,9 +44,9 @@ export function Component(content: ReportComponentProps) { const {isMobile} = useWebMediaQueries() const [isProcessing, setIsProcessing] = useState(false) const [showDetailsInput, setShowDetailsInput] = useState(false) - const [error, setError] = useState<string>() - const [issue, setIssue] = useState<string>() - const [details, setDetails] = useState<string>() + const [error, setError] = useState<string>('') + const [issue, setIssue] = useState<string>('') + const [details, setDetails] = useState<string>('') const isAccountReport = 'did' in content const subjectKey = isAccountReport ? content.did : content.uri const atUri = useMemo( |
