Merge remote-tracking branch 'origin/main' into hailey/explicitly-filter

This commit is contained in:
Hailey
2024-06-24 17:02:40 -07:00
19 changed files with 527 additions and 434 deletions
+4 -1
View File
@@ -88,7 +88,10 @@ export function Outer({
if (!isOpen) return
function handler(e: KeyboardEvent) {
if (e.key === 'Escape') close()
if (e.key === 'Escape') {
e.stopPropagation()
close()
}
}
document.addEventListener('keydown', handler)
+34 -21
View File
@@ -6,10 +6,10 @@ import {useLingui} from '@lingui/react'
import {differenceInSeconds} from 'date-fns'
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {isNative} from '#/platform/detection'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {HITSLOP_10} from 'lib/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {isWeb} from 'platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
@@ -70,19 +70,27 @@ export function NewskieDialog({
<Dialog.ScrollableInner
label={_(msg`New user info dialog`)}
style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}>
<View style={[a.gap_sm]}>
<View style={[a.gap_md]}>
<View style={[a.align_center]}>
<Newskie
width={64}
height={64}
fill="#FFC404"
style={{marginTop: -10}}
/>
<Text style={[a.font_bold, a.text_xl, {marginTop: -10}]}>
<View
style={[
{
height: 60,
width: 64,
},
]}>
<Newskie
width={64}
height={64}
fill="#FFC404"
style={[a.absolute, a.inset_0]}
/>
</View>
<Text style={[a.font_bold, a.text_xl]}>
<Trans>Say hello!</Trans>
</Text>
</View>
<Text style={[a.text_md, a.text_center, a.leading_tight]}>
<Text style={[a.text_md, a.text_center, a.leading_snug]}>
{profile.joinedViaStarterPack ? (
<Trans>
{profileName} joined Bluesky using a starter pack{' '}
@@ -116,18 +124,23 @@ export function NewskieDialog({
</View>
</StarterPackCard.Link>
) : null}
<Button
label={_(msg`Close`)}
variant="solid"
color="secondary"
size="small"
style={[a.mt_sm, isWeb && [a.self_center, {marginLeft: 'auto'}]]}
onPress={() => control.close()}>
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
{isNative && (
<Button
label={_(msg`Close`)}
variant="solid"
color="secondary"
size="small"
style={[a.mt_sm]}
onPress={() => control.close()}>
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
)}
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
</View>
+82 -68
View File
@@ -17,7 +17,6 @@ import {HITSLOP_10} from '#/lib/constants'
import {makeListLink, makeProfileLink} from '#/lib/routes/links'
import {logger} from '#/logger'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {RQKEY_ROOT as POST_THREAD_RQKEY_ROOT} from '#/state/queries/post-thread'
import {
ThreadgateSetting,
@@ -34,6 +33,7 @@ import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Text} from '#/components/Typography'
import {TextLink} from '../view/com/util/Link'
import {ThreadgateEditorDialog} from './dialogs/ThreadgateEditor'
import {PencilLine_Stroke2_Corner0_Rounded as PencilLine} from './icons/Pencil'
interface WhoCanReplyProps {
@@ -46,7 +46,15 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
const {_} = useLingui()
const t = useTheme()
const infoDialogControl = useDialogControl()
const {settings, isRootPost, onPressEdit} = useWhoCanReply(post)
const editDialogControl = useDialogControl()
const agent = useAgent()
const queryClient = useQueryClient()
const settings = React.useMemo(
() => threadgateViewToSettings(post.threadgate),
[post],
)
const isRootPost = !('reply' in post.record)
if (!isRootPost) {
return null
@@ -63,6 +71,55 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
? _(msg`Replies disabled`)
: _(msg`Some people can reply`)
const onPressEdit = () => {
if (isNative && Keyboard.isVisible()) {
Keyboard.dismiss()
}
if (isThreadAuthor) {
editDialogControl.open()
} else {
infoDialogControl.open()
}
}
const onEditConfirm = async (newSettings: ThreadgateSetting[]) => {
if (JSON.stringify(settings) === JSON.stringify(newSettings)) {
return
}
try {
if (newSettings.length) {
await createThreadgate(agent, post.uri, newSettings)
} else {
await agent.api.com.atproto.repo.deleteRecord({
repo: agent.session!.did,
collection: 'app.bsky.feed.threadgate',
rkey: new AtUri(post.uri).rkey,
})
}
await whenAppViewReady(agent, post.uri, res => {
const thread = res.data.thread
if (AppBskyFeedDefs.isThreadViewPost(thread)) {
const fetchedSettings = threadgateViewToSettings(
thread.post.threadgate,
)
return JSON.stringify(fetchedSettings) === JSON.stringify(newSettings)
}
return false
})
Toast.show(_(msg`Thread settings updated`))
queryClient.invalidateQueries({
queryKey: [POST_THREAD_RQKEY_ROOT],
})
} catch (err) {
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
)
logger.error('Failed to edit threadgate', {message: err})
}
}
return (
<>
<Button
@@ -93,7 +150,18 @@ export function WhoCanReply({post, isThreadAuthor, style}: WhoCanReplyProps) {
</View>
)}
</Button>
<WhoCanReplyDialog control={infoDialogControl} post={post} />
<WhoCanReplyDialog
control={infoDialogControl}
post={post}
settings={settings}
/>
{isThreadAuthor && (
<ThreadgateEditorDialog
control={editDialogControl}
threadgate={settings}
onConfirm={onEditConfirm}
/>
)}
</>
)
}
@@ -113,24 +181,31 @@ function Icon({
return <IconComponent fill={color} width={width} />
}
export function WhoCanReplyDialog({
function WhoCanReplyDialog({
control,
post,
settings,
}: {
control: Dialog.DialogControlProps
post: AppBskyFeedDefs.PostView
settings: ThreadgateSetting[]
}) {
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<WhoCanReplyDialogInner post={post} />
<WhoCanReplyDialogInner post={post} settings={settings} />
</Dialog.Outer>
)
}
function WhoCanReplyDialogInner({post}: {post: AppBskyFeedDefs.PostView}) {
function WhoCanReplyDialogInner({
post,
settings,
}: {
post: AppBskyFeedDefs.PostView
settings: ThreadgateSetting[]
}) {
const {_} = useLingui()
const {settings} = useWhoCanReply(post)
return (
<Dialog.ScrollableInner
label={_(msg`Who can reply dialog`)}
@@ -245,67 +320,6 @@ function Separator({i, length}: {i: number; length: number}) {
return <>, </>
}
function useWhoCanReply(post: AppBskyFeedDefs.PostView) {
const agent = useAgent()
const queryClient = useQueryClient()
const {openModal} = useModalControls()
const settings = React.useMemo(
() => threadgateViewToSettings(post.threadgate),
[post],
)
const isRootPost = !('reply' in post.record)
const onPressEdit = () => {
if (isNative && Keyboard.isVisible()) {
Keyboard.dismiss()
}
openModal({
name: 'threadgate',
settings,
async onConfirm(newSettings: ThreadgateSetting[]) {
if (JSON.stringify(settings) === JSON.stringify(newSettings)) {
return
}
try {
if (newSettings.length) {
await createThreadgate(agent, post.uri, newSettings)
} else {
await agent.api.com.atproto.repo.deleteRecord({
repo: agent.session!.did,
collection: 'app.bsky.feed.threadgate',
rkey: new AtUri(post.uri).rkey,
})
}
await whenAppViewReady(agent, post.uri, res => {
const thread = res.data.thread
if (AppBskyFeedDefs.isThreadViewPost(thread)) {
const fetchedSettings = threadgateViewToSettings(
thread.post.threadgate,
)
return (
JSON.stringify(fetchedSettings) === JSON.stringify(newSettings)
)
}
return false
})
Toast.show('Thread settings updated')
queryClient.invalidateQueries({
queryKey: [POST_THREAD_RQKEY_ROOT],
})
} catch (err) {
Toast.show(
'There was an issue. Please check your internet connection and try again.',
)
logger.error('Failed to edit threadgate', {message: err})
}
},
})
}
return {settings, isRootPost, onPressEdit}
}
async function whenAppViewReady(
agent: BskyAgent,
uri: string,
+1
View File
@@ -113,6 +113,7 @@ export function EmbedConsentDialog({
</ButtonText>
</Button>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
</Dialog.Outer>
)
+218
View File
@@ -0,0 +1,218 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import isEqual from 'lodash.isequal'
import {useMyListsQuery} from '#/state/queries/my-lists'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {Text} from '#/components/Typography'
interface ThreadgateEditorDialogProps {
control: Dialog.DialogControlProps
threadgate: ThreadgateSetting[]
onChange?: (v: ThreadgateSetting[]) => void
onConfirm?: (v: ThreadgateSetting[]) => void
}
export function ThreadgateEditorDialog({
control,
threadgate,
onChange,
onConfirm,
}: ThreadgateEditorDialogProps) {
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<DialogContent
seedThreadgate={threadgate}
onChange={onChange}
onConfirm={onConfirm}
/>
</Dialog.Outer>
)
}
function DialogContent({
seedThreadgate,
onChange,
onConfirm,
}: {
seedThreadgate: ThreadgateSetting[]
onChange?: (v: ThreadgateSetting[]) => void
onConfirm?: (v: ThreadgateSetting[]) => void
}) {
const {_} = useLingui()
const control = Dialog.useDialogContext()
const {data: lists} = useMyListsQuery('curate')
const [draft, setDraft] = React.useState(seedThreadgate)
const [prevSeedThreadgate, setPrevSeedThreadgate] =
React.useState(seedThreadgate)
if (seedThreadgate !== prevSeedThreadgate) {
// New data flowed from above (e.g. due to update coming through).
setPrevSeedThreadgate(seedThreadgate)
setDraft(seedThreadgate) // Reset draft.
}
function updateThreadgate(nextThreadgate: ThreadgateSetting[]) {
setDraft(nextThreadgate)
onChange?.(nextThreadgate)
}
const onPressEverybody = () => {
updateThreadgate([])
}
const onPressNobody = () => {
updateThreadgate([{type: 'nobody'}])
}
const onPressAudience = (setting: ThreadgateSetting) => {
// remove nobody
let newSelected = draft.filter(v => v.type !== 'nobody')
// toggle
const i = newSelected.findIndex(v => isEqual(v, setting))
if (i === -1) {
newSelected.push(setting)
} else {
newSelected.splice(i, 1)
}
updateThreadgate(newSelected)
}
const doneLabel = onConfirm ? _(msg`Save`) : _(msg`Done`)
return (
<Dialog.ScrollableInner
label={_(msg`Choose who can reply`)}
style={[{maxWidth: 500}, a.w_full]}>
<View style={[a.flex_1, a.gap_md]}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Chose who can reply</Trans>
</Text>
<Text style={a.mt_xs}>
<Trans>Either choose "Everybody" or "Nobody"</Trans>
</Text>
<View style={[a.flex_row, a.gap_sm]}>
<Selectable
label={_(msg`Everybody`)}
isSelected={draft.length === 0}
onPress={onPressEverybody}
style={{flex: 1}}
/>
<Selectable
label={_(msg`Nobody`)}
isSelected={!!draft.find(v => v.type === 'nobody')}
onPress={onPressNobody}
style={{flex: 1}}
/>
</View>
<Text style={a.mt_md}>
<Trans>Or combine these options:</Trans>
</Text>
<View style={[a.gap_sm]}>
<Selectable
label={_(msg`Mentioned users`)}
isSelected={!!draft.find(v => v.type === 'mention')}
onPress={() => onPressAudience({type: 'mention'})}
/>
<Selectable
label={_(msg`Followed users`)}
isSelected={!!draft.find(v => v.type === 'following')}
onPress={() => onPressAudience({type: 'following'})}
/>
{lists && lists.length > 0
? lists.map(list => (
<Selectable
key={list.uri}
label={_(msg`Users in "${list.name}"`)}
isSelected={
!!draft.find(v => v.type === 'list' && v.list === list.uri)
}
onPress={() =>
onPressAudience({type: 'list', list: list.uri})
}
/>
))
: // No loading states to avoid jumps for the common case (no lists)
null}
</View>
</View>
<Button
label={doneLabel}
onPress={() => {
control.close()
onConfirm?.(draft)
}}
onAccessibilityEscape={control.close}
color="primary"
size="medium"
variant="solid"
style={a.mt_xl}>
<ButtonText>{doneLabel}</ButtonText>
</Button>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}
function Selectable({
label,
isSelected,
onPress,
style,
}: {
label: string
isSelected: boolean
onPress: () => void
style?: StyleProp<ViewStyle>
}) {
const t = useTheme()
return (
<Button
onPress={onPress}
label={label}
accessibilityHint="Select this option"
accessibilityRole="checkbox"
aria-checked={isSelected}
accessibilityState={{
checked: isSelected,
}}
style={a.flex_1}>
{({hovered, focused}) => (
<View
style={[
a.flex_1,
a.flex_row,
a.align_center,
a.justify_between,
a.rounded_sm,
a.p_md,
{height: 40}, // for consistency with checkmark icon visible or not
t.atoms.bg_contrast_50,
(hovered || focused) && t.atoms.bg_contrast_100,
isSelected && {
backgroundColor:
t.name === 'light'
? t.palette.primary_50
: t.palette.primary_975,
},
style,
]}>
<Text style={[a.text_sm, isSelected && a.font_semibold]}>
{label}
</Text>
{isSelected ? (
<Check size="sm" fill={t.palette.primary_500} />
) : (
<View />
)}
</View>
)}
</Button>
)
}
+9
View File
@@ -13,6 +13,15 @@ export const EMBED_SERVICE = 'https://embed.bsky.app'
export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
export const BSKY_DOWNLOAD_URL = 'https://bsky.app/download'
// HACK
// Yes, this is exactly what it looks like. It's a hard-coded constant
// reflecting the number of new users in the last week. We don't have
// time to add a route to the servers for this so we're just going to hard
// code and update this number with each release until we can get the
// server route done.
// -prf
export const JOINED_THIS_WEEK = 37115 // as of June24 2024
const BASE_FEEDBACK_FORM_URL = `${HELP_DESK_URL}/requests/new`
export function FEEDBACK_FORM_URL({
email,
+44 -44
View File
@@ -18,7 +18,7 @@ msgstr ""
#: src/screens/Messages/List/ChatListItem.tsx:120
msgid "(contains embedded content)"
msgstr ""
msgstr "(té contingut incrustat)"
#: src/view/com/modals/VerifyEmail.tsx:150
msgid "(no email)"
@@ -113,7 +113,7 @@ msgstr ""
#: src/view/com/util/UserAvatar.tsx:419
msgid "{0}'s avatar"
msgstr ""
msgstr "Avatar de {0}"
#: src/screens/StarterPack/Wizard/StepDetails.tsx:68
msgid "{0}'s favorite feeds and people - join me!"
@@ -505,7 +505,7 @@ msgstr "Tots els canals que has desat, en un sol lloc."
#: src/view/com/modals/AddAppPasswords.tsx:187
#: src/view/com/modals/AddAppPasswords.tsx:194
msgid "Allow access to your direct messages"
msgstr ""
msgstr "Permet l'accés als teus missatges directes"
#: src/screens/Messages/Settings.tsx:61
#: src/screens/Messages/Settings.tsx:64
@@ -515,7 +515,7 @@ msgstr ""
#: src/screens/Messages/Settings.tsx:62
#: src/screens/Messages/Settings.tsx:65
msgid "Allow new messages from"
msgstr ""
msgstr "Permet missatges nou de"
#: src/screens/Login/ForgotPasswordForm.tsx:178
#: src/view/com/modals/ChangePassword.tsx:171
@@ -1039,7 +1039,7 @@ msgstr "Cancel·la la citació de la publicació"
#: src/screens/Deactivated.tsx:155
msgid "Cancel reactivation and log out"
msgstr ""
msgstr "Cancel·la la reactivació i surt"
#: src/view/com/modals/ListAddRemoveUsers.tsx:87
#: src/view/shell/desktop/Search.tsx:214
@@ -1118,7 +1118,7 @@ msgstr "Configuració del xat"
#: src/screens/Messages/Settings.tsx:59
#: src/view/screens/Settings/index.tsx:647
msgid "Chat Settings"
msgstr ""
msgstr "Configuració del xat"
#: src/components/dms/ConvoMenu.tsx:84
msgid "Chat unmuted"
@@ -1221,11 +1221,11 @@ msgstr "clica aquí"
#: src/view/com/modals/DeleteAccount.tsx:208
msgid "Click here for more information on deactivating your account"
msgstr ""
msgstr "Clica aquí per a més informació sobre desactivar el teu compte"
#: src/view/com/modals/DeleteAccount.tsx:216
msgid "Click here for more information."
msgstr ""
msgstr "Clica aquí per a més informació."
#: src/screens/Feeds/NoFollowingFeed.tsx:46
#~ msgid "Click here to add one."
@@ -1325,7 +1325,7 @@ msgstr "Tanca la visualització de la imatge de la capçalera"
#: src/view/com/notifications/FeedItem.tsx:226
msgid "Collapse list of users"
msgstr ""
msgstr "Plega la llista d'usuaris"
#: src/view/com/notifications/FeedItem.tsx:426
msgid "Collapses list of users for a given notification"
@@ -1744,11 +1744,11 @@ msgstr "Data de naixement"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:73
#: src/view/screens/Settings/index.tsx:806
msgid "Deactivate account"
msgstr ""
msgstr "Desactiva el compte"
#: src/view/screens/Settings/index.tsx:818
msgid "Deactivate my account"
msgstr ""
msgstr "Desactiva el meu compte"
#: src/view/screens/Settings/index.tsx:873
msgid "Debug Moderation"
@@ -2402,7 +2402,7 @@ msgstr "Expandeix el text alternatiu"
#: src/view/com/notifications/FeedItem.tsx:227
msgid "Expand list of users"
msgstr ""
msgstr "Expandeix la llista d'usuaris"
#: src/view/com/composer/ComposerReplyTo.tsx:82
#: src/view/com/composer/ComposerReplyTo.tsx:85
@@ -2679,7 +2679,7 @@ msgstr "Segueix {0}"
#: src/view/com/posts/AviFollowButton.tsx:71
msgid "Follow {name}"
msgstr ""
msgstr "Segueix a {name}"
#: src/view/com/profile/ProfileMenu.tsx:246
#: src/view/com/profile/ProfileMenu.tsx:257
@@ -2782,7 +2782,7 @@ msgstr "Seguint {0}"
#: src/view/com/posts/AviFollowButton.tsx:53
msgid "Following {name}"
msgstr ""
msgstr "Seguint a {name}"
#: src/view/screens/Settings/index.tsx:573
msgid "Following feed preferences"
@@ -3145,7 +3145,7 @@ msgstr "Si vols canviar la contrasenya t'enviarem un codi per a verificar que aq
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:92
msgid "If you're trying to change your handle or email, do so before you deactivate."
msgstr ""
msgstr "Si vols canviar el teu identificador o el correu fes-ho abans de desactivar el compte."
#: src/lib/moderation/useReportOptions.ts:38
msgid "Illegal and Urgent"
@@ -3622,7 +3622,7 @@ msgstr "Registre"
#: src/screens/Deactivated.tsx:214
#: src/screens/Deactivated.tsx:220
msgid "Log in or sign up"
msgstr ""
msgstr "Inicia sessió o registra't"
#: src/screens/SignupQueued.tsx:155
#: src/screens/SignupQueued.tsx:158
@@ -4380,7 +4380,7 @@ msgstr "Obre"
#: src/view/com/posts/AviFollowButton.tsx:89
msgid "Open {name} profile shortcut menu"
msgstr ""
msgstr "Obre el menú de drecera del perfil {name}"
#: src/screens/Onboarding/StepProfile/index.tsx:277
msgid "Open avatar creator"
@@ -4463,7 +4463,7 @@ msgstr "Obre la càmera del dispositiu"
#: src/view/screens/Settings/index.tsx:639
msgid "Opens chat settings"
msgstr ""
msgstr "Obre la configuració del xat"
#: src/view/com/composer/Prompt.tsx:27
msgid "Opens composer"
@@ -4517,7 +4517,7 @@ msgstr "Obre la llista de codis d'invitació"
#: src/view/screens/Settings/index.tsx:808
msgid "Opens modal for account deactivation confirmation"
msgstr ""
msgstr "Obre el modal per a la confirmació de la desactivació del compte"
#: src/view/screens/Settings/index.tsx:830
msgid "Opens modal for account deletion confirmation. Requires email code"
@@ -4604,7 +4604,7 @@ msgstr "Obre les preferències dels fils de debat"
#: src/view/com/notifications/FeedItem.tsx:513
#: src/view/com/util/UserAvatar.tsx:422
msgid "Opens this profile"
msgstr ""
msgstr "Obre aquest perfil"
#: src/view/com/util/forms/DropdownButton.tsx:293
msgid "Option {0} of {numItems}"
@@ -4621,11 +4621,11 @@ msgstr "O combina aquestes opcions:"
#: src/screens/Deactivated.tsx:211
msgid "Or, continue with another account."
msgstr ""
msgstr "O continua amb un altre compte."
#: src/screens/Deactivated.tsx:194
msgid "Or, log into one of your other accounts."
msgstr ""
msgstr "O inicia sessió en un altre dels teus comptes."
#: src/lib/moderation/useReportOptions.ts:27
msgid "Other"
@@ -5067,7 +5067,7 @@ msgstr "Proporcions"
#: src/screens/Deactivated.tsx:144
msgid "Reactivate your account"
msgstr ""
msgstr "Torna a activar el teu compte"
#: src/components/dms/ReportDialog.tsx:174
msgid "Reason:"
@@ -5129,7 +5129,7 @@ msgstr "Elimina el bàner"
#: src/screens/Messages/Conversation/MessageInputEmbed.tsx:218
msgid "Remove embed"
msgstr ""
msgstr "Elimina l'incrustat"
#: src/view/com/posts/FeedErrorMessage.tsx:168
#: src/view/com/posts/FeedShutdownMsg.tsx:113
@@ -5168,11 +5168,11 @@ msgstr "Elimina la paraula silenciada de la teva llista"
#: src/view/screens/Search/Search.tsx:974
msgid "Remove profile"
msgstr ""
msgstr "Elimina el perfil"
#: src/view/screens/Search/Search.tsx:976
msgid "Remove profile from search history"
msgstr ""
msgstr "Elimina el perfil de l'historial de cerca"
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:238
msgid "Remove quote"
@@ -5868,7 +5868,7 @@ msgstr "Envia el missatge"
#: src/components/dms/dialogs/ShareViaChatDialog.tsx:59
msgid "Send post to..."
msgstr ""
msgstr "Envia el missatge a..."
#: src/components/dms/ReportDialog.tsx:234
#: src/components/dms/ReportDialog.tsx:237
@@ -5893,7 +5893,7 @@ msgstr "Envia un correu de verificació"
#: src/view/com/util/forms/PostDropdownBtn.tsx:296
#: src/view/com/util/forms/PostDropdownBtn.tsx:299
msgid "Send via direct message"
msgstr ""
msgstr "Envia per missatge directe"
#: src/view/com/modals/DeleteAccount.tsx:151
msgid "Sends email with confirmation code for account deletion"
@@ -6143,7 +6143,7 @@ msgstr "Mostra seguidors semblants a {0}"
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
msgid "Show hidden replies"
msgstr ""
msgstr "Mostra les respostes ocultes"
#: src/view/com/util/forms/PostDropdownBtn.tsx:346
#: src/view/com/util/forms/PostDropdownBtn.tsx:348
@@ -6163,7 +6163,7 @@ msgstr "Mostra'n més com aquest"
#: src/view/com/post-thread/PostThreadShowHiddenReplies.tsx:23
msgid "Show muted replies"
msgstr ""
msgstr "Mostra les respostes silenciades"
#: src/view/screens/PreferencesFollowingFeed.tsx:257
msgid "Show Posts from My Feeds"
@@ -6371,7 +6371,7 @@ msgstr "Alguna cosa ha fallat"
#: src/screens/Deactivated.tsx:94
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:59
msgid "Something went wrong, please try again"
msgstr ""
msgstr "Alguna cosa ha fallat, torna-ho a provar"
#: src/components/ReportDialog/index.tsx:59
#: src/screens/Moderation/index.tsx:114
@@ -6715,7 +6715,7 @@ msgstr "Les condicions del servei han estat traslladades a"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:86
msgid "There is no time limit for account deactivation, come back any time."
msgstr ""
msgstr "No hi ha límit de temps per a la desactivació del compte, torna quan vulguis."
#: src/screens/Profile/Header/ProfileHeaderLabeler.tsx:115
#: src/view/screens/ProfileFeed.tsx:542
@@ -7517,7 +7517,7 @@ msgstr "Veure l'avatar de {0}"
#: src/view/com/notifications/FeedItem.tsx:234
msgid "View {0}'s profile"
msgstr ""
msgstr "Veure el perfil de {0}"
#: src/components/ProfileHoverCard/index.web.tsx:430
msgid "View blocked user's profile"
@@ -7682,7 +7682,7 @@ msgstr ""
#: src/screens/Deactivated.tsx:128
msgid "Welcome back!"
msgstr ""
msgstr "Bentornat!"
#: src/view/com/auth/onboarding/WelcomeMobile.tsx:48
#~ msgid "Welcome to <0>Bluesky</0>"
@@ -7808,7 +7808,7 @@ msgstr "Sí"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:106
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:108
msgid "Yes, deactivate"
msgstr ""
msgstr "Sí, desactiva'l"
#: src/screens/StarterPack/StarterPackScreen.tsx:525
msgid "Yes, delete this starter pack"
@@ -7816,7 +7816,7 @@ msgstr ""
#: src/screens/Deactivated.tsx:150
msgid "Yes, reactivate my account"
msgstr ""
msgstr "Sí, torna a activar el meu compte"
#: src/components/dms/MessageItem.tsx:188
msgid "Yesterday, {time}"
@@ -7841,7 +7841,7 @@ msgstr "També pots descobrir nous canals personalitzats per a seguir."
#: src/view/com/modals/DeleteAccount.tsx:202
msgid "You can also temporarily deactivate your account instead, and reactivate it at any time."
msgstr ""
msgstr "També pots desactivar el teu compte temporalment i reactivar-lo en qualsevol moment."
#: src/view/com/auth/create/Step1.tsx:106
#~ msgid "You can change hosting providers at any time."
@@ -7857,7 +7857,7 @@ msgstr "Pots canviar-ho quan vulguis."
#: src/screens/Messages/Settings.tsx:111
msgid "You can continue ongoing conversations regardless of which setting you choose."
msgstr ""
msgstr "Pots continuar les converses en curs independentment de la configuració que triïs."
#: src/screens/Login/index.tsx:158
#: src/screens/Login/PasswordUpdatedForm.tsx:33
@@ -7866,7 +7866,7 @@ msgstr "Ara pots iniciar sessió amb la nova contrasenya."
#: src/screens/Deactivated.tsx:136
msgid "You can reactivate your account to continue logging in. Your profile and posts will be visible to other users."
msgstr ""
msgstr "Pots reactivar el teu compte per continuar iniciant la sessió. El teu perfil i les publicacions seran visibles per a altres usuaris."
#: src/view/com/profile/ProfileFollowers.tsx:86
msgid "You do not have any followers."
@@ -8025,7 +8025,7 @@ msgstr "Has d'escollir almenys un etiquetador per a un informe"
#: src/screens/Deactivated.tsx:131
msgid "You previously deactivated @{0}."
msgstr ""
msgstr "Abans has desactivat @{0}."
#: src/view/com/util/forms/PostDropdownBtn.tsx:174
msgid "You will no longer receive notifications for this thread"
@@ -8045,11 +8045,11 @@ msgstr "Tu: {0}"
#: src/screens/Messages/List/ChatListItem.tsx:143
msgid "You: {defaultEmbeddedContentMessage}"
msgstr ""
msgstr "Tu: {defaultEmbeddedContentMessage}"
#: src/screens/Messages/List/ChatListItem.tsx:136
msgid "You: {short}"
msgstr ""
msgstr "Tu: {short}"
#: src/screens/Signup/index.tsx:169
msgid "You'll follow the suggested users and feeds once you finish creating your account!"
@@ -8084,7 +8084,7 @@ msgstr "Estàs a la cua"
#: src/screens/Deactivated.tsx:89
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:54
msgid "You're logged in with an App Password. Please log in with your main password to continue deactivating your account."
msgstr ""
msgstr "Has iniciat sessió amb una contrasenya d'aplicació. Inicia sessió amb la teva contrasenya principal per continuar la desactivació del teu compte."
#: src/screens/Onboarding/StepFinished.tsx:228
msgid "You're ready to go!"
@@ -8189,7 +8189,7 @@ msgstr "El teu perfil"
#: src/screens/Settings/components/DeactivateAccountDialog.tsx:75
msgid "Your profile, posts, feeds, and lists will no longer be visible to other Bluesky users. You can reactivate your account at any time by logging in."
msgstr ""
msgstr "El teu perfil, publicacions, fonts i llistes ja no seran visibles per a altres usuaris de Bluesky. Pots reactivar el teu compte en qualsevol moment iniciant sessió."
#: src/view/com/composer/Composer.tsx:365
msgid "Your reply has been published"
+2 -2
View File
@@ -110,8 +110,8 @@ export function StepFinished() {
// Any starter pack feeds will be pinned _after_ the defaults
if (starterPack && starterPack.feeds?.length) {
feedsToSave.concat(
starterPack.feeds.map(f => ({
feedsToSave.push(
...starterPack.feeds.map(f => ({
type: 'feed',
value: f.uri,
pinned: true,
+2 -2
View File
@@ -5,7 +5,7 @@ import {Trans} from '@lingui/macro'
import {Shadow} from '#/state/cache/types'
import {isInvalidHandle} from 'lib/strings/handles'
import {isAndroid} from 'platform/detection'
import {isIOS} from 'platform/detection'
import {atoms as a, useTheme, web} from '#/alf'
import {NewskieDialog} from '#/components/NewskieDialog'
import {Text} from '#/components/Typography'
@@ -23,7 +23,7 @@ export function ProfileHeaderHandle({
return (
<View
style={[a.flex_row, a.gap_xs, a.align_center]}
pointerEvents={disableTaps ? 'none' : isAndroid ? 'box-only' : 'auto'}>
pointerEvents={disableTaps ? 'none' : isIOS ? 'auto' : 'box-none'}>
<NewskieDialog profile={profile} disabled={disableTaps} />
{profile.viewer?.followedBy && !blockHide ? (
<View style={[t.atoms.bg_contrast_25, a.rounded_xs, a.px_sm, a.py_xs]}>
@@ -11,6 +11,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {JOINED_THIS_WEEK} from '#/lib/constants'
import {isAndroidWeb} from 'lib/browser'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack'
@@ -21,6 +22,7 @@ import {
useActiveStarterPack,
useSetActiveStarterPack,
} from 'state/shell/starter-pack'
import {formatCount} from '#/view/com/util/numeric/format'
import {LoggedOutScreenState} from 'view/com/auth/LoggedOut'
import {CenteredView} from 'view/com/util/Views'
import {Logo} from 'view/icons/Logo'
@@ -95,7 +97,7 @@ function LandingScreenLoaded({
setScreenState: (state: LoggedOutScreenState) => void
moderationOpts: ModerationOpts
}) {
const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack
const {record, creator, listItemsSample, feeds} = starterPack
const {_} = useLingui()
const t = useTheme()
const activeStarterPack = useActiveStarterPack()
@@ -200,24 +202,22 @@ function LandingScreenLoaded({
<Trans>Join Bluesky</Trans>
</ButtonText>
</Button>
{joinedWeekCount && joinedWeekCount >= 25 ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<FontAwesomeIcon
icon="arrow-trend-up"
size={12}
color={t.atoms.text_contrast_medium.color}
/>
<Text
style={[
a.font_semibold,
a.text_sm,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
123,659 joined this week
</Text>
</View>
) : null}
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<FontAwesomeIcon
icon="arrow-trend-up"
size={12}
color={t.atoms.text_contrast_medium.color}
/>
<Text
style={[
a.font_semibold,
a.text_sm,
t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
<Trans>{formatCount(JOINED_THIS_WEEK)} joined this week</Trans>
</Text>
</View>
</View>
<View style={[a.gap_3xl]}>
{Boolean(listItemsSample?.length) && (
@@ -231,18 +231,26 @@ function LandingScreenLoaded({
</Trans>
)}
</Text>
<View>
<View
style={
isTabletOrDesktop && [
a.border,
a.rounded_md,
t.atoms.border_contrast_low,
]
}>
{starterPack.listItemsSample
?.filter(p => !p.subject.associated?.labeler)
.slice(0, 8)
.map(item => (
.map((item, i) => (
<View
key={item.subject.did}
style={[
a.py_lg,
a.px_md,
a.border_t,
(!isTabletOrDesktop || i !== 0) && a.border_t,
t.atoms.border_contrast_low,
{pointerEvents: 'none'},
]}>
<ProfileCard
profile={item.subject}
@@ -259,13 +267,21 @@ function LandingScreenLoaded({
<Trans>You'll stay updated with these feeds</Trans>
</Text>
<View style={[{pointerEvents: 'none'}]}>
{feeds?.map(feed => (
<View
style={[
{pointerEvents: 'none'},
isTabletOrDesktop && [
a.border,
a.rounded_md,
t.atoms.border_contrast_low,
],
]}>
{feeds?.map((feed, i) => (
<View
style={[
a.py_lg,
a.px_md,
a.border_t,
(!isTabletOrDesktop || i !== 0) && a.border_t,
t.atoms.border_contrast_low,
]}
key={feed.uri}>
+12 -5
View File
@@ -7,6 +7,7 @@ import {
AppBskyGraphStarterpack,
AtUri,
ModerationOpts,
RichText as RichTextAPI,
} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
@@ -52,6 +53,7 @@ import {Loader} from '#/components/Loader'
import * as Menu from '#/components/Menu'
import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText'
import {FeedsList} from '#/components/StarterPack/Main/FeedsList'
import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList'
import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog'
@@ -280,6 +282,13 @@ function Header({
return null
}
const richText = record.description
? new RichTextAPI({
text: record.description,
facets: record.descriptionFacets,
})
: undefined
return (
<>
<ProfileSubpageHeader
@@ -324,12 +333,10 @@ function Header({
/>
</View>
</ProfileSubpageHeader>
{record.description || joinedAllTimeCount >= 25 ? (
{richText || joinedAllTimeCount >= 25 ? (
<View style={[a.px_lg, a.pt_md, a.pb_sm, a.gap_md]}>
{record.description ? (
<Text style={[a.text_md, a.leading_snug]}>
{record.description}
</Text>
{richText ? (
<RichText value={richText} style={[a.text_md, a.leading_snug]} />
) : null}
{joinedAllTimeCount >= 25 ? (
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
+13 -5
View File
@@ -32,7 +32,7 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
const throttledQuery = useThrottledValue(query, 500)
const {screenReaderEnabled} = useA11y()
const {data: savedFeedsAndLists, isLoading: isLoadingSavedFeeds} =
const {data: savedFeedsAndLists, isFetchedAfterMount: isFetchedSavedFeeds} =
useSavedFeeds()
const savedFeeds = savedFeedsAndLists?.feeds
.filter(f => f.type === 'feed' && f.view.uri !== DISCOVER_FEED_URI)
@@ -46,15 +46,23 @@ export function StepFeeds({moderationOpts}: {moderationOpts: ModerationOpts}) {
limit: 30,
})
const popularFeeds = popularFeedsPages?.pages.flatMap(p => p.feeds) ?? []
const suggestedFeeds = savedFeeds.concat(
popularFeeds.filter(f => !savedFeeds.some(sf => sf.uri === f.uri)),
)
// If we have saved feeds already loaded, display them immediately
// Then, when popular feeds have loaded we can concat them to the saved feeds
const suggestedFeeds =
savedFeeds || isFetchedSavedFeeds
? popularFeeds
? savedFeeds.concat(
popularFeeds.filter(f => !savedFeeds.some(sf => sf.uri === f.uri)),
)
: savedFeeds
: undefined
const {data: searchedFeeds, isFetching: isFetchingSearchedFeeds} =
useSearchPopularFeedsQuery({q: throttledQuery})
const isLoading =
isLoadingSavedFeeds || isLoadingPopularFeeds || isFetchingSearchedFeeds
!isFetchedSavedFeeds || isLoadingPopularFeeds || isFetchingSearchedFeeds
const renderItem = ({
item,
-2
View File
@@ -245,7 +245,6 @@ function WizardInner({
editStarterPack({
name: state.name?.trim() || getDefaultName(),
description: state.description?.trim(),
descriptionFacets: [],
profiles: state.profiles,
feeds: state.feeds,
currentStarterPack: currentStarterPack,
@@ -255,7 +254,6 @@ function WizardInner({
createStarterPack({
name: state.name?.trim() || getDefaultName(),
description: state.description?.trim(),
descriptionFacets: [],
profiles: state.profiles,
feeds: state.feeds,
})
-9
View File
@@ -5,7 +5,6 @@ import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {GalleryModel} from '#/state/models/media/gallery'
import {ImageModel} from '#/state/models/media/image'
import {ThreadgateSetting} from '../queries/threadgate'
export interface EditProfileModal {
name: 'edit-profile'
@@ -67,13 +66,6 @@ export interface SelfLabelModal {
onChange: (labels: string[]) => void
}
export interface ThreadgateModal {
name: 'threadgate'
settings: ThreadgateSetting[]
onChange?: (settings: ThreadgateSetting[]) => void
onConfirm?: (settings: ThreadgateSetting[]) => void
}
export interface ChangeHandleModal {
name: 'change-handle'
onChanged: () => void
@@ -149,7 +141,6 @@ export type Modal =
| CropImageModal
| EditImageModal
| SelfLabelModal
| ThreadgateModal
// Bluesky access
| WaitlistModal
+36 -14
View File
@@ -4,8 +4,10 @@ import {
AppBskyGraphDefs,
AppBskyGraphGetStarterPack,
AppBskyGraphStarterpack,
AppBskyRichtextFacet,
AtUri,
BskyAgent,
RichText,
} from '@atproto/api'
import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs'
import {
@@ -80,7 +82,6 @@ export async function invalidateStarterPack({
interface UseCreateStarterPackMutationParams {
name: string
description?: string
descriptionFacets: []
profiles: AppBskyActorDefs.ProfileViewBasic[]
feeds?: AppBskyFeedDefs.GeneratorView[]
}
@@ -100,16 +101,33 @@ export function useCreateStarterPackMutation({
Error,
UseCreateStarterPackMutationParams
>({
mutationFn: async params => {
mutationFn: async ({name, description, feeds, profiles}) => {
let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined
if (description) {
const rt = new RichText({text: description})
await rt.detectFacets(agent)
descriptionFacets = rt.facets
}
let listRes
listRes = await createStarterPackList({...params, agent})
listRes = await createStarterPackList({
name,
description,
profiles,
descriptionFacets,
agent,
})
return await agent.app.bsky.graph.starterpack.create(
{
repo: agent.session?.did,
},
{
...params,
name,
description,
descriptionFacets,
list: listRes?.uri,
feeds,
createdAt: new Date().toISOString(),
},
)
@@ -148,16 +166,20 @@ export function useEditStarterPackMutation({
currentListItems: AppBskyGraphDefs.ListItemView[]
}
>({
mutationFn: async params => {
const {
name,
description,
descriptionFacets,
feeds,
profiles,
currentStarterPack,
currentListItems,
} = params
mutationFn: async ({
name,
description,
feeds,
profiles,
currentStarterPack,
currentListItems,
}) => {
let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined
if (description) {
const rt = new RichText({text: description})
await rt.detectFacets(agent)
descriptionFacets = rt.facets
}
if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) {
throw new Error('Invalid starter pack')
@@ -5,11 +5,12 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {isNative} from '#/platform/detection'
import {useModalControls} from '#/state/modals'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {useAnalytics} from 'lib/analytics/analytics'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {ThreadgateEditorDialog} from '#/components/dialogs/ThreadgateEditor'
import {CircleBanSign_Stroke2_Corner0_Rounded as CircleBanSign} from '#/components/icons/CircleBanSign'
import {Earth_Stroke2_Corner0_Rounded as Earth} from '#/components/icons/Globe'
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
@@ -26,18 +27,15 @@ export function ThreadgateBtn({
const {track} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {openModal} = useModalControls()
const control = Dialog.useDialogControl()
const onPress = () => {
track('Composer:ThreadgateOpened')
if (isNative && Keyboard.isVisible()) {
Keyboard.dismiss()
}
openModal({
name: 'threadgate',
settings: threadgate,
onChange,
})
control.open()
}
const isEverybody = threadgate.length === 0
@@ -49,19 +47,29 @@ export function ThreadgateBtn({
: _(msg`Some people can reply`)
return (
<Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}>
<Button
variant="solid"
color="secondary"
size="xsmall"
testID="openReplyGateButton"
onPress={onPress}
label={label}>
<ButtonIcon
icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group}
/>
<ButtonText>{label}</ButtonText>
</Button>
</Animated.View>
<>
<Animated.View style={[a.flex_row, a.p_sm, t.atoms.bg, style]}>
<Button
variant="solid"
color="secondary"
size="xsmall"
testID="openReplyGateButton"
onPress={onPress}
label={label}
accessibilityHint={_(
msg`Opens a dialog to choose who can reply to this thread`,
)}>
<ButtonIcon
icon={isEverybody ? Earth : isNobody ? CircleBanSign : Group}
/>
<ButtonText>{label}</ButtonText>
</Button>
</Animated.View>
<ThreadgateEditorDialog
control={control}
threadgate={threadgate}
onChange={onChange}
/>
</>
)
}
-4
View File
@@ -23,7 +23,6 @@ import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettin
import * as LinkWarningModal from './LinkWarning'
import * as ListAddUserModal from './ListAddRemoveUsers'
import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate'
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
import * as VerifyEmailModal from './VerifyEmail'
@@ -76,9 +75,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'self-label') {
snapPoints = SelfLabelModal.snapPoints
element = <SelfLabelModal.Component {...activeModal} />
} else if (activeModal?.name === 'threadgate') {
snapPoints = ThreadgateModal.snapPoints
element = <ThreadgateModal.Component {...activeModal} />
} else if (activeModal?.name === 'alt-text-image') {
snapPoints = AltImageModal.snapPoints
element = <AltImageModal.Component {...activeModal} />
-3
View File
@@ -23,7 +23,6 @@ import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettin
import * as LinkWarningModal from './LinkWarning'
import * as ListAddUserModal from './ListAddRemoveUsers'
import * as SelfLabelModal from './SelfLabel'
import * as ThreadgateModal from './Threadgate'
import * as UserAddRemoveLists from './UserAddRemoveLists'
import * as VerifyEmailModal from './VerifyEmail'
@@ -84,8 +83,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <DeleteAccountModal.Component />
} else if (modal.name === 'self-label') {
element = <SelfLabelModal.Component {...modal} />
} else if (modal.name === 'threadgate') {
element = <ThreadgateModal.Component {...modal} />
} else if (modal.name === 'change-handle') {
element = <ChangeHandleModal.Component {...modal} />
} else if (modal.name === 'invite-codes') {
-208
View File
@@ -1,208 +0,0 @@
import React, {useState} from 'react'
import {
Pressable,
StyleProp,
StyleSheet,
TouchableOpacity,
View,
ViewStyle,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import isEqual from 'lodash.isequal'
import {useModalControls} from '#/state/modals'
import {useMyListsQuery} from '#/state/queries/my-lists'
import {ThreadgateSetting} from '#/state/queries/threadgate'
import {usePalette} from 'lib/hooks/usePalette'
import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection'
import {ScrollView} from 'view/com/modals/util'
import {Text} from '../util/text/Text'
export const snapPoints = ['60%']
export function Component({
settings,
onChange,
onConfirm,
}: {
settings: ThreadgateSetting[]
onChange?: (settings: ThreadgateSetting[]) => void
onConfirm?: (settings: ThreadgateSetting[]) => void
}) {
const pal = usePalette('default')
const {closeModal} = useModalControls()
const [selected, setSelected] = useState(settings)
const {_} = useLingui()
const {data: lists} = useMyListsQuery('curate')
const onPressEverybody = () => {
setSelected([])
onChange?.([])
}
const onPressNobody = () => {
setSelected([{type: 'nobody'}])
onChange?.([{type: 'nobody'}])
}
const onPressAudience = (setting: ThreadgateSetting) => {
// remove nobody
let newSelected = selected.filter(v => v.type !== 'nobody')
// toggle
const i = newSelected.findIndex(v => isEqual(v, setting))
if (i === -1) {
newSelected.push(setting)
} else {
newSelected.splice(i, 1)
}
setSelected(newSelected)
onChange?.(newSelected)
}
return (
<View testID="threadgateModal" style={[pal.view, styles.container]}>
<View style={styles.titleSection}>
<Text type="title-lg" style={[pal.text, styles.title]}>
<Trans>Who can reply</Trans>
</Text>
</View>
<ScrollView>
<Text style={[pal.text, styles.description]}>
<Trans>Choose "Everybody" or "Nobody"</Trans>
</Text>
<View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}>
<Selectable
label={_(msg`Everybody`)}
isSelected={selected.length === 0}
onPress={onPressEverybody}
style={{flex: 1}}
/>
<Selectable
label={_(msg`Nobody`)}
isSelected={!!selected.find(v => v.type === 'nobody')}
onPress={onPressNobody}
style={{flex: 1}}
/>
</View>
<Text style={[pal.text, styles.description]}>
<Trans>Or combine these options:</Trans>
</Text>
<View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}>
<Selectable
label={_(msg`Mentioned users`)}
isSelected={!!selected.find(v => v.type === 'mention')}
onPress={() => onPressAudience({type: 'mention'})}
/>
<Selectable
label={_(msg`Followed users`)}
isSelected={!!selected.find(v => v.type === 'following')}
onPress={() => onPressAudience({type: 'following'})}
/>
{lists?.length
? lists.map(list => (
<Selectable
key={list.uri}
label={_(msg`Users in "${list.name}"`)}
isSelected={
!!selected.find(
v => v.type === 'list' && v.list === list.uri,
)
}
onPress={() =>
onPressAudience({type: 'list', list: list.uri})
}
/>
))
: null}
</View>
</ScrollView>
<View style={[styles.btnContainer, pal.borderDark]}>
<TouchableOpacity
testID="confirmBtn"
onPress={() => {
closeModal()
onConfirm?.(selected)
}}
style={styles.btn}
accessibilityRole="button"
accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
<Trans context="action">Done</Trans>
</Text>
</TouchableOpacity>
</View>
</View>
)
}
function Selectable({
label,
isSelected,
onPress,
style,
}: {
label: string
isSelected: boolean
onPress: () => void
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette(isSelected ? 'inverted' : 'default')
return (
<Pressable
onPress={onPress}
accessibilityLabel={label}
accessibilityHint=""
style={[styles.selectable, pal.border, pal.view, style]}>
<Text type="lg" style={[pal.text]}>
{label}
</Text>
{isSelected ? (
<FontAwesomeIcon icon="check" color={pal.colors.text} size={18} />
) : null}
</Pressable>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: isWeb ? 0 : 40,
},
titleSection: {
paddingTop: isWeb ? 0 : 4,
},
title: {
textAlign: 'center',
fontWeight: '600',
},
description: {
textAlign: 'center',
paddingVertical: 16,
},
selectable: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 18,
paddingVertical: 16,
borderWidth: 1,
borderRadius: 6,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
padding: 14,
backgroundColor: colors.blue3,
},
btnContainer: {
paddingTop: 20,
paddingHorizontal: 20,
},
})