tangled
alpha
login
or
join now
hatlink.bsky.social
/
witchsky.app
forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
0
fork
atom
overview
issues
pulls
pipelines
Compare changes
Choose any two refs to compare.
base:
steam-validate
readme-update
post-text-option
main
linkat-integration
feat/tealfm
no tags found
compare:
steam-validate
readme-update
post-text-option
main
linkat-integration
feat/tealfm
no tags found
go
+146
-12
7 changed files
expand all
collapse all
unified
split
assets
icons
tealfm.svg
src
components
icons
Tealfm.tsx
screens
Profile
Header
ProfileHeaderStandard.tsx
index.tsx
state
cache
profile-shadow.ts
queries
profile.ts
view
screens
Profile.tsx
+1
assets/icons/tealfm.svg
···
1
1
+
<svg xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 80 80"><g transform="translate(0 -3)"><circle cx="40" cy="43" r="40" fill="#14B8A6"/><path fill="#fff" fill-rule="nonzero" d="M12.92 48.284c1.105 0 2.065.384 2.833 1.152s1.152 1.68 1.152 2.784-.384 2.064-1.152 2.832-1.728 1.152-2.832 1.152-2.064-.384-2.784-1.152c-.768-.816-1.152-1.776-1.152-2.832s.384-1.968 1.152-2.784c.768-.768 1.68-1.152 2.784-1.152ZM30.843 37.34l-3.12 13.584c-.48 2.16-1.152 4.416-1.968 6.768-1.248 3.456-2.689 6.048-4.369 7.68-1.488 1.488-2.976 2.208-4.463 2.208-.816 0-1.44-.24-1.872-.672-.385-.384-.528-.912-.528-1.536 0-.912.335-1.68 1.056-2.16.287-.192.624-.336 1.008-.336.576 0 1.152.384 1.776 1.2.335.384.576.576.816.576.192 0 .384-.192.624-.624s.575-1.296.912-2.688c.48-1.584.816-3.024 1.055-4.272l3.552-19.728H21.82c-.433 0-.624-.096-.624-.288 0-.048.047-.288.143-.624l.192-.72c.048-.288.145-.432.24-.48.049-.048.24-.048.672-.048h3.264c.145-.72.24-1.2.24-1.392.384-2.16.624-3.552.768-4.128.145-.624.385-1.344.72-2.208 2.305-6 5.665-9.024 10.128-9.024 1.68 0 2.928.432 3.84 1.296q.864.864.864 1.872c0 .576-.191 1.104-.672 1.536-.431.432-.911.672-1.488.672-.432 0-.768-.144-1.104-.384-.288-.192-.623-.672-.96-1.392-.48-1.008-1.008-1.488-1.632-1.488-.335 0-.816.192-1.343.624-.577.384-.96.96-1.297 1.776-.768 2.112-1.488 5.28-2.112 9.408-.096.528-.24 1.488-.431 2.832h4.32c.431 0 .624.048.624.192v.192c-.048.144-.096.24-.096.288l-.193.768c-.047.384-.143.576-.191.624-.097.048-.288.096-.672.096h-4.176ZM36.79 40.844l-1.103-.672a17.4 17.4 0 0 1 2.112-3.552c1.584-1.92 3.12-2.928 4.656-2.928 1.584 0 2.4.96 2.4 2.784 0 1.056-.24 2.496-.672 4.368.336-.384 1.008-1.296 2.112-2.736s2.448-2.592 4.08-3.504c1.056-.576 2.16-.912 3.36-.912.96 0 1.728.288 2.4.816.672.576 1.056 1.344 1.056 2.352 0 .288-.192 1.44-.624 3.456.288-.288 1.296-1.392 3.072-3.264.96-1.056 2.16-1.92 3.552-2.592a9.2 9.2 0 0 1 3.648-.768c.864 0 1.536.288 2.112.816.672.624 1.008 1.44 1.008 2.4q0 2.016-2.304 7.776c-1.296 3.168-1.92 5.232-1.92 6.144 0 .768.288 1.104.912 1.104q1.008 0 3.312-3.168l1.056 1.104c-3.12 4.224-5.952 6.384-8.352 6.384-.864 0-1.488-.288-1.968-.864-.48-.48-.672-1.104-.672-1.872 0-1.152.816-3.648 2.4-7.536 1.632-4.032 2.448-6.48 2.448-7.488q0-.648-.432-1.008c-.336-.288-.672-.432-1.104-.432-.96 0-2.16.72-3.6 2.112-1.632 1.632-3.168 4.128-4.656 7.584-1.104 2.64-1.968 4.896-2.592 6.72q-.648 1.944-.864 2.16c-.24.24-.72.336-1.392.336-1.056 0-1.92-.096-2.496-.336-.288-.144-.432-.288-.432-.528 0-.192.096-.528.24-.96l3.6-10.272c.768-2.304 1.2-3.936 1.2-4.944 0-.528-.144-.96-.48-1.296-.288-.384-.72-.576-1.2-.576-.912 0-1.92.576-2.928 1.776-1.344 1.488-2.88 4.272-4.656 8.448-.096.24-.72 1.824-1.824 4.656-.624 1.584-.96 2.448-1.008 2.64-.192.624-.48 1.008-.72 1.152-.288.192-.72.24-1.296.24-1.824 0-2.784-.336-2.784-1.056q0-.576.288-1.296l3.024-7.68c.24-.528.624-1.776 1.248-3.792.336-1.056.528-1.776.528-2.208 0-.384-.096-.672-.288-.912a.92.92 0 0 0-.768-.432c-.864 0-1.776.768-2.688 2.256Z"/></g></svg>
+42
src/components/icons/Tealfm.tsx
···
1
1
+
import Svg, {Path} from 'react-native-svg'
2
2
+
3
3
+
import {type Props, useCommonSVGProps} from './common'
4
4
+
import {createSinglePathSVG} from './TEMPLATE'
5
5
+
6
6
+
export const Mark = createSinglePathSVG({
7
7
+
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',
8
8
+
})
9
9
+
10
10
+
export function Full(
11
11
+
props: Omit<Props, 'fill' | 'size' | 'height'> & {
12
12
+
markFill?: Props['fill']
13
13
+
textFill?: Props['fill']
14
14
+
},
15
15
+
) {
16
16
+
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
17
17
+
const ratio = 123 / 555
18
18
+
19
19
+
return (
20
20
+
<Svg
21
21
+
fill="none"
22
22
+
{...rest}
23
23
+
viewBox="0 0 555 123"
24
24
+
width={size}
25
25
+
height={size * ratio}
26
26
+
style={[style]}>
27
27
+
{gradient}
28
28
+
<Path
29
29
+
fill={props.markFill ?? fill}
30
30
+
fillRule="evenodd"
31
31
+
clipRule="evenodd"
32
32
+
d="M101.821 7.673C112.575-.367 130-6.589 130 13.21c0 3.953-2.276 33.214-3.611 37.965-4.641 16.516-21.549 20.729-36.591 18.179 26.292 4.457 32.979 19.218 18.535 33.98-27.433 28.035-39.428-7.034-42.502-16.02-.563-1.647-.827-2.418-.831-1.763-.004-.655-.268.116-.831 1.763-3.074 8.986-15.07 44.055-42.502 16.02C7.223 88.571 13.91 73.81 40.202 69.353c-15.041 2.55-31.95-1.663-36.59-18.179C2.275 46.424 0 17.162 0 13.21 0-6.59 17.426-.368 28.18 7.673 43.084 18.817 59.114 41.413 65 53.54c5.886-12.125 21.917-34.722 36.821-45.866Z"
33
33
+
/>
34
34
+
<Path
35
35
+
fill={props.textFill ?? fill}
36
36
+
fillRule="evenodd"
37
37
+
clipRule="evenodd"
38
38
+
d="m454.459 63.823 24.128-25.056h32.638l4.825 15.104c3.561 11.357 6.664 22.598 9.422 33.72 2.527-9.6 5.744-20.84 9.536-33.603l4.826-15.221H555l-22.864 65.335c-2.413 6.673-5.4 11.475-9.192 14.168-3.791 2.693-9.192 3.98-16.315 3.98-2.413 0-4.481-.117-6.319-.352v-11.59h5.514c6.549 0 9.767-4.099 9.767-9.719 0-2.81-.92-6.908-2.758-12.177l-17.177-49.478-22.239 22.665L497.2 99.184h-16.545l-17.234-28.101-8.962 9.133v18.968h-14.246V15.817h14.246v48.006Zm-48.373-26.46c16.889 0 25.622 6.79 26.196 20.49h-13.673c-.344-7.377-4.595-9.954-12.523-9.954-6.894 0-10.341 2.342-10.341 7.026 0 4.215 2.987 6.089 9.881 7.377l7.469 1.17c14.361 2.694 20.566 8.08 20.566 18.384 0 12.176-9.652 18.967-26.311 18.967-17.235 0-26.311-6.908-27.116-20.842h14.132c.804 7.494 4.481 10.304 13.213 10.304 7.813 0 11.72-2.459 11.72-7.26 0-4.332-2.758-6.44-11.605-7.962l-6.778-1.17c-12.983-2.224-19.418-8.313-19.418-18.265 0-11.358 8.847-18.266 24.588-18.266ZM270.534 76.351c0 7.61 3.677 11.474 11.145 11.474 7.008 0 13.212-5.268 13.213-15.22v-33.84h14.476v60.418h-14.016v-8.782c-4.481 6.791-10.686 10.187-18.614 10.187-12.523 0-20.68-7.728-20.68-21.778V38.767h14.476v37.585Zm75.432-38.99c8.961 0 16.085 3.045 21.37 9.016s7.928 13.933 7.928 23.651v3.513h-44.35c1.034 10.42 6.664 15.572 15.396 15.572 6.663 0 11.144-2.927 13.557-8.664h13.903c-3.103 12.294-13.443 20.139-27.575 20.139-8.847 0-15.971-2.927-21.371-8.664-5.4-5.737-8.157-13.348-8.157-22.95 0-9.483 2.643-17.094 8.043-22.949 5.4-5.737 12.409-8.664 21.256-8.664ZM195.628 15.817c17.809 0 26.426 9.251 26.426 21.545 0 8.196-3.677 14.168-10.915 17.914 9.306 3.396 14.247 11.24 14.247 20.022 0 14.87-9.767 23.886-28.494 23.886h-38.26V15.817h36.996Zm51.264 83.367h-14.477V15.817h14.477v83.367ZM174.143 86.07h21.944c8.732 0 13.443-4.098 13.443-11.474 0-7.728-4.481-11.592-13.443-11.592h-21.944V86.07Zm171.708-37.233c-7.928 0-13.443 4.683-14.822 14.401h29.758c-1.264-8.781-6.549-14.401-14.936-14.401Zm-171.708 1.756h20.336c7.927 0 12.178-4.215 12.178-11.24 0-6.44-4.366-10.539-12.178-10.539h-20.336v21.779Z"
39
39
+
/>
40
40
+
</Svg>
41
41
+
)
42
42
+
}
+30
-3
src/screens/Profile/Header/ProfileHeaderStandard.tsx
···
1
1
import {memo, useMemo, useState} from 'react'
2
2
import {View} from 'react-native'
3
3
+
import {Image} from 'expo-image'
3
4
import {
4
5
type AppBskyActorDefs,
5
6
moderateProfile,
···
18
19
import {type Shadow, useProfileShadow} from '#/state/cache/profile-shadow'
19
20
import {useDisableFollowedByMetrics} from '#/state/preferences/disable-followed-by-metrics'
20
21
import {
22
22
+
type TealActorStatus,
21
23
useProfileBlockMutationQueue,
22
24
useProfileFollowMutationQueue,
23
25
} from '#/state/queries/profile'
···
30
32
import {useDialogControl} from '#/components/Dialog'
31
33
import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
32
34
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
35
35
+
import {tealfm} from '#/components/icons/tealfm'
33
36
import {
34
37
KnownFollowers,
35
38
shouldShowKnownFollowers,
···
47
50
import {AnimatedProfileHeaderSuggestedFollows} from './SuggestedFollows'
48
51
49
52
interface Props {
50
50
-
profile: AppBskyActorDefs.ProfileViewDetailed
53
53
+
profile: AppBskyActorDefs.ProfileViewDetailed & {
54
54
+
tealStatus: TealActorStatus | undefined
55
55
+
}
51
56
descriptionRT: RichTextAPI | null
52
57
moderationOpts: ModerationOpts
53
58
hideBackButton?: boolean
···
63
68
}: Props): React.ReactNode => {
64
69
const t = useTheme()
65
70
const {gtMobile} = useBreakpoints()
66
66
-
const profile =
67
67
-
useProfileShadow<AppBskyActorDefs.ProfileViewDetailed>(profileUnshadowed)
71
71
+
const profile = useProfileShadow<
72
72
+
AppBskyActorDefs.ProfileViewDetailed & {
73
73
+
tealStatus: TealActorStatus | undefined
74
74
+
}
75
75
+
>(profileUnshadowed)
68
76
const {currentAccount} = useSession()
69
77
const {_} = useLingui()
70
78
const moderation = useMemo(
···
167
175
</View>
168
176
) : undefined}
169
177
178
178
+
{profile.tealStatus && (
179
179
+
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
180
180
+
<Image
181
181
+
source={{
182
182
+
uri: `https://coverartarchive.org/release/${profile.tealStatus.item.releaseMbId}/front-250`,
183
183
+
}}
184
184
+
style={[{width: 30, height: 30, borderRadius: 4}]}
185
185
+
loading={'eager'}
186
186
+
accessibilityIgnoresInvertColors
187
187
+
/>
188
188
+
<Text>
189
189
+
Listening to {profile.tealStatus.item.trackName} by{' '}
190
190
+
{profile.tealStatus.item.artists
191
191
+
.map(artist => artist.artistName)
192
192
+
.join(', ')}
193
193
+
</Text>
194
194
+
</View>
195
195
+
)}
196
196
+
170
197
{!isMe &&
171
198
!disableFollowedByMetrics &&
172
199
!isBlockedUser &&
+7
-2
src/screens/Profile/Header/index.tsx
···
19
19
import {sanitizeHandle} from '#/lib/strings/handles'
20
20
import {useProfileShadow} from '#/state/cache/profile-shadow'
21
21
import {useModerationOpts} from '#/state/preferences/moderation-opts'
22
22
+
import {type TealActorStatus} from '#/state/queries/profile'
22
23
import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
23
24
import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
24
25
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
···
60
61
export {ProfileHeaderLoading}
61
62
62
63
interface Props {
63
63
-
profile: AppBskyActorDefs.ProfileViewDetailed
64
64
+
profile: AppBskyActorDefs.ProfileViewDetailed & {
65
65
+
tealStatus: TealActorStatus | undefined
66
66
+
}
64
67
labeler: AppBskyLabelerDefs.LabelerViewDetailed | undefined
65
68
descriptionRT: RichTextAPI | null
66
69
moderationOpts: ModerationOpts
···
105
108
hideBackButton = false,
106
109
}: {
107
110
onLayout: (e: LayoutChangeEvent) => void
108
108
-
profile: AppBskyActorDefs.ProfileViewDetailed
111
111
+
profile: AppBskyActorDefs.ProfileViewDetailed & {
112
112
+
tealStatus: TealActorStatus | undefined
113
113
+
}
109
114
labeler?: AppBskyLabelerDefs.LabelerViewDetailed
110
115
hideBackButton?: boolean
111
116
}) {
+7
-2
src/state/cache/profile-shadow.ts
···
21
21
import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '#/state/queries/post-liked-by'
22
22
import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '#/state/queries/post-quotes'
23
23
import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '#/state/queries/post-reposted-by'
24
24
-
import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#/state/queries/profile'
24
24
+
import {
25
25
+
findAllProfilesInQueryData as findAllProfilesInProfileQueryData,
26
26
+
type TealActorStatus,
27
27
+
} from '#/state/queries/profile'
25
28
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
26
29
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
27
30
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows'
···
49
52
const emitter = new EventEmitter()
50
53
51
54
export function useProfileShadow<
52
52
-
TProfileView extends bsky.profile.AnyProfileView,
55
55
+
TProfileView extends bsky.profile.AnyProfileView & {
56
56
+
tealStatus: TealActorStatus | undefined
57
57
+
},
53
58
>(profile: TProfileView): Shadow<TProfileView> {
54
59
const [shadow, setShadow] = useState(() => shadows.get(profile))
55
60
const [prevPost, setPrevPost] = useState(profile)
+55
-3
src/state/queries/profile.ts
···
55
55
const RQKEY_ROOT = 'profile'
56
56
export const RQKEY = (did: string) => [RQKEY_ROOT, did]
57
57
58
58
+
export type TealArtist = {
59
59
+
artistName: string
60
60
+
artistMbId?: string
61
61
+
}
62
62
+
63
63
+
export type TealPlayView = {
64
64
+
trackName: string
65
65
+
trackMbId?: string
66
66
+
recordingMbId?: string
67
67
+
duration?: number
68
68
+
artists: TealArtist[]
69
69
+
releaseName?: string
70
70
+
releaseMbId?: string
71
71
+
isrc?: string
72
72
+
originUrl?: string
73
73
+
musicServiceBaseDomain?: string
74
74
+
submissionClientAgent?: string
75
75
+
playedTime?: string // datetime
76
76
+
}
77
77
+
78
78
+
export type TealActorStatus = {
79
79
+
time: string // datetime
80
80
+
expiry?: string // datetime
81
81
+
item: TealPlayView
82
82
+
}
83
83
+
58
84
export const profilesQueryKeyRoot = 'profiles'
59
85
export const profilesQueryKey = (handles: string[]) => [
60
86
profilesQueryKeyRoot,
···
70
96
}) {
71
97
const agent = useAgent()
72
98
const {getUnstableProfile} = useUnstableProfileViewCache()
73
73
-
return useQuery<AppBskyActorDefs.ProfileViewDetailed>({
99
99
+
return useQuery<
100
100
+
AppBskyActorDefs.ProfileViewDetailed & {
101
101
+
tealStatus: TealActorStatus | undefined
102
102
+
}
103
103
+
>({
74
104
// WARNING
75
105
// this staleTime is load-bearing
76
106
// if you remove it, the UI infinite-loops
···
80
110
queryKey: RQKEY(did ?? ''),
81
111
queryFn: async () => {
82
112
const res = await agent.getProfile({actor: did ?? ''})
83
83
-
return res.data
113
113
+
try {
114
114
+
const teal = await fetch(
115
115
+
`https://slingshot.microcosm.blue/xrpc/com.atproto.repo.getRecord?repo=${did ?? ''}&collection=fm.teal.alpha.actor.status&rkey=self`,
116
116
+
{
117
117
+
method: 'GET',
118
118
+
},
119
119
+
)
120
120
+
const tealData = await teal.json()
121
121
+
122
122
+
return {
123
123
+
...res.data,
124
124
+
tealStatus: tealData.value as TealActorStatus | undefined,
125
125
+
}
126
126
+
} catch (e) {
127
127
+
return {
128
128
+
...res.data,
129
129
+
tealStatus: undefined as TealActorStatus | undefined,
130
130
+
}
131
131
+
}
84
132
},
85
133
placeholderData: () => {
86
134
if (!did) return
87
87
-
return getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed
135
135
+
const profile = getUnstableProfile(did) as AppBskyActorDefs.ProfileViewDetailed
136
136
+
return profile ? {
137
137
+
...profile,
138
138
+
tealStatus: undefined,
139
139
+
} : undefined
88
140
},
89
141
enabled: !!did,
90
142
})
+4
-2
src/view/screens/Profile.tsx
···
30
30
import {useModerationOpts} from '#/state/preferences/moderation-opts'
31
31
import {useLabelerInfoQuery} from '#/state/queries/labeler'
32
32
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
33
33
-
import {useProfileQuery} from '#/state/queries/profile'
33
33
+
import {type TealActorStatus, useProfileQuery} from '#/state/queries/profile'
34
34
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
35
35
import {useAgent, useSession} from '#/state/session'
36
36
import {useSetMinimalShellMode} from '#/state/shell'
···
167
167
moderationOpts,
168
168
hideBackButton,
169
169
}: {
170
170
-
profile: AppBskyActorDefs.ProfileViewDetailed
170
170
+
profile: AppBskyActorDefs.ProfileViewDetailed & {
171
171
+
tealStatus: TealActorStatus | undefined
172
172
+
}
171
173
moderationOpts: ModerationOpts
172
174
hideBackButton: boolean
173
175
isPlaceholderProfile: boolean