mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 14:16:39 +01:00
Rewrite status page + media modal
Media modals now have their own URLs
This commit is contained in:
parent
a60ad33b47
commit
f303c6d36c
10 changed files with 553 additions and 404 deletions
57
src/app.css
57
src/app.css
|
@ -772,6 +772,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
/* backdrop-filter: saturate(0.75); */
|
/* backdrop-filter: saturate(0.75); */
|
||||||
}
|
}
|
||||||
|
.deck-backdrop > .deck-loader {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
background-image: radial-gradient(closest-side, var(--bg-color), transparent);
|
||||||
|
}
|
||||||
@keyframes slide-in {
|
@keyframes slide-in {
|
||||||
0% {
|
0% {
|
||||||
transform: translate3d(100%, 0, 0);
|
transform: translate3d(100%, 0, 0);
|
||||||
|
@ -784,9 +792,11 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
width: var(--main-width);
|
width: var(--main-width);
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
animation: slide-in 0.5s var(--timing-function);
|
|
||||||
box-shadow: -1px 0 var(--bg-color);
|
box-shadow: -1px 0 var(--bg-color);
|
||||||
}
|
}
|
||||||
|
.deck-backdrop .deck.slide-in {
|
||||||
|
animation: slide-in 0.5s var(--timing-function);
|
||||||
|
}
|
||||||
.deck-backdrop .deck .status {
|
.deck-backdrop .deck .status {
|
||||||
max-width: var(--main-width);
|
max-width: var(--main-width);
|
||||||
}
|
}
|
||||||
|
@ -853,6 +863,14 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
/* CAROUSEL */
|
/* CAROUSEL */
|
||||||
/* use snap, center children, max width viewport */
|
/* use snap, center children, max width viewport */
|
||||||
|
|
||||||
|
.media-modal-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--backdrop-color);
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
animation: appear 0.3s var(--timing-function) both;
|
||||||
|
}
|
||||||
|
|
||||||
.carousel {
|
.carousel {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
@ -917,7 +935,7 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
top: env(safe-area-inset-top, 0);
|
top: env(safe-area-inset-top, 0);
|
||||||
}
|
}
|
||||||
:is(.carousel-top-controls, .carousel-controls) {
|
:is(.carousel-top-controls, .carousel-controls) {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
left: env(safe-area-inset-left, 0);
|
left: env(safe-area-inset-left, 0);
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -999,6 +1017,19 @@ body:has(.status-deck) .media-post-link {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ✨ New */
|
||||||
|
body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.media-modal-container + .status-deck {
|
||||||
|
/* display: none; */
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: calc(40em + 350px)) {
|
@media (min-width: calc(40em + 350px)) {
|
||||||
.media-post-link .button-label {
|
.media-post-link .button-label {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
@ -1024,6 +1055,26 @@ body:has(.status-deck) .media-post-link {
|
||||||
right: 350px;
|
right: 350px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
/* ✨ New */
|
||||||
|
.deck-backdrop > a {
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
.deck-backdrop .media-modal-container + .status-deck {
|
||||||
|
/* display: block; */
|
||||||
|
/* width: 350px; */
|
||||||
|
min-width: 350px;
|
||||||
|
position: static;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
|
.deck-backdrop .media-modal-container + .status-deck:not(.slide-in) {
|
||||||
|
animation: appear 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
body:has(.media-modal-container + .status-deck) .media-post-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* COMPOSE BUTTON */
|
/* COMPOSE BUTTON */
|
||||||
|
@ -1731,7 +1782,7 @@ ul.link-list li a .icon {
|
||||||
.deck-container {
|
.deck-container {
|
||||||
transition: transform 0.4s var(--timing-function);
|
transition: transform 0.4s var(--timing-function);
|
||||||
}
|
}
|
||||||
.deck-container:has(~ .deck-backdrop) {
|
.deck-container:has(~ .deck-backdrop .deck) {
|
||||||
transition: transform 0.4s ease-out;
|
transition: transform 0.4s ease-out;
|
||||||
transform: translate3d(-5vw, 0, 0);
|
transform: translate3d(-5vw, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,9 +73,7 @@
|
||||||
#compose-container .status-preview:has(.status-badge) {
|
#compose-container .status-preview:has(.status-badge) {
|
||||||
border-top-right-radius: 8px;
|
border-top-right-radius: 8px;
|
||||||
}
|
}
|
||||||
#compose-container .status-preview :is(.hashtag, .time) {
|
#compose-container .status-preview :is(.content-container, .time) {
|
||||||
/* Prevent hashtags from being clickable */
|
|
||||||
/* TODO: maybe use a different solution? */
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
#compose-container.standalone .status-preview * {
|
#compose-container.standalone .status-preview * {
|
||||||
|
|
|
@ -24,16 +24,16 @@ function MediaModal({
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
carouselFocusItem.current?.scrollIntoView();
|
carouselFocusItem.current?.scrollIntoView();
|
||||||
|
|
||||||
history.pushState({ mediaModal: true }, '');
|
// history.pushState({ mediaModal: true }, '');
|
||||||
const handlePopState = (e) => {
|
// const handlePopState = (e) => {
|
||||||
if (e.state?.mediaModal) {
|
// if (e.state?.mediaModal) {
|
||||||
onClose();
|
// onClose();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
window.addEventListener('popstate', handlePopState);
|
// window.addEventListener('popstate', handlePopState);
|
||||||
return () => {
|
// return () => {
|
||||||
window.removeEventListener('popstate', handlePopState);
|
// window.removeEventListener('popstate', handlePopState);
|
||||||
};
|
// };
|
||||||
}, []);
|
}, []);
|
||||||
const prevStatusID = useRef(statusID);
|
const prevStatusID = useRef(statusID);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -85,7 +85,7 @@ function MediaModal({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div class="media-modal-container">
|
||||||
<div
|
<div
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
|
@ -206,7 +206,11 @@ function MediaModal({
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
</Menu>{' '}
|
</Menu>{' '}
|
||||||
<Link
|
<Link
|
||||||
to={instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`}
|
to={`${instance ? `/${instance}` : ''}/s/${statusID}${
|
||||||
|
window.matchMedia('(min-width: calc(40em + 350px))').matches
|
||||||
|
? `?media=${currentIndex + 1}`
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
class="button carousel-button media-post-link plain3"
|
class="button carousel-button media-post-link plain3"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// if small screen (not media query min-width 40em + 350px), run onClose
|
// if small screen (not media query min-width 40em + 350px), run onClose
|
||||||
|
@ -267,7 +271,7 @@ function MediaModal({
|
||||||
<MediaAltModal alt={showMediaAlt} />
|
<MediaAltModal alt={showMediaAlt} />
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useCallback, useRef, useState } from 'preact/hooks';
|
||||||
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
|
import QuickPinchZoom, { make3dTransformValue } from 'react-quick-pinch-zoom';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
import Link from './link';
|
||||||
import { formatDuration } from './status';
|
import { formatDuration } from './status';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -15,7 +16,7 @@ video = Video clip
|
||||||
audio = Audio track
|
audio = Audio track
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
function Media({ media, to, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
|
const { blurhash, description, meta, previewUrl, remoteUrl, url, type } =
|
||||||
media;
|
media;
|
||||||
const { original = {}, small, focus } = meta || {};
|
const { original = {}, small, focus } = meta || {};
|
||||||
|
@ -73,11 +74,13 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
onUpdate,
|
onUpdate,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Parent = to ? (props) => <Link to={to} {...props} /> : 'div';
|
||||||
|
|
||||||
if (type === 'image' || (type === 'unknown' && previewUrl && url)) {
|
if (type === 'image' || (type === 'unknown' && previewUrl && url)) {
|
||||||
// Note: type: unknown might not have width/height
|
// Note: type: unknown might not have width/height
|
||||||
quickPinchZoomProps.containerProps.style.display = 'inherit';
|
quickPinchZoomProps.containerProps.style.display = 'inherit';
|
||||||
return (
|
return (
|
||||||
<div
|
<Parent
|
||||||
class={`media media-image`}
|
class={`media media-image`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={
|
style={
|
||||||
|
@ -120,7 +123,7 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Parent>
|
||||||
);
|
);
|
||||||
} else if (type === 'gifv' || type === 'video') {
|
} else if (type === 'gifv' || type === 'video') {
|
||||||
const shortDuration = original.duration < 31;
|
const shortDuration = original.duration < 31;
|
||||||
|
@ -148,7 +151,7 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Parent
|
||||||
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
||||||
autoGIFAnimate ? 'media-contain' : ''
|
autoGIFAnimate ? 'media-contain' : ''
|
||||||
}`}
|
}`}
|
||||||
|
@ -226,12 +229,12 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Parent>
|
||||||
);
|
);
|
||||||
} else if (type === 'audio') {
|
} else if (type === 'audio') {
|
||||||
const formattedDuration = formatDuration(original.duration);
|
const formattedDuration = formatDuration(original.duration);
|
||||||
return (
|
return (
|
||||||
<div
|
<Parent
|
||||||
class="media media-audio"
|
class="media media-audio"
|
||||||
data-formatted-duration={formattedDuration}
|
data-formatted-duration={formattedDuration}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
@ -252,7 +255,7 @@ function Media({ media, showOriginal, autoAnimate, onClick = () => {} }) {
|
||||||
<Icon icon="play" size="xxl" />
|
<Icon icon="play" size="xxl" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Parent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ function Status({
|
||||||
enableTranslate,
|
enableTranslate,
|
||||||
previewMode,
|
previewMode,
|
||||||
allowFilters,
|
allowFilters,
|
||||||
|
onMediaClick,
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
|
@ -1024,16 +1025,16 @@ function Status({
|
||||||
key={media.id}
|
key={media.id}
|
||||||
media={media}
|
media={media}
|
||||||
autoAnimate={isSizeLarge}
|
autoAnimate={isSizeLarge}
|
||||||
onClick={(e) => {
|
to={`/${instance}/s/${id}?${
|
||||||
e.preventDefault();
|
withinContext ? 'media' : 'media-only'
|
||||||
e.stopPropagation();
|
}=${i + 1}`}
|
||||||
states.showMediaModal = {
|
onClick={
|
||||||
mediaAttachments,
|
onMediaClick
|
||||||
index: i,
|
? (e) => {
|
||||||
instance,
|
onMediaClick(e, i, media);
|
||||||
statusID: readOnly ? null : id,
|
}
|
||||||
};
|
: undefined
|
||||||
}}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,6 +42,8 @@ function Timeline({
|
||||||
const [visible, setVisible] = useState(true);
|
const [visible, setVisible] = useState(true);
|
||||||
const scrollableRef = useRef();
|
const scrollableRef = useRef();
|
||||||
|
|
||||||
|
console.debug('RENDER Timeline', id, refresh);
|
||||||
|
|
||||||
const loadItems = useDebouncedCallback(
|
const loadItems = useDebouncedCallback(
|
||||||
(firstLoad) => {
|
(firstLoad) => {
|
||||||
setShowNew(false);
|
setShowNew(false);
|
||||||
|
|
|
@ -18,6 +18,8 @@ function Following({ title, path, id, ...props }) {
|
||||||
const homeIterator = useRef();
|
const homeIterator = useRef();
|
||||||
const latestItem = useRef();
|
const latestItem = useRef();
|
||||||
|
|
||||||
|
console.debug('RENDER Following', title, id);
|
||||||
|
|
||||||
async function fetchHome(firstLoad) {
|
async function fetchHome(firstLoad) {
|
||||||
if (firstLoad || !homeIterator.current) {
|
if (firstLoad || !homeIterator.current) {
|
||||||
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
homeIterator.current = masto.v1.timelines.listHome({ limit: LIMIT });
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -75,4 +76,4 @@ function Home() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home;
|
export default memo(Home);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import pRetry from 'p-retry';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { matchPath, useNavigate, useParams } from 'react-router-dom';
|
import { matchPath, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { useSnapshot } from 'valtio';
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import Avatar from '../components/avatar';
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
|
import MediaModal from '../components/media-modal';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
import RelativeTime from '../components/relative-time';
|
import RelativeTime from '../components/relative-time';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
|
@ -21,6 +22,7 @@ import { api } from '../utils/api';
|
||||||
import htmlContentLength from '../utils/html-content-length';
|
import htmlContentLength from '../utils/html-content-length';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states, {
|
import states, {
|
||||||
|
getStatus,
|
||||||
saveStatus,
|
saveStatus,
|
||||||
statusKey,
|
statusKey,
|
||||||
threadifyStatus,
|
threadifyStatus,
|
||||||
|
@ -43,13 +45,97 @@ function resetScrollPosition(id) {
|
||||||
function StatusPage() {
|
function StatusPage() {
|
||||||
const { id, ...params } = useParams();
|
const { id, ...params } = useParams();
|
||||||
const { masto, instance } = api({ instance: params.instance });
|
const { masto, instance } = api({ instance: params.instance });
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const mediaParam = searchParams.get('media');
|
||||||
|
const mediaOnlyParam = searchParams.get('media-only');
|
||||||
|
const mediaIndex = parseInt(mediaParam || mediaOnlyParam, 10);
|
||||||
|
let showMedia = mediaIndex > 0;
|
||||||
|
const mediaStatusID = searchParams.get('mediaStatusID');
|
||||||
|
const mediaStatus = getStatus(mediaStatusID, instance);
|
||||||
|
if (mediaStatusID && !mediaStatus) {
|
||||||
|
showMedia = false;
|
||||||
|
}
|
||||||
|
const showMediaOnly = showMedia && !!mediaOnlyParam;
|
||||||
|
|
||||||
|
const sKey = statusKey(id, instance);
|
||||||
|
const [heroStatus, setHeroStatus] = useState(states.statuses[sKey]);
|
||||||
|
|
||||||
|
const closeLink = useMemo(() => {
|
||||||
|
const { prevLocation } = states;
|
||||||
|
const pathname =
|
||||||
|
(prevLocation?.pathname || '') + (prevLocation?.search || '');
|
||||||
|
const matchStatusPath =
|
||||||
|
matchPath('/:instance/s/:id', pathname) || matchPath('/s/:id', pathname);
|
||||||
|
if (!pathname || matchStatusPath) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
return pathname;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!heroStatus && showMedia) {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const status = await masto.v1.statuses.fetch(id);
|
||||||
|
saveStatus(status, instance);
|
||||||
|
setHeroStatus(status);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert('Unable to load status.');
|
||||||
|
location.hash = closeLink;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const mediaAttachments = mediaStatusID
|
||||||
|
? mediaStatus?.mediaAttachments
|
||||||
|
: heroStatus?.mediaAttachments;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="deck-backdrop">
|
||||||
|
{showMedia ? (
|
||||||
|
mediaAttachments?.length ? (
|
||||||
|
<MediaModal
|
||||||
|
mediaAttachments={mediaAttachments}
|
||||||
|
statusID={mediaStatusID || id}
|
||||||
|
instance={instance}
|
||||||
|
index={mediaIndex - 1}
|
||||||
|
onClose={() => {
|
||||||
|
if (showMediaOnly) {
|
||||||
|
location.hash = closeLink;
|
||||||
|
} else {
|
||||||
|
searchParams.delete('media');
|
||||||
|
searchParams.delete('mediaStatusID');
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div class="deck-loader">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Link to={closeLink} />
|
||||||
|
)}
|
||||||
|
{!showMediaOnly && <StatusThread closeLink={closeLink} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusThread({ closeLink = '/' }) {
|
||||||
|
const { id, ...params } = useParams();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const mediaParam = searchParams.get('media');
|
||||||
|
const showMedia = parseInt(mediaParam, 10) > 0;
|
||||||
|
const { masto, instance } = api({ instance: params.instance });
|
||||||
const {
|
const {
|
||||||
masto: currentMasto,
|
masto: currentMasto,
|
||||||
instance: currentInstance,
|
instance: currentInstance,
|
||||||
authenticated,
|
authenticated,
|
||||||
} = api();
|
} = api();
|
||||||
const sameInstance = instance === currentInstance;
|
const sameInstance = instance === currentInstance;
|
||||||
const navigate = useNavigate();
|
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [statuses, setStatuses] = useState([]);
|
const [statuses, setStatuses] = useState([]);
|
||||||
const [uiState, setUIState] = useState('default');
|
const [uiState, setUIState] = useState('default');
|
||||||
|
@ -69,7 +155,7 @@ function StatusPage() {
|
||||||
states.scrollPositions[id] = scrollTop;
|
states.scrollPositions[id] = scrollTop;
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
scrollableRef.current.addEventListener('scroll', onScroll, {
|
scrollableRef.current?.addEventListener('scroll', onScroll, {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
onScroll();
|
onScroll();
|
||||||
|
@ -331,23 +417,6 @@ function StatusPage() {
|
||||||
return postInstance === instance;
|
return postInstance === instance;
|
||||||
}, [postInstance, instance]);
|
}, [postInstance, instance]);
|
||||||
|
|
||||||
const closeLink = useMemo(() => {
|
|
||||||
const { prevLocation } = snapStates;
|
|
||||||
const pathname =
|
|
||||||
(prevLocation?.pathname || '') + (prevLocation?.search || '');
|
|
||||||
if (
|
|
||||||
!pathname ||
|
|
||||||
matchPath('/:instance/s/:id', pathname) ||
|
|
||||||
matchPath('/s/:id', pathname)
|
|
||||||
) {
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
return pathname;
|
|
||||||
}, []);
|
|
||||||
const onClose = () => {
|
|
||||||
states.showMediaModal = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [limit, setLimit] = useState(LIMIT);
|
const [limit, setLimit] = useState(LIMIT);
|
||||||
const showMore = useMemo(() => {
|
const showMore = useMemo(() => {
|
||||||
// return number of statuses to show
|
// return number of statuses to show
|
||||||
|
@ -367,10 +436,20 @@ function StatusPage() {
|
||||||
return top > 0 ? 'down' : 'up';
|
return top > 0 ? 'down' : 'up';
|
||||||
}, [heroInView]);
|
}, [heroInView]);
|
||||||
|
|
||||||
useHotkeys(['esc', 'backspace'], () => {
|
useHotkeys(
|
||||||
// location.hash = closeLink;
|
'esc',
|
||||||
onClose();
|
() => {
|
||||||
navigate(closeLink);
|
location.hash = closeLink;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If media is open, esc to close media first
|
||||||
|
// Else close the status page
|
||||||
|
enabled: !showMedia,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// For backspace, will always close both media and status page
|
||||||
|
useHotkeys('backspace', () => {
|
||||||
|
location.hash = closeLink;
|
||||||
});
|
});
|
||||||
|
|
||||||
useHotkeys('j', () => {
|
useHotkeys('j', () => {
|
||||||
|
@ -464,15 +543,15 @@ function StatusPage() {
|
||||||
distanceFromStartPx: 16,
|
distanceFromStartPx: 16,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initialPageState = useRef(showMedia ? 'media+status' : 'status');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="deck-backdrop">
|
|
||||||
<Link to={closeLink} onClick={onClose}></Link>
|
|
||||||
<div
|
<div
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
ref={scrollableRef}
|
ref={scrollableRef}
|
||||||
class={`status-deck deck contained ${
|
class={`status-deck deck contained ${
|
||||||
statuses.length > 1 ? 'padded-bottom' : ''
|
statuses.length > 1 ? 'padded-bottom' : ''
|
||||||
}`}
|
} ${initialPageState.current === 'status' ? 'slide-in' : ''}`}
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class={`${heroInView ? 'inview' : ''}`}
|
class={`${heroInView ? 'inview' : ''}`}
|
||||||
|
@ -596,7 +675,7 @@ function StatusPage() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const statusURL = getInstanceStatusURL(heroStatus.url);
|
const statusURL = getInstanceStatusURL(heroStatus.url);
|
||||||
if (statusURL) {
|
if (statusURL) {
|
||||||
navigate(statusURL);
|
location.hash = statusURL;
|
||||||
} else {
|
} else {
|
||||||
alert('Unable to switch');
|
alert('Unable to switch');
|
||||||
}
|
}
|
||||||
|
@ -609,11 +688,7 @@ function StatusPage() {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link class="button plain deck-close" to={closeLink}>
|
||||||
class="button plain deck-close"
|
|
||||||
to={closeLink}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<Icon icon="x" size="xl" />
|
<Icon icon="x" size="xl" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -663,8 +738,8 @@ function StatusPage() {
|
||||||
{uiState !== 'loading' && !authenticated ? (
|
{uiState !== 'loading' && !authenticated ? (
|
||||||
<div class="post-status-banner">
|
<div class="post-status-banner">
|
||||||
<p>
|
<p>
|
||||||
You're not logged in. Interactions (reply, boost,
|
You're not logged in. Interactions (reply, boost, etc)
|
||||||
etc) are not possible.
|
are not possible.
|
||||||
</p>
|
</p>
|
||||||
<Link to="/login" class="button">
|
<Link to="/login" class="button">
|
||||||
Log in
|
Log in
|
||||||
|
@ -675,8 +750,8 @@ function StatusPage() {
|
||||||
<div class="post-status-banner">
|
<div class="post-status-banner">
|
||||||
<p>
|
<p>
|
||||||
This post is from another instance (
|
This post is from another instance (
|
||||||
<b>{instance}</b>). Interactions (reply, boost,
|
<b>{instance}</b>). Interactions (reply, boost, etc)
|
||||||
etc) are not possible.
|
are not possible.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -685,8 +760,7 @@ function StatusPage() {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const results =
|
const results = await currentMasto.v2.search({
|
||||||
await currentMasto.v2.search({
|
|
||||||
q: heroStatus.url,
|
q: heroStatus.url,
|
||||||
type: 'statuses',
|
type: 'statuses',
|
||||||
resolve: true,
|
resolve: true,
|
||||||
|
@ -694,11 +768,9 @@ function StatusPage() {
|
||||||
});
|
});
|
||||||
if (results.statuses.length) {
|
if (results.statuses.length) {
|
||||||
const status = results.statuses[0];
|
const status = results.statuses[0];
|
||||||
navigate(
|
location.hash = currentInstance
|
||||||
currentInstance
|
|
||||||
? `/${currentInstance}/s/${status.id}`
|
? `/${currentInstance}/s/${status.id}`
|
||||||
: `/s/${status.id}`,
|
: `/s/${status.id}`;
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No results');
|
throw new Error('No results');
|
||||||
}
|
}
|
||||||
|
@ -721,9 +793,7 @@ function StatusPage() {
|
||||||
<Link
|
<Link
|
||||||
class="status-link"
|
class="status-link"
|
||||||
to={
|
to={
|
||||||
instance
|
instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
|
||||||
? `/${instance}/s/${statusID}`
|
|
||||||
: `/s/${statusID}`
|
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
resetScrollPosition(statusID);
|
resetScrollPosition(statusID);
|
||||||
|
@ -735,8 +805,16 @@ function StatusPage() {
|
||||||
withinContext
|
withinContext
|
||||||
size={thread || ancestor ? 'm' : 's'}
|
size={thread || ancestor ? 'm' : 's'}
|
||||||
enableTranslate
|
enableTranslate
|
||||||
|
onMediaClick={(e, i) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setSearchParams({
|
||||||
|
media: i + 1,
|
||||||
|
mediaStatusID: statusID,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{ancestor && isThread && !!repliesCount && (
|
{ancestor && isThread && repliesCount > 1 && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
<Icon icon="comment" />{' '}
|
<Icon icon="comment" />{' '}
|
||||||
<span title={repliesCount}>
|
<span title={repliesCount}>
|
||||||
|
@ -836,7 +914,6 @@ function StatusPage() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -847,6 +924,7 @@ function SubComments({
|
||||||
hasParentThread,
|
hasParentThread,
|
||||||
level,
|
level,
|
||||||
}) {
|
}) {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
// Set isBrief = true:
|
// Set isBrief = true:
|
||||||
// - if less than or 2 replies
|
// - if less than or 2 replies
|
||||||
// - if replies have no sub-replies
|
// - if replies have no sub-replies
|
||||||
|
@ -946,6 +1024,14 @@ function SubComments({
|
||||||
withinContext
|
withinContext
|
||||||
size="s"
|
size="s"
|
||||||
enableTranslate
|
enableTranslate
|
||||||
|
onMediaClick={(e, i) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setSearchParams({
|
||||||
|
media: i + 1,
|
||||||
|
mediaStatusID: r.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{!r.replies?.length && r.repliesCount > 0 && (
|
{!r.replies?.length && r.repliesCount > 0 && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
|
|
|
@ -19,6 +19,7 @@ export default function useScroll({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollableElement = scrollableRef.current;
|
const scrollableElement = scrollableRef.current;
|
||||||
|
if (!scrollableElement) return {};
|
||||||
let previousScrollStart = isVertical
|
let previousScrollStart = isVertical
|
||||||
? scrollableElement.scrollTop
|
? scrollableElement.scrollTop
|
||||||
: scrollableElement.scrollLeft;
|
: scrollableElement.scrollLeft;
|
||||||
|
|
Loading…
Reference in a new issue