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