diff options
-rw-r--r-- | src/components/FeedInterstitials.tsx | 64 | ||||
-rw-r--r-- | src/components/ProfileCard.tsx | 39 | ||||
-rw-r--r-- | src/lib/api/feed-manip.ts | 27 | ||||
-rw-r--r-- | src/lib/statsig/gates.ts | 1 | ||||
-rw-r--r-- | src/screens/Profile/Header/ProfileHeaderStandard.tsx | 2 | ||||
-rw-r--r-- | src/state/queries/post-thread.ts | 8 | ||||
-rw-r--r-- | src/state/queries/suggested-follows.ts | 7 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 15 | ||||
-rw-r--r-- | src/view/com/composer/videos/SubtitleFilePicker.tsx | 10 | ||||
-rw-r--r-- | src/view/com/profile/FollowButton.tsx | 11 | ||||
-rw-r--r-- | src/view/com/util/images/AutoSizedImage.tsx | 69 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx | 2 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 11 |
14 files changed, 162 insertions, 108 deletions
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx index 5031f584e..286ded13e 100644 --- a/src/components/FeedInterstitials.tsx +++ b/src/components/FeedInterstitials.tsx @@ -60,16 +60,13 @@ function CardOuter({ export function SuggestedFollowPlaceholder() { const t = useTheme() return ( - <CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}> + <CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}> <ProfileCard.Header> <ProfileCard.AvatarPlaceholder /> - </ProfileCard.Header> - - <View style={[a.py_xs]}> <ProfileCard.NameAndHandlePlaceholder /> - </View> + </ProfileCard.Header> - <ProfileCard.DescriptionPlaceholder /> + <ProfileCard.DescriptionPlaceholder numberOfLines={2} /> </CardOuter> ) } @@ -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 <SuggestedFollowsProfile did={feedUri} /> + if (currentAccount?.did === feedUriOrDid) { + return null + } else { + return <SuggestedFollowsProfile did={feedUriOrDid} /> + } } else { return <SuggestedFollowsHome /> } @@ -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" /> </ProfileCard.Header> - <ProfileCard.Description profile={profile} /> + <ProfileCard.Description profile={profile} numberOfLines={2} /> </ProfileCard.Outer> </CardOuter> )} @@ -297,33 +303,31 @@ export function ProfileGrid({ return ( <View style={[a.border_t, t.atoms.border_contrast_low, t.atoms.bg_contrast_25]}> - <View style={[a.pt_2xl, a.px_lg, a.flex_row, a.pb_xs]}> - <Text - style={[ - a.flex_1, - a.text_lg, - a.font_bold, - t.atoms.text_contrast_medium, - ]}> - <Trans>Suggested for you</Trans> + <View + style={[ + a.p_lg, + a.pb_xs, + a.flex_row, + a.align_center, + 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> + )} </Text> - <Person fill={t.atoms.text_contrast_low.color} /> + <Person fill={t.atoms.text_contrast_low.color} size="sm" /> </View> {gtMobile ? ( - <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_md]}> + <View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}> + <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}> {content} </View> - <View - style={[ - a.flex_row, - a.justify_end, - a.align_center, - a.pt_xs, - a.gap_md, - ]}> + <View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}> <InlineLinkText label={_(msg`Browse more suggestions`)} to="/search" @@ -339,7 +343,7 @@ export function ProfileGrid({ showsHorizontalScrollIndicator={false} snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} decelerationRate="fast"> - <View style={[a.px_lg, a.pt_md, a.pb_xl, a.flex_row, a.gap_md]}> + <View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}> {content} <Button diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index 6f6d68049..b208903b4 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -220,8 +220,10 @@ export function NameAndHandlePlaceholder() { export function Description({ profile: profileUnshadowed, + numberOfLines = 3, }: { profile: AppBskyActorDefs.ProfileViewDetailed + numberOfLines?: number }) { const profile = useProfileShadow(profileUnshadowed) const {description} = profile @@ -244,31 +246,34 @@ export function Description({ <RichText value={rt} style={[a.leading_snug]} - numberOfLines={3} + numberOfLines={numberOfLines} disableLinks /> </View> ) } -export function DescriptionPlaceholder() { +export function DescriptionPlaceholder({ + numberOfLines = 3, +}: { + numberOfLines?: number +}) { const t = useTheme() return ( - <View style={[a.gap_xs]}> - <View - style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]} - /> - <View - style={[a.rounded_xs, a.w_full, t.atoms.bg_contrast_50, {height: 12}]} - /> - <View - style={[ - a.rounded_xs, - a.w_full, - t.atoms.bg_contrast_50, - {height: 12, width: 100}, - ]} - /> + <View style={[{gap: 8}]}> + {Array(numberOfLines) + .fill(0) + .map((_, i) => ( + <View + key={i} + style={[ + a.rounded_xs, + a.w_full, + t.atoms.bg_contrast_50, + {height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'}, + ]} + /> + ))} </View> ) } diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index eaa760b4b..0eca8b165 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 9b6c036b9..909b93e6b 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -3,4 +3,3 @@ export type Gate = | 'debug_show_feedcontext' | 'suggested_feeds_interstitial' | 'ten_million_dialog' - | 'video_upload' // upload videos diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index cf5fcb97e..846fa4424 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -219,6 +219,8 @@ let ProfileHeaderStandard = ({ <ButtonText> {profile.viewer?.following ? ( <Trans>Following</Trans> + ) : profile.viewer?.followedBy ? ( + <Trans>Follow Back</Trans> ) : ( <Trans>Follow</Trans> )} diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts index 83ca60c2a..a569cb160 100644 --- a/src/state/queries/post-thread.ts +++ b/src/state/queries/post-thread.ts @@ -408,10 +408,14 @@ export function* findAllPostsInQueryData( } } } - for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { + for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { + // Check notifications first. If you have a post in notifications, + // it's often due to a like or a repost, and we want to prioritize + // a post object with >0 likes/reposts over a stale version with no + // metrics in order to avoid a notification->post scroll jump. yield postViewToPlaceholderThread(post) } - for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { + for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { yield postViewToPlaceholderThread(post) } for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index f5d51a974..5ae831704 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<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({ - 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 4c7892bc0..dfdfb3ebd 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<CancelRef> }) { - 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({ ) : ( <ToolbarWrapper style={[a.flex_row, a.align_center, a.gap_xs]}> <SelectPhotoBtn gallery={gallery} disabled={!canSelectImages} /> - {gate('video_upload') && ( - <SelectVideoBtn - onSelectVideo={selectVideo} - disabled={!canSelectImages} - setError={setError} - /> - )} + <SelectVideoBtn + onSelectVideo={selectVideo} + disabled={!canSelectImages} + setError={setError} + /> <OpenCameraBtn gallery={gallery} disabled={!canSelectImages} /> <SelectGifBtn onClose={focusTextInput} diff --git a/src/view/com/composer/videos/SubtitleFilePicker.tsx b/src/view/com/composer/videos/SubtitleFilePicker.tsx index 9e0fe0aee..beb3f07a8 100644 --- a/src/view/com/composer/videos/SubtitleFilePicker.tsx +++ b/src/view/com/composer/videos/SubtitleFilePicker.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {logger} from '#/logger' import * as Toast from '#/view/com/util/Toast' import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -25,9 +26,16 @@ export function SubtitleFilePicker({ const handlePick = (evt: React.ChangeEvent<HTMLInputElement>) => { 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 42adea3cf..aaa5d3454 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 ( <Button type={unfollowedType} @@ -70,5 +70,14 @@ export function FollowButton({ label={_(msg({message: 'Follow', context: 'action'}))} /> ) + } else { + return ( + <Button + type={unfollowedType} + labelStyle={labelStyle} + onPress={onPressFollow} + label={_(msg({message: 'Follow Back', context: 'action'}))} + /> + ) } } diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx index f57ab4e3c..932a18280 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/view/com/util/images/AutoSizedImage.tsx @@ -10,7 +10,7 @@ import {Dimensions} from '#/lib/media/types' import {isNative} from '#/platform/detection' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Crop_Stroke2_Corner0_Rounded as Crop} from '#/components/icons/Crop' +import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' import {Text} from '#/components/Typography' export function useImageAspectRatio({ @@ -23,9 +23,10 @@ export function useImageAspectRatio({ const [raw, setAspectRatio] = React.useState<number>( 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<DimensionValue>(() => { - // 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]) @@ -146,33 +147,59 @@ export function AutoSizedImage({ style={[ a.absolute, a.flex_row, - a.align_center, - a.rounded_xs, - t.atoms.bg_contrast_25, { - gap: 3, - padding: 3, bottom: a.p_xs.padding, right: a.p_xs.padding, - opacity: 0.8, + gap: 3, }, largeAlt && [ { gap: 4, - padding: 5, }, ], ]}> {isCropped && ( - <Crop - fill={t.atoms.text_contrast_high.color} - width={largeAlt ? 18 : 12} - /> + <View + style={[ + a.rounded_xs, + t.atoms.bg_contrast_25, + { + padding: 3, + opacity: 0.8, + }, + largeAlt && [ + { + padding: 5, + }, + ], + ]}> + <Fullscreen + fill={t.atoms.text_contrast_high.color} + width={largeAlt ? 18 : 12} + /> + </View> )} {hasAlt && ( - <Text style={[a.font_heavy, largeAlt ? a.text_xs : {fontSize: 8}]}> - ALT - </Text> + <View + style={[ + a.justify_center, + a.rounded_xs, + t.atoms.bg_contrast_25, + { + padding: 3, + opacity: 0.8, + }, + largeAlt && [ + { + padding: 5, + }, + ], + ]}> + <Text + style={[a.font_heavy, largeAlt ? a.text_xs : {fontSize: 8}]}> + ALT + </Text> + </View> )} </View> ) : null} diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/TimeIndicator.tsx index 6636883f1..be3f90711 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}) { ]}> <Text style={[ - {color: t.palette.white, fontSize: 12}, + {color: t.palette.white, fontSize: 12, fontVariant: ['tabular-nums']}, a.font_bold, {lineHeight: 1.25}, ]}> - {minutes}:{seconds} + {`${minutes}:${seconds}`} </Text> </Animated.View> ) diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx index bb15db083..791025f70 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} /> <View style={a.flex_1} /> - <Text style={{color: t.palette.white}}> + <Text style={{color: t.palette.white, fontVariant: ['tabular-nums']}}> {formatTime(currentTime)} / {formatTime(duration)} </Text> {hasSubtitleTrack && ( diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 37111c02e..5ef645981 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) { |