Merge remote-tracking branch 'origin/main' into hailey/explicitly-filter
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -113,6 +113,7 @@ export function EmbedConsentDialog({
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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]}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user