+44
-22
src/lib/api/index.ts
+44
-22
src/lib/api/index.ts
···
14
14
type ComAtprotoRepoStrongRef,
15
15
RichText,
16
16
} from '@atproto/api'
17
-
import {TID} from '@atproto/common-web'
17
+
import { TID } from '@atproto/common-web'
18
18
import * as dcbor from '@ipld/dag-cbor'
19
-
import {t} from '@lingui/macro'
20
-
import {type QueryClient} from '@tanstack/react-query'
21
-
import {sha256} from 'js-sha256'
22
-
import {CID} from 'multiformats/cid'
19
+
import { t } from '@lingui/macro'
20
+
import { type QueryClient } from '@tanstack/react-query'
21
+
import { sha256 } from 'js-sha256'
22
+
import { CID } from 'multiformats/cid'
23
23
import * as Hasher from 'multiformats/hashes/hasher'
24
24
25
-
import {isNetworkError} from '#/lib/strings/errors'
26
-
import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
27
-
import {logger} from '#/logger'
28
-
import {compressImage} from '#/state/gallery'
25
+
import { isNetworkError } from '#/lib/strings/errors'
26
+
import { shortenLinks, stripInvalidMentions, parseMarkdownLinks } from '#/lib/strings/rich-text-manip'
27
+
import { logger } from '#/logger'
28
+
import { compressImage } from '#/state/gallery'
29
29
import {
30
30
fetchResolveGifQuery,
31
31
fetchResolveLinkQuery,
···
39
39
type PostDraft,
40
40
type ThreadDraft,
41
41
} from '#/view/com/composer/state/composer'
42
-
import {createGIFDescription} from '../gif-alt-text'
43
-
import {uploadBlob} from './upload-blob'
42
+
import { createGIFDescription } from '../gif-alt-text'
43
+
import { uploadBlob } from './upload-blob'
44
44
45
-
export {uploadBlob}
45
+
export { uploadBlob }
46
46
47
47
interface PostOpts {
48
48
thread: ThreadDraft
···
96
96
if (draft.labels.length) {
97
97
labels = {
98
98
$type: 'com.atproto.label.defs#selfLabels',
99
-
values: draft.labels.map(val => ({val})),
99
+
values: draft.labels.map(val => ({ val })),
100
100
}
101
101
}
102
102
···
190
190
}
191
191
}
192
192
193
-
return {uris}
193
+
return { uris }
194
194
}
195
195
196
196
async function resolveRT(agent: BskyAgent, richtext: RichText) {
···
199
199
.replace(/^(\s*\n)+/, '')
200
200
// Trim any trailing whitespace.
201
201
.trimEnd()
202
-
let rt = new RichText({text: trimmedText}, {cleanNewlines: true})
202
+
203
+
const { text: parsedText, facets: markdownFacets } =
204
+
parseMarkdownLinks(trimmedText)
205
+
206
+
let rt = new RichText({ text: parsedText }, { cleanNewlines: true })
203
207
await rt.detectFacets(agent)
204
208
209
+
if (markdownFacets.length > 0) {
210
+
const nonOverlapping = (rt.facets || []).filter(f => {
211
+
return !markdownFacets.some(mf => {
212
+
return (
213
+
(f.index.byteStart >= mf.index.byteStart &&
214
+
f.index.byteStart < mf.index.byteEnd) ||
215
+
(f.index.byteEnd > mf.index.byteStart &&
216
+
f.index.byteEnd <= mf.index.byteEnd) ||
217
+
(mf.index.byteStart >= f.index.byteStart &&
218
+
mf.index.byteStart < f.index.byteEnd)
219
+
)
220
+
})
221
+
})
222
+
rt.facets = [...nonOverlapping, ...markdownFacets].sort(
223
+
(a, b) => a.index.byteStart - b.index.byteStart,
224
+
)
225
+
}
226
+
205
227
rt = shortenLinks(rt)
206
228
rt = stripInvalidMentions(rt)
207
229
return rt
···
303
325
const images: AppBskyEmbedImages.Image[] = await Promise.all(
304
326
imagesDraft.map(async (image, i) => {
305
327
logger.debug(`Compressing image #${i}`)
306
-
const {path, width, height, mime} = await compressImage(image)
328
+
const { path, width, height, mime } = await compressImage(image)
307
329
logger.debug(`Uploading image #${i}`)
308
330
const res = await uploadBlob(agent, path, mime)
309
331
return {
310
332
image: res.data.blob,
311
333
alt: image.alt,
312
-
aspectRatio: {width, height},
334
+
aspectRatio: { width, height },
313
335
}
314
336
}),
315
337
)
···
327
349
videoDraft.captions
328
350
.filter(caption => caption.lang !== '')
329
351
.map(async caption => {
330
-
const {data} = await agent.uploadBlob(caption.file, {
352
+
const { data } = await agent.uploadBlob(caption.file, {
331
353
encoding: 'text/vtt',
332
354
})
333
-
return {lang: caption.lang, file: data.blob}
355
+
return { lang: caption.lang, file: data.blob }
334
356
}),
335
357
)
336
358
···
340
362
341
363
// aspect ratio values must be >0 - better to leave as unset otherwise
342
364
// posting will fail if aspect ratio is set to 0
343
-
const aspectRatio = width > 0 && height > 0 ? {width, height} : undefined
365
+
const aspectRatio = width > 0 && height > 0 ? { width, height } : undefined
344
366
345
367
if (!aspectRatio) {
346
368
logger.error(
···
366
388
let blob: BlobRef | undefined
367
389
if (resolvedGif.thumb) {
368
390
onStateChange?.(t`Uploading link thumbnail...`)
369
-
const {path, mime} = resolvedGif.thumb.source
391
+
const { path, mime } = resolvedGif.thumb.source
370
392
const response = await uploadBlob(agent, path, mime)
371
393
blob = response.data.blob
372
394
}
···
390
412
let blob: BlobRef | undefined
391
413
if (resolvedLink.thumb) {
392
414
onStateChange?.(t`Uploading link thumbnail...`)
393
-
const {path, mime} = resolvedLink.thumb.source
415
+
const { path, mime } = resolvedLink.thumb.source
394
416
const response = await uploadBlob(agent, path, mime)
395
417
blob = response.data.blob
396
418
}
+46
-3
src/lib/strings/rich-text-manip.ts
+46
-3
src/lib/strings/rich-text-manip.ts
···
1
-
import {AppBskyRichtextFacet, type RichText, UnicodeString} from '@atproto/api'
1
+
import { AppBskyRichtextFacet, type RichText, UnicodeString } from '@atproto/api'
2
2
3
-
import {toShortUrl} from './url-helpers'
3
+
import { toShortUrl } from './url-helpers'
4
4
5
5
export function shortenLinks(rt: RichText): RichText {
6
6
if (!rt.facets?.length) {
···
16
16
}
17
17
18
18
// extract and shorten the URL
19
-
const {byteStart, byteEnd} = facet.index
19
+
const { byteStart, byteEnd } = facet.index
20
20
const url = rt.unicodeText.slice(byteStart, byteEnd)
21
21
const shortened = new UnicodeString(toShortUrl(url))
22
22
···
49
49
}
50
50
return rt
51
51
}
52
+
53
+
export function parseMarkdownLinks(text: string): {
54
+
text: string
55
+
facets: AppBskyRichtextFacet.Main[]
56
+
} {
57
+
const regex = /\[([^\]]+)\]\(([^)]+)\)/g
58
+
let match
59
+
let newText = ''
60
+
let lastIndex = 0
61
+
const facets: AppBskyRichtextFacet.Main[] = []
62
+
63
+
while ((match = regex.exec(text)) !== null) {
64
+
const [fullMatch, linkText, linkUrl] = match
65
+
const matchStart = match.index
66
+
newText += text.slice(lastIndex, matchStart)
67
+
const startByte = new UnicodeString(newText).length
68
+
newText += linkText
69
+
const endByte = new UnicodeString(newText).length
70
+
let validUrl = linkUrl
71
+
if (!validUrl.startsWith('http://') && !validUrl.startsWith('https://') && !validUrl.startsWith('mailto:')) {
72
+
validUrl = `https://${validUrl}`
73
+
}
74
+
75
+
facets.push({
76
+
index: {
77
+
byteStart: startByte,
78
+
byteEnd: endByte,
79
+
},
80
+
features: [
81
+
{
82
+
$type: 'app.bsky.richtext.facet#link',
83
+
uri: validUrl,
84
+
},
85
+
],
86
+
})
87
+
88
+
lastIndex = matchStart + fullMatch.length
89
+
}
90
+
91
+
newText += text.slice(lastIndex)
92
+
93
+
return { text: newText, facets }
94
+
}