[Neue] Handle emoji within custom font (#5449)
* Support emoji in text with custom font * Add emoji support to elements that need it * Remove unused file causing lint failure * Fix a few more emoji locations * Couple more * No throw
This commit is contained in:
@@ -116,6 +116,7 @@
|
|||||||
"deprecated-react-native-prop-types": "^5.0.0",
|
"deprecated-react-native-prop-types": "^5.0.0",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
|
"emoji-regex": "^10.4.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "^51.0.8",
|
"expo": "^51.0.8",
|
||||||
"expo-application": "^5.9.1",
|
"expo-application": "^5.9.1",
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ import {msg, plural, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {precacheFeedFromGeneratorView} from '#/state/queries/feed'
|
||||||
import {
|
import {
|
||||||
useAddSavedFeedsMutation,
|
useAddSavedFeedsMutation,
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
useRemoveFeedMutation,
|
useRemoveFeedMutation,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {useSession} from '#/state/session'
|
||||||
import {precacheFeedFromGeneratorView} from 'state/queries/feed'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useSession} from 'state/session'
|
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
@@ -121,7 +121,10 @@ export function TitleAndByline({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}>
|
<Text
|
||||||
|
emoji
|
||||||
|
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||||
|
numberOfLines={1}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
{creator && (
|
{creator && (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Link, LinkProps} from '#/components/Link'
|
import {Link, LinkProps} from '#/components/Link'
|
||||||
@@ -185,11 +185,11 @@ function KnownFollowersInner({
|
|||||||
serverCount > 2 ? (
|
serverCount > 2 ? (
|
||||||
<Trans>
|
<Trans>
|
||||||
Followed by{' '}
|
Followed by{' '}
|
||||||
<Text key={slice[0].profile.did} style={textStyle}>
|
<Text emoji key={slice[0].profile.did} style={textStyle}>
|
||||||
{slice[0].profile.displayName}
|
{slice[0].profile.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
,{' '}
|
,{' '}
|
||||||
<Text key={slice[1].profile.did} style={textStyle}>
|
<Text emoji key={slice[1].profile.did} style={textStyle}>
|
||||||
{slice[1].profile.displayName}
|
{slice[1].profile.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
, and{' '}
|
, and{' '}
|
||||||
@@ -203,11 +203,11 @@ function KnownFollowersInner({
|
|||||||
// only 2
|
// only 2
|
||||||
<Trans>
|
<Trans>
|
||||||
Followed by{' '}
|
Followed by{' '}
|
||||||
<Text key={slice[0].profile.did} style={textStyle}>
|
<Text emoji key={slice[0].profile.did} style={textStyle}>
|
||||||
{slice[0].profile.displayName}
|
{slice[0].profile.displayName}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
and{' '}
|
and{' '}
|
||||||
<Text key={slice[1].profile.did} style={textStyle}>
|
<Text emoji key={slice[1].profile.did} style={textStyle}>
|
||||||
{slice[1].profile.displayName}
|
{slice[1].profile.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
</Trans>
|
</Trans>
|
||||||
@@ -216,7 +216,7 @@ function KnownFollowersInner({
|
|||||||
// 1-n followers, including blocks
|
// 1-n followers, including blocks
|
||||||
<Trans>
|
<Trans>
|
||||||
Followed by{' '}
|
Followed by{' '}
|
||||||
<Text key={slice[0].profile.did} style={textStyle}>
|
<Text emoji key={slice[0].profile.did} style={textStyle}>
|
||||||
{slice[0].profile.displayName}
|
{slice[0].profile.displayName}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
and{' '}
|
and{' '}
|
||||||
@@ -230,7 +230,7 @@ function KnownFollowersInner({
|
|||||||
// only 1
|
// only 1
|
||||||
<Trans>
|
<Trans>
|
||||||
Followed by{' '}
|
Followed by{' '}
|
||||||
<Text key={slice[0].profile.did} style={textStyle}>
|
<Text emoji key={slice[0].profile.did} style={textStyle}>
|
||||||
{slice[0].profile.displayName}
|
{slice[0].profile.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|||||||
@@ -44,17 +44,22 @@ export function Avatar({avatar}: {avatar?: string}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Title({value}: {value: string}) {
|
export function Title({value}: {value: string}) {
|
||||||
return <Text style={[a.text_md, a.font_bold, a.leading_tight]}>{value}</Text>
|
return (
|
||||||
|
<Text emoji style={[a.text_md, a.font_bold, a.leading_tight]}>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Description({value, handle}: {value?: string; handle: string}) {
|
export function Description({value, handle}: {value?: string; handle: string}) {
|
||||||
|
const {_} = useLingui()
|
||||||
return value ? (
|
return value ? (
|
||||||
<Text numberOfLines={2}>
|
<Text numberOfLines={2}>
|
||||||
<RichText value={value} style={[a.leading_snug]} />
|
<RichText value={value} style={[a.leading_snug]} />
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text style={[a.leading_snug]}>
|
<Text emoji style={[a.leading_snug]}>
|
||||||
<Trans>By {sanitizeHandle(handle, '@')}</Trans>
|
{_(msg`By ${sanitizeHandle(handle, '@')}`)}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -7,13 +7,14 @@ import {
|
|||||||
moderateUserList,
|
moderateUserList,
|
||||||
ModerationUI,
|
ModerationUI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {useModerationOpts} from 'state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {precacheList} from 'state/queries/feed'
|
import {precacheList} from '#/state/queries/feed'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -111,6 +112,7 @@ export function TitleAndByline({
|
|||||||
modUi?: ModerationUI
|
modUi?: ModerationUI
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,6 +132,7 @@ export function TitleAndByline({
|
|||||||
</Hider.Mask>
|
</Hider.Mask>
|
||||||
<Hider.Content>
|
<Hider.Content>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[a.text_md, a.font_bold, a.leading_snug]}
|
style={[a.text_md, a.font_bold, a.leading_snug]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{title}
|
{title}
|
||||||
@@ -139,15 +142,12 @@ export function TitleAndByline({
|
|||||||
|
|
||||||
{creator && (
|
{creator && (
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{purpose === MODLIST ? (
|
{purpose === MODLIST
|
||||||
<Trans>
|
? _(msg`Moderation list by ${sanitizeHandle(creator.handle, '@')}`)
|
||||||
Moderation list by {sanitizeHandle(creator.handle, '@')}
|
: _(msg`List by ${sanitizeHandle(creator.handle, '@')}`)}
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>List by {sanitizeHandle(creator.handle, '@')}</Trans>
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export function Label({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[
|
style={[
|
||||||
text,
|
text,
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {LogEvents} from '#/lib/statsig/statsig'
|
import {LogEvents} from '#/lib/statsig/statsig'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {useSession} from '#/state/session'
|
||||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
import {ProfileCardPills} from '#/view/com/profile/ProfileCard'
|
||||||
import {useSession} from 'state/session'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {ProfileCardPills} from 'view/com/profile/ProfileCard'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonProps, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonProps, 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'
|
||||||
@@ -175,11 +175,13 @@ export function NameAndHandle({
|
|||||||
return (
|
return (
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
|
style={[a.text_md, a.font_bold, a.leading_snug, a.self_start]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
style={[a.leading_snug, t.atoms.text_contrast_medium]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{handle}
|
{handle}
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ function LabelerToggle({title}: {title: string}) {
|
|||||||
a.z_10,
|
a.z_10,
|
||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[
|
style={[
|
||||||
native({marginTop: 2}),
|
native({marginTop: 2}),
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function RichText({
|
|||||||
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={[plainStyles, {fontSize}]}
|
style={[plainStyles, {fontSize}]}
|
||||||
@@ -77,6 +78,7 @@ export function RichText({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={plainStyles}
|
style={plainStyles}
|
||||||
@@ -148,7 +150,11 @@ export function RichText({
|
|||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
els.push(segment.text)
|
els.push(
|
||||||
|
<Text key={key} emoji style={plainStyles}>
|
||||||
|
{segment.text}
|
||||||
|
</Text>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
key++
|
key++
|
||||||
}
|
}
|
||||||
@@ -213,6 +219,7 @@ function RichTextTag({
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
|
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
{...native({
|
{...native({
|
||||||
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import React from 'react'
|
|||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {getStarterPackOgCard} from 'lib/strings/starter-pack'
|
import {getStarterPackOgCard} from '#/lib/strings/starter-pack'
|
||||||
import {precacheResolvedUri} from 'state/queries/resolve-uri'
|
import {precacheResolvedUri} from '#/state/queries/resolve-uri'
|
||||||
import {precacheStarterPack} from 'state/queries/starter-packs'
|
import {precacheStarterPack} from '#/state/queries/starter-packs'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import {BaseLink} from '#/components/Link'
|
import {BaseLink} from '#/components/Link'
|
||||||
@@ -66,21 +66,18 @@ export function Card({
|
|||||||
<View style={[a.flex_row, a.gap_sm]}>
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
{!noIcon ? <StarterPack width={40} gradient="sky" /> : null}
|
{!noIcon ? <StarterPack width={40} gradient="sky" /> : null}
|
||||||
<View>
|
<View>
|
||||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
<Text emoji style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||||
{record.name}
|
{record.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[a.leading_snug, t.atoms.text_contrast_medium]}>
|
<Text emoji style={[a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||||
<Trans>
|
{creator?.did === currentAccount?.did
|
||||||
Starter pack by{' '}
|
? _(msg`Starter pack by you`)
|
||||||
{creator?.did === currentAccount?.did
|
: _(msg`Starter pack by ${sanitizeHandle(creator.handle, '@')}`)}
|
||||||
? _(msg`you`)
|
|
||||||
: `@${sanitizeHandle(creator.handle)}`}
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{!noDescription && record.description ? (
|
{!noDescription && record.description ? (
|
||||||
<Text numberOfLines={3} style={[a.leading_snug]}>
|
<Text emoji numberOfLines={3} style={[a.leading_snug]}>
|
||||||
{record.description}
|
{record.description}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from 'lib/constants'
|
import {DISCOVER_FEED_URI, STARTER_PACK_MAX_SIZE} from '#/lib/constants'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
import {WizardAction, WizardState} from '#/screens/StarterPack/Wizard/State'
|
||||||
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'
|
||||||
@@ -78,6 +78,7 @@ function WizardListCard({
|
|||||||
/>
|
/>
|
||||||
<View style={[a.flex_1, a.gap_2xs]}>
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
|
|||||||
@@ -1,15 +1,85 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
||||||
import {UITextView} from 'react-native-uitextview'
|
import {UITextView} from 'react-native-uitextview'
|
||||||
|
import createEmojiRegex from 'emoji-regex'
|
||||||
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {logger} from '#/logger'
|
||||||
|
import {isIOS, isNative} from '#/platform/detection'
|
||||||
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
export type TextProps = RNTextProps & {
|
export type StringChild = string | (string | null)[]
|
||||||
|
|
||||||
|
export type TextProps = Omit<RNTextProps, 'children'> & {
|
||||||
/**
|
/**
|
||||||
* Lets the user select text, to use the native copy and paste functionality.
|
* Lets the user select text, to use the native copy and paste functionality.
|
||||||
*/
|
*/
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
|
/**
|
||||||
|
* Provides `data-*` attributes to the underlying `UITextView` component on
|
||||||
|
* web only.
|
||||||
|
*/
|
||||||
|
dataSet?: Record<string, string | number | undefined>
|
||||||
|
/**
|
||||||
|
* Appears as a small tooltip on web hover.
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
emoji: true
|
||||||
|
children: StringChild
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
emoji?: false
|
||||||
|
children: RNTextProps['children']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const EMOJI = createEmojiRegex()
|
||||||
|
|
||||||
|
export function childHasEmoji(children: React.ReactNode) {
|
||||||
|
return (Array.isArray(children) ? children : [children]).some(
|
||||||
|
child => typeof child === 'string' && createEmojiRegex().test(child),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function childIsString(
|
||||||
|
children: React.ReactNode,
|
||||||
|
): children is StringChild {
|
||||||
|
return (
|
||||||
|
typeof children === 'string' ||
|
||||||
|
(Array.isArray(children) &&
|
||||||
|
children.every(child => typeof child === 'string' || child === null))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderChildrenWithEmoji(children: StringChild) {
|
||||||
|
const normalized = Array.isArray(children) ? children : [children]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UITextView>
|
||||||
|
{normalized.map(child => {
|
||||||
|
if (typeof child !== 'string') return child
|
||||||
|
|
||||||
|
const emojis = child.match(EMOJI)
|
||||||
|
|
||||||
|
if (emojis === null) {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
return child.split(EMOJI).map((stringPart, index) => (
|
||||||
|
<UITextView key={index}>
|
||||||
|
{stringPart}
|
||||||
|
{emojis[index] ? (
|
||||||
|
<UITextView style={{color: 'black', fontFamily: 'System'}}>
|
||||||
|
{emojis[index]}
|
||||||
|
</UITextView>
|
||||||
|
) : null}
|
||||||
|
</UITextView>
|
||||||
|
))
|
||||||
|
})}
|
||||||
|
</UITextView>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +134,15 @@ export function normalizeTextStyles(
|
|||||||
/**
|
/**
|
||||||
* Our main text component. Use this most of the time.
|
* Our main text component. Use this most of the time.
|
||||||
*/
|
*/
|
||||||
export function Text({style, selectable, ...rest}: TextProps) {
|
export function Text({
|
||||||
|
children,
|
||||||
|
emoji,
|
||||||
|
style,
|
||||||
|
selectable,
|
||||||
|
title,
|
||||||
|
dataSet,
|
||||||
|
...rest
|
||||||
|
}: TextProps) {
|
||||||
const {fonts, flags} = useAlf()
|
const {fonts, flags} = useAlf()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
|
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
|
||||||
@@ -73,7 +151,29 @@ export function Text({style, selectable, ...rest}: TextProps) {
|
|||||||
flags,
|
flags,
|
||||||
})
|
})
|
||||||
|
|
||||||
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
|
if (IS_DEV) {
|
||||||
|
if (!emoji && childHasEmoji(children)) {
|
||||||
|
logger.warn(
|
||||||
|
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji && !childIsString(children)) {
|
||||||
|
logger.error('Text: when <Text emoji />, children can only be strings.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UITextView
|
||||||
|
selectable={selectable}
|
||||||
|
uiTextView
|
||||||
|
style={s}
|
||||||
|
{...rest}
|
||||||
|
// @ts-ignore
|
||||||
|
dataSet={Object.assign({tooltip: title}, dataSet || {})}>
|
||||||
|
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||||
|
</UITextView>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHeadingElement({level}: {level: number}) {
|
export function createHeadingElement({level}: {level: number}) {
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {BACK_HITSLOP} from 'lib/constants'
|
import {BACK_HITSLOP} from '#/lib/constants'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {isConvoActive, useConvo} from 'state/messages/convo'
|
import {isConvoActive, useConvo} from '#/state/messages/convo'
|
||||||
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
|
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||||
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
|
||||||
@@ -170,6 +170,7 @@ function HeaderReady({
|
|||||||
</View>
|
</View>
|
||||||
<View style={a.flex_1}>
|
<View style={a.flex_1}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_md,
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
|
|||||||
@@ -132,8 +132,10 @@ function Label({
|
|||||||
]}>
|
]}>
|
||||||
<View style={[a.p_md, a.gap_sm, a.flex_row]}>
|
<View style={[a.p_md, a.gap_sm, a.flex_row]}>
|
||||||
<View style={[a.flex_1, a.gap_xs]}>
|
<View style={[a.flex_1, a.gap_xs]}>
|
||||||
<Text style={[a.font_bold, a.text_md]}>{strings.name}</Text>
|
<Text emoji style={[a.font_bold, a.text_md]}>
|
||||||
<Text style={[t.atoms.text_contrast_medium, a.leading_snug]}>
|
{strings.name}
|
||||||
|
</Text>
|
||||||
|
<Text emoji style={[t.atoms.text_contrast_medium, a.leading_snug]}>
|
||||||
{strings.description}
|
{strings.description}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -118,7 +118,11 @@ function ModerationDetailsDialogInner({
|
|||||||
: _(msg`The author of this thread has hidden this reply.`)
|
: _(msg`The author of this thread has hidden this reply.`)
|
||||||
} else if (modcause.type === 'label') {
|
} else if (modcause.type === 'label') {
|
||||||
name = desc.name
|
name = desc.name
|
||||||
description = desc.description
|
description = (
|
||||||
|
<Text emoji style={[t.atoms.text, a.text_md, a.leading_snug]}>
|
||||||
|
{desc.description}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// should never happen
|
// should never happen
|
||||||
name = ''
|
name = ''
|
||||||
@@ -127,7 +131,7 @@ function ModerationDetailsDialogInner({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.ScrollableInner label={_(msg`Moderation details`)}>
|
<Dialog.ScrollableInner label={_(msg`Moderation details`)}>
|
||||||
<Text style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}>
|
<Text emoji style={[t.atoms.text, a.text_2xl, a.font_bold, a.mb_sm]}>
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[t.atoms.text, a.text_md, a.leading_snug]}>
|
<Text style={[t.atoms.text, a.text_md, a.leading_snug]}>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useHaptics} from '#/lib/haptics'
|
||||||
|
import {decrementBadgeCount} from '#/lib/notifications/notifications'
|
||||||
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {
|
import {
|
||||||
postUriToRelativePath,
|
postUriToRelativePath,
|
||||||
toBskyAppUrl,
|
toBskyAppUrl,
|
||||||
@@ -19,10 +23,6 @@ import {isNative} from '#/platform/detection'
|
|||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useHaptics} from 'lib/haptics'
|
|
||||||
import {decrementBadgeCount} from 'lib/notifications/notifications'
|
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
||||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||||
@@ -248,6 +248,7 @@ function ChatListItemReady({
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={[{maxWidth: '85%'}, web([a.leading_normal])]}>
|
style={[{maxWidth: '85%'}, web([a.leading_normal])]}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_md,
|
||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
@@ -301,6 +302,7 @@ function ChatListItemReady({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
style={[
|
style={[
|
||||||
a.text_sm,
|
a.text_sm,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function ProfileHeaderDisplayName({
|
|||||||
return (
|
return (
|
||||||
<View pointerEvents="none">
|
<View pointerEvents="none">
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
testID="profileHeaderDisplayName"
|
testID="profileHeaderDisplayName"
|
||||||
style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '600'}]}>
|
style={[t.atoms.text, a.text_4xl, a.self_start, {fontWeight: '600'}]}>
|
||||||
{sanitizeDisplayName(
|
{sanitizeDisplayName(
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {isInvalidHandle} from '#/lib/strings/handles'
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {isInvalidHandle} from 'lib/strings/handles'
|
|
||||||
import {isIOS} from 'platform/detection'
|
|
||||||
import {atoms as a, useTheme, web} from '#/alf'
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
import {NewskieDialog} from '#/components/NewskieDialog'
|
import {NewskieDialog} from '#/components/NewskieDialog'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
@@ -18,6 +19,7 @@ export function ProfileHeaderHandle({
|
|||||||
disableTaps?: boolean
|
disableTaps?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
const invalidHandle = isInvalidHandle(profile.handle)
|
const invalidHandle = isInvalidHandle(profile.handle)
|
||||||
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
|
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
|
||||||
return (
|
return (
|
||||||
@@ -33,6 +35,7 @@ export function ProfileHeaderHandle({
|
|||||||
</View>
|
</View>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={[
|
style={[
|
||||||
invalidHandle
|
invalidHandle
|
||||||
@@ -47,7 +50,7 @@ export function ProfileHeaderHandle({
|
|||||||
: [a.text_md, a.leading_tight, t.atoms.text_contrast_medium],
|
: [a.text_md, a.leading_tight, t.atoms.text_contrast_medium],
|
||||||
web({wordBreak: 'break-all'}),
|
web({wordBreak: 'break-all'}),
|
||||||
]}>
|
]}>
|
||||||
{invalidHandle ? <Trans>⚠Invalid Handle</Trans> : `@${profile.handle}`}
|
{invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,19 +19,19 @@ import PasteInput, {
|
|||||||
PasteInputRef,
|
PasteInputRef,
|
||||||
} from '@mattermost/react-native-paste-input'
|
} from '@mattermost/react-native-paste-input'
|
||||||
|
|
||||||
|
import {POST_IMG_MAX} from '#/lib/constants'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {downloadAndResize} from '#/lib/media/manip'
|
||||||
|
import {isUriImage} from '#/lib/media/util'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'
|
||||||
|
import {useTheme} from '#/lib/ThemeContext'
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {POST_IMG_MAX} from 'lib/constants'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {downloadAndResize} from 'lib/media/manip'
|
|
||||||
import {isUriImage} from 'lib/media/util'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {
|
import {
|
||||||
LinkFacetMatch,
|
LinkFacetMatch,
|
||||||
suggestLinkCardUri,
|
suggestLinkCardUri,
|
||||||
} from 'view/com/composer/text-input/text-input-util'
|
} from '#/view/com/composer/text-input/text-input-util'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {atoms as a, useAlf} from '#/alf'
|
import {atoms as a, useAlf} from '#/alf'
|
||||||
import {normalizeTextStyles} from '#/components/Typography'
|
import {normalizeTextStyles} from '#/components/Typography'
|
||||||
import {Autocomplete} from './mobile/Autocomplete'
|
import {Autocomplete} from './mobile/Autocomplete'
|
||||||
@@ -216,6 +216,7 @@ export const TextInput = forwardRef(function TextInputImpl(
|
|||||||
return Array.from(richtext.segments()).map(segment => {
|
return Array.from(richtext.segments()).map(segment => {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
key={i++}
|
key={i++}
|
||||||
style={[inputTextStyle, segment.facet ? pal.link : pal.text]}>
|
style={[inputTextStyle, segment.facet ? pal.link : pal.text]}>
|
||||||
{segment.text}
|
{segment.text}
|
||||||
|
|||||||
@@ -5,19 +5,20 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import {Pressable, StyleSheet, View} from 'react-native'
|
import {Pressable, StyleSheet, View} from 'react-native'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
import {ReactRenderer} from '@tiptap/react'
|
import {ReactRenderer} from '@tiptap/react'
|
||||||
import tippy, {Instance as TippyInstance} from 'tippy.js'
|
|
||||||
import {
|
import {
|
||||||
|
SuggestionKeyDownProps,
|
||||||
SuggestionOptions,
|
SuggestionOptions,
|
||||||
SuggestionProps,
|
SuggestionProps,
|
||||||
SuggestionKeyDownProps,
|
|
||||||
} from '@tiptap/suggestion'
|
} from '@tiptap/suggestion'
|
||||||
|
import tippy, {Instance as TippyInstance} from 'tippy.js'
|
||||||
|
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
import {ActorAutocompleteFn} from '#/state/queries/actor-autocomplete'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
|
||||||
import {useGrapheme} from '../hooks/useGrapheme'
|
import {useGrapheme} from '../hooks/useGrapheme'
|
||||||
import {Trans} from '@lingui/macro'
|
|
||||||
|
|
||||||
interface MentionListRef {
|
interface MentionListRef {
|
||||||
onKeyDown: (props: SuggestionKeyDownProps) => boolean
|
onKeyDown: (props: SuggestionKeyDownProps) => boolean
|
||||||
@@ -180,7 +181,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
|
|||||||
size={26}
|
size={26}
|
||||||
type={item.associated?.labeler ? 'labeler' : 'user'}
|
type={item.associated?.labeler ? 'labeler' : 'user'}
|
||||||
/>
|
/>
|
||||||
<Text style={pal.text} numberOfLines={1}>
|
<Text emoji style={pal.text} numberOfLines={1}>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {msg, Plural, Trans} from '@lingui/macro'
|
import {msg, Plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {s} from '#/lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {shouldClickOpenNewTab} from '#/platform/urls'
|
import {shouldClickOpenNewTab} from '#/platform/urls'
|
||||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
||||||
@@ -21,12 +25,8 @@ import {
|
|||||||
UsePreferencesQueryResponse,
|
UsePreferencesQueryResponse,
|
||||||
useRemoveFeedMutation,
|
useRemoveFeedMutation,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/preferences'
|
||||||
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import * as Toast from 'view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
@@ -242,7 +242,7 @@ export function FeedSourceCardLoaded({
|
|||||||
<UserAvatar type="algo" size={36} avatar={feed.avatar} />
|
<UserAvatar type="algo" size={36} avatar={feed.avatar} />
|
||||||
</View>
|
</View>
|
||||||
<View style={[styles.headerTextContainer]}>
|
<View style={[styles.headerTextContainer]}>
|
||||||
<Text style={[pal.text, s.bold]} numberOfLines={1}>
|
<Text emoji style={[pal.text, s.bold]} numberOfLines={1}>
|
||||||
{feed.displayName}
|
{feed.displayName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[pal.textLight]} numberOfLines={1}>
|
<Text style={[pal.textLight]} numberOfLines={1}>
|
||||||
|
|||||||
@@ -65,21 +65,27 @@ export function Component({
|
|||||||
return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
|
return [pal.border, {flex: 1, borderTopWidth: StyleSheet.hairlineWidth}]
|
||||||
}, [pal.border, screenHeight])
|
}, [pal.border, screenHeight])
|
||||||
|
|
||||||
|
const headerStyles = [
|
||||||
|
{
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: '600',
|
||||||
|
fontSize: 20,
|
||||||
|
marginBottom: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
} as const,
|
||||||
|
pal.text,
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
|
||||||
<Text
|
<Text style={headerStyles} numberOfLines={1}>
|
||||||
style={[
|
<Trans>
|
||||||
{
|
Update{' '}
|
||||||
textAlign: 'center',
|
<Text style={headerStyles} numberOfLines={1}>
|
||||||
fontWeight: '600',
|
{displayName}
|
||||||
fontSize: 20,
|
</Text>{' '}
|
||||||
marginBottom: 12,
|
in Lists
|
||||||
paddingHorizontal: 12,
|
</Trans>
|
||||||
},
|
|
||||||
pal.text,
|
|
||||||
]}
|
|
||||||
numberOfLines={1}>
|
|
||||||
<Trans>Update {displayName} in Lists</Trans>
|
|
||||||
</Text>
|
</Text>
|
||||||
<MyLists
|
<MyLists
|
||||||
filter="all"
|
filter="all"
|
||||||
|
|||||||
@@ -310,7 +310,11 @@ let FeedItem = ({
|
|||||||
key={authors[0].href}
|
key={authors[0].href}
|
||||||
style={[pal.text, s.bold]}
|
style={[pal.text, s.bold]}
|
||||||
href={authors[0].href}
|
href={authors[0].href}
|
||||||
text={forceLTR(firstAuthorName)}
|
text={
|
||||||
|
<Text emoji style={[pal.text, s.bold]}>
|
||||||
|
{forceLTR(firstAuthorName)}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
disableMismatchWarning
|
disableMismatchWarning
|
||||||
/>
|
/>
|
||||||
{authors.length > 1 ? (
|
{authors.length > 1 ? (
|
||||||
@@ -570,12 +574,13 @@ function ExpandedAuthorsList({
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
style={pal.text}
|
style={pal.text}
|
||||||
lineHeight={1.2}>
|
lineHeight={1.2}>
|
||||||
{sanitizeDisplayName(
|
<Text emoji type="lg-bold" style={pal.text} lineHeight={1.2}>
|
||||||
author.profile.displayName || author.profile.handle,
|
{sanitizeDisplayName(
|
||||||
)}
|
author.profile.displayName || author.profile.handle,
|
||||||
|
)}
|
||||||
|
</Text>{' '}
|
||||||
<Text style={[pal.textLight]} lineHeight={1.2}>
|
<Text style={[pal.textLight]} lineHeight={1.2}>
|
||||||
{sanitizeHandle(author.profile.handle)}
|
{sanitizeHandle(author.profile.handle, '@')}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -592,7 +597,11 @@ function AdditionalPostText({post}: {post?: AppBskyFeedDefs.PostView}) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{text?.length > 0 && <Text style={pal.textLight}>{text}</Text>}
|
{text?.length > 0 && (
|
||||||
|
<Text emoji style={pal.textLight}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<MediaPreview.Embed
|
<MediaPreview.Embed
|
||||||
embed={post.embed}
|
embed={post.embed}
|
||||||
style={styles.additionalPostImages}
|
style={styles.additionalPostImages}
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export function TabBar({
|
|||||||
onPress={() => onPressItem(i)}>
|
onPress={() => onPressItem(i)}>
|
||||||
<View style={[styles.itemInner, selected && indicatorStyle]}>
|
<View style={[styles.itemInner, selected && indicatorStyle]}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
|
type={isDesktop || isTablet ? 'xl-bold' : 'lg-bold'}
|
||||||
testID={testID ? `${testID}-${item}` : undefined}
|
testID={testID ? `${testID}-${item}` : undefined}
|
||||||
style={[
|
style={[
|
||||||
|
|||||||
@@ -12,24 +12,24 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {msg, Plural, Trans} from '@lingui/macro'
|
import {msg, Plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {MAX_POST_LINES} from '#/lib/constants'
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {countLines} from '#/lib/strings/helpers'
|
||||||
|
import {niceDate} from '#/lib/strings/time'
|
||||||
|
import {s} from '#/lib/styles'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
import {useOpenLink} from '#/state/preferences/in-app-browser'
|
||||||
import {ThreadPost} from '#/state/queries/post-thread'
|
import {ThreadPost} from '#/state/queries/post-thread'
|
||||||
|
import {useSession} from '#/state/session'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
import {MAX_POST_LINES} from 'lib/constants'
|
import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
|
||||||
import {countLines} from 'lib/strings/helpers'
|
|
||||||
import {niceDate} from 'lib/strings/time'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {isWeb} from 'platform/detection'
|
|
||||||
import {useSession} from 'state/session'
|
|
||||||
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {AppModerationCause} from '#/components/Pills'
|
import {AppModerationCause} from '#/components/Pills'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
@@ -308,6 +308,7 @@ let PostThreadItemLoaded = ({
|
|||||||
style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}>
|
style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}>
|
||||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
type="xl-bold"
|
type="xl-bold"
|
||||||
style={[pal.text, a.self_start]}
|
style={[pal.text, a.self_start]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
@@ -322,7 +323,11 @@ let PostThreadItemLoaded = ({
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
<Link style={s.flex1} href={authorHref} title={authorTitle}>
|
||||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
<Text
|
||||||
|
emoji
|
||||||
|
type="md"
|
||||||
|
style={[pal.textLight]}
|
||||||
|
numberOfLines={1}>
|
||||||
{sanitizeHandle(post.author.handle, '@')}
|
{sanitizeHandle(post.author.handle, '@')}
|
||||||
</Text>
|
</Text>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -316,11 +316,19 @@ let FeedItemInner = ({
|
|||||||
style={pal.textLight}
|
style={pal.textLight}
|
||||||
lineHeight={1.2}
|
lineHeight={1.2}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
text={sanitizeDisplayName(
|
text={
|
||||||
reason.by.displayName ||
|
<Text
|
||||||
sanitizeHandle(reason.by.handle),
|
emoji
|
||||||
moderation.ui('displayName'),
|
type="sm-bold"
|
||||||
)}
|
style={pal.textLight}
|
||||||
|
lineHeight={1.2}>
|
||||||
|
{sanitizeDisplayName(
|
||||||
|
reason.by.displayName ||
|
||||||
|
sanitizeHandle(reason.by.handle),
|
||||||
|
moderation.ui('displayName'),
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
href={makeProfileLink(reason.by)}
|
href={makeProfileLink(reason.by)}
|
||||||
onBeforePress={onOpenReposter}
|
onBeforePress={onOpenReposter}
|
||||||
/>
|
/>
|
||||||
@@ -527,9 +535,11 @@ function ReplyToLabel({
|
|||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
href={makeProfileLink(profile)}
|
href={makeProfileLink(profile)}
|
||||||
text={
|
text={
|
||||||
profile.displayName
|
<Text emoji type="md" style={pal.textLight} lineHeight={1.2}>
|
||||||
? sanitizeDisplayName(profile.displayName)
|
{profile.displayName
|
||||||
: sanitizeHandle(profile.handle)
|
? sanitizeDisplayName(profile.displayName)
|
||||||
|
: sanitizeHandle(profile.handle)}
|
||||||
|
</Text>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ProfileHoverCard>
|
</ProfileHoverCard>
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import {
|
|||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {getModerationCauseKey, isJustAMute} from '#/lib/moderation'
|
||||||
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {s} from '#/lib/styles'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {precacheProfile} from '#/state/queries/profile'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {getModerationCauseKey, isJustAMute} from 'lib/moderation'
|
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {precacheProfile} from 'state/queries/profile'
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {
|
import {
|
||||||
KnownFollowers,
|
KnownFollowers,
|
||||||
@@ -103,6 +103,7 @@ export function ProfileCard({
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
type="lg"
|
type="lg"
|
||||||
style={[s.bold, pal.text, a.self_start]}
|
style={[s.bold, pal.text, a.self_start]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
@@ -112,7 +113,7 @@ export function ProfileCard({
|
|||||||
moderation.ui('displayName'),
|
moderation.ui('displayName'),
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
|
<Text emoji type="md" style={[pal.textLight]} numberOfLines={1}>
|
||||||
{sanitizeHandle(profile.handle, '@')}
|
{sanitizeHandle(profile.handle, '@')}
|
||||||
</Text>
|
</Text>
|
||||||
<ProfileCardPills
|
<ProfileCardPills
|
||||||
@@ -128,7 +129,7 @@ export function ProfileCard({
|
|||||||
{profile.description || knownFollowersVisible ? (
|
{profile.description || knownFollowersVisible ? (
|
||||||
<View style={styles.details}>
|
<View style={styles.details}>
|
||||||
{profile.description ? (
|
{profile.description ? (
|
||||||
<Text style={pal.text} numberOfLines={4}>
|
<Text emoji style={pal.text} numberOfLines={4}>
|
||||||
{profile.description as string}
|
{profile.description as string}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {forceLTR} from '#/lib/strings/bidi'
|
||||||
|
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
|
||||||
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {niceDate} from '#/lib/strings/time'
|
||||||
|
import {TypographyVariant} from '#/lib/ThemeContext'
|
||||||
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {precacheProfile} from '#/state/queries/profile'
|
import {precacheProfile} from '#/state/queries/profile'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
|
||||||
import {forceLTR} from 'lib/strings/bidi'
|
|
||||||
import {NON_BREAKING_SPACE} from 'lib/strings/constants'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
|
||||||
import {niceDate} from 'lib/strings/time'
|
|
||||||
import {TypographyVariant} from 'lib/ThemeContext'
|
|
||||||
import {isAndroid} from 'platform/detection'
|
|
||||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||||
import {TextLinkOnWebOnly} from './Link'
|
import {TextLinkOnWebOnly} from './Link'
|
||||||
import {Text} from './text/Text'
|
import {Text} from './text/Text'
|
||||||
@@ -73,12 +73,20 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||||||
style={[pal.text]}
|
style={[pal.text]}
|
||||||
lineHeight={1.2}
|
lineHeight={1.2}
|
||||||
disableMismatchWarning
|
disableMismatchWarning
|
||||||
text={forceLTR(
|
text={
|
||||||
sanitizeDisplayName(
|
<Text
|
||||||
displayName,
|
type={opts.displayNameType || 'lg-bold'}
|
||||||
opts.moderation?.ui('displayName'),
|
emoji
|
||||||
),
|
style={[pal.text]}
|
||||||
)}
|
lineHeight={1.2}>
|
||||||
|
{forceLTR(
|
||||||
|
sanitizeDisplayName(
|
||||||
|
displayName,
|
||||||
|
opts.moderation?.ui('displayName'),
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
href={profileLink}
|
href={profileLink}
|
||||||
onBeforePress={onBeforePressAuthor}
|
onBeforePress={onBeforePressAuthor}
|
||||||
/>
|
/>
|
||||||
@@ -86,7 +94,11 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||||||
type="md"
|
type="md"
|
||||||
disableMismatchWarning
|
disableMismatchWarning
|
||||||
style={[pal.textLight, {flexShrink: 4}]}
|
style={[pal.textLight, {flexShrink: 4}]}
|
||||||
text={NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
|
text={
|
||||||
|
<Text emoji style={[pal.textLight, {flexShrink: 4}]}>
|
||||||
|
{NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
href={profileLink}
|
href={profileLink}
|
||||||
onBeforePress={onBeforePressAuthor}
|
onBeforePress={onBeforePressAuthor}
|
||||||
anchorNoUnderline
|
anchorNoUnderline
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
|
|
||||||
import {StyleProp, StyleSheet, TextStyle} from 'react-native'
|
import {StyleProp, StyleSheet, TextStyle} from 'react-native'
|
||||||
import {TextLinkOnWebOnly} from './Link'
|
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
|
||||||
import {Text} from './text/Text'
|
|
||||||
import {LoadingPlaceholder} from './LoadingPlaceholder'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {TypographyVariant} from 'lib/ThemeContext'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {TypographyVariant} from '#/lib/ThemeContext'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
|
import {TextLinkOnWebOnly} from './Link'
|
||||||
|
import {LoadingPlaceholder} from './LoadingPlaceholder'
|
||||||
|
import {Text} from './text/Text'
|
||||||
|
|
||||||
export function UserInfoText({
|
export function UserInfoText({
|
||||||
type = 'md',
|
type = 'md',
|
||||||
@@ -50,11 +51,15 @@ export function UserInfoText({
|
|||||||
lineHeight={1.2}
|
lineHeight={1.2}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
href={makeProfileLink(profile)}
|
href={makeProfileLink(profile)}
|
||||||
text={`${prefix || ''}${sanitizeDisplayName(
|
text={
|
||||||
typeof profile[attr] === 'string' && profile[attr]
|
<Text emoji type={type} style={style} lineHeight={1.2}>
|
||||||
? (profile[attr] as string)
|
{`${prefix || ''}${sanitizeDisplayName(
|
||||||
: sanitizeHandle(profile.handle),
|
typeof profile[attr] === 'string' && profile[attr]
|
||||||
)}`}
|
? (profile[attr] as string)
|
||||||
|
: sanitizeHandle(profile.handle),
|
||||||
|
)}`}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ import {AppBskyEmbedExternal} from '@atproto/api'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {shareUrl} from 'lib/sharing'
|
import {shareUrl} from '#/lib/sharing'
|
||||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
|
||||||
import {
|
import {
|
||||||
getStarterPackOgCard,
|
getStarterPackOgCard,
|
||||||
parseStarterPackUri,
|
parseStarterPackUri,
|
||||||
} from 'lib/strings/starter-pack'
|
} from '#/lib/strings/starter-pack'
|
||||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
import {isNative} from 'platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useExternalEmbedsPrefs} from 'state/preferences'
|
import {useExternalEmbedsPrefs} from '#/state/preferences'
|
||||||
import {Link} from 'view/com/util/Link'
|
import {Link} from '#/view/com/util/Link'
|
||||||
import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed'
|
import {ExternalGifEmbed} from '#/view/com/util/post-embeds/ExternalGifEmbed'
|
||||||
import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed'
|
import {ExternalPlayer} from '#/view/com/util/post-embeds/ExternalPlayerEmbed'
|
||||||
import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed'
|
import {GifEmbed} from '#/view/com/util/post-embeds/GifEmbed'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
import {Text} from '../text/Text'
|
import {Text} from '../text/Text'
|
||||||
@@ -115,12 +115,13 @@ export const ExternalLinkEmbed = ({
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
|
{!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
|
||||||
<Text type="lg-bold" numberOfLines={3} style={[pal.text]}>
|
<Text emoji type="lg-bold" numberOfLines={3} style={[pal.text]}>
|
||||||
{link.title || link.uri}
|
{link.title || link.uri}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{link.description ? (
|
{link.description ? (
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
type="md"
|
type="md"
|
||||||
numberOfLines={link.thumb ? 2 : 4}
|
numberOfLines={link.thumb ? 2 : 4}
|
||||||
style={[pal.text, a.mt_xs]}>
|
style={[pal.text, a.mt_xs]}>
|
||||||
|
|||||||
@@ -2,27 +2,40 @@ import React from 'react'
|
|||||||
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
|
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
|
||||||
import {UITextView} from 'react-native-uitextview'
|
import {UITextView} from 'react-native-uitextview'
|
||||||
|
|
||||||
import {lh, s} from 'lib/styles'
|
import {lh, s} from '#/lib/styles'
|
||||||
import {TypographyVariant, useTheme} from 'lib/ThemeContext'
|
import {TypographyVariant, useTheme} from '#/lib/ThemeContext'
|
||||||
import {isIOS, isWeb} from 'platform/detection'
|
import {logger} from '#/logger'
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
import {applyFonts, useAlf} from '#/alf'
|
import {applyFonts, useAlf} from '#/alf'
|
||||||
|
import {
|
||||||
|
childHasEmoji,
|
||||||
|
childIsString,
|
||||||
|
renderChildrenWithEmoji,
|
||||||
|
StringChild,
|
||||||
|
} from '#/components/Typography'
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
export type CustomTextProps = TextProps & {
|
export type CustomTextProps = Omit<TextProps, 'children'> & {
|
||||||
type?: TypographyVariant
|
type?: TypographyVariant
|
||||||
lineHeight?: number
|
lineHeight?: number
|
||||||
title?: string
|
title?: string
|
||||||
dataSet?: Record<string, string | number>
|
dataSet?: Record<string, string | number>
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
const fontFamilyStyle = {
|
emoji: true
|
||||||
fontFamily:
|
children: StringChild
|
||||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
|
}
|
||||||
}
|
| {
|
||||||
|
emoji?: false
|
||||||
|
children: TextProps['children']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export function Text({
|
export function Text({
|
||||||
type = 'md',
|
type = 'md',
|
||||||
children,
|
children,
|
||||||
|
emoji,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
style,
|
style,
|
||||||
title,
|
title,
|
||||||
@@ -35,6 +48,18 @@ export function Text({
|
|||||||
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
|
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
|
||||||
const {fonts} = useAlf()
|
const {fonts} = useAlf()
|
||||||
|
|
||||||
|
if (IS_DEV) {
|
||||||
|
if (!emoji && childHasEmoji(children)) {
|
||||||
|
logger.warn(
|
||||||
|
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji && !childIsString(children)) {
|
||||||
|
logger.error('Text: when <Text emoji />, children can only be strings.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (selectable && isIOS) {
|
if (selectable && isIOS) {
|
||||||
const flattened = StyleSheet.flatten([
|
const flattened = StyleSheet.flatten([
|
||||||
s.black,
|
s.black,
|
||||||
@@ -58,7 +83,7 @@ export function Text({
|
|||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
uiTextView
|
uiTextView
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||||
</UITextView>
|
</UITextView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -66,7 +91,6 @@ export function Text({
|
|||||||
const flattened = StyleSheet.flatten([
|
const flattened = StyleSheet.flatten([
|
||||||
s.black,
|
s.black,
|
||||||
typography,
|
typography,
|
||||||
isWeb && fontFamilyStyle,
|
|
||||||
lineHeightStyle,
|
lineHeightStyle,
|
||||||
style,
|
style,
|
||||||
])
|
])
|
||||||
@@ -87,7 +111,7 @@ export function Text({
|
|||||||
dataSet={Object.assign({tooltip: title}, dataSet || {})}
|
dataSet={Object.assign({tooltip: title}, dataSet || {})}
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||||
</RNText>
|
</RNText>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {CustomTextProps, Text} from './Text'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {addStyle} from 'lib/styles'
|
|
||||||
|
|
||||||
export type ThemedTextProps = CustomTextProps & {
|
|
||||||
fg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light'
|
|
||||||
bg?: 'default' | 'light' | 'error' | 'inverted' | 'inverted-light'
|
|
||||||
border?: 'default' | 'dark' | 'error' | 'inverted' | 'inverted-dark'
|
|
||||||
lineHeight?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThemedText({
|
|
||||||
fg,
|
|
||||||
bg,
|
|
||||||
border,
|
|
||||||
style,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.PropsWithChildren<ThemedTextProps>) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const palInverted = usePalette('inverted')
|
|
||||||
const palError = usePalette('error')
|
|
||||||
switch (fg) {
|
|
||||||
case 'default':
|
|
||||||
style = addStyle(style, pal.text)
|
|
||||||
break
|
|
||||||
case 'light':
|
|
||||||
style = addStyle(style, pal.textLight)
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
style = addStyle(style, {color: palError.colors.background})
|
|
||||||
break
|
|
||||||
case 'inverted':
|
|
||||||
style = addStyle(style, palInverted.text)
|
|
||||||
break
|
|
||||||
case 'inverted-light':
|
|
||||||
style = addStyle(style, palInverted.textLight)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch (bg) {
|
|
||||||
case 'default':
|
|
||||||
style = addStyle(style, pal.view)
|
|
||||||
break
|
|
||||||
case 'light':
|
|
||||||
style = addStyle(style, pal.viewLight)
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
style = addStyle(style, palError.view)
|
|
||||||
break
|
|
||||||
case 'inverted':
|
|
||||||
style = addStyle(style, palInverted.view)
|
|
||||||
break
|
|
||||||
case 'inverted-light':
|
|
||||||
style = addStyle(style, palInverted.viewLight)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch (border) {
|
|
||||||
case 'default':
|
|
||||||
style = addStyle(style, pal.border)
|
|
||||||
break
|
|
||||||
case 'dark':
|
|
||||||
style = addStyle(style, pal.borderDark)
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
style = addStyle(style, palError.border)
|
|
||||||
break
|
|
||||||
case 'inverted':
|
|
||||||
style = addStyle(style, palInverted.border)
|
|
||||||
break
|
|
||||||
case 'inverted-dark':
|
|
||||||
style = addStyle(style, palInverted.borderDark)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Text style={style} {...props}>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -959,6 +959,7 @@ function SearchHistory({
|
|||||||
accessibilityIgnoresInvertColors
|
accessibilityIgnoresInvertColors
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
style={[pal.text, styles.profileName]}
|
style={[pal.text, styles.profileName]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
{profile.displayName || profile.handle}
|
{profile.displayName || profile.handle}
|
||||||
|
|||||||
@@ -16,19 +16,19 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {precacheProfile} from '#/state/queries/profile'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {SearchInput} from '#/view/com/util/forms/SearchInput'
|
||||||
import {precacheProfile} from 'state/queries/profile'
|
|
||||||
import {Link} from '#/view/com/util/Link'
|
import {Link} from '#/view/com/util/Link'
|
||||||
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {SearchInput} from 'view/com/util/forms/SearchInput'
|
|
||||||
import {Text} from 'view/com/util/text/Text'
|
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
let SearchLinkCard = ({
|
let SearchLinkCard = ({
|
||||||
@@ -126,6 +126,7 @@ let SearchProfileCard = ({
|
|||||||
/>
|
/>
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
type="lg"
|
type="lg"
|
||||||
style={[s.bold, pal.text, a.self_start]}
|
style={[s.bold, pal.text, a.self_start]}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
|
|||||||
@@ -11360,6 +11360,11 @@ emoji-mart@^5.5.2:
|
|||||||
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
|
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
|
||||||
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
|
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
|
||||||
|
|
||||||
|
emoji-regex@^10.4.0:
|
||||||
|
version "10.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
|
||||||
|
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
|||||||
Reference in New Issue
Block a user