mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at offline-indicator 288 lines 7.5 kB view raw
1import { 2 AppBskyEmbedImages, 3 AppBskyEmbedExternal, 4 AppBskyEmbedRecord, 5 AppBskyEmbedRecordWithMedia, 6 AppBskyRichtextFacet, 7 ComAtprotoLabelDefs, 8 ComAtprotoRepoUploadBlob, 9 RichText, 10} from '@atproto/api' 11import {AtUri} from '@atproto/api' 12import {RootStoreModel} from 'state/models/root-store' 13import {isNetworkError} from 'lib/strings/errors' 14import {LinkMeta} from '../link-meta/link-meta' 15import {isWeb} from 'platform/detection' 16import {ImageModel} from 'state/models/media/image' 17import {shortenLinks} from 'lib/strings/rich-text-manip' 18 19export interface ExternalEmbedDraft { 20 uri: string 21 isLoading: boolean 22 meta?: LinkMeta 23 embed?: AppBskyEmbedRecord.Main 24 localThumb?: ImageModel 25} 26 27export async function resolveName(store: RootStoreModel, didOrHandle: string) { 28 if (!didOrHandle) { 29 throw new Error('Invalid handle: ""') 30 } 31 if (didOrHandle.startsWith('did:')) { 32 return didOrHandle 33 } 34 35 // we run the resolution always to ensure freshness 36 const promise = store.agent 37 .resolveHandle({ 38 handle: didOrHandle, 39 }) 40 .then(res => { 41 store.handleResolutions.cache.set(didOrHandle, res.data.did) 42 return res.data.did 43 }) 44 45 // but we can return immediately if it's cached 46 const cached = store.handleResolutions.cache.get(didOrHandle) 47 if (cached) { 48 return cached 49 } 50 51 return promise 52} 53 54export async function uploadBlob( 55 store: RootStoreModel, 56 blob: string, 57 encoding: string, 58): Promise<ComAtprotoRepoUploadBlob.Response> { 59 if (isWeb) { 60 // `blob` should be a data uri 61 return store.agent.uploadBlob(convertDataURIToUint8Array(blob), { 62 encoding, 63 }) 64 } else { 65 // `blob` should be a path to a file in the local FS 66 return store.agent.uploadBlob( 67 blob, // this will be special-cased by the fetch monkeypatch in /src/state/lib/api.ts 68 {encoding}, 69 ) 70 } 71} 72 73interface PostOpts { 74 rawText: string 75 replyTo?: string 76 quote?: { 77 uri: string 78 cid: string 79 } 80 extLink?: ExternalEmbedDraft 81 images?: ImageModel[] 82 labels?: string[] 83 knownHandles?: Set<string> 84 onStateChange?: (state: string) => void 85 langs?: string[] 86} 87 88export async function post(store: RootStoreModel, opts: PostOpts) { 89 let embed: 90 | AppBskyEmbedImages.Main 91 | AppBskyEmbedExternal.Main 92 | AppBskyEmbedRecord.Main 93 | AppBskyEmbedRecordWithMedia.Main 94 | undefined 95 let reply 96 let rt = new RichText( 97 {text: opts.rawText.trim()}, 98 { 99 cleanNewlines: true, 100 }, 101 ) 102 103 opts.onStateChange?.('Processing...') 104 await rt.detectFacets(store.agent) 105 rt = shortenLinks(rt) 106 107 // filter out any mention facets that didn't map to a user 108 rt.facets = rt.facets?.filter(facet => { 109 const mention = facet.features.find(feature => 110 AppBskyRichtextFacet.isMention(feature), 111 ) 112 if (mention && !mention.did) { 113 return false 114 } 115 return true 116 }) 117 118 // add quote embed if present 119 if (opts.quote) { 120 embed = { 121 $type: 'app.bsky.embed.record', 122 record: { 123 uri: opts.quote.uri, 124 cid: opts.quote.cid, 125 }, 126 } as AppBskyEmbedRecord.Main 127 } 128 129 // add image embed if present 130 if (opts.images?.length) { 131 const images: AppBskyEmbedImages.Image[] = [] 132 for (const image of opts.images) { 133 opts.onStateChange?.(`Uploading image #${images.length + 1}...`) 134 await image.compress() 135 const path = image.compressed?.path ?? image.path 136 const res = await uploadBlob(store, path, 'image/jpeg') 137 images.push({ 138 image: res.data.blob, 139 alt: image.altText ?? '', 140 }) 141 } 142 143 if (opts.quote) { 144 embed = { 145 $type: 'app.bsky.embed.recordWithMedia', 146 record: embed, 147 media: { 148 $type: 'app.bsky.embed.images', 149 images, 150 }, 151 } as AppBskyEmbedRecordWithMedia.Main 152 } else { 153 embed = { 154 $type: 'app.bsky.embed.images', 155 images, 156 } as AppBskyEmbedImages.Main 157 } 158 } 159 160 // add external embed if present 161 if (opts.extLink && !opts.images?.length) { 162 if (opts.extLink.embed) { 163 embed = opts.extLink.embed 164 } else { 165 let thumb 166 if (opts.extLink.localThumb) { 167 opts.onStateChange?.('Uploading link thumbnail...') 168 let encoding 169 if (opts.extLink.localThumb.mime) { 170 encoding = opts.extLink.localThumb.mime 171 } else if (opts.extLink.localThumb.path.endsWith('.png')) { 172 encoding = 'image/png' 173 } else if ( 174 opts.extLink.localThumb.path.endsWith('.jpeg') || 175 opts.extLink.localThumb.path.endsWith('.jpg') 176 ) { 177 encoding = 'image/jpeg' 178 } else { 179 store.log.warn( 180 'Unexpected image format for thumbnail, skipping', 181 opts.extLink.localThumb.path, 182 ) 183 } 184 if (encoding) { 185 const thumbUploadRes = await uploadBlob( 186 store, 187 opts.extLink.localThumb.path, 188 encoding, 189 ) 190 thumb = thumbUploadRes.data.blob 191 } 192 } 193 194 if (opts.quote) { 195 embed = { 196 $type: 'app.bsky.embed.recordWithMedia', 197 record: embed, 198 media: { 199 $type: 'app.bsky.embed.external', 200 external: { 201 uri: opts.extLink.uri, 202 title: opts.extLink.meta?.title || '', 203 description: opts.extLink.meta?.description || '', 204 thumb, 205 }, 206 } as AppBskyEmbedExternal.Main, 207 } as AppBskyEmbedRecordWithMedia.Main 208 } else { 209 embed = { 210 $type: 'app.bsky.embed.external', 211 external: { 212 uri: opts.extLink.uri, 213 title: opts.extLink.meta?.title || '', 214 description: opts.extLink.meta?.description || '', 215 thumb, 216 }, 217 } as AppBskyEmbedExternal.Main 218 } 219 } 220 } 221 222 // add replyTo if post is a reply to another post 223 if (opts.replyTo) { 224 const replyToUrip = new AtUri(opts.replyTo) 225 const parentPost = await store.agent.getPost({ 226 repo: replyToUrip.host, 227 rkey: replyToUrip.rkey, 228 }) 229 if (parentPost) { 230 const parentRef = { 231 uri: parentPost.uri, 232 cid: parentPost.cid, 233 } 234 reply = { 235 root: parentPost.value.reply?.root || parentRef, 236 parent: parentRef, 237 } 238 } 239 } 240 241 // set labels 242 let labels: ComAtprotoLabelDefs.SelfLabels | undefined 243 if (opts.labels?.length) { 244 labels = { 245 $type: 'com.atproto.label.defs#selfLabels', 246 values: opts.labels.map(val => ({val})), 247 } 248 } 249 250 // add top 3 languages from user preferences if langs is provided 251 let langs = opts.langs 252 if (opts.langs) { 253 langs = opts.langs.slice(0, 3) 254 } 255 256 try { 257 opts.onStateChange?.('Posting...') 258 return await store.agent.post({ 259 text: rt.text, 260 facets: rt.facets, 261 reply, 262 embed, 263 langs, 264 labels, 265 }) 266 } catch (e: any) { 267 console.error(`Failed to create post: ${e.toString()}`) 268 if (isNetworkError(e)) { 269 throw new Error( 270 'Post failed to upload. Please check your Internet connection and try again.', 271 ) 272 } else { 273 throw e 274 } 275 } 276} 277 278// helpers 279// = 280 281function convertDataURIToUint8Array(uri: string): Uint8Array { 282 var raw = window.atob(uri.substring(uri.indexOf(';base64,') + 8)) 283 var binary = new Uint8Array(new ArrayBuffer(raw.length)) 284 for (let i = 0; i < raw.length; i++) { 285 binary[i] = raw.charCodeAt(i) 286 } 287 return binary 288}