mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-25 10:45:41 +03: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,287 +543,286 @@ function StatusPage() {
|
||||||
distanceFromStartPx: 16,
|
distanceFromStartPx: 16,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const initialPageState = useRef(showMedia ? 'media+status' : 'status');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="deck-backdrop">
|
<div
|
||||||
<Link to={closeLink} onClick={onClose}></Link>
|
tabIndex="-1"
|
||||||
<div
|
ref={scrollableRef}
|
||||||
tabIndex="-1"
|
class={`status-deck deck contained ${
|
||||||
ref={scrollableRef}
|
statuses.length > 1 ? 'padded-bottom' : ''
|
||||||
class={`status-deck deck contained ${
|
} ${initialPageState.current === 'status' ? 'slide-in' : ''}`}
|
||||||
statuses.length > 1 ? 'padded-bottom' : ''
|
>
|
||||||
}`}
|
<header
|
||||||
|
class={`${heroInView ? 'inview' : ''}`}
|
||||||
|
onDblClick={(e) => {
|
||||||
|
// reload statuses
|
||||||
|
states.reloadStatusPage++;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<header
|
{/* <div>
|
||||||
class={`${heroInView ? 'inview' : ''}`}
|
|
||||||
onDblClick={(e) => {
|
|
||||||
// reload statuses
|
|
||||||
states.reloadStatusPage++;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* <div>
|
|
||||||
<Link class="button plain deck-close" href={closeLink}>
|
<Link class="button plain deck-close" href={closeLink}>
|
||||||
<Icon icon="chevron-left" size="xl" />
|
<Icon icon="chevron-left" size="xl" />
|
||||||
</Link>
|
</Link>
|
||||||
</div> */}
|
</div> */}
|
||||||
<div class="header-grid header-grid-2">
|
<div class="header-grid header-grid-2">
|
||||||
<h1>
|
<h1>
|
||||||
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
{!heroInView && heroStatus && uiState !== 'loading' ? (
|
||||||
<>
|
<>
|
||||||
<span class="hero-heading">
|
<span class="hero-heading">
|
||||||
<NameText
|
<NameText
|
||||||
account={heroStatus.account}
|
account={heroStatus.account}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
showAvatar
|
showAvatar
|
||||||
short
|
short
|
||||||
/>{' '}
|
/>{' '}
|
||||||
<span class="insignificant">
|
<span class="insignificant">
|
||||||
•{' '}
|
•{' '}
|
||||||
<RelativeTime
|
<RelativeTime
|
||||||
datetime={heroStatus.createdAt}
|
datetime={heroStatus.createdAt}
|
||||||
format="micro"
|
format="micro"
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>{' '}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ancestors-indicator light small"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
heroStatusRef.current.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</span>
|
||||||
</>
|
</span>{' '}
|
||||||
) : (
|
<button
|
||||||
<>
|
type="button"
|
||||||
Status{' '}
|
class="ancestors-indicator light small"
|
||||||
<button
|
onClick={(e) => {
|
||||||
type="button"
|
e.preventDefault();
|
||||||
class="ancestors-indicator light small"
|
e.stopPropagation();
|
||||||
onClick={(e) => {
|
heroStatusRef.current.scrollIntoView({
|
||||||
// Scroll to top
|
behavior: 'smooth',
|
||||||
e.preventDefault();
|
block: 'start',
|
||||||
e.stopPropagation();
|
});
|
||||||
scrollableRef.current.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
hidden={!ancestors.length || nearReachStart}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-up" />
|
|
||||||
<Icon icon="comment" />{' '}
|
|
||||||
<span class="insignificant">
|
|
||||||
{shortenNumber(ancestors.length)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</h1>
|
|
||||||
<div class="header-side">
|
|
||||||
{uiState === 'loading' ? (
|
|
||||||
<Loader abrupt />
|
|
||||||
) : (
|
|
||||||
<Menu
|
|
||||||
align="end"
|
|
||||||
portal={{
|
|
||||||
// Need this, else the menu click will cause scroll jump
|
|
||||||
target: scrollableRef.current,
|
|
||||||
}}
|
}}
|
||||||
menuButton={
|
|
||||||
<button type="button" class="button plain4">
|
|
||||||
<Icon icon="more" alt="Actions" size="xl" />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<MenuItem
|
<Icon
|
||||||
disabled={uiState === 'loading'}
|
icon={heroPointer === 'down' ? 'arrow-down' : 'arrow-up'}
|
||||||
onClick={() => {
|
/>
|
||||||
states.reloadStatusPage++;
|
</button>
|
||||||
}}
|
</>
|
||||||
>
|
) : (
|
||||||
<Icon icon="refresh" />
|
<>
|
||||||
<span>Refresh</span>
|
Status{' '}
|
||||||
</MenuItem>
|
<button
|
||||||
<MenuItem
|
type="button"
|
||||||
onClick={() => {
|
class="ancestors-indicator light small"
|
||||||
// Click all buttons with class .spoiler but not .spoiling
|
onClick={(e) => {
|
||||||
const buttons = Array.from(
|
// Scroll to top
|
||||||
scrollableRef.current.querySelectorAll(
|
e.preventDefault();
|
||||||
'button.spoiler:not(.spoiling)',
|
e.stopPropagation();
|
||||||
),
|
scrollableRef.current.scrollTo({
|
||||||
);
|
top: 0,
|
||||||
buttons.forEach((button) => {
|
behavior: 'smooth',
|
||||||
button.click();
|
});
|
||||||
});
|
}}
|
||||||
}}
|
hidden={!ancestors.length || nearReachStart}
|
||||||
>
|
>
|
||||||
<Icon icon="eye-open" />{' '}
|
<Icon icon="arrow-up" />
|
||||||
<span>Show all sensitive content</span>
|
<Icon icon="comment" />{' '}
|
||||||
</MenuItem>
|
<span class="insignificant">
|
||||||
<MenuDivider />
|
{shortenNumber(ancestors.length)}
|
||||||
<MenuHeader className="plain">Experimental</MenuHeader>
|
</span>
|
||||||
<MenuItem
|
</button>
|
||||||
disabled={postSameInstance}
|
</>
|
||||||
onClick={() => {
|
)}
|
||||||
const statusURL = getInstanceStatusURL(heroStatus.url);
|
</h1>
|
||||||
if (statusURL) {
|
<div class="header-side">
|
||||||
navigate(statusURL);
|
{uiState === 'loading' ? (
|
||||||
} else {
|
<Loader abrupt />
|
||||||
alert('Unable to switch');
|
) : (
|
||||||
}
|
<Menu
|
||||||
}}
|
align="end"
|
||||||
>
|
portal={{
|
||||||
<Icon icon="transfer" />
|
// Need this, else the menu click will cause scroll jump
|
||||||
<small class="menu-double-lines">
|
target: scrollableRef.current,
|
||||||
Switch to post's instance (<b>{postInstance}</b>)
|
}}
|
||||||
</small>
|
menuButton={
|
||||||
</MenuItem>
|
<button type="button" class="button plain4">
|
||||||
</Menu>
|
<Icon icon="more" alt="Actions" size="xl" />
|
||||||
)}
|
</button>
|
||||||
<Link
|
}
|
||||||
class="button plain deck-close"
|
|
||||||
to={closeLink}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
<Icon icon="x" size="xl" />
|
<MenuItem
|
||||||
</Link>
|
disabled={uiState === 'loading'}
|
||||||
</div>
|
onClick={() => {
|
||||||
</div>
|
states.reloadStatusPage++;
|
||||||
</header>
|
}}
|
||||||
{!!statuses.length && heroStatus ? (
|
|
||||||
<ul
|
|
||||||
class={`timeline flat contextual grow ${
|
|
||||||
uiState === 'loading' ? 'loading' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{statuses.slice(0, limit).map((status) => {
|
|
||||||
const {
|
|
||||||
id: statusID,
|
|
||||||
ancestor,
|
|
||||||
isThread,
|
|
||||||
descendant,
|
|
||||||
thread,
|
|
||||||
replies,
|
|
||||||
repliesCount,
|
|
||||||
} = status;
|
|
||||||
const isHero = statusID === id;
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={statusID}
|
|
||||||
ref={isHero ? heroStatusRef : null}
|
|
||||||
class={`${ancestor ? 'ancestor' : ''} ${
|
|
||||||
descendant ? 'descendant' : ''
|
|
||||||
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
|
||||||
>
|
>
|
||||||
{isHero ? (
|
<Icon icon="refresh" />
|
||||||
<>
|
<span>Refresh</span>
|
||||||
<InView
|
</MenuItem>
|
||||||
threshold={0.1}
|
<MenuItem
|
||||||
onChange={onView}
|
onClick={() => {
|
||||||
class="status-focus"
|
// Click all buttons with class .spoiler but not .spoiling
|
||||||
tabIndex={0}
|
const buttons = Array.from(
|
||||||
>
|
scrollableRef.current.querySelectorAll(
|
||||||
<Status
|
'button.spoiler:not(.spoiling)',
|
||||||
statusID={statusID}
|
),
|
||||||
instance={instance}
|
);
|
||||||
withinContext
|
buttons.forEach((button) => {
|
||||||
size="l"
|
button.click();
|
||||||
enableTranslate
|
});
|
||||||
/>
|
}}
|
||||||
</InView>
|
>
|
||||||
{uiState !== 'loading' && !authenticated ? (
|
<Icon icon="eye-open" />{' '}
|
||||||
<div class="post-status-banner">
|
<span>Show all sensitive content</span>
|
||||||
<p>
|
</MenuItem>
|
||||||
You're not logged in. Interactions (reply, boost,
|
<MenuDivider />
|
||||||
etc) are not possible.
|
<MenuHeader className="plain">Experimental</MenuHeader>
|
||||||
</p>
|
<MenuItem
|
||||||
<Link to="/login" class="button">
|
disabled={postSameInstance}
|
||||||
Log in
|
onClick={() => {
|
||||||
</Link>
|
const statusURL = getInstanceStatusURL(heroStatus.url);
|
||||||
</div>
|
if (statusURL) {
|
||||||
) : (
|
location.hash = statusURL;
|
||||||
!sameInstance && (
|
} else {
|
||||||
<div class="post-status-banner">
|
alert('Unable to switch');
|
||||||
<p>
|
}
|
||||||
This post is from another instance (
|
}}
|
||||||
<b>{instance}</b>). Interactions (reply, boost,
|
>
|
||||||
etc) are not possible.
|
<Icon icon="transfer" />
|
||||||
</p>
|
<small class="menu-double-lines">
|
||||||
<button
|
Switch to post's instance (<b>{postInstance}</b>)
|
||||||
type="button"
|
</small>
|
||||||
disabled={uiState === 'loading'}
|
</MenuItem>
|
||||||
onClick={() => {
|
</Menu>
|
||||||
setUIState('loading');
|
)}
|
||||||
(async () => {
|
<Link class="button plain deck-close" to={closeLink}>
|
||||||
try {
|
<Icon icon="x" size="xl" />
|
||||||
const results =
|
</Link>
|
||||||
await currentMasto.v2.search({
|
</div>
|
||||||
q: heroStatus.url,
|
</div>
|
||||||
type: 'statuses',
|
</header>
|
||||||
resolve: true,
|
{!!statuses.length && heroStatus ? (
|
||||||
limit: 1,
|
<ul
|
||||||
});
|
class={`timeline flat contextual grow ${
|
||||||
if (results.statuses.length) {
|
uiState === 'loading' ? 'loading' : ''
|
||||||
const status = results.statuses[0];
|
}`}
|
||||||
navigate(
|
>
|
||||||
currentInstance
|
{statuses.slice(0, limit).map((status) => {
|
||||||
? `/${currentInstance}/s/${status.id}`
|
const {
|
||||||
: `/s/${status.id}`,
|
id: statusID,
|
||||||
);
|
ancestor,
|
||||||
} else {
|
isThread,
|
||||||
throw new Error('No results');
|
descendant,
|
||||||
}
|
thread,
|
||||||
} catch (e) {
|
replies,
|
||||||
setUIState('default');
|
repliesCount,
|
||||||
alert('Error: ' + e);
|
} = status;
|
||||||
console.error(e);
|
const isHero = statusID === id;
|
||||||
}
|
return (
|
||||||
})();
|
<li
|
||||||
}}
|
key={statusID}
|
||||||
>
|
ref={isHero ? heroStatusRef : null}
|
||||||
<Icon icon="transfer" /> Switch to my instance to
|
class={`${ancestor ? 'ancestor' : ''} ${
|
||||||
enable interactions
|
descendant ? 'descendant' : ''
|
||||||
</button>
|
} ${thread ? 'thread' : ''} ${isHero ? 'hero' : ''}`}
|
||||||
</div>
|
>
|
||||||
)
|
{isHero ? (
|
||||||
)}
|
<>
|
||||||
</>
|
<InView
|
||||||
) : (
|
threshold={0.1}
|
||||||
<Link
|
onChange={onView}
|
||||||
class="status-link"
|
class="status-focus"
|
||||||
to={
|
tabIndex={0}
|
||||||
instance
|
|
||||||
? `/${instance}/s/${statusID}`
|
|
||||||
: `/s/${statusID}`
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
resetScrollPosition(statusID);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Status
|
<Status
|
||||||
statusID={statusID}
|
statusID={statusID}
|
||||||
instance={instance}
|
instance={instance}
|
||||||
withinContext
|
withinContext
|
||||||
size={thread || ancestor ? 'm' : 's'}
|
size="l"
|
||||||
enableTranslate
|
enableTranslate
|
||||||
/>
|
/>
|
||||||
{ancestor && isThread && !!repliesCount && (
|
</InView>
|
||||||
<div class="replies-link">
|
{uiState !== 'loading' && !authenticated ? (
|
||||||
<Icon icon="comment" />{' '}
|
<div class="post-status-banner">
|
||||||
<span title={repliesCount}>
|
<p>
|
||||||
{shortenNumber(repliesCount)}
|
You're not logged in. Interactions (reply, boost, etc)
|
||||||
</span>
|
are not possible.
|
||||||
|
</p>
|
||||||
|
<Link to="/login" class="button">
|
||||||
|
Log in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
!sameInstance && (
|
||||||
|
<div class="post-status-banner">
|
||||||
|
<p>
|
||||||
|
This post is from another instance (
|
||||||
|
<b>{instance}</b>). Interactions (reply, boost, etc)
|
||||||
|
are not possible.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => {
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const results = await currentMasto.v2.search({
|
||||||
|
q: heroStatus.url,
|
||||||
|
type: 'statuses',
|
||||||
|
resolve: true,
|
||||||
|
limit: 1,
|
||||||
|
});
|
||||||
|
if (results.statuses.length) {
|
||||||
|
const status = results.statuses[0];
|
||||||
|
location.hash = currentInstance
|
||||||
|
? `/${currentInstance}/s/${status.id}`
|
||||||
|
: `/s/${status.id}`;
|
||||||
|
} else {
|
||||||
|
throw new Error('No results');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setUIState('default');
|
||||||
|
alert('Error: ' + e);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="transfer" /> Switch to my instance to
|
||||||
|
enable interactions
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}{' '}
|
)
|
||||||
{/* {replies?.length > LIMIT && (
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
class="status-link"
|
||||||
|
to={
|
||||||
|
instance ? `/${instance}/s/${statusID}` : `/s/${statusID}`
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
resetScrollPosition(statusID);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
withinContext
|
||||||
|
size={thread || ancestor ? 'm' : 's'}
|
||||||
|
enableTranslate
|
||||||
|
onMediaClick={(e, i) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setSearchParams({
|
||||||
|
media: i + 1,
|
||||||
|
mediaStatusID: statusID,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{ancestor && isThread && repliesCount > 1 && (
|
||||||
|
<div class="replies-link">
|
||||||
|
<Icon icon="comment" />{' '}
|
||||||
|
<span title={repliesCount}>
|
||||||
|
{shortenNumber(repliesCount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}{' '}
|
||||||
|
{/* {replies?.length > LIMIT && (
|
||||||
<div class="replies-link">
|
<div class="replies-link">
|
||||||
<Icon icon="comment" />{' '}
|
<Icon icon="comment" />{' '}
|
||||||
<span title={replies.length}>
|
<span title={replies.length}>
|
||||||
|
@ -752,90 +830,89 @@ function StatusPage() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
{descendant && replies?.length > 0 && (
|
||||||
|
<SubComments
|
||||||
|
instance={instance}
|
||||||
|
hasManyStatuses={hasManyStatuses}
|
||||||
|
replies={replies}
|
||||||
|
hasParentThread={thread}
|
||||||
|
level={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{uiState === 'loading' &&
|
||||||
|
isHero &&
|
||||||
|
!!heroStatus?.repliesCount &&
|
||||||
|
!hasDescendants && (
|
||||||
|
<div class="status-loading">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{descendant && replies?.length > 0 && (
|
{uiState === 'error' &&
|
||||||
<SubComments
|
isHero &&
|
||||||
instance={instance}
|
!!heroStatus?.repliesCount &&
|
||||||
hasManyStatuses={hasManyStatuses}
|
!hasDescendants && (
|
||||||
replies={replies}
|
<div class="status-error">
|
||||||
hasParentThread={thread}
|
Unable to load replies.
|
||||||
level={1}
|
<br />
|
||||||
/>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain"
|
||||||
|
onClick={() => {
|
||||||
|
states.reloadStatusPage++;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{uiState === 'loading' &&
|
|
||||||
isHero &&
|
|
||||||
!!heroStatus?.repliesCount &&
|
|
||||||
!hasDescendants && (
|
|
||||||
<div class="status-loading">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{uiState === 'error' &&
|
|
||||||
isHero &&
|
|
||||||
!!heroStatus?.repliesCount &&
|
|
||||||
!hasDescendants && (
|
|
||||||
<div class="status-error">
|
|
||||||
Unable to load replies.
|
|
||||||
<br />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="plain"
|
|
||||||
onClick={() => {
|
|
||||||
states.reloadStatusPage++;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{showMore > 0 && (
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="plain block"
|
|
||||||
disabled={uiState === 'loading'}
|
|
||||||
onClick={() => setLimit((l) => l + LIMIT)}
|
|
||||||
style={{ marginBlockEnd: '6em' }}
|
|
||||||
>
|
|
||||||
Show more…{' '}
|
|
||||||
<span class="tag">
|
|
||||||
{showMore > LIMIT ? `${LIMIT}+` : showMore}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
)}
|
);
|
||||||
</ul>
|
})}
|
||||||
) : (
|
{showMore > 0 && (
|
||||||
<>
|
<li>
|
||||||
{uiState === 'loading' && (
|
<button
|
||||||
<ul class="timeline flat contextual grow loading">
|
type="button"
|
||||||
<li>
|
class="plain block"
|
||||||
<Status skeleton size="l" />
|
disabled={uiState === 'loading'}
|
||||||
</li>
|
onClick={() => setLimit((l) => l + LIMIT)}
|
||||||
</ul>
|
style={{ marginBlockEnd: '6em' }}
|
||||||
)}
|
>
|
||||||
{uiState === 'error' && (
|
Show more…{' '}
|
||||||
<p class="ui-state">
|
<span class="tag">
|
||||||
Unable to load status
|
{showMore > LIMIT ? `${LIMIT}+` : showMore}
|
||||||
<br />
|
</span>
|
||||||
<br />
|
</button>
|
||||||
<button
|
</li>
|
||||||
type="button"
|
)}
|
||||||
onClick={() => {
|
</ul>
|
||||||
states.reloadStatusPage++;
|
) : (
|
||||||
}}
|
<>
|
||||||
>
|
{uiState === 'loading' && (
|
||||||
Try again
|
<ul class="timeline flat contextual grow loading">
|
||||||
</button>
|
<li>
|
||||||
</p>
|
<Status skeleton size="l" />
|
||||||
)}
|
</li>
|
||||||
</>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
{uiState === 'error' && (
|
||||||
|
<p class="ui-state">
|
||||||
|
Unable to load status
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
states.reloadStatusPage++;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</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