2022-12-10 12:14:48 +03:00
|
|
|
|
import './status.css';
|
|
|
|
|
|
2023-03-18 15:05:12 +03:00
|
|
|
|
import '@justinribeiro/lite-youtube';
|
2023-03-02 10:15:49 +03:00
|
|
|
|
import {
|
|
|
|
|
ControlledMenu,
|
|
|
|
|
Menu,
|
|
|
|
|
MenuDivider,
|
|
|
|
|
MenuHeader,
|
|
|
|
|
MenuItem,
|
|
|
|
|
} from '@szhsin/react-menu';
|
2023-12-26 12:06:52 +03:00
|
|
|
|
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
2024-01-06 07:31:25 +03:00
|
|
|
|
import { shallowEqual } from 'fast-equals';
|
2024-03-02 13:55:05 +03:00
|
|
|
|
import prettify from 'html-prettify';
|
2024-04-15 12:06:44 +03:00
|
|
|
|
import { Fragment } from 'preact';
|
2023-01-07 15:26:23 +03:00
|
|
|
|
import { memo } from 'preact/compat';
|
2023-06-14 06:14:49 +03:00
|
|
|
|
import {
|
|
|
|
|
useCallback,
|
2023-11-03 16:45:31 +03:00
|
|
|
|
useContext,
|
2023-06-14 06:14:49 +03:00
|
|
|
|
useEffect,
|
|
|
|
|
useMemo,
|
|
|
|
|
useRef,
|
|
|
|
|
useState,
|
|
|
|
|
} from 'preact/hooks';
|
2024-04-02 04:03:13 +03:00
|
|
|
|
import punycode from 'punycode';
|
2024-04-15 19:09:53 +03:00
|
|
|
|
import { useHotkeys } from 'react-hotkeys-hook';
|
2024-05-28 12:59:17 +03:00
|
|
|
|
import { detectAll } from 'tinyld/light';
|
2023-03-07 19:01:51 +03:00
|
|
|
|
import { useLongPress } from 'use-long-press';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import { useSnapshot } from 'valtio';
|
|
|
|
|
|
2024-03-25 12:58:56 +03:00
|
|
|
|
import CustomEmoji from '../components/custom-emoji';
|
2023-06-14 12:37:41 +03:00
|
|
|
|
import EmojiText from '../components/emoji-text';
|
2024-03-26 11:35:02 +03:00
|
|
|
|
import LazyShazam from '../components/lazy-shazam';
|
2022-12-11 16:22:22 +03:00
|
|
|
|
import Loader from '../components/loader';
|
2023-11-18 16:11:07 +03:00
|
|
|
|
import Menu2 from '../components/menu2';
|
2023-07-17 16:01:00 +03:00
|
|
|
|
import MenuConfirm from '../components/menu-confirm';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import Modal from '../components/modal';
|
|
|
|
|
import NameText from '../components/name-text';
|
2023-04-22 19:55:47 +03:00
|
|
|
|
import Poll from '../components/poll';
|
2023-02-05 19:17:19 +03:00
|
|
|
|
import { api } from '../utils/api';
|
2023-09-26 11:23:41 +03:00
|
|
|
|
import emojifyText from '../utils/emojify-text';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import enhanceContent from '../utils/enhance-content';
|
2023-11-03 16:45:31 +03:00
|
|
|
|
import FilterContext from '../utils/filter-context';
|
|
|
|
|
import { isFiltered } from '../utils/filters';
|
2023-03-07 17:38:06 +03:00
|
|
|
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
2023-03-28 20:12:59 +03:00
|
|
|
|
import getHTMLText from '../utils/getHTMLText';
|
2023-01-31 14:31:25 +03:00
|
|
|
|
import handleContentLinks from '../utils/handle-content-links';
|
2022-12-14 19:41:48 +03:00
|
|
|
|
import htmlContentLength from '../utils/html-content-length';
|
2023-04-22 19:55:47 +03:00
|
|
|
|
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
2023-05-20 09:14:35 +03:00
|
|
|
|
import localeMatch from '../utils/locale-match';
|
2024-05-28 12:59:17 +03:00
|
|
|
|
import mem from '../utils/mem';
|
2023-03-01 15:07:22 +03:00
|
|
|
|
import niceDateTime from '../utils/nice-date-time';
|
2023-11-05 12:41:29 +03:00
|
|
|
|
import openCompose from '../utils/open-compose';
|
2023-10-14 15:33:40 +03:00
|
|
|
|
import pmem from '../utils/pmem';
|
2023-06-13 12:46:37 +03:00
|
|
|
|
import safeBoundingBoxPadding from '../utils/safe-bounding-box-padding';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import shortenNumber from '../utils/shorten-number';
|
2024-05-24 07:30:20 +03:00
|
|
|
|
import showCompose from '../utils/show-compose';
|
2023-02-26 19:55:04 +03:00
|
|
|
|
import showToast from '../utils/show-toast';
|
2023-12-24 16:06:26 +03:00
|
|
|
|
import { speak, supportsTTS } from '../utils/speech';
|
2023-03-17 12:14:54 +03:00
|
|
|
|
import states, { getStatus, saveStatus, statusKey } from '../utils/states';
|
2023-03-21 19:09:36 +03:00
|
|
|
|
import statusPeek from '../utils/status-peek';
|
2022-12-12 16:54:31 +03:00
|
|
|
|
import store from '../utils/store';
|
2024-04-12 19:06:34 +03:00
|
|
|
|
import { getCurrentAccountID } from '../utils/store-utils';
|
2024-04-14 12:20:18 +03:00
|
|
|
|
import supports from '../utils/supports';
|
2023-12-30 13:13:56 +03:00
|
|
|
|
import unfurlMastodonLink from '../utils/unfurl-link';
|
2023-09-19 11:27:22 +03:00
|
|
|
|
import useTruncated from '../utils/useTruncated';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import visibilityIconsMap from '../utils/visibility-icons-map';
|
|
|
|
|
|
|
|
|
|
import Avatar from './avatar';
|
|
|
|
|
import Icon from './icon';
|
2023-01-20 19:23:59 +03:00
|
|
|
|
import Link from './link';
|
2023-01-29 10:23:53 +03:00
|
|
|
|
import Media from './media';
|
2023-10-02 13:58:42 +03:00
|
|
|
|
import { isMediaCaptionLong } from './media';
|
2023-03-28 10:59:20 +03:00
|
|
|
|
import MenuLink from './menu-link';
|
2023-01-05 05:50:27 +03:00
|
|
|
|
import RelativeTime from './relative-time';
|
2023-03-07 17:38:06 +03:00
|
|
|
|
import TranslationBlock from './translation-block';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2023-11-30 18:47:58 +03:00
|
|
|
|
const SHOW_COMMENT_COUNT_LIMIT = 280;
|
2023-07-21 17:52:53 +03:00
|
|
|
|
const INLINE_TRANSLATE_LIMIT = 140;
|
2023-02-23 11:45:53 +03:00
|
|
|
|
|
2023-02-05 19:17:19 +03:00
|
|
|
|
function fetchAccount(id, masto) {
|
2023-10-14 15:33:40 +03:00
|
|
|
|
return masto.v1.accounts.$select(id).fetch();
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
2023-10-14 15:33:40 +03:00
|
|
|
|
const memFetchAccount = pmem(fetchAccount);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2023-02-26 19:55:04 +03:00
|
|
|
|
const visibilityText = {
|
|
|
|
|
public: 'Public',
|
|
|
|
|
unlisted: 'Unlisted',
|
|
|
|
|
private: 'Followers only',
|
2023-04-06 13:21:56 +03:00
|
|
|
|
direct: 'Private mention',
|
2023-02-26 19:55:04 +03:00
|
|
|
|
};
|
|
|
|
|
|
2023-11-04 14:05:14 +03:00
|
|
|
|
const isIOS =
|
|
|
|
|
window.ontouchstart !== undefined &&
|
|
|
|
|
/iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
|
|
|
|
2024-03-15 11:02:33 +03:00
|
|
|
|
const rtf = new Intl.RelativeTimeFormat();
|
|
|
|
|
|
2023-12-20 08:55:56 +03:00
|
|
|
|
const REACTIONS_LIMIT = 80;
|
|
|
|
|
|
2023-12-21 13:17:14 +03:00
|
|
|
|
function getPollText(poll) {
|
|
|
|
|
if (!poll?.options?.length) return '';
|
|
|
|
|
return `📊:\n${poll.options
|
|
|
|
|
.map(
|
|
|
|
|
(option) =>
|
|
|
|
|
`- ${option.title}${
|
|
|
|
|
option.votesCount >= 0 ? ` (${option.votesCount})` : ''
|
|
|
|
|
}`,
|
|
|
|
|
)
|
|
|
|
|
.join('\n')}`;
|
|
|
|
|
}
|
|
|
|
|
function getPostText(status) {
|
|
|
|
|
const { spoilerText, content, poll } = status;
|
|
|
|
|
return (
|
|
|
|
|
(spoilerText ? `${spoilerText}\n\n` : '') +
|
|
|
|
|
getHTMLText(content) +
|
|
|
|
|
getPollText(poll)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-06 12:30:10 +03:00
|
|
|
|
const PostContent = memo(
|
|
|
|
|
({ post, instance, previewMode }) => {
|
|
|
|
|
const { content, emojis, language, mentions, url } = post;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
lang={language}
|
|
|
|
|
dir="auto"
|
|
|
|
|
class="inner-content"
|
|
|
|
|
onClick={handleContentLinks({
|
|
|
|
|
mentions,
|
|
|
|
|
instance,
|
|
|
|
|
previewMode,
|
|
|
|
|
statusURL: url,
|
|
|
|
|
})}
|
|
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
|
__html: enhanceContent(content, {
|
|
|
|
|
emojis,
|
|
|
|
|
postEnhanceDOM: (dom) => {
|
|
|
|
|
// Remove target="_blank" from links
|
|
|
|
|
dom.querySelectorAll('a.u-url[target="_blank"]').forEach((a) => {
|
|
|
|
|
if (!/http/i.test(a.innerText.trim())) {
|
|
|
|
|
a.removeAttribute('target');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
(oldProps, newProps) => {
|
2024-02-16 12:36:46 +03:00
|
|
|
|
const { post: oldPost } = oldProps;
|
|
|
|
|
const { post: newPost } = newProps;
|
|
|
|
|
return oldPost.content === newPost.content;
|
2024-02-06 12:30:10 +03:00
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2024-04-14 12:20:18 +03:00
|
|
|
|
const SIZE_CLASS = {
|
|
|
|
|
s: 'small',
|
|
|
|
|
m: 'medium',
|
|
|
|
|
l: 'large',
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-28 12:59:17 +03:00
|
|
|
|
const detectLang = mem((text) => {
|
2024-05-29 10:26:58 +03:00
|
|
|
|
text = text?.trim();
|
|
|
|
|
|
2024-05-29 05:23:46 +03:00
|
|
|
|
// Ref: https://github.com/komodojp/tinyld/blob/develop/docs/benchmark.md
|
|
|
|
|
// 500 should be enough for now, also the default max chars for Mastodon
|
|
|
|
|
if (text?.length > 500) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-05-28 12:59:17 +03:00
|
|
|
|
const langs = detectAll(text);
|
|
|
|
|
const lang = langs[0];
|
|
|
|
|
if (lang?.lang && lang?.accuracy > 0.5) {
|
|
|
|
|
// If > 50% accurate, use it
|
|
|
|
|
// It can be accurate if < 50% but better be safe
|
|
|
|
|
// Though > 50% also can be inaccurate 🤷♂️
|
|
|
|
|
return lang.lang;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
function Status({
|
|
|
|
|
statusID,
|
|
|
|
|
status,
|
2023-02-06 11:35:03 +03:00
|
|
|
|
instance: propInstance,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
size = 'm',
|
2023-02-18 20:10:06 +03:00
|
|
|
|
contentTextWeight,
|
2023-12-14 20:58:29 +03:00
|
|
|
|
readOnly,
|
|
|
|
|
enableCommentHint,
|
|
|
|
|
withinContext,
|
|
|
|
|
skeleton,
|
2023-03-07 17:38:06 +03:00
|
|
|
|
enableTranslate,
|
2023-05-03 05:22:15 +03:00
|
|
|
|
forceTranslate: _forceTranslate,
|
2023-03-16 08:02:46 +03:00
|
|
|
|
previewMode,
|
2023-11-03 16:45:31 +03:00
|
|
|
|
// allowFilters,
|
2023-04-14 10:30:04 +03:00
|
|
|
|
onMediaClick,
|
2023-04-22 19:55:47 +03:00
|
|
|
|
quoted,
|
2023-07-31 19:15:07 +03:00
|
|
|
|
onStatusLinkClick = () => {},
|
2023-12-14 20:58:29 +03:00
|
|
|
|
showFollowedTags,
|
2024-01-11 05:44:24 +03:00
|
|
|
|
allowContextMenu,
|
2024-01-13 19:32:08 +03:00
|
|
|
|
showActionsBar,
|
2024-01-30 09:34:54 +03:00
|
|
|
|
showReplyParent,
|
2024-04-11 12:18:17 +03:00
|
|
|
|
mediaFirst,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}) {
|
|
|
|
|
if (skeleton) {
|
2022-12-10 12:14:48 +03:00
|
|
|
|
return (
|
2024-04-14 12:20:18 +03:00
|
|
|
|
<div
|
|
|
|
|
class={`status skeleton ${
|
|
|
|
|
mediaFirst ? 'status-media-first small' : ''
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{!mediaFirst && <Avatar size="xxl" />}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="container">
|
2024-04-11 12:18:17 +03:00
|
|
|
|
<div class="meta">
|
|
|
|
|
{(size === 's' || mediaFirst) && <Avatar size="m" />} ███ ████████
|
|
|
|
|
</div>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="content-container">
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{mediaFirst && <div class="media-first-container" />}
|
|
|
|
|
<div class={`content ${mediaFirst ? 'media-first-content' : ''}`}>
|
2023-02-11 16:09:36 +03:00
|
|
|
|
<p>████ ████████</p>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-02-19 09:49:53 +03:00
|
|
|
|
const { masto, instance, authenticated } = api({ instance: propInstance });
|
2023-02-17 12:40:39 +03:00
|
|
|
|
const { instance: currentInstance } = api();
|
|
|
|
|
const sameInstance = instance === currentInstance;
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2023-12-29 03:25:41 +03:00
|
|
|
|
let sKey = statusKey(statusID || status?.id, instance);
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const snapStates = useSnapshot(states);
|
|
|
|
|
if (!status) {
|
2023-02-11 13:55:21 +03:00
|
|
|
|
status = snapStates.statuses[sKey] || snapStates.statuses[statusID];
|
2023-05-19 20:06:16 +03:00
|
|
|
|
sKey = statusKey(status?.id, instance);
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
|
|
|
|
if (!status) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-10 12:14:48 +03:00
|
|
|
|
const {
|
2022-12-18 16:10:05 +03:00
|
|
|
|
account: {
|
|
|
|
|
acct,
|
|
|
|
|
avatar,
|
|
|
|
|
avatarStatic,
|
|
|
|
|
id: accountId,
|
2023-02-21 09:29:25 +03:00
|
|
|
|
url: accountURL,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
displayName,
|
|
|
|
|
username,
|
|
|
|
|
emojis: accountEmojis,
|
2023-04-10 19:26:43 +03:00
|
|
|
|
bot,
|
2023-08-07 16:26:43 +03:00
|
|
|
|
group,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
},
|
|
|
|
|
id,
|
|
|
|
|
repliesCount,
|
|
|
|
|
reblogged,
|
|
|
|
|
reblogsCount,
|
|
|
|
|
favourited,
|
|
|
|
|
favouritesCount,
|
|
|
|
|
bookmarked,
|
|
|
|
|
poll,
|
|
|
|
|
muted,
|
|
|
|
|
sensitive,
|
|
|
|
|
spoilerText,
|
|
|
|
|
visibility, // public, unlisted, private, direct
|
2024-05-28 12:59:17 +03:00
|
|
|
|
language: _language,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
editedAt,
|
|
|
|
|
filtered,
|
|
|
|
|
card,
|
|
|
|
|
createdAt,
|
2023-01-04 12:27:43 +03:00
|
|
|
|
inReplyToId,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
inReplyToAccountId,
|
|
|
|
|
content,
|
|
|
|
|
mentions,
|
|
|
|
|
mediaAttachments,
|
|
|
|
|
reblog,
|
|
|
|
|
uri,
|
2023-02-21 09:29:25 +03:00
|
|
|
|
url,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
emojis,
|
2023-12-14 20:58:29 +03:00
|
|
|
|
tags,
|
2023-02-17 05:12:59 +03:00
|
|
|
|
// Non-API props
|
2022-12-22 17:43:04 +03:00
|
|
|
|
_deleted,
|
2023-02-17 05:12:59 +03:00
|
|
|
|
_pinned,
|
2023-11-03 16:45:31 +03:00
|
|
|
|
// _filtered,
|
2024-03-25 12:58:56 +03:00
|
|
|
|
// Non-Mastodon
|
|
|
|
|
emojiReactions,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
} = status;
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2024-05-29 10:26:58 +03:00
|
|
|
|
const [languageAutoDetected, setLanguageAutoDetected] = useState(null);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!content) return;
|
|
|
|
|
if (_language) return;
|
|
|
|
|
let timer;
|
|
|
|
|
timer = setTimeout(() => {
|
|
|
|
|
let detected = detectLang(
|
|
|
|
|
getHTMLText(content, {
|
|
|
|
|
preProcess: (dom) => {
|
|
|
|
|
// Remove anything that can skew the language detection
|
|
|
|
|
|
|
|
|
|
// Remove .mention, .hashtag, pre, code, a:has(.invisible)
|
|
|
|
|
dom
|
|
|
|
|
.querySelectorAll(
|
|
|
|
|
'.mention, .hashtag, pre, code, a:has(.invisible)',
|
|
|
|
|
)
|
|
|
|
|
.forEach((a) => {
|
|
|
|
|
a.remove();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Remove links that contains text that starts with https?://
|
|
|
|
|
dom.querySelectorAll('a').forEach((a) => {
|
|
|
|
|
const text = a.innerText.trim();
|
|
|
|
|
if (text.startsWith('https://') || text.startsWith('http://')) {
|
|
|
|
|
a.remove();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
setLanguageAutoDetected(detected);
|
|
|
|
|
}, 1000);
|
|
|
|
|
return () => clearTimeout(timer);
|
|
|
|
|
}, [content, _language]);
|
2024-05-28 12:59:17 +03:00
|
|
|
|
const language = _language || languageAutoDetected;
|
|
|
|
|
|
2024-04-11 12:18:17 +03:00
|
|
|
|
// if (!mediaAttachments?.length) mediaFirst = false;
|
|
|
|
|
const hasMediaAttachments = !!mediaAttachments?.length;
|
|
|
|
|
if (mediaFirst && hasMediaAttachments) size = 's';
|
|
|
|
|
|
2023-11-03 16:45:31 +03:00
|
|
|
|
const currentAccount = useMemo(() => {
|
2024-04-12 19:06:34 +03:00
|
|
|
|
return getCurrentAccountID();
|
2023-11-03 16:45:31 +03:00
|
|
|
|
}, []);
|
|
|
|
|
const isSelf = useMemo(() => {
|
|
|
|
|
return currentAccount && currentAccount === accountId;
|
|
|
|
|
}, [accountId, currentAccount]);
|
|
|
|
|
|
|
|
|
|
const filterContext = useContext(FilterContext);
|
|
|
|
|
const filterInfo =
|
|
|
|
|
!isSelf && !readOnly && !previewMode && isFiltered(filtered, filterContext);
|
|
|
|
|
|
|
|
|
|
if (filterInfo?.action === 'hide') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-25 08:03:26 +03:00
|
|
|
|
console.debug('RENDER Status', id, status?.account.displayName, quoted);
|
2023-01-07 15:26:23 +03:00
|
|
|
|
|
2023-03-23 16:48:29 +03:00
|
|
|
|
const debugHover = (e) => {
|
|
|
|
|
if (e.shiftKey) {
|
2023-07-31 15:30:29 +03:00
|
|
|
|
console.log({
|
|
|
|
|
...status,
|
|
|
|
|
});
|
2023-03-23 16:48:29 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-11-03 16:45:31 +03:00
|
|
|
|
if (/*allowFilters && */ size !== 'l' && filterInfo) {
|
2023-03-21 19:09:36 +03:00
|
|
|
|
return (
|
|
|
|
|
<FilteredStatus
|
|
|
|
|
status={status}
|
2023-11-03 16:45:31 +03:00
|
|
|
|
filterInfo={filterInfo}
|
2023-03-21 19:09:36 +03:00
|
|
|
|
instance={instance}
|
2023-03-23 16:48:29 +03:00
|
|
|
|
containerProps={{
|
|
|
|
|
onMouseEnter: debugHover,
|
|
|
|
|
}}
|
2023-12-14 20:58:29 +03:00
|
|
|
|
showFollowedTags
|
2024-05-16 08:00:23 +03:00
|
|
|
|
quoted={quoted}
|
2023-03-21 19:09:36 +03:00
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const createdAtDate = new Date(createdAt);
|
|
|
|
|
const editedAtDate = new Date(editedAt);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
let inReplyToAccountRef = mentions?.find(
|
|
|
|
|
(mention) => mention.id === inReplyToAccountId,
|
|
|
|
|
);
|
|
|
|
|
if (!inReplyToAccountRef && inReplyToAccountId === id) {
|
2023-02-21 09:29:25 +03:00
|
|
|
|
inReplyToAccountRef = { url: accountURL, username, displayName };
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
|
|
|
|
const [inReplyToAccount, setInReplyToAccount] = useState(inReplyToAccountRef);
|
|
|
|
|
if (!withinContext && !inReplyToAccount && inReplyToAccountId) {
|
2023-01-07 15:26:23 +03:00
|
|
|
|
const account = states.accounts[inReplyToAccountId];
|
2022-12-18 16:10:05 +03:00
|
|
|
|
if (account) {
|
|
|
|
|
setInReplyToAccount(account);
|
|
|
|
|
} else {
|
2023-02-05 19:17:19 +03:00
|
|
|
|
memFetchAccount(inReplyToAccountId, masto)
|
2022-12-18 16:10:05 +03:00
|
|
|
|
.then((account) => {
|
|
|
|
|
setInReplyToAccount(account);
|
2023-01-07 15:26:23 +03:00
|
|
|
|
states.accounts[account.id] = account;
|
2022-12-18 16:10:05 +03:00
|
|
|
|
})
|
|
|
|
|
.catch((e) => {});
|
|
|
|
|
}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
}
|
2023-04-09 19:30:32 +03:00
|
|
|
|
const mentionSelf =
|
|
|
|
|
inReplyToAccountId === currentAccount ||
|
|
|
|
|
mentions?.find((mention) => mention.id === currentAccount);
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2023-09-14 06:22:24 +03:00
|
|
|
|
const readingExpandSpoilers = useMemo(() => {
|
|
|
|
|
const prefs = store.account.get('preferences') || {};
|
|
|
|
|
return !!prefs['reading:expand:spoilers'];
|
|
|
|
|
}, []);
|
2023-12-24 16:07:46 +03:00
|
|
|
|
const readingExpandMedia = useMemo(() => {
|
|
|
|
|
// default | show_all | hide_all
|
|
|
|
|
// Ignore hide_all because it means hide *ALL* media including non-sensitive ones
|
|
|
|
|
const prefs = store.account.get('preferences') || {};
|
|
|
|
|
return prefs['reading:expand:media'] || 'default';
|
|
|
|
|
}, []);
|
|
|
|
|
// FOR TESTING:
|
|
|
|
|
// const readingExpandSpoilers = true;
|
|
|
|
|
// const readingExpandMedia = 'show_all';
|
2023-09-14 06:22:24 +03:00
|
|
|
|
const showSpoiler =
|
2023-12-24 16:07:46 +03:00
|
|
|
|
previewMode || readingExpandSpoilers || !!snapStates.spoilers[id];
|
|
|
|
|
const showSpoilerMedia =
|
|
|
|
|
previewMode ||
|
|
|
|
|
readingExpandMedia === 'show_all' ||
|
|
|
|
|
!!snapStates.spoilersMedia[id];
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
if (reblog) {
|
2023-03-21 19:09:36 +03:00
|
|
|
|
// If has statusID, means useItemID (cached in states)
|
2023-08-07 16:26:43 +03:00
|
|
|
|
|
|
|
|
|
if (group) {
|
|
|
|
|
return (
|
2023-11-05 03:21:43 +03:00
|
|
|
|
<div
|
|
|
|
|
data-state-post-id={sKey}
|
|
|
|
|
class="status-group"
|
|
|
|
|
onMouseEnter={debugHover}
|
|
|
|
|
>
|
2023-08-07 16:26:43 +03:00
|
|
|
|
<div class="status-pre-meta">
|
|
|
|
|
<Icon icon="group" size="l" alt="Group" />{' '}
|
|
|
|
|
<NameText account={status.account} instance={instance} showAvatar />
|
|
|
|
|
</div>
|
|
|
|
|
<Status
|
|
|
|
|
status={statusID ? null : reblog}
|
|
|
|
|
statusID={statusID ? reblog.id : null}
|
|
|
|
|
instance={instance}
|
|
|
|
|
size={size}
|
|
|
|
|
contentTextWeight={contentTextWeight}
|
2023-11-05 12:40:58 +03:00
|
|
|
|
readOnly={readOnly}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
mediaFirst={mediaFirst}
|
2023-08-07 16:26:43 +03:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
return (
|
2023-11-05 03:21:43 +03:00
|
|
|
|
<div
|
|
|
|
|
data-state-post-id={sKey}
|
|
|
|
|
class="status-reblog"
|
|
|
|
|
onMouseEnter={debugHover}
|
|
|
|
|
>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="status-pre-meta">
|
|
|
|
|
<Icon icon="rocket" size="l" />{' '}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
<NameText account={status.account} instance={instance} showAvatar />{' '}
|
2023-04-06 08:21:53 +03:00
|
|
|
|
<span>boosted</span>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</div>
|
2023-02-18 20:10:06 +03:00
|
|
|
|
<Status
|
2023-03-21 19:09:36 +03:00
|
|
|
|
status={statusID ? null : reblog}
|
|
|
|
|
statusID={statusID ? reblog.id : null}
|
2023-02-18 20:10:06 +03:00
|
|
|
|
instance={instance}
|
|
|
|
|
size={size}
|
|
|
|
|
contentTextWeight={contentTextWeight}
|
2023-11-05 12:40:58 +03:00
|
|
|
|
readOnly={readOnly}
|
2023-11-14 17:45:13 +03:00
|
|
|
|
enableCommentHint
|
2024-04-11 12:18:17 +03:00
|
|
|
|
mediaFirst={mediaFirst}
|
2023-02-18 20:10:06 +03:00
|
|
|
|
/>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2023-12-14 20:58:29 +03:00
|
|
|
|
// Check followedTags
|
2024-04-15 16:37:03 +03:00
|
|
|
|
const FollowedTagsParent = useCallback(
|
|
|
|
|
({ children }) => (
|
|
|
|
|
<div
|
|
|
|
|
data-state-post-id={sKey}
|
|
|
|
|
class="status-followed-tags"
|
|
|
|
|
onMouseEnter={debugHover}
|
|
|
|
|
>
|
|
|
|
|
<div class="status-pre-meta">
|
|
|
|
|
<Icon icon="hashtag" size="l" />{' '}
|
|
|
|
|
{snapStates.statusFollowedTags[sKey].slice(0, 3).map((tag) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={tag}
|
|
|
|
|
to={instance ? `/${instance}/t/${tag}` : `/t/${tag}`}
|
|
|
|
|
class="status-followed-tag-item"
|
|
|
|
|
>
|
|
|
|
|
{tag}
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{children}
|
2023-12-14 20:58:29 +03:00
|
|
|
|
</div>
|
2024-04-15 16:37:03 +03:00
|
|
|
|
),
|
|
|
|
|
[sKey, instance, snapStates.statusFollowedTags[sKey]],
|
2024-04-15 12:06:44 +03:00
|
|
|
|
);
|
|
|
|
|
const StatusParent =
|
|
|
|
|
showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length
|
|
|
|
|
? FollowedTagsParent
|
|
|
|
|
: Fragment;
|
2023-12-14 20:58:29 +03:00
|
|
|
|
|
2023-07-18 08:31:26 +03:00
|
|
|
|
const isSizeLarge = size === 'l';
|
|
|
|
|
|
2023-05-03 05:22:15 +03:00
|
|
|
|
const [forceTranslate, setForceTranslate] = useState(_forceTranslate);
|
2023-03-07 17:38:06 +03:00
|
|
|
|
const targetLanguage = getTranslateTargetLanguage(true);
|
2023-03-28 14:04:52 +03:00
|
|
|
|
const contentTranslationHideLanguages =
|
|
|
|
|
snapStates.settings.contentTranslationHideLanguages || [];
|
2023-07-22 15:31:13 +03:00
|
|
|
|
const { contentTranslation, contentTranslationAutoInline } =
|
|
|
|
|
snapStates.settings;
|
|
|
|
|
if (!contentTranslation) enableTranslate = false;
|
2023-07-18 08:31:26 +03:00
|
|
|
|
const inlineTranslate = useMemo(() => {
|
2023-07-22 15:31:13 +03:00
|
|
|
|
if (
|
|
|
|
|
!contentTranslation ||
|
|
|
|
|
!contentTranslationAutoInline ||
|
|
|
|
|
readOnly ||
|
|
|
|
|
(withinContext && !isSizeLarge) ||
|
|
|
|
|
previewMode ||
|
|
|
|
|
spoilerText ||
|
|
|
|
|
sensitive ||
|
|
|
|
|
poll ||
|
|
|
|
|
card ||
|
|
|
|
|
mediaAttachments?.length
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2023-07-21 18:54:03 +03:00
|
|
|
|
const contentLength = htmlContentLength(content);
|
2023-07-22 15:31:13 +03:00
|
|
|
|
return contentLength > 0 && contentLength <= INLINE_TRANSLATE_LIMIT;
|
2023-07-21 17:52:53 +03:00
|
|
|
|
}, [
|
2023-07-22 15:31:13 +03:00
|
|
|
|
contentTranslation,
|
|
|
|
|
contentTranslationAutoInline,
|
2023-07-21 17:52:53 +03:00
|
|
|
|
readOnly,
|
|
|
|
|
withinContext,
|
2023-07-21 18:54:03 +03:00
|
|
|
|
isSizeLarge,
|
|
|
|
|
previewMode,
|
2023-07-21 17:52:53 +03:00
|
|
|
|
spoilerText,
|
2023-07-22 15:31:13 +03:00
|
|
|
|
sensitive,
|
2023-07-21 17:52:53 +03:00
|
|
|
|
poll,
|
2023-07-22 05:10:41 +03:00
|
|
|
|
card,
|
2023-07-21 17:52:53 +03:00
|
|
|
|
mediaAttachments,
|
|
|
|
|
content,
|
|
|
|
|
]);
|
2023-03-07 17:38:06 +03:00
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const [showEdited, setShowEdited] = useState(false);
|
2024-03-02 13:55:05 +03:00
|
|
|
|
const [showEmbed, setShowEmbed] = useState(false);
|
2022-12-18 16:10:05 +03:00
|
|
|
|
|
2023-09-19 11:27:22 +03:00
|
|
|
|
const spoilerContentRef = useTruncated();
|
|
|
|
|
const contentRef = useTruncated();
|
2023-09-20 12:27:54 +03:00
|
|
|
|
const mediaContainerRef = useTruncated();
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const readMoreText = 'Read more →';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2022-12-30 15:37:57 +03:00
|
|
|
|
const statusRef = useRef(null);
|
|
|
|
|
|
2023-04-29 17:22:07 +03:00
|
|
|
|
const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this post from another instance.`;
|
2023-02-05 19:17:19 +03:00
|
|
|
|
|
2023-06-14 06:14:49 +03:00
|
|
|
|
const textWeight = useCallback(
|
|
|
|
|
() =>
|
|
|
|
|
Math.max(
|
|
|
|
|
Math.round((spoilerText.length + htmlContentLength(content)) / 140) ||
|
|
|
|
|
1,
|
|
|
|
|
1,
|
|
|
|
|
),
|
|
|
|
|
[spoilerText, content],
|
|
|
|
|
);
|
2023-02-21 18:59:34 +03:00
|
|
|
|
|
2023-03-01 15:07:22 +03:00
|
|
|
|
const createdDateText = niceDateTime(createdAtDate);
|
|
|
|
|
const editedDateText = editedAt && niceDateTime(editedAtDate);
|
2023-02-26 19:55:04 +03:00
|
|
|
|
|
2023-03-10 10:49:23 +03:00
|
|
|
|
// Can boost if:
|
|
|
|
|
// - authenticated AND
|
|
|
|
|
// - visibility != direct OR
|
|
|
|
|
// - visibility = private AND isSelf
|
|
|
|
|
let canBoost =
|
|
|
|
|
authenticated && visibility !== 'direct' && visibility !== 'private';
|
|
|
|
|
if (visibility === 'private' && isSelf) {
|
|
|
|
|
canBoost = true;
|
|
|
|
|
}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
|
2023-11-05 12:41:29 +03:00
|
|
|
|
const replyStatus = (e) => {
|
2023-02-26 19:55:04 +03:00
|
|
|
|
if (!sameInstance || !authenticated) {
|
|
|
|
|
return alert(unauthInteractionErrorMessage);
|
|
|
|
|
}
|
2023-11-05 12:41:29 +03:00
|
|
|
|
// syntheticEvent comes from MenuItem
|
|
|
|
|
if (e?.shiftKey || e?.syntheticEvent?.shiftKey) {
|
|
|
|
|
const newWin = openCompose({
|
|
|
|
|
replyToStatus: status,
|
|
|
|
|
});
|
|
|
|
|
if (newWin) return;
|
|
|
|
|
}
|
2024-05-24 07:30:20 +03:00
|
|
|
|
showCompose({
|
2023-02-26 19:55:04 +03:00
|
|
|
|
replyToStatus: status,
|
2024-05-24 07:30:20 +03:00
|
|
|
|
});
|
2023-02-26 19:55:04 +03:00
|
|
|
|
};
|
|
|
|
|
|
2023-07-17 16:01:00 +03:00
|
|
|
|
// Check if media has no descriptions
|
|
|
|
|
const mediaNoDesc = useMemo(() => {
|
|
|
|
|
return mediaAttachments.some(
|
|
|
|
|
(attachment) => !attachment.description?.trim?.(),
|
|
|
|
|
);
|
|
|
|
|
}, [mediaAttachments]);
|
2024-03-15 11:02:33 +03:00
|
|
|
|
|
|
|
|
|
const statusMonthsAgo = useMemo(() => {
|
|
|
|
|
return Math.floor(
|
|
|
|
|
(new Date() - createdAtDate) / (1000 * 60 * 60 * 24 * 30),
|
|
|
|
|
);
|
|
|
|
|
}, [createdAtDate]);
|
|
|
|
|
|
2023-02-26 19:55:04 +03:00
|
|
|
|
const boostStatus = async () => {
|
|
|
|
|
if (!sameInstance || !authenticated) {
|
2023-04-04 13:46:05 +03:00
|
|
|
|
alert(unauthInteractionErrorMessage);
|
|
|
|
|
return false;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
if (!reblogged) {
|
2023-03-14 15:42:37 +03:00
|
|
|
|
let confirmText = 'Boost this post?';
|
2023-07-17 16:01:00 +03:00
|
|
|
|
if (mediaNoDesc) {
|
2023-03-14 15:42:37 +03:00
|
|
|
|
confirmText += '\n\n⚠️ Some media have no descriptions.';
|
|
|
|
|
}
|
|
|
|
|
const yes = confirm(confirmText);
|
2023-02-26 19:55:04 +03:00
|
|
|
|
if (!yes) {
|
2023-04-04 13:46:05 +03:00
|
|
|
|
return false;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Optimistic
|
|
|
|
|
states.statuses[sKey] = {
|
|
|
|
|
...status,
|
|
|
|
|
reblogged: !reblogged,
|
|
|
|
|
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
|
|
|
|
};
|
|
|
|
|
if (reblogged) {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).unreblog();
|
2023-02-26 19:55:04 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
2023-04-04 13:46:05 +03:00
|
|
|
|
return true;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
} else {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).reblog();
|
2023-02-26 19:55:04 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
2023-04-04 13:46:05 +03:00
|
|
|
|
return true;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
// Revert optimistism
|
|
|
|
|
states.statuses[sKey] = status;
|
2023-04-04 13:46:05 +03:00
|
|
|
|
return false;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
2023-07-17 16:01:00 +03:00
|
|
|
|
const confirmBoostStatus = async () => {
|
|
|
|
|
if (!sameInstance || !authenticated) {
|
|
|
|
|
alert(unauthInteractionErrorMessage);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// Optimistic
|
|
|
|
|
states.statuses[sKey] = {
|
|
|
|
|
...status,
|
|
|
|
|
reblogged: !reblogged,
|
|
|
|
|
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
|
|
|
|
};
|
|
|
|
|
if (reblogged) {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).unreblog();
|
2023-07-17 16:01:00 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
} else {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).reblog();
|
2023-07-17 16:01:00 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
}
|
2024-02-26 06:58:22 +03:00
|
|
|
|
return true;
|
2023-07-17 16:01:00 +03:00
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
// Revert optimistism
|
|
|
|
|
states.statuses[sKey] = status;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-02-26 19:55:04 +03:00
|
|
|
|
|
|
|
|
|
const favouriteStatus = async () => {
|
|
|
|
|
if (!sameInstance || !authenticated) {
|
2024-02-26 06:58:22 +03:00
|
|
|
|
alert(unauthInteractionErrorMessage);
|
|
|
|
|
return false;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// Optimistic
|
|
|
|
|
states.statuses[sKey] = {
|
|
|
|
|
...status,
|
|
|
|
|
favourited: !favourited,
|
|
|
|
|
favouritesCount: favouritesCount + (favourited ? -1 : 1),
|
|
|
|
|
};
|
|
|
|
|
if (favourited) {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).unfavourite();
|
2023-02-26 19:55:04 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
} else {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).favourite();
|
2023-02-26 19:55:04 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
}
|
2024-02-26 06:58:22 +03:00
|
|
|
|
return true;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
// Revert optimistism
|
|
|
|
|
states.statuses[sKey] = status;
|
2024-02-26 06:58:22 +03:00
|
|
|
|
return false;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
2024-02-26 06:58:22 +03:00
|
|
|
|
const favouriteStatusNotify = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const done = await favouriteStatus();
|
|
|
|
|
if (!isSizeLarge && done) {
|
|
|
|
|
showToast(
|
|
|
|
|
favourited
|
|
|
|
|
? `Unliked @${username || acct}'s post`
|
|
|
|
|
: `Liked @${username || acct}'s post`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
};
|
2023-02-26 19:55:04 +03:00
|
|
|
|
|
|
|
|
|
const bookmarkStatus = async () => {
|
2024-04-14 12:20:18 +03:00
|
|
|
|
if (!supports('@mastodon/post-bookmark')) return;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
if (!sameInstance || !authenticated) {
|
2024-02-26 06:58:22 +03:00
|
|
|
|
alert(unauthInteractionErrorMessage);
|
|
|
|
|
return false;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// Optimistic
|
|
|
|
|
states.statuses[sKey] = {
|
|
|
|
|
...status,
|
|
|
|
|
bookmarked: !bookmarked,
|
|
|
|
|
};
|
|
|
|
|
if (bookmarked) {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).unbookmark();
|
2023-02-26 19:55:04 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
} else {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses.$select(id).bookmark();
|
2023-02-26 19:55:04 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
}
|
2024-02-26 06:58:22 +03:00
|
|
|
|
return true;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
// Revert optimistism
|
|
|
|
|
states.statuses[sKey] = status;
|
2024-02-26 06:58:22 +03:00
|
|
|
|
return false;
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
2024-02-26 06:58:22 +03:00
|
|
|
|
const bookmarkStatusNotify = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const done = await bookmarkStatus();
|
|
|
|
|
if (!isSizeLarge && done) {
|
|
|
|
|
showToast(
|
|
|
|
|
bookmarked
|
|
|
|
|
? `Unbookmarked @${username || acct}'s post`
|
|
|
|
|
: `Bookmarked @${username || acct}'s post`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
};
|
2023-02-26 19:55:04 +03:00
|
|
|
|
|
2023-05-03 05:22:15 +03:00
|
|
|
|
const differentLanguage =
|
2023-10-01 09:39:44 +03:00
|
|
|
|
!!language &&
|
2023-05-03 05:22:15 +03:00
|
|
|
|
language !== targetLanguage &&
|
2023-05-20 09:14:35 +03:00
|
|
|
|
!localeMatch([language], [targetLanguage]) &&
|
2023-05-03 05:22:15 +03:00
|
|
|
|
!contentTranslationHideLanguages.find(
|
2023-05-20 09:14:35 +03:00
|
|
|
|
(l) => language === l || localeMatch([language], [l]),
|
2023-05-03 05:22:15 +03:00
|
|
|
|
);
|
|
|
|
|
|
2023-12-20 08:55:56 +03:00
|
|
|
|
const reblogIterator = useRef();
|
|
|
|
|
const favouriteIterator = useRef();
|
|
|
|
|
async function fetchBoostedLikedByAccounts(firstLoad) {
|
|
|
|
|
if (firstLoad) {
|
|
|
|
|
reblogIterator.current = masto.v1.statuses
|
|
|
|
|
.$select(statusID)
|
|
|
|
|
.rebloggedBy.list({
|
|
|
|
|
limit: REACTIONS_LIMIT,
|
|
|
|
|
});
|
|
|
|
|
favouriteIterator.current = masto.v1.statuses
|
|
|
|
|
.$select(statusID)
|
|
|
|
|
.favouritedBy.list({
|
|
|
|
|
limit: REACTIONS_LIMIT,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
const [{ value: reblogResults }, { value: favouriteResults }] =
|
|
|
|
|
await Promise.allSettled([
|
|
|
|
|
reblogIterator.current.next(),
|
|
|
|
|
favouriteIterator.current.next(),
|
|
|
|
|
]);
|
|
|
|
|
if (reblogResults.value?.length || favouriteResults.value?.length) {
|
|
|
|
|
const accounts = [];
|
|
|
|
|
if (reblogResults.value?.length) {
|
|
|
|
|
accounts.push(
|
|
|
|
|
...reblogResults.value.map((a) => {
|
|
|
|
|
a._types = ['reblog'];
|
|
|
|
|
return a;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (favouriteResults.value?.length) {
|
|
|
|
|
accounts.push(
|
|
|
|
|
...favouriteResults.value.map((a) => {
|
|
|
|
|
a._types = ['favourite'];
|
|
|
|
|
return a;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
value: accounts,
|
|
|
|
|
done: reblogResults.done && favouriteResults.done,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
value: [],
|
|
|
|
|
done: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-13 19:32:08 +03:00
|
|
|
|
const actionsRef = useRef();
|
2024-03-04 04:56:38 +03:00
|
|
|
|
const isPublic = ['public', 'unlisted'].includes(visibility);
|
|
|
|
|
const isPinnable = ['public', 'unlisted', 'private'].includes(visibility);
|
2023-02-26 19:55:04 +03:00
|
|
|
|
const StatusMenuItems = (
|
|
|
|
|
<>
|
2023-03-14 09:11:40 +03:00
|
|
|
|
{!isSizeLarge && sameInstance && (
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<>
|
2024-02-06 12:34:26 +03:00
|
|
|
|
<div class="menu-control-group-horizontal status-menu">
|
|
|
|
|
<MenuItem onClick={replyStatus}>
|
|
|
|
|
<Icon icon="comment" />
|
|
|
|
|
<span>
|
|
|
|
|
{repliesCount > 0 ? shortenNumber(repliesCount) : 'Reply'}
|
|
|
|
|
</span>
|
|
|
|
|
</MenuItem>
|
2023-07-17 16:01:00 +03:00
|
|
|
|
<MenuConfirm
|
|
|
|
|
subMenu
|
|
|
|
|
confirmLabel={
|
|
|
|
|
<>
|
|
|
|
|
<Icon icon="rocket" />
|
2024-03-09 16:29:44 +03:00
|
|
|
|
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
|
2023-07-17 16:01:00 +03:00
|
|
|
|
</>
|
|
|
|
|
}
|
2024-02-06 12:34:26 +03:00
|
|
|
|
className={`menu-reblog ${reblogged ? 'checked' : ''}`}
|
2024-03-09 16:29:44 +03:00
|
|
|
|
menuExtras={
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
2024-05-24 07:30:20 +03:00
|
|
|
|
showCompose({
|
2024-03-09 16:29:44 +03:00
|
|
|
|
draftStatus: {
|
|
|
|
|
status: `\n${url}`,
|
|
|
|
|
},
|
2024-05-24 07:30:20 +03:00
|
|
|
|
});
|
2024-03-09 16:29:44 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="quote" />
|
|
|
|
|
<span>Quote</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
}
|
2023-07-17 16:01:00 +03:00
|
|
|
|
menuFooter={
|
2024-03-15 11:02:33 +03:00
|
|
|
|
mediaNoDesc && !reblogged ? (
|
2023-07-17 16:01:00 +03:00
|
|
|
|
<div class="footer">
|
|
|
|
|
<Icon icon="alert" />
|
|
|
|
|
Some media have no descriptions.
|
|
|
|
|
</div>
|
2024-03-15 11:02:33 +03:00
|
|
|
|
) : (
|
|
|
|
|
statusMonthsAgo >= 3 && (
|
|
|
|
|
<div class="footer">
|
|
|
|
|
<Icon icon="info" />
|
|
|
|
|
<span>
|
|
|
|
|
Old post (
|
|
|
|
|
<strong>{rtf.format(-statusMonthsAgo, 'month')}</strong>
|
|
|
|
|
)
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2023-07-17 16:01:00 +03:00
|
|
|
|
)
|
|
|
|
|
}
|
2023-04-09 20:21:02 +03:00
|
|
|
|
disabled={!canBoost}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
onClick={async () => {
|
|
|
|
|
try {
|
2023-07-17 16:01:00 +03:00
|
|
|
|
const done = await confirmBoostStatus();
|
2023-04-04 13:46:05 +03:00
|
|
|
|
if (!isSizeLarge && done) {
|
2023-10-19 15:02:31 +03:00
|
|
|
|
showToast(
|
|
|
|
|
reblogged
|
|
|
|
|
? `Unboosted @${username || acct}'s post`
|
|
|
|
|
: `Boosted @${username || acct}'s post`,
|
|
|
|
|
);
|
2023-04-04 13:46:05 +03:00
|
|
|
|
}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
} catch (e) {}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2024-02-06 12:34:26 +03:00
|
|
|
|
<Icon icon="rocket" />
|
|
|
|
|
<span>
|
|
|
|
|
{reblogsCount > 0
|
|
|
|
|
? shortenNumber(reblogsCount)
|
|
|
|
|
: reblogged
|
|
|
|
|
? 'Unboost'
|
|
|
|
|
: 'Boost…'}
|
|
|
|
|
</span>
|
2023-07-17 16:01:00 +03:00
|
|
|
|
</MenuConfirm>
|
2023-04-09 20:21:02 +03:00
|
|
|
|
<MenuItem
|
2024-02-26 06:58:22 +03:00
|
|
|
|
onClick={favouriteStatusNotify}
|
2024-02-06 12:34:26 +03:00
|
|
|
|
className={`menu-favourite ${favourited ? 'checked' : ''}`}
|
2023-04-09 20:21:02 +03:00
|
|
|
|
>
|
2024-02-06 12:34:26 +03:00
|
|
|
|
<Icon icon="heart" />
|
|
|
|
|
<span>
|
|
|
|
|
{favouritesCount > 0
|
|
|
|
|
? shortenNumber(favouritesCount)
|
|
|
|
|
: favourited
|
|
|
|
|
? 'Unlike'
|
|
|
|
|
: 'Like'}
|
|
|
|
|
</span>
|
2023-04-18 19:00:47 +03:00
|
|
|
|
</MenuItem>
|
2024-04-14 12:20:18 +03:00
|
|
|
|
{supports('@mastodon/post-bookmark') && (
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={bookmarkStatusNotify}
|
|
|
|
|
className={`menu-bookmark ${bookmarked ? 'checked' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="bookmark" />
|
|
|
|
|
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
2023-04-09 20:21:02 +03:00
|
|
|
|
</div>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
</>
|
|
|
|
|
)}
|
2024-03-24 12:24:47 +03:00
|
|
|
|
{!isSizeLarge && sameInstance && (isSizeLarge || showActionsBar) && (
|
|
|
|
|
<MenuDivider />
|
|
|
|
|
)}
|
|
|
|
|
{(isSizeLarge || showActionsBar) && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
states.showGenericAccounts = {
|
|
|
|
|
heading: 'Boosted/Liked by…',
|
|
|
|
|
fetchAccounts: fetchBoostedLikedByAccounts,
|
|
|
|
|
instance,
|
|
|
|
|
showReactions: true,
|
|
|
|
|
postID: sKey,
|
|
|
|
|
};
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="react" />
|
|
|
|
|
<span>
|
|
|
|
|
Boosted/Liked by<span class="more-insignificant">…</span>
|
|
|
|
|
</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{!mediaFirst && (
|
|
|
|
|
<>
|
|
|
|
|
{(enableTranslate || !language || differentLanguage) && (
|
|
|
|
|
<MenuDivider />
|
2023-12-21 13:17:14 +03:00
|
|
|
|
)}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{enableTranslate ? (
|
|
|
|
|
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
2023-12-21 13:17:14 +03:00
|
|
|
|
<MenuItem
|
2024-04-11 12:18:17 +03:00
|
|
|
|
disabled={forceTranslate}
|
2023-12-21 13:17:14 +03:00
|
|
|
|
onClick={() => {
|
2024-04-11 12:18:17 +03:00
|
|
|
|
setForceTranslate(true);
|
2023-12-21 13:17:14 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
<Icon icon="translate" />
|
|
|
|
|
<span>Translate</span>
|
2023-12-21 13:17:14 +03:00
|
|
|
|
</MenuItem>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{supportsTTS && (
|
|
|
|
|
<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>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</>
|
2023-03-11 13:13:30 +03:00
|
|
|
|
)}
|
2024-03-20 06:04:18 +03:00
|
|
|
|
{((!isSizeLarge && sameInstance) ||
|
|
|
|
|
enableTranslate ||
|
|
|
|
|
!language ||
|
|
|
|
|
differentLanguage) && <MenuDivider />}
|
2024-02-06 12:34:26 +03:00
|
|
|
|
{!isSizeLarge && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuLink
|
|
|
|
|
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
onStatusLinkClick(e, status);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="arrows-right" />
|
|
|
|
|
<small>
|
|
|
|
|
View post by @{username || acct}
|
|
|
|
|
<br />
|
|
|
|
|
<span class="more-insignificant">
|
|
|
|
|
{visibilityText[visibility]} • {createdDateText}
|
|
|
|
|
</span>
|
|
|
|
|
</small>
|
|
|
|
|
</MenuLink>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{!!editedAt && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowEdited(id);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="history" />
|
|
|
|
|
<small>
|
|
|
|
|
Show Edit History
|
|
|
|
|
<br />
|
|
|
|
|
<span class="more-insignificant">Edited: {editedDateText}</span>
|
|
|
|
|
</small>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<MenuItem href={url} target="_blank">
|
|
|
|
|
<Icon icon="external" />
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<small class="menu-double-lines">{nicePostURL(url)}</small>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
</MenuItem>
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<div class="menu-horizontal">
|
2023-03-07 17:38:06 +03:00
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
2023-03-09 16:51:50 +03:00
|
|
|
|
// Copy url to clipboard
|
|
|
|
|
try {
|
|
|
|
|
navigator.clipboard.writeText(url);
|
|
|
|
|
showToast('Link copied');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
showToast('Unable to copy link');
|
|
|
|
|
}
|
2023-03-07 17:38:06 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<Icon icon="link" />
|
|
|
|
|
<span>Copy</span>
|
2023-03-07 17:38:06 +03:00
|
|
|
|
</MenuItem>
|
2024-03-04 04:56:38 +03:00
|
|
|
|
{isPublic &&
|
|
|
|
|
navigator?.share &&
|
2023-03-09 16:51:50 +03:00
|
|
|
|
navigator?.canShare?.({
|
|
|
|
|
url,
|
|
|
|
|
}) && (
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
try {
|
|
|
|
|
navigator.share({
|
|
|
|
|
url,
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
alert("Sharing doesn't seem to work.");
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="share" />
|
|
|
|
|
<span>Share…</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2024-03-04 04:56:38 +03:00
|
|
|
|
{isPublic && isSizeLarge && (
|
2024-03-02 13:55:05 +03:00
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowEmbed(true);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="code" />
|
2024-03-07 04:05:52 +03:00
|
|
|
|
<span>Embed post</span>
|
2024-03-02 13:55:05 +03:00
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
2023-04-09 19:30:32 +03:00
|
|
|
|
{(isSelf || mentionSelf) && <MenuDivider />}
|
|
|
|
|
{(isSelf || mentionSelf) && (
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
try {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
const newStatus = await masto.v1.statuses
|
|
|
|
|
.$select(id)
|
|
|
|
|
[muted ? 'unmute' : 'mute']();
|
2023-04-09 19:30:32 +03:00
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
showToast(muted ? 'Conversation unmuted' : 'Conversation muted');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
showToast(
|
|
|
|
|
muted
|
|
|
|
|
? 'Unable to unmute conversation'
|
|
|
|
|
: 'Unable to mute conversation',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{muted ? (
|
|
|
|
|
<>
|
|
|
|
|
<Icon icon="unmute" />
|
|
|
|
|
<span>Unmute conversation</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Icon icon="mute" />
|
|
|
|
|
<span>Mute conversation</span>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
2024-03-04 04:56:38 +03:00
|
|
|
|
{isSelf && isPinnable && (
|
2024-01-18 14:05:12 +03:00
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
try {
|
|
|
|
|
const newStatus = await masto.v1.statuses
|
|
|
|
|
.$select(id)
|
|
|
|
|
[_pinned ? 'unpin' : 'pin']();
|
|
|
|
|
// saveStatus(newStatus, instance);
|
|
|
|
|
showToast(
|
|
|
|
|
_pinned
|
|
|
|
|
? 'Post unpinned from profile'
|
|
|
|
|
: 'Post pinned to profile',
|
|
|
|
|
);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
showToast(
|
|
|
|
|
_pinned ? 'Unable to unpin post' : 'Unable to pin post',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{_pinned ? (
|
|
|
|
|
<>
|
|
|
|
|
<Icon icon="unpin" />
|
|
|
|
|
<span>Unpin from profile</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Icon icon="pin" />
|
|
|
|
|
<span>Pin to profile</span>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
{isSelf && (
|
2023-04-09 20:21:02 +03:00
|
|
|
|
<div class="menu-horizontal">
|
2024-04-14 12:20:18 +03:00
|
|
|
|
{supports('@mastodon/post-edit') && (
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
2024-05-24 07:30:20 +03:00
|
|
|
|
showCompose({
|
2024-04-14 12:20:18 +03:00
|
|
|
|
editStatus: status,
|
2024-05-24 07:30:20 +03:00
|
|
|
|
});
|
2024-04-14 12:20:18 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="pencil" />
|
|
|
|
|
<span>Edit</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
2023-03-17 12:14:54 +03:00
|
|
|
|
{isSizeLarge && (
|
2023-07-17 16:01:00 +03:00
|
|
|
|
<MenuConfirm
|
|
|
|
|
subMenu
|
|
|
|
|
confirmLabel={
|
|
|
|
|
<>
|
|
|
|
|
<Icon icon="trash" />
|
|
|
|
|
<span>Delete this post?</span>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
menuItemClassName="danger"
|
2023-03-17 12:14:54 +03:00
|
|
|
|
onClick={() => {
|
2023-07-17 16:01:00 +03:00
|
|
|
|
// const yes = confirm('Delete this post?');
|
|
|
|
|
// if (yes) {
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
2023-10-12 07:48:09 +03:00
|
|
|
|
await masto.v1.statuses.$select(id).remove();
|
2023-07-17 16:01:00 +03:00
|
|
|
|
const cachedStatus = getStatus(id, instance);
|
|
|
|
|
cachedStatus._deleted = true;
|
|
|
|
|
showToast('Deleted');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
showToast('Unable to delete');
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
// }
|
2023-03-17 12:14:54 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="trash" />
|
|
|
|
|
<span>Delete…</span>
|
2023-07-17 16:01:00 +03:00
|
|
|
|
</MenuConfirm>
|
2023-03-17 12:14:54 +03:00
|
|
|
|
)}
|
2023-04-09 20:21:02 +03:00
|
|
|
|
</div>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
)}
|
2024-02-26 06:58:22 +03:00
|
|
|
|
{!isSelf && isSizeLarge && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuDivider />
|
|
|
|
|
<MenuItem
|
|
|
|
|
className="danger"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
states.showReportModal = {
|
|
|
|
|
account: status.account,
|
|
|
|
|
post: status,
|
|
|
|
|
};
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="flag" />
|
|
|
|
|
<span>Report post…</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
|
2023-03-07 19:01:51 +03:00
|
|
|
|
const contextMenuRef = useRef();
|
2023-03-02 10:15:49 +03:00
|
|
|
|
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
2023-10-22 14:27:15 +03:00
|
|
|
|
const [contextMenuProps, setContextMenuProps] = useState({});
|
2023-11-02 03:00:00 +03:00
|
|
|
|
|
2024-01-11 05:44:24 +03:00
|
|
|
|
const showContextMenu =
|
|
|
|
|
allowContextMenu || (!isSizeLarge && !previewMode && !_deleted && !quoted);
|
2023-11-02 03:00:00 +03:00
|
|
|
|
|
2023-10-01 12:14:32 +03:00
|
|
|
|
// Only iOS/iPadOS browsers don't support contextmenu
|
|
|
|
|
// Some comments report iPadOS might support contextmenu if a mouse is connected
|
2023-06-14 17:23:56 +03:00
|
|
|
|
const bindLongPressContext = useLongPress(
|
2023-11-02 03:00:00 +03:00
|
|
|
|
isIOS && showContextMenu
|
2023-10-01 12:14:32 +03:00
|
|
|
|
? (e) => {
|
|
|
|
|
if (e.pointerType === 'mouse') return;
|
|
|
|
|
// There's 'pen' too, but not sure if contextmenu event would trigger from a pen
|
|
|
|
|
|
|
|
|
|
const { clientX, clientY } = e.touches?.[0] || e;
|
|
|
|
|
// link detection copied from onContextMenu because here it works
|
|
|
|
|
const link = e.target.closest('a');
|
2024-03-09 12:01:50 +03:00
|
|
|
|
if (
|
|
|
|
|
link &&
|
|
|
|
|
statusRef.current.contains(link) &&
|
|
|
|
|
!link.getAttribute('href').startsWith('#')
|
|
|
|
|
)
|
|
|
|
|
return;
|
2023-10-01 12:14:32 +03:00
|
|
|
|
e.preventDefault();
|
2023-10-22 14:27:15 +03:00
|
|
|
|
setContextMenuProps({
|
|
|
|
|
anchorPoint: {
|
|
|
|
|
x: clientX,
|
|
|
|
|
y: clientY,
|
|
|
|
|
},
|
|
|
|
|
direction: 'right',
|
2023-10-01 12:14:32 +03:00
|
|
|
|
});
|
|
|
|
|
setIsContextMenuOpen(true);
|
|
|
|
|
}
|
|
|
|
|
: null,
|
2023-03-07 19:01:51 +03:00
|
|
|
|
{
|
2023-04-24 16:36:03 +03:00
|
|
|
|
threshold: 600,
|
2023-03-07 19:01:51 +03:00
|
|
|
|
captureEvent: true,
|
|
|
|
|
detect: 'touch',
|
2023-09-29 19:26:51 +03:00
|
|
|
|
cancelOnMovement: 2, // true allows movement of up to 25 pixels
|
2023-03-07 19:01:51 +03:00
|
|
|
|
},
|
|
|
|
|
);
|
2023-03-02 10:15:49 +03:00
|
|
|
|
|
2023-12-29 13:16:08 +03:00
|
|
|
|
const hotkeysEnabled = !readOnly && !previewMode && !quoted;
|
2023-11-05 12:41:29 +03:00
|
|
|
|
const rRef = useHotkeys('r, shift+r', replyStatus, {
|
2023-09-08 10:32:55 +03:00
|
|
|
|
enabled: hotkeysEnabled,
|
|
|
|
|
});
|
2024-02-26 06:58:22 +03:00
|
|
|
|
const fRef = useHotkeys('f, l', favouriteStatusNotify, {
|
|
|
|
|
enabled: hotkeysEnabled,
|
|
|
|
|
});
|
|
|
|
|
const dRef = useHotkeys('d', bookmarkStatusNotify, {
|
|
|
|
|
enabled: hotkeysEnabled,
|
|
|
|
|
});
|
2023-09-08 10:32:55 +03:00
|
|
|
|
const bRef = useHotkeys(
|
|
|
|
|
'shift+b',
|
|
|
|
|
() => {
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const done = await confirmBoostStatus();
|
|
|
|
|
if (!isSizeLarge && done) {
|
2023-10-19 15:02:31 +03:00
|
|
|
|
showToast(
|
|
|
|
|
reblogged
|
|
|
|
|
? `Unboosted @${username || acct}'s post`
|
|
|
|
|
: `Boosted @${username || acct}'s post`,
|
|
|
|
|
);
|
2023-09-08 10:32:55 +03:00
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
})();
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
enabled: hotkeysEnabled && canBoost,
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-12-20 11:42:36 +03:00
|
|
|
|
const xRef = useHotkeys('x', (e) => {
|
|
|
|
|
const activeStatus = document.activeElement.closest(
|
|
|
|
|
'.status-link, .status-focus',
|
|
|
|
|
);
|
|
|
|
|
if (activeStatus) {
|
|
|
|
|
const spoilerButton = activeStatus.querySelector(
|
2023-12-24 16:07:46 +03:00
|
|
|
|
'.spoiler-button:not(.spoiling)',
|
2023-12-20 11:42:36 +03:00
|
|
|
|
);
|
|
|
|
|
if (spoilerButton) {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
spoilerButton.click();
|
2023-12-24 16:07:46 +03:00
|
|
|
|
} else {
|
|
|
|
|
const spoilerMediaButton = activeStatus.querySelector(
|
|
|
|
|
'.spoiler-media-button:not(.spoiling)',
|
|
|
|
|
);
|
|
|
|
|
if (spoilerMediaButton) {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
spoilerMediaButton.click();
|
|
|
|
|
}
|
2023-12-20 11:42:36 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-09-08 10:32:55 +03:00
|
|
|
|
|
2023-10-06 18:57:12 +03:00
|
|
|
|
const displayedMediaAttachments = mediaAttachments.slice(
|
|
|
|
|
0,
|
|
|
|
|
isSizeLarge ? undefined : 4,
|
|
|
|
|
);
|
2023-10-07 04:41:38 +03:00
|
|
|
|
const showMultipleMediaCaptions =
|
|
|
|
|
mediaAttachments.length > 1 &&
|
|
|
|
|
displayedMediaAttachments.some(
|
|
|
|
|
(media) => !!media.description && !isMediaCaptionLong(media.description),
|
|
|
|
|
);
|
2023-10-15 13:28:04 +03:00
|
|
|
|
const captionChildren = useMemo(() => {
|
|
|
|
|
if (!showMultipleMediaCaptions) return null;
|
|
|
|
|
const attachments = [];
|
|
|
|
|
displayedMediaAttachments.forEach((media, i) => {
|
|
|
|
|
if (!media.description) return;
|
|
|
|
|
const index = attachments.findIndex(
|
|
|
|
|
(attachment) => attachment.media.description === media.description,
|
|
|
|
|
);
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
attachments.push({
|
|
|
|
|
media,
|
|
|
|
|
indices: [i],
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
attachments[index].indices.push(i);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return attachments.map(({ media, indices }) => (
|
|
|
|
|
<div
|
|
|
|
|
key={media.id}
|
|
|
|
|
data-caption-index={indices.map((i) => i + 1).join(' ')}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
states.showMediaAlt = {
|
|
|
|
|
alt: media.description,
|
|
|
|
|
lang: language,
|
|
|
|
|
};
|
|
|
|
|
}}
|
|
|
|
|
title={media.description}
|
|
|
|
|
>
|
|
|
|
|
<sup>{indices.map((i) => i + 1).join(' ')}</sup> {media.description}
|
|
|
|
|
</div>
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
// return displayedMediaAttachments.map(
|
|
|
|
|
// (media, i) =>
|
|
|
|
|
// !!media.description && (
|
|
|
|
|
// <div
|
|
|
|
|
// key={media.id}
|
|
|
|
|
// data-caption-index={i + 1}
|
|
|
|
|
// onClick={(e) => {
|
|
|
|
|
// e.preventDefault();
|
|
|
|
|
// e.stopPropagation();
|
|
|
|
|
// states.showMediaAlt = {
|
|
|
|
|
// alt: media.description,
|
|
|
|
|
// lang: language,
|
|
|
|
|
// };
|
|
|
|
|
// }}
|
|
|
|
|
// title={media.description}
|
|
|
|
|
// >
|
|
|
|
|
// <sup>{i + 1}</sup> {media.description}
|
|
|
|
|
// </div>
|
|
|
|
|
// ),
|
|
|
|
|
// );
|
|
|
|
|
}, [showMultipleMediaCaptions, displayedMediaAttachments, language]);
|
2023-10-06 18:57:12 +03:00
|
|
|
|
|
2023-11-14 11:52:47 +03:00
|
|
|
|
const isThread = useMemo(() => {
|
|
|
|
|
return (
|
|
|
|
|
(!!inReplyToId && inReplyToAccountId === status.account?.id) ||
|
|
|
|
|
!!snapStates.statusThreadNumber[sKey]
|
|
|
|
|
);
|
2023-11-14 17:45:13 +03:00
|
|
|
|
}, [
|
|
|
|
|
inReplyToId,
|
|
|
|
|
inReplyToAccountId,
|
|
|
|
|
status.account?.id,
|
|
|
|
|
snapStates.statusThreadNumber[sKey],
|
|
|
|
|
]);
|
2023-11-14 11:52:47 +03:00
|
|
|
|
|
|
|
|
|
const showCommentHint = useMemo(() => {
|
|
|
|
|
return (
|
2023-11-14 17:45:13 +03:00
|
|
|
|
enableCommentHint &&
|
2023-11-14 11:52:47 +03:00
|
|
|
|
!isThread &&
|
|
|
|
|
!withinContext &&
|
|
|
|
|
!inReplyToId &&
|
|
|
|
|
visibility === 'public' &&
|
|
|
|
|
repliesCount > 0
|
|
|
|
|
);
|
2023-11-14 17:45:13 +03:00
|
|
|
|
}, [
|
|
|
|
|
enableCommentHint,
|
|
|
|
|
isThread,
|
|
|
|
|
withinContext,
|
|
|
|
|
inReplyToId,
|
|
|
|
|
repliesCount,
|
|
|
|
|
visibility,
|
|
|
|
|
]);
|
2023-11-30 18:47:58 +03:00
|
|
|
|
const showCommentCount = useMemo(() => {
|
|
|
|
|
if (
|
|
|
|
|
card ||
|
|
|
|
|
poll ||
|
|
|
|
|
sensitive ||
|
|
|
|
|
spoilerText ||
|
|
|
|
|
mediaAttachments?.length ||
|
|
|
|
|
isThread ||
|
|
|
|
|
withinContext ||
|
|
|
|
|
inReplyToId ||
|
|
|
|
|
repliesCount <= 0
|
|
|
|
|
) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const questionRegex = /[???︖❓❔⁇⁈⁉¿‽؟]/;
|
|
|
|
|
const containsQuestion = questionRegex.test(content);
|
|
|
|
|
if (!containsQuestion) return false;
|
|
|
|
|
const contentLength = htmlContentLength(content);
|
|
|
|
|
if (contentLength > 0 && contentLength <= SHOW_COMMENT_COUNT_LIMIT) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}, [
|
|
|
|
|
card,
|
|
|
|
|
poll,
|
|
|
|
|
sensitive,
|
|
|
|
|
spoilerText,
|
|
|
|
|
mediaAttachments,
|
|
|
|
|
reblog,
|
|
|
|
|
isThread,
|
|
|
|
|
withinContext,
|
|
|
|
|
inReplyToId,
|
|
|
|
|
repliesCount,
|
|
|
|
|
content,
|
|
|
|
|
]);
|
2023-11-14 11:52:47 +03:00
|
|
|
|
|
2022-12-10 12:14:48 +03:00
|
|
|
|
return (
|
2024-04-15 12:06:44 +03:00
|
|
|
|
<StatusParent>
|
2024-01-30 12:43:02 +03:00
|
|
|
|
{showReplyParent && !!(inReplyToId && inReplyToAccountId) && (
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<StatusCompact sKey={sKey} />
|
|
|
|
|
)}
|
|
|
|
|
<article
|
|
|
|
|
data-state-post-id={sKey}
|
|
|
|
|
ref={(node) => {
|
|
|
|
|
statusRef.current = node;
|
|
|
|
|
// Use parent node if it's in focus
|
|
|
|
|
// Use case: <a><status /></a>
|
|
|
|
|
// When navigating (j/k), the <a> is focused instead of <status />
|
|
|
|
|
// Hotkey binding doesn't bubble up thus this hack
|
|
|
|
|
const nodeRef =
|
|
|
|
|
node?.closest?.(
|
|
|
|
|
'.timeline-item, .timeline-item-alt, .status-link, .status-focus',
|
|
|
|
|
) || node;
|
|
|
|
|
rRef.current = nodeRef;
|
|
|
|
|
fRef.current = nodeRef;
|
|
|
|
|
dRef.current = nodeRef;
|
|
|
|
|
bRef.current = nodeRef;
|
|
|
|
|
xRef.current = nodeRef;
|
|
|
|
|
}}
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
class={`status ${
|
|
|
|
|
!withinContext && inReplyToId && inReplyToAccount
|
|
|
|
|
? 'status-reply-to'
|
|
|
|
|
: ''
|
|
|
|
|
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
|
2024-04-14 12:20:18 +03:00
|
|
|
|
SIZE_CLASS[size]
|
2024-01-30 09:34:54 +03:00
|
|
|
|
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''} ${
|
|
|
|
|
isContextMenuOpen ? 'status-menu-open' : ''
|
2024-04-11 12:18:17 +03:00
|
|
|
|
} ${mediaFirst && hasMediaAttachments ? 'status-media-first' : ''}`}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
onMouseEnter={debugHover}
|
|
|
|
|
onContextMenu={(e) => {
|
|
|
|
|
if (!showContextMenu) return;
|
|
|
|
|
if (e.metaKey) return;
|
|
|
|
|
// console.log('context menu', e);
|
|
|
|
|
const link = e.target.closest('a');
|
2024-03-09 12:01:50 +03:00
|
|
|
|
if (
|
|
|
|
|
link &&
|
|
|
|
|
statusRef.current.contains(link) &&
|
|
|
|
|
!link.getAttribute('href').startsWith('#')
|
|
|
|
|
)
|
|
|
|
|
return;
|
2024-01-30 09:34:54 +03:00
|
|
|
|
|
|
|
|
|
// If there's selected text, don't show custom context menu
|
|
|
|
|
const selection = window.getSelection?.();
|
|
|
|
|
if (selection.toString().length > 0) {
|
|
|
|
|
const { anchorNode } = selection;
|
|
|
|
|
if (statusRef.current?.contains(anchorNode)) {
|
|
|
|
|
return;
|
2023-08-06 11:54:13 +03:00
|
|
|
|
}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
}
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setContextMenuProps({
|
|
|
|
|
anchorPoint: {
|
|
|
|
|
x: e.clientX,
|
|
|
|
|
y: e.clientY,
|
2023-03-07 19:01:51 +03:00
|
|
|
|
},
|
2024-01-30 09:34:54 +03:00
|
|
|
|
direction: 'right',
|
|
|
|
|
});
|
|
|
|
|
setIsContextMenuOpen(true);
|
|
|
|
|
}}
|
|
|
|
|
{...(showContextMenu ? bindLongPressContext() : {})}
|
|
|
|
|
>
|
|
|
|
|
{showContextMenu && (
|
|
|
|
|
<ControlledMenu
|
|
|
|
|
ref={contextMenuRef}
|
|
|
|
|
state={isContextMenuOpen ? 'open' : undefined}
|
|
|
|
|
{...contextMenuProps}
|
|
|
|
|
onClose={(e) => {
|
|
|
|
|
setIsContextMenuOpen(false);
|
|
|
|
|
// statusRef.current?.focus?.();
|
|
|
|
|
if (e?.reason === 'click') {
|
|
|
|
|
statusRef.current?.closest('[tabindex]')?.focus?.();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
portal={{
|
|
|
|
|
target: document.body,
|
|
|
|
|
}}
|
|
|
|
|
containerProps={{
|
|
|
|
|
style: {
|
|
|
|
|
// Higher than the backdrop
|
|
|
|
|
zIndex: 1001,
|
|
|
|
|
},
|
|
|
|
|
onClick: () => {
|
|
|
|
|
contextMenuRef.current?.closeMenu?.();
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
overflow="auto"
|
|
|
|
|
boundingBoxPadding={safeBoundingBoxPadding()}
|
|
|
|
|
unmountOnClose
|
2024-01-13 19:32:08 +03:00
|
|
|
|
>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
{StatusMenuItems}
|
|
|
|
|
</ControlledMenu>
|
2024-01-15 17:05:18 +03:00
|
|
|
|
)}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
{showActionsBar &&
|
|
|
|
|
size !== 'l' &&
|
|
|
|
|
!previewMode &&
|
|
|
|
|
!readOnly &&
|
|
|
|
|
!_deleted &&
|
|
|
|
|
!quoted && (
|
|
|
|
|
<div
|
|
|
|
|
class={`status-actions ${
|
|
|
|
|
isContextMenuOpen === 'actions-bar' ? 'open' : ''
|
|
|
|
|
}`}
|
|
|
|
|
ref={actionsRef}
|
|
|
|
|
>
|
|
|
|
|
<StatusButton
|
|
|
|
|
size="s"
|
|
|
|
|
title="Reply"
|
|
|
|
|
alt="Reply"
|
|
|
|
|
class="reply-button"
|
|
|
|
|
icon="comment"
|
|
|
|
|
iconSize="m"
|
|
|
|
|
onClick={replyStatus}
|
|
|
|
|
/>
|
|
|
|
|
<StatusButton
|
|
|
|
|
size="s"
|
|
|
|
|
checked={favourited}
|
|
|
|
|
title={['Like', 'Unlike']}
|
|
|
|
|
alt={['Like', 'Liked']}
|
|
|
|
|
class="favourite-button"
|
|
|
|
|
icon="heart"
|
|
|
|
|
iconSize="m"
|
|
|
|
|
count={favouritesCount}
|
2024-02-26 06:58:22 +03:00
|
|
|
|
onClick={favouriteStatusNotify}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
title="More"
|
|
|
|
|
class="plain more-button"
|
2023-10-22 14:27:15 +03:00
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setContextMenuProps({
|
|
|
|
|
anchorRef: {
|
|
|
|
|
current: e.currentTarget,
|
|
|
|
|
},
|
2024-02-10 07:00:40 +03:00
|
|
|
|
align: 'start',
|
|
|
|
|
direction: 'left',
|
|
|
|
|
gap: 0,
|
|
|
|
|
shift: -8,
|
2023-10-22 14:27:15 +03:00
|
|
|
|
});
|
2024-01-30 09:34:54 +03:00
|
|
|
|
setIsContextMenuOpen('actions-bar');
|
2023-02-28 11:56:30 +03:00
|
|
|
|
}}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<Icon icon="more2" size="m" alt="More" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{size !== 'l' && (
|
|
|
|
|
<div class="status-badge">
|
|
|
|
|
{reblogged && <Icon class="reblog" icon="rocket" size="s" />}
|
|
|
|
|
{favourited && <Icon class="favourite" icon="heart" size="s" />}
|
|
|
|
|
{bookmarked && <Icon class="bookmark" icon="bookmark" size="s" />}
|
|
|
|
|
{_pinned && <Icon class="pin" icon="pin" size="s" />}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{size !== 's' && (
|
|
|
|
|
<a
|
|
|
|
|
href={accountURL}
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
// target="_blank"
|
|
|
|
|
title={`@${acct}`}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
states.showAccount = {
|
|
|
|
|
account: status.account,
|
|
|
|
|
instance,
|
|
|
|
|
};
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Avatar url={avatarStatic || avatar} size="xxl" squircle={bot} />
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="meta">
|
|
|
|
|
<span class="meta-name">
|
|
|
|
|
<NameText
|
|
|
|
|
account={status.account}
|
|
|
|
|
instance={instance}
|
|
|
|
|
showAvatar={size === 's'}
|
|
|
|
|
showAcct={isSizeLarge}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
|
|
|
|
<span class="ib">
|
|
|
|
|
<Icon icon="arrow-right" class="arrow" />{' '}
|
|
|
|
|
<NameText account={inReplyToAccount} instance={instance} short />
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
)} */}
|
|
|
|
|
{/* </span> */}{' '}
|
|
|
|
|
{size !== 'l' &&
|
|
|
|
|
(_deleted ? (
|
|
|
|
|
<span class="status-deleted-tag">Deleted</span>
|
2024-02-23 13:07:42 +03:00
|
|
|
|
) : url && !previewMode && !readOnly && !quoted ? (
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<Link
|
|
|
|
|
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (
|
|
|
|
|
e.metaKey ||
|
|
|
|
|
e.ctrlKey ||
|
|
|
|
|
e.shiftKey ||
|
|
|
|
|
e.altKey ||
|
|
|
|
|
e.which === 2
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onStatusLinkClick?.(e, status);
|
|
|
|
|
setContextMenuProps({
|
|
|
|
|
anchorRef: {
|
|
|
|
|
current: e.currentTarget,
|
|
|
|
|
},
|
|
|
|
|
align: 'end',
|
|
|
|
|
direction: 'bottom',
|
|
|
|
|
gap: 4,
|
|
|
|
|
});
|
|
|
|
|
setIsContextMenuOpen(true);
|
|
|
|
|
}}
|
|
|
|
|
class={`time ${
|
|
|
|
|
isContextMenuOpen && contextMenuProps?.anchorRef
|
|
|
|
|
? 'is-open'
|
|
|
|
|
: ''
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{showCommentHint && !showCommentCount ? (
|
|
|
|
|
<Icon
|
|
|
|
|
icon="comment2"
|
|
|
|
|
size="s"
|
|
|
|
|
alt={`${repliesCount} ${
|
|
|
|
|
repliesCount === 1 ? 'reply' : 'replies'
|
|
|
|
|
}`}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
2024-03-27 14:09:01 +03:00
|
|
|
|
visibility !== 'public' &&
|
|
|
|
|
visibility !== 'direct' && (
|
|
|
|
|
<Icon
|
|
|
|
|
icon={visibilityIconsMap[visibility]}
|
|
|
|
|
alt={visibilityText[visibility]}
|
|
|
|
|
size="s"
|
|
|
|
|
/>
|
|
|
|
|
)
|
2024-01-30 09:34:54 +03:00
|
|
|
|
)}{' '}
|
|
|
|
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
2024-02-23 13:07:42 +03:00
|
|
|
|
{!previewMode && !readOnly && (
|
|
|
|
|
<Icon icon="more2" class="more" />
|
|
|
|
|
)}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
</Link>
|
|
|
|
|
) : (
|
|
|
|
|
// <Menu
|
|
|
|
|
// instanceRef={menuInstanceRef}
|
|
|
|
|
// portal={{
|
|
|
|
|
// target: document.body,
|
|
|
|
|
// }}
|
|
|
|
|
// containerProps={{
|
|
|
|
|
// style: {
|
|
|
|
|
// // Higher than the backdrop
|
|
|
|
|
// zIndex: 1001,
|
|
|
|
|
// },
|
|
|
|
|
// onClick: (e) => {
|
|
|
|
|
// if (e.target === e.currentTarget)
|
|
|
|
|
// menuInstanceRef.current?.closeMenu?.();
|
|
|
|
|
// },
|
|
|
|
|
// }}
|
|
|
|
|
// align="end"
|
|
|
|
|
// gap={4}
|
|
|
|
|
// overflow="auto"
|
|
|
|
|
// viewScroll="close"
|
|
|
|
|
// boundingBoxPadding="8 8 8 8"
|
|
|
|
|
// unmountOnClose
|
|
|
|
|
// menuButton={({ open }) => (
|
|
|
|
|
// <Link
|
|
|
|
|
// to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
|
|
|
|
// onClick={(e) => {
|
|
|
|
|
// e.preventDefault();
|
|
|
|
|
// e.stopPropagation();
|
|
|
|
|
// onStatusLinkClick?.(e, status);
|
|
|
|
|
// }}
|
|
|
|
|
// class={`time ${open ? 'is-open' : ''}`}
|
|
|
|
|
// >
|
|
|
|
|
// <Icon
|
|
|
|
|
// icon={visibilityIconsMap[visibility]}
|
|
|
|
|
// alt={visibilityText[visibility]}
|
|
|
|
|
// size="s"
|
|
|
|
|
// />{' '}
|
|
|
|
|
// <RelativeTime datetime={createdAtDate} format="micro" />
|
|
|
|
|
// </Link>
|
|
|
|
|
// )}
|
|
|
|
|
// >
|
|
|
|
|
// {StatusMenuItems}
|
|
|
|
|
// </Menu>
|
|
|
|
|
<span class="time">
|
2024-03-27 14:09:01 +03:00
|
|
|
|
{visibility !== 'public' && visibility !== 'direct' && (
|
|
|
|
|
<>
|
|
|
|
|
<Icon
|
|
|
|
|
icon={visibilityIconsMap[visibility]}
|
|
|
|
|
alt={visibilityText[visibility]}
|
|
|
|
|
size="s"
|
|
|
|
|
/>{' '}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{visibility === 'direct' && (
|
|
|
|
|
<>
|
|
|
|
|
<div class="status-direct-badge">Private mention</div>{' '}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{!withinContext && (
|
|
|
|
|
<>
|
|
|
|
|
{isThread ? (
|
|
|
|
|
<div class="status-thread-badge">
|
|
|
|
|
<Icon icon="thread" size="s" />
|
|
|
|
|
Thread
|
|
|
|
|
{snapStates.statusThreadNumber[sKey]
|
|
|
|
|
? ` ${snapStates.statusThreadNumber[sKey]}/X`
|
|
|
|
|
: ''}
|
2022-12-23 20:22:25 +03:00
|
|
|
|
</div>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
) : (
|
|
|
|
|
!!inReplyToId &&
|
|
|
|
|
!!inReplyToAccount &&
|
|
|
|
|
(!!spoilerText ||
|
|
|
|
|
!mentions.find((mention) => {
|
|
|
|
|
return mention.id === inReplyToAccountId;
|
|
|
|
|
})) && (
|
|
|
|
|
<div class="status-reply-badge">
|
|
|
|
|
<Icon icon="reply" />{' '}
|
|
|
|
|
<NameText
|
|
|
|
|
account={inReplyToAccount}
|
|
|
|
|
instance={instance}
|
|
|
|
|
short
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<div
|
|
|
|
|
class={`content-container ${
|
|
|
|
|
spoilerText || sensitive ? 'has-spoiler' : ''
|
|
|
|
|
} ${showSpoiler ? 'show-spoiler' : ''} ${
|
|
|
|
|
showSpoilerMedia ? 'show-media' : ''
|
|
|
|
|
}`}
|
|
|
|
|
data-content-text-weight={contentTextWeight ? textWeight() : null}
|
|
|
|
|
style={
|
|
|
|
|
(isSizeLarge || contentTextWeight) && {
|
|
|
|
|
'--content-text-weight': textWeight(),
|
|
|
|
|
}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{mediaFirst && hasMediaAttachments ? (
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{(!!spoilerText || !!sensitive) && !readingExpandSpoilers && (
|
|
|
|
|
<>
|
|
|
|
|
{!!spoilerText && (
|
|
|
|
|
<span
|
|
|
|
|
class="spoiler-content media-first-spoiler-content"
|
|
|
|
|
lang={language}
|
|
|
|
|
dir="auto"
|
|
|
|
|
ref={spoilerContentRef}
|
|
|
|
|
data-read-more={readMoreText}
|
|
|
|
|
>
|
|
|
|
|
<EmojiText text={spoilerText} emojis={emojis} />{' '}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<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;
|
|
|
|
|
}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
|
|
|
|
{showSpoiler ? 'Show less' : 'Show content'}
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
)}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
<MediaFirstContainer
|
|
|
|
|
mediaAttachments={mediaAttachments}
|
|
|
|
|
language={language}
|
|
|
|
|
postID={id}
|
2024-02-06 12:30:10 +03:00
|
|
|
|
instance={instance}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
/>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{!!content && (
|
|
|
|
|
<div class="media-first-content content" ref={contentRef}>
|
|
|
|
|
<PostContent
|
|
|
|
|
post={status}
|
|
|
|
|
instance={instance}
|
|
|
|
|
previewMode={previewMode}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{!!spoilerText && (
|
|
|
|
|
<>
|
|
|
|
|
<div
|
|
|
|
|
class="content spoiler-content"
|
2024-01-30 09:34:54 +03:00
|
|
|
|
lang={language}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
dir="auto"
|
|
|
|
|
ref={spoilerContentRef}
|
|
|
|
|
data-read-more={readMoreText}
|
|
|
|
|
>
|
|
|
|
|
<p>
|
|
|
|
|
<EmojiText text={spoilerText} emojis={emojis} />
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
{readingExpandSpoilers || previewMode ? (
|
|
|
|
|
<div class="spoiler-divider">
|
|
|
|
|
<Icon icon="eye-open" /> Content warning
|
|
|
|
|
</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;
|
2024-01-30 09:34:54 +03:00
|
|
|
|
}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
{!!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}
|
2024-05-28 12:59:17 +03:00
|
|
|
|
autoDetected={languageAutoDetected}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
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
|
2024-01-30 09:34:54 +03:00
|
|
|
|
}
|
2024-04-11 12:18:17 +03:00
|
|
|
|
instance={currentInstance}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
/>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
)}
|
|
|
|
|
</>
|
2023-02-23 11:45:53 +03:00
|
|
|
|
)}
|
2023-11-30 18:47:58 +03:00
|
|
|
|
</div>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
{!isSizeLarge && showCommentCount && (
|
|
|
|
|
<div class="content-comment-hint insignificant">
|
|
|
|
|
<Icon icon="comment2" alt="Replies" /> {repliesCount}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{isSizeLarge && (
|
|
|
|
|
<>
|
|
|
|
|
<div class="extra-meta">
|
|
|
|
|
{_deleted ? (
|
|
|
|
|
<span class="status-deleted-tag">Deleted</span>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
2024-02-06 12:30:58 +03:00
|
|
|
|
{/* <Icon
|
2024-01-30 09:34:54 +03:00
|
|
|
|
icon={visibilityIconsMap[visibility]}
|
|
|
|
|
alt={visibilityText[visibility]}
|
2024-02-06 12:30:58 +03:00
|
|
|
|
/> */}
|
|
|
|
|
<span>{visibilityText[visibility]}</span> •{' '}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
2023-03-17 12:14:54 +03:00
|
|
|
|
<time
|
2024-01-30 09:34:54 +03:00
|
|
|
|
class="created"
|
|
|
|
|
datetime={createdAtDate.toISOString()}
|
|
|
|
|
title={createdAtDate.toLocaleString()}
|
2023-03-17 12:14:54 +03:00
|
|
|
|
>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
{createdDateText}
|
2023-03-17 12:14:54 +03:00
|
|
|
|
</time>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
</a>
|
|
|
|
|
{editedAt && (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
|
|
|
|
• <Icon icon="pencil" alt="Edited" />{' '}
|
|
|
|
|
<time
|
|
|
|
|
tabIndex="0"
|
|
|
|
|
class="edited"
|
|
|
|
|
datetime={editedAtDate.toISOString()}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowEdited(id);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{editedDateText}
|
|
|
|
|
</time>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2022-12-19 08:38:16 +03:00
|
|
|
|
</div>
|
2024-03-25 12:58:56 +03:00
|
|
|
|
{!!emojiReactions?.length && (
|
|
|
|
|
<div class="emoji-reactions">
|
|
|
|
|
{emojiReactions.map((emojiReaction) => {
|
2024-04-03 12:58:37 +03:00
|
|
|
|
const { name, count, me, url, staticUrl } = emojiReaction;
|
|
|
|
|
if (url) {
|
|
|
|
|
// Some servers return url and staticUrl
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
class={`emoji-reaction tag ${
|
|
|
|
|
me ? '' : 'insignificant'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<CustomEmoji
|
|
|
|
|
alt={name}
|
|
|
|
|
url={url}
|
|
|
|
|
staticUrl={staticUrl}
|
|
|
|
|
/>{' '}
|
|
|
|
|
{count}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-03-25 12:58:56 +03:00
|
|
|
|
const isShortCode = /^:.+?:$/.test(name);
|
|
|
|
|
if (isShortCode) {
|
|
|
|
|
const emoji = emojis.find(
|
|
|
|
|
(e) =>
|
|
|
|
|
e.shortcode ===
|
|
|
|
|
name.replace(/^:/, '').replace(/:$/, ''),
|
|
|
|
|
);
|
|
|
|
|
if (emoji) {
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
class={`emoji-reaction tag ${
|
|
|
|
|
me ? '' : 'insignificant'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<CustomEmoji
|
|
|
|
|
alt={name}
|
|
|
|
|
url={emoji.url}
|
|
|
|
|
staticUrl={emoji.staticUrl}
|
2024-04-03 12:58:37 +03:00
|
|
|
|
/>{' '}
|
2024-03-25 12:58:56 +03:00
|
|
|
|
{count}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<span
|
|
|
|
|
class={`emoji-reaction tag ${
|
|
|
|
|
me ? '' : 'insignificant'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{name} {count}
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<div class={`actions ${_deleted ? 'disabled' : ''}`}>
|
|
|
|
|
<div class="action has-count">
|
|
|
|
|
<StatusButton
|
|
|
|
|
title="Reply"
|
|
|
|
|
alt="Comments"
|
|
|
|
|
class="reply-button"
|
|
|
|
|
icon="comment"
|
|
|
|
|
count={repliesCount}
|
|
|
|
|
onClick={replyStatus}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{/* <div class="action has-count">
|
2023-04-03 04:09:52 +03:00
|
|
|
|
<StatusButton
|
|
|
|
|
checked={reblogged}
|
|
|
|
|
title={['Boost', 'Unboost']}
|
|
|
|
|
alt={['Boost', 'Boosted']}
|
|
|
|
|
class="reblog-button"
|
|
|
|
|
icon="rocket"
|
|
|
|
|
count={reblogsCount}
|
|
|
|
|
onClick={boostStatus}
|
|
|
|
|
disabled={!canBoost}
|
|
|
|
|
/>
|
2023-07-17 16:01:00 +03:00
|
|
|
|
</div> */}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<MenuConfirm
|
|
|
|
|
disabled={!canBoost}
|
|
|
|
|
onClick={confirmBoostStatus}
|
|
|
|
|
confirmLabel={
|
|
|
|
|
<>
|
|
|
|
|
<Icon icon="rocket" />
|
2024-03-09 16:29:44 +03:00
|
|
|
|
<span>{reblogged ? 'Unboost' : 'Boost'}</span>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
</>
|
|
|
|
|
}
|
2024-03-09 16:29:44 +03:00
|
|
|
|
menuExtras={
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
2024-05-24 07:30:20 +03:00
|
|
|
|
showCompose({
|
2024-03-09 16:29:44 +03:00
|
|
|
|
draftStatus: {
|
|
|
|
|
status: `\n${url}`,
|
|
|
|
|
},
|
2024-05-24 07:30:20 +03:00
|
|
|
|
});
|
2024-03-09 16:29:44 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="quote" />
|
|
|
|
|
<span>Quote</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
menuFooter={
|
|
|
|
|
mediaNoDesc &&
|
|
|
|
|
!reblogged && (
|
|
|
|
|
<div class="footer">
|
|
|
|
|
<Icon icon="alert" />
|
|
|
|
|
Some media have no descriptions.
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<div class="action has-count">
|
|
|
|
|
<StatusButton
|
|
|
|
|
checked={reblogged}
|
|
|
|
|
title={['Boost', 'Unboost']}
|
|
|
|
|
alt={['Boost', 'Boosted']}
|
|
|
|
|
class="reblog-button"
|
|
|
|
|
icon="rocket"
|
|
|
|
|
count={reblogsCount}
|
|
|
|
|
// onClick={boostStatus}
|
|
|
|
|
disabled={!canBoost}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</MenuConfirm>
|
2023-07-18 13:45:38 +03:00
|
|
|
|
<div class="action has-count">
|
|
|
|
|
<StatusButton
|
2024-01-30 09:34:54 +03:00
|
|
|
|
checked={favourited}
|
|
|
|
|
title={['Like', 'Unlike']}
|
|
|
|
|
alt={['Like', 'Liked']}
|
|
|
|
|
class="favourite-button"
|
|
|
|
|
icon="heart"
|
|
|
|
|
count={favouritesCount}
|
|
|
|
|
onClick={favouriteStatus}
|
2023-07-18 13:45:38 +03:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
2024-04-14 12:20:18 +03:00
|
|
|
|
{supports('@mastodon/post-bookmark') && (
|
|
|
|
|
<div class="action">
|
|
|
|
|
<StatusButton
|
|
|
|
|
checked={bookmarked}
|
|
|
|
|
title={['Bookmark', 'Unbookmark']}
|
|
|
|
|
alt={['Bookmark', 'Bookmarked']}
|
|
|
|
|
class="bookmark-button"
|
|
|
|
|
icon="bookmark"
|
|
|
|
|
onClick={bookmarkStatus}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
<Menu2
|
|
|
|
|
portal={{
|
|
|
|
|
target:
|
|
|
|
|
document.querySelector('.status-deck') || document.body,
|
|
|
|
|
}}
|
|
|
|
|
align="end"
|
|
|
|
|
gap={4}
|
|
|
|
|
overflow="auto"
|
|
|
|
|
viewScroll="close"
|
|
|
|
|
menuButton={
|
|
|
|
|
<div class="action">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
title="More"
|
|
|
|
|
class="plain more-button"
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="more" size="l" alt="More" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{StatusMenuItems}
|
|
|
|
|
</Menu2>
|
2022-12-19 08:38:16 +03:00
|
|
|
|
</div>
|
2024-01-30 09:34:54 +03:00
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{!!showEdited && (
|
|
|
|
|
<Modal
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
|
setShowEdited(false);
|
|
|
|
|
// statusRef.current?.focus();
|
|
|
|
|
}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
>
|
|
|
|
|
<EditedAtModal
|
|
|
|
|
statusID={showEdited}
|
|
|
|
|
instance={instance}
|
|
|
|
|
fetchStatusHistory={() => {
|
|
|
|
|
return masto.v1.statuses.$select(showEdited).history.list();
|
|
|
|
|
}}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowEdited(false);
|
|
|
|
|
statusRef.current?.focus();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Modal>
|
|
|
|
|
)}
|
2024-03-02 13:55:05 +03:00
|
|
|
|
{!!showEmbed && (
|
|
|
|
|
<Modal
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
|
setShowEmbed(false);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<EmbedModal
|
|
|
|
|
post={status}
|
|
|
|
|
instance={instance}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowEmbed(false);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Modal>
|
|
|
|
|
)}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
</article>
|
2024-04-15 12:06:44 +03:00
|
|
|
|
</StatusParent>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-02 07:21:26 +03:00
|
|
|
|
function MultipleMediaFigure(props) {
|
|
|
|
|
const { enabled, children, lang, captionChildren } = props;
|
|
|
|
|
if (!enabled || !captionChildren) return children;
|
|
|
|
|
return (
|
|
|
|
|
<figure class="media-figure-multiple">
|
|
|
|
|
{children}
|
|
|
|
|
<figcaption lang={lang} dir="auto">
|
2023-10-15 13:28:04 +03:00
|
|
|
|
{captionChildren}
|
2023-10-02 07:21:26 +03:00
|
|
|
|
</figcaption>
|
|
|
|
|
</figure>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-11 12:18:17 +03:00
|
|
|
|
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 (
|
|
|
|
|
<>
|
2024-04-15 18:34:58 +03:00
|
|
|
|
<div class="media-first-container">
|
|
|
|
|
<div class="media-first-carousel" ref={carouselRef}>
|
|
|
|
|
{mediaAttachments.map((media, i) => (
|
|
|
|
|
<div class="media-first-item" key={media.id}>
|
|
|
|
|
<Media
|
|
|
|
|
media={media}
|
|
|
|
|
lang={language}
|
|
|
|
|
to={`/${instance}/s/${postID}?media=${i + 1}`}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{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 && (
|
2024-04-15 14:59:57 +03:00
|
|
|
|
<div
|
|
|
|
|
class="media-carousel-dots"
|
|
|
|
|
style={{
|
|
|
|
|
'--dots-count': mediaAttachments.length,
|
|
|
|
|
}}
|
|
|
|
|
>
|
2024-04-11 12:18:17 +03:00
|
|
|
|
{mediaAttachments.map((media, i) => (
|
|
|
|
|
<span
|
|
|
|
|
key={media.id}
|
|
|
|
|
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 19:37:40 +03:00
|
|
|
|
function Card({ card, selfReferential, instance }) {
|
2023-04-22 19:55:47 +03:00
|
|
|
|
const snapStates = useSnapshot(states);
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const {
|
|
|
|
|
blurhash,
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
html,
|
|
|
|
|
providerName,
|
2023-10-04 17:40:34 +03:00
|
|
|
|
providerUrl,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
authorName,
|
2023-10-04 17:40:34 +03:00
|
|
|
|
authorUrl,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
image,
|
2023-10-04 17:40:34 +03:00
|
|
|
|
imageDescription,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
url,
|
|
|
|
|
type,
|
|
|
|
|
embedUrl,
|
2023-08-29 19:45:18 +03:00
|
|
|
|
language,
|
2023-10-04 17:40:34 +03:00
|
|
|
|
publishedAt,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
} = card;
|
|
|
|
|
|
|
|
|
|
/* type
|
|
|
|
|
link = Link OEmbed
|
|
|
|
|
photo = Photo OEmbed
|
|
|
|
|
video = Video OEmbed
|
|
|
|
|
rich = iframe OEmbed. Not currently accepted, so won’t show up in practice.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const hasText = title || providerName || authorName;
|
2022-12-29 06:34:29 +03:00
|
|
|
|
const isLandscape = width / height >= 1.2;
|
2023-01-07 06:52:23 +03:00
|
|
|
|
const size = isLandscape ? 'large' : '';
|
2022-12-18 16:10:05 +03:00
|
|
|
|
|
2023-02-23 11:45:53 +03:00
|
|
|
|
const [cardStatusURL, setCardStatusURL] = useState(null);
|
|
|
|
|
// const [cardStatusID, setCardStatusID] = useState(null);
|
|
|
|
|
useEffect(() => {
|
2023-12-20 19:37:40 +03:00
|
|
|
|
if (hasText && image && !selfReferential && isMastodonLinkMaybe(url)) {
|
2023-02-23 11:45:53 +03:00
|
|
|
|
unfurlMastodonLink(instance, url).then((result) => {
|
|
|
|
|
if (!result) return;
|
|
|
|
|
const { id, url } = result;
|
|
|
|
|
setCardStatusURL('#' + url);
|
|
|
|
|
|
|
|
|
|
// NOTE: This is for quote post
|
|
|
|
|
// (async () => {
|
|
|
|
|
// const { masto } = api({ instance });
|
2023-10-12 07:48:09 +03:00
|
|
|
|
// const status = await masto.v1.statuses.$select(id).fetch();
|
2023-02-23 11:45:53 +03:00
|
|
|
|
// saveStatus(status, instance);
|
|
|
|
|
// setCardStatusID(id);
|
|
|
|
|
// })();
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-12-20 19:37:40 +03:00
|
|
|
|
}, [hasText, image, selfReferential]);
|
2023-02-23 11:45:53 +03:00
|
|
|
|
|
|
|
|
|
// if (cardStatusID) {
|
|
|
|
|
// return (
|
|
|
|
|
// <Status statusID={cardStatusID} instance={instance} size="s" readOnly />
|
|
|
|
|
// );
|
|
|
|
|
// }
|
|
|
|
|
|
2023-04-22 19:55:47 +03:00
|
|
|
|
if (snapStates.unfurledLinks[url]) return null;
|
|
|
|
|
|
2024-01-06 11:46:45 +03:00
|
|
|
|
const hasIframeHTML = /<iframe/i.test(html);
|
|
|
|
|
const handleClick = useCallback(
|
|
|
|
|
(e) => {
|
|
|
|
|
if (hasIframeHTML) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
states.showEmbedModal = {
|
|
|
|
|
html,
|
|
|
|
|
url: url || embedUrl,
|
2024-01-19 15:31:05 +03:00
|
|
|
|
width,
|
|
|
|
|
height,
|
2024-01-06 11:46:45 +03:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[hasIframeHTML],
|
|
|
|
|
);
|
|
|
|
|
|
2023-08-04 19:16:18 +03:00
|
|
|
|
if (hasText && (image || (type === 'photo' && blurhash))) {
|
2024-04-02 04:03:13 +03:00
|
|
|
|
const domain = punycode.toUnicode(
|
|
|
|
|
new URL(url).hostname.replace(/^www\./, '').replace(/\/$/, ''),
|
|
|
|
|
);
|
2023-03-23 15:18:54 +03:00
|
|
|
|
let blurhashImage;
|
2023-12-26 12:06:52 +03:00
|
|
|
|
const rgbAverageColor =
|
|
|
|
|
image && blurhash ? getBlurHashAverageColor(blurhash) : null;
|
2023-03-23 15:18:54 +03:00
|
|
|
|
if (!image) {
|
|
|
|
|
const w = 44;
|
|
|
|
|
const h = 44;
|
|
|
|
|
const blurhashPixels = decodeBlurHash(blurhash, w, h);
|
2024-03-10 18:25:07 +03:00
|
|
|
|
const canvas = window.OffscreenCanvas
|
|
|
|
|
? new OffscreenCanvas(1, 1)
|
|
|
|
|
: document.createElement('canvas');
|
2023-03-23 15:18:54 +03:00
|
|
|
|
canvas.width = w;
|
|
|
|
|
canvas.height = h;
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
2024-03-10 18:25:07 +03:00
|
|
|
|
ctx.imageSmoothingEnabled = false;
|
2023-03-23 15:18:54 +03:00
|
|
|
|
const imageData = ctx.createImageData(w, h);
|
|
|
|
|
imageData.data.set(blurhashPixels);
|
|
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
|
|
|
blurhashImage = canvas.toDataURL();
|
|
|
|
|
}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
return (
|
|
|
|
|
<a
|
2023-02-23 11:45:53 +03:00
|
|
|
|
href={cardStatusURL || url}
|
|
|
|
|
target={cardStatusURL ? null : '_blank'}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
rel="nofollow noopener noreferrer"
|
2023-03-23 15:18:54 +03:00
|
|
|
|
class={`card link ${blurhashImage ? '' : size}`}
|
2023-08-29 19:45:18 +03:00
|
|
|
|
lang={language}
|
2023-09-23 07:58:12 +03:00
|
|
|
|
dir="auto"
|
2023-12-26 12:06:52 +03:00
|
|
|
|
style={{
|
|
|
|
|
'--average-color':
|
|
|
|
|
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
|
|
|
|
}}
|
2024-01-06 11:46:45 +03:00
|
|
|
|
onClick={handleClick}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
>
|
2023-01-07 15:25:13 +03:00
|
|
|
|
<div class="card-image">
|
|
|
|
|
<img
|
2023-03-23 15:18:54 +03:00
|
|
|
|
src={image || blurhashImage}
|
2023-01-07 15:25:13 +03:00
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
loading="lazy"
|
2023-10-04 17:40:34 +03:00
|
|
|
|
alt={imageDescription || ''}
|
2023-01-07 15:25:13 +03:00
|
|
|
|
onError={(e) => {
|
|
|
|
|
try {
|
|
|
|
|
e.target.style.display = 'none';
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="meta-container">
|
2024-03-23 07:26:50 +03:00
|
|
|
|
<p class="meta domain">
|
|
|
|
|
<span class="domain">{domain}</span>{' '}
|
|
|
|
|
{!!publishedAt && <>· </>}
|
|
|
|
|
{!!publishedAt && (
|
|
|
|
|
<>
|
|
|
|
|
<RelativeTime datetime={publishedAt} format="micro" />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2023-09-23 07:58:12 +03:00
|
|
|
|
</p>
|
2024-03-15 20:06:56 +03:00
|
|
|
|
<p class="title" dir="auto" title={title}>
|
2023-09-23 07:58:12 +03:00
|
|
|
|
{title}
|
|
|
|
|
</p>
|
2024-03-15 20:06:56 +03:00
|
|
|
|
<p class="meta" dir="auto" title={description}>
|
2023-11-04 10:36:13 +03:00
|
|
|
|
{description ||
|
|
|
|
|
(!!publishedAt && (
|
|
|
|
|
<RelativeTime datetime={publishedAt} format="micro" />
|
|
|
|
|
))}
|
2023-09-23 07:58:12 +03:00
|
|
|
|
</p>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
} else if (type === 'photo') {
|
|
|
|
|
return (
|
|
|
|
|
<a
|
|
|
|
|
href={url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="nofollow noopener noreferrer"
|
|
|
|
|
class="card photo"
|
2024-01-06 11:46:45 +03:00
|
|
|
|
onClick={handleClick}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={embedUrl}
|
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
alt={title || description}
|
|
|
|
|
loading="lazy"
|
|
|
|
|
style={{
|
|
|
|
|
height: 'auto',
|
|
|
|
|
aspectRatio: `${width}/${height}`,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
2024-01-06 11:46:45 +03:00
|
|
|
|
} else {
|
|
|
|
|
if (type === 'video') {
|
|
|
|
|
if (/youtube/i.test(providerName)) {
|
|
|
|
|
// Get ID from e.g. https://www.youtube.com/watch?v=[VIDEO_ID]
|
|
|
|
|
const videoID = url.match(/watch\?v=([^&]+)/)?.[1];
|
|
|
|
|
if (videoID) {
|
2024-03-02 13:53:35 +03:00
|
|
|
|
return (
|
|
|
|
|
<a class="card video" onClick={handleClick}>
|
|
|
|
|
<lite-youtube videoid={videoID} nocookie></lite-youtube>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
2024-01-06 11:46:45 +03:00
|
|
|
|
}
|
2023-03-18 15:05:12 +03:00
|
|
|
|
}
|
2024-01-06 11:46:45 +03:00
|
|
|
|
// return (
|
|
|
|
|
// <div
|
|
|
|
|
// class="card video"
|
|
|
|
|
// style={{
|
|
|
|
|
// aspectRatio: `${width}/${height}`,
|
|
|
|
|
// }}
|
|
|
|
|
// dangerouslySetInnerHTML={{ __html: html }}
|
|
|
|
|
// />
|
|
|
|
|
// );
|
|
|
|
|
}
|
|
|
|
|
if (hasText && !image) {
|
2024-04-02 04:03:13 +03:00
|
|
|
|
const domain = punycode.toUnicode(
|
|
|
|
|
new URL(url).hostname.replace(/^www\./, ''),
|
|
|
|
|
);
|
2024-01-06 11:46:45 +03:00
|
|
|
|
return (
|
|
|
|
|
<a
|
|
|
|
|
href={cardStatusURL || url}
|
|
|
|
|
target={cardStatusURL ? null : '_blank'}
|
|
|
|
|
rel="nofollow noopener noreferrer"
|
|
|
|
|
class={`card link no-image`}
|
|
|
|
|
lang={language}
|
|
|
|
|
onClick={handleClick}
|
|
|
|
|
>
|
|
|
|
|
<div class="meta-container">
|
|
|
|
|
<p class="meta domain">
|
2024-03-23 07:26:50 +03:00
|
|
|
|
<span class="domain">
|
|
|
|
|
<Icon icon="link" size="s" /> <span>{domain}</span>
|
|
|
|
|
</span>{' '}
|
|
|
|
|
{!!publishedAt && <>· </>}
|
|
|
|
|
{!!publishedAt && (
|
|
|
|
|
<>
|
|
|
|
|
<RelativeTime datetime={publishedAt} format="micro" />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2024-01-06 11:46:45 +03:00
|
|
|
|
</p>
|
2024-03-20 06:03:15 +03:00
|
|
|
|
<p class="title" title={title}>
|
|
|
|
|
{title}
|
|
|
|
|
</p>
|
|
|
|
|
<p class="meta" title={description || providerName || authorName}>
|
|
|
|
|
{description || providerName || authorName}
|
|
|
|
|
</p>
|
2024-01-06 11:46:45 +03:00
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
2023-03-18 15:05:12 +03:00
|
|
|
|
}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-05 19:17:19 +03:00
|
|
|
|
function EditedAtModal({
|
|
|
|
|
statusID,
|
|
|
|
|
instance,
|
|
|
|
|
fetchStatusHistory = () => {},
|
2023-04-20 11:10:57 +03:00
|
|
|
|
onClose,
|
2023-02-05 19:17:19 +03:00
|
|
|
|
}) {
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const [uiState, setUIState] = useState('default');
|
|
|
|
|
const [editHistory, setEditHistory] = useState([]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setUIState('loading');
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
2023-02-05 19:17:19 +03:00
|
|
|
|
const editHistory = await fetchStatusHistory();
|
2022-12-18 16:10:05 +03:00
|
|
|
|
console.log(editHistory);
|
|
|
|
|
setEditHistory(editHistory);
|
|
|
|
|
setUIState('default');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
setUIState('error');
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
2022-12-30 15:37:57 +03:00
|
|
|
|
<div id="edit-history" class="sheet">
|
2023-04-20 11:10:57 +03:00
|
|
|
|
{!!onClose && (
|
|
|
|
|
<button type="button" class="sheet-close" onClick={onClose}>
|
|
|
|
|
<Icon icon="x" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2022-12-25 13:01:01 +03:00
|
|
|
|
<header>
|
|
|
|
|
<h2>Edit History</h2>
|
|
|
|
|
{uiState === 'error' && <p>Failed to load history</p>}
|
|
|
|
|
{uiState === 'loading' && (
|
|
|
|
|
<p>
|
|
|
|
|
<Loader abrupt /> Loading…
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</header>
|
2022-12-30 15:37:57 +03:00
|
|
|
|
<main tabIndex="-1">
|
2022-12-25 13:01:01 +03:00
|
|
|
|
{editHistory.length > 0 && (
|
|
|
|
|
<ol>
|
|
|
|
|
{editHistory.map((status) => {
|
|
|
|
|
const { createdAt } = status;
|
|
|
|
|
const createdAtDate = new Date(createdAt);
|
|
|
|
|
return (
|
|
|
|
|
<li key={createdAt} class="history-item">
|
|
|
|
|
<h3>
|
2023-03-03 13:11:37 +03:00
|
|
|
|
<time>
|
|
|
|
|
{niceDateTime(createdAtDate, {
|
|
|
|
|
formatOpts: {
|
|
|
|
|
weekday: 'short',
|
|
|
|
|
second: 'numeric',
|
|
|
|
|
},
|
|
|
|
|
})}
|
|
|
|
|
</time>
|
2022-12-25 13:01:01 +03:00
|
|
|
|
</h3>
|
2023-02-05 19:17:19 +03:00
|
|
|
|
<Status
|
|
|
|
|
status={status}
|
|
|
|
|
instance={instance}
|
|
|
|
|
size="s"
|
|
|
|
|
withinContext
|
|
|
|
|
readOnly
|
2023-03-22 09:16:41 +03:00
|
|
|
|
previewMode
|
2023-02-05 19:17:19 +03:00
|
|
|
|
/>
|
2022-12-25 13:01:01 +03:00
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ol>
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
2023-04-06 17:51:48 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-03-02 13:55:05 +03:00
|
|
|
|
function generateHTMLCode(post, instance, level = 0) {
|
|
|
|
|
const {
|
|
|
|
|
account: {
|
|
|
|
|
url: accountURL,
|
|
|
|
|
displayName,
|
2024-03-03 07:12:40 +03:00
|
|
|
|
acct,
|
2024-03-02 13:55:05 +03:00
|
|
|
|
username,
|
|
|
|
|
emojis: accountEmojis,
|
|
|
|
|
bot,
|
|
|
|
|
group,
|
|
|
|
|
},
|
|
|
|
|
id,
|
|
|
|
|
poll,
|
|
|
|
|
spoilerText,
|
|
|
|
|
language,
|
|
|
|
|
editedAt,
|
|
|
|
|
createdAt,
|
|
|
|
|
content,
|
|
|
|
|
mediaAttachments,
|
|
|
|
|
url,
|
|
|
|
|
emojis,
|
|
|
|
|
} = post;
|
|
|
|
|
|
|
|
|
|
const sKey = statusKey(id, instance);
|
|
|
|
|
const quotes = states.statusQuotes[sKey] || [];
|
|
|
|
|
const uniqueQuotes = quotes.filter(
|
|
|
|
|
(q, i, arr) => arr.findIndex((q2) => q2.url === q.url) === i,
|
|
|
|
|
);
|
|
|
|
|
const quoteStatusesHTML =
|
|
|
|
|
uniqueQuotes.length && level <= 2
|
|
|
|
|
? uniqueQuotes
|
|
|
|
|
.map((quote) => {
|
|
|
|
|
const { id, instance } = quote;
|
|
|
|
|
const sKey = statusKey(id, instance);
|
|
|
|
|
const s = states.statuses[sKey];
|
|
|
|
|
if (s) {
|
|
|
|
|
return generateHTMLCode(s, instance, ++level);
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.join('')
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
const createdAtDate = new Date(createdAt);
|
|
|
|
|
// const editedAtDate = editedAt && new Date(editedAt);
|
|
|
|
|
|
|
|
|
|
const contentHTML =
|
|
|
|
|
emojifyText(content, emojis) +
|
|
|
|
|
'\n' +
|
|
|
|
|
quoteStatusesHTML +
|
|
|
|
|
'\n' +
|
|
|
|
|
(poll?.options?.length
|
|
|
|
|
? `
|
|
|
|
|
<p>📊:</p>
|
|
|
|
|
<ul>
|
|
|
|
|
${poll.options
|
|
|
|
|
.map(
|
|
|
|
|
(option) => `
|
|
|
|
|
<li>
|
|
|
|
|
${option.title}
|
|
|
|
|
${option.votesCount >= 0 ? ` (${option.votesCount})` : ''}
|
|
|
|
|
</li>
|
|
|
|
|
`,
|
|
|
|
|
)
|
|
|
|
|
.join('')}
|
|
|
|
|
</ul>`
|
|
|
|
|
: '') +
|
|
|
|
|
(mediaAttachments.length > 0
|
|
|
|
|
? '\n' +
|
|
|
|
|
mediaAttachments
|
|
|
|
|
.map((media) => {
|
|
|
|
|
const {
|
|
|
|
|
description,
|
|
|
|
|
meta,
|
|
|
|
|
previewRemoteUrl,
|
|
|
|
|
previewUrl,
|
|
|
|
|
remoteUrl,
|
|
|
|
|
url,
|
|
|
|
|
type,
|
|
|
|
|
} = media;
|
|
|
|
|
const { original = {}, small } = meta || {};
|
|
|
|
|
const width = small?.width || original?.width;
|
|
|
|
|
const height = small?.height || original?.height;
|
|
|
|
|
|
|
|
|
|
// Prefer remote over original
|
|
|
|
|
const sourceMediaURL = remoteUrl || url;
|
|
|
|
|
const previewMediaURL = previewRemoteUrl || previewUrl;
|
|
|
|
|
const mediaURL = previewMediaURL || sourceMediaURL;
|
|
|
|
|
|
|
|
|
|
const sourceMediaURLObj = sourceMediaURL
|
|
|
|
|
? new URL(sourceMediaURL)
|
|
|
|
|
: null;
|
|
|
|
|
const isVideoMaybe =
|
|
|
|
|
type === 'unknown' &&
|
|
|
|
|
sourceMediaURLObj &&
|
|
|
|
|
/\.(mp4|m4r|m4v|mov|webm)$/i.test(sourceMediaURLObj.pathname);
|
|
|
|
|
const isAudioMaybe =
|
|
|
|
|
type === 'unknown' &&
|
|
|
|
|
sourceMediaURLObj &&
|
|
|
|
|
/\.(mp3|ogg|wav|m4a|m4p|m4b)$/i.test(sourceMediaURLObj.pathname);
|
|
|
|
|
const isImage =
|
|
|
|
|
type === 'image' ||
|
|
|
|
|
(type === 'unknown' &&
|
|
|
|
|
previewMediaURL &&
|
|
|
|
|
!isVideoMaybe &&
|
|
|
|
|
!isAudioMaybe);
|
|
|
|
|
const isVideo = type === 'gifv' || type === 'video' || isVideoMaybe;
|
|
|
|
|
const isAudio = type === 'audio' || isAudioMaybe;
|
|
|
|
|
|
|
|
|
|
let mediaHTML = '';
|
|
|
|
|
if (isImage) {
|
|
|
|
|
mediaHTML = `<img src="${mediaURL}" width="${width}" height="${height}" alt="${description}" loading="lazy" />`;
|
|
|
|
|
} else if (isVideo) {
|
|
|
|
|
mediaHTML = `
|
|
|
|
|
<video src="${sourceMediaURL}" width="${width}" height="${height}" controls preload="auto" poster="${previewMediaURL}" loading="lazy"></video>
|
|
|
|
|
${description ? `<figcaption>${description}</figcaption>` : ''}
|
|
|
|
|
`;
|
|
|
|
|
} else if (isAudio) {
|
|
|
|
|
mediaHTML = `
|
|
|
|
|
<audio src="${sourceMediaURL}" controls preload="auto"></audio>
|
|
|
|
|
${description ? `<figcaption>${description}</figcaption>` : ''}
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
mediaHTML = `
|
|
|
|
|
<a href="${sourceMediaURL}">📄 ${
|
|
|
|
|
description || sourceMediaURL
|
|
|
|
|
}</a>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `<figure>${mediaHTML}</figure>`;
|
|
|
|
|
})
|
|
|
|
|
.join('\n')
|
|
|
|
|
: '');
|
|
|
|
|
|
|
|
|
|
const htmlCode = `
|
|
|
|
|
<blockquote lang="${language}" cite="${url}">
|
|
|
|
|
${
|
|
|
|
|
spoilerText
|
|
|
|
|
? `
|
|
|
|
|
<details>
|
|
|
|
|
<summary>${spoilerText}</summary>
|
|
|
|
|
${contentHTML}
|
|
|
|
|
</details>
|
|
|
|
|
`
|
|
|
|
|
: contentHTML
|
|
|
|
|
}
|
|
|
|
|
<footer>
|
|
|
|
|
— ${emojifyText(
|
|
|
|
|
displayName,
|
|
|
|
|
accountEmojis,
|
2024-03-03 07:12:40 +03:00
|
|
|
|
)} (@${acct}) <a href="${url}"><time datetime="${createdAtDate.toISOString()}">${createdAtDate.toLocaleString()}</time></a>
|
2024-03-02 13:55:05 +03:00
|
|
|
|
</footer>
|
|
|
|
|
</blockquote>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
return prettify(htmlCode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EmbedModal({ post, instance, onClose }) {
|
|
|
|
|
const {
|
|
|
|
|
account: {
|
|
|
|
|
url: accountURL,
|
|
|
|
|
displayName,
|
|
|
|
|
username,
|
|
|
|
|
emojis: accountEmojis,
|
|
|
|
|
bot,
|
|
|
|
|
group,
|
|
|
|
|
},
|
|
|
|
|
id,
|
|
|
|
|
poll,
|
|
|
|
|
spoilerText,
|
|
|
|
|
language,
|
|
|
|
|
editedAt,
|
|
|
|
|
createdAt,
|
|
|
|
|
content,
|
|
|
|
|
mediaAttachments,
|
|
|
|
|
url,
|
|
|
|
|
emojis,
|
|
|
|
|
} = post;
|
|
|
|
|
|
|
|
|
|
const htmlCode = generateHTMLCode(post, instance);
|
|
|
|
|
return (
|
|
|
|
|
<div id="embed-post" class="sheet">
|
|
|
|
|
{!!onClose && (
|
|
|
|
|
<button type="button" class="sheet-close" onClick={onClose}>
|
|
|
|
|
<Icon icon="x" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
<header>
|
|
|
|
|
<h2>Embed post</h2>
|
|
|
|
|
</header>
|
|
|
|
|
<main tabIndex="-1">
|
|
|
|
|
<h3>HTML Code</h3>
|
|
|
|
|
<textarea
|
|
|
|
|
class="embed-code"
|
|
|
|
|
readonly
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.target.select();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{htmlCode}
|
|
|
|
|
</textarea>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
try {
|
|
|
|
|
navigator.clipboard.writeText(htmlCode);
|
|
|
|
|
showToast('HTML code copied');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
showToast('Unable to copy HTML code');
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="clipboard" /> <span>Copy</span>
|
|
|
|
|
</button>
|
|
|
|
|
{!!mediaAttachments?.length && (
|
|
|
|
|
<section>
|
|
|
|
|
<p>Media attachments:</p>
|
|
|
|
|
<ol class="links-list">
|
|
|
|
|
{mediaAttachments.map((media) => {
|
|
|
|
|
return (
|
|
|
|
|
<li key={media.id}>
|
|
|
|
|
<a
|
|
|
|
|
href={media.remoteUrl || media.url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
download
|
|
|
|
|
>
|
|
|
|
|
{media.remoteUrl || media.url}
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ol>
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
{!!accountEmojis?.length && (
|
|
|
|
|
<section>
|
|
|
|
|
<p>Account Emojis:</p>
|
2024-03-06 13:58:12 +03:00
|
|
|
|
<ul>
|
2024-03-02 13:55:05 +03:00
|
|
|
|
{accountEmojis.map((emoji) => {
|
|
|
|
|
return (
|
|
|
|
|
<li key={emoji.shortcode}>
|
|
|
|
|
<picture>
|
|
|
|
|
<source
|
|
|
|
|
srcset={emoji.staticUrl}
|
|
|
|
|
media="(prefers-reduced-motion: reduce)"
|
|
|
|
|
></source>
|
|
|
|
|
<img
|
|
|
|
|
class="shortcode-emoji emoji"
|
|
|
|
|
src={emoji.url}
|
|
|
|
|
alt={`:${emoji.shortcode}:`}
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
decoding="async"
|
|
|
|
|
/>
|
|
|
|
|
</picture>{' '}
|
|
|
|
|
<code>:{emoji.shortcode}:</code> (
|
|
|
|
|
<a href={emoji.url} target="_blank" download>
|
|
|
|
|
url
|
|
|
|
|
</a>
|
|
|
|
|
)
|
|
|
|
|
{emoji.staticUrl ? (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
|
|
|
|
(
|
|
|
|
|
<a href={emoji.staticUrl} target="_blank" download>
|
|
|
|
|
static
|
|
|
|
|
</a>
|
|
|
|
|
)
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
{!!emojis?.length && (
|
|
|
|
|
<section>
|
|
|
|
|
<p>Emojis:</p>
|
2024-03-06 13:58:12 +03:00
|
|
|
|
<ul>
|
2024-03-02 13:55:05 +03:00
|
|
|
|
{emojis.map((emoji) => {
|
|
|
|
|
return (
|
|
|
|
|
<li key={emoji.shortcode}>
|
|
|
|
|
<picture>
|
|
|
|
|
<source
|
|
|
|
|
srcset={emoji.staticUrl}
|
|
|
|
|
media="(prefers-reduced-motion: reduce)"
|
|
|
|
|
></source>
|
|
|
|
|
<img
|
|
|
|
|
class="shortcode-emoji emoji"
|
|
|
|
|
src={emoji.url}
|
|
|
|
|
alt={`:${emoji.shortcode}:`}
|
|
|
|
|
width="16"
|
|
|
|
|
height="16"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
decoding="async"
|
|
|
|
|
/>
|
|
|
|
|
</picture>{' '}
|
|
|
|
|
<code>:{emoji.shortcode}:</code> (
|
|
|
|
|
<a href={emoji.url} target="_blank" download>
|
|
|
|
|
url
|
|
|
|
|
</a>
|
|
|
|
|
)
|
|
|
|
|
{emoji.staticUrl ? (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
|
|
|
|
(
|
|
|
|
|
<a href={emoji.staticUrl} target="_blank" download>
|
|
|
|
|
static
|
|
|
|
|
</a>
|
|
|
|
|
)
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
<section>
|
|
|
|
|
<small>
|
|
|
|
|
<p>Notes:</p>
|
|
|
|
|
<ul>
|
|
|
|
|
<li>
|
|
|
|
|
This is static, unstyled and scriptless. You may need to apply
|
|
|
|
|
your own styles and edit as needed.
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
|
|
|
|
Polls are not interactive, becomes a list with vote counts.
|
|
|
|
|
</li>
|
|
|
|
|
<li>
|
|
|
|
|
Media attachments can be images, videos, audios or any file
|
|
|
|
|
types.
|
|
|
|
|
</li>
|
|
|
|
|
<li>Post could be edited or deleted later.</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</small>
|
|
|
|
|
</section>
|
|
|
|
|
<h3>Preview</h3>
|
|
|
|
|
<output
|
|
|
|
|
class="embed-preview"
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: htmlCode }}
|
|
|
|
|
/>
|
|
|
|
|
<p>
|
|
|
|
|
<small>Note: This preview is lightly styled.</small>
|
|
|
|
|
</p>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-17 12:26:41 +03:00
|
|
|
|
function StatusButton({
|
|
|
|
|
checked,
|
|
|
|
|
count,
|
|
|
|
|
class: className,
|
|
|
|
|
title,
|
|
|
|
|
alt,
|
2024-01-13 19:32:08 +03:00
|
|
|
|
size,
|
2022-12-17 12:26:41 +03:00
|
|
|
|
icon,
|
2024-01-13 19:32:08 +03:00
|
|
|
|
iconSize = 'l',
|
2022-12-17 12:26:41 +03:00
|
|
|
|
onClick,
|
|
|
|
|
...props
|
|
|
|
|
}) {
|
|
|
|
|
if (typeof title === 'string') {
|
|
|
|
|
title = [title, title];
|
|
|
|
|
}
|
|
|
|
|
if (typeof alt === 'string') {
|
|
|
|
|
alt = [alt, alt];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [buttonTitle, setButtonTitle] = useState(title[0] || '');
|
|
|
|
|
const [iconAlt, setIconAlt] = useState(alt[0] || '');
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (checked) {
|
|
|
|
|
setButtonTitle(title[1] || '');
|
|
|
|
|
setIconAlt(alt[1] || '');
|
|
|
|
|
} else {
|
|
|
|
|
setButtonTitle(title[0] || '');
|
|
|
|
|
setIconAlt(alt[0] || '');
|
|
|
|
|
}
|
|
|
|
|
}, [checked, title, alt]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
title={buttonTitle}
|
2024-01-13 19:32:08 +03:00
|
|
|
|
class={`plain ${size ? 'small' : ''} ${className} ${
|
|
|
|
|
checked ? 'checked' : ''
|
|
|
|
|
}`}
|
2022-12-17 12:26:41 +03:00
|
|
|
|
onClick={(e) => {
|
2023-07-17 16:01:00 +03:00
|
|
|
|
if (!onClick) return;
|
2022-12-17 12:26:41 +03:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onClick(e);
|
|
|
|
|
}}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
2024-01-13 19:32:08 +03:00
|
|
|
|
<Icon icon={icon} size={iconSize} alt={iconAlt} />
|
2022-12-17 12:26:41 +03:00
|
|
|
|
{!!count && (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
2022-12-17 19:13:56 +03:00
|
|
|
|
<small title={count}>{shortenNumber(count)}</small>
|
2022-12-17 12:26:41 +03:00
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-09 16:51:50 +03:00
|
|
|
|
function nicePostURL(url) {
|
2023-03-12 14:23:42 +03:00
|
|
|
|
if (!url) return;
|
2023-03-09 16:51:50 +03:00
|
|
|
|
const urlObj = new URL(url);
|
|
|
|
|
const { host, pathname } = urlObj;
|
|
|
|
|
const path = pathname.replace(/\/$/, '');
|
|
|
|
|
// split only first slash
|
|
|
|
|
const [_, username, restPath] = path.match(/\/(@[^\/]+)\/(.*)/) || [];
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2024-04-02 04:03:13 +03:00
|
|
|
|
{punycode.toUnicode(host)}
|
2023-03-09 16:51:50 +03:00
|
|
|
|
{username ? (
|
|
|
|
|
<>
|
|
|
|
|
/{username}
|
2023-03-10 14:34:04 +03:00
|
|
|
|
<wbr />
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<span class="more-insignificant">/{restPath}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span class="more-insignificant">{path}</span>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-30 09:34:54 +03:00
|
|
|
|
function StatusCompact({ sKey }) {
|
|
|
|
|
const snapStates = useSnapshot(states);
|
|
|
|
|
const statusReply = snapStates.statusReply[sKey];
|
|
|
|
|
if (!statusReply) return null;
|
|
|
|
|
|
|
|
|
|
const { id, instance } = statusReply;
|
|
|
|
|
const status = getStatus(id, instance);
|
|
|
|
|
if (!status) return null;
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
sensitive,
|
|
|
|
|
spoilerText,
|
2024-05-29 13:46:14 +03:00
|
|
|
|
account: { avatar, avatarStatic, bot } = {},
|
2024-01-30 09:34:54 +03:00
|
|
|
|
visibility,
|
|
|
|
|
content,
|
2024-01-30 12:43:02 +03:00
|
|
|
|
language,
|
2024-02-28 10:04:01 +03:00
|
|
|
|
filtered,
|
2024-01-30 09:34:54 +03:00
|
|
|
|
} = status;
|
|
|
|
|
if (sensitive || spoilerText) return null;
|
|
|
|
|
if (!content) return null;
|
|
|
|
|
|
2024-01-31 08:45:34 +03:00
|
|
|
|
const srKey = statusKey(id, instance);
|
2024-01-30 09:34:54 +03:00
|
|
|
|
const statusPeekText = statusPeek(status);
|
2024-02-28 10:04:01 +03:00
|
|
|
|
|
|
|
|
|
const filterContext = useContext(FilterContext);
|
|
|
|
|
const filterInfo = isFiltered(filtered, filterContext);
|
|
|
|
|
|
|
|
|
|
if (filterInfo?.action === 'hide') return null;
|
|
|
|
|
|
|
|
|
|
const filterTitleStr = filterInfo?.titlesStr || '';
|
|
|
|
|
|
2024-01-30 09:34:54 +03:00
|
|
|
|
return (
|
|
|
|
|
<article
|
|
|
|
|
class={`status compact-reply ${
|
|
|
|
|
visibility === 'direct' ? 'visibility-direct' : ''
|
|
|
|
|
}`}
|
|
|
|
|
tabindex="-1"
|
2024-01-31 08:45:34 +03:00
|
|
|
|
data-state-post-id={srKey}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
>
|
|
|
|
|
<Avatar url={avatarStatic || avatar} squircle={bot} />
|
2024-01-30 12:43:02 +03:00
|
|
|
|
<div
|
|
|
|
|
class="content-compact"
|
|
|
|
|
title={statusPeekText}
|
|
|
|
|
lang={language}
|
|
|
|
|
dir="auto"
|
|
|
|
|
>
|
2024-02-28 10:04:01 +03:00
|
|
|
|
{filterInfo ? (
|
|
|
|
|
<b class="status-filtered-badge badge-meta" title={filterTitleStr}>
|
|
|
|
|
<span>Filtered</span>
|
|
|
|
|
<span>{filterTitleStr}</span>
|
|
|
|
|
</b>
|
|
|
|
|
) : (
|
2024-02-28 10:34:11 +03:00
|
|
|
|
<span>{statusPeekText}</span>
|
2024-02-28 10:04:01 +03:00
|
|
|
|
)}
|
2024-01-30 09:34:54 +03:00
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-14 20:58:29 +03:00
|
|
|
|
function FilteredStatus({
|
|
|
|
|
status,
|
|
|
|
|
filterInfo,
|
|
|
|
|
instance,
|
|
|
|
|
containerProps = {},
|
|
|
|
|
showFollowedTags,
|
2024-05-16 08:00:23 +03:00
|
|
|
|
quoted,
|
2023-12-14 20:58:29 +03:00
|
|
|
|
}) {
|
|
|
|
|
const snapStates = useSnapshot(states);
|
2023-03-21 19:09:36 +03:00
|
|
|
|
const {
|
2023-11-05 12:40:58 +03:00
|
|
|
|
id: statusID,
|
2023-08-08 15:21:09 +03:00
|
|
|
|
account: { avatar, avatarStatic, bot, group },
|
2023-03-21 19:09:36 +03:00
|
|
|
|
createdAt,
|
|
|
|
|
visibility,
|
2023-03-26 10:09:45 +03:00
|
|
|
|
reblog,
|
2023-03-21 19:09:36 +03:00
|
|
|
|
} = status;
|
2023-03-26 10:09:45 +03:00
|
|
|
|
const isReblog = !!reblog;
|
2023-03-21 19:09:36 +03:00
|
|
|
|
const filterTitleStr = filterInfo?.titlesStr || '';
|
|
|
|
|
const createdAtDate = new Date(createdAt);
|
2023-03-26 10:09:45 +03:00
|
|
|
|
const statusPeekText = statusPeek(status.reblog || status);
|
2023-03-21 19:09:36 +03:00
|
|
|
|
|
|
|
|
|
const [showPeek, setShowPeek] = useState(false);
|
2023-06-14 17:23:56 +03:00
|
|
|
|
const bindLongPressPeek = useLongPress(
|
2023-03-21 19:09:36 +03:00
|
|
|
|
() => {
|
|
|
|
|
setShowPeek(true);
|
|
|
|
|
},
|
|
|
|
|
{
|
2023-04-24 16:36:03 +03:00
|
|
|
|
threshold: 600,
|
2023-03-21 19:09:36 +03:00
|
|
|
|
captureEvent: true,
|
|
|
|
|
detect: 'touch',
|
2023-09-29 19:26:51 +03:00
|
|
|
|
cancelOnMovement: 2, // true allows movement of up to 25 pixels
|
2023-03-21 19:09:36 +03:00
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2023-09-23 14:14:11 +03:00
|
|
|
|
const statusPeekRef = useTruncated();
|
2023-12-14 20:58:29 +03:00
|
|
|
|
const sKey = statusKey(status.id, instance);
|
|
|
|
|
const ssKey =
|
2023-11-05 03:21:43 +03:00
|
|
|
|
statusKey(status.id, instance) +
|
|
|
|
|
' ' +
|
|
|
|
|
(statusKey(reblog?.id, instance) || '');
|
2023-09-23 14:14:11 +03:00
|
|
|
|
|
2023-11-05 12:40:58 +03:00
|
|
|
|
const actualStatusID = reblog?.id || statusID;
|
|
|
|
|
const url = instance
|
|
|
|
|
? `/${instance}/s/${actualStatusID}`
|
|
|
|
|
: `/s/${actualStatusID}`;
|
2023-12-14 20:58:29 +03:00
|
|
|
|
const isFollowedTags =
|
|
|
|
|
showFollowedTags && !!snapStates.statusFollowedTags[sKey]?.length;
|
2023-11-05 12:40:58 +03:00
|
|
|
|
|
2023-03-21 19:09:36 +03:00
|
|
|
|
return (
|
|
|
|
|
<div
|
2023-12-14 20:58:29 +03:00
|
|
|
|
class={
|
2024-05-16 08:00:23 +03:00
|
|
|
|
quoted
|
|
|
|
|
? ''
|
|
|
|
|
: isReblog
|
2023-12-14 20:58:29 +03:00
|
|
|
|
? group
|
|
|
|
|
? 'status-group'
|
|
|
|
|
: 'status-reblog'
|
|
|
|
|
: isFollowedTags
|
|
|
|
|
? 'status-followed-tags'
|
|
|
|
|
: ''
|
|
|
|
|
}
|
2023-03-23 16:48:29 +03:00
|
|
|
|
{...containerProps}
|
2024-02-01 19:27:12 +03:00
|
|
|
|
// title={statusPeekText}
|
2023-03-21 19:09:36 +03:00
|
|
|
|
onContextMenu={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setShowPeek(true);
|
|
|
|
|
}}
|
2023-06-14 17:23:56 +03:00
|
|
|
|
{...bindLongPressPeek()}
|
2023-03-21 19:09:36 +03:00
|
|
|
|
>
|
2024-05-16 08:00:23 +03:00
|
|
|
|
<article
|
|
|
|
|
data-state-post-id={ssKey}
|
|
|
|
|
class={`status filtered ${quoted ? 'status-card' : ''}`}
|
|
|
|
|
tabindex="-1"
|
|
|
|
|
>
|
2023-03-21 19:09:36 +03:00
|
|
|
|
<b
|
2023-03-22 07:26:28 +03:00
|
|
|
|
class="status-filtered-badge clickable badge-meta"
|
2023-03-21 19:09:36 +03:00
|
|
|
|
title={filterTitleStr}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setShowPeek(true);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-03-22 07:26:28 +03:00
|
|
|
|
<span>Filtered</span>
|
|
|
|
|
<span>{filterTitleStr}</span>
|
2023-03-21 19:09:36 +03:00
|
|
|
|
</b>{' '}
|
2023-04-10 19:26:43 +03:00
|
|
|
|
<Avatar url={avatarStatic || avatar} squircle={bot} />
|
2023-03-21 19:09:36 +03:00
|
|
|
|
<span class="status-filtered-info">
|
|
|
|
|
<span class="status-filtered-info-1">
|
|
|
|
|
<NameText account={status.account} instance={instance} />{' '}
|
|
|
|
|
<Icon
|
|
|
|
|
icon={visibilityIconsMap[visibility]}
|
|
|
|
|
alt={visibilityText[visibility]}
|
|
|
|
|
size="s"
|
|
|
|
|
/>{' '}
|
2023-03-26 10:09:45 +03:00
|
|
|
|
{isReblog ? (
|
|
|
|
|
'boosted'
|
2023-12-14 20:58:29 +03:00
|
|
|
|
) : isFollowedTags ? (
|
|
|
|
|
<span>
|
|
|
|
|
{snapStates.statusFollowedTags[sKey].slice(0, 3).map((tag) => (
|
|
|
|
|
<span key={tag} class="status-followed-tag-item">
|
|
|
|
|
#{tag}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</span>
|
2023-03-26 10:09:45 +03:00
|
|
|
|
) : (
|
|
|
|
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="status-filtered-info-2">
|
|
|
|
|
{isReblog && (
|
|
|
|
|
<>
|
|
|
|
|
<Avatar
|
|
|
|
|
url={reblog.account.avatarStatic || reblog.account.avatar}
|
2023-04-10 19:26:43 +03:00
|
|
|
|
squircle={bot}
|
2023-03-26 10:09:45 +03:00
|
|
|
|
/>{' '}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{statusPeekText}
|
2023-03-21 19:09:36 +03:00
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</article>
|
|
|
|
|
{!!showPeek && (
|
|
|
|
|
<Modal
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
|
setShowPeek(false);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div id="filtered-status-peek" class="sheet">
|
2023-04-20 11:10:57 +03:00
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="sheet-close"
|
|
|
|
|
onClick={() => setShowPeek(false)}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="x" />
|
|
|
|
|
</button>
|
|
|
|
|
<header>
|
|
|
|
|
<b class="status-filtered-badge">Filtered</b> {filterTitleStr}
|
|
|
|
|
</header>
|
2023-03-21 19:09:36 +03:00
|
|
|
|
<main tabIndex="-1">
|
|
|
|
|
<Link
|
2023-09-23 14:14:11 +03:00
|
|
|
|
ref={statusPeekRef}
|
2023-03-21 19:09:36 +03:00
|
|
|
|
class="status-link"
|
2023-11-05 12:40:58 +03:00
|
|
|
|
to={url}
|
2023-03-21 19:09:36 +03:00
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowPeek(false);
|
|
|
|
|
}}
|
2023-09-23 14:14:11 +03:00
|
|
|
|
data-read-more="Read more →"
|
2023-03-21 19:09:36 +03:00
|
|
|
|
>
|
|
|
|
|
<Status status={status} instance={instance} size="s" readOnly />
|
|
|
|
|
</Link>
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-25 08:03:26 +03:00
|
|
|
|
const QuoteStatuses = memo(({ id, instance, level = 0 }) => {
|
2023-05-19 20:06:16 +03:00
|
|
|
|
if (!id || !instance) return;
|
2023-04-22 19:55:47 +03:00
|
|
|
|
const snapStates = useSnapshot(states);
|
|
|
|
|
const sKey = statusKey(id, instance);
|
|
|
|
|
const quotes = snapStates.statusQuotes[sKey];
|
2023-05-17 11:13:49 +03:00
|
|
|
|
const uniqueQuotes = quotes?.filter(
|
|
|
|
|
(q, i, arr) => arr.findIndex((q2) => q2.url === q.url) === i,
|
|
|
|
|
);
|
2023-04-22 19:55:47 +03:00
|
|
|
|
|
2023-05-17 11:13:49 +03:00
|
|
|
|
if (!uniqueQuotes?.length) return;
|
2023-04-25 08:03:26 +03:00
|
|
|
|
if (level > 2) return;
|
2023-04-22 19:55:47 +03:00
|
|
|
|
|
2023-05-17 11:13:49 +03:00
|
|
|
|
return uniqueQuotes.map((q) => {
|
2023-04-22 19:55:47 +03:00
|
|
|
|
return (
|
2024-04-22 11:42:12 +03:00
|
|
|
|
<LazyShazam id={q.instance + q.id}>
|
2024-03-26 11:35:02 +03:00
|
|
|
|
<Link
|
|
|
|
|
key={q.instance + q.id}
|
|
|
|
|
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}
|
|
|
|
|
class="status-card-link"
|
|
|
|
|
data-read-more="Read more →"
|
|
|
|
|
>
|
|
|
|
|
<Status
|
|
|
|
|
statusID={q.id}
|
|
|
|
|
instance={q.instance}
|
|
|
|
|
size="s"
|
|
|
|
|
quoted={level + 1}
|
|
|
|
|
enableCommentHint
|
|
|
|
|
/>
|
|
|
|
|
</Link>
|
|
|
|
|
</LazyShazam>
|
2023-04-22 19:55:47 +03:00
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-01-06 07:31:25 +03:00
|
|
|
|
export default memo(Status, (oldProps, newProps) => {
|
|
|
|
|
// Shallow equal all props except 'status'
|
|
|
|
|
// This will be pure static until status ID changes
|
|
|
|
|
const { status, ...restOldProps } = oldProps;
|
|
|
|
|
const { status: newStatus, ...restNewProps } = newProps;
|
|
|
|
|
return (
|
|
|
|
|
status?.id === newStatus?.id && shallowEqual(restOldProps, restNewProps)
|
|
|
|
|
);
|
|
|
|
|
});
|