diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-06-24 23:15:11 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-24 23:15:11 +0100 |
commit | 29aaf09a8b8b199b249cef9123673022fde11c4f (patch) | |
tree | f644ce29826d823b5818825848518364b10c47a5 | |
parent | e0ac7d5bdcb096d6c23658c34da04bfa19579e4f (diff) | |
download | voidsky-29aaf09a8b8b199b249cef9123673022fde11c4f.tar.zst |
Composer - replace threadgate modal with alf dialog (#4329)
* replace threadgate modal with alf dialog * add accessibility to selectable * add aria * hide spinner once fetched * add `hasOpenDialogs` value to context * remove state * Rm loading state * Update the threadgate dialog button theming * Factor out the threadgate editor and add editing to post views * Mark messages for localization * Use colors from mute dialog * Remove unnecessary effect * Reset state on dialog dismiss * Clearer CTA * Fix bugs * Scope keyboard fix * Rm getAreDialogsActive (no longer needed) --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com> Co-authored-by: Paul Frazee <pfrazee@gmail.com>
-rw-r--r-- | src/components/Dialog/index.web.tsx | 5 | ||||
-rw-r--r-- | src/components/WhoCanReply.tsx | 150 | ||||
-rw-r--r-- | src/components/dialogs/EmbedConsent.tsx | 1 | ||||
-rw-r--r-- | src/components/dialogs/ThreadgateEditor.tsx | 218 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 9 | ||||
-rw-r--r-- | src/view/com/composer/threadgate/ThreadgateBtn.tsx | 50 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 3 | ||||
-rw-r--r-- | src/view/com/modals/Threadgate.tsx | 208 |
9 files changed, 334 insertions, 314 deletions
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 35d807b4b..aff1842f7 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -88,7 +88,10 @@ export function Outer({ if (!isOpen) return function handler(e: KeyboardEvent) { - if (e.key === 'Escape') close() + if (e.key === 'Escape') { + e.stopPropagation() + close() + } } document.addEventListener('keydown', handler) diff --git a/src/components/WhoCanReply.tsx b/src/components/WhoCanReply.tsx index cd171a0a4..a73aae850 100644 --- a/src/components/WhoCanReply.tsx +++ b/src/components/WhoCanReply.tsx @@ -17,7 +17,6 @@ import {HITSLOP_10} from '#/lib/constants' import {makeListLink, makeProfileLink} from '#/lib/routes/links' import {logger} from '#/logger' import {isNative} from '#/platform/detection' -import {useModalControls} from '#/state/modals' import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread' import { ThreadgateSetting, @@ -34,6 +33,7 @@ import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Text} from '#/components/Typography' import {TextLink} from '../view/com/util/Link' +import {ThreadgateEditorDialog} from './dialogs/ThreadgateEditor' import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil' interface WhoCanReplyProps { @@ -46,7 +46,15 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { const {_} = useLingui() const t = useTheme() const infoDialogControl = useDialogControl() - const {settings, isRootPost, onPressEdit} = useWhoCanReply(post) + const editDialogControl = useDialogControl() + const agent = useAgent() + const queryClient = useQueryClient() + + const settings = React.useMemo( + () => threadgateViewToSettings(post.threadgate), + [post], + ) + const isRootPost = !('reply' in post.record) if (!isRootPost) { return null @@ -63,6 +71,55 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { ? _(msg`Replies disabled`) : _(msg`Some people can reply`) + const onPressEdit = () => { + if (isNative && Keyboard.isVisible()) { + Keyboard.dismiss() + } + if (isThreadAuthor) { + editDialogControl.open() + } else { + infoDialogControl.open() + } + } + + const onEditConfirm = async (newSettings: ThreadgateSetting[]) => { + if (JSON.stringify(settings) === JSON.stringify(newSettings)) { + return + } + try { + if (newSettings.length) { + await createThreadgate(agent, post.uri, newSettings) + } else { + await agent.api.com.atproto.repo.deleteRecord({ + repo: agent.session!.did, + collection: 'app.bsky.feed.threadgate', + rkey: new AtUri(post.uri).rkey, + }) + } + await whenAppViewReady(agent, post.uri, res => { + const thread = res.data.thread + if (AppBskyFeedDefs.isThreadViewPost(thread)) { + const fetchedSettings = threadgateViewToSettings( + thread.post.threadgate, + ) + return JSON.stringify(fetchedSettings) === JSON.stringify(newSettings) + } + return false + }) + Toast.show(_(msg`Thread settings updated`)) + queryClient.invalidateQueries({ + queryKey: [POST_THREAD_RQKEY_ROOT], + }) + } catch (err) { + Toast.show( + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), + ) + logger.error('Failed to edit threadgate', {message: err}) + } + } + return ( <> <Button @@ -93,7 +150,18 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) { </View> )} </Button> - <WhoCanReplyDialog control={infoDialogControl} post={post} /> + <WhoCanReplyDialog + control={infoDialogControl} + post={post} + settings={settings} + /> + {isThreadAuthor && ( + <ThreadgateEditorDialog + control={editDialogControl} + threadgate={settings} + onConfirm={onEditConfirm} + /> + )} </> ) } @@ -113,24 +181,31 @@ function Icon({ return <IconComponent fill={color} width={width} /> } -export function WhoCanReplyDialog({ +function WhoCanReplyDialog({ control, post, + settings, }: { control: Dialog.DialogControlProps post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] }) { return ( <Dialog.Outer control={control}> <Dialog.Handle /> - <WhoCanReplyDialogInner post={post} /> + <WhoCanReplyDialogInner post={post} settings={settings} /> </Dialog.Outer> ) } -function WhoCanReplyDialogInner({post}: {post: AppBskyFeedDefs.PostView}) { +function WhoCanReplyDialogInner({ + post, + settings, +}: { + post: AppBskyFeedDefs.PostView + settings: ThreadgateSetting[] +}) { const {_} = useLingui() - const {settings} = useWhoCanReply(post) return ( <Dialog.ScrollableInner label={_(msg`Who can reply dialog`)} @@ -245,67 +320,6 @@ function Separator({i, length}: {i: number; length: number}) { return <>, </> } -function useWhoCanReply(post: AppBskyFeedDefs.PostView) { - const agent = useAgent() - const queryClient = useQueryClient() - const {openModal} = useModalControls() - - const settings = React.useMemo( - () => threadgateViewToSettings(post.threadgate), - [post], - ) - const isRootPost = !('reply' in post.record) - - const onPressEdit = () => { - if (isNative && Keyboard.isVisible()) { - Keyboard.dismiss() - } - openModal({ - name: 'threadgate', - settings, - async onConfirm(newSettings: ThreadgateSetting[]) { - if (JSON.stringify(settings) === JSON.stringify(newSettings)) { - return - } - try { - if (newSettings.length) { - await createThreadgate(agent, post.uri, newSettings) - } else { - await agent.api.com.atproto.repo.deleteRecord({ - repo: agent.session!.did, - collection: 'app.bsky.feed.threadgate', - rkey: new AtUri(post.uri).rkey, - }) - } - await whenAppViewReady(agent, post.uri, res => { - const thread = res.data.thread - if (AppBskyFeedDefs.isThreadViewPost(thread)) { - const fetchedSettings = threadgateViewToSettings( - thread.post.threadgate, - ) - return ( - JSON.stringify(fetchedSettings) === JSON.stringify(newSettings) - ) - } - return false - }) - Toast.show('Thread settings updated') - queryClient.invalidateQueries({ - queryKey: [POST_THREAD_RQKEY_ROOT], - }) - } catch (err) { - Toast.show( - 'There was an issue. Please check your internet connection and try again.', - ) - logger.error('Failed to edit threadgate', {message: err}) - } - }, - }) - } - - return {settings, isRootPost, onPressEdit} -} - async function whenAppViewReady( agent: BskyAgent, uri: string, diff --git a/src/components/dialogs/EmbedConsent.tsx b/src/components/dialogs/EmbedConsent.tsx index c3fefd9f0..f7e614597 100644 --- a/src/components/dialogs/EmbedConsent.tsx +++ b/src/components/dialogs/EmbedConsent.tsx @@ -113,6 +113,7 @@ export function EmbedConsentDialog({ </ButtonText> </Button> </View> + <Dialog.Close /> </Dialog.ScrollableInner> </Dialog.Outer> ) diff --git a/src/components/dialogs/ThreadgateEditor.tsx b/src/components/dialogs/ThreadgateEditor.tsx new file mode 100644 index 000000000..75383764f --- /dev/null +++ b/src/components/dialogs/ThreadgateEditor.tsx @@ -0,0 +1,218 @@ +import React from 'react' +import {StyleProp, View, ViewStyle} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import isEqual from 'lodash.isequal' + +import {useMyListsQuery} from '#/state/queries/my-lists' +import {ThreadgateSetting} from '#/state/queries/threadgate' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {Text} from '#/components/Typography' + +interface ThreadgateEditorDialogProps { + control: Dialog.DialogControlProps + threadgate: ThreadgateSetting[] + onChange?: (v: ThreadgateSetting[]) => void + onConfirm?: (v: ThreadgateSetting[]) => void +} + +export function ThreadgateEditorDialog({ + control, + threadgate, + onChange, + onConfirm, +}: ThreadgateEditorDialogProps) { + return ( + <Dialog.Outer control={control}> + <Dialog.Handle /> + <DialogContent + seedThreadgate={threadgate} + onChange={onChange} + onConfirm={onConfirm} + /> + </Dialog.Outer> + ) +} + +function DialogContent({ + seedThreadgate, + onChange, + onConfirm, +}: { + seedThreadgate: ThreadgateSetting[] + onChange?: (v: ThreadgateSetting[]) => void + onConfirm?: (v: ThreadgateSetting[]) => void +}) { + const {_} = useLingui() + const control = Dialog.useDialogContext() + const {data: lists} = useMyListsQuery('curate') + const [draft, setDraft] = React.useState(seedThreadgate) + + const [prevSeedThreadgate, setPrevSeedThreadgate] = + React.useState(seedThreadgate) + if (seedThreadgate !== prevSeedThreadgate) { + // New data flowed from above (e.g. due to update coming through). + setPrevSeedThreadgate(seedThreadgate) + setDraft(seedThreadgate) // Reset draft. + } + + function updateThreadgate(nextThreadgate: ThreadgateSetting[]) { + setDraft(nextThreadgate) + onChange?.(nextThreadgate) + } + + const onPressEverybody = () => { + updateThreadgate([]) + } + + const onPressNobody = () => { + updateThreadgate([{type: 'nobody'}]) + } + + const onPressAudience = (setting: ThreadgateSetting) => { + // remove nobody + let newSelected = draft.filter(v => v.type !== 'nobody') + // toggle + const i = newSelected.findIndex(v => isEqual(v, setting)) + if (i === -1) { + newSelected.push(setting) + } else { + newSelected.splice(i, 1) + } + updateThreadgate(newSelected) + } + + const doneLabel = onConfirm ? _(msg`Save`) : _(msg`Done`) + return ( + <Dialog.ScrollableInner + label={_(msg`Choose who can reply`)} + style={[{maxWidth: 500}, a.w_full]}> + <View style={[a.flex_1, a.gap_md]}> + <Text style={[a.text_2xl, a.font_bold]}> + <Trans>Chose who can reply</Trans> + </Text> + <Text style={a.mt_xs}> + <Trans>Either choose "Everybody" or "Nobody"</Trans> + </Text> + <View style={[a.flex_row, a.gap_sm]}> + <Selectable + label={_(msg`Everybody`)} + isSelected={draft.length === 0} + onPress={onPressEverybody} + style={{flex: 1}} + /> + <Selectable + label={_(msg`Nobody`)} + isSelected={!!draft.find(v => v.type === 'nobody')} + onPress={onPressNobody} + style={{flex: 1}} + /> + </View> + <Text style={a.mt_md}> + <Trans>Or combine these options:</Trans> + </Text> + <View style={[a.gap_sm]}> + <Selectable + label={_(msg`Mentioned users`)} + isSelected={!!draft.find(v => v.type === 'mention')} + onPress={() => onPressAudience({type: 'mention'})} + /> + <Selectable + label={_(msg`Followed users`)} + isSelected={!!draft.find(v => v.type === 'following')} + onPress={() => onPressAudience({type: 'following'})} + /> + {lists && lists.length > 0 + ? lists.map(list => ( + <Selectable + key={list.uri} + label={_(msg`Users in "${list.name}"`)} + isSelected={ + !!draft.find(v => v.type === 'list' && v.list === list.uri) + } + onPress={() => + onPressAudience({type: 'list', list: list.uri}) + } + /> + )) + : // No loading states to avoid jumps for the common case (no lists) + null} + </View> + </View> + <Button + label={doneLabel} + onPress={() => { + control.close() + onConfirm?.(draft) + }} + onAccessibilityEscape={control.close} + color="primary" + size="medium" + variant="solid" + style={a.mt_xl}> + <ButtonText>{doneLabel}</ButtonText> + </Button> + <Dialog.Close /> + </Dialog.ScrollableInner> + ) +} + +function Selectable({ + label, + isSelected, + onPress, + style, +}: { + label: string + isSelected: boolean + onPress: () => void + style?: StyleProp<ViewStyle> +}) { + const t = useTheme() + return ( + <Button + onPress={onPress} + label={label} + accessibilityHint="Select this option" + accessibilityRole="checkbox" + aria-checked={isSelected} + accessibilityState={{ + checked: isSelected, + }} + style={a.flex_1}> + {({hovered, focused}) => ( + <View + style={[ + a.flex_1, + a.flex_row, + a.align_center, + a.justify_between, + a.rounded_sm, + a.p_md, + {height: 40}, // for consistency with checkmark icon visible or not + t.atoms.bg_contrast_50, + (hovered || focused) && t.atoms.bg_contrast_100, + isSelected && { + backgroundColor: + t.name === 'light' + ? t.palette.primary_50 + : t.palette.primary_975, + }, + style, + ]}> + <Text style={[a.text_sm, isSelected && a.font_semibold]}> + {label} + </Text> + {isSelected ? ( + <Check size="sm" fill={t.palette.primary_500} /> + ) : ( + <View /> + )} + </View> + )} + </Button> + ) +} diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 685b10bd8..529dc5590 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -5,7 +5,6 @@ import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {GalleryModel} from '#/state/models/media/gallery' import {ImageModel} from '#/state/models/media/image' -import {ThreadgateSetting} from '../queries/threadgate' export interface EditProfileModal { name: 'edit-profile' @@ -67,13 +66,6 @@ export interface SelfLabelModal { onChange: (labels: string[]) => void } -export interface ThreadgateModal { - name: 'threadgate' - settings: ThreadgateSetting[] - onChange?: (settings: ThreadgateSetting[]) => void - onConfirm?: (settings: ThreadgateSetting[]) => void -} - export interface ChangeHandleModal { name: 'change-handle' onChanged: () => void @@ -149,7 +141,6 @@ export type Modal = | CropImageModal | EditImageModal | SelfLabelModal - | ThreadgateModal // Bluesky access | WaitlistModal diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx index 2aefdfbbf..6cf2eea2c 100644 --- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx +++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx @@ -5,11 +5,12 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isNative} from '#/platform/detection' -import {useModalControls} from '#/state/modals' import {ThreadgateSetting} from '#/state/queries/threadgate' import {useAnalytics} from 'lib/analytics/analytics' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {ThreadgateEditorDialog} from '#/components/dialogs/ThreadgateEditor' import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign' import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' @@ -26,18 +27,15 @@ export function ThreadgateBtn({ const {track} = useAnalytics() const {_} = useLingui() const t = useTheme() - const {openModal} = useModalControls() + const control = Dialog.useDialogControl() const onPress = () => { track('Composer:ThreadgateOpened') if (isNative && Keyboard.isVisible()) { Keyboard.dismiss() } - openModal({ - name: 'threadgate', - settings: threadgate, - onChange, - }) + + control.open() } const isEverybody = threadgate.length === 0 @@ -49,19 +47,29 @@ export function ThreadgateBtn({ : _(msg`Some people can reply`) return ( - <Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}> - <Button - variant="solid" - color="secondary" - size="xsmall" - testID="openReplyGateButton" - onPress={onPress} - label={label}> - <ButtonIcon - icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group} - /> - <ButtonText>{label}</ButtonText> - </Button> - </Animated.View> + <> + <Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}> + <Button + variant="solid" + color="secondary" + size="xsmall" + testID="openReplyGateButton" + onPress={onPress} + label={label} + accessibilityHint={_( + msg`Opens a dialog to choose who can reply to this thread`, + )}> + <ButtonIcon + icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group} + /> + <ButtonText>{label}</ButtonText> + </Button> + </Animated.View> + <ThreadgateEditorDialog + control={control} + threadgate={threadgate} + onChange={onChange} + /> + </> ) } diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index ecfe5806e..3455e1cdf 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -23,7 +23,6 @@ import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettin import * as LinkWarningModal from './LinkWarning' import * as ListAddUserModal from './ListAddRemoveUsers' import * as SelfLabelModal from './SelfLabel' -import * as ThreadgateModal from './Threadgate' import * as UserAddRemoveListsModal from './UserAddRemoveLists' import * as VerifyEmailModal from './VerifyEmail' @@ -76,9 +75,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'self-label') { snapPoints = SelfLabelModal.snapPoints element = <SelfLabelModal.Component {...activeModal} /> - } else if (activeModal?.name === 'threadgate') { - snapPoints = ThreadgateModal.snapPoints - element = <ThreadgateModal.Component {...activeModal} /> } else if (activeModal?.name === 'alt-text-image') { snapPoints = AltImageModal.snapPoints element = <AltImageModal.Component {...activeModal} /> diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 14ee99e57..c4bab6fb1 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -23,7 +23,6 @@ import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettin import * as LinkWarningModal from './LinkWarning' import * as ListAddUserModal from './ListAddRemoveUsers' import * as SelfLabelModal from './SelfLabel' -import * as ThreadgateModal from './Threadgate' import * as UserAddRemoveLists from './UserAddRemoveLists' import * as VerifyEmailModal from './VerifyEmail' @@ -84,8 +83,6 @@ function Modal({modal}: {modal: ModalIface}) { element = <DeleteAccountModal.Component /> } else if (modal.name === 'self-label') { element = <SelfLabelModal.Component {...modal} /> - } else if (modal.name === 'threadgate') { - element = <ThreadgateModal.Component {...modal} /> } else if (modal.name === 'change-handle') { element = <ChangeHandleModal.Component {...modal} /> } else if (modal.name === 'invite-codes') { diff --git a/src/view/com/modals/Threadgate.tsx b/src/view/com/modals/Threadgate.tsx deleted file mode 100644 index 4a9a9e2ab..000000000 --- a/src/view/com/modals/Threadgate.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import React, {useState} from 'react' -import { - Pressable, - StyleProp, - StyleSheet, - TouchableOpacity, - View, - ViewStyle, -} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import isEqual from 'lodash.isequal' - -import {useModalControls} from '#/state/modals' -import {useMyListsQuery} from '#/state/queries/my-lists' -import {ThreadgateSetting} from '#/state/queries/threadgate' -import {usePalette} from 'lib/hooks/usePalette' -import {colors, s} from 'lib/styles' -import {isWeb} from 'platform/detection' -import {ScrollView} from 'view/com/modals/util' -import {Text} from '../util/text/Text' - -export const snapPoints = ['60%'] - -export function Component({ - settings, - onChange, - onConfirm, -}: { - settings: ThreadgateSetting[] - onChange?: (settings: ThreadgateSetting[]) => void - onConfirm?: (settings: ThreadgateSetting[]) => void -}) { - const pal = usePalette('default') - const {closeModal} = useModalControls() - const [selected, setSelected] = useState(settings) - const {_} = useLingui() - const {data: lists} = useMyListsQuery('curate') - - const onPressEverybody = () => { - setSelected([]) - onChange?.([]) - } - - const onPressNobody = () => { - setSelected([{type: 'nobody'}]) - onChange?.([{type: 'nobody'}]) - } - - const onPressAudience = (setting: ThreadgateSetting) => { - // remove nobody - let newSelected = selected.filter(v => v.type !== 'nobody') - // toggle - const i = newSelected.findIndex(v => isEqual(v, setting)) - if (i === -1) { - newSelected.push(setting) - } else { - newSelected.splice(i, 1) - } - setSelected(newSelected) - onChange?.(newSelected) - } - - return ( - <View testID="threadgateModal" style={[pal.view, styles.container]}> - <View style={styles.titleSection}> - <Text type="title-lg" style={[pal.text, styles.title]}> - <Trans>Who can reply</Trans> - </Text> - </View> - - <ScrollView> - <Text style={[pal.text, styles.description]}> - <Trans>Choose "Everybody" or "Nobody"</Trans> - </Text> - <View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}> - <Selectable - label={_(msg`Everybody`)} - isSelected={selected.length === 0} - onPress={onPressEverybody} - style={{flex: 1}} - /> - <Selectable - label={_(msg`Nobody`)} - isSelected={!!selected.find(v => v.type === 'nobody')} - onPress={onPressNobody} - style={{flex: 1}} - /> - </View> - <Text style={[pal.text, styles.description]}> - <Trans>Or combine these options:</Trans> - </Text> - <View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}> - <Selectable - label={_(msg`Mentioned users`)} - isSelected={!!selected.find(v => v.type === 'mention')} - onPress={() => onPressAudience({type: 'mention'})} - /> - <Selectable - label={_(msg`Followed users`)} - isSelected={!!selected.find(v => v.type === 'following')} - onPress={() => onPressAudience({type: 'following'})} - /> - {lists?.length - ? lists.map(list => ( - <Selectable - key={list.uri} - label={_(msg`Users in "${list.name}"`)} - isSelected={ - !!selected.find( - v => v.type === 'list' && v.list === list.uri, - ) - } - onPress={() => - onPressAudience({type: 'list', list: list.uri}) - } - /> - )) - : null} - </View> - </ScrollView> - - <View style={[styles.btnContainer, pal.borderDark]}> - <TouchableOpacity - testID="confirmBtn" - onPress={() => { - closeModal() - onConfirm?.(selected) - }} - style={styles.btn} - accessibilityRole="button" - accessibilityLabel={_(msg({message: `Done`, context: 'action'}))} - accessibilityHint=""> - <Text style={[s.white, s.bold, s.f18]}> - <Trans context="action">Done</Trans> - </Text> - </TouchableOpacity> - </View> - </View> - ) -} - -function Selectable({ - label, - isSelected, - onPress, - style, -}: { - label: string - isSelected: boolean - onPress: () => void - style?: StyleProp<ViewStyle> -}) { - const pal = usePalette(isSelected ? 'inverted' : 'default') - return ( - <Pressable - onPress={onPress} - accessibilityLabel={label} - accessibilityHint="" - style={[styles.selectable, pal.border, pal.view, style]}> - <Text type="lg" style={[pal.text]}> - {label} - </Text> - {isSelected ? ( - <FontAwesomeIcon icon="check" color={pal.colors.text} size={18} /> - ) : null} - </Pressable> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingBottom: isWeb ? 0 : 40, - }, - titleSection: { - paddingTop: isWeb ? 0 : 4, - }, - title: { - textAlign: 'center', - fontWeight: '600', - }, - description: { - textAlign: 'center', - paddingVertical: 16, - }, - selectable: { - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: 18, - paddingVertical: 16, - borderWidth: 1, - borderRadius: 6, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - backgroundColor: colors.blue3, - }, - btnContainer: { - paddingTop: 20, - paddingHorizontal: 20, - }, -}) |