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 composerzio/stable
parent
f5d014d4c7
commit
28fa5e4919
|
@ -21,6 +21,7 @@ interface TrackPropertiesMap {
|
|||
'Composer:PastedPhotos': {}
|
||||
'Composer:CameraOpened': {}
|
||||
'Composer:GalleryOpened': {}
|
||||
'Composer:ThreadgateOpened': {}
|
||||
'HomeScreen:PressCompose': {}
|
||||
'ProfileScreen:PressCompose': {}
|
||||
// EDIT PROFILE events
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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</0><1>Recommended</1><2>Feeds</2>"
|
||||
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 ""
|
||||
|
|
|
@ -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</0><1>Recommended</1><2>Feeds</2>"
|
||||
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 ""
|
||||
|
|
|
@ -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</0><1>Recommended</1><2>Feeds</2>"
|
||||
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 ""
|
||||
|
|
|
@ -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</0><1>Recommended</1><2>Feeds</2>"
|
||||
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 ""
|
||||
|
|
|
@ -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</0><1>Recommended</1><2>Feeds</2>"
|
||||
msgstr "<0>अपना</0><1>पसंदीदा</1><2>फ़ीड चुनें</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 "चौड़ा"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export type ThreadgateSetting =
|
||||
| {type: 'nobody'}
|
||||
| {type: 'mention'}
|
||||
| {type: 'following'}
|
||||
| {type: 'list'; list: string}
|
|
@ -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<string[]>([])
|
||||
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
|
||||
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(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 : (
|
||||
<ThreadgateBtn
|
||||
threadgate={threadgate}
|
||||
onChange={setThreadgate}
|
||||
/>
|
||||
)}
|
||||
{canPost ? (
|
||||
<TouchableOpacity
|
||||
testID="composerPublishBtn"
|
||||
|
@ -458,9 +468,11 @@ const styles = StyleSheet.create({
|
|||
topbar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: 6,
|
||||
paddingBottom: 4,
|
||||
paddingHorizontal: 20,
|
||||
height: 55,
|
||||
gap: 4,
|
||||
},
|
||||
topbarDesktop: {
|
||||
paddingTop: 10,
|
||||
|
@ -470,6 +482,7 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 20,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 6,
|
||||
marginLeft: 12,
|
||||
},
|
||||
errorLine: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
@ -49,6 +49,6 @@ const styles = StyleSheet.create({
|
|||
paddingLeft: 12,
|
||||
},
|
||||
labelDesktopWeb: {
|
||||
paddingLeft: 20,
|
||||
paddingLeft: 12,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -38,7 +38,7 @@ export function LabelsBtn({
|
|||
}
|
||||
openModal({name: 'self-label', labels, hasMedia, onChange})
|
||||
}}>
|
||||
<ShieldExclamation style={pal.link} size={26} />
|
||||
<ShieldExclamation style={pal.link} size={24} />
|
||||
{labels.length > 0 ? (
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
|
@ -54,8 +54,7 @@ const styles = StyleSheet.create({
|
|||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 14,
|
||||
marginRight: 4,
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
dimmed: {
|
||||
opacity: 0.4,
|
||||
|
|
|
@ -98,7 +98,8 @@ const styles = StyleSheet.create({
|
|||
backgroundColor: 'transparent',
|
||||
border: 'none',
|
||||
paddingTop: 4,
|
||||
paddingHorizontal: 10,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
picker: {
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react'
|
||||
import {TouchableOpacity, StyleSheet} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||
|
||||
export function ThreadgateBtn({
|
||||
threadgate,
|
||||
onChange,
|
||||
}: {
|
||||
threadgate: ThreadgateSetting[]
|
||||
onChange: (v: ThreadgateSetting[]) => 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 (
|
||||
<TouchableOpacity
|
||||
testID="openReplyGateButton"
|
||||
onPress={onPress}
|
||||
style={styles.button}
|
||||
hitSlop={HITSLOP_10}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Who can reply`)}
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'comments']}
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={24}
|
||||
/>
|
||||
{threadgate.length ? (
|
||||
<FontAwesomeIcon
|
||||
icon="check"
|
||||
size={16}
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
/>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 6,
|
||||
gap: 4,
|
||||
},
|
||||
})
|
|
@ -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} />
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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) {
|
||||
|
|
|
@ -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 = ({
|
|||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
<WhoCanReply post={post} />
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
|
@ -450,6 +452,7 @@ let PostThreadItemLoaded = ({
|
|||
const isThreadedChildAdjacentBot =
|
||||
isThreadedChild && nextPost?.ctx.depth === depth
|
||||
return (
|
||||
<>
|
||||
<PostOuterWrapper
|
||||
post={post}
|
||||
depth={depth}
|
||||
|
@ -463,7 +466,9 @@ let PostThreadItemLoaded = ({
|
|||
moderation={moderation.content}
|
||||
iconSize={isThreadedChild ? 26 : 38}
|
||||
iconStyles={
|
||||
isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
|
||||
isThreadedChild
|
||||
? {marginRight: 4}
|
||||
: {marginLeft: 2, marginRight: 2}
|
||||
}>
|
||||
<PostSandboxWarning />
|
||||
|
||||
|
@ -608,6 +613,13 @@ let PostThreadItemLoaded = ({
|
|||
) : undefined}
|
||||
</PostHider>
|
||||
</PostOuterWrapper>
|
||||
<WhoCanReply
|
||||
post={post}
|
||||
style={{
|
||||
marginTop: 4,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ViewStyle>
|
||||
}) {
|
||||
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 (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: isMobile ? 8 : 10,
|
||||
paddingHorizontal: isMobile ? 16 : 18,
|
||||
paddingVertical: 12,
|
||||
borderWidth: 1,
|
||||
borderLeftWidth: isMobile ? 0 : 1,
|
||||
borderRightWidth: isMobile ? 0 : 1,
|
||||
},
|
||||
containerStyles,
|
||||
style,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 19,
|
||||
},
|
||||
iconStyles,
|
||||
]}>
|
||||
<FontAwesomeIcon
|
||||
icon={['far', 'comments']}
|
||||
size={16}
|
||||
color={'#fff'}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flex: 1}}>
|
||||
<Text type="sm" style={[{flexWrap: 'wrap'}, textStyles]}>
|
||||
{!record.allow?.length ? (
|
||||
<Trans>Replies to this thread are disabled</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Only{' '}
|
||||
{record.allow.map((rule, i) => (
|
||||
<>
|
||||
<Rule
|
||||
key={`rule-${i}`}
|
||||
rule={rule}
|
||||
post={post}
|
||||
lists={post.threadgate!.lists}
|
||||
/>
|
||||
<Separator
|
||||
key={`sep-${i}`}
|
||||
i={i}
|
||||
length={record.allow!.length}
|
||||
/>
|
||||
</>
|
||||
))}{' '}
|
||||
can reply.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
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 <Trans>mentioned users</Trans>
|
||||
}
|
||||
if (AppBskyFeedThreadgate.isFollowingRule(rule)) {
|
||||
return (
|
||||
<Trans>
|
||||
users followed by{' '}
|
||||
<TextLink
|
||||
href={makeProfileLink(post.author)}
|
||||
text={`@${post.author.handle}`}
|
||||
style={pal.link}
|
||||
/>
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
if (AppBskyFeedThreadgate.isListRule(rule)) {
|
||||
const list = lists?.find(l => l.uri === rule.list)
|
||||
if (list) {
|
||||
const listUrip = new AtUri(list.uri)
|
||||
return (
|
||||
<Trans>
|
||||
<TextLink
|
||||
href={makeListLink(listUrip.hostname, listUrip.rkey)}
|
||||
text={list.name}
|
||||
style={pal.link}
|
||||
/>{' '}
|
||||
members
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Separator({i, length}: {i: number; length: number}) {
|
||||
if (length < 2 || i === length - 1) {
|
||||
return null
|
||||
}
|
||||
if (i === length - 2) {
|
||||
return (
|
||||
<>
|
||||
{length > 2 ? ',' : ''} <Trans>and</Trans>{' '}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <>, </>
|
||||
}
|
|
@ -108,9 +108,16 @@ export function PostCtrls({
|
|||
<View style={[styles.ctrls, style]}>
|
||||
<TouchableOpacity
|
||||
testID="replyBtn"
|
||||
style={[styles.ctrl, !big && styles.ctrlPad, {paddingLeft: 0}]}
|
||||
style={[
|
||||
styles.ctrl,
|
||||
!big && styles.ctrlPad,
|
||||
{paddingLeft: 0},
|
||||
post.viewer?.replyDisabled ? {opacity: 0.5} : undefined,
|
||||
]}
|
||||
onPress={() => {
|
||||
if (!post.viewer?.replyDisabled) {
|
||||
requireAuth(() => onPressReply())
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Reply (${post.replyCount} ${
|
||||
|
|
Loading…
Reference in New Issue