diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 5031f584e5..286ded13e2 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -60,16 +60,13 @@ function CardOuter({
export function SuggestedFollowPlaceholder() {
const t = useTheme()
return (
-
+
+
-
-
-
-
-
+
)
}
@@ -176,9 +173,14 @@ function useExperimentalSuggestedUsersQuery() {
}
export function SuggestedFollows({feed}: {feed: FeedDescriptor}) {
- const [feedType, feedUri] = feed.split('|')
+ const {currentAccount} = useSession()
+ const [feedType, feedUriOrDid] = feed.split('|')
if (feedType === 'author') {
- return
+ if (currentAccount?.did === feedUriOrDid) {
+ return null
+ } else {
+ return
+ }
} else {
return
}
@@ -197,6 +199,7 @@ export function SuggestedFollowsProfile({did}: {did: string}) {
isSuggestionsLoading={isSuggestionsLoading}
profiles={data?.suggestions ?? []}
error={error}
+ viewContext="profile"
/>
)
}
@@ -212,6 +215,7 @@ export function SuggestedFollowsHome() {
isSuggestionsLoading={isSuggestionsLoading}
profiles={profiles}
error={error}
+ viewContext="feed"
/>
)
}
@@ -220,10 +224,12 @@ export function ProfileGrid({
isSuggestionsLoading,
error,
profiles,
+ viewContext = 'feed',
}: {
isSuggestionsLoading: boolean
profiles: AppBskyActorDefs.ProfileViewDetailed[]
error: Error | null
+ viewContext: 'profile' | 'feed'
}) {
const t = useTheme()
const {_} = useLingui()
@@ -280,7 +286,7 @@ export function ProfileGrid({
shape="round"
/>
-
+
)}
@@ -297,33 +303,31 @@ export function ProfileGrid({
return (
-
-
- Suggested for you
+
+
+ {viewContext === 'profile' ? (
+ Similar accounts
+ ) : (
+ Suggested for you
+ )}
-
+
{gtMobile ? (
-
-
+
+
{content}
-
+
-
+
{content}
)
}
-export function DescriptionPlaceholder() {
+export function DescriptionPlaceholder({
+ numberOfLines = 3,
+}: {
+ numberOfLines?: number
+}) {
const t = useTheme()
return (
-
-
-
-
+
+ {Array(numberOfLines)
+ .fill(0)
+ .map((_, i) => (
+
+ ))}
)
}
diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts
index eaa760b4be..0eca8b1655 100644
--- a/src/lib/api/feed-manip.ts
+++ b/src/lib/api/feed-manip.ts
@@ -379,7 +379,11 @@ export class FeedTuner {
): FeedViewPostsSlice[] => {
for (let i = 0; i < slices.length; i++) {
const slice = slices[i]
- if (slice.isReply && !shouldDisplayReplyInFollowing(slice, userDid)) {
+ if (
+ slice.isReply &&
+ !slice.isRepost &&
+ !shouldDisplayReplyInFollowing(slice.getAuthors(), userDid)
+ ) {
slices.splice(i, 1)
i--
}
@@ -443,13 +447,9 @@ function areSameAuthor(authors: AuthorContext): boolean {
}
function shouldDisplayReplyInFollowing(
- slice: FeedViewPostsSlice,
+ authors: AuthorContext,
userDid: string,
): boolean {
- if (slice.isRepost) {
- return true
- }
- const authors = slice.getAuthors()
const {author, parentAuthor, grandparentAuthor, rootAuthor} = authors
if (!isSelfOrFollowing(author, userDid)) {
// Only show replies from self or people you follow.
@@ -463,21 +463,6 @@ function shouldDisplayReplyInFollowing(
// Always show self-threads.
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.
if (
parentAuthor &&
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index df9daab447..7966767d1b 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -1,3 +1,3 @@
export type Gate =
// Keep this alphabetic please.
- 'debug_show_feedcontext' | 'suggested_feeds_interstitial' | 'video_upload' // upload videos
+ 'debug_show_feedcontext' | 'suggested_feeds_interstitial'
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
index cf5fcb97e3..846fa4424b 100644
--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx
@@ -219,6 +219,8 @@ let ProfileHeaderStandard = ({
{profile.viewer?.following ? (
Following
+ ) : profile.viewer?.followedBy ? (
+ Follow Back
) : (
Follow
)}
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index e0ee02294e..133d3ebc2e 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -175,9 +175,19 @@ async function fetchSubjects(
}> {
const postUris = new Set()
const packUris = new Set()
+
+ const postUrisWithLikes = new Set()
+ const postUrisWithReposts = new Set()
+
for (const notif of groupedNotifs) {
if (notif.subjectUri?.includes('app.bsky.feed.post')) {
postUris.add(notif.subjectUri)
+ if (notif.type === 'post-like') {
+ postUrisWithLikes.add(notif.subjectUri)
+ }
+ if (notif.type === 'repost') {
+ postUrisWithReposts.add(notif.subjectUri)
+ }
} else if (
notif.notification.reasonSubject?.includes('app.bsky.graph.starterpack')
) {
@@ -206,6 +216,15 @@ async function fetchSubjects(
AppBskyFeedPost.validateRecord(post.record).success
) {
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()) {
diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts
index f5d51a974a..5ae8317047 100644
--- a/src/state/queries/suggested-follows.ts
+++ b/src/state/queries/suggested-follows.ts
@@ -106,13 +106,16 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
const agent = useAgent()
return useQuery({
- gcTime: 0,
queryKey: suggestedFollowsByActorQueryKey(did),
queryFn: async () => {
const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
actor: did,
})
- return res.data
+ const data = res.data.isFallback ? {suggestions: []} : res.data
+ data.suggestions = data.suggestions.filter(profile => {
+ return !profile.viewer?.following
+ })
+ return data
},
})
}
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 4c7892bc09..dfdfb3ebdf 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -59,7 +59,7 @@ import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {LikelyType} from '#/lib/link-meta/link-meta'
-import {logEvent, useGate} from '#/lib/statsig/statsig'
+import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {insertMentionAt} from '#/lib/strings/mention-manip'
import {shortenLinks} from '#/lib/strings/rich-text-manip'
@@ -140,7 +140,6 @@ export const ComposePost = observer(function ComposePost({
}: Props & {
cancelRef?: React.RefObject
}) {
- const gate = useGate()
const {currentAccount} = useSession()
const agent = useAgent()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
@@ -803,13 +802,11 @@ export const ComposePost = observer(function ComposePost({
) : (
- {gate('video_upload') && (
-
- )}
+
) => {
const selectedFile = evt.target.files?.[0]
if (selectedFile) {
- if (selectedFile.type === 'text/vtt') {
+ if (
+ selectedFile.type === 'text/vtt' ||
+ (selectedFile.type === 'text/plain' &&
+ selectedFile.name.endsWith('.vtt'))
+ ) {
onSelectFile(selectedFile)
} else {
+ logger.error('Invalid subtitle file type', {
+ safeMessage: `File: ${selectedFile.name} (${selectedFile.type})`,
+ })
Toast.show(_(msg`Only WebVTT (.vtt) files are supported`))
}
}
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 42adea3cfc..aaa5d3454d 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -61,7 +61,7 @@ export function FollowButton({
label={_(msg({message: 'Unfollow', context: 'action'}))}
/>
)
- } else {
+ } else if (!profile.viewer.followedBy) {
return (
)
+ } else {
+ return (
+
+ )
}
}
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index f57ab4e3c9..80c48b133b 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -23,9 +23,10 @@ export function useImageAspectRatio({
const [raw, setAspectRatio] = React.useState(
dimensions ? calc(dimensions) : 1,
)
+ // this basically controls the width of the image
const {isCropped, constrained, max} = React.useMemo(() => {
- const a34 = 0.75 // max of 3:4 ratio in feeds
- const constrained = Math.max(raw, a34)
+ const ratio = 1 / 2 // max of 1:2 ratio in feeds
+ const constrained = Math.max(raw, ratio)
const max = Math.max(raw, 0.25) // max of 1:4 in thread
const isCropped = raw < constrained
return {
@@ -68,14 +69,14 @@ export function ConstrainedImage({
const t = useTheme()
const {gtMobile} = useBreakpoints()
/**
- * Computed as a % value to apply as `paddingTop`
+ * Computed as a % value to apply as `paddingTop`, this basically controls
+ * the height of the image.
*/
const outerAspectRatio = React.useMemo(() => {
- // capped to square or shorter
const ratio =
isNative || !gtMobile
- ? Math.min(1 / aspectRatio, 1.5)
- : Math.min(1 / aspectRatio, 1)
+ ? Math.min(1 / aspectRatio, 16 / 9) // 9:16 bounding box
+ : Math.min(1 / aspectRatio, 1) // 1:1 bounding box
return `${ratio * 100}%`
}, [aspectRatio, gtMobile])
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx
index 6636883f12..be3f907112 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx
@@ -37,11 +37,11 @@ export function TimeIndicator({time}: {time: number}) {
]}>
- {minutes}:{seconds}
+ {`${minutes}:${seconds}`}
)
diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx
index bb15db0833..791025f709 100644
--- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx
+++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx
@@ -370,7 +370,7 @@ export function Controls({
onPress={onPressPlayPause}
/>
-
+
{formatTime(currentTime)} / {formatTime(duration)}
{hasSubtitleTrack && (
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 37111c02e5..5ef6459810 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -41,6 +41,7 @@ import {ProfileFeedSection} from '#/screens/Profile/Sections/Feed'
import {ProfileLabelsSection} from '#/screens/Profile/Sections/Labels'
import {ScreenHider} from '#/components/moderation/ScreenHider'
import {ProfileStarterPacks} from '#/components/StarterPack/ProfileStarterPacks'
+import {navigate} from '#/Navigation'
import {ExpoScrollForwarderView} from '../../../modules/expo-scroll-forwarder'
import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
import {ProfileLists} from '../com/lists/ProfileLists'
@@ -86,6 +87,16 @@ export function ProfileScreen({route}: Props) {
}
}, [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.
React.useEffect(() => {
if (resolvedDid && profile?.viewer?.blockedBy) {