Early implementation of media-first UI experience

pull/493/head
Lim Chee Aun 2024-04-11 17:18:17 +08:00
rodzic 6e73728e2b
commit a0d2037007
5 zmienionych plików z 682 dodań i 230 usunięć

Wyświetl plik

@ -301,6 +301,34 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
} }
} }
} }
.deck-container-media-first {
.timeline {
> li:not(.timeline-item-carousel, .timeline-item-container) {
&:has(.status-media-first) {
width: fit-content;
background-color: transparent !important;
border: 0 !important;
box-shadow: none !important;
max-width: min(480px, 100%);
margin-inline: auto !important;
&:has(.skeleton) {
width: 100%;
}
}
&:has(.media[data-orientation='landscape']) {
max-width: 100%;
}
}
.status-link:has(.status-media-first):hover {
background-color: transparent;
}
}
}
.timeline.grow { .timeline.grow {
/* min-height: 100vh; /* min-height: 100vh;
min-height: 100dvh; */ min-height: 100dvh; */

Wyświetl plik

@ -618,6 +618,7 @@
~ *:not( ~ *:not(
.content.truncated, .content.truncated,
.media-container, .media-container,
.media-first-container,
.card, .card,
.media-figure-multiple, .media-figure-multiple,
.spoiler-media-button .spoiler-media-button
@ -638,6 +639,7 @@
~ *:not( ~ *:not(
.media-container, .media-container,
.media-first-container,
.card, .card,
.media-figure-multiple, .media-figure-multiple,
.spoiler-media-button .spoiler-media-button
@ -708,11 +710,12 @@
} }
} }
~ :is(.media-container, .media-figure-multiple) .media { ~ :is(.media-container, .media-first-container, .media-figure-multiple)
.media {
background-image: radial-gradient( background-image: radial-gradient(
circle at 50% 50%, circle at 50% 50%,
var(--average-color, var(--bg-faded-color)), var(--average-color, var(--bg-faded-color)),
var(--bg-color) 20em var(--bg-color) 25em
); );
> *:not(.media-play, .alt-badge) { > *:not(.media-play, .alt-badge) {
@ -1316,6 +1319,227 @@ body:has(#modal-container .carousel) .status .media img:hover {
background-blend-mode: multiply; background-blend-mode: multiply;
} }
.status.skeleton .media-first-container {
min-height: 3em;
background-color: var(--outline-color);
}
.status-media-first {
.meta-name {
opacity: 0.65;
transition: opacity 0.5s ease-in-out;
b + i {
opacity: 0;
transition: opacity 0.5s ease-in-out;
}
}
:is(:hover, :focus) > & .meta-name {
opacity: 1;
b + i {
opacity: 0.5;
}
}
.media-first-spoiler-content {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
transition: opacity 0.5s ease-in-out;
opacity: 0.5;
}
&:hover .media-first-spoiler-content {
opacity: 1;
}
.media-first-spoiler-button {
display: inline-flex !important;
}
.media-first-container {
margin-top: 8px;
display: flex;
max-height: 80vh;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
user-select: none;
margin-inline: -16px;
position: relative;
scrollbar-width: none;
/* border: var(--hairline-width) solid var(--outline-color);
border-inline-width: 0;
background-color: var(--bg-faded-color); */
@media (min-width: 40em) {
margin-inline: 0;
/* border-radius: 4px; */
border-inline-width: var(--hairline-width);
}
&::-webkit-scrollbar {
display: none;
}
> .media-first-item {
scroll-snap-align: center;
scroll-snap-stop: always;
flex-shrink: 0;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
&:not(:only-child) {
background-color: var(--bg-blur-color);
box-shadow: inset 0 0 0 var(--hairline-width) var(--outline-color);
}
.media {
/* background-color: var(--average-color, var(--bg-faded-color)); */
width: var(--width);
max-width: 100%;
max-height: 100%;
min-height: var(--min-dimension);
/* max-height: min(var(--height), 80vh); */
&:active {
transform: none;
}
img,
video {
object-fit: scale-down;
animation: none;
&:not([data-loaded='true']) {
background-color: var(--bg-color);
}
}
}
}
.media-carousel-controls {
flex-shrink: 0;
width: 100%;
position: sticky;
right: 0;
left: 0;
pointer-events: none;
display: flex;
justify-content: space-between;
}
.carousel-indexer {
z-index: 1;
position: absolute;
top: 8px;
right: 8px;
color: var(--media-fg-color);
background-color: var(--media-bg-color);
padding: 2px 8px;
border-radius: 16px;
font-size: 0.8em;
font-variant-numeric: tabular-nums;
opacity: 0.6;
transition: opacity 1.5s ease-in-out;
border: var(--hairline-width) solid var(--media-outline-color);
}
.media-carousel-button {
display: flex;
flex-shrink: 0;
padding-inline: 8px;
margin-block: 3em;
pointer-events: auto;
cursor: pointer;
align-items: center;
justify-content: center;
}
.carousel-button {
@media (pointer: coarse) {
display: none;
}
+ .carousel-button {
left: auto;
right: 8px;
}
}
@media (hover: hover) and (pointer: fine) {
.carousel-button {
filter: opacity(0);
}
&:hover .carousel-button {
filter: opacity(1);
}
}
}
:is(:hover, :focus) > & .carousel-indexer {
opacity: 0;
}
.media-carousel-dots {
pointer-events: none;
display: flex;
gap: 5px;
justify-content: center;
margin-top: 8px;
padding: 8px;
.carousel-dot {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: var(--text-color);
transition: all 0.3s ease-in-out;
opacity: 0.3;
&.active {
opacity: 1;
background-color: var(--text-color);
transform: scale(1.5);
}
}
}
.media-first-content {
margin-top: 8px;
height: 1.75em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9em;
mask-image: linear-gradient(to bottom, black 1.5em, transparent 1.75em);
opacity: 0.5;
transition: opacity 0.5s ease-in-out;
@media (min-width: 40em) {
margin-inline: 16px;
}
* {
text-align: center;
/* Brute force ellipsis */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap !important;
pointer-events: none;
}
a {
filter: grayscale(0.5);
}
}
:is(:hover, :focus) > & .media-first-content {
opacity: 1;
}
}
.status:not(.large) .hashtag-stuffing { .status:not(.large) .hashtag-stuffing {
opacity: 0.75; opacity: 0.75;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;

Wyświetl plik

@ -169,15 +169,19 @@ function Status({
allowContextMenu, allowContextMenu,
showActionsBar, showActionsBar,
showReplyParent, showReplyParent,
mediaFirst,
}) { }) {
if (skeleton) { if (skeleton) {
return ( return (
<div class="status skeleton"> <div class={`status skeleton ${mediaFirst ? 'status-media-first' : ''}`}>
<Avatar size="xxl" /> {!mediaFirst && <Avatar size="xxl" />}
<div class="container"> <div class="container">
<div class="meta"> </div> <div class="meta">
{(size === 's' || mediaFirst) && <Avatar size="m" />}
</div>
<div class="content-container"> <div class="content-container">
<div class="content"> {mediaFirst && <div class="media-first-container" />}
<div class={`content ${mediaFirst ? 'media-first-content' : ''}`}>
<p> </p> <p> </p>
</div> </div>
</div> </div>
@ -247,6 +251,10 @@ function Status({
emojiReactions, emojiReactions,
} = status; } = status;
// if (!mediaAttachments?.length) mediaFirst = false;
const hasMediaAttachments = !!mediaAttachments?.length;
if (mediaFirst && hasMediaAttachments) size = 's';
const currentAccount = useMemo(() => { const currentAccount = useMemo(() => {
return store.session.get('currentAccount'); return store.session.get('currentAccount');
}, []); }, []);
@ -354,6 +362,7 @@ function Status({
size={size} size={size}
contentTextWeight={contentTextWeight} contentTextWeight={contentTextWeight}
readOnly={readOnly} readOnly={readOnly}
mediaFirst={mediaFirst}
/> />
</div> </div>
); );
@ -378,6 +387,7 @@ function Status({
contentTextWeight={contentTextWeight} contentTextWeight={contentTextWeight}
readOnly={readOnly} readOnly={readOnly}
enableCommentHint enableCommentHint
mediaFirst={mediaFirst}
/> />
</div> </div>
); );
@ -411,6 +421,7 @@ function Status({
contentTextWeight={contentTextWeight} contentTextWeight={contentTextWeight}
readOnly={readOnly} readOnly={readOnly}
enableCommentHint enableCommentHint
mediaFirst={mediaFirst}
/> />
</div> </div>
); );
@ -848,56 +859,62 @@ function Status({
</MenuItem> </MenuItem>
</> </>
)} )}
{(enableTranslate || !language || differentLanguage) && <MenuDivider />} {!mediaFirst && (
{enableTranslate ? ( <>
<div class={supportsTTS ? 'menu-horizontal' : ''}> {(enableTranslate || !language || differentLanguage) && (
<MenuItem <MenuDivider />
disabled={forceTranslate}
onClick={() => {
setForceTranslate(true);
}}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuItem>
{supportsTTS && (
<MenuItem
onClick={() => {
const postText = getPostText(status);
if (postText) {
speak(postText, language);
}
}}
>
<Icon icon="speak" />
<span>Speak</span>
</MenuItem>
)} )}
</div> {enableTranslate ? (
) : ( <div class={supportsTTS ? 'menu-horizontal' : ''}>
(!language || differentLanguage) && (
<div class={supportsTTS ? 'menu-horizontal' : ''}>
<MenuLink
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuLink>
{supportsTTS && (
<MenuItem <MenuItem
disabled={forceTranslate}
onClick={() => { onClick={() => {
const postText = getPostText(status); setForceTranslate(true);
if (postText) {
speak(postText, language);
}
}} }}
> >
<Icon icon="speak" /> <Icon icon="translate" />
<span>Speak</span> <span>Translate</span>
</MenuItem> </MenuItem>
)} {supportsTTS && (
</div> <MenuItem
) onClick={() => {
const postText = getPostText(status);
if (postText) {
speak(postText, language);
}
}}
>
<Icon icon="speak" />
<span>Speak</span>
</MenuItem>
)}
</div>
) : (
(!language || differentLanguage) && (
<div class={supportsTTS ? 'menu-horizontal' : ''}>
<MenuLink
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
>
<Icon icon="translate" />
<span>Translate</span>
</MenuLink>
{supportsTTS && (
<MenuItem
onClick={() => {
const postText = getPostText(status);
if (postText) {
speak(postText, language);
}
}}
>
<Icon icon="speak" />
<span>Speak</span>
</MenuItem>
)}
</div>
)
)}
</>
)} )}
{((!isSizeLarge && sameInstance) || {((!isSizeLarge && sameInstance) ||
enableTranslate || enableTranslate ||
@ -1384,7 +1401,7 @@ function Status({
}[size] }[size]
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${ } ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${
isContextMenuOpen ? 'status-menu-open' : '' isContextMenuOpen ? 'status-menu-open' : ''
}`} } ${mediaFirst && hasMediaAttachments ? 'status-media-first' : ''}`}
onMouseEnter={debugHover} onMouseEnter={debugHover}
onContextMenu={(e) => { onContextMenu={(e) => {
if (!showContextMenu) return; if (!showContextMenu) return;
@ -1712,188 +1729,253 @@ function Status({
} }
} }
> >
{!!spoilerText && ( {mediaFirst && hasMediaAttachments ? (
<> <>
<div {(!!spoilerText || !!sensitive) && !readingExpandSpoilers && (
class="content spoiler-content" <>
lang={language} {!!spoilerText && (
dir="auto" <span
ref={spoilerContentRef} class="spoiler-content media-first-spoiler-content"
data-read-more={readMoreText} lang={language}
> dir="auto"
<p> ref={spoilerContentRef}
<EmojiText text={spoilerText} emojis={emojis} /> data-read-more={readMoreText}
</p> >
</div> <EmojiText text={spoilerText} emojis={emojis} />{' '}
{readingExpandSpoilers || previewMode ? ( </span>
<div class="spoiler-divider"> )}
<Icon icon="eye-open" /> Content warning <button
class={`light spoiler-button media-first-spoiler-button ${
showSpoiler ? 'spoiling' : ''
}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoiler) {
delete states.spoilers[id];
if (!readingExpandSpoilers) {
delete states.spoilersMedia[id];
}
} else {
states.spoilers[id] = true;
if (!readingExpandSpoilers) {
states.spoilersMedia[id] = true;
}
}
}}
>
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
{showSpoiler ? 'Show less' : 'Show content'}
</button>
</>
)}
<MediaFirstContainer
mediaAttachments={mediaAttachments}
language={language}
postID={id}
instance={instance}
/>
{!!content && (
<div class="media-first-content content" ref={contentRef}>
<PostContent
post={status}
instance={instance}
previewMode={previewMode}
/>
</div> </div>
) : (
<button
class={`light spoiler-button ${
showSpoiler ? 'spoiling' : ''
}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoiler) {
delete states.spoilers[id];
if (!readingExpandSpoilers) {
delete states.spoilersMedia[id];
}
} else {
states.spoilers[id] = true;
if (!readingExpandSpoilers) {
states.spoilersMedia[id] = true;
}
}
}}
>
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
{showSpoiler ? 'Show less' : 'Show content'}
</button>
)} )}
</> </>
)} ) : (
{!!content && ( <>
<div {!!spoilerText && (
class="content" <>
ref={contentRef} <div
data-read-more={readMoreText} class="content spoiler-content"
>
<PostContent
post={status}
instance={instance}
previewMode={previewMode}
/>
<QuoteStatuses id={id} instance={instance} level={quoted} />
</div>
)}
{!!poll && (
<Poll
lang={language}
poll={poll}
readOnly={readOnly || !sameInstance || !authenticated}
onUpdate={(newPoll) => {
states.statuses[sKey].poll = newPoll;
}}
refresh={() => {
return masto.v1.polls
.$select(poll.id)
.fetch()
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
votePoll={(choices) => {
return masto.v1.polls
.$select(poll.id)
.votes.create({
choices,
})
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
/>
)}
{(((enableTranslate || inlineTranslate) &&
!!content.trim() &&
!!getHTMLText(emojifyText(content, emojis)) &&
differentLanguage) ||
forceTranslate) && (
<TranslationBlock
forceTranslate={forceTranslate || inlineTranslate}
mini={!isSizeLarge && !withinContext}
sourceLanguage={language}
text={getPostText(status)}
/>
)}
{!previewMode &&
sensitive &&
!!mediaAttachments.length &&
readingExpandMedia !== 'show_all' && (
<button
class={`plain spoiler-media-button ${
showSpoilerMedia ? 'spoiling' : ''
}`}
type="button"
hidden={!readingExpandSpoilers && !!spoilerText}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoilerMedia) {
delete states.spoilersMedia[id];
} else {
states.spoilersMedia[id] = true;
}
}}
>
<Icon icon={showSpoilerMedia ? 'eye-open' : 'eye-close'} />{' '}
{showSpoilerMedia ? 'Show less' : 'Show media'}
</button>
)}
{!!mediaAttachments.length && (
<MultipleMediaFigure
lang={language}
enabled={showMultipleMediaCaptions}
captionChildren={captionChildren}
>
<div
ref={mediaContainerRef}
class={`media-container media-eq${mediaAttachments.length} ${
mediaAttachments.length > 2 ? 'media-gt2' : ''
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
>
{displayedMediaAttachments.map((media, i) => (
<Media
key={media.id}
media={media}
autoAnimate={isSizeLarge}
showCaption={mediaAttachments.length === 1}
allowLongerCaption={
!content && mediaAttachments.length === 1
}
lang={language} lang={language}
altIndex={ dir="auto"
showMultipleMediaCaptions && ref={spoilerContentRef}
!!media.description && data-read-more={readMoreText}
i + 1 >
} <p>
to={`/${instance}/s/${id}?${ <EmojiText text={spoilerText} emojis={emojis} />
withinContext ? 'media' : 'media-only' </p>
}=${i + 1}`} </div>
onClick={ {readingExpandSpoilers || previewMode ? (
onMediaClick <div class="spoiler-divider">
? (e) => { <Icon icon="eye-open" /> Content warning
onMediaClick(e, i, media, status); </div>
) : (
<button
class={`light spoiler-button ${
showSpoiler ? 'spoiling' : ''
}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoiler) {
delete states.spoilers[id];
if (!readingExpandSpoilers) {
delete states.spoilersMedia[id];
} }
: undefined } else {
} states.spoilers[id] = true;
if (!readingExpandSpoilers) {
states.spoilersMedia[id] = true;
}
}
}}
>
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
{showSpoiler ? 'Show less' : 'Show content'}
</button>
)}
</>
)}
{!!content && (
<div
class="content"
ref={contentRef}
data-read-more={readMoreText}
>
<PostContent
post={status}
instance={instance}
previewMode={previewMode}
/> />
))} <QuoteStatuses id={id} instance={instance} level={quoted} />
</div> </div>
</MultipleMediaFigure> )}
{!!poll && (
<Poll
lang={language}
poll={poll}
readOnly={readOnly || !sameInstance || !authenticated}
onUpdate={(newPoll) => {
states.statuses[sKey].poll = newPoll;
}}
refresh={() => {
return masto.v1.polls
.$select(poll.id)
.fetch()
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
votePoll={(choices) => {
return masto.v1.polls
.$select(poll.id)
.votes.create({
choices,
})
.then((pollResponse) => {
states.statuses[sKey].poll = pollResponse;
})
.catch((e) => {}); // Silently fail
}}
/>
)}
{(((enableTranslate || inlineTranslate) &&
!!content.trim() &&
!!getHTMLText(emojifyText(content, emojis)) &&
differentLanguage) ||
forceTranslate) && (
<TranslationBlock
forceTranslate={forceTranslate || inlineTranslate}
mini={!isSizeLarge && !withinContext}
sourceLanguage={language}
text={getPostText(status)}
/>
)}
{!previewMode &&
sensitive &&
!!mediaAttachments.length &&
readingExpandMedia !== 'show_all' && (
<button
class={`plain spoiler-media-button ${
showSpoilerMedia ? 'spoiling' : ''
}`}
type="button"
hidden={!readingExpandSpoilers && !!spoilerText}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (showSpoilerMedia) {
delete states.spoilersMedia[id];
} else {
states.spoilersMedia[id] = true;
}
}}
>
<Icon
icon={showSpoilerMedia ? 'eye-open' : 'eye-close'}
/>{' '}
{showSpoilerMedia ? 'Show less' : 'Show media'}
</button>
)}
{!!mediaAttachments.length && (
<MultipleMediaFigure
lang={language}
enabled={showMultipleMediaCaptions}
captionChildren={captionChildren}
>
<div
ref={mediaContainerRef}
class={`media-container media-eq${
mediaAttachments.length
} ${mediaAttachments.length > 2 ? 'media-gt2' : ''} ${
mediaAttachments.length > 4 ? 'media-gt4' : ''
}`}
>
{displayedMediaAttachments.map((media, i) => (
<Media
key={media.id}
media={media}
autoAnimate={isSizeLarge}
showCaption={mediaAttachments.length === 1}
allowLongerCaption={
!content && mediaAttachments.length === 1
}
lang={language}
altIndex={
showMultipleMediaCaptions &&
!!media.description &&
i + 1
}
to={`/${instance}/s/${id}?${
withinContext ? 'media' : 'media-only'
}=${i + 1}`}
onClick={
onMediaClick
? (e) => {
onMediaClick(e, i, media, status);
}
: undefined
}
/>
))}
</div>
</MultipleMediaFigure>
)}
{!!card &&
/^https/i.test(card?.url) &&
!sensitive &&
!spoilerText &&
!poll &&
!mediaAttachments.length &&
!snapStates.statusQuotes[sKey] && (
<Card
card={card}
selfReferential={
card?.url === status.url || card?.url === status.uri
}
instance={currentInstance}
/>
)}
</>
)} )}
{!!card &&
/^https/i.test(card?.url) &&
!sensitive &&
!spoilerText &&
!poll &&
!mediaAttachments.length &&
!snapStates.statusQuotes[sKey] && (
<Card
card={card}
selfReferential={
card?.url === status.url || card?.url === status.uri
}
instance={currentInstance}
/>
)}
</div> </div>
{!isSizeLarge && showCommentCount && ( {!isSizeLarge && showCommentCount && (
<div class="content-comment-hint insignificant"> <div class="content-comment-hint insignificant">
@ -2171,6 +2253,101 @@ function MultipleMediaFigure(props) {
); );
} }
function MediaFirstContainer(props) {
const { mediaAttachments, language, postID, instance } = props;
const moreThanOne = mediaAttachments.length > 1;
const carouselRef = useRef();
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
let handleScroll = () => {
const { clientWidth, scrollLeft } = carouselRef.current;
const index = Math.round(scrollLeft / clientWidth);
setCurrentIndex(index);
};
if (carouselRef.current) {
carouselRef.current.addEventListener('scroll', handleScroll, {
passive: true,
});
}
return () => {
if (carouselRef.current) {
carouselRef.current.removeEventListener('scroll', handleScroll);
}
};
}, []);
return (
<>
<div class="media-first-container" ref={carouselRef}>
{mediaAttachments.map((media, i) => (
<div class="media-first-item" key={media.id}>
<Media
media={media}
lang={language}
to={`/${instance}/s/${postID}?media-only=${i + 1}`}
/>
</div>
))}
{moreThanOne && (
<div class="media-carousel-controls">
<div class="carousel-indexer">
{currentIndex + 1}/{mediaAttachments.length}
</div>
<label class="media-carousel-button">
<button
type="button"
class="carousel-button"
hidden={currentIndex === 0}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex - 1),
behavior: 'smooth',
});
}}
>
<Icon icon="arrow-left" />
</button>
</label>
<label class="media-carousel-button">
<button
type="button"
class="carousel-button"
hidden={currentIndex === mediaAttachments.length - 1}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
carouselRef.current.focus();
carouselRef.current.scrollTo({
left: carouselRef.current.clientWidth * (currentIndex + 1),
behavior: 'smooth',
});
}}
>
<Icon icon="arrow-right" />
</button>
</label>
</div>
)}
</div>
{moreThanOne && (
<div class="media-carousel-dots">
{mediaAttachments.map((media, i) => (
<span
key={media.id}
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
/>
))}
</div>
)}
</>
);
}
function Card({ card, selfReferential, instance }) { function Card({ card, selfReferential, instance }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
const { const {

Wyświetl plik

@ -1,5 +1,11 @@
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; import {
useCallback,
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 { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
@ -9,6 +15,7 @@ import FilterContext from '../utils/filter-context';
import { filteredItems, isFiltered } from '../utils/filters'; import { filteredItems, isFiltered } from '../utils/filters';
import states, { statusKey } from '../utils/states'; import states, { statusKey } from '../utils/states';
import statusPeek from '../utils/status-peek'; import statusPeek from '../utils/status-peek';
import { isMediaFirstInstance } from '../utils/store-utils';
import { groupBoosts, groupContext } from '../utils/timeline-utils'; import { groupBoosts, groupContext } from '../utils/timeline-utils';
import useInterval from '../utils/useInterval'; import useInterval from '../utils/useInterval';
import usePageVisibility from '../utils/usePageVisibility'; import usePageVisibility from '../utils/usePageVisibility';
@ -59,6 +66,8 @@ function Timeline({
console.debug('RENDER Timeline', id, refresh); console.debug('RENDER Timeline', id, refresh);
const mediaFirst = useMemo(() => isMediaFirstInstance(), []);
const allowGrouping = view !== 'media'; const allowGrouping = view !== 'media';
const loadItems = useDebouncedCallback( const loadItems = useDebouncedCallback(
(firstLoad) => { (firstLoad) => {
@ -355,7 +364,9 @@ function Timeline({
<FilterContext.Provider value={filterContext}> <FilterContext.Provider value={filterContext}>
<div <div
id={`${id}-page`} id={`${id}-page`}
class="deck-container" class={`deck-container ${
mediaFirst ? 'deck-container-media-first' : ''
}`}
ref={(node) => { ref={(node) => {
scrollableRef.current = node; scrollableRef.current = node;
jRef.current = node; jRef.current = node;
@ -432,6 +443,7 @@ function Timeline({
view={view} view={view}
showFollowedTags={showFollowedTags} showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent} showReplyParent={showReplyParent}
mediaFirst={mediaFirst}
/> />
))} ))}
{showMore && {showMore &&
@ -443,14 +455,14 @@ function Timeline({
height: '20vh', height: '20vh',
}} }}
> >
<Status skeleton /> <Status skeleton mediaFirst={mediaFirst} />
</li> </li>
<li <li
style={{ style={{
height: '25vh', height: '25vh',
}} }}
> >
<Status skeleton /> <Status skeleton mediaFirst={mediaFirst} />
</li> </li>
</> </>
))} ))}
@ -490,7 +502,7 @@ function Timeline({
/> />
) : ( ) : (
<li key={i}> <li key={i}>
<Status skeleton /> <Status skeleton mediaFirst={mediaFirst} />
</li> </li>
), ),
)} )}
@ -525,6 +537,7 @@ const TimelineItem = memo(
view, view,
showFollowedTags, showFollowedTags,
showReplyParent, showReplyParent,
mediaFirst,
}) => { }) => {
console.debug('RENDER TimelineItem', status.id); console.debug('RENDER TimelineItem', status.id);
const { id: statusID, reblog, items, type, _pinned } = status; const { id: statusID, reblog, items, type, _pinned } = status;
@ -533,6 +546,7 @@ const TimelineItem = memo(
const url = instance const url = instance
? `/${instance}/s/${actualStatusID}` ? `/${instance}/s/${actualStatusID}`
: `/s/${actualStatusID}`; : `/s/${actualStatusID}`;
if (items) { if (items) {
const fItems = filteredItems(items, filterContext); const fItems = filteredItems(items, filterContext);
let title = ''; let title = '';
@ -585,6 +599,7 @@ const TimelineItem = memo(
contentTextWeight contentTextWeight
enableCommentHint enableCommentHint
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
) : ( ) : (
<Status <Status
@ -594,6 +609,7 @@ const TimelineItem = memo(
contentTextWeight contentTextWeight
enableCommentHint enableCommentHint
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
)} )}
</Link> </Link>
@ -689,6 +705,7 @@ const TimelineItem = memo(
showFollowedTags={showFollowedTags} showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent} showReplyParent={showReplyParent}
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
) : ( ) : (
<Status <Status
@ -698,6 +715,7 @@ const TimelineItem = memo(
showFollowedTags={showFollowedTags} showFollowedTags={showFollowedTags}
showReplyParent={showReplyParent} showReplyParent={showReplyParent}
// allowFilters={allowFilters} // allowFilters={allowFilters}
mediaFirst={mediaFirst}
/> />
)} )}
</Link> </Link>

Wyświetl plik

@ -126,3 +126,8 @@ export function getCurrentInstanceConfiguration() {
const instance = getCurrentInstance(); const instance = getCurrentInstance();
return getInstanceConfiguration(instance); return getInstanceConfiguration(instance);
} }
export function isMediaFirstInstance() {
const instance = getCurrentInstance();
return /pixelfed/i.test(instance?.version);
}