suggestions
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user