Bluesky app fork with some witchin' additions 💫
witchsky.app
bluesky
fork
client
1import {RichText} from '@atproto/api'
2import {i18n} from '@lingui/core'
3
4import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets'
5import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player'
6import {
7 createStarterPackGooglePlayUri,
8 createStarterPackLinkFromAndroidReferrer,
9 parseStarterPackUri,
10} from '#/lib/strings/starter-pack'
11import {messages} from '#/locale/locales/en/messages'
12import {klipyUrlToBskyGifUrl} from '#/state/queries/klipy'
13import {tenorUrlToBskyGifUrl} from '#/state/queries/tenor'
14import {cleanError} from '../../src/lib/strings/errors'
15import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
16import {enforceLen} from '../../src/lib/strings/helpers'
17import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
18import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
19import {
20 makeRecordUri,
21 toNiceDomain,
22 toShareUrl,
23 toShortUrl,
24} from '../../src/lib/strings/url-helpers'
25
26describe('detectLinkables', () => {
27 const inputs = [
28 'no linkable',
29 '@start middle end',
30 'start @middle end',
31 'start middle @end',
32 '@start @middle @end',
33 '@full123.test-of-chars',
34 'not@right',
35 '@bad!@#$chars',
36 '@newline1\n@newline2',
37 'parenthetical (@handle)',
38 'start https://middle.com end',
39 'start https://middle.com/foo/bar end',
40 'start https://middle.com/foo/bar?baz=bux end',
41 'start https://middle.com/foo/bar?baz=bux#hash end',
42 'https://start.com/foo/bar?baz=bux#hash middle end',
43 'start middle https://end.com/foo/bar?baz=bux#hash',
44 'https://newline1.com\nhttps://newline2.com',
45 'start middle.com end',
46 'start middle.com/foo/bar end',
47 'start middle.com/foo/bar?baz=bux end',
48 'start middle.com/foo/bar?baz=bux#hash end',
49 'start.com/foo/bar?baz=bux#hash middle end',
50 'start middle end.com/foo/bar?baz=bux#hash',
51 'newline1.com\nnewline2.com',
52 'not.. a..url ..here',
53 'e.g.',
54 'e.g. real.com fake.notreal',
55 'something-cool.jpg',
56 'website.com.jpg',
57 'e.g./foo',
58 'website.com.jpg/foo',
59 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
60 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/ ',
61 'https://foo.com https://bar.com/whatever https://baz.com',
62 'punctuation https://foo.com, https://bar.com/whatever; https://baz.com.',
63 'parenthetical (https://foo.com)',
64 'except for https://foo.com/thing_(cool)',
65 ]
66 const outputs = [
67 ['no linkable'],
68 [{link: '@start'}, ' middle end'],
69 ['start ', {link: '@middle'}, ' end'],
70 ['start middle ', {link: '@end'}],
71 [{link: '@start'}, ' ', {link: '@middle'}, ' ', {link: '@end'}],
72 [{link: '@full123.test-of-chars'}],
73 ['not@right'],
74 [{link: '@bad'}, '!@#$chars'],
75 [{link: '@newline1'}, '\n', {link: '@newline2'}],
76 ['parenthetical (', {link: '@handle'}, ')'],
77 ['start ', {link: 'https://middle.com'}, ' end'],
78 ['start ', {link: 'https://middle.com/foo/bar'}, ' end'],
79 ['start ', {link: 'https://middle.com/foo/bar?baz=bux'}, ' end'],
80 ['start ', {link: 'https://middle.com/foo/bar?baz=bux#hash'}, ' end'],
81 [{link: 'https://start.com/foo/bar?baz=bux#hash'}, ' middle end'],
82 ['start middle ', {link: 'https://end.com/foo/bar?baz=bux#hash'}],
83 [{link: 'https://newline1.com'}, '\n', {link: 'https://newline2.com'}],
84 ['start ', {link: 'middle.com'}, ' end'],
85 ['start ', {link: 'middle.com/foo/bar'}, ' end'],
86 ['start ', {link: 'middle.com/foo/bar?baz=bux'}, ' end'],
87 ['start ', {link: 'middle.com/foo/bar?baz=bux#hash'}, ' end'],
88 [{link: 'start.com/foo/bar?baz=bux#hash'}, ' middle end'],
89 ['start middle ', {link: 'end.com/foo/bar?baz=bux#hash'}],
90 [{link: 'newline1.com'}, '\n', {link: 'newline2.com'}],
91 ['not.. a..url ..here'],
92 ['e.g.'],
93 ['e.g. ', {link: 'real.com'}, ' fake.notreal'],
94 ['something-cool.jpg'],
95 ['website.com.jpg'],
96 ['e.g./foo'],
97 ['website.com.jpg/foo'],
98 [
99 'Classic article ',
100 {
101 link: 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
102 },
103 ],
104 [
105 'Classic article ',
106 {
107 link: 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
108 },
109 ' ',
110 ],
111 [
112 {link: 'https://foo.com'},
113 ' ',
114 {link: 'https://bar.com/whatever'},
115 ' ',
116 {link: 'https://baz.com'},
117 ],
118 [
119 'punctuation ',
120 {link: 'https://foo.com'},
121 ', ',
122 {link: 'https://bar.com/whatever'},
123 '; ',
124 {link: 'https://baz.com'},
125 '.',
126 ],
127 ['parenthetical (', {link: 'https://foo.com'}, ')'],
128 ['except for ', {link: 'https://foo.com/thing_(cool)'}],
129 ]
130 it('correctly handles a set of text inputs', () => {
131 for (let i = 0; i < inputs.length; i++) {
132 const input = inputs[i]
133 const output = detectLinkables(input)
134 expect(output).toEqual(outputs[i])
135 }
136 })
137})
138
139describe('makeRecordUri', () => {
140 const inputs: [string, string, string][] = [
141 ['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],
142 ]
143 const outputs = ['at://alice.test/app.bsky.feed.post/3jk7x4irgv52r']
144
145 it('correctly builds a record URI', () => {
146 for (let i = 0; i < inputs.length; i++) {
147 const input = inputs[i]
148 const result = makeRecordUri(...input)
149 expect(result).toEqual(outputs[i])
150 }
151 })
152})
153
154describe('makeValidHandle', () => {
155 const inputs = [
156 'test-handle-123',
157 'test!"#$%&/()=?_',
158 'this-handle-should-be-too-big',
159 ]
160 const outputs = ['test-handle-123', 'test', 'this-handle-should-b']
161
162 it('correctly parses and corrects handles', () => {
163 for (let i = 0; i < inputs.length; i++) {
164 const result = makeValidHandle(inputs[i])
165 expect(result).toEqual(outputs[i])
166 }
167 })
168})
169
170describe('createFullHandle', () => {
171 const inputs: [string, string][] = [
172 ['test-handle-123', 'test'],
173 ['.test.handle', 'test.test.'],
174 ['test.handle.', '.test.test'],
175 ]
176 const outputs = [
177 'test-handle-123.test',
178 '.test.handle.test.test.',
179 'test.handle.test.test',
180 ]
181
182 it('correctly parses and corrects handles', () => {
183 for (let i = 0; i < inputs.length; i++) {
184 const input = inputs[i]
185 const result = createFullHandle(...input)
186 expect(result).toEqual(outputs[i])
187 }
188 })
189})
190
191describe('enforceLen', () => {
192 const inputs: [string, number][] = [
193 ['Hello World!', 5],
194 ['Hello World!', 20],
195 ['', 5],
196 ]
197 const outputs = ['Hello', 'Hello World!', '']
198
199 it('correctly enforces defined length on a given string', () => {
200 for (let i = 0; i < inputs.length; i++) {
201 const input = inputs[i]
202 const result = enforceLen(...input)
203 expect(result).toEqual(outputs[i])
204 }
205 })
206})
207
208describe('cleanError', () => {
209 // cleanError uses lingui
210 i18n.loadAndActivate({locale: 'en', messages})
211
212 const inputs = [
213 'TypeError: Network request failed',
214 'Error: Aborted',
215 'Error: TypeError "x" is not a function',
216 'Error: SyntaxError unexpected token "export"',
217 'Some other error',
218 ]
219 const outputs = [
220 'Unable to connect. Please check your internet connection and try again.',
221 'Unable to connect. Please check your internet connection and try again.',
222 'TypeError "x" is not a function',
223 'SyntaxError unexpected token "export"',
224 'Some other error',
225 ]
226
227 it('removes extra content from error message', () => {
228 for (let i = 0; i < inputs.length; i++) {
229 const result = cleanError(inputs[i])
230 expect(result).toEqual(outputs[i])
231 }
232 })
233})
234
235describe('toNiceDomain', () => {
236 const inputs = [
237 'https://example.com/index.html',
238 'https://bsky.app',
239 'https://bsky.social',
240 '#123123123',
241 ]
242 const outputs = ['example.com', 'bsky.app', 'Bluesky Social', '#123123123']
243
244 it("displays the url's host in a easily readable manner", () => {
245 for (let i = 0; i < inputs.length; i++) {
246 const result = toNiceDomain(inputs[i])
247 expect(result).toEqual(outputs[i])
248 }
249 })
250})
251
252describe('toShortUrl', () => {
253 const inputs = [
254 'https://bsky.app',
255 'https://bsky.app/3jk7x4irgv52r',
256 'https://bsky.app/3jk7x4irgv52r2313y182h9',
257 'https://very-long-domain-name.com/foo',
258 'https://very-long-domain-name.com/foo?bar=baz#andsomemore',
259 ]
260 const outputs = [
261 'bsky.app',
262 'bsky.app/3jk7x4irgv52r',
263 'bsky.app/3jk7x4irgv52...',
264 'very-long-domain-name.com/foo',
265 'very-long-domain-name.com/foo?bar=baz#...',
266 ]
267
268 it('shortens the url', () => {
269 for (let i = 0; i < inputs.length; i++) {
270 const result = toShortUrl(inputs[i])
271 expect(result).toEqual(outputs[i])
272 }
273 })
274})
275
276describe('toShareUrl', () => {
277 const inputs = ['https://bsky.app', '/3jk7x4irgv52r', 'item/test/123']
278 const outputs = [
279 'https://bsky.app',
280 'https://bsky.app/3jk7x4irgv52r',
281 'https://bsky.app/item/test/123',
282 ]
283
284 it('appends https, when not present', () => {
285 for (let i = 0; i < inputs.length; i++) {
286 const result = toShareUrl(inputs[i])
287 expect(result).toEqual(outputs[i])
288 }
289 })
290})
291
292describe('shortenLinks', () => {
293 const inputs = [
294 'start https://middle.com/foo/bar?baz=bux#hash end',
295 'https://start.com/foo/bar?baz=bux#hash middle end',
296 'start middle https://end.com/foo/bar?baz=bux#hash',
297 'https://newline1.com/very/long/url/here\nhttps://newline2.com/very/long/url/here',
298 'Classic article https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
299 ]
300 const outputs = [
301 [
302 'start middle.com/foo/bar?baz=... end',
303 ['https://middle.com/foo/bar?baz=bux#hash'],
304 ],
305 [
306 'start.com/foo/bar?baz=... middle end',
307 ['https://start.com/foo/bar?baz=bux#hash'],
308 ],
309 [
310 'start middle end.com/foo/bar?baz=...',
311 ['https://end.com/foo/bar?baz=bux#hash'],
312 ],
313 [
314 'newline1.com/very/long/ur...\nnewline2.com/very/long/ur...',
315 [
316 'https://newline1.com/very/long/url/here',
317 'https://newline2.com/very/long/url/here',
318 ],
319 ],
320 [
321 'Classic article socket3.wordpress.com/2018/02/03/d...',
322 [
323 'https://socket3.wordpress.com/2018/02/03/designing-windows-95s-user-interface/',
324 ],
325 ],
326 ]
327
328 it('correctly shortens rich text while preserving facet URIs', () => {
329 for (let i = 0; i < inputs.length; i++) {
330 const input = inputs[i]
331 const inputRT = new RichText({text: input})
332 detectFacetsWithoutResolution(inputRT)
333 const outputRT = shortenLinks(inputRT)
334 expect(outputRT.text).toEqual(outputs[i][0])
335 expect(outputRT.facets?.length).toEqual(outputs[i][1].length)
336 for (let j = 0; j < outputs[i][1].length; j++) {
337 // @ts-expect-error whatever
338 expect(outputRT.facets![j].features[0].uri).toEqual(outputs[i][1][j])
339 }
340 }
341 })
342})
343
344describe('parseEmbedPlayerFromUrl', () => {
345 const inputs = [
346 'https://youtu.be/videoId',
347 'https://youtu.be/videoId?t=1s',
348 'https://www.youtube.com/watch?v=videoId',
349 'https://www.youtube.com/watch?v=videoId&feature=share',
350 'https://www.youtube.com/watch?v=videoId&t=1s',
351 'https://youtube.com/watch?v=videoId',
352 'https://youtube.com/watch?v=videoId&feature=share',
353 'https://youtube.com/shorts/videoId',
354 'https://youtube.com/live/videoId',
355 'https://m.youtube.com/watch?v=videoId',
356 'https://music.youtube.com/watch?v=videoId',
357
358 'https://youtube.com/shorts/',
359 'https://youtube.com/',
360 'https://youtube.com/random',
361 'https://youtube.com/live/',
362
363 'https://twitch.tv/channelName',
364 'https://www.twitch.tv/channelName',
365 'https://m.twitch.tv/channelName',
366
367 'https://twitch.tv/channelName/clip/clipId',
368 'https://twitch.tv/videos/videoId',
369
370 'https://open.spotify.com/playlist/playlistId',
371 'https://open.spotify.com/playlist/playlistId?param=value',
372 'https://open.spotify.com/locale/playlist/playlistId',
373
374 'https://open.spotify.com/track/songId',
375 'https://open.spotify.com/track/songId?param=value',
376 'https://open.spotify.com/locale/track/songId',
377
378 'https://open.spotify.com/album/albumId',
379 'https://open.spotify.com/album/albumId?param=value',
380 'https://open.spotify.com/locale/album/albumId',
381
382 'https://soundcloud.com/user/track',
383 'https://soundcloud.com/user/sets/set',
384 'https://soundcloud.com/user/',
385
386 'https://music.apple.com/us/playlist/playlistName/playlistId',
387 'https://music.apple.com/us/album/albumName/albumId',
388 'https://music.apple.com/us/album/albumName/albumId?i=songId',
389 'https://music.apple.com/us/song/songName/songId',
390
391 'https://vimeo.com/videoId',
392 'https://vimeo.com/videoId?autoplay=0',
393
394 'https://giphy.com/gifs/some-random-gif-name-gifId',
395 'https://giphy.com/gif/some-random-gif-name-gifId',
396 'https://giphy.com/gifs/',
397
398 'https://giphy.com/gifs/39248209509382934029?hh=100&ww=100',
399
400 'https://media.giphy.com/media/gifId/giphy.webp',
401 'https://media0.giphy.com/media/gifId/giphy.webp',
402 'https://media1.giphy.com/media/gifId/giphy.gif',
403 'https://media2.giphy.com/media/gifId/giphy.webp',
404 'https://media3.giphy.com/media/gifId/giphy.mp4',
405 'https://media4.giphy.com/media/gifId/giphy.webp',
406 'https://media5.giphy.com/media/gifId/giphy.mp4',
407 'https://media0.giphy.com/media/gifId/giphy.mp3',
408 'https://media1.google.com/media/gifId/giphy.webp',
409
410 'https://media.giphy.com/media/trackingId/gifId/giphy.webp',
411
412 'https://i.giphy.com/media/gifId/giphy.webp',
413 'https://i.giphy.com/media/gifId/giphy.webp',
414 'https://i.giphy.com/gifId.gif',
415 'https://i.giphy.com/gifId.gif',
416
417 'https://tenor.com/view/gifId',
418 'https://tenor.com/notView/gifId',
419 'https://tenor.com/view',
420 'https://tenor.com/view/gifId.gif',
421 'https://tenor.com/intl/view/gifId.gif',
422
423 'https://media.tenor.com/someID_AAAAC/someName.gif?hh=100&ww=100',
424 'https://media.tenor.com/someID_AAAAC/someName.gif',
425 'https://media.tenor.com/someID/someName.gif',
426 'https://media.tenor.com/someID',
427 'https://media.tenor.com',
428
429 'https://www.flickr.com/photos/username/albums/72177720308493661',
430 'https://flickr.com/photos/username/albums/72177720308493661',
431 'https://flickr.com/photos/username/albums/72177720308493661/',
432 'https://flickr.com/photos/username/albums/72177720308493661//',
433 'https://flic.kr/s/aHBqjAES3i',
434
435 'https://flickr.com/foetoes/username/albums/3903',
436 'https://flickr.com/albums/3903',
437 'https://flic.kr/s/OolI',
438 'https://flic.kr/t/aHBqjAES3i',
439
440 'https://www.flickr.com/groups/898944@N23/pool',
441 'https://flickr.com/groups/898944@N23/pool',
442 'https://flickr.com/groups/898944@N23/pool/',
443 'https://flickr.com/groups/898944@N23/pool//',
444 'https://flic.kr/go/8WJtR',
445
446 'https://www.flickr.com/groups/898944@N23/',
447 'https://www.flickr.com/groups',
448
449 'https://maxblansjaar.bandcamp.com/album/false-comforts',
450 'https://grmnygrmny.bandcamp.com/track/fluid',
451 'https://sufjanstevens.bandcamp.com/',
452 'https://sufjanstevens.bandcamp.com',
453 'https://bandcamp.com/',
454 'https://bandcamp.com',
455
456 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
457 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300&mp4=videoSlugMp4&webm=videoSlugWebm',
458 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200',
459 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif',
460 'https://static.klipy.com/other/path.gif?hh=200&ww=300',
461 'https://static.klipy.com',
462 ]
463
464 const outputs = [
465 {
466 type: 'youtube_video',
467 source: 'youtube',
468 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
469 },
470 {
471 type: 'youtube_video',
472 source: 'youtube',
473 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=1',
474 },
475 {
476 type: 'youtube_video',
477 source: 'youtube',
478 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
479 },
480 {
481 type: 'youtube_video',
482 source: 'youtube',
483 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
484 },
485 {
486 type: 'youtube_video',
487 source: 'youtube',
488 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=1',
489 },
490 {
491 type: 'youtube_video',
492 source: 'youtube',
493 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
494 },
495 {
496 type: 'youtube_video',
497 source: 'youtube',
498 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
499 },
500 {
501 type: 'youtube_short',
502 source: 'youtubeShorts',
503 hideDetails: true,
504 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
505 },
506 {
507 type: 'youtube_video',
508 source: 'youtube',
509 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
510 },
511 {
512 type: 'youtube_video',
513 source: 'youtube',
514 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
515 },
516 {
517 type: 'youtube_video',
518 source: 'youtube',
519 playerUri: 'https://bsky.app/iframe/youtube.html?videoId=videoId&start=0',
520 },
521
522 undefined,
523 undefined,
524 undefined,
525 undefined,
526
527 {
528 type: 'twitch_video',
529 source: 'twitch',
530 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
531 },
532 {
533 type: 'twitch_video',
534 source: 'twitch',
535 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
536 },
537 {
538 type: 'twitch_video',
539 source: 'twitch',
540 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
541 },
542 {
543 type: 'twitch_video',
544 source: 'twitch',
545 playerUri: `https://clips.twitch.tv/embed?volume=0.5&autoplay=true&clip=clipId&parent=localhost`,
546 },
547 {
548 type: 'twitch_video',
549 source: 'twitch',
550 playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&video=videoId&parent=localhost`,
551 },
552
553 {
554 type: 'spotify_playlist',
555 source: 'spotify',
556 playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
557 },
558 {
559 type: 'spotify_playlist',
560 source: 'spotify',
561 playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
562 },
563 {
564 type: 'spotify_playlist',
565 source: 'spotify',
566 playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
567 },
568
569 {
570 type: 'spotify_song',
571 source: 'spotify',
572 playerUri: `https://open.spotify.com/embed/track/songId`,
573 },
574 {
575 type: 'spotify_song',
576 source: 'spotify',
577 playerUri: `https://open.spotify.com/embed/track/songId`,
578 },
579 {
580 type: 'spotify_song',
581 source: 'spotify',
582 playerUri: `https://open.spotify.com/embed/track/songId`,
583 },
584
585 {
586 type: 'spotify_album',
587 source: 'spotify',
588 playerUri: `https://open.spotify.com/embed/album/albumId`,
589 },
590 {
591 type: 'spotify_album',
592 source: 'spotify',
593 playerUri: `https://open.spotify.com/embed/album/albumId`,
594 },
595 {
596 type: 'spotify_album',
597 source: 'spotify',
598 playerUri: `https://open.spotify.com/embed/album/albumId`,
599 },
600
601 {
602 type: 'soundcloud_track',
603 source: 'soundcloud',
604 playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/track&auto_play=true&visual=false&hide_related=true`,
605 },
606 {
607 type: 'soundcloud_set',
608 source: 'soundcloud',
609 playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/sets/set&auto_play=true&visual=false&hide_related=true`,
610 },
611 undefined,
612
613 {
614 type: 'apple_music_playlist',
615 source: 'appleMusic',
616 playerUri:
617 'https://embed.music.apple.com/us/playlist/playlistName/playlistId',
618 },
619 {
620 type: 'apple_music_album',
621 source: 'appleMusic',
622 playerUri: 'https://embed.music.apple.com/us/album/albumName/albumId',
623 },
624 {
625 type: 'apple_music_song',
626 source: 'appleMusic',
627 playerUri:
628 'https://embed.music.apple.com/us/album/albumName/albumId?i=songId',
629 },
630 {
631 type: 'apple_music_song',
632 source: 'appleMusic',
633 playerUri: 'https://embed.music.apple.com/us/song/songName/songId',
634 },
635
636 {
637 type: 'vimeo_video',
638 source: 'vimeo',
639 playerUri: 'https://player.vimeo.com/video/videoId?autoplay=1',
640 },
641 {
642 type: 'vimeo_video',
643 source: 'vimeo',
644 playerUri: 'https://player.vimeo.com/video/videoId?autoplay=1',
645 },
646
647 {
648 type: 'giphy_gif',
649 source: 'giphy',
650 isGif: true,
651 hideDetails: true,
652 metaUri: 'https://giphy.com/gifs/gifId',
653 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
654 },
655 undefined,
656 undefined,
657 {
658 type: 'giphy_gif',
659 source: 'giphy',
660 isGif: true,
661 hideDetails: true,
662 metaUri: 'https://giphy.com/gifs/39248209509382934029',
663 playerUri: 'https://i.giphy.com/media/39248209509382934029/200.webp',
664 },
665 {
666 type: 'giphy_gif',
667 source: 'giphy',
668 isGif: true,
669 hideDetails: true,
670 metaUri: 'https://giphy.com/gifs/gifId',
671 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
672 },
673 {
674 type: 'giphy_gif',
675 source: 'giphy',
676 isGif: true,
677 hideDetails: true,
678 metaUri: 'https://giphy.com/gifs/gifId',
679 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
680 },
681 {
682 type: 'giphy_gif',
683 source: 'giphy',
684 isGif: true,
685 hideDetails: true,
686 metaUri: 'https://giphy.com/gifs/gifId',
687 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
688 },
689 {
690 type: 'giphy_gif',
691 source: 'giphy',
692 isGif: true,
693 hideDetails: true,
694 metaUri: 'https://giphy.com/gifs/gifId',
695 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
696 },
697 {
698 type: 'giphy_gif',
699 source: 'giphy',
700 isGif: true,
701 hideDetails: true,
702 metaUri: 'https://giphy.com/gifs/gifId',
703 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
704 },
705 {
706 type: 'giphy_gif',
707 source: 'giphy',
708 isGif: true,
709 hideDetails: true,
710 metaUri: 'https://giphy.com/gifs/gifId',
711 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
712 },
713 undefined,
714 undefined,
715 undefined,
716
717 {
718 type: 'giphy_gif',
719 source: 'giphy',
720 isGif: true,
721 hideDetails: true,
722 metaUri: 'https://giphy.com/gifs/gifId',
723 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
724 },
725
726 {
727 type: 'giphy_gif',
728 source: 'giphy',
729 isGif: true,
730 hideDetails: true,
731 metaUri: 'https://giphy.com/gifs/gifId',
732 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
733 },
734 {
735 type: 'giphy_gif',
736 source: 'giphy',
737 isGif: true,
738 hideDetails: true,
739 metaUri: 'https://giphy.com/gifs/gifId',
740 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
741 },
742 {
743 type: 'giphy_gif',
744 source: 'giphy',
745 isGif: true,
746 hideDetails: true,
747 metaUri: 'https://giphy.com/gifs/gifId',
748 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
749 },
750 {
751 type: 'giphy_gif',
752 source: 'giphy',
753 isGif: true,
754 hideDetails: true,
755 metaUri: 'https://giphy.com/gifs/gifId',
756 playerUri: 'https://i.giphy.com/media/gifId/200.webp',
757 },
758
759 undefined,
760 undefined,
761 undefined,
762 undefined,
763 undefined,
764
765 {
766 type: 'tenor_gif',
767 source: 'tenor',
768 isGif: true,
769 hideDetails: true,
770 playerUri: 'https://t.gifs.bsky.app/someID_AAAAM/someName.gif',
771 dimensions: {
772 width: 100,
773 height: 100,
774 },
775 },
776 undefined,
777 undefined,
778 undefined,
779 undefined,
780
781 {
782 type: 'flickr_album',
783 source: 'flickr',
784 playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
785 },
786 {
787 type: 'flickr_album',
788 source: 'flickr',
789 playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
790 },
791 {
792 type: 'flickr_album',
793 source: 'flickr',
794 playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
795 },
796 {
797 type: 'flickr_album',
798 source: 'flickr',
799 playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
800 },
801 {
802 type: 'flickr_album',
803 source: 'flickr',
804 playerUri: 'https://embedr.flickr.com/photosets/72177720308493661',
805 },
806
807 undefined,
808 undefined,
809 undefined,
810 undefined,
811
812 {
813 type: 'flickr_album',
814 source: 'flickr',
815 playerUri: 'https://embedr.flickr.com/groups/898944@N23',
816 },
817 {
818 type: 'flickr_album',
819 source: 'flickr',
820 playerUri: 'https://embedr.flickr.com/groups/898944@N23',
821 },
822 {
823 type: 'flickr_album',
824 source: 'flickr',
825 playerUri: 'https://embedr.flickr.com/groups/898944@N23',
826 },
827 {
828 type: 'flickr_album',
829 source: 'flickr',
830 playerUri: 'https://embedr.flickr.com/groups/898944@N23',
831 },
832 {
833 type: 'flickr_album',
834 source: 'flickr',
835 playerUri: 'https://embedr.flickr.com/groups/898944@N23',
836 },
837
838 undefined,
839 undefined,
840
841 {
842 type: 'bandcamp_album',
843 source: 'bandcamp',
844 playerUri:
845 'https://bandcamp.com/EmbeddedPlayer/url=https%3A%2F%2Fmaxblansjaar.bandcamp.com%2Falbum%2Ffalse-comforts/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/',
846 },
847 {
848 type: 'bandcamp_track',
849 source: 'bandcamp',
850 playerUri:
851 'https://bandcamp.com/EmbeddedPlayer/url=https%3A%2F%2Fgrmnygrmny.bandcamp.com%2Ftrack%2Ffluid/size=large/bgcol=ffffff/linkcol=0687f5/minimal=true/transparent=true/',
852 },
853 undefined,
854 undefined,
855 undefined,
856 undefined,
857
858 {
859 type: 'klipy_gif',
860 source: 'klipy',
861 isGif: true,
862 hideDetails: true,
863 playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif',
864 dimensions: {
865 width: 300,
866 height: 200,
867 },
868 },
869 // With video slug params — on native (test env), keeps gif filename,
870 // strips mp4/webm params. On web, would swap to video filename.
871 {
872 type: 'klipy_gif',
873 source: 'klipy',
874 isGif: true,
875 hideDetails: true,
876 playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif',
877 dimensions: {
878 width: 300,
879 height: 200,
880 },
881 },
882 undefined,
883 undefined,
884 undefined,
885 undefined,
886 ]
887
888 it('correctly grabs the correct id from uri', () => {
889 for (let i = 0; i < inputs.length; i++) {
890 const input = inputs[i]
891 const output = outputs[i]
892
893 const res = parseEmbedPlayerFromUrl(input)
894
895 expect(res).toEqual(output)
896 }
897 })
898})
899
900describe('createStarterPackLinkFromAndroidReferrer', () => {
901 const validOutput = 'at://haileyok.com/app.bsky.graph.starterpack/rkey'
902
903 it('returns a link when input contains utm_source and utm_content', () => {
904 expect(
905 createStarterPackLinkFromAndroidReferrer(
906 'utm_source=bluesky&utm_content=starterpack_haileyok.com_rkey',
907 ),
908 ).toEqual(validOutput)
909
910 expect(
911 createStarterPackLinkFromAndroidReferrer(
912 'utm_source=bluesky&utm_content=starterpack_test-lover-9000.com_rkey',
913 ),
914 ).toEqual('at://test-lover-9000.com/app.bsky.graph.starterpack/rkey')
915 })
916
917 it('returns a link when input contains utm_source and utm_content in different order', () => {
918 expect(
919 createStarterPackLinkFromAndroidReferrer(
920 'utm_content=starterpack_haileyok.com_rkey&utm_source=bluesky',
921 ),
922 ).toEqual(validOutput)
923 })
924
925 it('returns a link when input contains other parameters as well', () => {
926 expect(
927 createStarterPackLinkFromAndroidReferrer(
928 'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack_haileyok.com_rkey',
929 ),
930 ).toEqual(validOutput)
931 })
932
933 it('returns null when utm_source is not present', () => {
934 expect(
935 createStarterPackLinkFromAndroidReferrer(
936 'utm_content=starterpack_haileyok.com_rkey',
937 ),
938 ).toEqual(null)
939 })
940
941 it('returns null when utm_content is not present', () => {
942 expect(
943 createStarterPackLinkFromAndroidReferrer('utm_source=bluesky'),
944 ).toEqual(null)
945 })
946
947 it('returns null when utm_content is malformed', () => {
948 expect(
949 createStarterPackLinkFromAndroidReferrer(
950 'utm_content=starterpack_haileyok.com',
951 ),
952 ).toEqual(null)
953
954 expect(
955 createStarterPackLinkFromAndroidReferrer('utm_content=starterpack'),
956 ).toEqual(null)
957
958 expect(
959 createStarterPackLinkFromAndroidReferrer(
960 'utm_content=starterpack_haileyok.com_rkey_more',
961 ),
962 ).toEqual(null)
963
964 expect(
965 createStarterPackLinkFromAndroidReferrer(
966 'utm_content=notastarterpack_haileyok.com_rkey',
967 ),
968 ).toEqual(null)
969 })
970})
971
972describe('parseStarterPackHttpUri', () => {
973 const baseUri = 'https://bsky.app/start'
974
975 it('returns a valid at uri when http uri is valid', () => {
976 const validHttpUri = `${baseUri}/haileyok.com/rkey`
977 expect(parseStarterPackUri(validHttpUri)).toEqual({
978 name: 'haileyok.com',
979 rkey: 'rkey',
980 })
981
982 const validHttpUri2 = `${baseUri}/haileyok.com/ilovetesting`
983 expect(parseStarterPackUri(validHttpUri2)).toEqual({
984 name: 'haileyok.com',
985 rkey: 'ilovetesting',
986 })
987
988 const validHttpUri3 = `${baseUri}/testlover9000.com/rkey`
989 expect(parseStarterPackUri(validHttpUri3)).toEqual({
990 name: 'testlover9000.com',
991 rkey: 'rkey',
992 })
993 })
994
995 it('returns null when there is no rkey', () => {
996 const validHttpUri = `${baseUri}/haileyok.com`
997 expect(parseStarterPackUri(validHttpUri)).toEqual(null)
998 })
999
1000 it('returns null when there is an extra path', () => {
1001 const validHttpUri = `${baseUri}/haileyok.com/rkey/other`
1002 expect(parseStarterPackUri(validHttpUri)).toEqual(null)
1003 })
1004
1005 it('returns null when there is no handle or rkey', () => {
1006 const validHttpUri = `${baseUri}`
1007 expect(parseStarterPackUri(validHttpUri)).toEqual(null)
1008 })
1009
1010 it('returns null when the route is not /start or /starter-pack', () => {
1011 const validHttpUri = 'https://bsky.app/start/haileyok.com/rkey'
1012 expect(parseStarterPackUri(validHttpUri)).toEqual({
1013 name: 'haileyok.com',
1014 rkey: 'rkey',
1015 })
1016
1017 const validHttpUri2 = 'https://bsky.app/starter-pack/haileyok.com/rkey'
1018 expect(parseStarterPackUri(validHttpUri2)).toEqual({
1019 name: 'haileyok.com',
1020 rkey: 'rkey',
1021 })
1022
1023 const invalidHttpUri = 'https://bsky.app/profile/haileyok.com/rkey'
1024 expect(parseStarterPackUri(invalidHttpUri)).toEqual(null)
1025 })
1026
1027 it('returns the at uri when the input is a valid starterpack at uri', () => {
1028 const validAtUri = 'at://did:plc:123/app.bsky.graph.starterpack/rkey'
1029 expect(parseStarterPackUri(validAtUri)).toEqual({
1030 name: 'did:plc:123',
1031 rkey: 'rkey',
1032 })
1033 })
1034
1035 it('returns null when the at uri has no rkey', () => {
1036 const validAtUri = 'at://did:plc:123/app.bsky.graph.starterpack'
1037 expect(parseStarterPackUri(validAtUri)).toEqual(null)
1038 })
1039
1040 it('returns null when the collection is not app.bsky.graph.starterpack', () => {
1041 const validAtUri = 'at://did:plc:123/app.bsky.graph.list/rkey'
1042 expect(parseStarterPackUri(validAtUri)).toEqual(null)
1043 })
1044
1045 it('returns null when the input is undefined', () => {
1046 expect(parseStarterPackUri(undefined)).toEqual(null)
1047 })
1048})
1049
1050describe('createStarterPackGooglePlayUri', () => {
1051 const base =
1052 'https://play.google.com/store/apps/details?id=app.witchsky&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_'
1053
1054 it('returns valid google play uri when input is valid', () => {
1055 expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
1056 `${base}name_rkey`,
1057 )
1058 })
1059
1060 it('returns null when no rkey is supplied', () => {
1061 // @ts-expect-error test
1062 expect(createStarterPackGooglePlayUri('name', undefined)).toEqual(null)
1063 })
1064
1065 it('returns null when no name or rkey are supplied', () => {
1066 // @ts-expect-error test
1067 expect(createStarterPackGooglePlayUri(undefined, undefined)).toEqual(null)
1068 })
1069
1070 it('returns null when rkey is supplied but no name', () => {
1071 // @ts-expect-error test
1072 expect(createStarterPackGooglePlayUri(undefined, 'rkey')).toEqual(null)
1073 })
1074})
1075
1076describe('tenorUrlToBskyGifUrl', () => {
1077 const inputs = [
1078 'https://media.tenor.com/someID_AAAAC/someName.gif',
1079 'https://media.tenor.com/someID/someName.gif',
1080 ]
1081
1082 it.each(inputs)(
1083 'returns url with t.gifs.bsky.app as hostname for input url',
1084 input => {
1085 const out = tenorUrlToBskyGifUrl(input)
1086 expect(out.startsWith('https://t.gifs.bsky.app/')).toEqual(true)
1087 },
1088 )
1089})
1090
1091describe('klipyUrlToBskyGifUrl', () => {
1092 const inputs = [
1093 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif',
1094 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
1095 ]
1096
1097 it.each(inputs)(
1098 'returns url with k.gifs.bsky.app as hostname for input url',
1099 input => {
1100 const out = klipyUrlToBskyGifUrl(input)
1101 expect(out.startsWith('https://k.gifs.bsky.app/')).toEqual(true)
1102 },
1103 )
1104
1105 it('preserves the path and query params when rewriting', () => {
1106 const out = klipyUrlToBskyGifUrl(
1107 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
1108 )
1109 expect(out).toEqual(
1110 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif?hh=200&ww=300',
1111 )
1112 })
1113
1114 it('returns empty string for invalid URLs', () => {
1115 expect(klipyUrlToBskyGifUrl('not-a-url')).toEqual('')
1116 })
1117})