Alrighty, this is media-view layout
This commit is contained in:
parent
35f7cae01f
commit
b40bbb32c2
11 changed files with 440 additions and 53 deletions
72
src/app.css
72
src/app.css
|
@ -209,6 +209,60 @@ a[href^='http'][rel*='nofollow']:visited:not(:has(div)) {
|
||||||
.timeline {
|
.timeline {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&.timeline-media {
|
||||||
|
--grid-gap: 8px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-auto-rows: fit-content;
|
||||||
|
gap: var(--grid-gap);
|
||||||
|
padding: var(--grid-gap);
|
||||||
|
|
||||||
|
&:not(#columns &) {
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
&:not(#columns &) {
|
||||||
|
--grid-gap: 16px;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
width: 95vw;
|
||||||
|
max-width: calc(320px * 3.3);
|
||||||
|
transform: translateX(calc(-50% + var(--main-width) / 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#columns & {
|
||||||
|
padding-inline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (grid-template-rows: masonry) {
|
||||||
|
grid-template-rows: masonry;
|
||||||
|
masonry-auto-flow: pack;
|
||||||
|
|
||||||
|
.media-post a {
|
||||||
|
aspect-ratio: revert !important;
|
||||||
|
|
||||||
|
video,
|
||||||
|
img,
|
||||||
|
audio {
|
||||||
|
min-height: 88px; /* for extreme dimensions */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.timeline.grow {
|
.timeline.grow {
|
||||||
/* min-height: 100vh;
|
/* min-height: 100vh;
|
||||||
|
@ -1678,6 +1732,24 @@ body > .szh-menu-container {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.szh-menu
|
||||||
|
.szh-menu__item--type-checkbox:not(.szh-menu__item--disabled):not(
|
||||||
|
.szh-menu__item--hover
|
||||||
|
) {
|
||||||
|
.icon {
|
||||||
|
opacity: 0.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.szh-menu__item--checked {
|
||||||
|
color: var(--link-color);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
opacity: 1;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.szh-menu .menu-wrap {
|
.szh-menu .menu-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
@ -25,6 +25,7 @@ body.cloak,
|
||||||
}
|
}
|
||||||
|
|
||||||
.status :is(img, video, audio),
|
.status :is(img, video, audio),
|
||||||
|
.media-post .media,
|
||||||
.avatar,
|
.avatar,
|
||||||
.emoji,
|
.emoji,
|
||||||
.header-banner {
|
.header-banner {
|
||||||
|
|
|
@ -102,6 +102,7 @@ export const ICONS = {
|
||||||
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
keyboard: () => import('@iconify-icons/mingcute/keyboard-line'),
|
||||||
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
||||||
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
||||||
|
media: () => import('@iconify-icons/mingcute/photo-album-line'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function Icon({
|
function Icon({
|
||||||
|
|
87
src/components/media-post.css
Normal file
87
src/components/media-post.css
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
.media-post {
|
||||||
|
--item-radius: 16px;
|
||||||
|
position: relative;
|
||||||
|
animation: appear-smooth 1s ease-out;
|
||||||
|
|
||||||
|
&:is(.filtered, .has-spoiler) :is(img, video) {
|
||||||
|
filter: blur(32px);
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filtered[data-filtered-text]:before,
|
||||||
|
&.has-spoiler[data-spoiler-text]:before {
|
||||||
|
pointer-events: none;
|
||||||
|
content: attr(data-spoiler-text);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--bg-blur-color);
|
||||||
|
margin: 8px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: calc(var(--item-radius) / 2);
|
||||||
|
font-size: 90%;
|
||||||
|
border: var(--hairline-width) dashed var(--bg-color);
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media {
|
||||||
|
border-radius: var(--item-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 1 !important;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
content: '';
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.media-audio) {
|
||||||
|
background-color: var(--average-color, var(--media-bg-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
--drop-shadow: var(--drop-shadow-color);
|
||||||
|
box-shadow: 0 8px 16px -4px var(--drop-shadow),
|
||||||
|
0 4px 8px var(--drop-shadow);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
--drop-shadow: var(--link-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:has(button:active)) {
|
||||||
|
box-shadow: none;
|
||||||
|
filter: brightness(0.8);
|
||||||
|
transform: scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
video,
|
||||||
|
img,
|
||||||
|
audio {
|
||||||
|
border-radius: 16px;
|
||||||
|
/* object-fit: scale-down; */
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:is(:hover, :focus) img {
|
||||||
|
/* Less delay here to make it feel more responsive */
|
||||||
|
animation: position-object 5s ease-in-out 0.3s 5;
|
||||||
|
animation-duration: var(--anim-duration, 5s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
126
src/components/media-post.jsx
Normal file
126
src/components/media-post.jsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import './media-post.css';
|
||||||
|
|
||||||
|
import { memo } from 'preact/compat';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
|
import states, { statusKey } from '../utils/states';
|
||||||
|
|
||||||
|
import Media from './media';
|
||||||
|
|
||||||
|
function MediaPost({
|
||||||
|
class: className,
|
||||||
|
statusID,
|
||||||
|
status,
|
||||||
|
instance,
|
||||||
|
parent,
|
||||||
|
allowFilters,
|
||||||
|
onMediaClick,
|
||||||
|
}) {
|
||||||
|
let sKey = statusKey(statusID, instance);
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
if (!status) {
|
||||||
|
status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
|
||||||
|
sKey = statusKey(status?.id, instance);
|
||||||
|
}
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
account: {
|
||||||
|
acct,
|
||||||
|
avatar,
|
||||||
|
avatarStatic,
|
||||||
|
id: accountId,
|
||||||
|
url: accountURL,
|
||||||
|
displayName,
|
||||||
|
username,
|
||||||
|
emojis: accountEmojis,
|
||||||
|
bot,
|
||||||
|
group,
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
repliesCount,
|
||||||
|
reblogged,
|
||||||
|
reblogsCount,
|
||||||
|
favourited,
|
||||||
|
favouritesCount,
|
||||||
|
bookmarked,
|
||||||
|
poll,
|
||||||
|
muted,
|
||||||
|
sensitive,
|
||||||
|
spoilerText,
|
||||||
|
visibility, // public, unlisted, private, direct
|
||||||
|
language,
|
||||||
|
editedAt,
|
||||||
|
filtered,
|
||||||
|
card,
|
||||||
|
createdAt,
|
||||||
|
inReplyToId,
|
||||||
|
inReplyToAccountId,
|
||||||
|
content,
|
||||||
|
mentions,
|
||||||
|
mediaAttachments,
|
||||||
|
reblog,
|
||||||
|
uri,
|
||||||
|
url,
|
||||||
|
emojis,
|
||||||
|
// Non-API props
|
||||||
|
_deleted,
|
||||||
|
_pinned,
|
||||||
|
_filtered,
|
||||||
|
} = status;
|
||||||
|
|
||||||
|
if (!mediaAttachments?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const debugHover = (e) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
console.log({
|
||||||
|
...status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug('RENDER Media post', id, status?.account.displayName);
|
||||||
|
|
||||||
|
// const readingExpandSpoilers = useMemo(() => {
|
||||||
|
// const prefs = store.account.get('preferences') || {};
|
||||||
|
// return !!prefs['reading:expand:spoilers'];
|
||||||
|
// }, []);
|
||||||
|
const hasSpoiler = spoilerText || sensitive;
|
||||||
|
|
||||||
|
const Parent = parent || 'div';
|
||||||
|
|
||||||
|
return mediaAttachments.map((media, i) => {
|
||||||
|
const mediaKey = `${sKey}-${media.id}`;
|
||||||
|
return (
|
||||||
|
<Parent
|
||||||
|
onMouseEnter={debugHover}
|
||||||
|
key={mediaKey}
|
||||||
|
data-spoiler-text={
|
||||||
|
spoilerText || (sensitive ? 'Sensitive media' : undefined)
|
||||||
|
}
|
||||||
|
data-filtered-text={_filtered ? 'Filtered' : undefined}
|
||||||
|
class={`
|
||||||
|
media-post
|
||||||
|
${allowFilters && _filtered ? 'filtered' : ''}
|
||||||
|
${hasSpoiler ? 'has-spoiler' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<Media
|
||||||
|
class={className}
|
||||||
|
media={media}
|
||||||
|
lang={language}
|
||||||
|
to={`/${instance}/s/${id}?media-only=${i + 1}`}
|
||||||
|
onClick={
|
||||||
|
onMediaClick ? (e) => onMediaClick(e, i, media, status) : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Parent>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(MediaPost);
|
|
@ -62,6 +62,7 @@ export const isMediaCaptionLong = mem((caption) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
function Media({
|
function Media({
|
||||||
|
class: className = '',
|
||||||
media,
|
media,
|
||||||
to,
|
to,
|
||||||
lang,
|
lang,
|
||||||
|
@ -170,6 +171,9 @@ function Media({
|
||||||
const maxAspectHeight =
|
const maxAspectHeight =
|
||||||
window.innerHeight * (orientation === 'portrait' ? 0.45 : 0.33);
|
window.innerHeight * (orientation === 'portrait' ? 0.45 : 0.33);
|
||||||
const maxHeight = orientation === 'portrait' ? 0 : 160;
|
const maxHeight = orientation === 'portrait' ? 0 : 160;
|
||||||
|
const averageColorStyle = {
|
||||||
|
'--average-color': rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||||
|
};
|
||||||
const mediaStyles =
|
const mediaStyles =
|
||||||
width && height
|
width && height
|
||||||
? {
|
? {
|
||||||
|
@ -180,8 +184,11 @@ function Media({
|
||||||
(width / height) * Math.max(maxHeight, maxAspectHeight)
|
(width / height) * Math.max(maxHeight, maxAspectHeight)
|
||||||
}px`,
|
}px`,
|
||||||
aspectRatio: `${width} / ${height}`,
|
aspectRatio: `${width} / ${height}`,
|
||||||
|
...averageColorStyle,
|
||||||
}
|
}
|
||||||
: {};
|
: {
|
||||||
|
...averageColorStyle,
|
||||||
|
};
|
||||||
|
|
||||||
const longDesc = isMediaCaptionLong(description);
|
const longDesc = isMediaCaptionLong(description);
|
||||||
const showInlineDesc =
|
const showInlineDesc =
|
||||||
|
@ -233,7 +240,7 @@ function Media({
|
||||||
<Figure>
|
<Figure>
|
||||||
<Parent
|
<Parent
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
class={`media media-image`}
|
class={`media media-image ${className}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
data-has-alt={!showInlineDesc}
|
data-has-alt={!showInlineDesc}
|
||||||
|
@ -244,6 +251,7 @@ function Media({
|
||||||
backgroundSize: imageSmallerThanParent
|
backgroundSize: imageSmallerThanParent
|
||||||
? `${width}px ${height}px`
|
? `${width}px ${height}px`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
...averageColorStyle,
|
||||||
}
|
}
|
||||||
: mediaStyles
|
: mediaStyles
|
||||||
}
|
}
|
||||||
|
@ -341,11 +349,13 @@ function Media({
|
||||||
return (
|
return (
|
||||||
<Figure>
|
<Figure>
|
||||||
<Parent
|
<Parent
|
||||||
class={`media media-${isGIF ? 'gif' : 'video'} ${
|
class={`media ${className} media-${isGIF ? 'gif' : 'video'} ${
|
||||||
autoGIFAnimate ? 'media-contain' : ''
|
autoGIFAnimate ? 'media-contain' : ''
|
||||||
}`}
|
}`}
|
||||||
data-orientation={orientation}
|
data-orientation={orientation}
|
||||||
data-formatted-duration={formattedDuration}
|
data-formatted-duration={
|
||||||
|
!showOriginal ? formattedDuration : undefined
|
||||||
|
}
|
||||||
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
|
data-label={isGIF && !showOriginal && !autoGIFAnimate ? 'GIF' : ''}
|
||||||
data-has-alt={!showInlineDesc}
|
data-has-alt={!showInlineDesc}
|
||||||
// style={{
|
// style={{
|
||||||
|
@ -448,8 +458,10 @@ function Media({
|
||||||
return (
|
return (
|
||||||
<Figure>
|
<Figure>
|
||||||
<Parent
|
<Parent
|
||||||
class="media media-audio"
|
class={`media media-audio ${className}`}
|
||||||
data-formatted-duration={formattedDuration}
|
data-formatted-duration={
|
||||||
|
!showOriginal ? formattedDuration : undefined
|
||||||
|
}
|
||||||
data-has-alt={!showInlineDesc}
|
data-has-alt={!showInlineDesc}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
style={!showOriginal && mediaStyles}
|
style={!showOriginal && mediaStyles}
|
||||||
|
|
|
@ -104,6 +104,11 @@ const TYPE_PARAMS = {
|
||||||
placeholder: 'e.g. PixelArt (Max 5, space-separated)',
|
placeholder: 'e.g. PixelArt (Max 5, space-separated)',
|
||||||
pattern: '[^#]+',
|
pattern: '[^#]+',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Media only',
|
||||||
|
name: 'media',
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Instance',
|
text: 'Instance',
|
||||||
name: 'instance',
|
name: 'instance',
|
||||||
|
@ -186,8 +191,10 @@ export const SHORTCUTS_META = {
|
||||||
id: 'hashtag',
|
id: 'hashtag',
|
||||||
title: ({ hashtag }) => hashtag,
|
title: ({ hashtag }) => hashtag,
|
||||||
subtitle: ({ instance }) => instance || api().instance,
|
subtitle: ({ instance }) => instance || api().instance,
|
||||||
path: ({ hashtag, instance }) =>
|
path: ({ hashtag, instance, media }) =>
|
||||||
`${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}`,
|
`${instance ? `/${instance}` : ''}/t/${hashtag.split(/\s+/).join('+')}${
|
||||||
|
media ? '?media=1' : ''
|
||||||
|
}`,
|
||||||
icon: 'hashtag',
|
icon: 'hashtag',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -846,7 +846,7 @@
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.status .media {
|
.media {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&[data-has-alt] {
|
&[data-has-alt] {
|
||||||
|
@ -885,7 +885,7 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
position: relative;
|
position: relative;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
}
|
}
|
||||||
.status :is(.media-video, .media-audio) .media-play {
|
:is(.media-video, .media-audio) .media-play {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
@ -902,10 +902,10 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
border-radius: 70px;
|
border-radius: 70px;
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
.status :is(.media-video, .media-audio):hover:not(:active) .media-play {
|
:is(.media-video, .media-audio):hover:not(:active) .media-play {
|
||||||
transform: translate(-50%, -50%) scale(1.1);
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
}
|
}
|
||||||
.status :is(.media-video, .media-audio)[data-formatted-duration]:after {
|
:is(.media-video, .media-audio)[data-formatted-duration]:after {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
content: attr(data-formatted-duration);
|
content: attr(data-formatted-duration);
|
||||||
|
@ -918,10 +918,10 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
.status .media-audio[data-formatted-duration]:after {
|
.media-audio[data-formatted-duration]:after {
|
||||||
content: '♬ ' attr(data-formatted-duration);
|
content: '♬ ' attr(data-formatted-duration);
|
||||||
}
|
}
|
||||||
.status .media-gif[data-label]:not(:hover):after {
|
.media-gif[data-label]:not(:hover):after {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
@ -953,12 +953,12 @@ body:has(#modal-container .carousel) .status .media img:hover {
|
||||||
.status .media-audio audio {
|
.status .media-audio audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
} */
|
} */
|
||||||
.status .media-audio {
|
.media-audio {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle at center center,
|
circle at center center,
|
||||||
var(--bg-color),
|
transparent,
|
||||||
var(--bg-faded-color)
|
var(--bg-faded-color)
|
||||||
),
|
),
|
||||||
repeating-radial-gradient(
|
repeating-radial-gradient(
|
||||||
|
|
|
@ -13,6 +13,8 @@ import useScroll from '../utils/useScroll';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
|
import Media from './media';
|
||||||
|
import MediaPost from './media-post';
|
||||||
import NavMenu from './nav-menu';
|
import NavMenu from './nav-menu';
|
||||||
import Status from './status';
|
import Status from './status';
|
||||||
|
|
||||||
|
@ -39,6 +41,7 @@ function Timeline({
|
||||||
timelineStart,
|
timelineStart,
|
||||||
allowFilters,
|
allowFilters,
|
||||||
refresh,
|
refresh,
|
||||||
|
view,
|
||||||
}) {
|
}) {
|
||||||
const snapStates = useSnapshot(states);
|
const snapStates = useSnapshot(states);
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
|
@ -50,6 +53,7 @@ function Timeline({
|
||||||
|
|
||||||
console.debug('RENDER Timeline', id, refresh);
|
console.debug('RENDER Timeline', id, refresh);
|
||||||
|
|
||||||
|
const allowGrouping = view !== 'media';
|
||||||
const loadItems = useDebouncedCallback(
|
const loadItems = useDebouncedCallback(
|
||||||
(firstLoad) => {
|
(firstLoad) => {
|
||||||
setShowNew(false);
|
setShowNew(false);
|
||||||
|
@ -59,10 +63,12 @@ function Timeline({
|
||||||
try {
|
try {
|
||||||
let { done, value } = await fetchItems(firstLoad);
|
let { done, value } = await fetchItems(firstLoad);
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
if (allowGrouping) {
|
||||||
if (boostsCarousel) {
|
if (boostsCarousel) {
|
||||||
value = groupBoosts(value);
|
value = groupBoosts(value);
|
||||||
}
|
}
|
||||||
value = groupContext(value);
|
value = groupContext(value);
|
||||||
|
}
|
||||||
console.log(value);
|
console.log(value);
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
setItems(value);
|
setItems(value);
|
||||||
|
@ -210,6 +216,14 @@ function Timeline({
|
||||||
}
|
}
|
||||||
}, [nearReachEnd, showMore]);
|
}, [nearReachEnd, showMore]);
|
||||||
|
|
||||||
|
const prevView = useRef(view);
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevView.current !== view) {
|
||||||
|
prevView.current = view;
|
||||||
|
setItems([]);
|
||||||
|
}
|
||||||
|
}, [view]);
|
||||||
|
|
||||||
const loadOrCheckUpdates = useCallback(
|
const loadOrCheckUpdates = useCallback(
|
||||||
async ({ disableIdleCheck = false } = {}) => {
|
async ({ disableIdleCheck = false } = {}) => {
|
||||||
console.log('✨ Load or check updates', {
|
console.log('✨ Load or check updates', {
|
||||||
|
@ -346,7 +360,7 @@ function Timeline({
|
||||||
)}
|
)}
|
||||||
{!!items.length ? (
|
{!!items.length ? (
|
||||||
<>
|
<>
|
||||||
<ul class="timeline">
|
<ul class={`timeline ${view ? `timeline-${view}` : ''}`}>
|
||||||
{items.map((status) => (
|
{items.map((status) => (
|
||||||
<TimelineItem
|
<TimelineItem
|
||||||
status={status}
|
status={status}
|
||||||
|
@ -354,9 +368,12 @@ function Timeline({
|
||||||
useItemID={useItemID}
|
useItemID={useItemID}
|
||||||
allowFilters={allowFilters}
|
allowFilters={allowFilters}
|
||||||
key={status.id + status?._pinned}
|
key={status.id + status?._pinned}
|
||||||
|
view={view}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{showMore && uiState === 'loading' && (
|
{showMore &&
|
||||||
|
uiState === 'loading' &&
|
||||||
|
(view === 'media' ? null : (
|
||||||
<>
|
<>
|
||||||
<li
|
<li
|
||||||
style={{
|
style={{
|
||||||
|
@ -373,7 +390,7 @@ function Timeline({
|
||||||
<Status skeleton />
|
<Status skeleton />
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{uiState === 'default' &&
|
{uiState === 'default' &&
|
||||||
(showMore ? (
|
(showMore ? (
|
||||||
|
@ -399,11 +416,19 @@ function Timeline({
|
||||||
</>
|
</>
|
||||||
) : uiState === 'loading' ? (
|
) : uiState === 'loading' ? (
|
||||||
<ul class="timeline">
|
<ul class="timeline">
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) =>
|
||||||
|
view === 'media' ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: '50vh',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<li key={i}>
|
<li key={i}>
|
||||||
<Status skeleton />
|
<Status skeleton />
|
||||||
</li>
|
</li>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
uiState !== 'error' && <p class="ui-state">{emptyText}</p>
|
uiState !== 'error' && <p class="ui-state">{emptyText}</p>
|
||||||
|
@ -426,7 +451,7 @@ function Timeline({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineItem({ status, instance, useItemID, allowFilters }) {
|
function TimelineItem({ status, instance, useItemID, allowFilters, view }) {
|
||||||
const { id: statusID, reblog, items, type, _pinned } = status;
|
const { id: statusID, reblog, items, type, _pinned } = status;
|
||||||
const actualStatusID = reblog?.id || statusID;
|
const actualStatusID = reblog?.id || statusID;
|
||||||
const url = instance
|
const url = instance
|
||||||
|
@ -531,8 +556,33 @@ function TimelineItem({ status, instance, useItemID, allowFilters }) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const itemKey = `timeline-${statusID + _pinned}`;
|
||||||
|
|
||||||
|
if (view === 'media') {
|
||||||
|
return useItemID ? (
|
||||||
|
<MediaPost
|
||||||
|
class="timeline-item"
|
||||||
|
parent="li"
|
||||||
|
key={itemKey}
|
||||||
|
statusID={statusID}
|
||||||
|
instance={instance}
|
||||||
|
allowFilters={allowFilters}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<MediaPost
|
||||||
|
class="timeline-item"
|
||||||
|
parent="li"
|
||||||
|
key={itemKey}
|
||||||
|
status={status}
|
||||||
|
instance={instance}
|
||||||
|
allowFilters={allowFilters}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={`timeline-${statusID + _pinned}`}>
|
<li key={itemKey}>
|
||||||
<Link class="status-link timeline-item" to={url}>
|
<Link class="status-link timeline-item" to={url}>
|
||||||
{useItemID ? (
|
{useItemID ? (
|
||||||
<Status
|
<Status
|
||||||
|
|
|
@ -414,6 +414,7 @@ function AccountStatuses() {
|
||||||
errorText="Unable to load posts"
|
errorText="Unable to load posts"
|
||||||
fetchItems={fetchAccountStatuses}
|
fetchItems={fetchAccountStatuses}
|
||||||
useItemID
|
useItemID
|
||||||
|
view={media ? 'media' : undefined}
|
||||||
boostsCarousel={snapStates.settings.boostsCarousel}
|
boostsCarousel={snapStates.settings.boostsCarousel}
|
||||||
timelineStart={TimelineStart}
|
timelineStart={TimelineStart}
|
||||||
refresh={[
|
refresh={[
|
||||||
|
|
|
@ -2,10 +2,11 @@ import {
|
||||||
FocusableItem,
|
FocusableItem,
|
||||||
MenuDivider,
|
MenuDivider,
|
||||||
MenuGroup,
|
MenuGroup,
|
||||||
|
MenuHeader,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import Icon from '../components/icon';
|
import Icon from '../components/icon';
|
||||||
import Menu2 from '../components/menu2';
|
import Menu2 from '../components/menu2';
|
||||||
|
@ -25,13 +26,16 @@ const LIMIT = 20;
|
||||||
const TAGS_LIMIT_PER_MODE = 4;
|
const TAGS_LIMIT_PER_MODE = 4;
|
||||||
const TOTAL_TAGS_LIMIT = TAGS_LIMIT_PER_MODE + 1;
|
const TOTAL_TAGS_LIMIT = TAGS_LIMIT_PER_MODE + 1;
|
||||||
|
|
||||||
function Hashtags({ columnMode, ...props }) {
|
function Hashtags({ media: mediaView, columnMode, ...props }) {
|
||||||
// const navigate = useNavigate();
|
// const navigate = useNavigate();
|
||||||
let { hashtag, ...params } = columnMode ? {} : useParams();
|
let { hashtag, ...params } = columnMode ? {} : useParams();
|
||||||
if (props.hashtag) hashtag = props.hashtag;
|
if (props.hashtag) hashtag = props.hashtag;
|
||||||
let hashtags = hashtag.trim().split(/[\s+]+/);
|
let hashtags = hashtag.trim().split(/[\s+]+/);
|
||||||
hashtags.sort();
|
hashtags.sort();
|
||||||
hashtag = hashtags[0];
|
hashtag = hashtags[0];
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const media = mediaView || !!searchParams.get('media');
|
||||||
|
const linkParams = media ? '?media=1' : '';
|
||||||
|
|
||||||
const { masto, instance, authenticated } = api({
|
const { masto, instance, authenticated } = api({
|
||||||
instance: props?.instance || params.instance,
|
instance: props?.instance || params.instance,
|
||||||
|
@ -60,6 +64,7 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
any: hashtags.slice(1),
|
any: hashtags.slice(1),
|
||||||
maxId: firstLoad ? undefined : maxID.current,
|
maxId: firstLoad ? undefined : maxID.current,
|
||||||
|
onlyMedia: media,
|
||||||
})
|
})
|
||||||
.next();
|
.next();
|
||||||
const { value } = results;
|
const { value } = results;
|
||||||
|
@ -69,7 +74,9 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
value.forEach((item) => {
|
value.forEach((item) => {
|
||||||
saveStatus(item, instance);
|
saveStatus(item, instance, {
|
||||||
|
skipThreading: media, // If media view, no need to form threads
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
maxID.current = value[value.length - 1].id;
|
maxID.current = value[value.length - 1].id;
|
||||||
|
@ -136,6 +143,8 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
fetchItems={fetchHashtags}
|
fetchItems={fetchHashtags}
|
||||||
checkForUpdates={checkForUpdates}
|
checkForUpdates={checkForUpdates}
|
||||||
useItemID
|
useItemID
|
||||||
|
view={media ? 'media' : undefined}
|
||||||
|
refresh={media}
|
||||||
headerEnd={
|
headerEnd={
|
||||||
<Menu2
|
<Menu2
|
||||||
portal
|
portal
|
||||||
|
@ -209,6 +218,23 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<MenuHeader className="plain">Filters</MenuHeader>
|
||||||
|
<MenuItem
|
||||||
|
type="checkbox"
|
||||||
|
checked={!!media}
|
||||||
|
onClick={() => {
|
||||||
|
if (media) {
|
||||||
|
searchParams.delete('media');
|
||||||
|
} else {
|
||||||
|
searchParams.set('media', '1');
|
||||||
|
}
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="check-circle" />{' '}
|
||||||
|
<span class="menu-grow">Media only</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
<FocusableItem className="menu-field" disabled={reachLimit}>
|
<FocusableItem className="menu-field" disabled={reachLimit}>
|
||||||
{({ ref }) => (
|
{({ ref }) => (
|
||||||
<form
|
<form
|
||||||
|
@ -231,7 +257,7 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
// );
|
// );
|
||||||
location.hash = instance
|
location.hash = instance
|
||||||
? `/${instance}/t/${hashtags.join('+')}`
|
? `/${instance}/t/${hashtags.join('+')}`
|
||||||
: `/t/${hashtags.join('+')}`;
|
: `/t/${hashtags.join('+')}${linkParams}`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -267,8 +293,8 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
// : `/t/${hashtags.join('+')}`,
|
// : `/t/${hashtags.join('+')}`,
|
||||||
// );
|
// );
|
||||||
location.hash = instance
|
location.hash = instance
|
||||||
? `/${instance}/t/${hashtags.join('+')}`
|
? `/${instance}/t/${hashtags.join('+')}${linkParams}`
|
||||||
: `/t/${hashtags.join('+')}`;
|
: `/t/${hashtags.join('+')}${linkParams}`;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
<Icon icon="x" alt="Remove hashtag" class="danger-icon" />
|
||||||
|
@ -287,6 +313,7 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
type: 'hashtag',
|
type: 'hashtag',
|
||||||
hashtag: hashtags.join(' '),
|
hashtag: hashtags.join(' '),
|
||||||
instance,
|
instance,
|
||||||
|
media: media ? 'on' : undefined,
|
||||||
};
|
};
|
||||||
// Check if already exists
|
// Check if already exists
|
||||||
const exists = states.shortcuts.some(
|
const exists = states.shortcuts.some(
|
||||||
|
@ -300,7 +327,8 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
.split(/[\s+]+/)
|
.split(/[\s+]+/)
|
||||||
.sort()
|
.sort()
|
||||||
.join(' ') &&
|
.join(' ') &&
|
||||||
(s.instance ? s.instance === shortcut.instance : true),
|
(s.instance ? s.instance === shortcut.instance : true) &&
|
||||||
|
(s.media ? !!s.media === !!shortcut.media : true),
|
||||||
);
|
);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
alert('This shortcut already exists');
|
alert('This shortcut already exists');
|
||||||
|
@ -324,7 +352,9 @@ function Hashtags({ columnMode, ...props }) {
|
||||||
if (newInstance) {
|
if (newInstance) {
|
||||||
newInstance = newInstance.toLowerCase().trim();
|
newInstance = newInstance.toLowerCase().trim();
|
||||||
// navigate(`/${newInstance}/t/${hashtags.join('+')}`);
|
// navigate(`/${newInstance}/t/${hashtags.join('+')}`);
|
||||||
location.hash = `/${newInstance}/t/${hashtags.join('+')}`;
|
location.hash = `/${newInstance}/t/${hashtags.join(
|
||||||
|
'+',
|
||||||
|
)}${linkParams}`;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
Loading…
Add table
Reference in a new issue