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>
This commit is contained in:
parent
e0ac7d5bdc
commit
29aaf09a8b
9 changed files with 334 additions and 314 deletions
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue