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