Add "Who can reply" controls [WIP] (#1954)

* Add threadgating

* UI improvements

* More ui work

* Remove comment

* Tweak colors

* Add missing keys

* Tweak sizing

* Only show composer option on non-reply

* Flex wrap fix

* Move the threadgate control to the top of the composer
This commit is contained in:
Paul Frazee 2023-12-10 12:01:34 -08:00 committed by GitHub
parent f5d014d4c7
commit 28fa5e4919
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 883 additions and 148 deletions

View file

@ -16,6 +16,7 @@ import * as ProfilePreviewModal from './ProfilePreview'
import * as ServerInputModal from './ServerInput'
import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate'
import * as CreateOrEditListModal from './CreateOrEditList'
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
import * as ListAddUserModal from './ListAddRemoveUsers'
@ -127,6 +128,9 @@ 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} />

View file

@ -18,6 +18,7 @@ import * as ListAddUserModal from './ListAddRemoveUsers'
import * as DeleteAccountModal from './DeleteAccount'
import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate'
import * as CropImageModal from './crop-image/CropImage.web'
import * as AltTextImageModal from './AltImage'
import * as EditImageModal from './EditImage'
@ -98,6 +99,8 @@ function Modal({modal}: {modal: ModalIface}) {
element = <RepostModal.Component {...modal} />
} 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 === 'waitlist') {

View file

@ -0,0 +1,204 @@
import React, {useState} from 'react'
import {
Pressable,
StyleProp,
StyleSheet,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {Text} from '../util/text/Text'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {ScrollView} from 'view/com/modals/util'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {useMyListsQuery} from '#/state/queries/my-lists'
import isEqual from 'lodash.isequal'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
export const snapPoints = ['60%']
export function Component({
settings,
onChange,
}: {
settings: ThreadgateSetting[]
onChange: (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]}>
Choose "Everybody" or "Nobody"
</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]}>
Or combine these options:
</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()
}}
style={styles.btn}
accessibilityRole="button"
accessibilityLabel={_(msg`Done`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
<Trans>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="xl" 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,
},
})