Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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