From d1f401545a19fde40e25ca3b76f12cf754cec596 Mon Sep 17 00:00:00 2001 From: "xan.lol" Date: Sun, 1 Feb 2026 02:18:30 -0800 Subject: [PATCH] tealfm on profiles integration progress shoo unfinished (tealfm.tsx is a LIE that's actually the BLUESKY logo you must convert the TEALFM.SVG --- assets/icons/tealfm.svg | 1 + src/components/icons/Tealfm.tsx | 42 ++++++++++++++ .../Profile/Header/ProfileHeaderStandard.tsx | 33 ++++++++++- src/screens/Profile/Header/index.tsx | 9 ++- src/state/cache/profile-shadow.ts | 9 ++- src/state/queries/profile.ts | 58 ++++++++++++++++++- src/view/screens/Profile.tsx | 6 +- 7 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 assets/icons/tealfm.svg create mode 100644 src/components/icons/Tealfm.tsx diff --git a/assets/icons/tealfm.svg b/assets/icons/tealfm.svg new file mode 100644 index 000000000..ce7354acb --- /dev/null +++ b/assets/icons/tealfm.svg @@ -0,0 +1 @@ + diff --git a/src/components/icons/Tealfm.tsx b/src/components/icons/Tealfm.tsx new file mode 100644 index 000000000..75c5cb420 --- /dev/null +++ b/src/components/icons/Tealfm.tsx @@ -0,0 +1,42 @@ +import Svg, {Path} from 'react-native-svg' + +import {type Props, useCommonSVGProps} from './common' +import {createSinglePathSVG} from './TEMPLATE' + +export const Mark = createSinglePathSVG({ + path: 'M6.335 4.212c2.293 1.76 4.76 5.327 5.665 7.241.906-1.914 3.372-5.482 5.665-7.241C19.319 2.942 22 1.96 22 5.086c0 .624-.35 5.244-.556 5.994-.713 2.608-3.315 3.273-5.629 2.87 4.045.704 5.074 3.035 2.852 5.366-4.22 4.426-6.066-1.111-6.54-2.53-.086-.26-.126-.382-.127-.278 0-.104-.041.018-.128.278-.473 1.419-2.318 6.956-6.539 2.53-2.222-2.331-1.193-4.662 2.852-5.366-2.314.403-4.916-.262-5.63-2.87C2.35 10.33 2 5.71 2 5.086c0-3.126 2.68-2.144 4.335-.874Z', +}) + +export function Full( + props: Omit & { + markFill?: Props['fill'] + textFill?: Props['fill'] + }, +) { + const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props) + const ratio = 123 / 555 + + return ( + + {gradient} + + + + ) +} diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index f02fedeb8..ecceccd40 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -1,5 +1,6 @@ import {memo, useMemo, useState} from 'react' import {View} from 'react-native' +import {Image} from 'expo-image' import { type AppBskyActorDefs, moderateProfile, @@ -18,6 +19,7 @@ import {logger} from '#/logger' import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow' import {useDisableFollowedByMetrics} from '#/state/preferences/disable-followed-by-metrics' import { + type TealActorStatus, useProfileBlockMutationQueue, useProfileFollowMutationQueue, } from '#/state/queries/profile' @@ -30,6 +32,7 @@ import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' import {useDialogControl} from '#/components/Dialog' import {MessageProfileButton} from '#/components/dms/MessageProfileButton' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {tealfm} from '#/components/icons/tealfm' import { KnownFollowers, shouldShowKnownFollowers, @@ -47,7 +50,9 @@ import {ProfileHeaderShell} from './Shell' import {AnimatedProfileHeaderSuggestedFollows} from './SuggestedFollows' interface Props { - profile: AppBskyActorDefs.ProfileViewDetailed + profile: AppBskyActorDefs.ProfileViewDetailed & { + tealStatus: TealActorStatus | undefined + } descriptionRT: RichTextAPI | null moderationOpts: ModerationOpts hideBackButton?: boolean @@ -63,8 +68,11 @@ let ProfileHeaderStandard = ({ }: Props): React.ReactNode => { const t = useTheme() const {gtMobile} = useBreakpoints() - const profile = - useProfileShadow(profileUnshadowed) + const profile = useProfileShadow< + AppBskyActorDefs.ProfileViewDetailed & { + tealStatus: TealActorStatus | undefined + } + >(profileUnshadowed) const {currentAccount} = useSession() const {_} = useLingui() const moderation = useMemo( @@ -167,6 +175,25 @@ let ProfileHeaderStandard = ({ ) : undefined} + {profile.tealStatus && ( + + + + Listening to {profile.tealStatus.item.trackName} by{' '} + {profile.tealStatus.item.artists + .map(artist => artist.artistName) + .join(', ')} + + + )} + {!isMe && !disableFollowedByMetrics && !isBlockedUser && diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index b064ea329..ad21fb59d 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -19,6 +19,7 @@ import {useIsFocused} from '@react-navigation/native' import {sanitizeHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {type TealActorStatus} from '#/state/queries/profile' import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' @@ -60,7 +61,9 @@ ProfileHeaderLoading = memo(ProfileHeaderLoading) export {ProfileHeaderLoading} interface Props { - profile: AppBskyActorDefs.ProfileViewDetailed + profile: AppBskyActorDefs.ProfileViewDetailed & { + tealStatus: TealActorStatus | undefined + } labeler: AppBskyLabelerDefs.LabelerViewDetailed | undefined descriptionRT: RichTextAPI | null moderationOpts: ModerationOpts @@ -105,7 +108,9 @@ const MinimalHeader = memo(function MinimalHeader({ hideBackButton = false, }: { onLayout: (e: LayoutChangeEvent) => void - profile: AppBskyActorDefs.ProfileViewDetailed + profile: AppBskyActorDefs.ProfileViewDetailed & { + tealStatus: TealActorStatus | undefined + } labeler?: AppBskyLabelerDefs.LabelerViewDetailed hideBackButton?: boolean }) { diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts index 5063d943b..9c42deffa 100644 --- a/src/state/cache/profile-shadow.ts +++ b/src/state/cache/profile-shadow.ts @@ -21,7 +21,10 @@ import { import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by' import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes' import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by' -import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile' +import { + findAllProfilesInQueryData as findAllProfilesInProfileQueryData, + type TealActorStatus, +} from '#/state/queries/profile' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows' @@ -49,7 +52,9 @@ const shadows: WeakMap< const emitter = new EventEmitter() export function useProfileShadow< - TProfileView extends bsky.profile.AnyProfileView, + TProfileView extends bsky.profile.AnyProfileView & { + tealStatus: TealActorStatus | undefined + }, >(profile: TProfileView): Shadow { const [shadow, setShadow] = useState(() => shadows.get(profile)) const [prevPost, setPrevPost] = useState(profile) diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 3dcfc46f5..4d6fcea8f 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -55,6 +55,32 @@ export const precacheProfile = unstableCacheProfileView const RQKEY_ROOT = 'profile' export const RQKEY = (did: string) => [RQKEY_ROOT, did] +export type TealArtist = { + artistName: string + artistMbId?: string +} + +export type TealPlayView = { + trackName: string + trackMbId?: string + recordingMbId?: string + duration?: number + artists: TealArtist[] + releaseName?: string + releaseMbId?: string + isrc?: string + originUrl?: string + musicServiceBaseDomain?: string + submissionClientAgent?: string + playedTime?: string // datetime +} + +export type TealActorStatus = { + time: string // datetime + expiry?: string // datetime + item: TealPlayView +} + export const profilesQueryKeyRoot = 'profiles' export const profilesQueryKey = (handles: string[]) => [ profilesQueryKeyRoot, @@ -70,7 +96,11 @@ export function useProfileQuery({ }) { const agent = useAgent() const {getUnstableProfile} = useUnstableProfileViewCache() - return useQuery({ + return useQuery< + AppBskyActorDefs.ProfileViewDetailed & { + tealStatus: TealActorStatus | undefined + } + >({ // WARNING // this staleTime is load-bearing // if you remove it, the UI infinite-loops @@ -80,11 +110,33 @@ export function useProfileQuery({ queryKey: RQKEY(did ?? ''), queryFn: async () => { const res = await agent.getProfile({actor: did ?? ''}) - return res.data + try { + const teal = await fetch( + `https://slingshot.microcosm.blue/xrpc/com.atproto.repo.getRecord?repo=${did ?? ''}&collection=fm.teal.alpha.actor.status&rkey=self`, + { + method: 'GET', + }, + ) + const tealData = await teal.json() + + return { + ...res.data, + tealStatus: tealData.value as TealActorStatus | undefined, + } + } catch (e) { + return { + ...res.data, + tealStatus: undefined as TealActorStatus | undefined, + } + } }, placeholderData: () => { if (!did) return - return getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed + const profile = getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed + return profile ? { + ...profile, + tealStatus: undefined, + } : undefined }, enabled: !!did, }) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 5bb46f0d2..01936aaa4 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -30,7 +30,7 @@ import {listenSoftReset} from '#/state/events' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useLabelerInfoQuery} from '#/state/queries/labeler' import {resetProfilePostsQueries} from '#/state/queries/post-feed' -import {useProfileQuery} from '#/state/queries/profile' +import {type TealActorStatus, useProfileQuery} from '#/state/queries/profile' import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useAgent, useSession} from '#/state/session' import {useSetMinimalShellMode} from '#/state/shell' @@ -167,7 +167,9 @@ function ProfileScreenLoaded({ moderationOpts, hideBackButton, }: { - profile: AppBskyActorDefs.ProfileViewDetailed + profile: AppBskyActorDefs.ProfileViewDetailed & { + tealStatus: TealActorStatus | undefined + } moderationOpts: ModerationOpts hideBackButton: boolean isPlaceholderProfile: boolean -- knotmirror 0.0.1