import './status.css';
import { Menu, MenuItem } from '@szhsin/react-menu';
import { getBlurHashAverageColor } from 'fast-blurhash';
import mem from 'mem';
import { memo } from 'preact/compat';
import {
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import 'swiped-events';
import useResizeObserver from 'use-resize-observer';
import { useSnapshot } from 'valtio';
import Loader from '../components/loader';
import Modal from '../components/modal';
import NameText from '../components/name-text';
import enhanceContent from '../utils/enhance-content';
import handleAccountLinks from '../utils/handle-account-links';
import htmlContentLength from '../utils/html-content-length';
import shortenNumber from '../utils/shorten-number';
import states, { saveStatus } from '../utils/states';
import store from '../utils/store';
import visibilityIconsMap from '../utils/visibility-icons-map';
import Avatar from './avatar';
import Icon from './icon';
import Link from './link';
import RelativeTime from './relative-time';
function fetchAccount(id) {
try {
return masto.v1.accounts.fetch(id);
} catch (e) {
return Promise.reject(e);
}
}
const memFetchAccount = mem(fetchAccount);
function Status({
statusID,
status,
withinContext,
size = 'm',
skeleton,
readOnly,
}) {
if (skeleton) {
return (
{
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
onClick(e);
}}
onMouseEnter={() => {
if (hoverAnimate) {
try {
videoRef.current.play();
} catch (e) {}
}
}}
onMouseLeave={() => {
if (hoverAnimate) {
try {
videoRef.current.pause();
} catch (e) {}
}
}}
>
{showOriginal || autoGIFAnimate ? (
`,
}}
/>
) : isGIF ? (
) : (
)}
);
} else if (type === 'audio') {
const formattedDuration = formatDuration(original.duration);
return (
);
}
}
function Card({ card }) {
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;
const isLandscape = width / height >= 1.2;
const size = isLandscape ? 'large' : '';
if (hasText && image) {
const domain = new URL(url).hostname.replace(/^www\./, '');
return (
{
try {
e.target.style.display = 'none';
} catch (e) {}
}}
/>
);
} else if (type === 'photo') {
return (
);
} else if (type === 'video') {
return (
);
}
}
function Poll({ poll, lang, readOnly, onUpdate = () => {} }) {
const [uiState, setUIState] = useState('default');
const {
expired,
expiresAt,
id,
multiple,
options,
ownVotes,
voted,
votersCount,
votesCount,
} = poll;
const expiresAtDate = !!expiresAt && new Date(expiresAt);
// 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 () => {
try {
const pollResponse = await masto.v1.polls.fetch(id);
onUpdate(pollResponse);
} catch (e) {
// Silent fail
}
setUIState('default');
})();
}, ms);
}
}
return () => {
clearTimeout(timeout);
};
}, [expired, expiresAtDate]);
const pollVotesCount = votersCount || votesCount;
let roundPrecision = 0;
if (pollVotesCount <= 1000) {
roundPrecision = 0;
} else if (pollVotesCount <= 10000) {
roundPrecision = 1;
} else if (pollVotesCount <= 100000) {
roundPrecision = 2;
}
return (
{voted || expired ? (
options.map((option, i) => {
const { title, votesCount: optionVotesCount } = option;
const percentage = pollVotesCount
? ((optionVotesCount / pollVotesCount) * 100).toFixed(
roundPrecision,
)
: 0;
// check if current poll choice is the leading one
const isLeading =
optionVotesCount > 0 &&
optionVotesCount === Math.max(...options.map((o) => o.votesCount));
return (
{title}
{voted && ownVotes.includes(i) && (
<>
{' '}
>
)}
{percentage}%
);
})
) : (
)}
{!readOnly && (
{!expired && (
<>
{' '}
•{' '}
>
)}
{shortenNumber(votesCount)} vote
{votesCount === 1 ? '' : 's'}
{!!votersCount && votersCount !== votesCount && (
<>
{' '}
•{' '}
{shortenNumber(votersCount)}{' '}
voter
{votersCount === 1 ? '' : 's'}
>
)}{' '}
• {expired ? 'Ended' : 'Ending'}{' '}
{!!expiresAtDate && }
)}
);
}
function EditedAtModal({ statusID, onClose = () => {} }) {
const [uiState, setUIState] = useState('default');
const [editHistory, setEditHistory] = useState([]);
useEffect(() => {
setUIState('loading');
(async () => {
try {
const editHistory = await masto.v1.statuses.listHistory(statusID);
console.log(editHistory);
setEditHistory(editHistory);
setUIState('default');
} catch (e) {
console.error(e);
setUIState('error');
}
})();
}, []);
const currentYear = new Date().getFullYear();
return (
{/* */}
Edit History
{uiState === 'error' && Failed to load history
}
{uiState === 'loading' && (
Loading…
)}
{editHistory.length > 0 && (
{editHistory.map((status) => {
const { createdAt } = status;
const createdAtDate = new Date(createdAt);
return (
-
);
})}
)}
);
}
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 (
);
}
function Carousel({ mediaAttachments, index = 0, onClose = () => {} }) {
const carouselRef = useRef(null);
const [currentIndex, setCurrentIndex] = useState(index);
const carouselFocusItem = useRef(null);
useLayoutEffect(() => {
carouselFocusItem.current?.scrollIntoView();
}, []);
const [showControls, setShowControls] = useState(true);
useEffect(() => {
let handleSwipe = () => {
onClose();
};
if (carouselRef.current) {
carouselRef.current.addEventListener('swiped-down', handleSwipe);
}
return () => {
if (carouselRef.current) {
carouselRef.current.removeEventListener('swiped-down', handleSwipe);
}
};
}, []);
useHotkeys('esc', onClose, [onClose]);
const [showMediaAlt, setShowMediaAlt] = useState(false);
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 (
<>
{
if (
e.target.classList.contains('carousel-item') ||
e.target.classList.contains('media')
) {
onClose();
}
}}
>
{mediaAttachments?.map((media, i) => {
const { blurhash } = media;
const rgbAverageColor = blurhash
? getBlurHashAverageColor(blurhash)
: null;
return (
{
if (e.target !== e.currentTarget) {
setShowControls(!showControls);
}
}}
>
{!!media.description && (
)}
);
})}
{mediaAttachments?.length > 1 ? (
{mediaAttachments?.map((media, i) => (
))}
) : (
)}
{' '}
{mediaAttachments?.length > 1 && (
)}
{!!showMediaAlt && (
{
if (e.target === e.currentTarget) {
setShowMediaAlt(false);
}
}}
>
)}
>
);
}
function formatDuration(time) {
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')}`;
}
}
export default memo(Status);