2022-12-10 12:14:48 +03:00
|
|
|
|
import './status.css';
|
|
|
|
|
|
2023-03-02 10:15:49 +03:00
|
|
|
|
import {
|
|
|
|
|
ControlledMenu,
|
|
|
|
|
Menu,
|
|
|
|
|
MenuDivider,
|
|
|
|
|
MenuHeader,
|
|
|
|
|
MenuItem,
|
|
|
|
|
} from '@szhsin/react-menu';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import mem from 'mem';
|
2023-02-23 11:45:53 +03:00
|
|
|
|
import pThrottle from 'p-throttle';
|
2023-01-07 15:26:23 +03:00
|
|
|
|
import { memo } from 'preact/compat';
|
2023-02-05 19:17:19 +03:00
|
|
|
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
2022-12-28 14:43:02 +03:00
|
|
|
|
import 'swiped-events';
|
2023-03-07 19:01:51 +03:00
|
|
|
|
import { useLongPress } from 'use-long-press';
|
2022-12-17 16:06:51 +03:00
|
|
|
|
import useResizeObserver from 'use-resize-observer';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import { useSnapshot } from 'valtio';
|
|
|
|
|
|
2022-12-11 16:22:22 +03:00
|
|
|
|
import Loader from '../components/loader';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import Modal from '../components/modal';
|
|
|
|
|
import NameText from '../components/name-text';
|
2023-02-05 19:17:19 +03:00
|
|
|
|
import { api } from '../utils/api';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import enhanceContent from '../utils/enhance-content';
|
2023-03-07 17:38:06 +03:00
|
|
|
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
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-03-01 15:07:22 +03:00
|
|
|
|
import niceDateTime from '../utils/nice-date-time';
|
2022-12-10 12:14:48 +03:00
|
|
|
|
import shortenNumber from '../utils/shorten-number';
|
2023-02-26 19:55:04 +03:00
|
|
|
|
import showToast from '../utils/show-toast';
|
2023-02-06 11:35:03 +03:00
|
|
|
|
import states, { saveStatus, statusKey } from '../utils/states';
|
2022-12-12 16:54:31 +03:00
|
|
|
|
import store from '../utils/store';
|
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-02-26 19:55:04 +03:00
|
|
|
|
import MenuLink from './MenuLink';
|
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-02-23 11:45:53 +03:00
|
|
|
|
const throttle = pThrottle({
|
|
|
|
|
limit: 1,
|
|
|
|
|
interval: 1000,
|
|
|
|
|
});
|
|
|
|
|
|
2023-02-05 19:17:19 +03:00
|
|
|
|
function fetchAccount(id, masto) {
|
2023-01-28 13:52:18 +03:00
|
|
|
|
try {
|
|
|
|
|
return masto.v1.accounts.fetch(id);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return Promise.reject(e);
|
|
|
|
|
}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
|
|
|
|
const memFetchAccount = mem(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-02-27 06:21:11 +03:00
|
|
|
|
direct: 'Direct',
|
2023-02-26 19:55:04 +03:00
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
|
withinContext,
|
|
|
|
|
size = 'm',
|
|
|
|
|
skeleton,
|
|
|
|
|
readOnly,
|
2023-02-18 20:10:06 +03:00
|
|
|
|
contentTextWeight,
|
2023-03-07 17:38:06 +03:00
|
|
|
|
enableTranslate,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}) {
|
|
|
|
|
if (skeleton) {
|
2022-12-10 12:14:48 +03:00
|
|
|
|
return (
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="status skeleton">
|
2023-02-17 20:45:44 +03:00
|
|
|
|
<Avatar size="xxl" />
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="container">
|
2023-02-11 16:09:36 +03:00
|
|
|
|
<div class="meta">███ ████████</div>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="content-container">
|
|
|
|
|
<div class="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-02-06 11:35:03 +03:00
|
|
|
|
const sKey = statusKey(statusID, 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];
|
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,
|
|
|
|
|
},
|
|
|
|
|
id,
|
|
|
|
|
repliesCount,
|
|
|
|
|
reblogged,
|
|
|
|
|
reblogsCount,
|
|
|
|
|
favourited,
|
|
|
|
|
favouritesCount,
|
|
|
|
|
bookmarked,
|
|
|
|
|
poll,
|
|
|
|
|
muted,
|
|
|
|
|
sensitive,
|
|
|
|
|
spoilerText,
|
|
|
|
|
visibility, // public, unlisted, private, direct
|
|
|
|
|
language,
|
|
|
|
|
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-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,
|
2022-12-18 16:10:05 +03:00
|
|
|
|
} = status;
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2023-01-07 15:26:23 +03:00
|
|
|
|
console.debug('RENDER Status', id, status?.account.displayName);
|
|
|
|
|
|
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
|
|
|
|
const isSelf = useMemo(() => {
|
|
|
|
|
const currentAccount = store.session.get('currentAccount');
|
|
|
|
|
return currentAccount && currentAccount === accountId;
|
|
|
|
|
}, [accountId]);
|
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-01-07 15:26:23 +03:00
|
|
|
|
const showSpoiler = !!snapStates.spoilers[id] || false;
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const debugHover = (e) => {
|
|
|
|
|
if (e.shiftKey) {
|
|
|
|
|
console.log(status);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (reblog) {
|
|
|
|
|
return (
|
|
|
|
|
<div class="status-reblog" onMouseEnter={debugHover}>
|
|
|
|
|
<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 />{' '}
|
|
|
|
|
boosted
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</div>
|
2023-02-18 20:10:06 +03:00
|
|
|
|
<Status
|
|
|
|
|
status={reblog}
|
|
|
|
|
instance={instance}
|
|
|
|
|
size={size}
|
|
|
|
|
contentTextWeight={contentTextWeight}
|
|
|
|
|
/>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
|
2023-03-07 17:38:06 +03:00
|
|
|
|
const [forceTranslate, setForceTranslate] = useState(false);
|
|
|
|
|
const targetLanguage = getTranslateTargetLanguage(true);
|
|
|
|
|
if (!snapStates.settings.contentTranslation) enableTranslate = false;
|
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const [showEdited, setShowEdited] = useState(false);
|
|
|
|
|
|
|
|
|
|
const spoilerContentRef = useRef(null);
|
|
|
|
|
useResizeObserver({
|
|
|
|
|
ref: spoilerContentRef,
|
|
|
|
|
onResize: () => {
|
|
|
|
|
if (spoilerContentRef.current) {
|
|
|
|
|
const { scrollHeight, clientHeight } = spoilerContentRef.current;
|
|
|
|
|
spoilerContentRef.current.classList.toggle(
|
|
|
|
|
'truncated',
|
|
|
|
|
scrollHeight > clientHeight,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const contentRef = useRef(null);
|
|
|
|
|
useResizeObserver({
|
|
|
|
|
ref: contentRef,
|
|
|
|
|
onResize: () => {
|
|
|
|
|
if (contentRef.current) {
|
|
|
|
|
const { scrollHeight, clientHeight } = contentRef.current;
|
|
|
|
|
contentRef.current.classList.toggle(
|
|
|
|
|
'truncated',
|
|
|
|
|
scrollHeight > clientHeight,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
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-02-05 19:17:19 +03:00
|
|
|
|
const unauthInteractionErrorMessage = `Sorry, your current logged-in instance can't interact with this status from another instance.`;
|
|
|
|
|
|
2023-02-21 18:59:34 +03:00
|
|
|
|
const textWeight = () =>
|
2023-03-06 13:20:49 +03:00
|
|
|
|
Math.max(
|
|
|
|
|
Math.round((spoilerText.length + htmlContentLength(content)) / 140) || 1,
|
|
|
|
|
1,
|
|
|
|
|
);
|
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
|
|
|
|
|
|
|
|
|
const isSizeLarge = size === 'l';
|
|
|
|
|
// TODO: if visibility = private, only can boost own statuses
|
|
|
|
|
const canBoost = authenticated && visibility !== 'direct';
|
|
|
|
|
|
|
|
|
|
const replyStatus = () => {
|
|
|
|
|
if (!sameInstance || !authenticated) {
|
|
|
|
|
return alert(unauthInteractionErrorMessage);
|
|
|
|
|
}
|
|
|
|
|
states.showCompose = {
|
|
|
|
|
replyToStatus: status,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const boostStatus = async () => {
|
|
|
|
|
if (!sameInstance || !authenticated) {
|
|
|
|
|
return alert(unauthInteractionErrorMessage);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
if (!reblogged) {
|
|
|
|
|
const yes = confirm('Boost this post?');
|
|
|
|
|
if (!yes) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Optimistic
|
|
|
|
|
states.statuses[sKey] = {
|
|
|
|
|
...status,
|
|
|
|
|
reblogged: !reblogged,
|
|
|
|
|
reblogsCount: reblogsCount + (reblogged ? -1 : 1),
|
|
|
|
|
};
|
|
|
|
|
if (reblogged) {
|
|
|
|
|
const newStatus = await masto.v1.statuses.unreblog(id);
|
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
} else {
|
|
|
|
|
const newStatus = await masto.v1.statuses.reblog(id);
|
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
// Revert optimistism
|
|
|
|
|
states.statuses[sKey] = status;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const favouriteStatus = async () => {
|
|
|
|
|
if (!sameInstance || !authenticated) {
|
|
|
|
|
return alert(unauthInteractionErrorMessage);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// Optimistic
|
|
|
|
|
states.statuses[sKey] = {
|
|
|
|
|
...status,
|
|
|
|
|
favourited: !favourited,
|
|
|
|
|
favouritesCount: favouritesCount + (favourited ? -1 : 1),
|
|
|
|
|
};
|
|
|
|
|
if (favourited) {
|
|
|
|
|
const newStatus = await masto.v1.statuses.unfavourite(id);
|
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
} else {
|
|
|
|
|
const newStatus = await masto.v1.statuses.favourite(id);
|
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
// Revert optimistism
|
|
|
|
|
states.statuses[sKey] = status;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const bookmarkStatus = async () => {
|
|
|
|
|
if (!sameInstance || !authenticated) {
|
|
|
|
|
return alert(unauthInteractionErrorMessage);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// Optimistic
|
|
|
|
|
states.statuses[sKey] = {
|
|
|
|
|
...status,
|
|
|
|
|
bookmarked: !bookmarked,
|
|
|
|
|
};
|
|
|
|
|
if (bookmarked) {
|
|
|
|
|
const newStatus = await masto.v1.statuses.unbookmark(id);
|
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
} else {
|
|
|
|
|
const newStatus = await masto.v1.statuses.bookmark(id);
|
|
|
|
|
saveStatus(newStatus, instance);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(e);
|
|
|
|
|
// Revert optimistism
|
|
|
|
|
states.statuses[sKey] = status;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-02-28 10:45:38 +03:00
|
|
|
|
const menuInstanceRef = useRef();
|
2023-02-26 19:55:04 +03:00
|
|
|
|
const StatusMenuItems = (
|
|
|
|
|
<>
|
|
|
|
|
{!isSizeLarge && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuHeader>
|
|
|
|
|
<span class="ib">
|
|
|
|
|
<Icon icon={visibilityIconsMap[visibility]} size="s" />{' '}
|
|
|
|
|
<span>{visibilityText[visibility]}</span>
|
|
|
|
|
</span>{' '}
|
|
|
|
|
<span class="ib">
|
|
|
|
|
{repliesCount > 0 && (
|
|
|
|
|
<span>
|
|
|
|
|
<Icon icon="reply" alt="Replies" size="s" />{' '}
|
|
|
|
|
<span>{shortenNumber(repliesCount)}</span>
|
|
|
|
|
</span>
|
|
|
|
|
)}{' '}
|
|
|
|
|
{reblogsCount > 0 && (
|
|
|
|
|
<span>
|
|
|
|
|
<Icon icon="rocket" alt="Boosts" size="s" />{' '}
|
|
|
|
|
<span>{shortenNumber(reblogsCount)}</span>
|
|
|
|
|
</span>
|
|
|
|
|
)}{' '}
|
|
|
|
|
{favouritesCount > 0 && (
|
|
|
|
|
<span>
|
|
|
|
|
<Icon icon="heart" alt="Favourites" size="s" />{' '}
|
|
|
|
|
<span>{shortenNumber(favouritesCount)}</span>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<br />
|
|
|
|
|
{createdDateText}
|
|
|
|
|
</MenuHeader>
|
|
|
|
|
<MenuLink to={instance ? `/${instance}/s/${id}` : `/s/${id}`}>
|
|
|
|
|
<Icon icon="arrow-right" />
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<span>View post by @{username || acct}</span>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
</MenuLink>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{!!editedAt && (
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowEdited(id);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="history" />
|
|
|
|
|
<span>
|
|
|
|
|
Show Edit History
|
|
|
|
|
<br />
|
|
|
|
|
<small class="more-insignificant">Edited: {editedDateText}</small>
|
|
|
|
|
</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
|
|
|
|
{(!isSizeLarge || !!editedAt) && <MenuDivider />}
|
|
|
|
|
{!isSizeLarge && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuItem onClick={replyStatus}>
|
|
|
|
|
<Icon icon="reply" />
|
|
|
|
|
<span>Reply</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
{canBoost && (
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
try {
|
|
|
|
|
await boostStatus();
|
|
|
|
|
if (!isSizeLarge)
|
|
|
|
|
showToast(reblogged ? 'Unboosted' : 'Boosted');
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<Icon
|
|
|
|
|
icon="rocket"
|
|
|
|
|
style={{
|
|
|
|
|
color: reblogged && 'var(--reblog-color)',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<span>{reblogged ? 'Unboost' : 'Boost…'}</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
try {
|
|
|
|
|
favouriteStatus();
|
|
|
|
|
if (!isSizeLarge)
|
|
|
|
|
showToast(favourited ? 'Unfavourited' : 'Favourited');
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<Icon
|
|
|
|
|
icon="heart"
|
|
|
|
|
style={{
|
|
|
|
|
color: favourited && 'var(--favourite-color)',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<span>{favourited ? 'Unfavourite' : 'Favourite'}</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
try {
|
|
|
|
|
bookmarkStatus();
|
|
|
|
|
if (!isSizeLarge)
|
|
|
|
|
showToast(bookmarked ? 'Unbookmarked' : 'Bookmarked');
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-03-09 16:51:50 +03:00
|
|
|
|
<Icon
|
|
|
|
|
icon="bookmark"
|
|
|
|
|
style={{
|
|
|
|
|
color: bookmarked && 'var(--favourite-color)',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<span>{bookmarked ? 'Unbookmark' : 'Bookmark'}</span>
|
|
|
|
|
</MenuItem>
|
2023-03-09 16:51:50 +03:00
|
|
|
|
{enableTranslate && (
|
|
|
|
|
<MenuItem
|
|
|
|
|
disabled={forceTranslate}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setForceTranslate(true);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="translate" />
|
|
|
|
|
<span>Translate</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
)}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<MenuDivider />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<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>
|
2023-03-09 16:51:50 +03:00
|
|
|
|
{navigator?.share &&
|
|
|
|
|
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>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
{isSelf && (
|
|
|
|
|
<>
|
|
|
|
|
<MenuDivider />
|
|
|
|
|
<MenuItem
|
|
|
|
|
onClick={() => {
|
|
|
|
|
states.showCompose = {
|
|
|
|
|
editStatus: status,
|
|
|
|
|
};
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="pencil" />
|
|
|
|
|
<span>Edit</span>
|
|
|
|
|
</MenuItem>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
|
2023-03-07 19:01:51 +03:00
|
|
|
|
const contextMenuRef = useRef();
|
2023-03-02 10:15:49 +03:00
|
|
|
|
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
|
|
|
|
const [contextMenuAnchorPoint, setContextMenuAnchorPoint] = useState({
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
});
|
2023-03-07 19:01:51 +03:00
|
|
|
|
const bindLongPress = useLongPress(
|
|
|
|
|
(e) => {
|
|
|
|
|
const { clientX, clientY } = e.touches?.[0] || e;
|
2023-03-07 17:40:29 +03:00
|
|
|
|
setContextMenuAnchorPoint({
|
2023-03-07 19:01:51 +03:00
|
|
|
|
x: clientX,
|
|
|
|
|
y: clientY,
|
2023-03-07 17:40:29 +03:00
|
|
|
|
});
|
|
|
|
|
setIsContextMenuOpen(true);
|
2023-03-07 19:01:51 +03:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
captureEvent: true,
|
|
|
|
|
detect: 'touch',
|
|
|
|
|
cancelOnMovement: true,
|
|
|
|
|
},
|
|
|
|
|
);
|
2023-03-02 10:15:49 +03:00
|
|
|
|
|
2022-12-10 12:14:48 +03:00
|
|
|
|
return (
|
2022-12-29 11:12:09 +03:00
|
|
|
|
<article
|
2022-12-30 15:37:57 +03:00
|
|
|
|
ref={statusRef}
|
|
|
|
|
tabindex="-1"
|
2022-12-18 16:10:05 +03:00
|
|
|
|
class={`status ${
|
|
|
|
|
!withinContext && inReplyToAccount ? 'status-reply-to' : ''
|
2023-02-17 05:12:59 +03:00
|
|
|
|
} visibility-${visibility} ${_pinned ? 'status-pinned' : ''} ${
|
2022-12-18 16:10:05 +03:00
|
|
|
|
{
|
|
|
|
|
s: 'small',
|
|
|
|
|
m: 'medium',
|
|
|
|
|
l: 'large',
|
|
|
|
|
}[size]
|
|
|
|
|
}`}
|
|
|
|
|
onMouseEnter={debugHover}
|
2023-03-02 10:15:49 +03:00
|
|
|
|
onContextMenu={(e) => {
|
2023-03-02 15:37:40 +03:00
|
|
|
|
if (size === 'l') return;
|
2023-03-02 10:15:49 +03:00
|
|
|
|
if (e.metaKey) return;
|
2023-03-09 16:51:50 +03:00
|
|
|
|
console.log('context menu', e);
|
|
|
|
|
const link = e.target.closest('a');
|
|
|
|
|
if (link && /^https?:\/\//.test(link.getAttribute('href'))) return;
|
2023-03-02 10:15:49 +03:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
setContextMenuAnchorPoint({
|
|
|
|
|
x: e.clientX,
|
|
|
|
|
y: e.clientY,
|
|
|
|
|
});
|
|
|
|
|
setIsContextMenuOpen(true);
|
|
|
|
|
}}
|
2023-03-07 19:01:51 +03:00
|
|
|
|
{...bindLongPress()}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
>
|
2023-03-02 15:37:40 +03:00
|
|
|
|
{size !== 'l' && (
|
|
|
|
|
<ControlledMenu
|
2023-03-07 19:01:51 +03:00
|
|
|
|
ref={contextMenuRef}
|
2023-03-02 15:37:40 +03:00
|
|
|
|
state={isContextMenuOpen ? 'open' : undefined}
|
|
|
|
|
anchorPoint={contextMenuAnchorPoint}
|
|
|
|
|
direction="right"
|
|
|
|
|
onClose={() => setIsContextMenuOpen(false)}
|
|
|
|
|
portal={{
|
|
|
|
|
target: document.body,
|
|
|
|
|
}}
|
|
|
|
|
containerProps={{
|
|
|
|
|
style: {
|
|
|
|
|
// Higher than the backdrop
|
|
|
|
|
zIndex: 1001,
|
|
|
|
|
},
|
2023-03-07 19:01:51 +03:00
|
|
|
|
onClick: () => {
|
|
|
|
|
contextMenuRef.current?.closeMenu?.();
|
|
|
|
|
},
|
2023-03-02 15:37:40 +03:00
|
|
|
|
}}
|
|
|
|
|
overflow="auto"
|
|
|
|
|
boundingBoxPadding="8 8 8 8"
|
|
|
|
|
unmountOnClose
|
|
|
|
|
>
|
|
|
|
|
{StatusMenuItems}
|
|
|
|
|
</ControlledMenu>
|
|
|
|
|
)}
|
2022-12-20 15:17:38 +03:00
|
|
|
|
{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" />}
|
2023-02-17 05:12:59 +03:00
|
|
|
|
{_pinned && <Icon class="pin" icon="pin" size="s" />}
|
2022-12-20 15:17:38 +03:00
|
|
|
|
</div>
|
|
|
|
|
)}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
{size !== 's' && (
|
|
|
|
|
<a
|
2023-02-21 09:29:25 +03:00
|
|
|
|
href={accountURL}
|
2022-12-29 11:11:58 +03:00
|
|
|
|
tabindex="-1"
|
2022-12-18 16:10:05 +03:00
|
|
|
|
// target="_blank"
|
|
|
|
|
title={`@${acct}`}
|
|
|
|
|
onClick={(e) => {
|
2022-12-10 12:14:48 +03:00
|
|
|
|
e.preventDefault();
|
2022-12-18 16:10:05 +03:00
|
|
|
|
e.stopPropagation();
|
2023-02-05 19:17:19 +03:00
|
|
|
|
states.showAccount = {
|
|
|
|
|
account: status.account,
|
|
|
|
|
instance,
|
|
|
|
|
};
|
2022-12-10 12:14:48 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<Avatar url={avatarStatic} size="xxl" />
|
|
|
|
|
</a>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
)}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="meta">
|
2022-12-22 05:35:39 +03:00
|
|
|
|
{/* <span> */}
|
|
|
|
|
<NameText
|
|
|
|
|
account={status.account}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
instance={instance}
|
2022-12-22 05:35:39 +03:00
|
|
|
|
showAvatar={size === 's'}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
showAcct={isSizeLarge}
|
2022-12-22 05:35:39 +03:00
|
|
|
|
/>
|
|
|
|
|
{/* {inReplyToAccount && !withinContext && size !== 's' && (
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<>
|
|
|
|
|
{' '}
|
|
|
|
|
<span class="ib">
|
|
|
|
|
<Icon icon="arrow-right" class="arrow" />{' '}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
<NameText account={inReplyToAccount} instance={instance} short />
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</span>
|
|
|
|
|
</>
|
2022-12-22 05:35:39 +03:00
|
|
|
|
)} */}
|
|
|
|
|
{/* </span> */}{' '}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
{size !== 'l' &&
|
2023-02-21 09:29:25 +03:00
|
|
|
|
(url ? (
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<Menu
|
2023-02-28 10:45:38 +03:00
|
|
|
|
instanceRef={menuInstanceRef}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
portal={{
|
2023-03-01 10:44:58 +03:00
|
|
|
|
target: document.body,
|
2023-02-26 19:55:04 +03:00
|
|
|
|
}}
|
2023-02-28 11:56:30 +03:00
|
|
|
|
containerProps={{
|
2023-03-01 10:44:58 +03:00
|
|
|
|
style: {
|
|
|
|
|
// Higher than the backdrop
|
|
|
|
|
zIndex: 1001,
|
|
|
|
|
},
|
2023-02-28 11:56:30 +03:00
|
|
|
|
onClick: () => {
|
|
|
|
|
menuInstanceRef.current?.closeMenu?.();
|
|
|
|
|
},
|
|
|
|
|
}}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
align="end"
|
|
|
|
|
offsetY={4}
|
|
|
|
|
overflow="auto"
|
|
|
|
|
viewScroll="close"
|
|
|
|
|
boundingBoxPadding="8 8 8 8"
|
2023-02-28 10:45:38 +03:00
|
|
|
|
unmountOnClose
|
2023-03-01 14:17:04 +03:00
|
|
|
|
menuButton={({ open }) => (
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<Link
|
|
|
|
|
to={instance ? `/${instance}/s/${id}` : `/s/${id}`}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
}}
|
2023-03-01 14:17:04 +03:00
|
|
|
|
class={`time ${open ? 'is-open' : ''}`}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
>
|
|
|
|
|
<Icon
|
|
|
|
|
icon={visibilityIconsMap[visibility]}
|
|
|
|
|
alt={visibilityText[visibility]}
|
|
|
|
|
size="s"
|
|
|
|
|
/>{' '}
|
|
|
|
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
|
|
|
|
</Link>
|
2023-03-01 14:17:04 +03:00
|
|
|
|
)}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
{StatusMenuItems}
|
|
|
|
|
</Menu>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
) : (
|
|
|
|
|
<span class="time">
|
|
|
|
|
<Icon
|
|
|
|
|
icon={visibilityIconsMap[visibility]}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
alt={visibilityText[visibility]}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
size="s"
|
|
|
|
|
/>{' '}
|
2023-01-05 05:50:27 +03:00
|
|
|
|
<RelativeTime datetime={createdAtDate} format="micro" />
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2023-01-23 15:34:53 +03:00
|
|
|
|
{!withinContext && (
|
2023-01-10 14:59:02 +03:00
|
|
|
|
<>
|
|
|
|
|
{inReplyToAccountId === status.account?.id ||
|
2023-02-19 16:16:23 +03:00
|
|
|
|
!!snapStates.statusThreadNumber[sKey] ? (
|
2023-01-10 14:59:02 +03:00
|
|
|
|
<div class="status-thread-badge">
|
|
|
|
|
<Icon icon="thread" size="s" />
|
|
|
|
|
Thread
|
2023-02-19 16:16:23 +03:00
|
|
|
|
{snapStates.statusThreadNumber[sKey]
|
|
|
|
|
? ` ${snapStates.statusThreadNumber[sKey]}/X`
|
2023-01-10 14:59:02 +03:00
|
|
|
|
: ''}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
!!inReplyToId &&
|
|
|
|
|
!!inReplyToAccount &&
|
|
|
|
|
(!!spoilerText ||
|
|
|
|
|
!mentions.find((mention) => {
|
|
|
|
|
return mention.id === inReplyToAccountId;
|
|
|
|
|
})) && (
|
|
|
|
|
<div class="status-reply-badge">
|
|
|
|
|
<Icon icon="reply" />{' '}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
<NameText
|
|
|
|
|
account={inReplyToAccount}
|
|
|
|
|
instance={instance}
|
|
|
|
|
short
|
|
|
|
|
/>
|
2022-12-23 20:22:25 +03:00
|
|
|
|
</div>
|
2023-01-10 14:59:02 +03:00
|
|
|
|
)
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div
|
2023-02-07 07:56:26 +03:00
|
|
|
|
class={`content-container ${
|
|
|
|
|
spoilerText || sensitive ? 'has-spoiler' : ''
|
|
|
|
|
} ${showSpoiler ? 'show-spoiler' : ''}`}
|
2023-02-21 18:59:34 +03:00
|
|
|
|
data-content-text-weight={contentTextWeight ? textWeight() : null}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
style={
|
2023-02-26 19:55:04 +03:00
|
|
|
|
(isSizeLarge || contentTextWeight) && {
|
2023-02-21 18:59:34 +03:00
|
|
|
|
'--content-text-weight': textWeight(),
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
>
|
2023-02-05 19:19:22 +03:00
|
|
|
|
{!!spoilerText && (
|
2022-12-14 16:48:17 +03:00
|
|
|
|
<>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div
|
|
|
|
|
class="content"
|
|
|
|
|
lang={language}
|
|
|
|
|
ref={spoilerContentRef}
|
|
|
|
|
data-read-more={readMoreText}
|
|
|
|
|
>
|
|
|
|
|
<p>{spoilerText}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
2023-01-28 17:34:36 +03:00
|
|
|
|
class={`light spoiler ${showSpoiler ? 'spoiling' : ''}`}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
type="button"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2022-12-20 14:14:50 +03:00
|
|
|
|
if (showSpoiler) {
|
2023-01-07 15:26:23 +03:00
|
|
|
|
delete states.spoilers[id];
|
2022-12-20 14:14:50 +03:00
|
|
|
|
} else {
|
2023-01-07 15:26:23 +03:00
|
|
|
|
states.spoilers[id] = true;
|
2022-12-20 14:14:50 +03:00
|
|
|
|
}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} />{' '}
|
|
|
|
|
{showSpoiler ? 'Show less' : 'Show more'}
|
|
|
|
|
</button>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<div
|
|
|
|
|
class="content"
|
2022-12-17 16:06:51 +03:00
|
|
|
|
lang={language}
|
|
|
|
|
ref={contentRef}
|
|
|
|
|
data-read-more={readMoreText}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
onClick={handleContentLinks({ mentions, instance })}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
dangerouslySetInnerHTML={{
|
|
|
|
|
__html: enhanceContent(content, {
|
|
|
|
|
emojis,
|
2022-12-11 04:28:02 +03:00
|
|
|
|
postEnhanceDOM: (dom) => {
|
2023-02-23 11:45:53 +03:00
|
|
|
|
// Remove target="_blank" from links
|
2022-12-11 04:28:02 +03:00
|
|
|
|
dom
|
|
|
|
|
.querySelectorAll('a.u-url[target="_blank"]')
|
|
|
|
|
.forEach((a) => {
|
2023-01-19 10:51:54 +03:00
|
|
|
|
if (!/http/i.test(a.innerText.trim())) {
|
|
|
|
|
a.removeAttribute('target');
|
|
|
|
|
}
|
2022-12-11 04:28:02 +03:00
|
|
|
|
});
|
2023-02-23 11:45:53 +03:00
|
|
|
|
// Unfurl Mastodon links
|
|
|
|
|
dom
|
|
|
|
|
.querySelectorAll(
|
|
|
|
|
'a[href]:not(.u-url):not(.mention):not(.hashtag)',
|
|
|
|
|
)
|
|
|
|
|
.forEach((a) => {
|
|
|
|
|
if (isMastodonLinkMaybe(a.href)) {
|
|
|
|
|
unfurlMastodonLink(currentInstance, a.href).then(() => {
|
|
|
|
|
a.removeAttribute('target');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2022-12-11 04:28:02 +03:00
|
|
|
|
},
|
2022-12-10 12:14:48 +03:00
|
|
|
|
}),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2022-12-21 14:29:37 +03:00
|
|
|
|
{!!poll && (
|
|
|
|
|
<Poll
|
2022-12-27 13:09:23 +03:00
|
|
|
|
lang={language}
|
2022-12-21 14:29:37 +03:00
|
|
|
|
poll={poll}
|
2023-02-19 09:49:53 +03:00
|
|
|
|
readOnly={readOnly || !sameInstance || !authenticated}
|
2022-12-21 14:29:37 +03:00
|
|
|
|
onUpdate={(newPoll) => {
|
2023-02-06 11:35:03 +03:00
|
|
|
|
states.statuses[sKey].poll = newPoll;
|
2022-12-21 14:29:37 +03:00
|
|
|
|
}}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
refresh={() => {
|
|
|
|
|
return masto.v1.polls
|
|
|
|
|
.fetch(poll.id)
|
|
|
|
|
.then((pollResponse) => {
|
2023-02-06 11:35:03 +03:00
|
|
|
|
states.statuses[sKey].poll = pollResponse;
|
2023-02-05 19:17:19 +03:00
|
|
|
|
})
|
|
|
|
|
.catch((e) => {}); // Silently fail
|
|
|
|
|
}}
|
|
|
|
|
votePoll={(choices) => {
|
|
|
|
|
return masto.v1.polls
|
|
|
|
|
.vote(poll.id, {
|
|
|
|
|
choices,
|
|
|
|
|
})
|
|
|
|
|
.then((pollResponse) => {
|
2023-02-06 11:35:03 +03:00
|
|
|
|
states.statuses[sKey].poll = pollResponse;
|
2023-02-05 19:17:19 +03:00
|
|
|
|
})
|
|
|
|
|
.catch((e) => {}); // Silently fail
|
|
|
|
|
}}
|
2022-12-21 14:29:37 +03:00
|
|
|
|
/>
|
|
|
|
|
)}
|
2023-03-07 17:38:06 +03:00
|
|
|
|
{((enableTranslate &&
|
|
|
|
|
!!content.trim() &&
|
|
|
|
|
language &&
|
|
|
|
|
language !== targetLanguage) ||
|
|
|
|
|
forceTranslate) && (
|
|
|
|
|
<TranslationBlock
|
|
|
|
|
forceTranslate={forceTranslate}
|
|
|
|
|
sourceLanguage={language}
|
|
|
|
|
text={
|
|
|
|
|
(spoilerText ? `${spoilerText}\n\n` : '') +
|
|
|
|
|
getHTMLText(content) +
|
|
|
|
|
(poll?.options?.length
|
|
|
|
|
? `\n\nPoll:\n${poll.options
|
|
|
|
|
.map((option) => `- ${option.title}`)
|
|
|
|
|
.join('\n')}`
|
|
|
|
|
: '')
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
|
|
|
|
<button
|
2023-01-28 17:34:36 +03:00
|
|
|
|
class={`plain spoiler ${showSpoiler ? 'spoiling' : ''}`}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
type="button"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2022-12-20 14:14:50 +03:00
|
|
|
|
if (showSpoiler) {
|
2023-01-07 15:26:23 +03:00
|
|
|
|
delete states.spoilers[id];
|
2022-12-20 14:14:50 +03:00
|
|
|
|
} else {
|
2023-01-07 15:26:23 +03:00
|
|
|
|
states.spoilers[id] = true;
|
2022-12-20 14:14:50 +03:00
|
|
|
|
}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon={showSpoiler ? 'eye-open' : 'eye-close'} /> Sensitive
|
|
|
|
|
content
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2022-12-12 11:25:55 +03:00
|
|
|
|
{!!mediaAttachments.length && (
|
2022-12-22 05:47:45 +03:00
|
|
|
|
<div
|
2023-01-23 15:35:15 +03:00
|
|
|
|
class={`media-container media-eq${mediaAttachments.length} ${
|
2022-12-22 05:47:45 +03:00
|
|
|
|
mediaAttachments.length > 2 ? 'media-gt2' : ''
|
|
|
|
|
} ${mediaAttachments.length > 4 ? 'media-gt4' : ''}`}
|
|
|
|
|
>
|
2022-12-28 05:51:57 +03:00
|
|
|
|
{mediaAttachments
|
2023-02-26 19:55:04 +03:00
|
|
|
|
.slice(0, isSizeLarge ? undefined : 4)
|
2022-12-28 05:51:57 +03:00
|
|
|
|
.map((media, i) => (
|
|
|
|
|
<Media
|
|
|
|
|
key={media.id}
|
|
|
|
|
media={media}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
autoAnimate={isSizeLarge}
|
2022-12-28 05:51:57 +03:00
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2023-01-29 10:23:53 +03:00
|
|
|
|
states.showMediaModal = {
|
|
|
|
|
mediaAttachments,
|
|
|
|
|
index: i,
|
2023-02-05 19:17:19 +03:00
|
|
|
|
instance,
|
2023-01-29 10:23:53 +03:00
|
|
|
|
statusID: readOnly ? null : id,
|
|
|
|
|
};
|
2022-12-28 05:51:57 +03:00
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!!card &&
|
2023-01-05 08:29:44 +03:00
|
|
|
|
!sensitive &&
|
|
|
|
|
!spoilerText &&
|
2023-01-05 10:28:04 +03:00
|
|
|
|
!poll &&
|
2023-02-23 11:45:53 +03:00
|
|
|
|
!mediaAttachments.length && (
|
|
|
|
|
<Card card={card} instance={currentInstance} />
|
|
|
|
|
)}
|
2022-12-10 12:14:48 +03:00
|
|
|
|
</div>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
{isSizeLarge && (
|
2022-12-11 16:22:22 +03:00
|
|
|
|
<>
|
|
|
|
|
<div class="extra-meta">
|
|
|
|
|
<Icon icon={visibilityIconsMap[visibility]} alt={visibility} />{' '}
|
2023-02-21 09:29:25 +03:00
|
|
|
|
<a href={url} target="_blank">
|
2022-12-11 16:22:22 +03:00
|
|
|
|
<time class="created" datetime={createdAtDate.toISOString()}>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
{createdDateText}
|
2022-12-11 16:22:22 +03:00
|
|
|
|
</time>
|
|
|
|
|
</a>
|
|
|
|
|
{editedAt && (
|
2022-12-10 12:14:48 +03:00
|
|
|
|
<>
|
|
|
|
|
{' '}
|
2022-12-11 16:22:22 +03:00
|
|
|
|
• <Icon icon="pencil" alt="Edited" />{' '}
|
|
|
|
|
<time
|
|
|
|
|
class="edited"
|
|
|
|
|
datetime={editedAtDate.toISOString()}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowEdited(id);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
{editedDateText}
|
2022-12-11 16:22:22 +03:00
|
|
|
|
</time>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
</>
|
|
|
|
|
)}
|
2022-12-11 16:22:22 +03:00
|
|
|
|
</div>
|
|
|
|
|
<div class="actions">
|
2022-12-19 08:38:16 +03:00
|
|
|
|
<div class="action has-count">
|
|
|
|
|
<StatusButton
|
|
|
|
|
title="Reply"
|
|
|
|
|
alt="Comments"
|
|
|
|
|
class="reply-button"
|
|
|
|
|
icon="comment"
|
|
|
|
|
count={repliesCount}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
onClick={replyStatus}
|
2022-12-19 08:38:16 +03:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
{canBoost && (
|
2022-12-19 08:38:16 +03:00
|
|
|
|
<div class="action has-count">
|
|
|
|
|
<StatusButton
|
|
|
|
|
checked={reblogged}
|
|
|
|
|
title={['Boost', 'Unboost']}
|
|
|
|
|
alt={['Boost', 'Boosted']}
|
|
|
|
|
class="reblog-button"
|
|
|
|
|
icon="rocket"
|
|
|
|
|
count={reblogsCount}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
onClick={boostStatus}
|
2022-12-19 08:38:16 +03:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div class="action has-count">
|
2022-12-17 12:26:41 +03:00
|
|
|
|
<StatusButton
|
2022-12-19 08:38:16 +03:00
|
|
|
|
checked={favourited}
|
|
|
|
|
title={['Favourite', 'Unfavourite']}
|
|
|
|
|
alt={['Favourite', 'Favourited']}
|
|
|
|
|
class="favourite-button"
|
|
|
|
|
icon="heart"
|
|
|
|
|
count={favouritesCount}
|
2023-02-26 19:55:04 +03:00
|
|
|
|
onClick={favouriteStatus}
|
2022-12-17 12:26:41 +03:00
|
|
|
|
/>
|
2022-12-19 08:38:16 +03:00
|
|
|
|
</div>
|
|
|
|
|
<div class="action">
|
|
|
|
|
<StatusButton
|
|
|
|
|
checked={bookmarked}
|
|
|
|
|
title={['Bookmark', 'Unbookmark']}
|
|
|
|
|
alt={['Bookmark', 'Bookmarked']}
|
|
|
|
|
class="bookmark-button"
|
|
|
|
|
icon="bookmark"
|
2023-02-26 19:55:04 +03:00
|
|
|
|
onClick={bookmarkStatus}
|
2022-12-19 08:38:16 +03:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<Menu
|
|
|
|
|
portal={{
|
|
|
|
|
target:
|
|
|
|
|
document.querySelector('.status-deck') || document.body,
|
|
|
|
|
}}
|
|
|
|
|
align="end"
|
|
|
|
|
offsetY={4}
|
|
|
|
|
overflow="auto"
|
|
|
|
|
viewScroll="close"
|
|
|
|
|
boundingBoxPadding="8 8 8 8"
|
|
|
|
|
menuButton={
|
|
|
|
|
<div class="action">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
title="More"
|
|
|
|
|
class="plain more-button"
|
2023-01-24 15:56:43 +03:00
|
|
|
|
>
|
2023-02-26 19:55:04 +03:00
|
|
|
|
<Icon icon="more" size="l" alt="More" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{StatusMenuItems}
|
|
|
|
|
</Menu>
|
2022-12-11 16:22:22 +03:00
|
|
|
|
</div>
|
|
|
|
|
</>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
)}
|
|
|
|
|
</div>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
{!!showEdited && (
|
|
|
|
|
<Modal
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) {
|
|
|
|
|
setShowEdited(false);
|
2022-12-30 15:37:57 +03:00
|
|
|
|
statusRef.current?.focus();
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<EditedAtModal
|
|
|
|
|
statusID={showEdited}
|
2023-02-05 19:17:19 +03:00
|
|
|
|
instance={instance}
|
|
|
|
|
fetchStatusHistory={() => {
|
|
|
|
|
return masto.v1.statuses.listHistory(showEdited);
|
|
|
|
|
}}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowEdited(false);
|
2022-12-30 15:37:57 +03:00
|
|
|
|
statusRef.current?.focus();
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Modal>
|
|
|
|
|
)}
|
2022-12-29 11:12:09 +03:00
|
|
|
|
</article>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 11:45:53 +03:00
|
|
|
|
function Card({ card, instance }) {
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const {
|
|
|
|
|
blurhash,
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
html,
|
|
|
|
|
providerName,
|
|
|
|
|
authorName,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
image,
|
|
|
|
|
url,
|
|
|
|
|
type,
|
|
|
|
|
embedUrl,
|
|
|
|
|
} = 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(() => {
|
|
|
|
|
if (hasText && image && isMastodonLinkMaybe(url)) {
|
|
|
|
|
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 });
|
|
|
|
|
// const status = await masto.v1.statuses.fetch(id);
|
|
|
|
|
// saveStatus(status, instance);
|
|
|
|
|
// setCardStatusID(id);
|
|
|
|
|
// })();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [hasText, image]);
|
|
|
|
|
|
|
|
|
|
// if (cardStatusID) {
|
|
|
|
|
// return (
|
|
|
|
|
// <Status statusID={cardStatusID} instance={instance} size="s" readOnly />
|
|
|
|
|
// );
|
|
|
|
|
// }
|
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
if (hasText && image) {
|
|
|
|
|
const domain = new URL(url).hostname.replace(/^www\./, '');
|
|
|
|
|
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"
|
2022-12-29 03:57:01 +03:00
|
|
|
|
class={`card link ${size}`}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
>
|
2023-01-07 15:25:13 +03:00
|
|
|
|
<div class="card-image">
|
|
|
|
|
<img
|
|
|
|
|
src={image}
|
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
loading="lazy"
|
|
|
|
|
alt=""
|
|
|
|
|
onError={(e) => {
|
|
|
|
|
try {
|
|
|
|
|
e.target.style.display = 'none';
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<div class="meta-container">
|
|
|
|
|
<p class="meta domain">{domain}</p>
|
2023-02-12 09:24:27 +03:00
|
|
|
|
<p class="title">{title}</p>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<p class="meta">{description || providerName || authorName}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
} else if (type === 'photo') {
|
|
|
|
|
return (
|
|
|
|
|
<a
|
|
|
|
|
href={url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="nofollow noopener noreferrer"
|
|
|
|
|
class="card photo"
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={embedUrl}
|
|
|
|
|
width={width}
|
|
|
|
|
height={height}
|
|
|
|
|
alt={title || description}
|
|
|
|
|
loading="lazy"
|
|
|
|
|
style={{
|
|
|
|
|
height: 'auto',
|
|
|
|
|
aspectRatio: `${width}/${height}`,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
} else if (type === 'video') {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
class="card video"
|
|
|
|
|
style={{
|
|
|
|
|
aspectRatio: `${width}/${height}`,
|
|
|
|
|
}}
|
|
|
|
|
dangerouslySetInnerHTML={{ __html: html }}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-05 19:17:19 +03:00
|
|
|
|
function Poll({
|
|
|
|
|
poll,
|
|
|
|
|
lang,
|
|
|
|
|
readOnly,
|
|
|
|
|
refresh = () => {},
|
|
|
|
|
votePoll = () => {},
|
|
|
|
|
}) {
|
2022-12-18 16:10:05 +03:00
|
|
|
|
const [uiState, setUIState] = useState('default');
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
expired,
|
|
|
|
|
expiresAt,
|
|
|
|
|
id,
|
|
|
|
|
multiple,
|
|
|
|
|
options,
|
|
|
|
|
ownVotes,
|
|
|
|
|
voted,
|
|
|
|
|
votersCount,
|
|
|
|
|
votesCount,
|
2022-12-21 14:29:37 +03:00
|
|
|
|
} = poll;
|
2022-12-18 16:10:05 +03:00
|
|
|
|
|
|
|
|
|
const expiresAtDate = !!expiresAt && new Date(expiresAt);
|
|
|
|
|
|
2022-12-22 16:52:59 +03:00
|
|
|
|
// Update poll at point of expiry
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let timeout;
|
|
|
|
|
if (!expired && expiresAtDate) {
|
|
|
|
|
const ms = expiresAtDate.getTime() - Date.now() + 1; // +1 to give it a little buffer
|
|
|
|
|
if (ms > 0) {
|
|
|
|
|
timeout = setTimeout(() => {
|
|
|
|
|
setUIState('loading');
|
|
|
|
|
(async () => {
|
2023-02-05 19:17:19 +03:00
|
|
|
|
await refresh();
|
2022-12-22 16:52:59 +03:00
|
|
|
|
setUIState('default');
|
|
|
|
|
})();
|
|
|
|
|
}, ms);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return () => {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
};
|
|
|
|
|
}, [expired, expiresAtDate]);
|
|
|
|
|
|
2022-12-22 09:59:36 +03:00
|
|
|
|
const pollVotesCount = votersCount || votesCount;
|
|
|
|
|
let roundPrecision = 0;
|
|
|
|
|
if (pollVotesCount <= 1000) {
|
|
|
|
|
roundPrecision = 0;
|
|
|
|
|
} else if (pollVotesCount <= 10000) {
|
|
|
|
|
roundPrecision = 1;
|
|
|
|
|
} else if (pollVotesCount <= 100000) {
|
|
|
|
|
roundPrecision = 2;
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-18 16:10:05 +03:00
|
|
|
|
return (
|
2022-12-21 14:46:38 +03:00
|
|
|
|
<div
|
2022-12-27 13:09:23 +03:00
|
|
|
|
lang={lang}
|
2022-12-21 14:46:38 +03:00
|
|
|
|
class={`poll ${readOnly ? 'read-only' : ''} ${
|
|
|
|
|
uiState === 'loading' ? 'loading' : ''
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
{voted || expired ? (
|
|
|
|
|
options.map((option, i) => {
|
|
|
|
|
const { title, votesCount: optionVotesCount } = option;
|
2022-12-25 18:29:25 +03:00
|
|
|
|
const percentage = pollVotesCount
|
|
|
|
|
? ((optionVotesCount / pollVotesCount) * 100).toFixed(
|
|
|
|
|
roundPrecision,
|
|
|
|
|
)
|
|
|
|
|
: 0;
|
2022-12-18 16:10:05 +03:00
|
|
|
|
// check if current poll choice is the leading one
|
|
|
|
|
const isLeading =
|
|
|
|
|
optionVotesCount > 0 &&
|
|
|
|
|
optionVotesCount === Math.max(...options.map((o) => o.votesCount));
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={`${i}-${title}-${optionVotesCount}`}
|
|
|
|
|
class={`poll-option ${isLeading ? 'poll-option-leading' : ''}`}
|
|
|
|
|
style={{
|
|
|
|
|
'--percentage': `${percentage}%`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div class="poll-option-title">
|
|
|
|
|
{title}
|
|
|
|
|
{voted && ownVotes.includes(i) && (
|
|
|
|
|
<>
|
|
|
|
|
{' '}
|
2022-12-18 18:06:05 +03:00
|
|
|
|
<Icon icon="check-circle" />
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="poll-option-votes"
|
|
|
|
|
title={`${optionVotesCount} vote${
|
|
|
|
|
optionVotesCount === 1 ? '' : 's'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{percentage}%
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
) : (
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const form = e.target;
|
|
|
|
|
const formData = new FormData(form);
|
2023-02-05 19:17:19 +03:00
|
|
|
|
const choices = [];
|
2022-12-18 16:10:05 +03:00
|
|
|
|
formData.forEach((value, key) => {
|
|
|
|
|
if (key === 'poll') {
|
2023-02-05 19:17:19 +03:00
|
|
|
|
choices.push(value);
|
2022-12-18 16:10:05 +03:00
|
|
|
|
}
|
|
|
|
|
});
|
2023-02-24 18:38:59 +03:00
|
|
|
|
if (!choices.length) return;
|
2022-12-18 16:10:05 +03:00
|
|
|
|
setUIState('loading');
|
2023-02-05 19:17:19 +03:00
|
|
|
|
await votePoll(choices);
|
2022-12-18 16:10:05 +03:00
|
|
|
|
setUIState('default');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{options.map((option, i) => {
|
|
|
|
|
const { title } = option;
|
|
|
|
|
return (
|
|
|
|
|
<div class="poll-option">
|
|
|
|
|
<label class="poll-label">
|
|
|
|
|
<input
|
|
|
|
|
type={multiple ? 'checkbox' : 'radio'}
|
|
|
|
|
name="poll"
|
|
|
|
|
value={i}
|
|
|
|
|
disabled={uiState === 'loading'}
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
/>
|
|
|
|
|
<span class="poll-option-title">{title}</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
{!readOnly && (
|
2022-12-10 18:39:12 +03:00
|
|
|
|
<button
|
2022-12-18 16:10:05 +03:00
|
|
|
|
class="poll-vote-button"
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={uiState === 'loading'}
|
2022-12-10 18:39:12 +03:00
|
|
|
|
>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
Vote
|
2022-12-10 18:39:12 +03:00
|
|
|
|
</button>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
)}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</form>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
)}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
{!readOnly && (
|
|
|
|
|
<p class="poll-meta">
|
2022-12-21 14:46:38 +03:00
|
|
|
|
{!expired && (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="textual"
|
|
|
|
|
disabled={uiState === 'loading'}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
setUIState('loading');
|
|
|
|
|
(async () => {
|
2023-02-05 19:17:19 +03:00
|
|
|
|
await refresh();
|
2022-12-21 14:46:38 +03:00
|
|
|
|
setUIState('default');
|
|
|
|
|
})();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Refresh
|
|
|
|
|
</button>{' '}
|
|
|
|
|
•{' '}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2023-01-14 19:17:07 +03:00
|
|
|
|
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
|
|
|
|
|
{votesCount === 1 ? '' : 's'}
|
|
|
|
|
{!!votersCount && votersCount !== votesCount && (
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<>
|
|
|
|
|
{' '}
|
2023-01-14 19:17:07 +03:00
|
|
|
|
•{' '}
|
|
|
|
|
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
|
|
|
|
|
voter
|
|
|
|
|
{votersCount === 1 ? '' : 's'}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</>
|
|
|
|
|
)}{' '}
|
|
|
|
|
• {expired ? 'Ended' : 'Ending'}{' '}
|
2023-01-05 05:50:27 +03:00
|
|
|
|
{!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
|
2022-12-18 16:10:05 +03:00
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-05 19:17:19 +03:00
|
|
|
|
function EditedAtModal({
|
|
|
|
|
statusID,
|
|
|
|
|
instance,
|
|
|
|
|
fetchStatusHistory = () => {},
|
|
|
|
|
onClose = () => {},
|
|
|
|
|
}) {
|
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">
|
2022-12-25 13:01:01 +03:00
|
|
|
|
<header>
|
|
|
|
|
{/* <button type="button" class="close-button plain large" onClick={onClose}>
|
2022-12-18 16:10:05 +03:00
|
|
|
|
<Icon icon="x" alt="Close" />
|
2022-12-21 15:00:45 +03:00
|
|
|
|
</button> */}
|
2022-12-25 13:01:01 +03:00
|
|
|
|
<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
|
|
|
|
|
/>
|
2022-12-25 13:01:01 +03:00
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</ol>
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
2022-12-10 12:14:48 +03:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-17 12:26:41 +03:00
|
|
|
|
function StatusButton({
|
|
|
|
|
checked,
|
|
|
|
|
count,
|
|
|
|
|
class: className,
|
|
|
|
|
title,
|
|
|
|
|
alt,
|
|
|
|
|
icon,
|
|
|
|
|
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}
|
|
|
|
|
class={`plain ${className} ${checked ? 'checked' : ''}`}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onClick(e);
|
|
|
|
|
}}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon={icon} size="l" alt={iconAlt} />
|
|
|
|
|
{!!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-01-29 10:23:53 +03:00
|
|
|
|
export function formatDuration(time) {
|
2023-01-02 09:21:38 +03:00
|
|
|
|
if (!time) return;
|
|
|
|
|
let hours = Math.floor(time / 3600);
|
|
|
|
|
let minutes = Math.floor((time % 3600) / 60);
|
|
|
|
|
let seconds = Math.round(time % 60);
|
|
|
|
|
|
|
|
|
|
if (hours === 0) {
|
|
|
|
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
|
|
|
} else {
|
|
|
|
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds
|
|
|
|
|
.toString()
|
|
|
|
|
.padStart(2, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 11:45:53 +03:00
|
|
|
|
function isMastodonLinkMaybe(url) {
|
|
|
|
|
return /^https:\/\/.*\/\d+$/i.test(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const denylistDomains = /(twitter|github)\.com/i;
|
2023-02-23 17:53:28 +03:00
|
|
|
|
const failedUnfurls = {};
|
2023-02-23 11:45:53 +03:00
|
|
|
|
|
|
|
|
|
function _unfurlMastodonLink(instance, url) {
|
|
|
|
|
if (denylistDomains.test(url)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-02-23 17:53:28 +03:00
|
|
|
|
if (failedUnfurls[url]) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-02-23 11:45:53 +03:00
|
|
|
|
const instanceRegex = new RegExp(instance + '/');
|
|
|
|
|
if (instanceRegex.test(states.unfurledLinks[url]?.url)) {
|
|
|
|
|
return Promise.resolve(states.unfurledLinks[url]);
|
|
|
|
|
}
|
|
|
|
|
console.debug('🦦 Unfurling URL', url);
|
2023-02-28 15:54:26 +03:00
|
|
|
|
|
|
|
|
|
let remoteInstanceFetch;
|
|
|
|
|
const urlObj = new URL(url);
|
|
|
|
|
const domain = urlObj.hostname;
|
|
|
|
|
const path = urlObj.pathname;
|
|
|
|
|
// Regex /:username/:id, where username = @username or @username@domain, id = number
|
|
|
|
|
const statusRegex = /\/@([^@\/]+)@?([^\/]+)?\/(\d+)$/i;
|
|
|
|
|
const statusMatch = statusRegex.exec(path);
|
|
|
|
|
if (statusMatch) {
|
|
|
|
|
const id = statusMatch[3];
|
|
|
|
|
const { masto } = api({ instance: domain });
|
|
|
|
|
remoteInstanceFetch = masto.v1.statuses
|
|
|
|
|
.fetch(id)
|
|
|
|
|
.then((status) => {
|
|
|
|
|
if (status?.id) {
|
|
|
|
|
const statusURL = `/${domain}/s/${id}`;
|
|
|
|
|
const result = {
|
|
|
|
|
id,
|
|
|
|
|
url: statusURL,
|
|
|
|
|
};
|
|
|
|
|
console.debug('🦦 Unfurled URL', url, id, statusURL);
|
|
|
|
|
states.unfurledLinks[url] = result;
|
|
|
|
|
return result;
|
|
|
|
|
} else {
|
|
|
|
|
failedUnfurls[url] = true;
|
|
|
|
|
throw new Error('No results');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
failedUnfurls[url] = true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 11:45:53 +03:00
|
|
|
|
const { masto } = api({ instance });
|
2023-02-28 15:54:26 +03:00
|
|
|
|
const mastoSearchFetch = masto.v2
|
2023-02-23 11:45:53 +03:00
|
|
|
|
.search({
|
|
|
|
|
q: url,
|
|
|
|
|
type: 'statuses',
|
|
|
|
|
resolve: true,
|
|
|
|
|
limit: 1,
|
|
|
|
|
})
|
|
|
|
|
.then((results) => {
|
|
|
|
|
if (results.statuses.length > 0) {
|
|
|
|
|
const status = results.statuses[0];
|
|
|
|
|
const { id } = status;
|
|
|
|
|
const statusURL = `/${instance}/s/${id}`;
|
|
|
|
|
const result = {
|
|
|
|
|
id,
|
|
|
|
|
url: statusURL,
|
|
|
|
|
};
|
|
|
|
|
console.debug('🦦 Unfurled URL', url, id, statusURL);
|
|
|
|
|
states.unfurledLinks[url] = result;
|
|
|
|
|
return result;
|
|
|
|
|
} else {
|
2023-02-23 17:53:28 +03:00
|
|
|
|
failedUnfurls[url] = true;
|
2023-02-23 11:45:53 +03:00
|
|
|
|
throw new Error('No results');
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
2023-02-23 17:53:28 +03:00
|
|
|
|
failedUnfurls[url] = true;
|
2023-02-23 20:26:37 +03:00
|
|
|
|
// console.warn(e);
|
2023-02-23 11:45:53 +03:00
|
|
|
|
// Silently fail
|
|
|
|
|
});
|
2023-02-28 15:54:26 +03:00
|
|
|
|
|
|
|
|
|
return Promise.any([remoteInstanceFetch, mastoSearchFetch]);
|
2023-02-23 11:45:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-03-09 16:51:50 +03:00
|
|
|
|
function nicePostURL(url) {
|
|
|
|
|
const urlObj = new URL(url);
|
|
|
|
|
const { host, pathname } = urlObj;
|
|
|
|
|
const path = pathname.replace(/\/$/, '');
|
|
|
|
|
// split only first slash
|
|
|
|
|
const [_, username, restPath] = path.match(/\/(@[^\/]+)\/(.*)/) || [];
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{host}
|
|
|
|
|
{username ? (
|
|
|
|
|
<>
|
|
|
|
|
/{username}
|
|
|
|
|
<span class="more-insignificant">/{restPath}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span class="more-insignificant">{path}</span>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-23 11:45:53 +03:00
|
|
|
|
const unfurlMastodonLink = throttle(_unfurlMastodonLink);
|
|
|
|
|
|
2023-03-07 17:38:06 +03:00
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
function getHTMLText(html) {
|
|
|
|
|
if (!html) return 0;
|
|
|
|
|
div.innerHTML = html
|
|
|
|
|
.replace(/<\/p>/g, '</p>\n\n')
|
|
|
|
|
.replace(/<\/li>/g, '</li>\n');
|
|
|
|
|
div.querySelectorAll('br').forEach((br) => {
|
|
|
|
|
br.replaceWith('\n');
|
|
|
|
|
});
|
|
|
|
|
return div.innerText.replace(/[\r\n]{3,}/g, '\n\n').trim();
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-07 15:26:23 +03:00
|
|
|
|
export default memo(Status);
|