Add post embeds (images and external links)
This commit is contained in:
parent
345ec83f26
commit
4966b2152e
30 changed files with 936 additions and 242 deletions
|
|
@ -5,12 +5,15 @@
|
|||
|
||||
// import {ReactNativeStore} from './auth'
|
||||
import {sessionClient as AtpApi} from '../../third-party/api'
|
||||
import * as Profile from '../../third-party/api/src/client/types/app/bsky/actor/profile'
|
||||
import * as Post from '../../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import {APP_BSKY_GRAPH} from '../../third-party/api'
|
||||
import * as AppBskyEmbedImages from '../../third-party/api/src/client/types/app/bsky/embed/images'
|
||||
import * as AppBskyEmbedExternal from '../../third-party/api/src/client/types/app/bsky/embed/External'
|
||||
import {RootStoreModel} from '../models/root-store'
|
||||
import {extractEntities} from '../../lib/strings'
|
||||
import {isNetworkError} from '../../lib/errors'
|
||||
import {downloadAndResize} from '../../lib/download'
|
||||
import {getLikelyType, LikelyType, getLinkMeta} from '../../lib/link-meta'
|
||||
|
||||
const TIMEOUT = 10e3 // 10s
|
||||
|
||||
|
|
@ -21,12 +24,112 @@ export function doPolyfill() {
|
|||
export async function post(
|
||||
store: RootStoreModel,
|
||||
text: string,
|
||||
replyTo?: Post.ReplyRef,
|
||||
replyTo?: string,
|
||||
images?: string[],
|
||||
knownHandles?: Set<string>,
|
||||
onStateChange?: (state: string) => void,
|
||||
) {
|
||||
let embed: AppBskyEmbedImages.Main | AppBskyEmbedExternal.Main | undefined
|
||||
let reply
|
||||
|
||||
onStateChange?.('Processing...')
|
||||
const entities = extractEntities(text, knownHandles)
|
||||
if (entities) {
|
||||
for (const ent of entities) {
|
||||
if (ent.type === 'mention') {
|
||||
const prof = await store.profiles.getProfile(ent.value)
|
||||
ent.value = prof.data.did
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (images?.length) {
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.images',
|
||||
images: [],
|
||||
} as AppBskyEmbedImages.Main
|
||||
let i = 1
|
||||
for (const image of images) {
|
||||
onStateChange?.(`Uploading image #${i++}...`)
|
||||
const res = await store.api.com.atproto.blob.upload(
|
||||
image, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
|
||||
{encoding: 'image/jpeg'},
|
||||
)
|
||||
embed.images.push({
|
||||
image: {
|
||||
cid: res.data.cid,
|
||||
mimeType: 'image/jpeg',
|
||||
},
|
||||
alt: '', // TODO supply alt text
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!embed && entities) {
|
||||
const link = entities.find(
|
||||
ent =>
|
||||
ent.type === 'link' &&
|
||||
getLikelyType(ent.value || '') === LikelyType.HTML,
|
||||
)
|
||||
if (link) {
|
||||
try {
|
||||
onStateChange?.(`Fetching link metadata...`)
|
||||
let thumb
|
||||
const linkMeta = await getLinkMeta(link.value)
|
||||
if (linkMeta.image) {
|
||||
onStateChange?.(`Downloading link thumbnail...`)
|
||||
const thumbLocal = await downloadAndResize({
|
||||
uri: linkMeta.image,
|
||||
width: 250,
|
||||
height: 250,
|
||||
mode: 'contain',
|
||||
timeout: 15e3,
|
||||
}).catch(() => undefined)
|
||||
if (thumbLocal) {
|
||||
onStateChange?.(`Uploading link thumbnail...`)
|
||||
let encoding
|
||||
if (thumbLocal.uri.endsWith('.png')) {
|
||||
encoding = 'image/png'
|
||||
} else if (
|
||||
thumbLocal.uri.endsWith('.jpeg') ||
|
||||
thumbLocal.uri.endsWith('.jpg')
|
||||
) {
|
||||
encoding = 'image/jpeg'
|
||||
} else {
|
||||
console.error(
|
||||
'Unexpected image format for thumbnail, skipping',
|
||||
thumbLocal.uri,
|
||||
)
|
||||
}
|
||||
if (encoding) {
|
||||
const thumbUploadRes = await store.api.com.atproto.blob.upload(
|
||||
thumbLocal.uri, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts
|
||||
{encoding},
|
||||
)
|
||||
thumb = {
|
||||
cid: thumbUploadRes.data.cid,
|
||||
mimeType: encoding,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
embed = {
|
||||
$type: 'app.bsky.embed.external',
|
||||
external: {
|
||||
uri: link.value,
|
||||
title: linkMeta.title || linkMeta.url,
|
||||
description: linkMeta.description || '',
|
||||
thumb,
|
||||
},
|
||||
} as AppBskyEmbedExternal.Main
|
||||
} catch (e: any) {
|
||||
console.error('Failed to fetch link meta', link.value, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (replyTo) {
|
||||
const replyToUrip = new AtUri(replyTo.uri)
|
||||
const replyToUrip = new AtUri(replyTo)
|
||||
const parentPost = await store.api.app.bsky.feed.post.get({
|
||||
user: replyToUrip.host,
|
||||
rkey: replyToUrip.rkey,
|
||||
|
|
@ -42,24 +145,29 @@ export async function post(
|
|||
}
|
||||
}
|
||||
}
|
||||
const entities = extractEntities(text, knownHandles)
|
||||
if (entities) {
|
||||
for (const ent of entities) {
|
||||
if (ent.type === 'mention') {
|
||||
const prof = await store.profiles.getProfile(ent.value)
|
||||
ent.value = prof.data.did
|
||||
}
|
||||
|
||||
try {
|
||||
onStateChange?.(`Posting...`)
|
||||
return await store.api.app.bsky.feed.post.create(
|
||||
{did: store.me.did || ''},
|
||||
{
|
||||
text,
|
||||
reply,
|
||||
embed,
|
||||
entities,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
} catch (e: any) {
|
||||
console.error(`Failed to create post: ${e.toString()}`)
|
||||
if (isNetworkError(e)) {
|
||||
throw new Error(
|
||||
'Post failed to upload. Please check your Internet connection and try again.',
|
||||
)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return await store.api.app.bsky.feed.post.create(
|
||||
{did: store.me.did || ''},
|
||||
{
|
||||
text,
|
||||
reply,
|
||||
entities,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function repost(store: RootStoreModel, uri: string, cid: string) {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export class FeedItemModel implements GetTimeline.FeedItem {
|
|||
repostedBy?: ActorRef.WithInfo
|
||||
trendedBy?: ActorRef.WithInfo
|
||||
record: Record<string, unknown> = {}
|
||||
embed?: GetTimeline.FeedItem['embed']
|
||||
replyCount: number = 0
|
||||
repostCount: number = 0
|
||||
upvoteCount: number = 0
|
||||
|
|
@ -78,6 +79,7 @@ export class FeedItemModel implements GetTimeline.FeedItem {
|
|||
this.repostedBy = v.repostedBy
|
||||
this.trendedBy = v.trendedBy
|
||||
this.record = v.record
|
||||
this.embed = v.embed
|
||||
this.replyCount = v.replyCount
|
||||
this.repostCount = v.repostCount
|
||||
this.upvoteCount = v.upvoteCount
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {AppBskyFeedGetPostThread as GetPostThread} from '../../third-party/api'
|
||||
import * as Embed from '../../third-party/api/src/client/types/app/bsky/feed/embed'
|
||||
import * as ActorRef from '../../third-party/api/src/client/types/app/bsky/actor/ref'
|
||||
import {AtUri} from '../../third-party/uri'
|
||||
import _omit from 'lodash.omit'
|
||||
|
|
@ -60,7 +59,7 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
|
|||
declaration: {cid: '', actorType: ''},
|
||||
}
|
||||
record: Record<string, unknown> = {}
|
||||
embed?: Embed.Main = undefined
|
||||
embed?: GetPostThread.Post['embed'] = undefined
|
||||
parent?: PostThreadViewPostModel
|
||||
replyCount: number = 0
|
||||
replies?: PostThreadViewPostModel[]
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ export class ProfileImageLightbox {
|
|||
}
|
||||
}
|
||||
|
||||
export class ImageLightbox {
|
||||
name = 'image'
|
||||
constructor(public uri: string) {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComposerOptsPostRef {
|
||||
uri: string
|
||||
cid: string
|
||||
|
|
@ -84,7 +91,7 @@ export class ShellUiModel {
|
|||
| ServerInputModal
|
||||
| undefined
|
||||
isLightboxActive = false
|
||||
activeLightbox: ProfileImageLightbox | undefined
|
||||
activeLightbox: ProfileImageLightbox | ImageLightbox | undefined
|
||||
isComposerActive = false
|
||||
composerOpts: ComposerOpts | undefined
|
||||
|
||||
|
|
@ -116,7 +123,7 @@ export class ShellUiModel {
|
|||
this.activeModal = undefined
|
||||
}
|
||||
|
||||
openLightbox(lightbox: ProfileImageLightbox) {
|
||||
openLightbox(lightbox: ProfileImageLightbox | ImageLightbox) {
|
||||
this.isLightboxActive = true
|
||||
this.activeLightbox = lightbox
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue