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