suggestions

This commit is contained in:
Matthieu Sieben
2025-01-07 16:27:39 +01:00
parent 5e1262b6a5
commit fcbb9c2e1f
10 changed files with 85 additions and 221 deletions
+32 -30
View File
@@ -1,15 +1,20 @@
import React from 'react' import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {AppBskyFeedDefs} from '@atproto/api' import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
} from '@atproto/api'
import {$Typed} from '@atproto/api/dist/client/util'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {parseTenorGif} from '#/lib/strings/embed-player' import {isTenorGifUri} from '#/lib/strings/embed-player'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
import {parseEmbed} from '#/types/atproto/post'
/** /**
* Streamlined MediaPreview component which just handles images, gifs, and videos * Streamlined MediaPreview component which just handles images, gifs, and videos
@@ -18,17 +23,22 @@ export function Embed({
embed, embed,
style, style,
}: { }: {
embed: AppBskyFeedDefs.PostView['embed'] embed?:
| $Typed<AppBskyEmbedRecordWithMedia.View>
| $Typed<AppBskyEmbedImages.View>
| $Typed<AppBskyEmbedVideo.View>
| $Typed<AppBskyEmbedExternal.View>
| {$type: string}
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const e = parseEmbed(embed) if (!embed) {
return null
if (!e) return null } else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
return <Embed embed={embed.media} style={style} />
if (e.type === 'images') { } else if (AppBskyEmbedImages.isView(embed)) {
return ( return (
<Outer style={style}> <Outer style={style}>
{e.view.images.map(image => ( {embed.images.map(image => (
<ImageItem <ImageItem
key={image.thumb} key={image.thumb}
thumbnail={image.thumb} thumbnail={image.thumb}
@@ -37,32 +47,24 @@ export function Embed({
))} ))}
</Outer> </Outer>
) )
} else if (e.type === 'link' && e.view.external.thumb) { } else if (AppBskyEmbedExternal.isView(embed)) {
let url: URL | undefined if (!embed.external.thumb) return null
try { if (!isTenorGifUri(embed.external.uri)) return null
url = new URL(e.view.external.uri)
} catch {}
if (url) {
const {success} = parseTenorGif(url)
if (success) {
return (
<Outer style={style}>
<GifItem
thumbnail={e.view.external.thumb}
alt={e.view.external.title}
/>
</Outer>
)
}
}
} else if (e.type === 'video') {
return ( return (
<Outer style={style}> <Outer style={style}>
<VideoItem thumbnail={e.view.thumbnail} alt={e.view.alt} /> <GifItem thumbnail={embed.external.thumb} alt={embed.external.title} />
</Outer>
)
} else if (AppBskyEmbedVideo.isView(embed)) {
return (
<Outer style={style}>
<VideoItem thumbnail={embed.thumbnail} alt={embed.alt} />
</Outer> </Outer>
) )
} }
// media is {$type: string}
return null return null
} }
+18 -20
View File
@@ -59,26 +59,24 @@ export function Inner() {
) : !trending?.topics ? null : ( ) : !trending?.topics ? null : (
<> <>
{trending.topics.map(topic => ( {trending.topics.map(topic => (
<> <TrendingTopicLink
<TrendingTopicLink key={topic.link}
key={topic.link} topic={topic}
topic={topic} onPress={() => {
onPress={() => { logEvent('trendingTopic:click', {context: 'interstitial'})
logEvent('trendingTopic:click', {context: 'interstitial'}) }}>
}}> <View style={[a.py_lg]}>
<View style={[a.py_lg]}> <Text
<Text style={[
style={[ t.atoms.text,
t.atoms.text, a.text_sm,
a.text_sm, a.font_bold,
a.font_bold, {opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar
{opacity: 0.7}, // NOTE: we use opacity 0.7 instead of a color to match the color of the home pager tab bar ]}>
]}> {topic.topic}
{topic.topic} </Text>
</Text> </View>
</View> </TrendingTopicLink>
</TrendingTopicLink>
</>
))} ))}
<Button <Button
label={_(msg`Hide trending topics`)} label={_(msg`Hide trending topics`)}
+9
View File
@@ -568,3 +568,12 @@ export function parseTenorGif(urlp: URL):
dimensions, dimensions,
} }
} }
export function isTenorGifUri(url: URL | string) {
try {
return parseTenorGif(typeof url === 'string' ? new URL(url) : url).success
} catch {
// Invalid URL
return false
}
}
@@ -111,11 +111,7 @@ export function MessageInputEmbed({
) )
const {rt, record} = useMemo(() => { const {rt, record} = useMemo(() => {
if ( if (post && AppBskyFeedPost.isValidRecord(post.record)) {
post &&
AppBskyFeedPost.isRecord(post.record) &&
AppBskyFeedPost.validateRecord(post.record).success
) {
return { return {
rt: new RichTextAPI({ rt: new RichTextAPI({
text: post.record.text, text: post.record.text,
+3 -1
View File
@@ -32,7 +32,9 @@ interface State {
currentStep: Step currentStep: Step
name?: string name?: string
description?: string description?: string
profiles: AppBskyActorDefs.ProfileViewBasic[] profiles: Array<
AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
>
feeds: GeneratorView[] feeds: GeneratorView[]
processing: boolean processing: boolean
error?: string error?: string
+10 -12
View File
@@ -14,7 +14,6 @@ import {QueryClient} from '@tanstack/react-query'
import chunk from 'lodash.chunk' import chunk from 'lodash.chunk'
import {labelIsHideableOffense} from '#/lib/moderation' import {labelIsHideableOffense} from '#/lib/moderation'
import * as atp from '#/types/atproto'
import {precacheProfile} from '../profile' import {precacheProfile} from '../profile'
import {FeedNotification, FeedPage, NotificationType} from './types' import {FeedNotification, FeedPage, NotificationType} from './types'
@@ -256,18 +255,17 @@ function getSubjectUri(
return notif.uri return notif.uri
} else if (type === 'post-like' || type === 'repost') { } else if (type === 'post-like' || type === 'repost') {
if ( if (
atp.fastIsType<AppBskyFeedRepost.Record>( AppBskyFeedRepost.isRecord(notif.record) ||
notif.record, AppBskyFeedLike.isRecord(notif.record)
AppBskyFeedRepost.isRecord,
) ||
atp.fastIsType<AppBskyFeedLike.Record>(
notif.record,
AppBskyFeedLike.isRecord,
)
) { ) {
return typeof notif.record.subject?.uri === 'string' // Type casting here is actually safe because here we check for the
? notif.record.subject?.uri // actual type bellow
: undefined const record = notif.record as
| AppBskyFeedRepost.Record
| AppBskyFeedLike.Record
const uri = record.subject?.uri
return typeof uri === 'string' ? uri : undefined
} }
} else if (type === 'feedgen-like') { } else if (type === 'feedgen-like') {
return notif.reasonSubject return notif.reasonSubject
+8 -11
View File
@@ -6,10 +6,9 @@ import {
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
} from '@atproto/api' } from '@atproto/api'
import {$Typed} from '@atproto/api/dist/client/util'
import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query' import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query'
import * as atp from '#/types/atproto'
export async function truncateAndInvalidate<T = any>( export async function truncateAndInvalidate<T = any>(
queryClient: QueryClient, queryClient: QueryClient,
queryKey: QueryKey, queryKey: QueryKey,
@@ -44,22 +43,20 @@ export function didOrHandleUriMatches(
} }
export function getEmbeddedPost( export function getEmbeddedPost(
v: unknown, v:
| undefined
| $Typed<AppBskyEmbedRecord.View>
| $Typed<AppBskyEmbedRecordWithMedia.View>
| {$type: string},
): AppBskyEmbedRecord.ViewRecord | undefined { ): AppBskyEmbedRecord.ViewRecord | undefined {
if (atp.fastIsType<AppBskyEmbedRecord.View>(v, AppBskyEmbedRecord.isView)) { if (AppBskyEmbedRecord.isView(v)) {
if ( if (
AppBskyEmbedRecord.isViewRecord(v.record) && AppBskyEmbedRecord.isViewRecord(v.record) &&
AppBskyFeedPost.isRecord(v.record.value) AppBskyFeedPost.isRecord(v.record.value)
) { ) {
return v.record return v.record
} }
} } else if (AppBskyEmbedRecordWithMedia.isView(v)) {
if (
atp.fastIsType<AppBskyEmbedRecordWithMedia.View>(
v,
AppBskyEmbedRecordWithMedia.isView,
)
) {
if ( if (
AppBskyEmbedRecord.isViewRecord(v.record.record) && AppBskyEmbedRecord.isViewRecord(v.record.record) &&
AppBskyFeedPost.isRecord(v.record.record.value) AppBskyFeedPost.isRecord(v.record.record.value)
+2 -2
View File
@@ -16,9 +16,9 @@ export * as profile from '#/types/atproto/profile'
* } * }
* ``` * ```
*/ */
export function fastIsType<R>( export function fastIsType<R extends {$type?: string}>(
record: unknown, record: unknown,
identity: <V>(v: V) => boolean, identity: <V>(v: V) => v is V & {$type: NonNullable<R['$type']>},
): record is R { ): record is R {
return identity(record) return identity(record)
} }
-136
View File
@@ -1,136 +0,0 @@
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedVideo,
AppBskyFeedDefs,
AppBskyGraphDefs,
AppBskyLabelerDefs,
} from '@atproto/api'
export type View =
| {
type: 'post'
view: AppBskyEmbedRecord.ViewRecord
}
| {
type: 'post_not_found'
view: AppBskyEmbedRecord.ViewNotFound
}
| {
type: 'post_blocked'
view: AppBskyEmbedRecord.ViewBlocked
}
| {
type: 'post_detached'
view: AppBskyEmbedRecord.ViewDetached
}
| {
type: 'feed'
view: AppBskyFeedDefs.GeneratorView
}
| {
type: 'list'
view: AppBskyGraphDefs.ListView
}
| {
type: 'labeler'
view: AppBskyLabelerDefs.LabelerView
}
| {
type: 'starter_pack'
view: AppBskyGraphDefs.StarterPackViewBasic
}
| {
type: 'images'
view: AppBskyEmbedImages.View
}
| {
type: 'link'
view: AppBskyEmbedExternal.View
}
| {
type: 'video'
view: AppBskyEmbedVideo.View
}
| {
type: 'post_with_media'
view: View | undefined
media: View | undefined
}
export function parseEmbedView(
view: AppBskyEmbedRecord.View,
): View | undefined {
if (AppBskyEmbedRecord.isViewRecord(view.record)) {
return {
type: 'post',
view: view.record,
}
} else if (AppBskyEmbedRecord.isViewNotFound(view.record)) {
return {
type: 'post_not_found',
view: view.record,
}
} else if (AppBskyEmbedRecord.isViewBlocked(view.record)) {
return {
type: 'post_blocked',
view: view.record,
}
} else if (AppBskyEmbedRecord.isViewDetached(view.record)) {
return {
type: 'post_detached',
view: view.record,
}
} else if (AppBskyFeedDefs.isGeneratorView(view.record)) {
return {
type: 'feed',
view: view.record,
}
} else if (AppBskyGraphDefs.isListView(view.record)) {
return {
type: 'list',
view: view.record,
}
} else if (AppBskyLabelerDefs.isLabelerView(view.record)) {
return {
type: 'labeler',
view: view.record,
}
} else if (AppBskyGraphDefs.isStarterPackViewBasic(view.record)) {
return {
type: 'starter_pack',
view: view.record,
}
}
}
export function parseEmbed(
embed: AppBskyFeedDefs.PostView['embed'],
): View | undefined {
if (AppBskyEmbedImages.isView(embed)) {
return {
type: 'images',
view: embed,
}
} else if (AppBskyEmbedExternal.isView(embed)) {
return {
type: 'link',
view: embed,
}
} else if (AppBskyEmbedVideo.isView(embed)) {
return {
type: 'video',
view: embed,
}
} else if (AppBskyEmbedRecord.isView(embed)) {
return parseEmbedView(embed)
} else if (AppBskyEmbedRecordWithMedia.isView(embed)) {
return {
type: 'post_with_media',
view: parseEmbedView(embed.record),
media: parseEmbed(embed.media),
}
}
}
+2 -4
View File
@@ -42,10 +42,8 @@ export function anyToBasic(
associated: view.associated, associated: view.associated,
viewer: view.viewer, viewer: view.viewer,
labels: view.labels, labels: view.labels,
// @ts-expect-error `createdAt` doesn't exist on chat view // `createdAt` doesn't exist in ChatBskyActorDefs.ProfileViewBasic
createdAt: ChatBskyActorDefs.isProfileViewBasic(view) createdAt: 'createdAt' in view ? view.createdAt : undefined,
? undefined
: view.createdAt,
} }
} }