diff options
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/lib/api.ts | 148 | ||||
-rw-r--r-- | src/state/models/feed-view.ts | 2 | ||||
-rw-r--r-- | src/state/models/post-thread-view.ts | 3 | ||||
-rw-r--r-- | src/state/models/shell-ui.ts | 11 |
4 files changed, 140 insertions, 24 deletions
diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts index e3edb2871..ec4338b73 100644 --- a/src/state/lib/api.ts +++ b/src/state/lib/api.ts @@ -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) { diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 75ed82684..503e2a4c6 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -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 diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts index a71587d87..ea9d123d0 100644 --- a/src/state/models/post-thread-view.ts +++ b/src/state/models/post-thread-view.ts @@ -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[] diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts index 01e1cacad..90c6ef475 100644 --- a/src/state/models/shell-ui.ts +++ b/src/state/models/shell-ui.ts @@ -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 } |