Enforce Text suffix for Text-rendering components (#3407)

* Rm unused

* Add Text suffix to Title/Description

* Add Text suffix to text components

* Add Text suffix to props

* Validate Text components returns
zio/stable
dan 2024-04-04 21:34:55 +01:00 committed by GitHub
parent c190fd58ec
commit 3915bb4316
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 453 additions and 366 deletions

View File

@ -1,5 +1,3 @@
const bskyEslint = require('./eslint')
module.exports = { module.exports = {
root: true, root: true,
extends: [ extends: [
@ -27,29 +25,18 @@ module.exports = {
{ {
impliedTextComponents: [ impliedTextComponents: [
'Button', // TODO: Not always safe. 'Button', // TODO: Not always safe.
'ButtonText',
'DateField.Label',
'Description',
'H1', 'H1',
'H2', 'H2',
'H3', 'H3',
'H4', 'H4',
'H5', 'H5',
'H6', 'H6',
'InlineLink',
'Label',
'P', 'P',
'Prompt.Title',
'Prompt.Description',
'Prompt.Cancel', // TODO: Not always safe. 'Prompt.Cancel', // TODO: Not always safe.
'Prompt.Action', // TODO: Not always safe. 'Prompt.Action', // TODO: Not always safe.
'TextField.Label',
'TextField.Suffix',
'Title',
'Toggle.Label',
'ToggleButton.Button', // TODO: Not always safe. 'ToggleButton.Button', // TODO: Not always safe.
], ],
impliedTextProps: ['FormContainer title'], impliedTextProps: [],
}, },
], ],
'simple-import-sort/imports': [ 'simple-import-sort/imports': [

View File

@ -246,6 +246,41 @@ describe('avoid-unwrapped-text', () => {
</Foo> </Foo>
`, `,
}, },
{
code: `
function Stuff() {
return <Text>foo</Text>
}
`,
},
{
code: `
function Stuff({ foo }) {
return <View>{foo}</View>
}
`,
},
{
code: `
function MyText() {
return <Text>foo</Text>
}
`,
},
{
code: `
function MyText({ foo }) {
if (foo) {
return <Text>foo</Text>
}
return <Text>foo</Text>
}
`,
},
], ],
invalid: [ invalid: [
@ -390,6 +425,36 @@ describe('avoid-unwrapped-text', () => {
`, `,
errors: 1, errors: 1,
}, },
{
code: `
function MyText() {
return <Foo />
}
`,
errors: 1,
},
{
code: `
function MyText({ foo }) {
return <Foo>{foo}</Foo>
}
`,
errors: 1,
},
{
code: `
function MyText({ foo }) {
if (foo) {
return <Foo>{foo}</Foo>
}
return <Text>foo</Text>
}
`,
errors: 1,
},
], ],
} }

View File

@ -35,6 +35,11 @@ exports.create = function create(context) {
const impliedTextComponents = options.impliedTextComponents ?? [] const impliedTextComponents = options.impliedTextComponents ?? []
const textProps = [...impliedTextProps] const textProps = [...impliedTextProps]
const textComponents = ['Text', ...impliedTextComponents] const textComponents = ['Text', ...impliedTextComponents]
function isTextComponent(tagName) {
return textComponents.includes(tagName) || tagName.endsWith('Text')
}
return { return {
JSXText(node) { JSXText(node) {
if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) { if (typeof node.value !== 'string' || hasOnlyLineBreak(node.value)) {
@ -44,7 +49,7 @@ exports.create = function create(context) {
while (parent) { while (parent) {
if (parent.type === 'JSXElement') { if (parent.type === 'JSXElement') {
const tagName = getTagName(parent) const tagName = getTagName(parent)
if (textComponents.includes(tagName) || tagName.endsWith('Text')) { if (isTextComponent(tagName)) {
// We're good. // We're good.
return return
} }
@ -107,5 +112,36 @@ exports.create = function create(context) {
continue continue
} }
}, },
ReturnStatement(node) {
let fnScope = context.getScope()
while (fnScope && fnScope.type !== 'function') {
fnScope = fnScope.upper
}
if (!fnScope) {
return
}
const fn = fnScope.block
if (!fn.id || fn.id.type !== 'Identifier' || !fn.id.name) {
return
}
if (!/^[A-Z]\w*Text$/.test(fn.id.name)) {
return
}
if (!node.argument || node.argument.type !== 'JSXElement') {
return
}
const openingEl = node.argument.openingElement
if (openingEl.name.type !== 'JSXIdentifier') {
return
}
const returnedComponentName = openingEl.name.name
if (!isTextComponent(returnedComponentName)) {
context.report({
node,
message:
'Components ending with *Text must return <Text> or <SomeText>.',
})
}
},
} }
} }

View File

@ -250,7 +250,7 @@ export type InlineLinkProps = React.PropsWithChildren<
BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'> BaseLinkProps & TextStyleProp & Pick<TextProps, 'selectable'>
> >
export function InlineLink({ export function InlineLinkText({
children, children,
to, to,
action = 'push', action = 'push',

View File

@ -51,7 +51,7 @@ export function Outer({
) )
} }
export function Title({children}: React.PropsWithChildren<{}>) { export function TitleText({children}: React.PropsWithChildren<{}>) {
const {titleId} = React.useContext(Context) const {titleId} = React.useContext(Context)
return ( return (
<Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}> <Text nativeID={titleId} style={[a.text_2xl, a.font_bold, a.pb_sm]}>
@ -60,7 +60,7 @@ export function Title({children}: React.PropsWithChildren<{}>) {
) )
} }
export function Description({children}: React.PropsWithChildren<{}>) { export function DescriptionText({children}: React.PropsWithChildren<{}>) {
const t = useTheme() const t = useTheme()
const {descriptionId} = React.useContext(Context) const {descriptionId} = React.useContext(Context)
return ( return (
@ -175,8 +175,8 @@ export function Basic({
}>) { }>) {
return ( return (
<Outer control={control} testID="confirmModal"> <Outer control={control} testID="confirmModal">
<Title>{title}</Title> <TitleText>{title}</TitleText>
<Description>{description}</Description> <DescriptionText>{description}</DescriptionText>
<Actions> <Actions>
<Action <Action
cta={confirmButtonCta} cta={confirmButtonCta}

View File

@ -7,7 +7,7 @@ import {toShortUrl} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf' import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
import {InlineLink} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {TagMenu, useTagMenuControl} from '#/components/TagMenu' import {TagMenu, useTagMenuControl} from '#/components/TagMenu'
import {Text, TextProps} from '#/components/Typography' import {Text, TextProps} from '#/components/Typography'
@ -84,7 +84,7 @@ export function RichText({
!disableLinks !disableLinks
) { ) {
els.push( els.push(
<InlineLink <InlineLinkText
selectable={selectable} selectable={selectable}
key={key} key={key}
to={`/profile/${mention.did}`} to={`/profile/${mention.did}`}
@ -92,14 +92,14 @@ export function RichText({
// @ts-ignore TODO // @ts-ignore TODO
dataSet={WORD_WRAP}> dataSet={WORD_WRAP}>
{segment.text} {segment.text}
</InlineLink>, </InlineLinkText>,
) )
} else if (link && AppBskyRichtextFacet.validateLink(link).success) { } else if (link && AppBskyRichtextFacet.validateLink(link).success) {
if (disableLinks) { if (disableLinks) {
els.push(toShortUrl(segment.text)) els.push(toShortUrl(segment.text))
} else { } else {
els.push( els.push(
<InlineLink <InlineLinkText
selectable={selectable} selectable={selectable}
key={key} key={key}
to={link.uri} to={link.uri}
@ -108,7 +108,7 @@ export function RichText({
dataSet={WORD_WRAP} dataSet={WORD_WRAP}
shareOnLongPress> shareOnLongPress>
{toShortUrl(segment.text)} {toShortUrl(segment.text)}
</InlineLink>, </InlineLinkText>,
) )
} }
} else if ( } else if (

View File

@ -1,37 +1,36 @@
import React from 'react' import React from 'react'
import {Keyboard, View} from 'react-native' import {Keyboard, View} from 'react-native'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {AppBskyActorDefs, sanitizeMutedWordValue} from '@atproto/api'
import { import {logger} from '#/logger'
usePreferencesQuery,
useUpsertMutedWordsMutation,
useRemoveMutedWordMutation,
} from '#/state/queries/preferences'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {
usePreferencesQuery,
useRemoveMutedWordMutation,
useUpsertMutedWordsMutation,
} from '#/state/queries/preferences'
import { import {
atoms as a, atoms as a,
useTheme, native,
useBreakpoints, useBreakpoints,
useTheme,
ViewStyleProp, ViewStyleProp,
web, web,
native,
} from '#/alf' } from '#/alf'
import {Text} from '#/components/Typography'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import * as Dialog from '#/components/Dialog'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle'
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText' import {PageText_Stroke2_Corner0_Rounded as PageText} from '#/components/icons/PageText'
import {Divider} from '#/components/Divider' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {logger} from '#/logger'
import * as Dialog from '#/components/Dialog'
import * as Toggle from '#/components/forms/Toggle'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
export function MutedWordsDialog() { export function MutedWordsDialog() {
const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext() const {mutedWordsDialogControl: control} = useGlobalDialogsControlContext()
@ -130,9 +129,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle> <TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}> <View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label> <Toggle.LabelText>
<Trans>Mute in text & tags</Trans> <Trans>Mute in text & tags</Trans>
</Toggle.Label> </Toggle.LabelText>
</View> </View>
<PageText size="sm" /> <PageText size="sm" />
</TargetToggle> </TargetToggle>
@ -145,9 +144,9 @@ function MutedWordsInner({}: {control: Dialog.DialogOuterProps['control']}) {
<TargetToggle> <TargetToggle>
<View style={[a.flex_row, a.align_center, a.gap_sm]}> <View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label> <Toggle.LabelText>
<Trans>Mute in tags only</Trans> <Trans>Mute in tags only</Trans>
</Toggle.Label> </Toggle.LabelText>
</View> </View>
<Hashtag size="sm" /> <Hashtag size="sm" />
</TargetToggle> </TargetToggle>

View File

@ -8,7 +8,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared' import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label export const LabelText = TextField.LabelText
export function DateField({ export function DateField({
value, value,

View File

@ -13,7 +13,7 @@ import * as TextField from '#/components/forms/TextField'
import {DateFieldButton} from './index.shared' import {DateFieldButton} from './index.shared'
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label export const LabelText = TextField.LabelText
/** /**
* Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date * Date-only input. Accepts a date in the format YYYY-MM-DD, and reports date

View File

@ -9,7 +9,7 @@ import * as TextField from '#/components/forms/TextField'
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label export const LabelText = TextField.LabelText
const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>( const InputBase = React.forwardRef<HTMLInputElement, TextInputProps>(
({style, ...props}, ref) => { ({style, ...props}, ref) => {

View File

@ -225,7 +225,7 @@ export function createInput(Component: typeof TextInput) {
export const Input = createInput(TextInput) export const Input = createInput(TextInput)
export function Label({ export function LabelText({
nativeID, nativeID,
children, children,
}: React.PropsWithChildren<{nativeID?: string}>) { }: React.PropsWithChildren<{nativeID?: string}>) {
@ -288,7 +288,7 @@ export function Icon({icon: Comp}: {icon: React.ComponentType<SVGIconProps>}) {
) )
} }
export function Suffix({ export function SuffixText({
children, children,
label, label,
accessibilityHint, accessibilityHint,

View File

@ -3,16 +3,16 @@ import {Pressable, View, ViewStyle} from 'react-native'
import {HITSLOP_10} from 'lib/constants' import {HITSLOP_10} from 'lib/constants'
import { import {
useTheme,
atoms as a, atoms as a,
native,
flatten, flatten,
ViewStyleProp, native,
TextStyleProp, TextStyleProp,
useTheme,
ViewStyleProp,
} from '#/alf' } from '#/alf'
import {Text} from '#/components/Typography'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check' import {CheckThick_Stroke2_Corner0_Rounded as Checkmark} from '#/components/icons/Check'
import {Text} from '#/components/Typography'
export type ItemState = { export type ItemState = {
name: string name: string
@ -234,7 +234,7 @@ export function Item({
) )
} }
export function Label({ export function LabelText({
children, children,
style, style,
}: React.PropsWithChildren<TextStyleProp>) { }: React.PropsWithChildren<TextStyleProp>) {

View File

@ -13,7 +13,7 @@ import {
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {InlineLink} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '../icons/CircleInfo'
@ -243,9 +243,9 @@ export function LabelerLabelPreference({
) : isGlobalLabel ? ( ) : isGlobalLabel ? (
<Trans> <Trans>
Configured in{' '} Configured in{' '}
<InlineLink to="/moderation" style={a.text_sm}> <InlineLinkText to="/moderation" style={a.text_sm}>
moderation settings moderation settings
</InlineLink> </InlineLinkText>
. .
</Trans> </Trans>
) : null} ) : null}

View File

@ -1,20 +1,19 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog'
import {Button, ButtonText} from '#/components/Button'
import {InlineLink} from '#/components/Link'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {Divider} from '../Divider' import {Divider} from '../Divider'
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog' export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
@ -145,13 +144,13 @@ function Label({
<View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}> <View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}>
<Text style={[t.atoms.text_contrast_medium]}> <Text style={[t.atoms.text_contrast_medium]}>
<Trans>Source:</Trans>{' '} <Trans>Source:</Trans>{' '}
<InlineLink <InlineLinkText
to={makeProfileLink( to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''}, labeler ? labeler.creator : {did: label.src, handle: ''},
)} )}
onPress={() => control.close()}> onPress={() => control.close()}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src} {labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink> </InlineLinkText>
</Text> </Text>
</View> </View>
</View> </View>
@ -204,14 +203,14 @@ function AppealForm({
<Text style={[a.text_md, a.leading_snug]}> <Text style={[a.text_md, a.leading_snug]}>
<Trans> <Trans>
This appeal will be sent to{' '} This appeal will be sent to{' '}
<InlineLink <InlineLinkText
to={makeProfileLink( to={makeProfileLink(
labeler ? labeler.creator : {did: label.src, handle: ''}, labeler ? labeler.creator : {did: label.src, handle: ''},
)} )}
onPress={() => control.close()} onPress={() => control.close()}
style={[a.text_md, a.leading_snug]}> style={[a.text_md, a.leading_snug]}>
{labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src} {labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src}
</InlineLink> </InlineLinkText>
. .
</Trans> </Trans>
</Text> </Text>

View File

@ -1,19 +1,18 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {ModerationCause} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {ModerationCause} from '@atproto/api'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription' import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {listUriToHref} from '#/lib/strings/url-helpers'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useTheme, atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {InlineLink} from '#/components/Link'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog' export {useDialogControl as useModerationDetailsDialogControl} from '#/components/Dialog'
@ -55,9 +54,9 @@ function ModerationDetailsDialogInner({
description = ( description = (
<Trans> <Trans>
This user is included in the{' '} This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}> <InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name} {list.name}
</InlineLink>{' '} </InlineLinkText>{' '}
list which you have blocked. list which you have blocked.
</Trans> </Trans>
) )
@ -84,9 +83,9 @@ function ModerationDetailsDialogInner({
description = ( description = (
<Trans> <Trans>
This user is included in the{' '} This user is included in the{' '}
<InlineLink to={listUriToHref(list.uri)} style={[a.text_sm]}> <InlineLinkText to={listUriToHref(list.uri)} style={[a.text_sm]}>
{list.name} {list.name}
</InlineLink>{' '} </InlineLinkText>{' '}
list which you have muted. list which you have muted.
</Trans> </Trans>
) )
@ -127,12 +126,12 @@ function ModerationDetailsDialogInner({
{modcause.source.type === 'user' ? ( {modcause.source.type === 'user' ? (
<Trans>the author</Trans> <Trans>the author</Trans>
) : ( ) : (
<InlineLink <InlineLinkText
to={makeProfileLink({did: modcause.label.src, handle: ''})} to={makeProfileLink({did: modcause.label.src, handle: ''})}
onPress={() => control.close()} onPress={() => control.close()}
style={a.text_md}> style={a.text_md}>
{desc.source} {desc.source}
</InlineLink> </InlineLinkText>
)} )}
. .
</Trans> </Trans>

View File

@ -58,11 +58,11 @@ export const ChooseAccountForm = ({
return ( return (
<FormContainer <FormContainer
testID="chooseAccountForm" testID="chooseAccountForm"
title={<Trans>Select account</Trans>}> titleText={<Trans>Select account</Trans>}>
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Sign in as...</Trans> <Trans>Sign in as...</Trans>
</TextField.Label> </TextField.LabelText>
<AccountList <AccountList
onSelectAccount={onSelect} onSelectAccount={onSelect}
onSelectOther={() => onSelectAccount()} onSelectOther={() => onSelectAccount()}

View File

@ -83,11 +83,11 @@ export const ForgotPasswordForm = ({
return ( return (
<FormContainer <FormContainer
testID="forgotPasswordForm" testID="forgotPasswordForm"
title={<Trans>Reset password</Trans>}> titleText={<Trans>Reset password</Trans>}>
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Hosting provider</Trans> <Trans>Hosting provider</Trans>
</TextField.Label> </TextField.LabelText>
<HostingProvider <HostingProvider
serviceUrl={serviceUrl} serviceUrl={serviceUrl}
onSelectServiceUrl={setServiceUrl} onSelectServiceUrl={setServiceUrl}
@ -95,9 +95,9 @@ export const ForgotPasswordForm = ({
/> />
</View> </View>
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Email address</Trans> <Trans>Email address</Trans>
</TextField.Label> </TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={At} /> <TextField.Icon icon={At} />
<TextField.Input <TextField.Input

View File

@ -6,12 +6,12 @@ import {Text} from '#/components/Typography'
export function FormContainer({ export function FormContainer({
testID, testID,
title, titleText,
children, children,
style, style,
}: { }: {
testID?: string testID?: string
title?: React.ReactNode titleText?: React.ReactNode
children: React.ReactNode children: React.ReactNode
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
@ -21,9 +21,9 @@ export function FormContainer({
<View <View
testID={testID} testID={testID}
style={[a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.py_md], style]}> style={[a.gap_md, a.flex_1, !gtMobile && [a.px_lg, a.py_md], style]}>
{title && !gtMobile && ( {titleText && !gtMobile && (
<Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}> <Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
{title} {titleText}
</Text> </Text>
)} )}
{children} {children}

View File

@ -128,11 +128,11 @@ export const LoginForm = ({
const isReady = !!serviceDescription && !!identifier && !!password const isReady = !!serviceDescription && !!identifier && !!password
return ( return (
<FormContainer testID="loginForm" title={<Trans>Sign in</Trans>}> <FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Hosting provider</Trans> <Trans>Hosting provider</Trans>
</TextField.Label> </TextField.LabelText>
<HostingProvider <HostingProvider
serviceUrl={serviceUrl} serviceUrl={serviceUrl}
onSelectServiceUrl={setServiceUrl} onSelectServiceUrl={setServiceUrl}
@ -140,9 +140,9 @@ export const LoginForm = ({
/> />
</View> </View>
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Account</Trans> <Trans>Account</Trans>
</TextField.Label> </TextField.LabelText>
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={At} /> <TextField.Icon icon={At} />

View File

@ -99,7 +99,7 @@ export const SetNewPasswordForm = ({
return ( return (
<FormContainer <FormContainer
testID="setNewPasswordForm" testID="setNewPasswordForm"
title={<Trans>Set new password</Trans>}> titleText={<Trans>Set new password</Trans>}>
<Text style={[a.leading_snug, a.mb_sm]}> <Text style={[a.leading_snug, a.mb_sm]}>
<Trans> <Trans>
You will receive an email with a "reset code." Enter that code here, You will receive an email with a "reset code." Enter that code here,
@ -108,7 +108,7 @@ export const SetNewPasswordForm = ({
</Text> </Text>
<View> <View>
<TextField.Label>Reset code</TextField.Label> <TextField.LabelText>Reset code</TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Ticket} /> <TextField.Icon icon={Ticket} />
<TextField.Input <TextField.Input
@ -131,7 +131,7 @@ export const SetNewPasswordForm = ({
</View> </View>
<View> <View>
<TextField.Label>New password</TextField.Label> <TextField.LabelText>New password</TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Lock} /> <TextField.Icon icon={Lock} />
<TextField.Input <TextField.Input

View File

@ -40,7 +40,7 @@ import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filte
import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group' import {Group3_Stroke2_Corner0_Rounded as Group} from '#/components/icons/Group'
import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
import * as LabelingService from '#/components/LabelingServiceCard' import * as LabelingService from '#/components/LabelingServiceCard'
import {InlineLink, Link} from '#/components/Link' import {InlineLinkText, Link} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {GlobalLabelPreference} from '#/components/moderation/LabelPreference' import {GlobalLabelPreference} from '#/components/moderation/LabelPreference'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@ -518,11 +518,11 @@ function PwiOptOut() {
msg`Discourage apps from showing my account to logged-out users`, msg`Discourage apps from showing my account to logged-out users`,
)}> )}>
<Toggle.Switch /> <Toggle.Switch />
<Toggle.Label style={[a.text_md, a.flex_1]}> <Toggle.LabelText style={[a.text_md, a.flex_1]}>
<Trans> <Trans>
Discourage apps from showing my account to logged-out users Discourage apps from showing my account to logged-out users
</Trans> </Trans>
</Toggle.Label> </Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
{updateProfile.isPending && <Loader />} {updateProfile.isPending && <Loader />}
@ -545,9 +545,9 @@ function PwiOptOut() {
</Trans> </Trans>
</Text> </Text>
<InlineLink to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> <InlineLinkText to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
<Trans>Learn more about what is public on Bluesky.</Trans> <Trans>Learn more about what is public on Bluesky.</Trans>
</InlineLink> </InlineLinkText>
</View> </View>
</View> </View>
) )

View File

@ -1,29 +1,27 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {IS_DEV} from '#/env'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
import {
useTheme,
atoms as a,
useBreakpoints,
web,
native,
flatten,
TextStyleProp,
} from '#/alf'
import {P, leading, Text} from '#/components/Typography'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Button, ButtonIcon} from '#/components/Button'
import {ScrollView} from '#/view/com/util/Views' import {ScrollView} from '#/view/com/util/Views'
import {createPortalGroup} from '#/components/Portal'
import {Context} from '#/screens/Onboarding/state' import {Context} from '#/screens/Onboarding/state'
import {
atoms as a,
flatten,
native,
TextStyleProp,
useBreakpoints,
useTheme,
web,
} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {createPortalGroup} from '#/components/Portal'
import {leading, P, Text} from '#/components/Typography'
import {IS_DEV} from '#/env'
const COL_WIDTH = 500 const COL_WIDTH = 500
@ -204,7 +202,7 @@ export function Layout({children}: React.PropsWithChildren<{}>) {
) )
} }
export function Title({ export function TitleText({
children, children,
style, style,
}: React.PropsWithChildren<TextStyleProp>) { }: React.PropsWithChildren<TextStyleProp>) {
@ -224,7 +222,7 @@ export function Title({
) )
} }
export function Description({ export function DescriptionText({
children, children,
style, style,
}: React.PropsWithChildren<TextStyleProp>) { }: React.PropsWithChildren<TextStyleProp>) {

View File

@ -6,9 +6,9 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import { import {
Description, DescriptionText,
OnboardingControls, OnboardingControls,
Title, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state' import {Context} from '#/screens/Onboarding/state'
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
@ -105,15 +105,15 @@ export function StepAlgoFeeds() {
<View style={[a.align_start]}> <View style={[a.align_start]}>
<IconCircle icon={ListSparkle} style={[a.mb_2xl]} /> <IconCircle icon={ListSparkle} style={[a.mb_2xl]} />
<Title> <TitleText>
<Trans>Choose your main feeds</Trans> <Trans>Choose your main feeds</Trans>
</Title> </TitleText>
<Description> <DescriptionText>
<Trans> <Trans>
Custom feeds built by the community bring you new experiences and help Custom feeds built by the community bring you new experiences and help
you find the content you love. you find the content you love.
</Trans> </Trans>
</Description> </DescriptionText>
<View style={[a.w_full, a.pb_2xl]}> <View style={[a.w_full, a.pb_2xl]}>
<Toggle.Group <Toggle.Group

View File

@ -10,9 +10,9 @@ import {useSetSaveFeedsMutation} from '#/state/queries/preferences'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
import { import {
Description, DescriptionText,
OnboardingControls, OnboardingControls,
Title, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state' import {Context} from '#/screens/Onboarding/state'
import { import {
@ -87,12 +87,12 @@ export function StepFinished() {
<View style={[a.align_start]}> <View style={[a.align_start]}>
<IconCircle icon={Check} style={[a.mb_2xl]} /> <IconCircle icon={Check} style={[a.mb_2xl]} />
<Title> <TitleText>
<Trans>You're ready to go!</Trans> <Trans>You're ready to go!</Trans>
</Title> </TitleText>
<Description> <DescriptionText>
<Trans>We hope you have a wonderful time. Remember, Bluesky is:</Trans> <Trans>We hope you have a wonderful time. Remember, Bluesky is:</Trans>
</Description> </DescriptionText>
<View style={[a.pt_5xl, a.gap_3xl]}> <View style={[a.pt_5xl, a.gap_3xl]}>
<View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}> <View style={[a.flex_row, a.align_center, a.w_full, a.gap_lg]}>

View File

@ -10,9 +10,9 @@ import {
useSetFeedViewPreferencesMutation, useSetFeedViewPreferencesMutation,
} from 'state/queries/preferences' } from 'state/queries/preferences'
import { import {
Description, DescriptionText,
OnboardingControls, OnboardingControls,
Title, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state' import {Context} from '#/screens/Onboarding/state'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
@ -58,12 +58,12 @@ export function StepFollowingFeed() {
<View style={[a.align_start]}> <View style={[a.align_start]}>
<IconCircle icon={FilterTimeline} style={[a.mb_2xl]} /> <IconCircle icon={FilterTimeline} style={[a.mb_2xl]} />
<Title> <TitleText>
<Trans>Your default feed is "Following"</Trans> <Trans>Your default feed is "Following"</Trans>
</Title> </TitleText>
<Description style={[a.mb_md]}> <DescriptionText style={[a.mb_md]}>
<Trans>It shows posts from the people you follow as they happen.</Trans> <Trans>It shows posts from the people you follow as they happen.</Trans>
</Description> </DescriptionText>
<View style={[a.w_full]}> <View style={[a.w_full]}>
<Toggle.Item <Toggle.Item
@ -139,9 +139,9 @@ export function StepFollowingFeed() {
</Toggle.Item> </Toggle.Item>
</View> </View>
<Description style={[a.mt_lg]}> <DescriptionText style={[a.mt_lg]}>
<Trans>You can change these settings later.</Trans> <Trans>You can change these settings later.</Trans>
</Description> </DescriptionText>
<OnboardingControls.Portal> <OnboardingControls.Portal>
<Button <Button

View File

@ -11,9 +11,9 @@ import {logger} from '#/logger'
import {getAgent} from '#/state/session' import {getAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
import { import {
Description, DescriptionText,
OnboardingControls, OnboardingControls,
Title, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {ApiResponseMap, Context} from '#/screens/Onboarding/state' import {ApiResponseMap, Context} from '#/screens/Onboarding/state'
import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton'
@ -163,8 +163,8 @@ export function StepInterests() {
]} ]}
/> />
<Title>{title}</Title> <TitleText>{title}</TitleText>
<Description>{description}</Description> <DescriptionText>{description}</DescriptionText>
<View style={[a.w_full, a.pt_2xl]}> <View style={[a.w_full, a.pt_2xl]}>
{isLoading ? ( {isLoading ? (

View File

@ -113,15 +113,15 @@ export function AdultContentEnabledPref({
)} )}
<Prompt.Outer control={prompt}> <Prompt.Outer control={prompt}>
<Prompt.Title> <Prompt.TitleText>
<Trans>Adult Content</Trans> <Trans>Adult Content</Trans>
</Prompt.Title> </Prompt.TitleText>
<Prompt.Description> <Prompt.DescriptionText>
<Trans> <Trans>
Due to Apple policies, adult content can only be enabled on the web Due to Apple policies, adult content can only be enabled on the web
after completing sign up. after completing sign up.
</Trans> </Trans>
</Prompt.Description> </Prompt.DescriptionText>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Action onPress={() => prompt.close()} cta={_(msg`OK`)} /> <Prompt.Action onPress={() => prompt.close()} cta={_(msg`OK`)} />
</Prompt.Actions> </Prompt.Actions>

View File

@ -9,9 +9,9 @@ import {logEvent} from '#/lib/statsig/statsig'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {usePreferencesSetAdultContentMutation} from 'state/queries/preferences' import {usePreferencesSetAdultContentMutation} from 'state/queries/preferences'
import { import {
Description, DescriptionText,
OnboardingControls, OnboardingControls,
Title, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state' import {Context} from '#/screens/Onboarding/state'
import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/AdultContentEnabledPref' import {AdultContentEnabledPref} from '#/screens/Onboarding/StepModeration/AdultContentEnabledPref'
@ -56,14 +56,14 @@ export function StepModeration() {
<View style={[a.align_start]}> <View style={[a.align_start]}>
<IconCircle icon={EyeSlash} style={[a.mb_2xl]} /> <IconCircle icon={EyeSlash} style={[a.mb_2xl]} />
<Title> <TitleText>
<Trans>You're in control</Trans> <Trans>You're in control</Trans>
</Title> </TitleText>
<Description style={[a.mb_xl]}> <DescriptionText style={[a.mb_xl]}>
<Trans> <Trans>
Select what you want to see (or not see), and well handle the rest. Select what you want to see (or not see), and well handle the rest.
</Trans> </Trans>
</Description> </DescriptionText>
{!preferences ? ( {!preferences ? (
<View style={[a.pt_md]}> <View style={[a.pt_md]}>

View File

@ -10,9 +10,9 @@ import {capitalize} from '#/lib/strings/capitalize'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {useProfilesQuery} from '#/state/queries/profile' import {useProfilesQuery} from '#/state/queries/profile'
import { import {
Description, DescriptionText,
OnboardingControls, OnboardingControls,
Title, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state' import {Context} from '#/screens/Onboarding/state'
import { import {
@ -136,16 +136,16 @@ export function StepSuggestedAccounts() {
<View style={[a.align_start]}> <View style={[a.align_start]}>
<IconCircle icon={At} style={[a.mb_2xl]} /> <IconCircle icon={At} style={[a.mb_2xl]} />
<Title> <TitleText>
<Trans>Here are some accounts for you to follow</Trans> <Trans>Here are some accounts for you to follow</Trans>
</Title> </TitleText>
<Description> <DescriptionText>
{state.interestsStepResults.selectedInterests.length ? ( {state.interestsStepResults.selectedInterests.length ? (
<Trans>Based on your interest in {interestsText}</Trans> <Trans>Based on your interest in {interestsText}</Trans>
) : ( ) : (
<Trans>These are popular accounts you might like:</Trans> <Trans>These are popular accounts you might like:</Trans>
)} )}
</Description> </DescriptionText>
<View style={[a.w_full, a.pt_xl]}> <View style={[a.w_full, a.pt_xl]}>
{isLoading ? ( {isLoading ? (

View File

@ -9,9 +9,9 @@ import {capitalize} from '#/lib/strings/capitalize'
import {IS_TEST_USER} from 'lib/constants' import {IS_TEST_USER} from 'lib/constants'
import {useSession} from 'state/session' import {useSession} from 'state/session'
import { import {
Description, DescriptionText,
OnboardingControls, OnboardingControls,
Title, TitleText,
} from '#/screens/Onboarding/Layout' } from '#/screens/Onboarding/Layout'
import {Context} from '#/screens/Onboarding/state' import {Context} from '#/screens/Onboarding/state'
import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard' import {FeedCard} from '#/screens/Onboarding/StepAlgoFeeds/FeedCard'
@ -76,10 +76,10 @@ export function StepTopicalFeeds() {
<View style={[a.align_start]}> <View style={[a.align_start]}>
<IconCircle icon={ListMagnifyingGlass} style={[a.mb_2xl]} /> <IconCircle icon={ListMagnifyingGlass} style={[a.mb_2xl]} />
<Title> <TitleText>
<Trans>Feeds can be topical as well!</Trans> <Trans>Feeds can be topical as well!</Trans>
</Title> </TitleText>
<Description> <DescriptionText>
{state.interestsStepResults.selectedInterests.length ? ( {state.interestsStepResults.selectedInterests.length ? (
<Trans> <Trans>
Here are some topical feeds based on your interests: {interestsText} Here are some topical feeds based on your interests: {interestsText}
@ -91,7 +91,7 @@ export function StepTopicalFeeds() {
many as you like. many as you like.
</Trans> </Trans>
)} )}
</Description> </DescriptionText>
<View style={[a.w_full, a.pb_2xl, a.pt_2xl]}> <View style={[a.w_full, a.pb_2xl, a.pt_2xl]}>
<Toggle.Group <Toggle.Group

View File

@ -1,17 +1,16 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {Trans, msg} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Shadow} from '#/state/cache/types'
import {pluralize} from '#/lib/strings/helpers' import {pluralize} from '#/lib/strings/helpers'
import {Shadow} from '#/state/cache/types'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {formatCount} from 'view/com/util/numeric/format' import {formatCount} from 'view/com/util/numeric/format'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {InlineLink} from '#/components/Link'
export function ProfileHeaderMetrics({ export function ProfileHeaderMetrics({
profile, profile,
@ -28,7 +27,7 @@ export function ProfileHeaderMetrics({
<View <View
style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]} style={[a.flex_row, a.gap_sm, a.align_center, a.pb_md]}
pointerEvents="box-none"> pointerEvents="box-none">
<InlineLink <InlineLinkText
testID="profileHeaderFollowersButton" testID="profileHeaderFollowersButton"
style={[a.flex_row, t.atoms.text]} style={[a.flex_row, t.atoms.text]}
to={makeProfileLink(profile, 'followers')} to={makeProfileLink(profile, 'followers')}
@ -37,8 +36,8 @@ export function ProfileHeaderMetrics({
<Text style={[t.atoms.text_contrast_medium, a.text_md]}> <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
{pluralizedFollowers} {pluralizedFollowers}
</Text> </Text>
</InlineLink> </InlineLinkText>
<InlineLink <InlineLinkText
testID="profileHeaderFollowsButton" testID="profileHeaderFollowsButton"
style={[a.flex_row, t.atoms.text]} style={[a.flex_row, t.atoms.text]}
to={makeProfileLink(profile, 'follows')} to={makeProfileLink(profile, 'follows')}
@ -49,7 +48,7 @@ export function ProfileHeaderMetrics({
following following
</Text> </Text>
</Trans> </Trans>
</InlineLink> </InlineLinkText>
<Text style={[a.font_bold, t.atoms.text, a.text_md]}> <Text style={[a.font_bold, t.atoms.text, a.text_md]}>
{formatCount(profile.postsCount || 0)}{' '} {formatCount(profile.postsCount || 0)}{' '}
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> <Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>

View File

@ -316,13 +316,13 @@ function CantSubscribePrompt({
const {_} = useLingui() const {_} = useLingui()
return ( return (
<Prompt.Outer control={control}> <Prompt.Outer control={control}>
<Prompt.Title>Unable to subscribe</Prompt.Title> <Prompt.TitleText>Unable to subscribe</Prompt.TitleText>
<Prompt.Description> <Prompt.DescriptionText>
<Trans> <Trans>
We're sorry! You can only subscribe to ten labelers, and you've We're sorry! You can only subscribe to ten labelers, and you've
reached your limit of ten. reached your limit of ten.
</Trans> </Trans>
</Prompt.Description> </Prompt.DescriptionText>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Action onPress={control.close} cta={_(msg`OK`)} /> <Prompt.Action onPress={control.close} cta={_(msg`OK`)} />
</Prompt.Actions> </Prompt.Actions>

View File

@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {InlineLink} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export const Policies = ({ export const Policies = ({
@ -45,16 +45,16 @@ export const Policies = ({
const els = [] const els = []
if (tos) { if (tos) {
els.push( els.push(
<InlineLink key="tos" to={tos}> <InlineLinkText key="tos" to={tos}>
{_(msg`Terms of Service`)} {_(msg`Terms of Service`)}
</InlineLink>, </InlineLinkText>,
) )
} }
if (pp) { if (pp) {
els.push( els.push(
<InlineLink key="pp" to={pp}> <InlineLinkText key="pp" to={pp}>
{_(msg`Privacy Policy`)} {_(msg`Privacy Policy`)}
</InlineLink>, </InlineLinkText>,
) )
} }
if (els.length === 2) { if (els.length === 2) {

View File

@ -36,9 +36,9 @@ export function StepInfo() {
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<FormError error={state.error} /> <FormError error={state.error} />
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Hosting provider</Trans> <Trans>Hosting provider</Trans>
</TextField.Label> </TextField.LabelText>
<HostingProvider <HostingProvider
serviceUrl={state.serviceUrl} serviceUrl={state.serviceUrl}
onSelectServiceUrl={v => onSelectServiceUrl={v =>
@ -54,9 +54,9 @@ export function StepInfo() {
<> <>
{state.serviceDescription.inviteCodeRequired && ( {state.serviceDescription.inviteCodeRequired && (
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Invite code</Trans> <Trans>Invite code</Trans>
</TextField.Label> </TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Ticket} /> <TextField.Icon icon={Ticket} />
<TextField.Input <TextField.Input
@ -76,9 +76,9 @@ export function StepInfo() {
</View> </View>
)} )}
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Email</Trans> <Trans>Email</Trans>
</TextField.Label> </TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Envelope} /> <TextField.Icon icon={Envelope} />
<TextField.Input <TextField.Input
@ -97,9 +97,9 @@ export function StepInfo() {
</TextField.Root> </TextField.Root>
</View> </View>
<View> <View>
<TextField.Label> <TextField.LabelText>
<Trans>Password</Trans> <Trans>Password</Trans>
</TextField.Label> </TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Lock} /> <TextField.Icon icon={Lock} />
<TextField.Input <TextField.Input
@ -117,9 +117,9 @@ export function StepInfo() {
</TextField.Root> </TextField.Root>
</View> </View>
<View> <View>
<DateField.Label> <DateField.LabelText>
<Trans>Your birth date</Trans> <Trans>Your birth date</Trans>
</DateField.Label> </DateField.LabelText>
<DateField.DateField <DateField.DateField
testID="date" testID="date"
value={DateField.utils.toSimpleDateString(state.dateOfBirth)} value={DateField.utils.toSimpleDateString(state.dateOfBirth)}

View File

@ -24,7 +24,7 @@ import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {InlineLink} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function Signup({onPressBack}: {onPressBack: () => void}) { export function Signup({onPressBack}: {onPressBack: () => void}) {
@ -215,9 +215,9 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<View style={[a.w_full, a.py_lg]}> <View style={[a.w_full, a.py_lg]}>
<Text style={[t.atoms.text_contrast_medium]}> <Text style={[t.atoms.text_contrast_medium]}>
<Trans>Having trouble?</Trans>{' '} <Trans>Having trouble?</Trans>{' '}
<InlineLink to={FEEDBACK_FORM_URL({email: state.email})}> <InlineLinkText to={FEEDBACK_FORM_URL({email: state.email})}>
<Trans>Contact support</Trans> <Trans>Contact support</Trans>
</InlineLink> </InlineLinkText>
</Text> </Text>
</View> </View>
</View> </View>

View File

@ -14,7 +14,7 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {InlineLink} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
@ -162,15 +162,15 @@ function Footer() {
a.flex_1, a.flex_1,
t.atoms.border_contrast_medium, t.atoms.border_contrast_medium,
]}> ]}>
<InlineLink to="https://bsky.social"> <InlineLinkText to="https://bsky.social">
<Trans>Business</Trans> <Trans>Business</Trans>
</InlineLink> </InlineLinkText>
<InlineLink to="https://bsky.social/about/blog"> <InlineLinkText to="https://bsky.social/about/blog">
<Trans>Blog</Trans> <Trans>Blog</Trans>
</InlineLink> </InlineLinkText>
<InlineLink to="https://bsky.social/about/join"> <InlineLinkText to="https://bsky.social/about/join">
<Trans>Jobs</Trans> <Trans>Jobs</Trans>
</InlineLink> </InlineLinkText>
<View style={a.flex_1} /> <View style={a.flex_1} />

View File

@ -1,17 +1,17 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {BSKY_SERVICE} from 'lib/constants'
import * as persisted from '#/state/persisted'
import * as persisted from '#/state/persisted'
import {BSKY_SERVICE} from 'lib/constants'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Text, P} from '#/components/Typography'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import * as ToggleButton from '#/components/forms/ToggleButton'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {P, Text} from '#/components/Typography'
export function ServerInputDialog({ export function ServerInputDialog({
control, control,
@ -106,9 +106,9 @@ export function ServerInputDialog({
a.px_md, a.px_md,
a.py_md, a.py_md,
]}> ]}>
<TextField.Label nativeID="address-input-label"> <TextField.LabelText nativeID="address-input-label">
<Trans>Server address</Trans> <Trans>Server address</Trans>
</TextField.Label> </TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Globe} /> <TextField.Icon icon={Globe} />
<Dialog.Input <Dialog.Input

View File

@ -1,51 +1,51 @@
import React from 'react' import React from 'react'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
ComAtprotoLabelDefs,
interpretLabelValueDefinition,
LabelPreference,
LABELS, LABELS,
mock, mock,
moderatePost, moderatePost,
moderateProfile, moderateProfile,
ModerationOpts,
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
LabelPreference,
ModerationDecision,
ModerationBehavior, ModerationBehavior,
ModerationDecision,
ModerationOpts,
RichText, RichText,
ComAtprotoLabelDefs,
interpretLabelValueDefinition,
} from '@atproto/api' } from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
import {useSession} from '#/state/session' import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {FeedNotification} from '#/state/queries/notifications/types' import {FeedNotification} from '#/state/queries/notifications/types'
import { import {
groupNotifications, groupNotifications,
shouldFilterNotif, shouldFilterNotif,
} from '#/state/queries/notifications/util' } from '#/state/queries/notifications/util'
import {moderationOptsOverrideContext} from '#/state/queries/preferences'
import {atoms as a, useTheme} from '#/alf' import {useSession} from '#/state/session'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {CenteredView, ScrollView} from '#/view/com/util/Views' import {CenteredView, ScrollView} from '#/view/com/util/Views'
import {H1, H3, P, Text} from '#/components/Typography' import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import { import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom, ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottom,
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop, ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron' } from '#/components/icons/Chevron'
import {H1, H3, P, Text} from '#/components/Typography'
import {ScreenHider} from '../../components/moderation/ScreenHider' import {ScreenHider} from '../../components/moderation/ScreenHider'
import {ProfileHeaderStandard} from '#/screens/Profile/Header/ProfileHeaderStandard'
import {ProfileCard} from '../com/profile/ProfileCard'
import {FeedItem} from '../com/posts/FeedItem'
import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem' import {FeedItem as NotifFeedItem} from '../com/notifications/FeedItem'
import {PostThreadItem} from '../com/post-thread/PostThreadItem' import {PostThreadItem} from '../com/post-thread/PostThreadItem'
import {Divider} from '#/components/Divider' import {FeedItem} from '../com/posts/FeedItem'
import {ProfileCard} from '../com/profile/ProfileCard'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys( const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
LABELS, LABELS,
@ -320,7 +320,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
disabled={disabled} disabled={disabled}
style={disabled ? {opacity: 0.5} : undefined}> style={disabled ? {opacity: 0.5} : undefined}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>{labelValue}</Toggle.Label> <Toggle.LabelText>{labelValue}</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
) )
})} })}
@ -330,7 +330,7 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
disabled={isSelfLabel} disabled={isSelfLabel}
style={isSelfLabel ? {opacity: 0.5} : undefined}> style={isSelfLabel ? {opacity: 0.5} : undefined}>
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Custom label</Toggle.Label> <Toggle.LabelText>Custom label</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>
@ -358,23 +358,23 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
<View style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}> <View style={[a.gap_md, a.flex_row, a.flex_wrap, a.pt_md]}>
<Toggle.Item name="targetMe" label="Target is me"> <Toggle.Item name="targetMe" label="Target is me">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Target is me</Toggle.Label> <Toggle.LabelText>Target is me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="following" label="Following target"> <Toggle.Item name="following" label="Following target">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Following target</Toggle.Label> <Toggle.LabelText>Following target</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="selfLabel" label="Self label"> <Toggle.Item name="selfLabel" label="Self label">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Self label</Toggle.Label> <Toggle.LabelText>Self label</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="noAdult" label="Adult disabled"> <Toggle.Item name="noAdult" label="Adult disabled">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Adult disabled</Toggle.Label> <Toggle.LabelText>Adult disabled</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="loggedOut" label="Logged out"> <Toggle.Item name="loggedOut" label="Logged out">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Logged out</Toggle.Label> <Toggle.LabelText>Logged out</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>
@ -400,15 +400,15 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
]}> ]}>
<Toggle.Item name="hide" label="Hide"> <Toggle.Item name="hide" label="Hide">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Hide</Toggle.Label> <Toggle.LabelText>Hide</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="warn" label="Warn"> <Toggle.Item name="warn" label="Warn">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Warn</Toggle.Label> <Toggle.LabelText>Warn</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="ignore" label="Ignore"> <Toggle.Item name="ignore" label="Ignore">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Ignore</Toggle.Label> <Toggle.LabelText>Ignore</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>
@ -446,19 +446,19 @@ export const DebugModScreen = ({}: NativeStackScreenProps<
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}> <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
<Toggle.Item name="account" label="Account"> <Toggle.Item name="account" label="Account">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Account</Toggle.Label> <Toggle.LabelText>Account</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="profile" label="Profile"> <Toggle.Item name="profile" label="Profile">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Profile</Toggle.Label> <Toggle.LabelText>Profile</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="post" label="Post"> <Toggle.Item name="post" label="Post">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Post</Toggle.Label> <Toggle.LabelText>Post</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="embed" label="Embed"> <Toggle.Item name="embed" label="Embed">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Embed</Toggle.Label> <Toggle.LabelText>Embed</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>
@ -623,15 +623,15 @@ function CustomLabelForm({
<View style={[a.flex_row, a.gap_md, a.flex_wrap]}> <View style={[a.flex_row, a.gap_md, a.flex_wrap]}>
<Toggle.Item name="content" label="Content"> <Toggle.Item name="content" label="Content">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Content</Toggle.Label> <Toggle.LabelText>Content</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="media" label="Media"> <Toggle.Item name="media" label="Media">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Media</Toggle.Label> <Toggle.LabelText>Media</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="none" label="None"> <Toggle.Item name="none" label="None">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>None</Toggle.Label> <Toggle.LabelText>None</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>
@ -658,15 +658,15 @@ function CustomLabelForm({
<View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}> <View style={[a.flex_row, a.gap_md, a.flex_wrap, a.align_center]}>
<Toggle.Item name="alert" label="Alert"> <Toggle.Item name="alert" label="Alert">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Alert</Toggle.Label> <Toggle.LabelText>Alert</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="inform" label="Inform"> <Toggle.Item name="inform" label="Inform">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Inform</Toggle.Label> <Toggle.LabelText>Inform</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="none" label="None"> <Toggle.Item name="none" label="None">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>None</Toggle.Label> <Toggle.LabelText>None</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>

View File

@ -1,70 +1,71 @@
import React, {useMemo, useCallback} from 'react' import React, {useCallback, useMemo} from 'react'
import {StyleSheet, View, Pressable} from 'react-native' import {Pressable, StyleSheet, View} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {msg, Trans} from '@lingui/macro'
import {useIsFocused, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {usePalette} from 'lib/hooks/usePalette'
import {CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {s} from 'lib/styles'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed'
import {InlineLink} from '#/components/Link'
import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text'
import {RichText} from '#/components/RichText'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB'
import {EmptyState} from 'view/com/util/EmptyState'
import {LoadingScreen} from 'view/com/util/LoadingScreen'
import * as Toast from 'view/com/util/Toast'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {shareUrl} from 'lib/sharing'
import {toShareUrl} from 'lib/strings/url-helpers'
import {Haptics} from 'lib/haptics'
import {useAnalytics} from 'lib/analytics/analytics'
import {makeCustomFeedLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers'
import {CenteredView} from 'view/com/util/Views'
import {NavigationProp} from 'lib/routes/types'
import {ComposeIcon2} from 'lib/icons'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {useIsFocused, useNavigation} from '@react-navigation/native'
import {useFeedSourceInfoQuery, FeedSourceFeedInfo} from '#/state/queries/feed' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useQueryClient} from '@tanstack/react-query'
import {
UsePreferencesQueryResponse, import {HITSLOP_20} from '#/lib/constants'
usePreferencesQuery, import {logger} from '#/logger'
useSaveFeedMutation,
useRemoveFeedMutation,
usePinFeedMutation,
useUnpinFeedMutation,
} from '#/state/queries/preferences'
import {useSession} from '#/state/session'
import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {useComposerControls} from '#/state/shell/composer'
import {truncateAndInvalidate} from '#/state/queries/util'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {atoms as a, useTheme} from '#/alf' import {FeedSourceFeedInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import * as Menu from '#/components/Menu' import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
import {HITSLOP_20} from '#/lib/constants' import {FeedDescriptor} from '#/state/queries/post-feed'
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import { import {
Heart2_Stroke2_Corner0_Rounded as HeartOutline, usePinFeedMutation,
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, usePreferencesQuery,
} from '#/components/icons/Heart2' UsePreferencesQueryResponse,
useRemoveFeedMutation,
useSaveFeedMutation,
useUnpinFeedMutation,
} from '#/state/queries/preferences'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {useAnalytics} from 'lib/analytics/analytics'
import {Haptics} from 'lib/haptics'
import {usePalette} from 'lib/hooks/usePalette'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {ComposeIcon2} from 'lib/icons'
import {makeCustomFeedLink} from 'lib/routes/links'
import {CommonNavigatorParams} from 'lib/routes/types'
import {NavigationProp} from 'lib/routes/types'
import {shareUrl} from 'lib/sharing'
import {pluralize} from 'lib/strings/helpers'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {toShareUrl} from 'lib/strings/url-helpers'
import {s} from 'lib/styles'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {Feed} from 'view/com/posts/Feed'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {EmptyState} from 'view/com/util/EmptyState'
import {FAB} from 'view/com/util/fab/FAB'
import {Button} from 'view/com/util/forms/Button'
import {ListRef} from 'view/com/util/List'
import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {LoadingScreen} from 'view/com/util/LoadingScreen'
import {Text} from 'view/com/util/text/Text'
import * as Toast from 'view/com/util/Toast'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useTheme} from '#/alf'
import {Button as NewButton, ButtonText} from '#/components/Button' import {Button as NewButton, ButtonText} from '#/components/Button'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
Heart2_Stroke2_Corner0_Rounded as HeartOutline,
} from '#/components/icons/Heart2'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {InlineLinkText} from '#/components/Link'
import * as Menu from '#/components/Menu'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText'
const SECTION_TITLES = ['Posts'] const SECTION_TITLES = ['Posts']
@ -580,12 +581,12 @@ function AboutSection({
)} )}
</NewButton> </NewButton>
{typeof likeCount === 'number' && ( {typeof likeCount === 'number' && (
<InlineLink <InlineLinkText
label={_(msg`View users who like this feed`)} label={_(msg`View users who like this feed`)}
to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
style={[t.atoms.text_contrast_medium, a.font_bold]}> style={[t.atoms.text_contrast_medium, a.font_bold]}>
{_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)} {_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
</InlineLink> </InlineLinkText>
)} )}
</View> </View>
</View> </View>

View File

@ -1,14 +1,14 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Text, P} from '#/components/Typography'
import {Button, ButtonText} from '#/components/Button'
import {InlineLink, Link} from '#/components/Link'
import {getAgent, useSession} from '#/state/session' import {getAgent, useSession} from '#/state/session'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InlineLinkText, Link} from '#/components/Link'
import {P, Text} from '#/components/Typography'
export function ExportCarDialog({ export function ExportCarDialog({
control, control,
@ -75,11 +75,11 @@ export function ExportCarDialog({
<Trans> <Trans>
This feature is in beta. You can read more about repository This feature is in beta. You can read more about repository
exports in{' '} exports in{' '}
<InlineLink <InlineLinkText
to="https://docs.bsky.app/blog/repo-export" to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}> style={[a.text_sm]}>
this blogpost this blogpost
</InlineLink> </InlineLinkText>
. .
</Trans> </Trans>
</P> </P>

View File

@ -1,12 +1,12 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useDialogStateControlContext} from '#/state/dialogs'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {H3, P} from '#/components/Typography'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
import {useDialogStateControlContext} from '#/state/dialogs' import {H3, P} from '#/components/Typography'
export function Dialogs() { export function Dialogs() {
const scrollable = Dialog.useDialogControl() const scrollable = Dialog.useDialogControl()
@ -61,11 +61,11 @@ export function Dialogs() {
</Button> </Button>
<Prompt.Outer control={prompt}> <Prompt.Outer control={prompt}>
<Prompt.Title>This is a prompt</Prompt.Title> <Prompt.TitleText>This is a prompt</Prompt.TitleText>
<Prompt.Description> <Prompt.DescriptionText>
This is a generic prompt component. It accepts a title and a This is a generic prompt component. It accepts a title and a
description, as well as two actions. description, as well as two actions.
</Prompt.Description> </Prompt.DescriptionText>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Cancel>Cancel</Prompt.Cancel> <Prompt.Cancel>Cancel</Prompt.Cancel>
<Prompt.Action onPress={() => {}}>Confirm</Prompt.Action> <Prompt.Action onPress={() => {}}>Confirm</Prompt.Action>

View File

@ -2,13 +2,13 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {H1, H3} from '#/components/Typography' import {Button} from '#/components/Button'
import {DateField, LabelText} from '#/components/forms/DateField'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {DateField, Label} from '#/components/forms/DateField'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import * as ToggleButton from '#/components/forms/ToggleButton' import * as ToggleButton from '#/components/forms/ToggleButton'
import {Button} from '#/components/Button'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {H1, H3} from '#/components/Typography'
export function Forms() { export function Forms() {
const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a']) const [toggleGroupAValues, setToggleGroupAValues] = React.useState(['a'])
@ -42,7 +42,7 @@ export function Forms() {
</TextField.Root> </TextField.Root>
<View style={[a.w_full]}> <View style={[a.w_full]}>
<TextField.Label>Text field</TextField.Label> <TextField.LabelText>Text field</TextField.LabelText>
<TextField.Root> <TextField.Root>
<TextField.Icon icon={Globe} /> <TextField.Icon icon={Globe} />
<TextField.Input <TextField.Input
@ -50,12 +50,14 @@ export function Forms() {
onChangeText={setValue} onChangeText={setValue}
label="Text field" label="Text field"
/> />
<TextField.Suffix label="@gmail.com">@gmail.com</TextField.Suffix> <TextField.SuffixText label="@gmail.com">
@gmail.com
</TextField.SuffixText>
</TextField.Root> </TextField.Root>
</View> </View>
<View style={[a.w_full]}> <View style={[a.w_full]}>
<TextField.Label>Textarea</TextField.Label> <TextField.LabelText>Textarea</TextField.LabelText>
<TextField.Input <TextField.Input
multiline multiline
numberOfLines={4} numberOfLines={4}
@ -68,7 +70,7 @@ export function Forms() {
<H3>DateField</H3> <H3>DateField</H3>
<View style={[a.w_full]}> <View style={[a.w_full]}>
<Label>Date</Label> <LabelText>Date</LabelText>
<DateField <DateField
testID="date" testID="date"
value={date} value={date}
@ -86,7 +88,7 @@ export function Forms() {
<Toggle.Item name="a" label="Click me"> <Toggle.Item name="a" label="Click me">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Uncontrolled toggle</Toggle.Label> <Toggle.LabelText>Uncontrolled toggle</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Group <Toggle.Group
@ -98,23 +100,23 @@ export function Forms() {
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<Toggle.Item name="a" label="Click me"> <Toggle.Item name="a" label="Click me">
<Toggle.Switch /> <Toggle.Switch />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="b" label="Click me"> <Toggle.Item name="b" label="Click me">
<Toggle.Switch /> <Toggle.Switch />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="c" label="Click me"> <Toggle.Item name="c" label="Click me">
<Toggle.Switch /> <Toggle.Switch />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="d" disabled label="Click me"> <Toggle.Item name="d" disabled label="Click me">
<Toggle.Switch /> <Toggle.Switch />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="e" isInvalid label="Click me"> <Toggle.Item name="e" isInvalid label="Click me">
<Toggle.Switch /> <Toggle.Switch />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>
@ -128,23 +130,23 @@ export function Forms() {
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<Toggle.Item name="a" label="Click me"> <Toggle.Item name="a" label="Click me">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="b" label="Click me"> <Toggle.Item name="b" label="Click me">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="c" label="Click me"> <Toggle.Item name="c" label="Click me">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="d" disabled label="Click me"> <Toggle.Item name="d" disabled label="Click me">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="e" isInvalid label="Click me"> <Toggle.Item name="e" isInvalid label="Click me">
<Toggle.Checkbox /> <Toggle.Checkbox />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>
@ -157,23 +159,23 @@ export function Forms() {
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
<Toggle.Item name="a" label="Click me"> <Toggle.Item name="a" label="Click me">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="b" label="Click me"> <Toggle.Item name="b" label="Click me">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="c" label="Click me"> <Toggle.Item name="c" label="Click me">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="d" disabled label="Click me"> <Toggle.Item name="d" disabled label="Click me">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
<Toggle.Item name="e" isInvalid label="Click me"> <Toggle.Item name="e" isInvalid label="Click me">
<Toggle.Radio /> <Toggle.Radio />
<Toggle.Label>Click me</Toggle.Label> <Toggle.LabelText>Click me</Toggle.LabelText>
</Toggle.Item> </Toggle.Item>
</View> </View>
</Toggle.Group> </Toggle.Group>

View File

@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useTheme, atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {ButtonText} from '#/components/Button' import {ButtonText} from '#/components/Button'
import {InlineLink, Link} from '#/components/Link' import {InlineLinkText, Link} from '#/components/Link'
import {H1, Text} from '#/components/Typography' import {H1, Text} from '#/components/Typography'
export function Links() { export function Links() {
@ -13,20 +13,22 @@ export function Links() {
<H1>Links</H1> <H1>Links</H1>
<View style={[a.gap_md, a.align_start]}> <View style={[a.gap_md, a.align_start]}>
<InlineLink to="https://google.com" style={[a.text_lg]}> <InlineLinkText to="https://google.com" style={[a.text_lg]}>
https://google.com https://google.com
</InlineLink> </InlineLinkText>
<InlineLink to="https://google.com" style={[a.text_lg]}> <InlineLinkText to="https://google.com" style={[a.text_lg]}>
External with custom children (google.com) External with custom children (google.com)
</InlineLink> </InlineLinkText>
<InlineLink <InlineLinkText
to="https://bsky.social" to="https://bsky.social"
style={[a.text_md, t.atoms.text_contrast_low]}> style={[a.text_md, t.atoms.text_contrast_low]}>
Internal (bsky.social) Internal (bsky.social)
</InlineLink> </InlineLinkText>
<InlineLink to="https://bsky.app/profile/bsky.app" style={[a.text_md]}> <InlineLinkText
to="https://bsky.app/profile/bsky.app"
style={[a.text_md]}>
Internal (bsky.app) Internal (bsky.app)
</InlineLink> </InlineLinkText>
<Link <Link
variant="solid" variant="solid"