Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 1051 lines 32 kB view raw
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})