this repo has no description
0
fork

Configure Feed

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

View transitions for media

+271 -64
+14
src/app.jsx
··· 345 345 }, 346 346 }; 347 347 348 + if (import.meta.env.DEV) { 349 + // If press shift down, set --time-scale to 10 in root 350 + document.addEventListener('keydown', (e) => { 351 + if (e.key === 'Shift') { 352 + document.documentElement.classList.add('slow-mo'); 353 + } 354 + }); 355 + document.addEventListener('keyup', (e) => { 356 + if (e.key === 'Shift') { 357 + document.documentElement.classList.remove('slow-mo'); 358 + } 359 + }); 360 + } 361 + 348 362 function App() { 349 363 const [isLoggedIn, setIsLoggedIn] = useState(false); 350 364 const [uiState, setUIState] = useState('loading');
+11 -7
src/components/media-modal.jsx
··· 67 67 const [showControls, setShowControls] = useState(true); 68 68 69 69 useEffect(() => { 70 - let handleSwipe = () => { 71 - onClose(); 70 + let handleSwipe = (e) => { 71 + onClose(e, currentIndex, mediaAttachments, carouselRef); 72 72 }; 73 73 if (carouselRef.current) { 74 74 carouselRef.current.addEventListener('swiped-down', handleSwipe); ··· 78 78 carouselRef.current.removeEventListener('swiped-down', handleSwipe); 79 79 } 80 80 }; 81 - }, []); 81 + }, [currentIndex, mediaAttachments]); 82 82 83 83 useHotkeys( 84 84 'esc', 85 - onClose, 85 + (e) => { 86 + onClose(e, currentIndex, mediaAttachments, carouselRef); 87 + }, 86 88 { 87 89 ignoreEventWhen: (e) => { 88 90 const hasModal = !!document.querySelector('#modal-container > *'); ··· 90 92 }, 91 93 useKey: true, 92 94 }, 93 - [onClose], 95 + [onClose, currentIndex, mediaAttachments], 94 96 ); 95 97 96 98 useEffect(() => { ··· 240 242 e.target.classList.contains('media') || 241 243 e.target.classList.contains('media-zoom') 242 244 ) { 243 - onClose(); 245 + onClose(e, currentIndex, mediaAttachments, carouselRef); 244 246 } 245 247 }} 246 248 style={ ··· 318 320 <button 319 321 type="button" 320 322 class="carousel-button" 321 - onClick={() => onClose()} 323 + onClick={(e) => 324 + onClose(e, currentIndex, mediaAttachments, carouselRef) 325 + } 322 326 > 323 327 <Icon icon="x" alt={t`Close`} /> 324 328 </button>
+74 -7
src/components/media.jsx
··· 76 76 allowLongerCaption, 77 77 altIndex, 78 78 checkAspectRatio = true, 79 - onClick = () => {}, 79 + onClick, 80 80 }) { 81 81 let { 82 + id, 82 83 blurhash, 83 84 description, 84 85 meta, ··· 91 92 if (/no\-preview\./i.test(previewUrl)) { 92 93 previewUrl = null; 93 94 } 95 + const mediaVTN = getSafeViewTransitionName(id || blurhash || url); 94 96 const { original = {}, small, focus } = meta || {}; 95 97 96 98 const width = showOriginal ··· 103 105 const remoteMediaURL = showOriginal 104 106 ? remoteUrl 105 107 : previewRemoteUrl || remoteUrl; 108 + const hasPreviewDimensions = small?.width && small?.height; 106 109 const hasDimensions = width && height; 107 110 const orientation = hasDimensions 108 111 ? width > height ··· 260 263 261 264 const [hasNaturalAspectRatio, setHasNaturalAspectRatio] = useState(undefined); 262 265 266 + const postViewState = () => 267 + window.matchMedia('(min-width: calc(40em + 350px))').matches 268 + ? 'large' 269 + : 'small'; 270 + const interceptOnClick = useCallback( 271 + (e) => { 272 + const isOnPostPage = e.target.closest('.status-deck'); 273 + if ( 274 + showOriginal || 275 + (postViewState() === 'large' && isOnPostPage) || 276 + !document.startViewTransition 277 + ) { 278 + onClick?.(e); 279 + return; 280 + } 281 + const el = 282 + e.target.closest('[data-view-transition-name]') || 283 + e.target.querySelector('[data-view-transition-name]'); 284 + if (el) { 285 + if (!onClick) e.preventDefault(); 286 + el.style.viewTransitionName = mediaVTN; 287 + document.startViewTransition(() => { 288 + el.style.viewTransitionName = ''; 289 + onClick?.(e); 290 + if (!onClick || !e.defaultPrevented) { 291 + location.hash = `#${to}`; 292 + } 293 + }); 294 + } else { 295 + onClick?.(e); 296 + } 297 + }, 298 + [mediaVTN, showOriginal, onClick], 299 + ); 300 + 263 301 if (isImage) { 264 302 // Note: type: unknown might not have width/height 265 303 quickPinchZoomProps.containerProps.style.display = 'inherit'; ··· 282 320 <Parent 283 321 ref={parentRef} 284 322 class={`media media-image ${className}`} 285 - onClick={onClick} 323 + onClick={interceptOnClick} 286 324 data-orientation={orientation} 287 325 data-has-alt={!showInlineDesc || undefined} 288 326 data-has-natural-aspect-ratio={hasNaturalAspectRatio || undefined} ··· 290 328 showOriginal 291 329 ? { 292 330 backgroundImage: `url(${previewUrl})`, 331 + '--bg-image': `url(${previewUrl})`, 293 332 backgroundSize: imageSmallerThanParent 294 333 ? `${width}px ${height}px` 295 334 : undefined, ··· 309 348 data-orientation={orientation} 310 349 loading="eager" 311 350 decoding="sync" 351 + style={{ 352 + 'view-transition-name': mediaVTN, 353 + }} 312 354 onLoad={(e) => { 313 - e.target.closest('.media-image').style.backgroundImage = ''; 314 - e.target.closest('.media-zoom').style.display = ''; 355 + const el = e.target; 356 + const mediaImage = el.closest('.media-image'); 357 + if (mediaImage) { 358 + mediaImage.style.backgroundImage = `url(${el.src})`; 359 + mediaImage.style.removeProperty('--bg-image'); 360 + } 361 + el.closest('.media-zoom').style.display = ''; 315 362 setPinchZoomEnabled(true); 316 363 }} 317 364 onError={(e) => { ··· 335 382 height={height} 336 383 data-orientation={orientation} 337 384 loading="lazy" 385 + data-view-transition-name={mediaVTN} 338 386 style={{ 339 387 // backgroundColor: 340 388 // rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`, ··· 352 400 // e.target.closest('.media-image').style.backgroundImage = ''; 353 401 e.target.dataset.loaded = true; 354 402 const $media = e.target.closest('.media'); 355 - if (!hasDimensions && $media) { 403 + if (!hasPreviewDimensions && $media) { 356 404 const { naturalWidth, naturalHeight } = e.target; 357 405 $media.dataset.orientation = 358 406 naturalWidth > naturalHeight ? 'landscape' : 'portrait'; ··· 440 488 width="${width}" 441 489 height="${height}" 442 490 data-orientation="${orientation}" 491 + style="view-transition-name: ${mediaVTN}" 443 492 preload="auto" 444 493 autoplay 445 494 muted ··· 461 510 width="${width}" 462 511 height="${height}" 463 512 data-orientation="${orientation}" 513 + style="view-transition-name: ${mediaVTN}" 464 514 preload="auto" 465 515 autoplay 466 516 playsinline ··· 472 522 return ( 473 523 <Figure> 474 524 <Parent 525 + ref={parentRef} 475 526 class={`media ${className} media-${isGIF ? 'gif' : 'video'} ${ 476 527 autoGIFAnimate ? 'media-contain' : '' 477 528 } ${hoverAnimate ? 'media-hover-animate' : ''}`} ··· 494 545 videoRef.current.pause(); 495 546 } catch (e) {} 496 547 } 497 - onClick(e); 548 + interceptOnClick(e); 498 549 }} 499 550 onMouseEnter={() => { 500 551 if (hoverAnimate) { ··· 556 607 width={width} 557 608 height={height} 558 609 data-orientation={orientation} 610 + data-view-transition-name={mediaVTN} 559 611 preload="auto" 560 612 // controls 561 613 playsinline ··· 589 641 data-orientation={orientation} 590 642 loading="lazy" 591 643 decoding="async" 644 + data-view-transition-name={mediaVTN} 592 645 onLoad={(e) => { 593 - if (!hasDimensions) { 646 + if (!hasPreviewDimensions) { 594 647 const $media = e.target.closest('.media'); 595 648 if ($media) { 596 649 const { naturalHeight, naturalWidth } = e.target; ··· 617 670 width={width} 618 671 height={height} 619 672 data-orientation={orientation} 673 + data-view-transition-name={mediaVTN} 620 674 preload="metadata" 621 675 muted 622 676 disablePictureInPicture ··· 696 750 function getURLObj(url) { 697 751 // Fake base URL if url doesn't have https:// prefix 698 752 return URL.parse(url, location.origin); 753 + } 754 + 755 + export function getSafeViewTransitionName(inputString) { 756 + // Replace any character that is not a letter, number, hyphen, or underscore with a hyphen. 757 + let safeName = inputString.replace(/[^a-zA-Z0-9_-]/g, '-'); 758 + 759 + // Ensure it starts with a letter, underscore, or two hyphens (to prevent starting with a number or single hyphen). 760 + // This covers edge cases where the original string might start with an invalid character after replacement. 761 + if (safeName.match(/^[0-9-]/)) { 762 + safeName = 'vt-' + safeName; 763 + } 764 + 765 + return safeName; 699 766 } 700 767 701 768 export default memo(Media, (oldProps, newProps) => {
+65 -1
src/components/status.css
··· 1228 1228 border-color: var(--outline-hover-color); 1229 1229 } 1230 1230 .status .media:active:not(:has(button:active)) { 1231 - filter: brightness(0.8); 1231 + /* filter: brightness(0.8); */ 1232 1232 transform: scale(0.99); 1233 1233 } 1234 1234 .status .media :is(img, video) { ··· 2845 2845 } 2846 2846 } 2847 2847 } 2848 + 2849 + /* VIEW TRANSITIONS */ 2850 + @media not (prefers-reduced-motion: reduce) { 2851 + :root { 2852 + --media-swoosh-duration: 0.1s; 2853 + } 2854 + :root.slow-mo { 2855 + --media-swoosh-duration: 3s; 2856 + } 2857 + ::view-transition-group(.media-swoosh) { 2858 + animation-duration: var(--media-swoosh-duration); 2859 + animation-fill-mode: forwards; 2860 + animation-timing-function: var(--timing-function); 2861 + } 2862 + ::view-transition-old(.media-swoosh), 2863 + ::view-transition-new(.media-swoosh) { 2864 + overflow: clip; 2865 + animation: none; 2866 + width: 100%; 2867 + height: 100%; 2868 + } 2869 + ::view-transition-old(.media-swoosh-in) { 2870 + object-fit: cover; 2871 + } 2872 + ::view-transition-new(.media-swoosh-in) { 2873 + object-fit: contain; 2874 + } 2875 + ::view-transition-old(.media-swoosh-out) { 2876 + object-fit: contain; 2877 + transform: scale(0.99); /* pressed down */ 2878 + } 2879 + ::view-transition-new(.media-swoosh-out) { 2880 + object-fit: cover; 2881 + } 2882 + .status .media :is(img, video) { 2883 + view-transition-class: media-swoosh media-swoosh-in; 2884 + } 2885 + .carousel .carousel-item :is(img, video) { 2886 + view-transition-class: media-swoosh media-swoosh-out; 2887 + } 2888 + /* delay render .media bg image */ 2889 + .carousel .carousel-item .media { 2890 + animation: delayBg calc(var(--media-swoosh-duration) + 0.1s) steps(1) 2891 + forwards; 2892 + } 2893 + } 2894 + 2895 + @keyframes delayBg { 2896 + from { 2897 + background-image: none; 2898 + } 2899 + to { 2900 + background-image: var(--bg-image); 2901 + } 2902 + } 2903 + 2904 + .status .media :is(img, video), 2905 + .carousel .carousel-item :is(img, video) { 2906 + interpolate-size: allow-keywords; 2907 + transition-property: width, height; 2908 + transition-timing-function: var(--timing-function); 2909 + transition-duration: 0.1s; 2910 + transition-behavior: allow-discrete; 2911 + }
+43 -43
src/locales/en.po
··· 110 110 #: src/components/account-info.jsx:1236 111 111 #: src/components/compose.jsx:2783 112 112 #: src/components/media-alt-modal.jsx:55 113 - #: src/components/media-modal.jsx:359 113 + #: src/components/media-modal.jsx:363 114 114 #: src/components/status.jsx:1836 115 115 #: src/components/status.jsx:1853 116 116 #: src/components/status.jsx:1978 ··· 122 122 #: src/pages/list.jsx:171 123 123 #: src/pages/public.jsx:116 124 124 #: src/pages/scheduled-posts.jsx:89 125 - #: src/pages/status.jsx:1247 125 + #: src/pages/status.jsx:1305 126 126 #: src/pages/trending.jsx:474 127 127 msgid "More" 128 128 msgstr "" ··· 207 207 #: src/pages/catchup.jsx:71 208 208 #: src/pages/catchup.jsx:1448 209 209 #: src/pages/catchup.jsx:2061 210 - #: src/pages/status.jsx:966 211 - #: src/pages/status.jsx:1593 210 + #: src/pages/status.jsx:1024 211 + #: src/pages/status.jsx:1651 212 212 msgid "Replies" 213 213 msgstr "" 214 214 ··· 460 460 #: src/components/keyboard-shortcuts-help.jsx:43 461 461 #: src/components/list-add-edit.jsx:37 462 462 #: src/components/media-alt-modal.jsx:43 463 - #: src/components/media-modal.jsx:323 463 + #: src/components/media-modal.jsx:327 464 464 #: src/components/notification-service.jsx:157 465 465 #: src/components/report-modal.jsx:75 466 466 #: src/components/shortcuts-settings.jsx:230 ··· 476 476 #: src/pages/notifications.jsx:942 477 477 #: src/pages/scheduled-posts.jsx:259 478 478 #: src/pages/settings.jsx:87 479 - #: src/pages/status.jsx:1334 479 + #: src/pages/status.jsx:1392 480 480 msgid "Close" 481 481 msgstr "" 482 482 ··· 982 982 msgstr "" 983 983 984 984 #: src/components/compose.jsx:3794 985 - #: src/components/media-modal.jsx:465 985 + #: src/components/media-modal.jsx:469 986 986 #: src/components/timeline.jsx:927 987 987 msgid "Previous" 988 988 msgstr "" 989 989 990 990 #: src/components/compose.jsx:3812 991 - #: src/components/media-modal.jsx:484 991 + #: src/components/media-modal.jsx:488 992 992 #: src/components/timeline.jsx:944 993 993 msgid "Next" 994 994 msgstr "" ··· 1087 1087 #: src/pages/list.jsx:321 1088 1088 #: src/pages/notifications.jsx:922 1089 1089 #: src/pages/search.jsx:561 1090 - #: src/pages/status.jsx:1367 1090 + #: src/pages/status.jsx:1425 1091 1091 msgid "Show more…" 1092 1092 msgstr "" 1093 1093 ··· 1315 1315 msgid "Speak" 1316 1316 msgstr "" 1317 1317 1318 - #: src/components/media-modal.jsx:370 1318 + #: src/components/media-modal.jsx:374 1319 1319 msgid "Open original media in new window" 1320 1320 msgstr "" 1321 1321 1322 - #: src/components/media-modal.jsx:374 1322 + #: src/components/media-modal.jsx:378 1323 1323 msgid "Open original media" 1324 1324 msgstr "" 1325 1325 1326 - #: src/components/media-modal.jsx:390 1326 + #: src/components/media-modal.jsx:394 1327 1327 msgid "Attempting to describe image. Please wait…" 1328 1328 msgstr "" 1329 1329 1330 - #: src/components/media-modal.jsx:405 1330 + #: src/components/media-modal.jsx:409 1331 1331 msgid "Failed to describe image" 1332 1332 msgstr "" 1333 1333 1334 - #: src/components/media-modal.jsx:415 1334 + #: src/components/media-modal.jsx:419 1335 1335 msgid "Describe image…" 1336 1336 msgstr "" 1337 1337 1338 - #: src/components/media-modal.jsx:439 1338 + #: src/components/media-modal.jsx:443 1339 1339 msgid "View post" 1340 1340 msgstr "" 1341 1341 ··· 1357 1357 msgid "Filtered" 1358 1358 msgstr "" 1359 1359 1360 - #: src/components/media.jsx:417 1360 + #: src/components/media.jsx:465 1361 1361 msgid "Open file" 1362 1362 msgstr "Open file" 1363 1363 ··· 1492 1492 #: src/components/nav-menu.jsx:326 1493 1493 #: src/pages/login.jsx:32 1494 1494 #: src/pages/login.jsx:195 1495 - #: src/pages/status.jsx:866 1495 + #: src/pages/status.jsx:924 1496 1496 #: src/pages/welcome.jsx:65 1497 1497 msgid "Log in" 1498 1498 msgstr "" ··· 1714 1714 #: src/components/poll.jsx:208 1715 1715 #: src/components/poll.jsx:210 1716 1716 #: src/pages/scheduled-posts.jsx:100 1717 - #: src/pages/status.jsx:1236 1718 - #: src/pages/status.jsx:1259 1717 + #: src/pages/status.jsx:1294 1718 + #: src/pages/status.jsx:1317 1719 1719 msgid "Refresh" 1720 1720 msgstr "" 1721 1721 ··· 2522 2522 #: src/components/timeline.jsx:586 2523 2523 #: src/pages/home.jsx:228 2524 2524 #: src/pages/notifications.jsx:898 2525 - #: src/pages/status.jsx:1019 2526 - #: src/pages/status.jsx:1396 2525 + #: src/pages/status.jsx:1077 2526 + #: src/pages/status.jsx:1454 2527 2527 msgid "Try again" 2528 2528 msgstr "" 2529 2529 ··· 3848 3848 msgstr "" 3849 3849 3850 3850 #. js-lingui-explicit-id 3851 - #: src/pages/status.jsx:599 3852 - #: src/pages/status.jsx:1162 3851 + #: src/pages/status.jsx:657 3852 + #: src/pages/status.jsx:1220 3853 3853 msgid "post.title" 3854 3854 msgstr "Post" 3855 3855 3856 - #: src/pages/status.jsx:853 3856 + #: src/pages/status.jsx:911 3857 3857 msgid "You're not logged in. Interactions (reply, boost, etc) are not possible." 3858 3858 msgstr "" 3859 3859 3860 - #: src/pages/status.jsx:873 3860 + #: src/pages/status.jsx:931 3861 3861 msgid "This post is from another instance (<0>{instance}</0>). Interactions (reply, boost, etc) are not possible." 3862 3862 msgstr "" 3863 3863 3864 - #: src/pages/status.jsx:901 3864 + #: src/pages/status.jsx:959 3865 3865 msgid "Error: {e}" 3866 3866 msgstr "" 3867 3867 3868 - #: src/pages/status.jsx:908 3868 + #: src/pages/status.jsx:966 3869 3869 msgid "Switch to my instance to enable interactions" 3870 3870 msgstr "" 3871 3871 3872 - #: src/pages/status.jsx:1010 3872 + #: src/pages/status.jsx:1068 3873 3873 msgid "Unable to load replies." 3874 3874 msgstr "" 3875 3875 3876 - #: src/pages/status.jsx:1122 3876 + #: src/pages/status.jsx:1180 3877 3877 msgid "Back" 3878 3878 msgstr "" 3879 3879 3880 - #: src/pages/status.jsx:1153 3880 + #: src/pages/status.jsx:1211 3881 3881 msgid "Go to main post" 3882 3882 msgstr "" 3883 3883 3884 3884 #. placeholder {0}: ancestors.length 3885 - #: src/pages/status.jsx:1176 3885 + #: src/pages/status.jsx:1234 3886 3886 msgid "{0} posts above ‒ Go to top" 3887 3887 msgstr "" 3888 3888 3889 - #: src/pages/status.jsx:1223 3890 - #: src/pages/status.jsx:1286 3889 + #: src/pages/status.jsx:1281 3890 + #: src/pages/status.jsx:1344 3891 3891 msgid "Switch to Side Peek view" 3892 3892 msgstr "" 3893 3893 3894 - #: src/pages/status.jsx:1287 3894 + #: src/pages/status.jsx:1345 3895 3895 msgid "Switch to Full view" 3896 3896 msgstr "" 3897 3897 3898 - #: src/pages/status.jsx:1305 3898 + #: src/pages/status.jsx:1363 3899 3899 msgid "Show all sensitive content" 3900 3900 msgstr "" 3901 3901 3902 - #: src/pages/status.jsx:1310 3902 + #: src/pages/status.jsx:1368 3903 3903 msgid "Experimental" 3904 3904 msgstr "" 3905 3905 3906 - #: src/pages/status.jsx:1319 3906 + #: src/pages/status.jsx:1377 3907 3907 msgid "Unable to switch" 3908 3908 msgstr "" 3909 3909 3910 3910 #. placeholder {0}: punycode.toUnicode( postInstance, ) 3911 - #: src/pages/status.jsx:1326 3911 + #: src/pages/status.jsx:1384 3912 3912 msgid "Switch to post's instance ({0})" 3913 3913 msgstr "Switch to post's instance ({0})" 3914 3914 3915 - #: src/pages/status.jsx:1329 3915 + #: src/pages/status.jsx:1387 3916 3916 msgid "Switch to post's instance" 3917 3917 msgstr "" 3918 3918 3919 - #: src/pages/status.jsx:1387 3919 + #: src/pages/status.jsx:1445 3920 3920 msgid "Unable to load post" 3921 3921 msgstr "" 3922 3922 3923 3923 #. placeholder {0}: replies.length 3924 3924 #. placeholder {1}: shortenNumber(replies.length) 3925 - #: src/pages/status.jsx:1523 3925 + #: src/pages/status.jsx:1581 3926 3926 msgid "{0, plural, one {# reply} other {<0>{1}</0> replies}}" 3927 3927 msgstr "" 3928 3928 3929 3929 #. placeholder {0}: shortenNumber(totalComments) 3930 - #: src/pages/status.jsx:1541 3930 + #: src/pages/status.jsx:1599 3931 3931 msgid "{totalComments, plural, one {# comment} other {<0>{0}</0> comments}}" 3932 3932 msgstr "" 3933 3933 3934 - #: src/pages/status.jsx:1563 3934 + #: src/pages/status.jsx:1621 3935 3935 msgid "View post with its replies" 3936 3936 msgstr "" 3937 3937
+64 -6
src/pages/status.jsx
··· 23 23 import Icon from '../components/icon'; 24 24 import Link from '../components/link'; 25 25 import Loader from '../components/loader'; 26 + import { getSafeViewTransitionName } from '../components/media'; 26 27 import MediaModal from '../components/media-modal'; 27 28 import Menu2 from '../components/menu2'; 28 29 import NameText from '../components/name-text'; ··· 39 40 } from '../utils/states'; 40 41 import statusPeek from '../utils/status-peek'; 41 42 import { getCurrentAccount } from '../utils/store-utils'; 42 - import useScroll from '../utils/useScroll'; 43 43 import useTitle from '../utils/useTitle'; 44 44 45 45 import getInstanceStatusURL from './../utils/get-instance-status-url'; ··· 129 129 ? snapStates.statuses[statusKey(mediaStatusID, instance)]?.mediaAttachments 130 130 : heroStatus?.mediaAttachments; 131 131 132 - const handleMediaClose = useCallback(() => { 133 - if ( 134 - !window.matchMedia('(min-width: calc(40em + 350px))').matches && 135 - snapStates.prevLocation 136 - ) { 132 + const postViewState = () => 133 + window.matchMedia('(min-width: calc(40em + 350px))').matches 134 + ? 'large' 135 + : 'small'; 136 + const mediaClose = useCallback(() => { 137 + console.log('xxx', { 138 + postViewState: postViewState(), 139 + showMediaOnly, 140 + }); 141 + if (postViewState() === 'small' && snapStates.prevLocation) { 137 142 history.back(); 138 143 } else { 139 144 if (showMediaOnly) { ··· 145 150 } 146 151 } 147 152 }, [showMediaOnly, closeLink, snapStates.prevLocation]); 153 + const handleMediaClose = useCallback( 154 + (e, currentIndex, mediaAttachments, carouselRef) => { 155 + if (postViewState() === 'large' && !showMediaOnly) { 156 + mediaClose(); 157 + return; 158 + } 159 + if (showMedia && document.startViewTransition) { 160 + const media = mediaAttachments[currentIndex]; 161 + const { id, blurhash, url } = media; 162 + const mediaVTN = getSafeViewTransitionName(id || blurhash || url); 163 + const els = document.querySelectorAll( 164 + `.status .media [data-view-transition-name="${mediaVTN}"]`, 165 + ); 166 + const foundEls = [...els]?.filter?.((el) => { 167 + const elBounds = el.getBoundingClientRect(); 168 + return ( 169 + elBounds.top < window.innerHeight && 170 + elBounds.bottom > 0 && 171 + elBounds.left < window.innerWidth && 172 + elBounds.right > 0 173 + ); 174 + }); 175 + // If more than one, get the one in status page 176 + const el = 177 + foundEls.length === 1 178 + ? foundEls[0] 179 + : foundEls.find((el) => !!el.closest('.status-deck')); 180 + 181 + console.log('xxx', { media, id, els, el }); 182 + if (el) { 183 + const transition = document.startViewTransition(() => { 184 + el.style.viewTransitionName = mediaVTN; 185 + if (carouselRef?.current) { 186 + carouselRef.current 187 + .querySelectorAll('.media img, .media video') 188 + ?.forEach((el) => { 189 + el.style.viewTransitionName = ''; 190 + }); 191 + } 192 + mediaClose(); 193 + }); 194 + transition.ready.finally(() => { 195 + el.style.viewTransitionName = ''; 196 + }); 197 + } else { 198 + mediaClose(); 199 + } 200 + } else { 201 + mediaClose(); 202 + } 203 + }, 204 + [showMedia, showMediaOnly], 205 + ); 148 206 149 207 useEffect(() => { 150 208 let timer = setTimeout(() => {