Compare commits
No commits in common. "026f23ae2a0557b24664666f600f83bfa503d878" and "4ac1141c8a25ca019aab1fc71e3bc53f57dabe37" have entirely different histories.
026f23ae2a
...
4ac1141c8a
|
@ -372,7 +372,7 @@ function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full overflow-hidden rounded-lg aspect-square relative"
|
className="w-full overflow-hidden rounded-lg aspect-square"
|
||||||
style={{aspectRatio: `${aspectRatio} / 1`}}>
|
style={{aspectRatio: `${aspectRatio} / 1`}}>
|
||||||
<img
|
<img
|
||||||
src={content.thumbnail}
|
src={content.thumbnail}
|
||||||
|
|
|
@ -259,6 +259,7 @@
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
</style>
|
||||||
{% include "scripts.html" %}
|
{% include "scripts.html" %}
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "bsky.app",
|
"name": "bsky.app",
|
||||||
"version": "1.91.0-zio24257",
|
"version": "1.91.0-zio24255",
|
||||||
"private": true,
|
"private": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.13.7",
|
"@atproto/api": "0.13.5",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
|
|
|
@ -60,13 +60,16 @@ function CardOuter({
|
||||||
export function SuggestedFollowPlaceholder() {
|
export function SuggestedFollowPlaceholder() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
|
<CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}>
|
||||||
<ProfileCard.Header>
|
<ProfileCard.Header>
|
||||||
<ProfileCard.AvatarPlaceholder />
|
<ProfileCard.AvatarPlaceholder />
|
||||||
<ProfileCard.NameAndHandlePlaceholder />
|
|
||||||
</ProfileCard.Header>
|
</ProfileCard.Header>
|
||||||
|
|
||||||
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
|
<View style={[a.py_xs]}>
|
||||||
|
<ProfileCard.NameAndHandlePlaceholder />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ProfileCard.DescriptionPlaceholder />
|
||||||
</CardOuter>
|
</CardOuter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -173,14 +176,9 @@ function useExperimentalSuggestedUsersQuery() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
|
export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
|
||||||
const {currentAccount} = useSession()
|
const [feedType, feedUri] = feed.split('|')
|
||||||
const [feedType, feedUriOrDid] = feed.split('|')
|
|
||||||
if (feedType === 'author') {
|
if (feedType === 'author') {
|
||||||
if (currentAccount?.did === feedUriOrDid) {
|
return <SuggestedFollowsProfile did={feedUri} />
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
return <SuggestedFollowsProfile did={feedUriOrDid} />
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return <SuggestedFollowsHome />
|
return <SuggestedFollowsHome />
|
||||||
}
|
}
|
||||||
|
@ -199,7 +197,6 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
|
||||||
isSuggestionsLoading={isSuggestionsLoading}
|
isSuggestionsLoading={isSuggestionsLoading}
|
||||||
profiles={data?.suggestions ?? []}
|
profiles={data?.suggestions ?? []}
|
||||||
error={error}
|
error={error}
|
||||||
viewContext="profile"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -215,7 +212,6 @@ export function SuggestedFollowsHome() {
|
||||||
isSuggestionsLoading={isSuggestionsLoading}
|
isSuggestionsLoading={isSuggestionsLoading}
|
||||||
profiles={profiles}
|
profiles={profiles}
|
||||||
error={error}
|
error={error}
|
||||||
viewContext="feed"
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -224,12 +220,10 @@ export function ProfileGrid({
|
||||||
isSuggestionsLoading,
|
isSuggestionsLoading,
|
||||||
error,
|
error,
|
||||||
profiles,
|
profiles,
|
||||||
viewContext = 'feed',
|
|
||||||
}: {
|
}: {
|
||||||
isSuggestionsLoading: boolean
|
isSuggestionsLoading: boolean
|
||||||
profiles: AppBskyActorDefs.ProfileViewDetailed[]
|
profiles: AppBskyActorDefs.ProfileViewDetailed[]
|
||||||
error: Error | null
|
error: Error | null
|
||||||
viewContext: 'profile' | 'feed'
|
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
@ -286,7 +280,7 @@ export function ProfileGrid({
|
||||||
shape="round"
|
shape="round"
|
||||||
/>
|
/>
|
||||||
</ProfileCard.Header>
|
</ProfileCard.Header>
|
||||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
<ProfileCard.Description profile={profile} />
|
||||||
</ProfileCard.Outer>
|
</ProfileCard.Outer>
|
||||||
</CardOuter>
|
</CardOuter>
|
||||||
)}
|
)}
|
||||||
|
@ -303,31 +297,33 @@ export function ProfileGrid({
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
|
style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}>
|
||||||
<View
|
<View style={[a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs]}>
|
||||||
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.p_lg,
|
a.flex_1,
|
||||||
a.pb_xs,
|
a.text_lg,
|
||||||
a.flex_row,
|
a.font_bold,
|
||||||
a.align_center,
|
t.atoms.text_contrast_medium,
|
||||||
a.justify_between,
|
|
||||||
]}>
|
]}>
|
||||||
<Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
|
|
||||||
{viewContext === 'profile' ? (
|
|
||||||
<Trans>Similar accounts</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>Suggested for you</Trans>
|
<Trans>Suggested for you</Trans>
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Person fill={t.atoms.text_contrast_low.color} size="sm" />
|
<Person fill={t.atoms.text_contrast_low.color} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{gtMobile ? (
|
{gtMobile ? (
|
||||||
<View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}>
|
<View style={[a.flex_1, a.px_lg, a.pt_md, a.pb_xl, a.gap_md]}>
|
||||||
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}>
|
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
|
||||||
{content}
|
{content}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_end,
|
||||||
|
a.align_center,
|
||||||
|
a.pt_xs,
|
||||||
|
a.gap_md,
|
||||||
|
]}>
|
||||||
<InlineLinkText
|
<InlineLinkText
|
||||||
label={_(msg`Browse more suggestions`)}
|
label={_(msg`Browse more suggestions`)}
|
||||||
to="/search"
|
to="/search"
|
||||||
|
@ -343,7 +339,7 @@ export function ProfileGrid({
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||||
decelerationRate="fast">
|
decelerationRate="fast">
|
||||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
<View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}>
|
||||||
{content}
|
{content}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -220,10 +220,8 @@ export function NameAndHandlePlaceholder() {
|
||||||
|
|
||||||
export function Description({
|
export function Description({
|
||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
numberOfLines = 3,
|
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
||||||
numberOfLines?: number
|
|
||||||
}) {
|
}) {
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
const {description} = profile
|
const {description} = profile
|
||||||
|
@ -246,34 +244,31 @@ export function Description({
|
||||||
<RichText
|
<RichText
|
||||||
value={rt}
|
value={rt}
|
||||||
style={[a.leading_snug]}
|
style={[a.leading_snug]}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={3}
|
||||||
disableLinks
|
disableLinks
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DescriptionPlaceholder({
|
export function DescriptionPlaceholder() {
|
||||||
numberOfLines = 3,
|
|
||||||
}: {
|
|
||||||
numberOfLines?: number
|
|
||||||
}) {
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View style={[{gap: 8}]}>
|
<View style={[a.gap_xs]}>
|
||||||
{Array(numberOfLines)
|
<View
|
||||||
.fill(0)
|
style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]}
|
||||||
.map((_, i) => (
|
/>
|
||||||
|
<View
|
||||||
|
style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]}
|
||||||
|
/>
|
||||||
<View
|
<View
|
||||||
key={i}
|
|
||||||
style={[
|
style={[
|
||||||
a.rounded_xs,
|
a.rounded_xs,
|
||||||
a.w_full,
|
a.w_full,
|
||||||
t.atoms.bg_contrast_50,
|
t.atoms.bg_contrast_50,
|
||||||
{height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'},
|
{height: 12, width: 100},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -379,11 +379,7 @@ export class FeedTuner {
|
||||||
): FeedViewPostsSlice[] => {
|
): FeedViewPostsSlice[] => {
|
||||||
for (let i = 0; i < slices.length; i++) {
|
for (let i = 0; i < slices.length; i++) {
|
||||||
const slice = slices[i]
|
const slice = slices[i]
|
||||||
if (
|
if (slice.isReply && !shouldDisplayReplyInFollowing(slice, userDid)) {
|
||||||
slice.isReply &&
|
|
||||||
!slice.isRepost &&
|
|
||||||
!shouldDisplayReplyInFollowing(slice.getAuthors(), userDid)
|
|
||||||
) {
|
|
||||||
slices.splice(i, 1)
|
slices.splice(i, 1)
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
|
@ -447,9 +443,13 @@ function areSameAuthor(authors: AuthorContext): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldDisplayReplyInFollowing(
|
function shouldDisplayReplyInFollowing(
|
||||||
authors: AuthorContext,
|
slice: FeedViewPostsSlice,
|
||||||
userDid: string,
|
userDid: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (slice.isRepost) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const authors = slice.getAuthors()
|
||||||
const {author, parentAuthor, grandparentAuthor, rootAuthor} = authors
|
const {author, parentAuthor, grandparentAuthor, rootAuthor} = authors
|
||||||
if (!isSelfOrFollowing(author, userDid)) {
|
if (!isSelfOrFollowing(author, userDid)) {
|
||||||
// Only show replies from self or people you follow.
|
// Only show replies from self or people you follow.
|
||||||
|
@ -463,6 +463,21 @@ function shouldDisplayReplyInFollowing(
|
||||||
// Always show self-threads.
|
// Always show self-threads.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
parentAuthor &&
|
||||||
|
parentAuthor.did !== author.did &&
|
||||||
|
rootAuthor &&
|
||||||
|
rootAuthor.did === author.did &&
|
||||||
|
slice.items.length > 2
|
||||||
|
) {
|
||||||
|
// If you follow A, show A -> someone[>0 likes] -> A chains too.
|
||||||
|
// This is different from cases below because you only know one person.
|
||||||
|
const parentPost = slice.items[1].post
|
||||||
|
const parentLikeCount = parentPost.likeCount ?? 0
|
||||||
|
if (parentLikeCount > 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
// From this point on we need at least one more reason to show it.
|
// From this point on we need at least one more reason to show it.
|
||||||
if (
|
if (
|
||||||
parentAuthor &&
|
parentAuthor &&
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
export type Gate =
|
export type Gate =
|
||||||
// Keep this alphabetic please.
|
// Keep this alphabetic please.
|
||||||
'debug_show_feedcontext' | 'suggested_feeds_interstitial'
|
'debug_show_feedcontext' | 'suggested_feeds_interstitial' | 'video_upload' // upload videos
|
||||||
|
|
|
@ -219,8 +219,6 @@ let ProfileHeaderStandard = ({
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
{profile.viewer?.following ? (
|
{profile.viewer?.following ? (
|
||||||
<Trans>Following</Trans>
|
<Trans>Following</Trans>
|
||||||
) : profile.viewer?.followedBy ? (
|
|
||||||
<Trans>Follow Back</Trans>
|
|
||||||
) : (
|
) : (
|
||||||
<Trans>Follow</Trans>
|
<Trans>Follow</Trans>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -172,7 +172,6 @@ export function StepInfo({
|
||||||
defaultValue={state.password}
|
defaultValue={state.password}
|
||||||
secureTextEntry
|
secureTextEntry
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -175,19 +175,9 @@ async function fetchSubjects(
|
||||||
}> {
|
}> {
|
||||||
const postUris = new Set<string>()
|
const postUris = new Set<string>()
|
||||||
const packUris = new Set<string>()
|
const packUris = new Set<string>()
|
||||||
|
|
||||||
const postUrisWithLikes = new Set<string>()
|
|
||||||
const postUrisWithReposts = new Set<string>()
|
|
||||||
|
|
||||||
for (const notif of groupedNotifs) {
|
for (const notif of groupedNotifs) {
|
||||||
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
|
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
|
||||||
postUris.add(notif.subjectUri)
|
postUris.add(notif.subjectUri)
|
||||||
if (notif.type === 'post-like') {
|
|
||||||
postUrisWithLikes.add(notif.subjectUri)
|
|
||||||
}
|
|
||||||
if (notif.type === 'repost') {
|
|
||||||
postUrisWithReposts.add(notif.subjectUri)
|
|
||||||
}
|
|
||||||
} else if (
|
} else if (
|
||||||
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
|
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
|
||||||
) {
|
) {
|
||||||
|
@ -216,15 +206,6 @@ async function fetchSubjects(
|
||||||
AppBskyFeedPost.validateRecord(post.record).success
|
AppBskyFeedPost.validateRecord(post.record).success
|
||||||
) {
|
) {
|
||||||
postsMap.set(post.uri, post)
|
postsMap.set(post.uri, post)
|
||||||
|
|
||||||
// HACK. In some cases, the appview appears to lag behind and returns empty counters.
|
|
||||||
// To prevent scroll jump due to missing metrics, fill in 1 like/repost instead of 0.
|
|
||||||
if (post.likeCount === 0 && postUrisWithLikes.has(post.uri)) {
|
|
||||||
post.likeCount = 1
|
|
||||||
}
|
|
||||||
if (post.repostCount === 0 && postUrisWithReposts.has(post.uri)) {
|
|
||||||
post.repostCount = 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const pack of packsChunks.flat()) {
|
for (const pack of packsChunks.flat()) {
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import zod from 'zod'
|
|
||||||
|
|
||||||
import {BaseNux} from '#/state/queries/nuxs/types'
|
|
||||||
|
|
||||||
export enum Nux {
|
|
||||||
One = 'one',
|
|
||||||
Two = 'two',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const nuxNames = new Set(Object.values(Nux))
|
|
||||||
|
|
||||||
export type AppNux =
|
|
||||||
| BaseNux<{
|
|
||||||
id: Nux.One
|
|
||||||
data: {
|
|
||||||
likes: number
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
| BaseNux<{
|
|
||||||
id: Nux.Two
|
|
||||||
data: undefined
|
|
||||||
}>
|
|
||||||
|
|
||||||
export const NuxSchemas = {
|
|
||||||
[Nux.One]: zod.object({
|
|
||||||
likes: zod.number(),
|
|
||||||
}),
|
|
||||||
[Nux.Two]: undefined,
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {AppNux, Nux} from '#/state/queries/nuxs/definitions'
|
|
||||||
import {parseAppNux, serializeAppNux} from '#/state/queries/nuxs/util'
|
|
||||||
import {
|
|
||||||
preferencesQueryKey,
|
|
||||||
usePreferencesQuery,
|
|
||||||
} from '#/state/queries/preferences'
|
|
||||||
import {useAgent} from '#/state/session'
|
|
||||||
|
|
||||||
export {Nux} from '#/state/queries/nuxs/definitions'
|
|
||||||
|
|
||||||
export function useNuxs() {
|
|
||||||
const {data, ...rest} = usePreferencesQuery()
|
|
||||||
|
|
||||||
if (data && rest.isSuccess) {
|
|
||||||
const nuxs = data.bskyAppState.nuxs
|
|
||||||
?.map(parseAppNux)
|
|
||||||
?.filter(Boolean) as AppNux[]
|
|
||||||
|
|
||||||
if (nuxs) {
|
|
||||||
return {
|
|
||||||
nuxs,
|
|
||||||
...rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
nuxs: undefined,
|
|
||||||
...rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNux<T extends Nux>(id: T) {
|
|
||||||
const {nuxs, ...rest} = useNuxs()
|
|
||||||
|
|
||||||
if (nuxs && rest.isSuccess) {
|
|
||||||
const nux = nuxs.find(nux => nux.id === id)
|
|
||||||
|
|
||||||
if (nux) {
|
|
||||||
return {
|
|
||||||
nux: nux as Extract<AppNux, {id: T}>,
|
|
||||||
...rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
nux: undefined,
|
|
||||||
...rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpsertNuxMutation() {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const agent = useAgent()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (nux: AppNux) => {
|
|
||||||
await agent.bskyAppUpsertNux(serializeAppNux(nux))
|
|
||||||
// triggers a refetch
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: preferencesQueryKey,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useRemoveNuxsMutation() {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const agent = useAgent()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (ids: string[]) => {
|
|
||||||
await agent.bskyAppRemoveNuxs(ids)
|
|
||||||
// triggers a refetch
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: preferencesQueryKey,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import {AppBskyActorDefs} from '@atproto/api'
|
|
||||||
|
|
||||||
export type Data = Record<string, unknown> | undefined
|
|
||||||
|
|
||||||
export type BaseNux<
|
|
||||||
T extends Pick<AppBskyActorDefs.Nux, 'id' | 'expiresAt'> & {data: Data},
|
|
||||||
> = T & {
|
|
||||||
completed: boolean
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import {AppBskyActorDefs, nuxSchema} from '@atproto/api'
|
|
||||||
|
|
||||||
import {
|
|
||||||
AppNux,
|
|
||||||
Nux,
|
|
||||||
nuxNames,
|
|
||||||
NuxSchemas,
|
|
||||||
} from '#/state/queries/nuxs/definitions'
|
|
||||||
|
|
||||||
export function parseAppNux(nux: AppBskyActorDefs.Nux): AppNux | undefined {
|
|
||||||
if (!nuxNames.has(nux.id as Nux)) return
|
|
||||||
if (!nuxSchema.safeParse(nux).success) return
|
|
||||||
|
|
||||||
const {data, ...rest} = nux
|
|
||||||
|
|
||||||
const schema = NuxSchemas[nux.id as Nux]
|
|
||||||
|
|
||||||
if (schema && data) {
|
|
||||||
const parsedData = JSON.parse(data)
|
|
||||||
|
|
||||||
if (!schema.safeParse(parsedData).success) return
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
data: parsedData,
|
|
||||||
} as AppNux
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
data: undefined,
|
|
||||||
} as AppNux
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serializeAppNux(nux: AppNux): AppBskyActorDefs.Nux {
|
|
||||||
const {data, ...rest} = nux
|
|
||||||
const schema = NuxSchemas[nux.id as Nux]
|
|
||||||
|
|
||||||
const result: AppBskyActorDefs.Nux = {
|
|
||||||
...rest,
|
|
||||||
data: undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schema) {
|
|
||||||
schema.parse(data)
|
|
||||||
result.data = JSON.stringify(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
nuxSchema.parse(result)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
|
@ -37,6 +37,5 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
|
||||||
bskyAppState: {
|
bskyAppState: {
|
||||||
queuedNudges: [],
|
queuedNudges: [],
|
||||||
activeProgressGuide: undefined,
|
activeProgressGuide: undefined,
|
||||||
nuxs: [],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,16 +106,13 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
|
||||||
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
|
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
|
return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
|
||||||
|
gcTime: 0,
|
||||||
queryKey: suggestedFollowsByActorQueryKey(did),
|
queryKey: suggestedFollowsByActorQueryKey(did),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
|
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
|
||||||
actor: did,
|
actor: did,
|
||||||
})
|
})
|
||||||
const data = res.data.isFallback ? {suggestions: []} : res.data
|
return res.data
|
||||||
data.suggestions = data.suggestions.filter(profile => {
|
|
||||||
return !profile.viewer?.following
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||||
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 {LikelyType} from '#/lib/link-meta/link-meta'
|
import {LikelyType} from '#/lib/link-meta/link-meta'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent, useGate} from '#/lib/statsig/statsig'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {insertMentionAt} from '#/lib/strings/mention-manip'
|
import {insertMentionAt} from '#/lib/strings/mention-manip'
|
||||||
import {shortenLinks} from '#/lib/strings/rich-text-manip'
|
import {shortenLinks} from '#/lib/strings/rich-text-manip'
|
||||||
|
@ -140,6 +140,7 @@ export const ComposePost = observer(function ComposePost({
|
||||||
}: Props & {
|
}: Props & {
|
||||||
cancelRef?: React.RefObject<CancelRef>
|
cancelRef?: React.RefObject<CancelRef>
|
||||||
}) {
|
}) {
|
||||||
|
const gate = useGate()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
|
||||||
|
@ -802,11 +803,13 @@ export const ComposePost = observer(function ComposePost({
|
||||||
) : (
|
) : (
|
||||||
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
<ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}>
|
||||||
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
<SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} />
|
||||||
|
{gate('video_upload') && (
|
||||||
<SelectVideoBtn
|
<SelectVideoBtn
|
||||||
onSelectVideo={selectVideo}
|
onSelectVideo={selectVideo}
|
||||||
disabled={!canSelectImages}
|
disabled={!canSelectImages}
|
||||||
setError={setError}
|
setError={setError}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
<OpenCameraBtn gallery={gallery} disabled={!canSelectImages} />
|
||||||
<SelectGifBtn
|
<SelectGifBtn
|
||||||
onClose={focusTextInput}
|
onClose={focusTextInput}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import {View} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
@ -26,16 +25,9 @@ export function SubtitleFilePicker({
|
||||||
const handlePick = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
const handlePick = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selectedFile = evt.target.files?.[0]
|
const selectedFile = evt.target.files?.[0]
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
if (
|
if (selectedFile.type === 'text/vtt') {
|
||||||
selectedFile.type === 'text/vtt' ||
|
|
||||||
(selectedFile.type === 'text/plain' &&
|
|
||||||
selectedFile.name.endsWith('.vtt'))
|
|
||||||
) {
|
|
||||||
onSelectFile(selectedFile)
|
onSelectFile(selectedFile)
|
||||||
} else {
|
} else {
|
||||||
logger.error('Invalid subtitle file type', {
|
|
||||||
safeMessage: `File: ${selectedFile.name} (${selectedFile.type})`,
|
|
||||||
})
|
|
||||||
Toast.show(_(msg`Only WebVTT (.vtt) files are supported`))
|
Toast.show(_(msg`Only WebVTT (.vtt) files are supported`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ export function FollowButton({
|
||||||
label={_(msg({message: 'Unfollow', context: 'action'}))}
|
label={_(msg({message: 'Unfollow', context: 'action'}))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (!profile.viewer.followedBy) {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type={unfollowedType}
|
type={unfollowedType}
|
||||||
|
@ -70,14 +70,5 @@ export function FollowButton({
|
||||||
label={_(msg({message: 'Follow', context: 'action'}))}
|
label={_(msg({message: 'Follow', context: 'action'}))}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type={unfollowedType}
|
|
||||||
labelStyle={labelStyle}
|
|
||||||
onPress={onPressFollow}
|
|
||||||
label={_(msg({message: 'Follow Back', context: 'action'}))}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,9 @@ export function useImageAspectRatio({
|
||||||
const [raw, setAspectRatio] = React.useState<number>(
|
const [raw, setAspectRatio] = React.useState<number>(
|
||||||
dimensions ? calc(dimensions) : 1,
|
dimensions ? calc(dimensions) : 1,
|
||||||
)
|
)
|
||||||
// this basically controls the width of the image
|
|
||||||
const {isCropped, constrained, max} = React.useMemo(() => {
|
const {isCropped, constrained, max} = React.useMemo(() => {
|
||||||
const ratio = 1 / 2 // max of 1:2 ratio in feeds
|
const a34 = 0.75 // max of 3:4 ratio in feeds
|
||||||
const constrained = Math.max(raw, ratio)
|
const constrained = Math.max(raw, a34)
|
||||||
const max = Math.max(raw, 0.25) // max of 1:4 in thread
|
const max = Math.max(raw, 0.25) // max of 1:4 in thread
|
||||||
const isCropped = raw < constrained
|
const isCropped = raw < constrained
|
||||||
return {
|
return {
|
||||||
|
@ -69,14 +68,14 @@ export function ConstrainedImage({
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
/**
|
/**
|
||||||
* Computed as a % value to apply as `paddingTop`, this basically controls
|
* Computed as a % value to apply as `paddingTop`
|
||||||
* the height of the image.
|
|
||||||
*/
|
*/
|
||||||
const outerAspectRatio = React.useMemo<DimensionValue>(() => {
|
const outerAspectRatio = React.useMemo<DimensionValue>(() => {
|
||||||
|
// capped to square or shorter
|
||||||
const ratio =
|
const ratio =
|
||||||
isNative || !gtMobile
|
isNative || !gtMobile
|
||||||
? Math.min(1 / aspectRatio, 16 / 9) // 9:16 bounding box
|
? Math.min(1 / aspectRatio, 1.5)
|
||||||
: Math.min(1 / aspectRatio, 1) // 1:1 bounding box
|
: Math.min(1 / aspectRatio, 1)
|
||||||
return `${ratio * 100}%`
|
return `${ratio * 100}%`
|
||||||
}, [aspectRatio, gtMobile])
|
}, [aspectRatio, gtMobile])
|
||||||
|
|
||||||
|
|
|
@ -37,11 +37,11 @@ export function TimeIndicator({time}: {time: number}) {
|
||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
{color: t.palette.white, fontSize: 12, fontVariant: ['tabular-nums']},
|
{color: t.palette.white, fontSize: 12},
|
||||||
a.font_bold,
|
a.font_bold,
|
||||||
{lineHeight: 1.25},
|
{lineHeight: 1.25},
|
||||||
]}>
|
]}>
|
||||||
{`${minutes}:${seconds}`}
|
{minutes}:{seconds}
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -370,7 +370,7 @@ export function Controls({
|
||||||
onPress={onPressPlayPause}
|
onPress={onPressPlayPause}
|
||||||
/>
|
/>
|
||||||
<View style={a.flex_1} />
|
<View style={a.flex_1} />
|
||||||
<Text style={{color: t.palette.white, fontVariant: ['tabular-nums']}}>
|
<Text style={{color: t.palette.white}}>
|
||||||
{formatTime(currentTime)} / {formatTime(duration)}
|
{formatTime(currentTime)} / {formatTime(duration)}
|
||||||
</Text>
|
</Text>
|
||||||
{hasSubtitleTrack && (
|
{hasSubtitleTrack && (
|
||||||
|
|
|
@ -41,7 +41,6 @@ import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
|
||||||
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
|
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
|
||||||
import {ScreenHider} from '#/components/moderation/ScreenHider'
|
import {ScreenHider} from '#/components/moderation/ScreenHider'
|
||||||
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
|
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
|
||||||
import {navigate} from '#/Navigation'
|
|
||||||
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
|
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
|
||||||
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
|
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
|
||||||
import {ProfileLists} from '../com/lists/ProfileLists'
|
import {ProfileLists} from '../com/lists/ProfileLists'
|
||||||
|
@ -87,16 +86,6 @@ export function ProfileScreen({route}: Props) {
|
||||||
}
|
}
|
||||||
}, [resolveError, refetchDid, refetchProfile])
|
}, [resolveError, refetchDid, refetchProfile])
|
||||||
|
|
||||||
// Apply hard-coded redirects as need
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (resolveError) {
|
|
||||||
if (name === 'lulaoficial.bsky.social') {
|
|
||||||
console.log('Applying redirect to lula.com.br')
|
|
||||||
navigate('Profile', {name: 'lula.com.br'})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [name, resolveError])
|
|
||||||
|
|
||||||
// When we open the profile, we want to reset the posts query if we are blocked.
|
// When we open the profile, we want to reset the posts query if we are blocked.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (resolvedDid && profile?.viewer?.blockedBy) {
|
if (resolvedDid && profile?.viewer?.blockedBy) {
|
||||||
|
|
35
yarn.lock
35
yarn.lock
|
@ -72,6 +72,19 @@
|
||||||
resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106"
|
resolved "https://registry.yarnpkg.com/@atproto-labs/simple-store/-/simple-store-0.1.1.tgz#e743a2722b5d8732166f0a72aca8bd10e9bff106"
|
||||||
integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg==
|
integrity sha512-WKILW2b3QbAYKh+w5U2x6p5FqqLl0nAeLwGeDY+KjX01K4Dq3vQTR9b/qNp0jZm48CabPQVrqCv0PPU9LgRRRg==
|
||||||
|
|
||||||
|
"@atproto/api@0.13.5":
|
||||||
|
version "0.13.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.5.tgz#04305cdb0a467ba366305c5e95cebb7ce0d39735"
|
||||||
|
integrity sha512-yT/YimcKYkrI0d282Zxo7O30OSYR+KDW89f81C6oYZfDRBcShC1aniVV8kluP5LrEAg8O27yrOSnBgx2v7XPew==
|
||||||
|
dependencies:
|
||||||
|
"@atproto/common-web" "^0.3.0"
|
||||||
|
"@atproto/lexicon" "^0.4.1"
|
||||||
|
"@atproto/syntax" "^0.3.0"
|
||||||
|
"@atproto/xrpc" "^0.6.1"
|
||||||
|
await-lock "^2.2.2"
|
||||||
|
multiformats "^9.9.0"
|
||||||
|
tlds "^1.234.0"
|
||||||
|
|
||||||
"@atproto/api@^0.13.0":
|
"@atproto/api@^0.13.0":
|
||||||
version "0.13.0"
|
version "0.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.0.tgz#d1c65a407f1c3c6aba5be9425f4f739a01419bd8"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.0.tgz#d1c65a407f1c3c6aba5be9425f4f739a01419bd8"
|
||||||
|
@ -85,20 +98,6 @@
|
||||||
multiformats "^9.9.0"
|
multiformats "^9.9.0"
|
||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
|
|
||||||
"@atproto/api@^0.13.7":
|
|
||||||
version "0.13.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.13.7.tgz#072eba2025d5251505f17b0b5d2de33749ea5ee4"
|
|
||||||
integrity sha512-41kSLmFWDbuPOenb52WRq1lnBkSZrL+X29tWcvEt6SZXK4xBoKAalw1MjF+oabhzff12iMtNaNvmmt2fu1L+cw==
|
|
||||||
dependencies:
|
|
||||||
"@atproto/common-web" "^0.3.0"
|
|
||||||
"@atproto/lexicon" "^0.4.1"
|
|
||||||
"@atproto/syntax" "^0.3.0"
|
|
||||||
"@atproto/xrpc" "^0.6.2"
|
|
||||||
await-lock "^2.2.2"
|
|
||||||
multiformats "^9.9.0"
|
|
||||||
tlds "^1.234.0"
|
|
||||||
zod "^3.23.8"
|
|
||||||
|
|
||||||
"@atproto/aws@^0.2.2":
|
"@atproto/aws@^0.2.2":
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.2.tgz#703e5e06f288bcf61c6d99a990738f1e7299e653"
|
resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.2.tgz#703e5e06f288bcf61c6d99a990738f1e7299e653"
|
||||||
|
@ -444,10 +443,10 @@
|
||||||
"@atproto/lexicon" "^0.4.1"
|
"@atproto/lexicon" "^0.4.1"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@atproto/xrpc@^0.6.2":
|
"@atproto/xrpc@^0.6.1":
|
||||||
version "0.6.2"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.2.tgz#634228a7e533de01bda2214837d11574fdadad55"
|
resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.6.1.tgz#dcd1315c8c60eef5af2db7fa4e35a38ebc6d79d5"
|
||||||
integrity sha512-as/gb08xJb02HAGNrSQSumCe10WnOAcnM6bR6KMatQyQJuEu7OY6ZDSTM/4HfjjoxsNqdvPmbYuoUab1bKTNlA==
|
integrity sha512-Zy5ydXEdk6sY7FDUZcEVfCL1jvbL4tXu5CcdPqbEaW6LQtk9GLds/DK1bCX9kswTGaBC88EMuqQMfkxOhp2t4A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/lexicon" "^0.4.1"
|
"@atproto/lexicon" "^0.4.1"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
Loading…
Reference in New Issue