From 28fa5e4919ccf24073ccc92d88efb7e4b73b0b2b Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sun, 10 Dec 2023 12:01:34 -0800 Subject: [PATCH] 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 --- src/lib/analytics/types.ts | 1 + src/lib/api/index.ts | 52 +++- src/locale/locales/cs/messages.po | 37 +++ src/locale/locales/en/messages.po | 33 ++ src/locale/locales/es/messages.po | 37 +++ src/locale/locales/fr/messages.po | 37 +++ src/locale/locales/hi/messages.po | 33 ++ src/state/modals/index.tsx | 8 + src/state/queries/threadgate.ts | 5 + src/view/com/composer/Composer.tsx | 13 + src/view/com/composer/Prompt.tsx | 2 +- src/view/com/composer/labels/LabelsBtn.tsx | 5 +- .../text-input/web/EmojiPicker.web.tsx | 3 +- .../com/composer/threadgate/ThreadgateBtn.tsx | 68 ++++ src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/Modal.web.tsx | 3 + src/view/com/modals/Threadgate.tsx | 204 ++++++++++++ src/view/com/post-thread/PostThread.tsx | 2 +- src/view/com/post-thread/PostThreadItem.tsx | 290 +++++++++--------- src/view/com/threadgate/WhoCanReply.tsx | 183 +++++++++++ src/view/com/util/post-ctrls/PostCtrls.tsx | 11 +- 21 files changed, 883 insertions(+), 148 deletions(-) create mode 100644 src/state/queries/threadgate.ts create mode 100644 src/view/com/composer/threadgate/ThreadgateBtn.tsx create mode 100644 src/view/com/modals/Threadgate.tsx create mode 100644 src/view/com/threadgate/WhoCanReply.tsx diff --git a/src/lib/analytics/types.ts b/src/lib/analytics/types.ts index 9883cc36..3d2ebb31 100644 --- a/src/lib/analytics/types.ts +++ b/src/lib/analytics/types.ts @@ -21,6 +21,7 @@ interface TrackPropertiesMap { 'Composer:PastedPhotos': {} 'Composer:CameraOpened': {} 'Composer:GalleryOpened': {} + 'Composer:ThreadgateOpened': {} 'HomeScreen:PressCompose': {} 'ProfileScreen:PressCompose': {} // EDIT PROFILE events diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index a78abcac..d94ee464 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -3,6 +3,7 @@ import { AppBskyEmbedExternal, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, + AppBskyFeedThreadgate, AppBskyRichtextFacet, BskyAgent, ComAtprotoLabelDefs, @@ -16,6 +17,7 @@ import {isWeb} from 'platform/detection' import {ImageModel} from 'state/models/media/image' import {shortenLinks} from 'lib/strings/rich-text-manip' import {logger} from '#/logger' +import {ThreadgateSetting} from '#/state/queries/threadgate' export interface ExternalEmbedDraft { uri: string @@ -54,6 +56,7 @@ interface PostOpts { extLink?: ExternalEmbedDraft images?: ImageModel[] labels?: string[] + threadgate?: ThreadgateSetting[] onStateChange?: (state: string) => void langs?: string[] } @@ -227,9 +230,10 @@ export async function post(agent: BskyAgent, opts: PostOpts) { langs = opts.langs.slice(0, 3) } + let res try { opts.onStateChange?.('Posting...') - return await agent.post({ + res = await agent.post({ text: rt.text, facets: rt.facets, reply, @@ -247,6 +251,52 @@ export async function post(agent: BskyAgent, opts: PostOpts) { throw e } } + + try { + // TODO: this needs to be batch-created with the post! + if (opts.threadgate?.length) { + await createThreadgate(agent, res.uri, opts.threadgate) + } + } catch (e: any) { + console.error(`Failed to create threadgate: ${e.toString()}`) + throw new Error( + 'Post reply-controls failed to be set. Your post was created but anyone can reply to it.', + ) + } + + return res +} + +async function createThreadgate( + agent: BskyAgent, + postUri: string, + threadgate: ThreadgateSetting[], +) { + let allow: ( + | AppBskyFeedThreadgate.MentionRule + | AppBskyFeedThreadgate.FollowingRule + | AppBskyFeedThreadgate.ListRule + )[] = [] + if (!threadgate.find(v => v.type === 'nobody')) { + for (const rule of threadgate) { + if (rule.type === 'mention') { + allow.push({$type: 'app.bsky.feed.threadgate#mentionRule'}) + } else if (rule.type === 'following') { + allow.push({$type: 'app.bsky.feed.threadgate#followingRule'}) + } else if (rule.type === 'list') { + allow.push({ + $type: 'app.bsky.feed.threadgate#listRule', + list: rule.list, + }) + } + } + } + + const postUrip = new AtUri(postUri) + await agent.api.app.bsky.feed.threadgate.create( + {repo: agent.session!.did, rkey: postUrip.rkey}, + {post: postUri, createdAt: new Date().toISOString(), allow}, + ) } // helpers diff --git a/src/locale/locales/cs/messages.po b/src/locale/locales/cs/messages.po index 48c587eb..ce4ebf11 100644 --- a/src/locale/locales/cs/messages.po +++ b/src/locale/locales/cs/messages.po @@ -51,6 +51,10 @@ msgstr "" msgid "{message}" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:130 +msgid "<0/> members" +msgstr "" + #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your<1>Recommended<2>Feeds" msgstr "" @@ -804,6 +808,10 @@ msgstr "" msgid "Error:" msgstr "" +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "" + #: src/view/com/lightbox/Lightbox.web.tsx:156 msgid "Expand alt text" msgstr "" @@ -866,6 +874,10 @@ msgstr "" msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." msgstr "" +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:145 msgid "Followed users only" msgstr "" @@ -1360,6 +1372,10 @@ msgstr "" msgid "No results found for {query}" msgstr "" +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "" + #: src/view/com/modals/SelfLabel.tsx:136 #~ msgid "Not Applicable" #~ msgstr "" @@ -1649,6 +1665,10 @@ msgstr "" msgid "Removed from list" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:74 +msgid "Replies to this thread are disabled" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:135 msgid "Reply Filters" msgstr "" @@ -2241,6 +2261,14 @@ msgstr "" msgid "Users" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:115 +msgid "Users followed by <0/>" +msgstr "" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "" + #: src/view/screens/Settings.tsx:750 msgid "Verify email" msgstr "" @@ -2306,6 +2334,15 @@ msgstr "" msgid "Which languages would you like to see in your algorithmic feeds?" msgstr "" +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:43 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "" + +#: src/view/com/threadgate/WhoCanReply.tsx:79 +msgid "Who can reply?" +msgstr "" + #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" msgstr "" diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 7a4a182c..e3d076db 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -51,6 +51,10 @@ msgstr "" msgid "{message}" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:130 +msgid "<0/> members" +msgstr "" + #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your<1>Recommended<2>Feeds" msgstr "" @@ -812,6 +816,10 @@ msgstr "" msgid "Error:" msgstr "" +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "" + #: src/view/com/lightbox/Lightbox.web.tsx:156 msgid "Expand alt text" msgstr "" @@ -875,6 +883,10 @@ msgstr "" msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." msgstr "" +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:145 msgid "Followed users only" msgstr "" @@ -1382,6 +1394,10 @@ msgstr "" msgid "No results found for {query}" msgstr "" +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "" + #: src/view/com/modals/SelfLabel.tsx:136 #~ msgid "Not Applicable" #~ msgstr "" @@ -2275,6 +2291,14 @@ msgstr "" msgid "Users" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:115 +msgid "Users followed by <0/>" +msgstr "" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "" + #: src/view/screens/Settings.tsx:750 msgid "Verify email" msgstr "" @@ -2340,6 +2364,15 @@ msgstr "" msgid "Which languages would you like to see in your algorithmic feeds?" msgstr "" +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:43 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "" + +#: src/view/com/threadgate/WhoCanReply.tsx:79 +msgid "Who can reply?" +msgstr "" + #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" msgstr "" diff --git a/src/locale/locales/es/messages.po b/src/locale/locales/es/messages.po index c1fca7e9..04aef65a 100644 --- a/src/locale/locales/es/messages.po +++ b/src/locale/locales/es/messages.po @@ -51,6 +51,10 @@ msgstr "" msgid "{message}" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:130 +msgid "<0/> members" +msgstr "" + #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your<1>Recommended<2>Feeds" msgstr "" @@ -804,6 +808,10 @@ msgstr "" msgid "Error:" msgstr "" +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "" + #: src/view/com/lightbox/Lightbox.web.tsx:156 msgid "Expand alt text" msgstr "" @@ -866,6 +874,10 @@ msgstr "" msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." msgstr "" +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:145 msgid "Followed users only" msgstr "" @@ -1360,6 +1372,10 @@ msgstr "" msgid "No results found for {query}" msgstr "" +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "" + #: src/view/com/modals/SelfLabel.tsx:136 #~ msgid "Not Applicable" #~ msgstr "" @@ -1649,6 +1665,10 @@ msgstr "" msgid "Removed from list" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:74 +msgid "Replies to this thread are disabled" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:135 msgid "Reply Filters" msgstr "" @@ -2241,6 +2261,14 @@ msgstr "" msgid "Users" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:115 +msgid "Users followed by <0/>" +msgstr "" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "" + #: src/view/screens/Settings.tsx:750 msgid "Verify email" msgstr "" @@ -2306,6 +2334,15 @@ msgstr "" msgid "Which languages would you like to see in your algorithmic feeds?" msgstr "" +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:43 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "" + +#: src/view/com/threadgate/WhoCanReply.tsx:79 +msgid "Who can reply?" +msgstr "" + #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" msgstr "" diff --git a/src/locale/locales/fr/messages.po b/src/locale/locales/fr/messages.po index 414e397c..0a195de0 100644 --- a/src/locale/locales/fr/messages.po +++ b/src/locale/locales/fr/messages.po @@ -51,6 +51,10 @@ msgstr "" msgid "{message}" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:130 +msgid "<0/> members" +msgstr "" + #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your<1>Recommended<2>Feeds" msgstr "" @@ -804,6 +808,10 @@ msgstr "" msgid "Error:" msgstr "" +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "" + #: src/view/com/lightbox/Lightbox.web.tsx:156 msgid "Expand alt text" msgstr "" @@ -866,6 +874,10 @@ msgstr "" msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." msgstr "" +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:145 msgid "Followed users only" msgstr "" @@ -1360,6 +1372,10 @@ msgstr "" msgid "No results found for {query}" msgstr "" +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "" + #: src/view/com/modals/SelfLabel.tsx:136 #~ msgid "Not Applicable" #~ msgstr "" @@ -1649,6 +1665,10 @@ msgstr "" msgid "Removed from list" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:74 +msgid "Replies to this thread are disabled" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:135 msgid "Reply Filters" msgstr "" @@ -2241,6 +2261,14 @@ msgstr "" msgid "Users" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:115 +msgid "Users followed by <0/>" +msgstr "" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "" + #: src/view/screens/Settings.tsx:750 msgid "Verify email" msgstr "" @@ -2306,6 +2334,15 @@ msgstr "" msgid "Which languages would you like to see in your algorithmic feeds?" msgstr "" +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:43 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "" + +#: src/view/com/threadgate/WhoCanReply.tsx:79 +msgid "Who can reply?" +msgstr "" + #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" msgstr "" diff --git a/src/locale/locales/hi/messages.po b/src/locale/locales/hi/messages.po index 3e907ee3..7f4ff2dd 100644 --- a/src/locale/locales/hi/messages.po +++ b/src/locale/locales/hi/messages.po @@ -51,6 +51,10 @@ msgstr "" msgid "{message}" msgstr "" +#: src/view/com/threadgate/WhoCanReply.tsx:130 +msgid "<0/> members" +msgstr "" + #: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30 msgid "<0>Choose your<1>Recommended<2>Feeds" msgstr "<0>अपना<1>पसंदीदा<2>फ़ीड चुनें" @@ -808,6 +812,10 @@ msgstr "अपने यूज़रनेम और पासवर्ड द msgid "Error:" msgstr "" +#: src/view/com/modals/Threadgate.tsx:76 +msgid "Everybody" +msgstr "" + #: src/view/com/lightbox/Lightbox.web.tsx:156 msgid "Expand alt text" msgstr "ऑल्ट टेक्स्ट" @@ -867,6 +875,10 @@ msgstr "फॉलो" msgid "Follow some users to get started. We can recommend you more users based on who you find interesting." msgstr "आरंभ करने के लिए कुछ उपयोगकर्ताओं का अनुसरण करें. आपको कौन दिलचस्प लगता है, इसके आधार पर हम आपको और अधिक उपयोगकर्ताओं की अनुशंसा कर सकते हैं।" +#: src/view/com/modals/Threadgate.tsx:98 +msgid "Followed users" +msgstr "" + #: src/view/screens/PreferencesHomeFeed.tsx:145 msgid "Followed users only" msgstr "केवल वे यूजर को फ़ॉलो किया गया" @@ -1374,6 +1386,10 @@ msgstr "\"{query}\" के लिए कोई परिणाम नहीं msgid "No results found for {query}" msgstr "" +#: src/view/com/modals/Threadgate.tsx:82 +msgid "Nobody" +msgstr "" + #: src/view/com/modals/SelfLabel.tsx:136 #~ msgid "Not Applicable" #~ msgstr "लागू नहीं" @@ -2267,6 +2283,14 @@ msgstr "यूजर नाम या ईमेल पता" msgid "Users" msgstr "यूजर लोग" +#: src/view/com/threadgate/WhoCanReply.tsx:115 +msgid "Users followed by <0/>" +msgstr "" + +#: src/view/com/modals/Threadgate.tsx:106 +msgid "Users in \"{0}\"" +msgstr "" + #: src/view/screens/Settings.tsx:750 msgid "Verify email" msgstr "ईमेल सत्यापित करें" @@ -2332,6 +2356,15 @@ msgstr "इस पोस्ट में किस भाषा का उपय msgid "Which languages would you like to see in your algorithmic feeds?" msgstr "कौन से भाषाएं आपको अपने एल्गोरिदमिक फ़ीड में देखना पसंद करती हैं?" +#: src/view/com/composer/threadgate/ThreadgateBtn.tsx:43 +#: src/view/com/modals/Threadgate.tsx:66 +msgid "Who can reply" +msgstr "" + +#: src/view/com/threadgate/WhoCanReply.tsx:79 +msgid "Who can reply?" +msgstr "" + #: src/view/com/modals/crop-image/CropImage.web.tsx:102 msgid "Wide" msgstr "चौड़ा" diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index bff27f84..81a220d1 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -6,6 +6,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker' import {ImageModel} from '#/state/models/media/image' import {GalleryModel} from '#/state/models/media/gallery' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' +import {ThreadgateSetting} from '../queries/threadgate' export interface ConfirmModal { name: 'confirm' @@ -121,6 +122,12 @@ export interface SelfLabelModal { onChange: (labels: string[]) => void } +export interface ThreadgateModal { + name: 'threadgate' + settings: ThreadgateSetting[] + onChange: (settings: ThreadgateSetting[]) => void +} + export interface ChangeHandleModal { name: 'change-handle' onChanged: () => void @@ -207,6 +214,7 @@ export type Modal = | ServerInputModal | RepostModal | SelfLabelModal + | ThreadgateModal // Bluesky access | WaitlistModal diff --git a/src/state/queries/threadgate.ts b/src/state/queries/threadgate.ts new file mode 100644 index 00000000..48911758 --- /dev/null +++ b/src/state/queries/threadgate.ts @@ -0,0 +1,5 @@ +export type ThreadgateSetting = + | {type: 'nobody'} + | {type: 'mention'} + | {type: 'following'} + | {type: 'list'; list: string} diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index d8af6d0c..97d44345 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -35,6 +35,7 @@ import {shortenLinks} from 'lib/strings/rich-text-manip' import {toShortUrl} from 'lib/strings/url-helpers' import {SelectPhotoBtn} from './photos/SelectPhotoBtn' import {OpenCameraBtn} from './photos/OpenCameraBtn' +import {ThreadgateBtn} from './threadgate/ThreadgateBtn' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useExternalLinkFetch} from './useExternalLinkFetch' @@ -61,6 +62,7 @@ import {useProfileQuery} from '#/state/queries/profile' import {useComposerControls} from '#/state/shell/composer' import {until} from '#/lib/async/until' import {emitPostCreated} from '#/state/events' +import {ThreadgateSetting} from '#/state/queries/threadgate' type Props = ComposerOpts export const ComposePost = observer(function ComposePost({ @@ -105,6 +107,7 @@ export const ComposePost = observer(function ComposePost({ ) const {extLink, setExtLink} = useExternalLinkFetch({setQuote}) const [labels, setLabels] = useState([]) + const [threadgate, setThreadgate] = useState([]) const [suggestedLinks, setSuggestedLinks] = useState>(new Set()) const gallery = useMemo(() => new GalleryModel(), []) const onClose = useCallback(() => { @@ -220,6 +223,7 @@ export const ComposePost = observer(function ComposePost({ quote, extLink, labels, + threadgate, onStateChange: setProcessingState, langs: toPostLanguages(langPrefs.postLanguage), }) @@ -296,6 +300,12 @@ export const ComposePost = observer(function ComposePost({ onChange={setLabels} hasMedia={hasMedia} /> + {replyTo ? null : ( + + )} {canPost ? ( - + {labels.length > 0 ? ( void +}) { + const pal = usePalette('default') + const {track} = useAnalytics() + const {_} = useLingui() + const {openModal} = useModalControls() + + const onPress = () => { + track('Composer:ThreadgateOpened') + openModal({ + name: 'threadgate', + settings: threadgate, + onChange, + }) + } + + return ( + + + {threadgate.length ? ( + + ) : null} + + ) +} + +const styles = StyleSheet.create({ + button: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 6, + gap: 4, + }, +}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 0384e301..90629d33 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -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 = + } 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 ce1e67fa..12138f54 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -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 = } else if (modal.name === 'self-label') { element = + } else if (modal.name === 'threadgate') { + element = } else if (modal.name === 'change-handle') { element = } else if (modal.name === 'waitlist') { diff --git a/src/view/com/modals/Threadgate.tsx b/src/view/com/modals/Threadgate.tsx new file mode 100644 index 00000000..9d78a2e6 --- /dev/null +++ b/src/view/com/modals/Threadgate.tsx @@ -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 ( + + + + 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() + }} + style={styles.btn} + accessibilityRole="button" + accessibilityLabel={_(msg`Done`)} + 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, + }, +}) diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index cf43d205..633968c8 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -468,7 +468,7 @@ function* flattenThreadSkeleton( yield PARENT_SPINNER } yield node - if (node.ctx.isHighlightedPost) { + if (node.ctx.isHighlightedPost && !node.post.viewer?.replyDisabled) { yield REPLY_PROMPT } if (node.replies?.length) { diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index a2aa3716..2636fdfb 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -44,6 +44,7 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {ThreadPost} from '#/state/queries/post-thread' import {LabelInfo} from '../util/moderation/LabelInfo' import {useSession} from '#/state/session' +import {WhoCanReply} from '../threadgate/WhoCanReply' export function PostThreadItem({ post, @@ -441,6 +442,7 @@ let PostThreadItemLoaded = ({ + ) } else { @@ -450,164 +452,174 @@ let PostThreadItemLoaded = ({ const isThreadedChildAdjacentBot = isThreadedChild && nextPost?.ctx.depth === depth return ( - - - + <> + + + - - - {!isThreadedChild && showParentReplyLine && ( - - )} - - - - - {!isThreadedChild && ( - - - - {showChildReplyLine && ( + + + {!isThreadedChild && showParentReplyLine && ( )} - )} + - - - - {richText?.text ? ( - - + {!isThreadedChild && ( + + + + {showChildReplyLine && ( + + )} - ) : undefined} - {limitLines ? ( - + - ) : undefined} - {post.embed && ( - - + {richText?.text ? ( + + + + ) : undefined} + {limitLines ? ( + + ) : undefined} + {post.embed && ( + - - )} - + ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)} + ignoreQuoteDecisions> + + + )} + + - - {hasMore ? ( - - - More - - - - ) : undefined} - - + {hasMore ? ( + + + More + + + + ) : undefined} + + + + ) } } diff --git a/src/view/com/threadgate/WhoCanReply.tsx b/src/view/com/threadgate/WhoCanReply.tsx new file mode 100644 index 00000000..1c34623d --- /dev/null +++ b/src/view/com/threadgate/WhoCanReply.tsx @@ -0,0 +1,183 @@ +import React from 'react' +import {StyleProp, View, ViewStyle} from 'react-native' +import { + AppBskyFeedDefs, + AppBskyFeedThreadgate, + AppBskyGraphDefs, + AtUri, +} from '@atproto/api' +import {Trans} from '@lingui/macro' +import {usePalette} from '#/lib/hooks/usePalette' +import {Text} from '../util/text/Text' +import {TextLink} from '../util/Link' +import {makeProfileLink, makeListLink} from '#/lib/routes/links' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' +import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' + +import {colors} from '#/lib/styles' + +export function WhoCanReply({ + post, + style, +}: { + post: AppBskyFeedDefs.PostView + style?: StyleProp +}) { + const pal = usePalette('default') + const {isMobile} = useWebMediaQueries() + const containerStyles = useColorSchemeStyle( + { + borderColor: pal.colors.unreadNotifBorder, + backgroundColor: pal.colors.unreadNotifBg, + }, + { + borderColor: pal.colors.unreadNotifBorder, + backgroundColor: pal.colors.unreadNotifBg, + }, + ) + const iconStyles = useColorSchemeStyle( + { + backgroundColor: colors.blue3, + }, + { + backgroundColor: colors.blue3, + }, + ) + const textStyles = useColorSchemeStyle( + {color: colors.gray7}, + {color: colors.blue1}, + ) + const record = React.useMemo( + () => + post.threadgate && + AppBskyFeedThreadgate.isRecord(post.threadgate.record) && + AppBskyFeedThreadgate.validateRecord(post.threadgate.record).success + ? post.threadgate.record + : null, + [post], + ) + if (record) { + return ( + + + + + + + {!record.allow?.length ? ( + Replies to this thread are disabled + ) : ( + + Only{' '} + {record.allow.map((rule, i) => ( + <> + + + + ))}{' '} + can reply. + + )} + + + + ) + } + return null +} + +function Rule({ + rule, + post, + lists, +}: { + rule: any + post: AppBskyFeedDefs.PostView + lists: AppBskyGraphDefs.ListViewBasic[] | undefined +}) { + const pal = usePalette('default') + if (AppBskyFeedThreadgate.isMentionRule(rule)) { + return mentioned users + } + if (AppBskyFeedThreadgate.isFollowingRule(rule)) { + return ( + + users followed by{' '} + + + ) + } + if (AppBskyFeedThreadgate.isListRule(rule)) { + const list = lists?.find(l => l.uri === rule.list) + if (list) { + const listUrip = new AtUri(list.uri) + return ( + + {' '} + members + + ) + } + } +} + +function Separator({i, length}: {i: number; length: number}) { + if (length < 2 || i === length - 1) { + return null + } + if (i === length - 2) { + return ( + <> + {length > 2 ? ',' : ''} and{' '} + + ) + } + return <>, +} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index e548c45f..c0c5d470 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -108,9 +108,16 @@ export function PostCtrls({ { - requireAuth(() => onPressReply()) + if (!post.viewer?.replyDisabled) { + requireAuth(() => onPressReply()) + } }} accessibilityRole="button" accessibilityLabel={`Reply (${post.replyCount} ${