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