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