This commit is contained in:
Samuel Newman
2025-05-10 00:06:06 +03:00
committed by GitHub
parent 2e80fa3dac
commit a0bd804262
45 changed files with 2021 additions and 451 deletions
+4
View File
@@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import type React from 'react'
import {useActorStatus} from '#/lib/actor-status'
import {makeProfileLink} from '#/lib/routes/links'
import {forceLTR} from '#/lib/strings/bidi'
import {NON_BREAKING_SPACE} from '#/lib/strings/constants'
@@ -55,6 +56,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
const timestampLabel = niceDate(i18n, opts.timestamp)
const verification = useSimpleVerificationState({profile: author})
const {isActive: live} = useActorStatus(author)
return (
<View
@@ -74,6 +76,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
profile={author}
moderation={opts.moderation?.ui('avatar')}
type={author.associated?.labeler ? 'labeler' : 'user'}
live={live}
hideLiveBadge
/>
</View>
)}
+75 -13
View File
@@ -14,6 +14,9 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useActorStatus} from '#/lib/actor-status'
import {isTouchDevice} from '#/lib/browser'
import {useHaptics} from '#/lib/haptics'
import {
useCameraPermission,
usePhotoLibraryPermission,
@@ -22,6 +25,8 @@ import {compressIfNeeded} from '#/lib/media/manip'
import {openCamera, openCropper, openPicker} from '#/lib/media/picker'
import {type PickerImage} from '#/lib/media/picker.shared'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {isAndroid, isNative, isWeb} from '#/platform/detection'
import {
@@ -33,6 +38,7 @@ import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog'
import {HighPriorityImage} from '#/view/com/util/images/Image'
import {atoms as a, tokens, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper'
import {
@@ -42,6 +48,8 @@ import {
import {StreamingLive_Stroke2_Corner0_Rounded as LibraryIcon} from '#/components/icons/StreamingLive'
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
import {Link} from '#/components/Link'
import {LiveIndicator} from '#/components/live/LiveIndicator'
import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import * as Menu from '#/components/Menu'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -54,6 +62,8 @@ interface BaseUserAvatarProps {
shape?: 'circle' | 'square'
size: number
avatar?: string | null
live?: boolean
hideLiveBadge?: boolean
}
interface UserAvatarProps extends BaseUserAvatarProps {
@@ -196,27 +206,38 @@ let UserAvatar = ({
usePlainRNImage = false,
onLoad,
style,
live,
hideLiveBadge,
}: UserAvatarProps): React.ReactNode => {
const t = useTheme()
const backgroundColor = t.palette.contrast_25
const finalShape = overrideShape ?? (type === 'user' ? 'circle' : 'square')
const aviStyle = useMemo(() => {
let borderRadius
if (finalShape === 'square') {
return {
width: size,
height: size,
borderRadius: size > 32 ? 8 : 3,
backgroundColor,
}
borderRadius = size > 32 ? 8 : 3
} else {
borderRadius = Math.floor(size / 2)
}
return {
width: size,
height: size,
borderRadius: Math.floor(size / 2),
backgroundColor,
borderRadius,
backgroundColor: t.palette.contrast_25,
}
}, [finalShape, size, backgroundColor])
}, [finalShape, size, t])
const borderStyle = useMemo(() => {
return [
{borderRadius: aviStyle.borderRadius},
live && {
borderColor: t.palette.negative_500,
borderWidth: size > 16 ? 2 : 1,
opacity: 1,
},
]
}, [aviStyle.borderRadius, live, t, size])
const alert = useMemo(() => {
if (!moderation?.alert) {
@@ -277,12 +298,19 @@ let UserAvatar = ({
onLoad={onLoad}
/>
)}
<MediaInsetBorder style={[{borderRadius: aviStyle.borderRadius}]} />
<MediaInsetBorder style={borderStyle} />
{live && size > 16 && !hideLiveBadge && (
<LiveIndicator size={size > 32 ? 'small' : 'tiny'} />
)}
{alert}
</View>
) : (
<View style={containerStyle}>
<DefaultAvatar type={type} shape={finalShape} size={size} />
<MediaInsetBorder style={borderStyle} />
{live && size > 16 && !hideLiveBadge && (
<LiveIndicator size={size > 32 ? 'small' : 'tiny'} />
)}
{alert}
</View>
)
@@ -486,21 +514,32 @@ let PreviewableUserAvatar = ({
disableHoverCard,
disableNavigation,
onBeforePress,
live,
...rest
}: PreviewableUserAvatarProps): React.ReactNode => {
const {_} = useLingui()
const queryClient = useQueryClient()
const status = useActorStatus(profile)
const liveControl = useDialogControl()
const playHaptic = useHaptics()
const onPress = React.useCallback(() => {
const onPress = useCallback(() => {
onBeforePress?.()
unstableCacheProfileView(queryClient, profile)
}, [profile, queryClient, onBeforePress])
const onOpenLiveStatus = useCallback(() => {
playHaptic('Light')
logger.metric('live:card:open', {subject: profile.did, from: 'post'})
liveControl.open()
}, [liveControl, playHaptic, profile.did])
const avatarEl = (
<UserAvatar
avatar={profile.avatar}
moderation={moderation}
type={profile.associated?.labeler ? 'labeler' : 'user'}
live={status.isActive || live}
{...rest}
/>
)
@@ -509,9 +548,32 @@ let PreviewableUserAvatar = ({
<ProfileHoverCard did={profile.did} disable={disableHoverCard}>
{disableNavigation ? (
avatarEl
) : status.isActive && (isNative || isTouchDevice) ? (
<>
<Button
label={_(
msg`${sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
)}'s avatar`,
)}
accessibilityHint={_(msg`Opens live status dialog`)}
onPress={onOpenLiveStatus}>
{avatarEl}
</Button>
<LiveStatusDialog
control={liveControl}
profile={profile}
status={status}
embed={status.embed}
/>
</>
) : (
<Link
label={_(msg`${profile.displayName || profile.handle}'s avatar`)}
label={_(
msg`${sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
)}'s avatar`,
)}
accessibilityHint={_(msg`Opens this profile`)}
to={makeProfileLink({
did: profile.did,