Compare commits

..

4 Commits

Author SHA1 Message Date
Eric Bailey 1c988e6471 Update e2e file 2025-08-14 11:08:42 +03:00
Eric Bailey 4199aab5cc Refactor API 2025-08-14 11:08:42 +03:00
Eric Bailey 185840d389 Fix animation on iOS 2025-08-14 11:08:42 +03:00
Eric Bailey dbcef53e70 Integrate Sonner for toasts 2025-08-14 11:08:42 +03:00
26 changed files with 266 additions and 232 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '16.4'
xcode-version: '16.2'
- name: ☕️ Setup Cocoapods
uses: maxim-lobanov/setup-cocoapods@v1
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg>

Before

Width:  |  Height:  |  Size: 334 B

-63
View File
@@ -1,63 +0,0 @@
import {View} from 'react-native'
import {Trans} from '@lingui/macro'
import {useSession} from '#/state/session'
import {UserInfoText} from '#/view/com/util/UserInfoText'
import {atoms as a, useTheme} from '#/alf'
import {ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
export function PostRepliedTo({
parentAuthor,
isParentBlocked,
isParentNotFound,
}: {
parentAuthor: string | bsky.profile.AnyProfileView | undefined
isParentBlocked?: boolean
isParentNotFound?: boolean
}) {
const t = useTheme()
const {currentAccount} = useSession()
const textStyle = [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]
let label
if (isParentBlocked) {
label = <Trans context="description">Replied to a blocked post</Trans>
} else if (isParentNotFound) {
label = <Trans context="description">Replied to a post</Trans>
} else if (parentAuthor) {
const did =
typeof parentAuthor === 'string' ? parentAuthor : parentAuthor.did
const isMe = currentAccount?.did === did
if (isMe) {
label = <Trans context="description">Replied to you</Trans>
} else {
label = (
<Trans context="description">
Replied to{' '}
<ProfileHoverCard did={did}>
<UserInfoText did={did} attr="displayName" style={textStyle} />
</ProfileHoverCard>
</Trans>
)
}
}
if (!label) {
// Should not happen.
return null
}
return (
<View style={[a.flex_row, a.align_center, a.pb_xs, a.gap_xs]}>
<ArrowCornerDownRightIcon
size="xs"
style={[t.atoms.text_contrast_medium, {top: -1}]}
/>
<Text style={textStyle}>{label}</Text>
</View>
)
}
@@ -539,7 +539,7 @@ let Tab = ({
]}>
<Text
style={[
a.font_medium,
/* TODO: medium weight */
active || hovered || pressed || focused
? t.atoms.text
: t.atoms.text_contrast_medium,
+1 -1
View File
@@ -92,7 +92,7 @@ export function ToastText({children}: {children: React.ReactNode}) {
selectable={false}
style={[
a.text_md,
a.font_medium,
a.font_bold,
a.leading_snug,
a.pointer_events_none,
{
@@ -1,7 +0,0 @@
import {createSinglePathSVG} from './TEMPLATE'
export const ArrowCornerDownRight_Stroke2_Corner2_Rounded = createSinglePathSVG(
{
path: 'M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z',
},
)
+35 -28
View File
@@ -2685,6 +2685,11 @@ msgstr ""
msgid "Dismiss interests"
msgstr ""
#. Accessibility label for dismissing a toast notification
#: src/components/Toast/index.web.tsx:82
msgid "Dismiss message"
msgstr ""
#: src/components/interstitials/TrendingVideos.tsx:89
msgid "Dismiss this section"
msgstr ""
@@ -3805,7 +3810,7 @@ msgstr ""
msgid "From @{sanitizedAuthor}"
msgstr ""
#: src/view/com/posts/PostFeedItem.tsx:326
#: src/view/com/posts/PostFeedItem.tsx:330
msgctxt "from-feed"
msgid "From <0/>"
msgstr ""
@@ -6106,7 +6111,7 @@ msgstr ""
msgid "Pin to your profile"
msgstr ""
#: src/view/com/posts/PostFeedItem.tsx:407
#: src/view/com/posts/PostFeedItem.tsx:411
msgid "Pinned"
msgstr ""
@@ -6873,26 +6878,6 @@ msgstr ""
msgid "Replace with Discover"
msgstr ""
#: src/components/Post/PostRepliedTo.tsx:39
msgctxt "description"
msgid "Replied to <0><1/></0>"
msgstr ""
#: src/components/Post/PostRepliedTo.tsx:28
msgctxt "description"
msgid "Replied to a blocked post"
msgstr ""
#: src/components/Post/PostRepliedTo.tsx:30
msgctxt "description"
msgid "Replied to a post"
msgstr ""
#: src/components/Post/PostRepliedTo.tsx:36
msgctxt "description"
msgid "Replied to you"
msgstr ""
#: src/components/activity-notifications/SubscribeProfileDialog.tsx:267
#: src/components/activity-notifications/SubscribeProfileDialog.tsx:279
#: src/lib/hooks/useNotificationHandler.ts:140
@@ -6948,6 +6933,28 @@ msgstr ""
msgid "Reply sorting"
msgstr ""
#: src/view/com/post/Post.tsx:204
#: src/view/com/posts/PostFeedItem.tsx:602
msgctxt "description"
msgid "Reply to <0><1/></0>"
msgstr ""
#: src/view/com/posts/PostFeedItem.tsx:593
msgctxt "description"
msgid "Reply to a blocked post"
msgstr ""
#: src/view/com/posts/PostFeedItem.tsx:595
msgctxt "description"
msgid "Reply to a post"
msgstr ""
#: src/view/com/post/Post.tsx:202
#: src/view/com/posts/PostFeedItem.tsx:599
msgctxt "description"
msgid "Reply to you"
msgstr ""
#: src/components/PostControls/PostMenu/PostMenuItems.tsx:335
msgctxt "toast"
msgid "Reply visibility updated"
@@ -7073,16 +7080,16 @@ msgstr ""
msgid "Reposted By"
msgstr ""
#: src/view/com/posts/PostFeedItem.tsx:347
#: src/view/com/posts/PostFeedItem.tsx:351
msgid "Reposted by {0}"
msgstr ""
#: src/view/com/posts/PostFeedItem.tsx:366
#: src/view/com/posts/PostFeedItem.tsx:370
msgid "Reposted by <0><1/></0>"
msgstr ""
#: src/view/com/posts/PostFeedItem.tsx:345
#: src/view/com/posts/PostFeedItem.tsx:364
#: src/view/com/posts/PostFeedItem.tsx:349
#: src/view/com/posts/PostFeedItem.tsx:368
msgid "Reposted by you"
msgstr ""
@@ -8098,7 +8105,7 @@ msgstr ""
msgid "Something wrong? Let us know."
msgstr ""
#: src/App.native.tsx:126
#: src/App.native.tsx:125
#: src/App.web.tsx:102
msgid "Sorry! Your session expired. Please sign in again."
msgstr ""
@@ -9625,7 +9632,7 @@ msgstr ""
msgid "Videos must be less than 3 minutes long"
msgstr ""
#: src/screens/Profile/Header/Shell.tsx:229
#: src/screens/Profile/Header/Shell.tsx:211
msgid "View {0}'s avatar"
msgstr ""
@@ -254,7 +254,6 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
author: post.author,
embed: post.embed,
moderation,
langs: record.langs,
},
onPostSuccess: onPostSuccess,
})
@@ -234,7 +234,6 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
author: post.author,
embed: post.embed,
moderation,
langs: post.record.langs,
},
onPostSuccess: onPostSuccess,
})
@@ -298,7 +298,6 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
author: post.author,
embed: post.embed,
moderation,
langs: post.record.langs,
},
onPostSuccess: onPostSuccess,
})
-1
View File
@@ -91,7 +91,6 @@ export function PostThread({uri}: {uri: string}) {
author: post.author,
embed: post.embed,
moderation: anchor.moderation,
langs: post.record.langs,
},
onPostSuccess: optimisticOnPostReply,
})
+55 -41
View File
@@ -1,5 +1,5 @@
import {memo, useCallback, useEffect, useMemo} from 'react'
import {TouchableWithoutFeedback, View} from 'react-native'
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
import Animated, {
measure,
type MeasuredDimensions,
@@ -26,8 +26,6 @@ import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {UserBanner} from '#/view/com/util/UserBanner'
import {atoms as a, platform, useTheme} from '#/alf'
import {transparentifyColor} from '#/alf/util/colorGeneration'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
import {EditLiveDialog} from '#/components/live/EditLiveDialog'
@@ -150,46 +148,30 @@ let ProfileHeaderShell = ({
<StatusBarShadow />
<GrowableBanner
backButton={
!hideBackButton && (
<Button
testID="profileHeaderBackBtn"
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
label={_(msg`Back`)}
style={[
a.absolute,
a.pointer,
{
top: platform({
web: 10,
default: topInset,
}),
left: platform({
web: 18,
default: 12,
}),
},
]}>
{({hovered}) => (
<>
{!hideBackButton && (
<TouchableWithoutFeedback
testID="profileHeaderBackBtn"
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
accessibilityRole="button"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<View
style={[
a.align_center,
a.justify_center,
a.rounded_sm,
styles.backBtnWrapper,
{
width: 31,
height: 31,
backgroundColor: transparentifyColor('#000', 0.5),
},
hovered && {
backgroundColor: transparentifyColor('#000', 0.75),
top: platform({
web: 10,
default: topInset,
}),
},
]}>
<ArrowLeftIcon size="lg" fill="white" />
</View>
)}
</Button>
)
</TouchableWithoutFeedback>
)}
</>
}>
{isPlaceholderProfile ? (
<LoadingPlaceholder
@@ -221,7 +203,7 @@ let ProfileHeaderShell = ({
</View>
)}
<GrowableAvatar style={[a.absolute, {top: 104, left: 10}]}>
<GrowableAvatar style={styles.aviPosition}>
<TouchableWithoutFeedback
testID="profileHeaderAviButton"
onPress={onPressAvi}
@@ -233,14 +215,13 @@ let ProfileHeaderShell = ({
t.atoms.bg,
a.rounded_full,
{
width: 94,
height: 94,
borderWidth: live.isActive ? 3 : 2,
borderColor: live.isActive
? t.palette.negative_500
: t.atoms.bg.backgroundColor,
},
profile.associated?.labeler && a.rounded_md,
styles.avi,
profile.associated?.labeler && styles.aviLabeler,
]}>
<Animated.View ref={aviRef} collapsable={false}>
<UserAvatar
@@ -274,6 +255,39 @@ let ProfileHeaderShell = ({
</View>
)
}
ProfileHeaderShell = memo(ProfileHeaderShell)
export {ProfileHeaderShell}
const styles = StyleSheet.create({
backBtnWrapper: {
position: 'absolute',
left: 12,
width: 30,
height: 30,
overflow: 'hidden',
borderRadius: 15,
cursor: 'pointer',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
alignItems: 'center',
justifyContent: 'center',
},
backBtn: {
width: 30,
height: 30,
borderRadius: 15,
alignItems: 'center',
justifyContent: 'center',
},
aviPosition: {
position: 'absolute',
top: 104,
left: 10,
},
avi: {
width: 94,
height: 94,
},
aviLabeler: {
borderRadius: 10,
},
})
@@ -147,7 +147,7 @@ let Tab = ({
]}>
<Text
style={[
a.font_medium,
/* TODO: medium weight */
active || hovered || pressed || focused
? t.atoms.text
: t.atoms.text_contrast_medium,
@@ -163,7 +163,10 @@ function TrendingIndicator({type}: {type: TrendingIndicatorType | 'skeleton'}) {
a.align_center,
a.gap_xs,
a.rounded_full,
{height: 28, paddingHorizontal: 10},
a.px_sm,
{
height: 28,
},
]
let Icon: React.ComponentType<SVGIconProps> | null = null
@@ -215,7 +218,7 @@ function TrendingIndicator({type}: {type: TrendingIndicatorType | 'skeleton'}) {
return (
<View style={[pillStyles, {backgroundColor}]}>
{Icon && <Icon size="sm" style={{color}} />}
<Text style={[a.text_sm, a.font_medium, {color}]}>{text}</Text>
<Text style={[a.text_sm, {color}]}>{text}</Text>
</View>
)
}
-1
View File
@@ -753,7 +753,6 @@ function Overlay({
text: record?.text || '',
author: post.author,
embed: post.embed,
langs: record?.langs,
},
})
}, [openComposer, post, record])
+2 -9
View File
@@ -307,16 +307,9 @@ export function sortAndAnnotateThreadItems(
metadata.isPartOfLastBranchFromDepth = metadata.depth
/**
* If the parent is part of the last branch of the sub-tree, so
* is the child. However, if the child is also a last sibling,
* then we need to start tracking `isPartOfLastBranchFromDepth`
* from this point onwards, always updating it to the depth of
* the last sibling as we go down.
* If the parent is part of the last branch of the sub-tree, so is the child.
*/
if (
!metadata.isLastSibling &&
metadata.parentMetadata.isPartOfLastBranchFromDepth
) {
if (metadata.parentMetadata.isPartOfLastBranchFromDepth) {
metadata.isPartOfLastBranchFromDepth =
metadata.parentMetadata.isPartOfLastBranchFromDepth
}
+2 -2
View File
@@ -151,8 +151,8 @@ export type TraversalMetadata = {
*/
isLastChild: boolean
/**
* Indicates if the post is the left-most AND lower-most branch of the reply
* tree. Value corresponds to the depth at which this branch started.
* Indicates if the post is the left/lower-most branch of the reply tree.
* Value corresponds to the depth at which this branch started.
*/
isPartOfLastBranchFromDepth?: number
/**
-1
View File
@@ -20,7 +20,6 @@ export interface ComposerOptsPostRef {
uri: string
cid: string
text: string
langs?: string[]
author: AppBskyActorDefs.ProfileViewBasic
embed?: AppBskyFeedDefs.PostView['embed']
moderation?: ModerationDecision
+1 -5
View File
@@ -621,11 +621,7 @@ export const ComposePost = ({
const footer = (
<>
<SuggestedLanguage
text={activePost.richtext.text}
// NOTE(@elijaharita): currently just choosing the first language if any exists
replyToLanguage={replyTo?.langs?.[0]}
/>
<SuggestedLanguage text={activePost.richtext.text} />
<ComposerPills
isReply={!!replyTo}
post={activePost}
@@ -19,29 +19,16 @@ import {Text} from '#/components/Typography'
const onIdle = globalThis.requestIdleCallback || (cb => setTimeout(cb, 1))
const cancelIdle = globalThis.cancelIdleCallback || clearTimeout
export function SuggestedLanguage({
text,
replyToLanguage,
}: {
text: string
replyToLanguage?: string
}) {
export function SuggestedLanguage({text}: {text: string}) {
const [suggestedLanguage, setSuggestedLanguage] = useState<
string | undefined
>(text.length === 0 ? replyToLanguage : undefined)
>()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
const t = useTheme()
const {_} = useLingui()
useEffect(() => {
// For replies, suggest the language of the post being replied to if no text
// has been typed yet
if (replyToLanguage && text.length === 0) {
setSuggestedLanguage(replyToLanguage)
return
}
const textTrimmed = text.trim()
// Don't run the language model on small posts, the results are likely
@@ -56,7 +43,7 @@ export function SuggestedLanguage({
})
return () => cancelIdle(idle)
}, [text, replyToLanguage])
}, [text])
if (
suggestedLanguage &&
@@ -220,11 +220,7 @@ export const TextInput = React.forwardRef(function TextInputImpl(
textInputWebEmitter.emit('publish')
return true
}
if (
event.code === 'Backspace' &&
!(event.metaKey || event.altKey || event.ctrlKey)
) {
if (event.code === 'Backspace') {
const isNotSelection = view.state.selection.empty
if (isNotSelection) {
const cursorPosition = view.state.selection.$anchor.pos
-1
View File
@@ -420,7 +420,6 @@ export function PostThread({uri}: {uri: string}) {
author: thread.post.author,
embed: thread.post.embed,
moderation: threadModerationCache.get(thread),
langs: thread.record.langs,
},
onPost: onPostReply,
})
@@ -299,7 +299,6 @@ let PostThreadItemLoaded = ({
author: post.author,
embed: post.embed,
moderation,
langs: record.langs,
},
onPost: onPostReply,
onPostSuccess: onPostSuccess,
+40 -6
View File
@@ -8,6 +8,8 @@ import {
type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans} from '@lingui/macro'
import {useQueryClient} from '@tanstack/react-query'
import {MAX_POST_LINES} from '#/lib/constants'
@@ -15,25 +17,28 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers'
import {colors} from '#/lib/styles'
import {colors, s} from '#/lib/styles'
import {
POST_TOMBSTONE,
type Shadow,
usePostShadow,
} from '#/state/cache/post-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {unstableCacheProfileView} from '#/state/queries/profile'
import {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {Link} from '#/view/com/util/Link'
import {PostMeta} from '#/view/com/util/PostMeta'
import {Text} from '#/view/com/util/text/Text'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {UserInfoText} from '#/view/com/util/UserInfoText'
import {atoms as a} from '#/alf'
import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
import {PostControls} from '#/components/PostControls'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover'
import * as bsky from '#/types/bsky'
@@ -131,7 +136,6 @@ function PostInner({
author: post.author,
embed: post.embed,
moderation,
langs: record.langs,
},
})
}, [openComposer, post, record, moderation])
@@ -141,9 +145,12 @@ function PostInner({
}, [setLimitLines])
const onBeforePress = useCallback(() => {
unstableCacheProfileView(queryClient, post.author)
precacheProfile(queryClient, post.author)
}, [queryClient, post.author])
const {currentAccount} = useSession()
const isMe = replyAuthorDid === currentAccount?.did
const [hover, setHover] = useState(false)
return (
<Link
@@ -180,7 +187,34 @@ function PostInner({
postHref={itemHref}
/>
{replyAuthorDid !== '' && (
<PostRepliedTo parentAuthor={replyAuthorDid} />
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
<FontAwesomeIcon
icon="reply"
size={9}
style={[pal.textLight, s.mr5]}
/>
<Text
type="sm"
style={[pal.textLight, s.mr2]}
lineHeight={1.2}
numberOfLines={1}>
{isMe ? (
<Trans context="description">Reply to you</Trans>
) : (
<Trans context="description">
Reply to{' '}
<ProfileHoverCard did={replyAuthorDid}>
<UserInfoText
type="sm"
did={replyAuthorDid}
attr="displayName"
style={[pal.textLight]}
/>
</ProfileHoverCard>
</Trans>
)}
</Text>
</View>
)}
<LabelsOnMyPost post={post} />
<ContentHider
+79 -6
View File
@@ -9,6 +9,10 @@ import {
type ModerationDecision,
RichText as RichTextAPI,
} from '@atproto/api'
import {
FontAwesomeIcon,
type FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
@@ -22,6 +26,7 @@ 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 {s} from '#/lib/styles'
import {
POST_TOMBSTONE,
type Shadow,
@@ -49,7 +54,6 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
import {type AppModerationCause} from '#/components/Pills'
import {Embed} from '#/components/Post/Embed'
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
import {PostControls} from '#/components/PostControls'
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
@@ -193,7 +197,6 @@ let FeedItemInner = ({
author: post.author,
embed: post.embed,
moderation,
langs: record.langs,
},
})
}
@@ -445,10 +448,10 @@ let FeedItemInner = ({
/>
{showReplyTo &&
(parentAuthor || isParentBlocked || isParentNotFound) && (
<PostRepliedTo
parentAuthor={parentAuthor}
isParentBlocked={isParentBlocked}
isParentNotFound={isParentNotFound}
<ReplyToLabel
blocked={isParentBlocked}
notFound={isParentNotFound}
profile={parentAuthor}
/>
)}
<LabelsOnMyPost post={post} />
@@ -573,10 +576,80 @@ let PostContent = ({
}
PostContent = memo(PostContent)
function ReplyToLabel({
profile,
blocked,
notFound,
}: {
profile: AppBskyActorDefs.ProfileViewBasic | undefined
blocked?: boolean
notFound?: boolean
}) {
const pal = usePalette('default')
const {currentAccount} = useSession()
let label
if (blocked) {
label = <Trans context="description">Reply to a blocked post</Trans>
} else if (notFound) {
label = <Trans context="description">Reply to a post</Trans>
} else if (profile != null) {
const isMe = profile.did === currentAccount?.did
if (isMe) {
label = <Trans context="description">Reply to you</Trans>
} else {
label = (
<Trans context="description">
Reply to{' '}
<ProfileHoverCard did={profile.did}>
<TextLinkOnWebOnly
type="md"
style={pal.textLight}
lineHeight={1.2}
numberOfLines={1}
href={makeProfileLink(profile)}
text={
<Text emoji type="md" style={pal.textLight} lineHeight={1.2}>
{profile.displayName
? sanitizeDisplayName(profile.displayName)
: sanitizeHandle(profile.handle)}
</Text>
}
/>
</ProfileHoverCard>
</Trans>
)
}
}
if (!label) {
// Should not happen.
return null
}
return (
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
<FontAwesomeIcon
icon="reply"
size={9}
style={[{color: pal.colors.textLight} as FontAwesomeIconStyle, s.mr5]}
/>
<Text
type="md"
style={[pal.textLight, s.mr2]}
lineHeight={1.2}
numberOfLines={1}>
{label}
</Text>
</View>
)
}
const styles = StyleSheet.create({
outer: {
paddingLeft: 10,
paddingRight: 15,
// @ts-ignore web only -prf
cursor: 'pointer',
},
replyLine: {
+39 -29
View File
@@ -1,25 +1,27 @@
import {type StyleProp, type TextStyle} from 'react-native'
import {type AppBskyActorGetProfile} from '@atproto/api'
import {StyleProp, StyleSheet, TextStyle} from 'react-native'
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {TypographyVariant} from '#/lib/ThemeContext'
import {STALE} from '#/state/queries'
import {useProfileQuery} from '#/state/queries/profile'
import {atoms as a} from '#/alf'
import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography'
import {TextLinkOnWebOnly} from './Link'
import {LoadingPlaceholder} from './LoadingPlaceholder'
import {Text} from './text/Text'
export function UserInfoText({
type = 'md',
did,
attr,
failed,
prefix,
style,
}: {
type?: TypographyVariant
did: string
attr?: keyof AppBskyActorGetProfile.OutputSchema
attr?: keyof GetProfile.OutputSchema
loading?: string
failed?: string
prefix?: string
@@ -33,37 +35,45 @@ export function UserInfoText({
staleTime: STALE.INFINITY,
})
let inner
if (isError) {
return (
<Text style={style} numberOfLines={1}>
inner = (
<Text type={type} style={style} numberOfLines={1}>
{failed}
</Text>
)
} else if (profile) {
const text = `${prefix || ''}${sanitizeDisplayName(
typeof profile[attr] === 'string' && profile[attr]
? (profile[attr] as string)
: sanitizeHandle(profile.handle),
)}`
return (
<InlineLinkText
label={text}
inner = (
<TextLinkOnWebOnly
type={type}
style={style}
lineHeight={1.2}
numberOfLines={1}
to={makeProfileLink(profile)}>
<Text emoji style={style}>
{text}
</Text>
</InlineLinkText>
href={makeProfileLink(profile)}
text={
<Text emoji type={type} style={style} lineHeight={1.2}>
{`${prefix || ''}${sanitizeDisplayName(
typeof profile[attr] === 'string' && profile[attr]
? (profile[attr] as string)
: sanitizeHandle(profile.handle),
)}`}
</Text>
}
/>
)
} else {
inner = (
<LoadingPlaceholder
width={80}
height={8}
style={styles.loadingPlaceholder}
/>
)
}
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
return (
<LoadingPlaceholder
width={80}
height={8}
style={[a.relative, {top: 1, left: 2}]}
/>
)
return inner
}
const styles = StyleSheet.create({
loadingPlaceholder: {position: 'relative', top: 1, left: 2},
})