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:PastedPhotos': {}
|
||||||
'Composer:CameraOpened': {}
|
'Composer:CameraOpened': {}
|
||||||
'Composer:GalleryOpened': {}
|
'Composer:GalleryOpened': {}
|
||||||
|
'Composer:ThreadgateOpened': {}
|
||||||
'HomeScreen:PressCompose': {}
|
'HomeScreen:PressCompose': {}
|
||||||
'ProfileScreen:PressCompose': {}
|
'ProfileScreen:PressCompose': {}
|
||||||
// EDIT PROFILE events
|
// EDIT PROFILE events
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
AppBskyEmbedExternal,
|
AppBskyEmbedExternal,
|
||||||
AppBskyEmbedRecord,
|
AppBskyEmbedRecord,
|
||||||
AppBskyEmbedRecordWithMedia,
|
AppBskyEmbedRecordWithMedia,
|
||||||
|
AppBskyFeedThreadgate,
|
||||||
AppBskyRichtextFacet,
|
AppBskyRichtextFacet,
|
||||||
BskyAgent,
|
BskyAgent,
|
||||||
ComAtprotoLabelDefs,
|
ComAtprotoLabelDefs,
|
||||||
|
@ -16,6 +17,7 @@ import {isWeb} from 'platform/detection'
|
||||||
import {ImageModel} from 'state/models/media/image'
|
import {ImageModel} from 'state/models/media/image'
|
||||||
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
import {shortenLinks} from 'lib/strings/rich-text-manip'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||||
|
|
||||||
export interface ExternalEmbedDraft {
|
export interface ExternalEmbedDraft {
|
||||||
uri: string
|
uri: string
|
||||||
|
@ -54,6 +56,7 @@ interface PostOpts {
|
||||||
extLink?: ExternalEmbedDraft
|
extLink?: ExternalEmbedDraft
|
||||||
images?: ImageModel[]
|
images?: ImageModel[]
|
||||||
labels?: string[]
|
labels?: string[]
|
||||||
|
threadgate?: ThreadgateSetting[]
|
||||||
onStateChange?: (state: string) => void
|
onStateChange?: (state: string) => void
|
||||||
langs?: string[]
|
langs?: string[]
|
||||||
}
|
}
|
||||||
|
@ -227,9 +230,10 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
||||||
langs = opts.langs.slice(0, 3)
|
langs = opts.langs.slice(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let res
|
||||||
try {
|
try {
|
||||||
opts.onStateChange?.('Posting...')
|
opts.onStateChange?.('Posting...')
|
||||||
return await agent.post({
|
res = await agent.post({
|
||||||
text: rt.text,
|
text: rt.text,
|
||||||
facets: rt.facets,
|
facets: rt.facets,
|
||||||
reply,
|
reply,
|
||||||
|
@ -247,6 +251,52 @@ export async function post(agent: BskyAgent, opts: PostOpts) {
|
||||||
throw e
|
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
|
// helpers
|
||||||
|
|
|
@ -51,6 +51,10 @@ msgstr ""
|
||||||
msgid "{message}"
|
msgid "{message}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:130
|
||||||
|
msgid "<0/> members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
||||||
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -804,6 +808,10 @@ msgstr ""
|
||||||
msgid "Error:"
|
msgid "Error:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:76
|
||||||
|
msgid "Everybody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
||||||
msgid "Expand alt text"
|
msgid "Expand alt text"
|
||||||
msgstr ""
|
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."
|
msgid "Follow some users to get started. We can recommend you more users based on who you find interesting."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:98
|
||||||
|
msgid "Followed users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
||||||
msgid "Followed users only"
|
msgid "Followed users only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1360,6 +1372,10 @@ msgstr ""
|
||||||
msgid "No results found for {query}"
|
msgid "No results found for {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:82
|
||||||
|
msgid "Nobody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/modals/SelfLabel.tsx:136
|
#: src/view/com/modals/SelfLabel.tsx:136
|
||||||
#~ msgid "Not Applicable"
|
#~ msgid "Not Applicable"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
@ -1649,6 +1665,10 @@ msgstr ""
|
||||||
msgid "Removed from list"
|
msgid "Removed from list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:74
|
||||||
|
msgid "Replies to this thread are disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:135
|
#: src/view/screens/PreferencesHomeFeed.tsx:135
|
||||||
msgid "Reply Filters"
|
msgid "Reply Filters"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2241,6 +2261,14 @@ msgstr ""
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
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
|
#: src/view/screens/Settings.tsx:750
|
||||||
msgid "Verify email"
|
msgid "Verify email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2306,6 +2334,15 @@ msgstr ""
|
||||||
msgid "Which languages would you like to see in your algorithmic feeds?"
|
msgid "Which languages would you like to see in your algorithmic feeds?"
|
||||||
msgstr ""
|
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
|
#: src/view/com/modals/crop-image/CropImage.web.tsx:102
|
||||||
msgid "Wide"
|
msgid "Wide"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -51,6 +51,10 @@ msgstr ""
|
||||||
msgid "{message}"
|
msgid "{message}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:130
|
||||||
|
msgid "<0/> members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
||||||
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -812,6 +816,10 @@ msgstr ""
|
||||||
msgid "Error:"
|
msgid "Error:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:76
|
||||||
|
msgid "Everybody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
||||||
msgid "Expand alt text"
|
msgid "Expand alt text"
|
||||||
msgstr ""
|
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."
|
msgid "Follow some users to get started. We can recommend you more users based on who you find interesting."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:98
|
||||||
|
msgid "Followed users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
||||||
msgid "Followed users only"
|
msgid "Followed users only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1382,6 +1394,10 @@ msgstr ""
|
||||||
msgid "No results found for {query}"
|
msgid "No results found for {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:82
|
||||||
|
msgid "Nobody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/modals/SelfLabel.tsx:136
|
#: src/view/com/modals/SelfLabel.tsx:136
|
||||||
#~ msgid "Not Applicable"
|
#~ msgid "Not Applicable"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
@ -2275,6 +2291,14 @@ msgstr ""
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
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
|
#: src/view/screens/Settings.tsx:750
|
||||||
msgid "Verify email"
|
msgid "Verify email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2340,6 +2364,15 @@ msgstr ""
|
||||||
msgid "Which languages would you like to see in your algorithmic feeds?"
|
msgid "Which languages would you like to see in your algorithmic feeds?"
|
||||||
msgstr ""
|
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
|
#: src/view/com/modals/crop-image/CropImage.web.tsx:102
|
||||||
msgid "Wide"
|
msgid "Wide"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -51,6 +51,10 @@ msgstr ""
|
||||||
msgid "{message}"
|
msgid "{message}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:130
|
||||||
|
msgid "<0/> members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
||||||
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -804,6 +808,10 @@ msgstr ""
|
||||||
msgid "Error:"
|
msgid "Error:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:76
|
||||||
|
msgid "Everybody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
||||||
msgid "Expand alt text"
|
msgid "Expand alt text"
|
||||||
msgstr ""
|
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."
|
msgid "Follow some users to get started. We can recommend you more users based on who you find interesting."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:98
|
||||||
|
msgid "Followed users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
||||||
msgid "Followed users only"
|
msgid "Followed users only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1360,6 +1372,10 @@ msgstr ""
|
||||||
msgid "No results found for {query}"
|
msgid "No results found for {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:82
|
||||||
|
msgid "Nobody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/modals/SelfLabel.tsx:136
|
#: src/view/com/modals/SelfLabel.tsx:136
|
||||||
#~ msgid "Not Applicable"
|
#~ msgid "Not Applicable"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
@ -1649,6 +1665,10 @@ msgstr ""
|
||||||
msgid "Removed from list"
|
msgid "Removed from list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:74
|
||||||
|
msgid "Replies to this thread are disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:135
|
#: src/view/screens/PreferencesHomeFeed.tsx:135
|
||||||
msgid "Reply Filters"
|
msgid "Reply Filters"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2241,6 +2261,14 @@ msgstr ""
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
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
|
#: src/view/screens/Settings.tsx:750
|
||||||
msgid "Verify email"
|
msgid "Verify email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2306,6 +2334,15 @@ msgstr ""
|
||||||
msgid "Which languages would you like to see in your algorithmic feeds?"
|
msgid "Which languages would you like to see in your algorithmic feeds?"
|
||||||
msgstr ""
|
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
|
#: src/view/com/modals/crop-image/CropImage.web.tsx:102
|
||||||
msgid "Wide"
|
msgid "Wide"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -51,6 +51,10 @@ msgstr ""
|
||||||
msgid "{message}"
|
msgid "{message}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:130
|
||||||
|
msgid "<0/> members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
||||||
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -804,6 +808,10 @@ msgstr ""
|
||||||
msgid "Error:"
|
msgid "Error:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:76
|
||||||
|
msgid "Everybody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
||||||
msgid "Expand alt text"
|
msgid "Expand alt text"
|
||||||
msgstr ""
|
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."
|
msgid "Follow some users to get started. We can recommend you more users based on who you find interesting."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:98
|
||||||
|
msgid "Followed users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
||||||
msgid "Followed users only"
|
msgid "Followed users only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1360,6 +1372,10 @@ msgstr ""
|
||||||
msgid "No results found for {query}"
|
msgid "No results found for {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:82
|
||||||
|
msgid "Nobody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/modals/SelfLabel.tsx:136
|
#: src/view/com/modals/SelfLabel.tsx:136
|
||||||
#~ msgid "Not Applicable"
|
#~ msgid "Not Applicable"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
@ -1649,6 +1665,10 @@ msgstr ""
|
||||||
msgid "Removed from list"
|
msgid "Removed from list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:74
|
||||||
|
msgid "Replies to this thread are disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:135
|
#: src/view/screens/PreferencesHomeFeed.tsx:135
|
||||||
msgid "Reply Filters"
|
msgid "Reply Filters"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2241,6 +2261,14 @@ msgstr ""
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr ""
|
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
|
#: src/view/screens/Settings.tsx:750
|
||||||
msgid "Verify email"
|
msgid "Verify email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2306,6 +2334,15 @@ msgstr ""
|
||||||
msgid "Which languages would you like to see in your algorithmic feeds?"
|
msgid "Which languages would you like to see in your algorithmic feeds?"
|
||||||
msgstr ""
|
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
|
#: src/view/com/modals/crop-image/CropImage.web.tsx:102
|
||||||
msgid "Wide"
|
msgid "Wide"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -51,6 +51,10 @@ msgstr ""
|
||||||
msgid "{message}"
|
msgid "{message}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/threadgate/WhoCanReply.tsx:130
|
||||||
|
msgid "<0/> members"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
#: src/view/com/auth/onboarding/RecommendedFeeds.tsx:30
|
||||||
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
msgid "<0>Choose your</0><1>Recommended</1><2>Feeds</2>"
|
||||||
msgstr "<0>अपना</0><1>पसंदीदा</1><2>फ़ीड चुनें</2>"
|
msgstr "<0>अपना</0><1>पसंदीदा</1><2>फ़ीड चुनें</2>"
|
||||||
|
@ -808,6 +812,10 @@ msgstr "अपने यूज़रनेम और पासवर्ड द
|
||||||
msgid "Error:"
|
msgid "Error:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:76
|
||||||
|
msgid "Everybody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
#: src/view/com/lightbox/Lightbox.web.tsx:156
|
||||||
msgid "Expand alt text"
|
msgid "Expand alt text"
|
||||||
msgstr "ऑल्ट टेक्स्ट"
|
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."
|
msgid "Follow some users to get started. We can recommend you more users based on who you find interesting."
|
||||||
msgstr "आरंभ करने के लिए कुछ उपयोगकर्ताओं का अनुसरण करें. आपको कौन दिलचस्प लगता है, इसके आधार पर हम आपको और अधिक उपयोगकर्ताओं की अनुशंसा कर सकते हैं।"
|
msgstr "आरंभ करने के लिए कुछ उपयोगकर्ताओं का अनुसरण करें. आपको कौन दिलचस्प लगता है, इसके आधार पर हम आपको और अधिक उपयोगकर्ताओं की अनुशंसा कर सकते हैं।"
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:98
|
||||||
|
msgid "Followed users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
#: src/view/screens/PreferencesHomeFeed.tsx:145
|
||||||
msgid "Followed users only"
|
msgid "Followed users only"
|
||||||
msgstr "केवल वे यूजर को फ़ॉलो किया गया"
|
msgstr "केवल वे यूजर को फ़ॉलो किया गया"
|
||||||
|
@ -1374,6 +1386,10 @@ msgstr "\"{query}\" के लिए कोई परिणाम नहीं
|
||||||
msgid "No results found for {query}"
|
msgid "No results found for {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/view/com/modals/Threadgate.tsx:82
|
||||||
|
msgid "Nobody"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/view/com/modals/SelfLabel.tsx:136
|
#: src/view/com/modals/SelfLabel.tsx:136
|
||||||
#~ msgid "Not Applicable"
|
#~ msgid "Not Applicable"
|
||||||
#~ msgstr "लागू नहीं"
|
#~ msgstr "लागू नहीं"
|
||||||
|
@ -2267,6 +2283,14 @@ msgstr "यूजर नाम या ईमेल पता"
|
||||||
msgid "Users"
|
msgid "Users"
|
||||||
msgstr "यूजर लोग"
|
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
|
#: src/view/screens/Settings.tsx:750
|
||||||
msgid "Verify email"
|
msgid "Verify email"
|
||||||
msgstr "ईमेल सत्यापित करें"
|
msgstr "ईमेल सत्यापित करें"
|
||||||
|
@ -2332,6 +2356,15 @@ msgstr "इस पोस्ट में किस भाषा का उपय
|
||||||
msgid "Which languages would you like to see in your algorithmic feeds?"
|
msgid "Which languages would you like to see in your algorithmic feeds?"
|
||||||
msgstr "कौन से भाषाएं आपको अपने एल्गोरिदमिक फ़ीड में देखना पसंद करती हैं?"
|
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
|
#: src/view/com/modals/crop-image/CropImage.web.tsx:102
|
||||||
msgid "Wide"
|
msgid "Wide"
|
||||||
msgstr "चौड़ा"
|
msgstr "चौड़ा"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {ImageModel} from '#/state/models/media/image'
|
import {ImageModel} from '#/state/models/media/image'
|
||||||
import {GalleryModel} from '#/state/models/media/gallery'
|
import {GalleryModel} from '#/state/models/media/gallery'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
|
import {ThreadgateSetting} from '../queries/threadgate'
|
||||||
|
|
||||||
export interface ConfirmModal {
|
export interface ConfirmModal {
|
||||||
name: 'confirm'
|
name: 'confirm'
|
||||||
|
@ -121,6 +122,12 @@ export interface SelfLabelModal {
|
||||||
onChange: (labels: string[]) => void
|
onChange: (labels: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ThreadgateModal {
|
||||||
|
name: 'threadgate'
|
||||||
|
settings: ThreadgateSetting[]
|
||||||
|
onChange: (settings: ThreadgateSetting[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChangeHandleModal {
|
export interface ChangeHandleModal {
|
||||||
name: 'change-handle'
|
name: 'change-handle'
|
||||||
onChanged: () => void
|
onChanged: () => void
|
||||||
|
@ -207,6 +214,7 @@ export type Modal =
|
||||||
| ServerInputModal
|
| ServerInputModal
|
||||||
| RepostModal
|
| RepostModal
|
||||||
| SelfLabelModal
|
| SelfLabelModal
|
||||||
|
| ThreadgateModal
|
||||||
|
|
||||||
// Bluesky access
|
// Bluesky access
|
||||||
| WaitlistModal
|
| 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 {toShortUrl} from 'lib/strings/url-helpers'
|
||||||
import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
|
import {SelectPhotoBtn} from './photos/SelectPhotoBtn'
|
||||||
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
import {OpenCameraBtn} from './photos/OpenCameraBtn'
|
||||||
|
import {ThreadgateBtn} from './threadgate/ThreadgateBtn'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
import {useExternalLinkFetch} from './useExternalLinkFetch'
|
||||||
|
@ -61,6 +62,7 @@ import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {until} from '#/lib/async/until'
|
import {until} from '#/lib/async/until'
|
||||||
import {emitPostCreated} from '#/state/events'
|
import {emitPostCreated} from '#/state/events'
|
||||||
|
import {ThreadgateSetting} from '#/state/queries/threadgate'
|
||||||
|
|
||||||
type Props = ComposerOpts
|
type Props = ComposerOpts
|
||||||
export const ComposePost = observer(function ComposePost({
|
export const ComposePost = observer(function ComposePost({
|
||||||
|
@ -105,6 +107,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
)
|
)
|
||||||
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
|
||||||
const [labels, setLabels] = useState<string[]>([])
|
const [labels, setLabels] = useState<string[]>([])
|
||||||
|
const [threadgate, setThreadgate] = useState<ThreadgateSetting[]>([])
|
||||||
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
|
const [suggestedLinks, setSuggestedLinks] = useState<Set<string>>(new Set())
|
||||||
const gallery = useMemo(() => new GalleryModel(), [])
|
const gallery = useMemo(() => new GalleryModel(), [])
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
|
@ -220,6 +223,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
quote,
|
quote,
|
||||||
extLink,
|
extLink,
|
||||||
labels,
|
labels,
|
||||||
|
threadgate,
|
||||||
onStateChange: setProcessingState,
|
onStateChange: setProcessingState,
|
||||||
langs: toPostLanguages(langPrefs.postLanguage),
|
langs: toPostLanguages(langPrefs.postLanguage),
|
||||||
})
|
})
|
||||||
|
@ -296,6 +300,12 @@ export const ComposePost = observer(function ComposePost({
|
||||||
onChange={setLabels}
|
onChange={setLabels}
|
||||||
hasMedia={hasMedia}
|
hasMedia={hasMedia}
|
||||||
/>
|
/>
|
||||||
|
{replyTo ? null : (
|
||||||
|
<ThreadgateBtn
|
||||||
|
threadgate={threadgate}
|
||||||
|
onChange={setThreadgate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{canPost ? (
|
{canPost ? (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="composerPublishBtn"
|
testID="composerPublishBtn"
|
||||||
|
@ -458,9 +468,11 @@ const styles = StyleSheet.create({
|
||||||
topbar: {
|
topbar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
paddingTop: 6,
|
||||||
paddingBottom: 4,
|
paddingBottom: 4,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
height: 55,
|
height: 55,
|
||||||
|
gap: 4,
|
||||||
},
|
},
|
||||||
topbarDesktop: {
|
topbarDesktop: {
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
|
@ -470,6 +482,7 @@ const styles = StyleSheet.create({
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
|
marginLeft: 12,
|
||||||
},
|
},
|
||||||
errorLine: {
|
errorLine: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|
|
@ -49,6 +49,6 @@ const styles = StyleSheet.create({
|
||||||
paddingLeft: 12,
|
paddingLeft: 12,
|
||||||
},
|
},
|
||||||
labelDesktopWeb: {
|
labelDesktopWeb: {
|
||||||
paddingLeft: 20,
|
paddingLeft: 12,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -38,7 +38,7 @@ export function LabelsBtn({
|
||||||
}
|
}
|
||||||
openModal({name: 'self-label', labels, hasMedia, onChange})
|
openModal({name: 'self-label', labels, hasMedia, onChange})
|
||||||
}}>
|
}}>
|
||||||
<ShieldExclamation style={pal.link} size={26} />
|
<ShieldExclamation style={pal.link} size={24} />
|
||||||
{labels.length > 0 ? (
|
{labels.length > 0 ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon="check"
|
icon="check"
|
||||||
|
@ -54,8 +54,7 @@ const styles = StyleSheet.create({
|
||||||
button: {
|
button: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 6,
|
||||||
marginRight: 4,
|
|
||||||
},
|
},
|
||||||
dimmed: {
|
dimmed: {
|
||||||
opacity: 0.4,
|
opacity: 0.4,
|
||||||
|
|
|
@ -98,7 +98,8 @@ const styles = StyleSheet.create({
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingHorizontal: 10,
|
paddingLeft: 12,
|
||||||
|
paddingRight: 12,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
},
|
},
|
||||||
picker: {
|
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 ServerInputModal from './ServerInput'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as SelfLabelModal from './SelfLabel'
|
import * as SelfLabelModal from './SelfLabel'
|
||||||
|
import * as ThreadgateModal from './Threadgate'
|
||||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||||
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
||||||
import * as ListAddUserModal from './ListAddRemoveUsers'
|
import * as ListAddUserModal from './ListAddRemoveUsers'
|
||||||
|
@ -127,6 +128,9 @@ export function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'self-label') {
|
} else if (activeModal?.name === 'self-label') {
|
||||||
snapPoints = SelfLabelModal.snapPoints
|
snapPoints = SelfLabelModal.snapPoints
|
||||||
element = <SelfLabelModal.Component {...activeModal} />
|
element = <SelfLabelModal.Component {...activeModal} />
|
||||||
|
} else if (activeModal?.name === 'threadgate') {
|
||||||
|
snapPoints = ThreadgateModal.snapPoints
|
||||||
|
element = <ThreadgateModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'alt-text-image') {
|
} else if (activeModal?.name === 'alt-text-image') {
|
||||||
snapPoints = AltImageModal.snapPoints
|
snapPoints = AltImageModal.snapPoints
|
||||||
element = <AltImageModal.Component {...activeModal} />
|
element = <AltImageModal.Component {...activeModal} />
|
||||||
|
|
|
@ -18,6 +18,7 @@ import * as ListAddUserModal from './ListAddRemoveUsers'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as RepostModal from './Repost'
|
import * as RepostModal from './Repost'
|
||||||
import * as SelfLabelModal from './SelfLabel'
|
import * as SelfLabelModal from './SelfLabel'
|
||||||
|
import * as ThreadgateModal from './Threadgate'
|
||||||
import * as CropImageModal from './crop-image/CropImage.web'
|
import * as CropImageModal from './crop-image/CropImage.web'
|
||||||
import * as AltTextImageModal from './AltImage'
|
import * as AltTextImageModal from './AltImage'
|
||||||
import * as EditImageModal from './EditImage'
|
import * as EditImageModal from './EditImage'
|
||||||
|
@ -98,6 +99,8 @@ function Modal({modal}: {modal: ModalIface}) {
|
||||||
element = <RepostModal.Component {...modal} />
|
element = <RepostModal.Component {...modal} />
|
||||||
} else if (modal.name === 'self-label') {
|
} else if (modal.name === 'self-label') {
|
||||||
element = <SelfLabelModal.Component {...modal} />
|
element = <SelfLabelModal.Component {...modal} />
|
||||||
|
} else if (modal.name === 'threadgate') {
|
||||||
|
element = <ThreadgateModal.Component {...modal} />
|
||||||
} else if (modal.name === 'change-handle') {
|
} else if (modal.name === 'change-handle') {
|
||||||
element = <ChangeHandleModal.Component {...modal} />
|
element = <ChangeHandleModal.Component {...modal} />
|
||||||
} else if (modal.name === 'waitlist') {
|
} 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 PARENT_SPINNER
|
||||||
}
|
}
|
||||||
yield node
|
yield node
|
||||||
if (node.ctx.isHighlightedPost) {
|
if (node.ctx.isHighlightedPost && !node.post.viewer?.replyDisabled) {
|
||||||
yield REPLY_PROMPT
|
yield REPLY_PROMPT
|
||||||
}
|
}
|
||||||
if (node.replies?.length) {
|
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 {ThreadPost} from '#/state/queries/post-thread'
|
||||||
import {LabelInfo} from '../util/moderation/LabelInfo'
|
import {LabelInfo} from '../util/moderation/LabelInfo'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {WhoCanReply} from '../threadgate/WhoCanReply'
|
||||||
|
|
||||||
export function PostThreadItem({
|
export function PostThreadItem({
|
||||||
post,
|
post,
|
||||||
|
@ -441,6 +442,7 @@ let PostThreadItemLoaded = ({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
|
<WhoCanReply post={post} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -450,164 +452,174 @@ let PostThreadItemLoaded = ({
|
||||||
const isThreadedChildAdjacentBot =
|
const isThreadedChildAdjacentBot =
|
||||||
isThreadedChild && nextPost?.ctx.depth === depth
|
isThreadedChild && nextPost?.ctx.depth === depth
|
||||||
return (
|
return (
|
||||||
<PostOuterWrapper
|
<>
|
||||||
post={post}
|
<PostOuterWrapper
|
||||||
depth={depth}
|
post={post}
|
||||||
showParentReplyLine={!!showParentReplyLine}
|
depth={depth}
|
||||||
treeView={treeView}
|
showParentReplyLine={!!showParentReplyLine}
|
||||||
hasPrecedingItem={hasPrecedingItem}>
|
treeView={treeView}
|
||||||
<PostHider
|
hasPrecedingItem={hasPrecedingItem}>
|
||||||
testID={`postThreadItem-by-${post.author.handle}`}
|
<PostHider
|
||||||
href={postHref}
|
testID={`postThreadItem-by-${post.author.handle}`}
|
||||||
style={[pal.view]}
|
href={postHref}
|
||||||
moderation={moderation.content}
|
style={[pal.view]}
|
||||||
iconSize={isThreadedChild ? 26 : 38}
|
moderation={moderation.content}
|
||||||
iconStyles={
|
iconSize={isThreadedChild ? 26 : 38}
|
||||||
isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
|
iconStyles={
|
||||||
}>
|
isThreadedChild
|
||||||
<PostSandboxWarning />
|
? {marginRight: 4}
|
||||||
|
: {marginLeft: 2, marginRight: 2}
|
||||||
|
}>
|
||||||
|
<PostSandboxWarning />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 10,
|
gap: 10,
|
||||||
paddingLeft: 8,
|
paddingLeft: 8,
|
||||||
height: isThreadedChildAdjacentTop ? 8 : 16,
|
height: isThreadedChildAdjacentTop ? 8 : 16,
|
||||||
}}>
|
}}>
|
||||||
<View style={{width: 38}}>
|
<View style={{width: 38}}>
|
||||||
{!isThreadedChild && showParentReplyLine && (
|
{!isThreadedChild && showParentReplyLine && (
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.replyLine,
|
|
||||||
{
|
|
||||||
flexGrow: 1,
|
|
||||||
backgroundColor: pal.colors.border,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.layout,
|
|
||||||
{
|
|
||||||
paddingBottom:
|
|
||||||
showChildReplyLine && !isThreadedChild
|
|
||||||
? 0
|
|
||||||
: isThreadedChildAdjacentBot
|
|
||||||
? 4
|
|
||||||
: 8,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
{!isThreadedChild && (
|
|
||||||
<View style={styles.layoutAvi}>
|
|
||||||
<PreviewableUserAvatar
|
|
||||||
size={38}
|
|
||||||
did={post.author.did}
|
|
||||||
handle={post.author.handle}
|
|
||||||
avatar={post.author.avatar}
|
|
||||||
moderation={moderation.avatar}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showChildReplyLine && (
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.replyLine,
|
styles.replyLine,
|
||||||
{
|
{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
backgroundColor: pal.colors.border,
|
backgroundColor: pal.colors.border,
|
||||||
marginTop: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
</View>
|
||||||
|
|
||||||
<View style={styles.layoutContent}>
|
<View
|
||||||
<PostMeta
|
style={[
|
||||||
author={post.author}
|
styles.layout,
|
||||||
authorHasWarning={!!post.author.labels?.length}
|
{
|
||||||
timestamp={post.indexedAt}
|
paddingBottom:
|
||||||
postHref={postHref}
|
showChildReplyLine && !isThreadedChild
|
||||||
showAvatar={isThreadedChild}
|
? 0
|
||||||
avatarSize={28}
|
: isThreadedChildAdjacentBot
|
||||||
displayNameType="md-bold"
|
? 4
|
||||||
displayNameStyle={isThreadedChild && s.ml2}
|
: 8,
|
||||||
style={isThreadedChild && s.mb2}
|
},
|
||||||
/>
|
]}>
|
||||||
<PostAlerts
|
{!isThreadedChild && (
|
||||||
moderation={moderation.content}
|
<View style={styles.layoutAvi}>
|
||||||
style={styles.alert}
|
<PreviewableUserAvatar
|
||||||
/>
|
size={38}
|
||||||
{richText?.text ? (
|
did={post.author.did}
|
||||||
<View style={styles.postTextContainer}>
|
handle={post.author.handle}
|
||||||
<RichText
|
avatar={post.author.avatar}
|
||||||
type="post-text"
|
moderation={moderation.avatar}
|
||||||
richText={richText}
|
|
||||||
style={[pal.text, s.flex1]}
|
|
||||||
lineHeight={1.3}
|
|
||||||
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showChildReplyLine && (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.replyLine,
|
||||||
|
{
|
||||||
|
flexGrow: 1,
|
||||||
|
backgroundColor: pal.colors.border,
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : undefined}
|
)}
|
||||||
{limitLines ? (
|
|
||||||
<TextLink
|
<View style={styles.layoutContent}>
|
||||||
text="Show More"
|
<PostMeta
|
||||||
style={pal.link}
|
author={post.author}
|
||||||
onPress={onPressShowMore}
|
authorHasWarning={!!post.author.labels?.length}
|
||||||
href="#"
|
timestamp={post.indexedAt}
|
||||||
|
postHref={postHref}
|
||||||
|
showAvatar={isThreadedChild}
|
||||||
|
avatarSize={28}
|
||||||
|
displayNameType="md-bold"
|
||||||
|
displayNameStyle={isThreadedChild && s.ml2}
|
||||||
|
style={isThreadedChild && s.mb2}
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
<PostAlerts
|
||||||
{post.embed && (
|
moderation={moderation.content}
|
||||||
<ContentHider
|
style={styles.alert}
|
||||||
style={styles.contentHider}
|
/>
|
||||||
moderation={moderation.embed}
|
{richText?.text ? (
|
||||||
moderationDecisions={moderation.decisions}
|
<View style={styles.postTextContainer}>
|
||||||
ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
|
<RichText
|
||||||
ignoreQuoteDecisions>
|
type="post-text"
|
||||||
<PostEmbeds
|
richText={richText}
|
||||||
embed={post.embed}
|
style={[pal.text, s.flex1]}
|
||||||
|
lineHeight={1.3}
|
||||||
|
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
) : undefined}
|
||||||
|
{limitLines ? (
|
||||||
|
<TextLink
|
||||||
|
text="Show More"
|
||||||
|
style={pal.link}
|
||||||
|
onPress={onPressShowMore}
|
||||||
|
href="#"
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{post.embed && (
|
||||||
|
<ContentHider
|
||||||
|
style={styles.contentHider}
|
||||||
moderation={moderation.embed}
|
moderation={moderation.embed}
|
||||||
moderationDecisions={moderation.decisions}
|
moderationDecisions={moderation.decisions}
|
||||||
/>
|
ignoreMute={isEmbedByEmbedder(post.embed, post.author.did)}
|
||||||
</ContentHider>
|
ignoreQuoteDecisions>
|
||||||
)}
|
<PostEmbeds
|
||||||
<PostCtrls
|
embed={post.embed}
|
||||||
post={post}
|
moderation={moderation.embed}
|
||||||
record={record}
|
moderationDecisions={moderation.decisions}
|
||||||
onPressReply={onPressReply}
|
/>
|
||||||
/>
|
</ContentHider>
|
||||||
|
)}
|
||||||
|
<PostCtrls
|
||||||
|
post={post}
|
||||||
|
record={record}
|
||||||
|
onPressReply={onPressReply}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
{hasMore ? (
|
||||||
{hasMore ? (
|
<Link
|
||||||
<Link
|
style={[
|
||||||
style={[
|
styles.loadMore,
|
||||||
styles.loadMore,
|
{
|
||||||
{
|
paddingLeft: treeView ? 8 : 70,
|
||||||
paddingLeft: treeView ? 8 : 70,
|
paddingTop: 0,
|
||||||
paddingTop: 0,
|
paddingBottom: treeView ? 4 : 12,
|
||||||
paddingBottom: treeView ? 4 : 12,
|
},
|
||||||
},
|
]}
|
||||||
]}
|
href={postHref}
|
||||||
href={postHref}
|
title={itemTitle}
|
||||||
title={itemTitle}
|
noFeedback>
|
||||||
noFeedback>
|
<Text type="sm-medium" style={pal.textLight}>
|
||||||
<Text type="sm-medium" style={pal.textLight}>
|
More
|
||||||
More
|
</Text>
|
||||||
</Text>
|
<FontAwesomeIcon
|
||||||
<FontAwesomeIcon
|
icon="angle-right"
|
||||||
icon="angle-right"
|
color={pal.colors.textLight}
|
||||||
color={pal.colors.textLight}
|
size={14}
|
||||||
size={14}
|
/>
|
||||||
/>
|
</Link>
|
||||||
</Link>
|
) : undefined}
|
||||||
) : undefined}
|
</PostHider>
|
||||||
</PostHider>
|
</PostOuterWrapper>
|
||||||
</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]}>
|
<View style={[styles.ctrls, style]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="replyBtn"
|
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={() => {
|
onPress={() => {
|
||||||
requireAuth(() => onPressReply())
|
if (!post.viewer?.replyDisabled) {
|
||||||
|
requireAuth(() => onPressReply())
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`Reply (${post.replyCount} ${
|
accessibilityLabel={`Reply (${post.replyCount} ${
|
||||||
|
|
Loading…
Reference in New Issue