diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 35d807b4..aff1842f 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 cd171a0a..a73aae85 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 (
<>
-
+
+ {isThreadAuthor && (
+
+ )}
>
)
}
@@ -113,24 +181,31 @@ function Icon({
return
}
-export function WhoCanReplyDialog({
+function WhoCanReplyDialog({
control,
post,
+ settings,
}: {
control: Dialog.DialogControlProps
post: AppBskyFeedDefs.PostView
+ settings: ThreadgateSetting[]
}) {
return (
-
+
)
}
-function WhoCanReplyDialogInner({post}: {post: AppBskyFeedDefs.PostView}) {
+function WhoCanReplyDialogInner({
+ post,
+ settings,
+}: {
+ post: AppBskyFeedDefs.PostView
+ settings: ThreadgateSetting[]
+}) {
const {_} = useLingui()
- const {settings} = useWhoCanReply(post)
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 c3fefd9f..f7e61459 100644
--- a/src/components/dialogs/EmbedConsent.tsx
+++ b/src/components/dialogs/EmbedConsent.tsx
@@ -113,6 +113,7 @@ export function EmbedConsentDialog({
+
)
diff --git a/src/components/dialogs/ThreadgateEditor.tsx b/src/components/dialogs/ThreadgateEditor.tsx
new file mode 100644
index 00000000..75383764
--- /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 (
+
+
+
+
+ )
+}
+
+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 (
+
+
+
+ Chose who can reply
+
+
+ Either choose "Everybody" or "Nobody"
+
+
+
+ v.type === 'nobody')}
+ onPress={onPressNobody}
+ style={{flex: 1}}
+ />
+
+
+ Or combine these options:
+
+
+ v.type === 'mention')}
+ onPress={() => onPressAudience({type: 'mention'})}
+ />
+ v.type === 'following')}
+ onPress={() => onPressAudience({type: 'following'})}
+ />
+ {lists && lists.length > 0
+ ? lists.map(list => (
+ 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}
+
+
+
+
+
+ )
+}
+
+function Selectable({
+ label,
+ isSelected,
+ onPress,
+ style,
+}: {
+ label: string
+ isSelected: boolean
+ onPress: () => void
+ style?: StyleProp
+}) {
+ const t = useTheme()
+ return (
+
+ )
+}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 685b10bd..529dc559 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 2aefdfbb..6cf2eea2 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 (
-
-
-
+ <>
+
+
+
+
+ >
)
}
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index ecfe5806..3455e1cd 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 =
- } else if (activeModal?.name === 'threadgate') {
- snapPoints = ThreadgateModal.snapPoints
- element =
} else if (activeModal?.name === 'alt-text-image') {
snapPoints = AltImageModal.snapPoints
element =
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 14ee99e5..c4bab6fb 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 =
} else if (modal.name === 'self-label') {
element =
- } else if (modal.name === 'threadgate') {
- element =
} else if (modal.name === 'change-handle') {
element =
} 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 4a9a9e2a..00000000
--- 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 (
-
-
-
- Who can reply
-
-
-
-
-
- Choose "Everybody" or "Nobody"
-
-
-
- v.type === 'nobody')}
- onPress={onPressNobody}
- style={{flex: 1}}
- />
-
-
- Or combine these options:
-
-
- v.type === 'mention')}
- onPress={() => onPressAudience({type: 'mention'})}
- />
- v.type === 'following')}
- onPress={() => onPressAudience({type: 'following'})}
- />
- {lists?.length
- ? lists.map(list => (
- v.type === 'list' && v.list === list.uri,
- )
- }
- onPress={() =>
- onPressAudience({type: 'list', list: list.uri})
- }
- />
- ))
- : null}
-
-
-
-
- {
- closeModal()
- onConfirm?.(selected)
- }}
- style={styles.btn}
- accessibilityRole="button"
- accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
- accessibilityHint="">
-
- Done
-
-
-
-
- )
-}
-
-function Selectable({
- label,
- isSelected,
- onPress,
- style,
-}: {
- label: string
- isSelected: boolean
- onPress: () => void
- style?: StyleProp
-}) {
- const pal = usePalette(isSelected ? 'inverted' : 'default')
- return (
-
-
- {label}
-
- {isSelected ? (
-
- ) : null}
-
- )
-}
-
-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,
- },
-})