mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-01-22 16:46:28 +01:00
New experiment: rendering quote toots
This commit is contained in:
parent
7e302770d1
commit
3eaf3ef62b
5 changed files with 352 additions and 250 deletions
216
src/components/poll.jsx
Normal file
216
src/components/poll.jsx
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
import emojifyText from '../utils/emojify-text';
|
||||||
|
import shortenNumber from '../utils/shorten-number';
|
||||||
|
|
||||||
|
import RelativeTime from './relative-time';
|
||||||
|
|
||||||
|
export default function Poll({
|
||||||
|
poll,
|
||||||
|
lang,
|
||||||
|
readOnly,
|
||||||
|
refresh = () => {},
|
||||||
|
votePoll = () => {},
|
||||||
|
}) {
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const {
|
||||||
|
expired,
|
||||||
|
expiresAt,
|
||||||
|
id,
|
||||||
|
multiple,
|
||||||
|
options,
|
||||||
|
ownVotes,
|
||||||
|
voted,
|
||||||
|
votersCount,
|
||||||
|
votesCount,
|
||||||
|
emojis,
|
||||||
|
} = poll;
|
||||||
|
const expiresAtDate = !!expiresAt && new Date(expiresAt); // Update poll at point of expiry
|
||||||
|
// NOTE: Disable this because setTimeout runs immediately if delay is too large
|
||||||
|
// https://stackoverflow.com/a/56718027/20838
|
||||||
|
// 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 () => {
|
||||||
|
// // await refresh();
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [showResults, setShowResults] = useState(false);
|
||||||
|
const optionsHaveVoteCounts = options.every((o) => o.votesCount !== null);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
lang={lang}
|
||||||
|
dir="auto"
|
||||||
|
class={`poll ${readOnly ? 'read-only' : ''} ${
|
||||||
|
uiState === 'loading' ? 'loading' : ''
|
||||||
|
}`}
|
||||||
|
onDblClick={() => {
|
||||||
|
setShowResults(!showResults);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(showResults && optionsHaveVoteCounts) || voted || expired ? (
|
||||||
|
<div class="poll-options">
|
||||||
|
{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 (
|
||||||
|
<div
|
||||||
|
key={`${i}-${title}-${optionVotesCount}`}
|
||||||
|
class={`poll-option poll-result ${
|
||||||
|
isLeading ? 'poll-option-leading' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
'--percentage': `${percentage}%`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="poll-option-title">
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: emojifyText(title, emojis),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{voted && ownVotes.includes(i) && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<Icon icon="check-circle" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="poll-option-votes"
|
||||||
|
title={`${optionVotesCount} vote${
|
||||||
|
optionVotesCount === 1 ? '' : 's'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{percentage}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const choices = [];
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
if (key === 'poll') {
|
||||||
|
choices.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!choices.length) return;
|
||||||
|
setUIState('loading');
|
||||||
|
await votePoll(choices);
|
||||||
|
setUIState('default');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="poll-options">
|
||||||
|
{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"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: emojifyText(title, emojis),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{!readOnly && (
|
||||||
|
<button
|
||||||
|
class="poll-vote-button"
|
||||||
|
type="submit"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
>
|
||||||
|
Vote
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
{!readOnly && (
|
||||||
|
<p class="poll-meta">
|
||||||
|
{!expired && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="textual"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setUIState('loading');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await refresh();
|
||||||
|
setUIState('default');
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</button>{' '}
|
||||||
|
•{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
|
||||||
|
{votesCount === 1 ? '' : 's'}
|
||||||
|
{!!votersCount && votersCount !== votesCount && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
•{' '}
|
||||||
|
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
|
||||||
|
voter
|
||||||
|
{votersCount === 1 ? '' : 's'}
|
||||||
|
</>
|
||||||
|
)}{' '}
|
||||||
|
• {expired ? 'Ended' : 'Ending'}{' '}
|
||||||
|
{!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -60,6 +60,7 @@
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
font-size: var(--text-size);
|
||||||
}
|
}
|
||||||
.status.large {
|
.status.large {
|
||||||
--fade-in-out-bg: linear-gradient(
|
--fade-in-out-bg: linear-gradient(
|
||||||
|
@ -76,6 +77,51 @@
|
||||||
background-image: var(--fade-in-out-bg), var(--yellow-stripes);
|
background-image: var(--fade-in-out-bg), var(--yellow-stripes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-card-link {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.status-card-link:is(:hover, :focus) .status-card {
|
||||||
|
border-color: var(--outline-hover-color);
|
||||||
|
box-shadow: inset 0 0 0 4px var(--bg-faded-blur-color);
|
||||||
|
}
|
||||||
|
.status-card-link:is(:active) .status-card {
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
}
|
||||||
|
.status-card {
|
||||||
|
font-size: calc(var(--text-size) * 0.9);
|
||||||
|
margin: 1em 0 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--outline-color);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
/* box-shadow: inset 0 0 0 2px var(--bg-faded-color); */
|
||||||
|
}
|
||||||
|
.status-card:has(.status-badge:not(:empty)) {
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
}
|
||||||
|
.status-card > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.status-card :is(.content, .poll, .media-container) {
|
||||||
|
max-height: 160px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.status.small .status-card :is(.content, .poll, .media-container) {
|
||||||
|
max-height: 80px !important;
|
||||||
|
}
|
||||||
|
.status-card :is(.content, .poll) {
|
||||||
|
font-size: inherit !important;
|
||||||
|
mask-image: linear-gradient(to bottom, #000 80px, transparent);
|
||||||
|
}
|
||||||
|
.status.small .status-card :is(.content, .poll) {
|
||||||
|
mask-image: linear-gradient(to bottom, #000 40px, transparent);
|
||||||
|
}
|
||||||
|
.status-card .card {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes skeleton-breathe {
|
@keyframes skeleton-breathe {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -170,7 +216,7 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.status:not(.small) .container {
|
.status:not(.small) > .container {
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,7 +371,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.large .content-container {
|
.status.large > .container > .content-container {
|
||||||
margin-left: calc(-50px - 16px);
|
margin-left: calc(-50px - 16px);
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
@ -460,7 +506,7 @@
|
||||||
.status .content .ellipsis::after {
|
.status .content .ellipsis::after {
|
||||||
content: '…';
|
content: '…';
|
||||||
}
|
}
|
||||||
.status.large .content {
|
.status.large .content:not(.content .content) {
|
||||||
font-size: 150%;
|
font-size: 150%;
|
||||||
font-size: min(calc(100% + 50% / var(--content-text-weight)), 150%);
|
font-size: min(calc(100% + 50% / var(--content-text-weight)), 150%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import AccountBlock from '../components/account-block';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import Modal from '../components/modal';
|
import Modal from '../components/modal';
|
||||||
import NameText from '../components/name-text';
|
import NameText from '../components/name-text';
|
||||||
|
import Poll from '../components/poll';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import enhanceContent from '../utils/enhance-content';
|
import enhanceContent from '../utils/enhance-content';
|
||||||
|
@ -31,6 +32,7 @@ import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
import getHTMLText from '../utils/getHTMLText';
|
import getHTMLText from '../utils/getHTMLText';
|
||||||
import handleContentLinks from '../utils/handle-content-links';
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import htmlContentLength from '../utils/html-content-length';
|
import htmlContentLength from '../utils/html-content-length';
|
||||||
|
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
import shortenNumber from '../utils/shorten-number';
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import showToast from '../utils/show-toast';
|
import showToast from '../utils/show-toast';
|
||||||
|
@ -81,6 +83,7 @@ function Status({
|
||||||
previewMode,
|
previewMode,
|
||||||
allowFilters,
|
allowFilters,
|
||||||
onMediaClick,
|
onMediaClick,
|
||||||
|
quoted,
|
||||||
}) {
|
}) {
|
||||||
if (skeleton) {
|
if (skeleton) {
|
||||||
return (
|
return (
|
||||||
|
@ -689,7 +692,7 @@ function Status({
|
||||||
m: 'medium',
|
m: 'medium',
|
||||||
l: 'large',
|
l: 'large',
|
||||||
}[size]
|
}[size]
|
||||||
} ${_deleted ? 'status-deleted' : ''}`}
|
} ${_deleted ? 'status-deleted' : ''} ${quoted ? 'status-card' : ''}`}
|
||||||
onMouseEnter={debugHover}
|
onMouseEnter={debugHover}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
if (size === 'l') return;
|
if (size === 'l') return;
|
||||||
|
@ -922,36 +925,50 @@ function Status({
|
||||||
dir="auto"
|
dir="auto"
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
data-read-more={readMoreText}
|
data-read-more={readMoreText}
|
||||||
onClick={handleContentLinks({ mentions, instance, previewMode })}
|
>
|
||||||
dangerouslySetInnerHTML={{
|
<div
|
||||||
__html: enhanceContent(content, {
|
onClick={handleContentLinks({ mentions, instance, previewMode })}
|
||||||
emojis,
|
dangerouslySetInnerHTML={{
|
||||||
postEnhanceDOM: (dom) => {
|
__html: enhanceContent(content, {
|
||||||
// Remove target="_blank" from links
|
emojis,
|
||||||
dom
|
postEnhanceDOM: (dom) => {
|
||||||
.querySelectorAll('a.u-url[target="_blank"]')
|
// Remove target="_blank" from links
|
||||||
.forEach((a) => {
|
dom
|
||||||
if (!/http/i.test(a.innerText.trim())) {
|
.querySelectorAll('a.u-url[target="_blank"]')
|
||||||
a.removeAttribute('target');
|
.forEach((a) => {
|
||||||
}
|
if (!/http/i.test(a.innerText.trim())) {
|
||||||
});
|
|
||||||
if (previewMode) return;
|
|
||||||
// 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');
|
a.removeAttribute('target');
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
});
|
if (previewMode) return;
|
||||||
},
|
// Unfurl Mastodon links
|
||||||
}),
|
dom
|
||||||
}}
|
.querySelectorAll(
|
||||||
/>
|
'a[href]:not(.u-url):not(.mention):not(.hashtag)',
|
||||||
|
)
|
||||||
|
.forEach((a, i) => {
|
||||||
|
if (isMastodonLinkMaybe(a.href)) {
|
||||||
|
unfurlMastodonLink(currentInstance, a.href).then(
|
||||||
|
(result) => {
|
||||||
|
if (!result) return;
|
||||||
|
console.log('TAG', result);
|
||||||
|
a.removeAttribute('target');
|
||||||
|
if (!Array.isArray(states.statusQuotes[sKey])) {
|
||||||
|
states.statusQuotes[sKey] = [];
|
||||||
|
}
|
||||||
|
if (!states.statusQuotes[sKey][i]) {
|
||||||
|
states.statusQuotes[sKey].splice(i, 0, result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<QuoteStatuses id={id} instance={instance} />
|
||||||
|
</div>
|
||||||
{!!poll && (
|
{!!poll && (
|
||||||
<Poll
|
<Poll
|
||||||
lang={language}
|
lang={language}
|
||||||
|
@ -1208,6 +1225,7 @@ function Status({
|
||||||
}
|
}
|
||||||
|
|
||||||
function Card({ card, instance }) {
|
function Card({ card, instance }) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
const {
|
const {
|
||||||
blurhash,
|
blurhash,
|
||||||
title,
|
title,
|
||||||
|
@ -1260,6 +1278,8 @@ function Card({ card, instance }) {
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if (snapStates.unfurledLinks[url]) return null;
|
||||||
|
|
||||||
if (hasText && (image || (!type !== 'photo' && blurhash))) {
|
if (hasText && (image || (!type !== 'photo' && blurhash))) {
|
||||||
const domain = new URL(url).hostname.replace(/^www\./, '');
|
const domain = new URL(url).hostname.replace(/^www\./, '');
|
||||||
let blurhashImage;
|
let blurhashImage;
|
||||||
|
@ -1361,219 +1381,6 @@ function Card({ card, instance }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Poll({
|
|
||||||
poll,
|
|
||||||
lang,
|
|
||||||
readOnly,
|
|
||||||
refresh = () => {},
|
|
||||||
votePoll = () => {},
|
|
||||||
}) {
|
|
||||||
const [uiState, setUIState] = useState('default');
|
|
||||||
|
|
||||||
const {
|
|
||||||
expired,
|
|
||||||
expiresAt,
|
|
||||||
id,
|
|
||||||
multiple,
|
|
||||||
options,
|
|
||||||
ownVotes,
|
|
||||||
voted,
|
|
||||||
votersCount,
|
|
||||||
votesCount,
|
|
||||||
emojis,
|
|
||||||
} = poll;
|
|
||||||
|
|
||||||
const expiresAtDate = !!expiresAt && new Date(expiresAt);
|
|
||||||
|
|
||||||
// Update poll at point of expiry
|
|
||||||
// NOTE: Disable this because setTimeout runs immediately if delay is too large
|
|
||||||
// https://stackoverflow.com/a/56718027/20838
|
|
||||||
// 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 () => {
|
|
||||||
// // await refresh();
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [showResults, setShowResults] = useState(false);
|
|
||||||
const optionsHaveVoteCounts = options.every((o) => o.votesCount !== null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
lang={lang}
|
|
||||||
dir="auto"
|
|
||||||
class={`poll ${readOnly ? 'read-only' : ''} ${
|
|
||||||
uiState === 'loading' ? 'loading' : ''
|
|
||||||
}`}
|
|
||||||
onDblClick={() => {
|
|
||||||
setShowResults(!showResults);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(showResults && optionsHaveVoteCounts) || voted || expired ? (
|
|
||||||
<div class="poll-options">
|
|
||||||
{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 (
|
|
||||||
<div
|
|
||||||
key={`${i}-${title}-${optionVotesCount}`}
|
|
||||||
class={`poll-option poll-result ${
|
|
||||||
isLeading ? 'poll-option-leading' : ''
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
'--percentage': `${percentage}%`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="poll-option-title">
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: emojifyText(title, emojis),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{voted && ownVotes.includes(i) && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
<Icon icon="check-circle" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="poll-option-votes"
|
|
||||||
title={`${optionVotesCount} vote${
|
|
||||||
optionVotesCount === 1 ? '' : 's'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{percentage}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form
|
|
||||||
onSubmit={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const choices = [];
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
if (key === 'poll') {
|
|
||||||
choices.push(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!choices.length) return;
|
|
||||||
setUIState('loading');
|
|
||||||
await votePoll(choices);
|
|
||||||
setUIState('default');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="poll-options">
|
|
||||||
{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"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: emojifyText(title, emojis),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{!readOnly && (
|
|
||||||
<button
|
|
||||||
class="poll-vote-button"
|
|
||||||
type="submit"
|
|
||||||
disabled={uiState === 'loading'}
|
|
||||||
>
|
|
||||||
Vote
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
{!readOnly && (
|
|
||||||
<p class="poll-meta">
|
|
||||||
{!expired && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="textual"
|
|
||||||
disabled={uiState === 'loading'}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setUIState('loading');
|
|
||||||
(async () => {
|
|
||||||
await refresh();
|
|
||||||
setUIState('default');
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
</button>{' '}
|
|
||||||
•{' '}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
|
|
||||||
{votesCount === 1 ? '' : 's'}
|
|
||||||
{!!votersCount && votersCount !== votesCount && (
|
|
||||||
<>
|
|
||||||
{' '}
|
|
||||||
•{' '}
|
|
||||||
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
|
|
||||||
voter
|
|
||||||
{votersCount === 1 ? '' : 's'}
|
|
||||||
</>
|
|
||||||
)}{' '}
|
|
||||||
• {expired ? 'Ended' : 'Ending'}{' '}
|
|
||||||
{!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditedAtModal({
|
function EditedAtModal({
|
||||||
statusID,
|
statusID,
|
||||||
instance,
|
instance,
|
||||||
|
@ -1871,10 +1678,6 @@ export function formatDuration(time) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMastodonLinkMaybe(url) {
|
|
||||||
return /^https:\/\/.*\/\d+$/i.test(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
const denylistDomains = /(twitter|github)\.com/i;
|
const denylistDomains = /(twitter|github)\.com/i;
|
||||||
const failedUnfurls = {};
|
const failedUnfurls = {};
|
||||||
|
|
||||||
|
@ -1908,10 +1711,14 @@ function _unfurlMastodonLink(instance, url) {
|
||||||
const statusURL = `/${domain}/s/${id}`;
|
const statusURL = `/${domain}/s/${id}`;
|
||||||
const result = {
|
const result = {
|
||||||
id,
|
id,
|
||||||
|
instance: domain,
|
||||||
url: statusURL,
|
url: statusURL,
|
||||||
};
|
};
|
||||||
console.debug('🦦 Unfurled URL', url, id, statusURL);
|
console.debug('🦦 Unfurled URL', url, id, statusURL);
|
||||||
states.unfurledLinks[url] = result;
|
states.unfurledLinks[url] = result;
|
||||||
|
saveStatus(status, domain, {
|
||||||
|
skipThreading: true,
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
failedUnfurls[url] = true;
|
failedUnfurls[url] = true;
|
||||||
|
@ -1938,10 +1745,14 @@ function _unfurlMastodonLink(instance, url) {
|
||||||
const statusURL = `/${instance}/s/${id}`;
|
const statusURL = `/${instance}/s/${id}`;
|
||||||
const result = {
|
const result = {
|
||||||
id,
|
id,
|
||||||
|
instance,
|
||||||
url: statusURL,
|
url: statusURL,
|
||||||
};
|
};
|
||||||
console.debug('🦦 Unfurled URL', url, id, statusURL);
|
console.debug('🦦 Unfurled URL', url, id, statusURL);
|
||||||
states.unfurledLinks[url] = result;
|
states.unfurledLinks[url] = result;
|
||||||
|
saveStatus(status, instance, {
|
||||||
|
skipThreading: true,
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
failedUnfurls[url] = true;
|
failedUnfurls[url] = true;
|
||||||
|
@ -2119,4 +1930,29 @@ function FilteredStatus({ status, filterInfo, instance, containerProps = {} }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QuoteStatuses = memo(({ id, instance }) => {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const sKey = statusKey(id, instance);
|
||||||
|
const quotes = snapStates.statusQuotes[sKey];
|
||||||
|
|
||||||
|
if (!quotes?.length) return;
|
||||||
|
|
||||||
|
return quotes.map((q) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`${q.instance ? `/${q.instance}` : ''}/s/${q.id}`}
|
||||||
|
class="status-card-link"
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
statusID={q.id}
|
||||||
|
instance={q.instance}
|
||||||
|
size="s"
|
||||||
|
quoted
|
||||||
|
previewMode
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export default memo(Status);
|
export default memo(Status);
|
||||||
|
|
3
src/utils/isMastodonLinkMaybe.jsx
Normal file
3
src/utils/isMastodonLinkMaybe.jsx
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function isMastodonLinkMaybe(url) {
|
||||||
|
return /^https:\/\/.*\/\d+$/i.test(url);
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ const states = proxy({
|
||||||
spoilers: {},
|
spoilers: {},
|
||||||
scrollPositions: {},
|
scrollPositions: {},
|
||||||
unfurledLinks: {},
|
unfurledLinks: {},
|
||||||
|
statusQuotes: {},
|
||||||
accounts: {},
|
accounts: {},
|
||||||
// Modals
|
// Modals
|
||||||
showCompose: false,
|
showCompose: false,
|
||||||
|
|
Loading…
Reference in a new issue