kopia lustrzana https://github.com/cheeaun/phanpy
Refactor components from status
rodzic
9ccbe244af
commit
1a25b32cdc
|
@ -0,0 +1,26 @@
|
|||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import Icon from './icon';
|
||||
import NameText from './name-text';
|
||||
|
||||
function Byline({ authors, hidden, children }) {
|
||||
if (hidden) return children;
|
||||
if (!authors?.[0]?.account?.id) return children;
|
||||
const author = authors[0].account;
|
||||
|
||||
return (
|
||||
<div class="card-byline">
|
||||
{children}
|
||||
<div class="card-byline-author">
|
||||
<Icon icon="link" size="s" />{' '}
|
||||
<small>
|
||||
<Trans comment="More from [Author]">
|
||||
More from <NameText account={author} showAvatar />
|
||||
</Trans>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Byline;
|
|
@ -0,0 +1,164 @@
|
|||
import 'temml/dist/Temml-Local.css';
|
||||
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useCallback, useState } from 'preact/hooks';
|
||||
|
||||
import showToast from '../utils/show-toast';
|
||||
|
||||
import Icon from './icon';
|
||||
|
||||
// Follow https://mathstodon.xyz/about
|
||||
// > You can use LaTeX in toots here! Use \( and \) for inline, and \[ and \] for display mode.
|
||||
const DELIMITERS_PATTERNS = [
|
||||
// '\\$\\$[\\s\\S]*?\\$\\$', // $$...$$
|
||||
'\\\\\\[[\\s\\S]*?\\\\\\]', // \[...\]
|
||||
'\\\\\\([\\s\\S]*?\\\\\\)', // \(...\)
|
||||
// '\\\\begin\\{(?:equation\\*?|align\\*?|alignat\\*?|gather\\*?|CD)\\}[\\s\\S]*?\\\\end\\{(?:equation\\*?|align\\*?|alignat\\*?|gather\\*?|CD)\\}', // AMS environments
|
||||
// '\\\\(?:ref|eqref)\\{[^}]*\\}', // \ref{...}, \eqref{...}
|
||||
];
|
||||
const DELIMITERS_REGEX = new RegExp(DELIMITERS_PATTERNS.join('|'), 'g');
|
||||
|
||||
function cleanDOMForTemml(dom) {
|
||||
// Define start and end delimiter patterns
|
||||
const START_DELIMITERS = ['\\\\\\[', '\\\\\\(']; // \[ and \(
|
||||
const startRegex = new RegExp(`(${START_DELIMITERS.join('|')})`);
|
||||
|
||||
// Walk through all text nodes
|
||||
const walker = document.createTreeWalker(dom, NodeFilter.SHOW_TEXT);
|
||||
const textNodes = [];
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
|
||||
for (const textNode of textNodes) {
|
||||
const text = textNode.textContent;
|
||||
const startMatch = text.match(startRegex);
|
||||
|
||||
if (!startMatch) continue; // No start delimiter in this text node
|
||||
|
||||
// Find the matching end delimiter
|
||||
const startDelimiter = startMatch[0];
|
||||
const endDelimiter = startDelimiter === '\\[' ? '\\]' : '\\)';
|
||||
|
||||
// Collect nodes from start delimiter until end delimiter
|
||||
const nodesToCombine = [textNode];
|
||||
let currentNode = textNode;
|
||||
let foundEnd = false;
|
||||
let combinedText = text;
|
||||
|
||||
// Check if end delimiter is in the same text node
|
||||
if (text.includes(endDelimiter)) {
|
||||
foundEnd = true;
|
||||
} else {
|
||||
// Look through sibling nodes
|
||||
while (currentNode.nextSibling && !foundEnd) {
|
||||
const nextSibling = currentNode.nextSibling;
|
||||
|
||||
if (nextSibling.nodeType === Node.TEXT_NODE) {
|
||||
nodesToCombine.push(nextSibling);
|
||||
combinedText += nextSibling.textContent;
|
||||
if (nextSibling.textContent.includes(endDelimiter)) {
|
||||
foundEnd = true;
|
||||
}
|
||||
} else if (
|
||||
nextSibling.nodeType === Node.ELEMENT_NODE &&
|
||||
nextSibling.tagName === 'BR'
|
||||
) {
|
||||
nodesToCombine.push(nextSibling);
|
||||
combinedText += '\n';
|
||||
} else {
|
||||
// Found a non-BR element, stop and don't process
|
||||
break;
|
||||
}
|
||||
|
||||
currentNode = nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
// Only process if we found the end delimiter and have nodes to combine
|
||||
if (foundEnd && nodesToCombine.length > 1) {
|
||||
// Replace the first text node with combined text
|
||||
textNode.textContent = combinedText;
|
||||
|
||||
// Remove the other nodes
|
||||
for (let i = 1; i < nodesToCombine.length; i++) {
|
||||
nodesToCombine[i].remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MathBlock = ({ content, contentRef, onRevert }) => {
|
||||
DELIMITERS_REGEX.lastIndex = 0; // Reset index to prevent g trap
|
||||
const hasLatexContent = DELIMITERS_REGEX.test(content);
|
||||
|
||||
if (!hasLatexContent) return null;
|
||||
|
||||
const { t } = useLingui();
|
||||
const [mathRendered, setMathRendered] = useState(false);
|
||||
const toggleMathRendering = useCallback(
|
||||
async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (mathRendered) {
|
||||
// Revert to original content by refreshing PostContent
|
||||
setMathRendered(false);
|
||||
onRevert();
|
||||
} else {
|
||||
// Render math
|
||||
try {
|
||||
// This needs global because the codebase inside temml is calling a function from global.temml 🤦♂️
|
||||
const temml =
|
||||
window.temml || (window.temml = (await import('temml'))?.default);
|
||||
|
||||
cleanDOMForTemml(contentRef.current);
|
||||
const originalContentRefHTML = contentRef.current.innerHTML;
|
||||
temml.renderMathInElement(contentRef.current, {
|
||||
fences: '(', // This should sync with DELIMITERS_REGEX
|
||||
annotate: true,
|
||||
throwOnError: true,
|
||||
errorCallback: (err) => {
|
||||
console.warn('Failed to render LaTeX:', err);
|
||||
},
|
||||
});
|
||||
|
||||
const hasMath = contentRef.current.querySelector('math.tml-display');
|
||||
const htmlChanged =
|
||||
contentRef.current.innerHTML !== originalContentRefHTML;
|
||||
if (hasMath && htmlChanged) {
|
||||
setMathRendered(true);
|
||||
} else {
|
||||
showToast(t`Unable to format math`);
|
||||
setMathRendered(false);
|
||||
onRevert(); // Revert because DOM modified by cleanDOMForTemml
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to LaTeX:', e);
|
||||
}
|
||||
}
|
||||
},
|
||||
[mathRendered],
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="math-block">
|
||||
<Icon icon="formula" size="s" /> <span>{t`Math expressions found.`}</span>{' '}
|
||||
<button type="button" class="light small" onClick={toggleMathRendering}>
|
||||
{mathRendered
|
||||
? t({
|
||||
comment:
|
||||
'Action to switch from rendered math back to raw (LaTeX) markup',
|
||||
message: 'Show markup',
|
||||
})
|
||||
: t({
|
||||
comment:
|
||||
'Action to render math expressions from raw (LaTeX) markup',
|
||||
message: 'Format math',
|
||||
})}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MathBlock;
|
|
@ -0,0 +1,116 @@
|
|||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
|
||||
import isRTL from '../utils/is-rtl';
|
||||
|
||||
import Icon from './icon';
|
||||
import Media from './media';
|
||||
|
||||
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(Math.abs(scrollLeft) / clientWidth);
|
||||
setCurrentIndex(index);
|
||||
};
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.addEventListener('scroll', handleScroll, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
if (carouselRef.current) {
|
||||
carouselRef.current.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
{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) *
|
||||
(isRTL() ? -1 : 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) *
|
||||
(isRTL() ? -1 : 1),
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Icon icon="arrow-right" />
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{moreThanOne && (
|
||||
<div
|
||||
class="media-carousel-dots"
|
||||
style={{
|
||||
'--dots-count': mediaAttachments.length,
|
||||
}}
|
||||
>
|
||||
{mediaAttachments.map((media, i) => (
|
||||
<span
|
||||
key={media.id}
|
||||
class={`carousel-dot ${i === currentIndex ? 'active' : ''}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaFirstContainer;
|
|
@ -0,0 +1,14 @@
|
|||
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">
|
||||
{captionChildren}
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
|
||||
export default MultipleMediaFigure;
|
|
@ -0,0 +1,64 @@
|
|||
import { useLayoutEffect, useRef } from 'preact/hooks';
|
||||
|
||||
import enhanceContent from '../utils/enhance-content';
|
||||
import handleContentLinks from '../utils/handle-content-links';
|
||||
|
||||
const HTTP_REGEX = /^http/i;
|
||||
|
||||
const PostContent =
|
||||
/*memo(*/
|
||||
({ post, instance, previewMode }) => {
|
||||
const { content, emojis, language, mentions, url } = post;
|
||||
|
||||
const divRef = useRef();
|
||||
useLayoutEffect(() => {
|
||||
if (!divRef.current) return;
|
||||
const dom = enhanceContent(content, {
|
||||
emojis,
|
||||
returnDOM: true,
|
||||
});
|
||||
// Remove target="_blank" from links
|
||||
for (const a of dom.querySelectorAll('a.u-url[target="_blank"]')) {
|
||||
if (!HTTP_REGEX.test(a.innerText.trim())) {
|
||||
a.removeAttribute('target');
|
||||
}
|
||||
}
|
||||
divRef.current.replaceChildren(dom.cloneNode(true));
|
||||
}, [content, emojis?.length]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
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) => {
|
||||
const { post: oldPost } = oldProps;
|
||||
const { post: newPost } = newProps;
|
||||
return oldPost.content === newPost.content;
|
||||
},
|
||||
);*/
|
||||
|
||||
export default PostContent;
|
|
@ -0,0 +1,394 @@
|
|||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import prettify from 'html-prettify';
|
||||
|
||||
import emojifyText from '../utils/emojify-text';
|
||||
import showToast from '../utils/show-toast';
|
||||
import states, { statusKey } from '../utils/states';
|
||||
|
||||
import Icon from './icon';
|
||||
|
||||
function generateHTMLCode(post, instance, level = 0) {
|
||||
const {
|
||||
account: {
|
||||
url: accountURL,
|
||||
displayName,
|
||||
acct,
|
||||
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
|
||||
? URL.parse(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}" data-source="fediverse">
|
||||
${
|
||||
spoilerText
|
||||
? `
|
||||
<details>
|
||||
<summary>${spoilerText}</summary>
|
||||
${contentHTML}
|
||||
</details>
|
||||
`
|
||||
: contentHTML
|
||||
}
|
||||
<footer>
|
||||
— ${emojifyText(
|
||||
displayName,
|
||||
accountEmojis,
|
||||
)} (@${acct}) ${!!createdAt ? `<a href="${url}"><time datetime="${createdAtDate.toISOString()}">${createdAtDate.toLocaleString()}</time></a>` : ''}
|
||||
</footer>
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
return prettify(htmlCode);
|
||||
}
|
||||
|
||||
function PostEmbedModal({ post, instance, onClose }) {
|
||||
const { t } = useLingui();
|
||||
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" alt={t`Close`} />
|
||||
</button>
|
||||
)}
|
||||
<header>
|
||||
<h2>
|
||||
<Trans>Embed post</Trans>
|
||||
</h2>
|
||||
</header>
|
||||
<main tabIndex="-1">
|
||||
<h3>
|
||||
<Trans>HTML Code</Trans>
|
||||
</h3>
|
||||
<textarea
|
||||
class="embed-code"
|
||||
readonly
|
||||
onClick={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
dir="auto"
|
||||
>
|
||||
{htmlCode}
|
||||
</textarea>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
try {
|
||||
navigator.clipboard.writeText(htmlCode);
|
||||
showToast(t`HTML code copied`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(t`Unable to copy HTML code`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="clipboard" />{' '}
|
||||
<span>
|
||||
<Trans>Copy</Trans>
|
||||
</span>
|
||||
</button>
|
||||
{!!mediaAttachments?.length && (
|
||||
<section>
|
||||
<p>
|
||||
<Trans>Media attachments:</Trans>
|
||||
</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>
|
||||
<Trans>Account Emojis:</Trans>
|
||||
</p>
|
||||
<ul>
|
||||
{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>
|
||||
<Trans>static URL</Trans>
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!emojis?.length && (
|
||||
<section>
|
||||
<p>
|
||||
<Trans>Emojis:</Trans>
|
||||
</p>
|
||||
<ul>
|
||||
{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>
|
||||
<Trans>static URL</Trans>
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<small>
|
||||
<p>
|
||||
<Trans>Notes:</Trans>
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<Trans>
|
||||
This is static, unstyled and scriptless. You may need to apply
|
||||
your own styles and edit as needed.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
Polls are not interactive, becomes a list with vote counts.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>
|
||||
Media attachments can be images, videos, audios or any file
|
||||
types.
|
||||
</Trans>
|
||||
</li>
|
||||
<li>
|
||||
<Trans>Post could be edited or deleted later.</Trans>
|
||||
</li>
|
||||
</ul>
|
||||
</small>
|
||||
</section>
|
||||
<h3>
|
||||
<Trans>Preview</Trans>
|
||||
</h3>
|
||||
<output
|
||||
class="embed-preview"
|
||||
dangerouslySetInnerHTML={{ __html: htmlCode }}
|
||||
dir="auto"
|
||||
/>
|
||||
<p>
|
||||
<small>
|
||||
<Trans>Note: This preview is lightly styled.</Trans>
|
||||
</small>
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PostEmbedModal;
|
|
@ -0,0 +1,68 @@
|
|||
import { forwardRef } from 'preact/compat';
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
|
||||
import Icon from './icon';
|
||||
|
||||
const StatusButton = forwardRef((props, ref) => {
|
||||
let {
|
||||
checked,
|
||||
count,
|
||||
class: className,
|
||||
title,
|
||||
alt,
|
||||
size,
|
||||
icon,
|
||||
iconSize = 'l',
|
||||
onClick,
|
||||
...otherProps
|
||||
} = 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
|
||||
ref={ref}
|
||||
type="button"
|
||||
title={buttonTitle}
|
||||
class={`plain ${size ? 'small' : ''} ${className} ${
|
||||
checked ? 'checked' : ''
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
if (!onClick) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick(e);
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon icon={icon} size={iconSize} alt={iconAlt} />
|
||||
{!!count && (
|
||||
<>
|
||||
{' '}
|
||||
<small title={count}>{shortenNumber(count)}</small>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export default StatusButton;
|
|
@ -0,0 +1,290 @@
|
|||
import '@justinribeiro/lite-youtube';
|
||||
|
||||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import getDomain from '../utils/get-domain';
|
||||
import isMastodonLinkMaybe from '../utils/isMastodonLinkMaybe';
|
||||
import states from '../utils/states';
|
||||
import unfurlMastodonLink from '../utils/unfurl-link';
|
||||
|
||||
import Byline from './byline';
|
||||
import Icon from './icon';
|
||||
import RelativeTime from './relative-time';
|
||||
|
||||
// "Post": Quote post + card link preview combo
|
||||
// Assume all links from these domains are "posts"
|
||||
// Mastodon links are "posts" too but they are converted to real quote posts and there's too many domains to check
|
||||
// This is just "Progressive Enhancement"
|
||||
function isCardPost(domain) {
|
||||
return [
|
||||
'x.com',
|
||||
'twitter.com',
|
||||
'threads.net',
|
||||
'bsky.app',
|
||||
'bsky.brid.gy',
|
||||
'fed.brid.gy',
|
||||
].includes(domain);
|
||||
}
|
||||
|
||||
function StatusCard({ card, selfReferential, selfAuthor, instance }) {
|
||||
const snapStates = useSnapshot(states);
|
||||
const {
|
||||
blurhash,
|
||||
title,
|
||||
description,
|
||||
html,
|
||||
providerName,
|
||||
providerUrl,
|
||||
authorName,
|
||||
authorUrl,
|
||||
width,
|
||||
height,
|
||||
image,
|
||||
imageDescription,
|
||||
url,
|
||||
type,
|
||||
embedUrl,
|
||||
language,
|
||||
publishedAt,
|
||||
authors,
|
||||
} = 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' : '';
|
||||
|
||||
const [cardStatusURL, setCardStatusURL] = useState(null);
|
||||
// const [cardStatusID, setCardStatusID] = useState(null);
|
||||
useEffect(() => {
|
||||
if (hasText && image && !selfReferential && 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.$select(id).fetch();
|
||||
// saveStatus(status, instance);
|
||||
// setCardStatusID(id);
|
||||
// })();
|
||||
});
|
||||
}
|
||||
}, [hasText, image, selfReferential]);
|
||||
|
||||
// if (cardStatusID) {
|
||||
// return (
|
||||
// <Status statusID={cardStatusID} instance={instance} size="s" readOnly />
|
||||
// );
|
||||
// }
|
||||
|
||||
if (snapStates.unfurledLinks[url]) return null;
|
||||
|
||||
const hasIframeHTML = /<iframe/i.test(html);
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
if (hasIframeHTML) {
|
||||
e.preventDefault();
|
||||
states.showEmbedModal = {
|
||||
html,
|
||||
url: url || embedUrl,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
},
|
||||
[hasIframeHTML],
|
||||
);
|
||||
|
||||
const [blurhashImage, setBlurhashImage] = useState(null);
|
||||
if (hasText && (image || (type === 'photo' && blurhash))) {
|
||||
const domain = getDomain(url);
|
||||
const rgbAverageColor =
|
||||
image && blurhash ? getBlurHashAverageColor(blurhash) : null;
|
||||
if (!image) {
|
||||
const w = 44;
|
||||
const h = 44;
|
||||
const blurhashPixels = decodeBlurHash(blurhash, w, h);
|
||||
const canvas = window.OffscreenCanvas
|
||||
? new OffscreenCanvas(1, 1)
|
||||
: document.createElement('canvas');
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
const imageData = ctx.createImageData(w, h);
|
||||
imageData.data.set(blurhashPixels);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
try {
|
||||
if (window.OffscreenCanvas) {
|
||||
canvas.convertToBlob().then((blob) => {
|
||||
setBlurhashImage(URL.createObjectURL(blob));
|
||||
});
|
||||
} else {
|
||||
setBlurhashImage(canvas.toDataURL());
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently fail
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const isPost = isCardPost(domain);
|
||||
|
||||
return (
|
||||
<Byline hidden={!!selfAuthor} authors={authors}>
|
||||
<a
|
||||
href={cardStatusURL || url}
|
||||
target={cardStatusURL ? null : '_blank'}
|
||||
rel="nofollow noopener"
|
||||
class={`card link ${isPost ? 'card-post' : ''} ${
|
||||
blurhashImage ? '' : size
|
||||
}`}
|
||||
style={{
|
||||
'--average-color':
|
||||
rgbAverageColor && `rgb(${rgbAverageColor.join(',')})`,
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div class="card-image">
|
||||
<img
|
||||
src={image || blurhashImage}
|
||||
width={width}
|
||||
height={height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
fetchPriority="low"
|
||||
alt={imageDescription || ''}
|
||||
onError={(e) => {
|
||||
try {
|
||||
e.target.style.display = 'none';
|
||||
} catch (e) {}
|
||||
}}
|
||||
style={{
|
||||
'--anim-duration':
|
||||
width &&
|
||||
height &&
|
||||
`${Math.min(
|
||||
Math.max(Math.max(width, height) / 100, 5),
|
||||
120,
|
||||
)}s`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="meta-container" lang={language}>
|
||||
<p class="meta domain">
|
||||
<span class="domain">{domain}</span>{' '}
|
||||
{!!publishedAt && <>· </>}
|
||||
{!!publishedAt && (
|
||||
<>
|
||||
<RelativeTime datetime={publishedAt} format="micro" />
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p class="title" dir="auto" title={title}>
|
||||
{title}
|
||||
</p>
|
||||
<p class="meta" dir="auto" title={description}>
|
||||
{description ||
|
||||
(!!publishedAt && (
|
||||
<RelativeTime datetime={publishedAt} format="micro" />
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</Byline>
|
||||
);
|
||||
} else if (type === 'photo') {
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="nofollow noopener"
|
||||
class="card photo"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<img
|
||||
src={embedUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={title || description}
|
||||
loading="lazy"
|
||||
style={{
|
||||
height: 'auto',
|
||||
aspectRatio: `${width}/${height}`,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} 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) {
|
||||
return (
|
||||
<a class="card video" onClick={handleClick}>
|
||||
<lite-youtube videoid={videoID} nocookie autoPause></lite-youtube>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
// return (
|
||||
// <div
|
||||
// class="card video"
|
||||
// style={{
|
||||
// aspectRatio: `${width}/${height}`,
|
||||
// }}
|
||||
// dangerouslySetInnerHTML={{ __html: html }}
|
||||
// />
|
||||
// );
|
||||
}
|
||||
if (hasText && !image) {
|
||||
const domain = getDomain(url);
|
||||
const isPost = isCardPost(domain);
|
||||
return (
|
||||
<a
|
||||
href={cardStatusURL || url}
|
||||
target={cardStatusURL ? null : '_blank'}
|
||||
rel="nofollow noopener"
|
||||
class={`card link ${isPost ? 'card-post' : ''} no-image`}
|
||||
lang={language}
|
||||
dir="auto"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div class="meta-container">
|
||||
<p class="meta domain">
|
||||
<span class="domain">
|
||||
<Icon icon="link" size="s" /> <span>{domain}</span>
|
||||
</span>{' '}
|
||||
{!!publishedAt && <>· </>}
|
||||
{!!publishedAt && (
|
||||
<>
|
||||
<RelativeTime datetime={publishedAt} format="micro" />
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p class="title" title={title}>
|
||||
{title}
|
||||
</p>
|
||||
<p class="meta" title={description || providerName || authorName}>
|
||||
{description || providerName || authorName}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StatusCard;
|
|
@ -0,0 +1,82 @@
|
|||
import { Trans } from '@lingui/react/macro';
|
||||
import { useContext } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import FilterContext from '../utils/filter-context';
|
||||
import { isFiltered } from '../utils/filters';
|
||||
import states, { getStatus, statusKey } from '../utils/states';
|
||||
import statusPeek from '../utils/status-peek';
|
||||
import { getCurrentAccID } from '../utils/store-utils';
|
||||
|
||||
import Avatar from './avatar';
|
||||
|
||||
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 {
|
||||
account: { id: accountId },
|
||||
sensitive,
|
||||
spoilerText,
|
||||
account: { avatar, avatarStatic, bot } = {},
|
||||
visibility,
|
||||
content,
|
||||
language,
|
||||
filtered,
|
||||
} = status;
|
||||
if (sensitive || spoilerText) return null;
|
||||
if (!content) return null;
|
||||
|
||||
const srKey = statusKey(id, instance);
|
||||
const statusPeekText = statusPeek(status);
|
||||
|
||||
const currentAccount = getCurrentAccID();
|
||||
const isSelf = currentAccount && currentAccount === accountId;
|
||||
|
||||
const filterContext = useContext(FilterContext);
|
||||
let filterInfo = !isSelf && isFiltered(filtered, filterContext);
|
||||
|
||||
// This is fine. Images are converted to emojis so they are
|
||||
// in a way, already "obscured"
|
||||
if (filterInfo?.action === 'blur') filterInfo = null;
|
||||
|
||||
if (filterInfo?.action === 'hide') return null;
|
||||
|
||||
const filterTitleStr = filterInfo?.titlesStr || '';
|
||||
|
||||
return (
|
||||
<article
|
||||
class={`status compact-reply shazam ${
|
||||
visibility === 'direct' ? 'visibility-direct' : ''
|
||||
}`}
|
||||
tabindex="-1"
|
||||
data-state-post-id={srKey}
|
||||
>
|
||||
<Avatar url={avatarStatic || avatar} squircle={bot} />
|
||||
<div
|
||||
class="content-compact"
|
||||
title={statusPeekText}
|
||||
lang={language}
|
||||
dir="auto"
|
||||
>
|
||||
{filterInfo ? (
|
||||
<b class="status-filtered-badge badge-meta" title={filterTitleStr}>
|
||||
<span>
|
||||
<Trans>Filtered</Trans>
|
||||
</span>
|
||||
<span>{filterTitleStr}</span>
|
||||
</b>
|
||||
) : (
|
||||
<span>{statusPeekText}</span>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default StatusCompact;
|
Plik diff jest za duży
Load Diff
|
@ -34,7 +34,7 @@ msgstr ""
|
|||
|
||||
#: src/components/account-block.jsx:190
|
||||
#: src/components/account-info.jsx:710
|
||||
#: src/components/status.jsx:754
|
||||
#: src/components/status.jsx:519
|
||||
msgid "Group"
|
||||
msgstr ""
|
||||
|
||||
|
@ -108,11 +108,11 @@ msgstr ""
|
|||
#: src/components/compose.jsx:2812
|
||||
#: src/components/media-alt-modal.jsx:55
|
||||
#: src/components/media-modal.jsx:363
|
||||
#: src/components/status.jsx:2006
|
||||
#: src/components/status.jsx:2023
|
||||
#: src/components/status.jsx:2154
|
||||
#: src/components/status.jsx:2778
|
||||
#: src/components/status.jsx:2781
|
||||
#: src/components/status.jsx:1771
|
||||
#: src/components/status.jsx:1788
|
||||
#: src/components/status.jsx:1919
|
||||
#: src/components/status.jsx:2543
|
||||
#: src/components/status.jsx:2546
|
||||
#: src/pages/account-statuses.jsx:539
|
||||
#: src/pages/accounts.jsx:118
|
||||
#: src/pages/hashtag.jsx:203
|
||||
|
@ -217,7 +217,7 @@ msgid "Original"
|
|||
msgstr ""
|
||||
|
||||
#: src/components/account-info.jsx:978
|
||||
#: src/components/status.jsx:2563
|
||||
#: src/components/status.jsx:2328
|
||||
#: src/pages/catchup.jsx:71
|
||||
#: src/pages/catchup.jsx:1448
|
||||
#: src/pages/catchup.jsx:2061
|
||||
|
@ -343,30 +343,30 @@ msgid "Add/Remove from Lists"
|
|||
msgstr ""
|
||||
|
||||
#: src/components/account-info.jsx:1522
|
||||
#: src/components/status.jsx:1428
|
||||
#: src/components/status.jsx:1193
|
||||
msgid "Link copied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/account-info.jsx:1525
|
||||
#: src/components/status.jsx:1431
|
||||
#: src/components/status.jsx:1196
|
||||
msgid "Unable to copy link"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/account-info.jsx:1531
|
||||
#: src/components/post-embed-modal.jsx:232
|
||||
#: src/components/shortcuts-settings.jsx:1059
|
||||
#: src/components/status.jsx:1437
|
||||
#: src/components/status.jsx:3556
|
||||
#: src/components/status.jsx:1202
|
||||
msgid "Copy"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/account-info.jsx:1546
|
||||
#: src/components/shortcuts-settings.jsx:1077
|
||||
#: src/components/status.jsx:1453
|
||||
#: src/components/status.jsx:1218
|
||||
msgid "Sharing doesn't seem to work."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/account-info.jsx:1552
|
||||
#: src/components/status.jsx:1459
|
||||
#: src/components/status.jsx:1224
|
||||
msgid "Share…"
|
||||
msgstr ""
|
||||
|
||||
|
@ -476,13 +476,13 @@ msgstr ""
|
|||
#: src/components/media-alt-modal.jsx:43
|
||||
#: src/components/media-modal.jsx:327
|
||||
#: src/components/notification-service.jsx:157
|
||||
#: src/components/post-embed-modal.jsx:196
|
||||
#: src/components/report-modal.jsx:118
|
||||
#: src/components/shortcuts-settings.jsx:230
|
||||
#: src/components/shortcuts-settings.jsx:583
|
||||
#: src/components/shortcuts-settings.jsx:783
|
||||
#: src/components/status.jsx:3280
|
||||
#: src/components/status.jsx:3520
|
||||
#: src/components/status.jsx:4029
|
||||
#: src/components/status.jsx:2742
|
||||
#: src/components/status.jsx:2954
|
||||
#: src/pages/accounts.jsx:45
|
||||
#: src/pages/catchup.jsx:1584
|
||||
#: src/pages/filters.jsx:225
|
||||
|
@ -605,6 +605,11 @@ msgstr ""
|
|||
msgid "Cloak mode enabled"
|
||||
msgstr ""
|
||||
|
||||
#. More from [Author]
|
||||
#: src/components/byline.jsx:17
|
||||
msgid "More from <0/>"
|
||||
msgstr "More from <0/>"
|
||||
|
||||
#: src/components/columns.jsx:27
|
||||
#: src/components/nav-menu.jsx:181
|
||||
#: src/components/shortcuts-settings.jsx:139
|
||||
|
@ -732,7 +737,7 @@ msgid "Attachment #{i} failed"
|
|||
msgstr "Attachment #{i} failed"
|
||||
|
||||
#: src/components/compose.jsx:1215
|
||||
#: src/components/status.jsx:2338
|
||||
#: src/components/status.jsx:2103
|
||||
#: src/components/timeline.jsx:1015
|
||||
msgid "Content warning"
|
||||
msgstr ""
|
||||
|
@ -742,7 +747,7 @@ msgid "Content warning or sensitive media"
|
|||
msgstr "Content warning or sensitive media"
|
||||
|
||||
#: src/components/compose.jsx:1267
|
||||
#: src/components/status.jsx:101
|
||||
#: src/components/status.jsx:87
|
||||
#: src/pages/settings.jsx:318
|
||||
msgid "Public"
|
||||
msgstr ""
|
||||
|
@ -750,25 +755,25 @@ msgstr ""
|
|||
#: src/components/compose.jsx:1272
|
||||
#: src/components/nav-menu.jsx:349
|
||||
#: src/components/shortcuts-settings.jsx:165
|
||||
#: src/components/status.jsx:102
|
||||
#: src/components/status.jsx:88
|
||||
msgid "Local"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/compose.jsx:1276
|
||||
#: src/components/status.jsx:103
|
||||
#: src/components/status.jsx:89
|
||||
#: src/pages/settings.jsx:321
|
||||
msgid "Unlisted"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/compose.jsx:1279
|
||||
#: src/components/status.jsx:104
|
||||
#: src/components/status.jsx:90
|
||||
#: src/pages/settings.jsx:324
|
||||
msgid "Followers only"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/compose.jsx:1282
|
||||
#: src/components/status.jsx:105
|
||||
#: src/components/status.jsx:2218
|
||||
#: src/components/status.jsx:91
|
||||
#: src/components/status.jsx:1983
|
||||
msgid "Private mention"
|
||||
msgstr ""
|
||||
|
||||
|
@ -805,10 +810,10 @@ msgstr "Schedule"
|
|||
|
||||
#: src/components/compose.jsx:1671
|
||||
#: src/components/keyboard-shortcuts-help.jsx:155
|
||||
#: src/components/status.jsx:1200
|
||||
#: src/components/status.jsx:1986
|
||||
#: src/components/status.jsx:1987
|
||||
#: src/components/status.jsx:2682
|
||||
#: src/components/status.jsx:965
|
||||
#: src/components/status.jsx:1751
|
||||
#: src/components/status.jsx:1752
|
||||
#: src/components/status.jsx:2447
|
||||
msgid "Reply"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1030,7 +1035,7 @@ msgstr ""
|
|||
|
||||
#: src/components/drafts.jsx:126
|
||||
#: src/components/list-add-edit.jsx:188
|
||||
#: src/components/status.jsx:1603
|
||||
#: src/components/status.jsx:1368
|
||||
#: src/pages/filters.jsx:603
|
||||
#: src/pages/scheduled-posts.jsx:368
|
||||
msgid "Delete…"
|
||||
|
@ -1239,10 +1244,10 @@ msgid "<0>l</0> or <1>f</1>"
|
|||
msgstr ""
|
||||
|
||||
#: src/components/keyboard-shortcuts-help.jsx:176
|
||||
#: src/components/status.jsx:1208
|
||||
#: src/components/status.jsx:2709
|
||||
#: src/components/status.jsx:2732
|
||||
#: src/components/status.jsx:2733
|
||||
#: src/components/status.jsx:973
|
||||
#: src/components/status.jsx:2474
|
||||
#: src/components/status.jsx:2497
|
||||
#: src/components/status.jsx:2498
|
||||
msgid "Boost"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1251,9 +1256,9 @@ msgid "<0>Shift</0> + <1>b</1>"
|
|||
msgstr ""
|
||||
|
||||
#: src/components/keyboard-shortcuts-help.jsx:184
|
||||
#: src/components/status.jsx:1271
|
||||
#: src/components/status.jsx:2757
|
||||
#: src/components/status.jsx:2758
|
||||
#: src/components/status.jsx:1036
|
||||
#: src/components/status.jsx:2522
|
||||
#: src/components/status.jsx:2523
|
||||
msgid "Bookmark"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1312,20 +1317,38 @@ msgstr ""
|
|||
msgid "Posts on this list are hidden from Home/Following"
|
||||
msgstr "Posts on this list are hidden from Home/Following"
|
||||
|
||||
#: src/components/math-block.jsx:132
|
||||
msgid "Unable to format math"
|
||||
msgstr "Unable to format math"
|
||||
|
||||
#: src/components/math-block.jsx:146
|
||||
msgid "Math expressions found."
|
||||
msgstr "Math expressions found."
|
||||
|
||||
#. Action to switch from rendered math back to raw (LaTeX) markup
|
||||
#: src/components/math-block.jsx:149
|
||||
msgid "Show markup"
|
||||
msgstr "Show markup"
|
||||
|
||||
#. Action to render math expressions from raw (LaTeX) markup
|
||||
#: src/components/math-block.jsx:154
|
||||
msgid "Format math"
|
||||
msgstr "Format math"
|
||||
|
||||
#: src/components/media-alt-modal.jsx:48
|
||||
#: src/components/media.jsx:51
|
||||
msgid "Media description"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/media-alt-modal.jsx:67
|
||||
#: src/components/status.jsx:1314
|
||||
#: src/components/status.jsx:1323
|
||||
#: src/components/status.jsx:1079
|
||||
#: src/components/status.jsx:1088
|
||||
#: src/components/translation-block.jsx:239
|
||||
msgid "Translate"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/media-alt-modal.jsx:78
|
||||
#: src/components/status.jsx:1342
|
||||
#: src/components/status.jsx:1107
|
||||
msgid "Speak"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1362,9 +1385,9 @@ msgid "Filtered: {filterTitleStr}"
|
|||
msgstr ""
|
||||
|
||||
#: src/components/media-post.jsx:133
|
||||
#: src/components/status.jsx:3859
|
||||
#: src/components/status.jsx:3955
|
||||
#: src/components/status.jsx:4033
|
||||
#: src/components/status-compact.jsx:70
|
||||
#: src/components/status.jsx:2880
|
||||
#: src/components/status.jsx:2958
|
||||
#: src/components/timeline.jsx:1004
|
||||
#: src/pages/catchup.jsx:75
|
||||
#: src/pages/catchup.jsx:1880
|
||||
|
@ -1676,8 +1699,8 @@ msgid "[Unknown notification type: {type}]"
|
|||
msgstr ""
|
||||
|
||||
#: src/components/notification.jsx:451
|
||||
#: src/components/status.jsx:1285
|
||||
#: src/components/status.jsx:1295
|
||||
#: src/components/status.jsx:1050
|
||||
#: src/components/status.jsx:1060
|
||||
msgid "Boosted/Liked by…"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1703,7 +1726,7 @@ msgid "View #Wrapstodon"
|
|||
msgstr "View #Wrapstodon"
|
||||
|
||||
#: src/components/notification.jsx:801
|
||||
#: src/components/status.jsx:486
|
||||
#: src/components/status.jsx:260
|
||||
msgid "Read more →"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1766,6 +1789,68 @@ msgstr ""
|
|||
msgid "Ending"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:201
|
||||
#: src/components/status.jsx:1237
|
||||
msgid "Embed post"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:206
|
||||
msgid "HTML Code"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:223
|
||||
msgid "HTML code copied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:226
|
||||
msgid "Unable to copy HTML code"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:238
|
||||
msgid "Media attachments:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:260
|
||||
msgid "Account Emojis:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:291
|
||||
#: src/components/post-embed-modal.jsx:336
|
||||
msgid "static URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:305
|
||||
msgid "Emojis:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:350
|
||||
msgid "Notes:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:354
|
||||
msgid "This is static, unstyled and scriptless. You may need to apply your own styles and edit as needed."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:360
|
||||
msgid "Polls are not interactive, becomes a list with vote counts."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:365
|
||||
msgid "Media attachments can be images, videos, audios or any file types."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:371
|
||||
msgid "Post could be edited or deleted later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:377
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/post-embed-modal.jsx:386
|
||||
msgid "Note: This preview is lightly styled."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/recent-searches.jsx:27
|
||||
msgid "Cleared recent searches"
|
||||
msgstr "Cleared recent searches"
|
||||
|
@ -2030,7 +2115,7 @@ msgid "Move down"
|
|||
msgstr ""
|
||||
|
||||
#: src/components/shortcuts-settings.jsx:379
|
||||
#: src/components/status.jsx:1565
|
||||
#: src/components/status.jsx:1330
|
||||
#: src/pages/list.jsx:195
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
@ -2229,356 +2314,271 @@ msgstr ""
|
|||
msgid "Import/export settings from/to instance server (Very experimental)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:351
|
||||
msgid "Unable to format math"
|
||||
msgstr "Unable to format math"
|
||||
|
||||
#: src/components/status.jsx:365
|
||||
msgid "Math expressions found."
|
||||
msgstr "Math expressions found."
|
||||
|
||||
#. Action to switch from rendered math back to raw (LaTeX) markup
|
||||
#: src/components/status.jsx:368
|
||||
msgid "Show markup"
|
||||
msgstr "Show markup"
|
||||
|
||||
#. Action to render math expressions from raw (LaTeX) markup
|
||||
#: src/components/status.jsx:373
|
||||
msgid "Format math"
|
||||
msgstr "Format math"
|
||||
|
||||
#: src/components/status.jsx:778
|
||||
#: src/components/status.jsx:543
|
||||
msgid "<0/> <1>boosted</1>"
|
||||
msgstr "<0/> <1>boosted</1>"
|
||||
|
||||
#: src/components/status.jsx:881
|
||||
#: src/components/status.jsx:646
|
||||
msgid "Sorry, your current logged-in instance can't interact with this post from another instance."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: username || acct
|
||||
#: src/components/status.jsx:1035
|
||||
#: src/components/status.jsx:800
|
||||
msgid "Unliked @{0}'s post"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: username || acct
|
||||
#: src/components/status.jsx:1036
|
||||
#: src/components/status.jsx:801
|
||||
msgid "Liked @{0}'s post"
|
||||
msgstr "Liked @{0}'s post"
|
||||
|
||||
#. placeholder {0}: username || acct
|
||||
#: src/components/status.jsx:1075
|
||||
#: src/components/status.jsx:840
|
||||
msgid "Unbookmarked @{0}'s post"
|
||||
msgstr "Unbookmarked @{0}'s post"
|
||||
|
||||
#. placeholder {0}: username || acct
|
||||
#: src/components/status.jsx:1076
|
||||
#: src/components/status.jsx:841
|
||||
msgid "Bookmarked @{0}'s post"
|
||||
msgstr "Bookmarked @{0}'s post"
|
||||
|
||||
#: src/components/status.jsx:1177
|
||||
#: src/components/status.jsx:942
|
||||
msgid "Some media have no descriptions."
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: rtf.format(-statusMonthsAgo, 'month')
|
||||
#: src/components/status.jsx:1184
|
||||
#: src/components/status.jsx:949
|
||||
msgid "Old post (<0>{0}</0>)"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1208
|
||||
#: src/components/status.jsx:1248
|
||||
#: src/components/status.jsx:2709
|
||||
#: src/components/status.jsx:2732
|
||||
#: src/components/status.jsx:973
|
||||
#: src/components/status.jsx:1013
|
||||
#: src/components/status.jsx:2474
|
||||
#: src/components/status.jsx:2497
|
||||
msgid "Unboost"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1224
|
||||
#: src/components/status.jsx:2724
|
||||
#: src/components/status.jsx:989
|
||||
#: src/components/status.jsx:2489
|
||||
msgid "Quote"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: username || acct
|
||||
#: src/components/status.jsx:1236
|
||||
#: src/components/status.jsx:1702
|
||||
#: src/components/status.jsx:1001
|
||||
#: src/components/status.jsx:1467
|
||||
msgid "Unboosted @{0}'s post"
|
||||
msgstr "Unboosted @{0}'s post"
|
||||
|
||||
#. placeholder {0}: username || acct
|
||||
#: src/components/status.jsx:1237
|
||||
#: src/components/status.jsx:1703
|
||||
#: src/components/status.jsx:1002
|
||||
#: src/components/status.jsx:1468
|
||||
msgid "Boosted @{0}'s post"
|
||||
msgstr "Boosted @{0}'s post"
|
||||
|
||||
#: src/components/status.jsx:1249
|
||||
#: src/components/status.jsx:1014
|
||||
msgid "Boost…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1261
|
||||
#: src/components/status.jsx:1996
|
||||
#: src/components/status.jsx:2745
|
||||
#: src/components/status.jsx:1026
|
||||
#: src/components/status.jsx:1761
|
||||
#: src/components/status.jsx:2510
|
||||
msgid "Unlike"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1262
|
||||
#: src/components/status.jsx:1996
|
||||
#: src/components/status.jsx:1997
|
||||
#: src/components/status.jsx:2745
|
||||
#: src/components/status.jsx:2746
|
||||
#: src/components/status.jsx:1027
|
||||
#: src/components/status.jsx:1761
|
||||
#: src/components/status.jsx:1762
|
||||
#: src/components/status.jsx:2510
|
||||
#: src/components/status.jsx:2511
|
||||
msgid "Like"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1271
|
||||
#: src/components/status.jsx:2757
|
||||
#: src/components/status.jsx:1036
|
||||
#: src/components/status.jsx:2522
|
||||
msgid "Unbookmark"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1354
|
||||
#: src/components/status.jsx:1119
|
||||
msgid "Post text copied"
|
||||
msgstr "Post text copied"
|
||||
|
||||
#: src/components/status.jsx:1357
|
||||
#: src/components/status.jsx:1122
|
||||
msgid "Unable to copy post text"
|
||||
msgstr "Unable to copy post text"
|
||||
|
||||
#: src/components/status.jsx:1363
|
||||
#: src/components/status.jsx:1128
|
||||
msgid "Copy post text"
|
||||
msgstr "Copy post text"
|
||||
|
||||
#. placeholder {0}: username || acct
|
||||
#: src/components/status.jsx:1381
|
||||
#: src/components/status.jsx:1146
|
||||
msgid "View post by <0>@{0}</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1402
|
||||
#: src/components/status.jsx:1167
|
||||
msgid "Show Edit History"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1405
|
||||
#: src/components/status.jsx:1170
|
||||
msgid "Edited: {editedDateText}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1472
|
||||
#: src/components/status.jsx:3525
|
||||
msgid "Embed post"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1486
|
||||
#: src/components/status.jsx:1251
|
||||
msgid "Conversation unmuted"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1486
|
||||
#: src/components/status.jsx:1251
|
||||
msgid "Conversation muted"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1492
|
||||
#: src/components/status.jsx:1257
|
||||
msgid "Unable to unmute conversation"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1493
|
||||
#: src/components/status.jsx:1258
|
||||
msgid "Unable to mute conversation"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1502
|
||||
#: src/components/status.jsx:1267
|
||||
msgid "Unmute conversation"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1509
|
||||
#: src/components/status.jsx:1274
|
||||
msgid "Mute conversation"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1525
|
||||
#: src/components/status.jsx:1290
|
||||
msgid "Post unpinned from profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1526
|
||||
#: src/components/status.jsx:1291
|
||||
msgid "Post pinned to profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1531
|
||||
#: src/components/status.jsx:1296
|
||||
msgid "Unable to unpin post"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1531
|
||||
#: src/components/status.jsx:1296
|
||||
msgid "Unable to pin post"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1540
|
||||
#: src/components/status.jsx:1305
|
||||
msgid "Unpin from profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1547
|
||||
#: src/components/status.jsx:1312
|
||||
msgid "Pin to profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1576
|
||||
#: src/components/status.jsx:1341
|
||||
msgid "Delete this post?"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1592
|
||||
#: src/components/status.jsx:1357
|
||||
msgid "Post deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1595
|
||||
#: src/components/status.jsx:1360
|
||||
msgid "Unable to delete post"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1623
|
||||
#: src/components/status.jsx:1388
|
||||
msgid "Report post…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:1997
|
||||
#: src/components/status.jsx:2033
|
||||
#: src/components/status.jsx:2746
|
||||
#: src/components/status.jsx:1762
|
||||
#: src/components/status.jsx:1798
|
||||
#: src/components/status.jsx:2511
|
||||
msgid "Liked"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2030
|
||||
#: src/components/status.jsx:2733
|
||||
#: src/components/status.jsx:1795
|
||||
#: src/components/status.jsx:2498
|
||||
msgid "Boosted"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2040
|
||||
#: src/components/status.jsx:2758
|
||||
#: src/components/status.jsx:1805
|
||||
#: src/components/status.jsx:2523
|
||||
msgid "Bookmarked"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2044
|
||||
#: src/components/status.jsx:1809
|
||||
msgid "Pinned"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2096
|
||||
#: src/components/status.jsx:2571
|
||||
#: src/components/status.jsx:1861
|
||||
#: src/components/status.jsx:2336
|
||||
msgid "Deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2137
|
||||
#: src/components/status.jsx:1902
|
||||
msgid "{repliesCount, plural, one {# reply} other {# replies}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2301
|
||||
#: src/components/status.jsx:2363
|
||||
#: src/components/status.jsx:2467
|
||||
#: src/components/status.jsx:2066
|
||||
#: src/components/status.jsx:2128
|
||||
#: src/components/status.jsx:2232
|
||||
msgid "Show less"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2301
|
||||
#: src/components/status.jsx:2363
|
||||
#: src/components/status.jsx:2066
|
||||
#: src/components/status.jsx:2128
|
||||
msgid "Show content"
|
||||
msgstr ""
|
||||
|
||||
#. placeholder {0}: filterInfo.titlesStr
|
||||
#. placeholder {0}: filterInfo?.titlesStr
|
||||
#: src/components/status.jsx:2463
|
||||
#: src/components/status.jsx:2228
|
||||
#: src/pages/catchup.jsx:1879
|
||||
msgid "Filtered: {0}"
|
||||
msgstr "Filtered: {0}"
|
||||
|
||||
#: src/components/status.jsx:2467
|
||||
#: src/components/status.jsx:2232
|
||||
msgid "Show media"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2606
|
||||
#: src/components/status.jsx:2371
|
||||
msgid "Edited"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:2683
|
||||
#: src/components/status.jsx:2448
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
|
||||
#. More from [Author]
|
||||
#: src/components/status.jsx:2983
|
||||
msgid "More from <0/>"
|
||||
msgstr "More from <0/>"
|
||||
#: src/components/status.jsx:2633
|
||||
msgid "Post hidden by your filters"
|
||||
msgstr "Post hidden by your filters"
|
||||
|
||||
#: src/components/status.jsx:3285
|
||||
#: src/components/status.jsx:2634
|
||||
msgid "Post pending"
|
||||
msgstr "Post pending"
|
||||
|
||||
#: src/components/status.jsx:2635
|
||||
#: src/components/status.jsx:2636
|
||||
#: src/components/status.jsx:2637
|
||||
#: src/components/status.jsx:2638
|
||||
msgid "Post unavailable"
|
||||
msgstr "Post unavailable"
|
||||
|
||||
#: src/components/status.jsx:2747
|
||||
msgid "Edit History"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3289
|
||||
#: src/components/status.jsx:2751
|
||||
msgid "Failed to load history"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3294
|
||||
#: src/components/status.jsx:2756
|
||||
#: src/pages/annual-report.jsx:45
|
||||
msgid "Loading…"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3530
|
||||
msgid "HTML Code"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3547
|
||||
msgid "HTML code copied"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3550
|
||||
msgid "Unable to copy HTML code"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3562
|
||||
msgid "Media attachments:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3584
|
||||
msgid "Account Emojis:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3615
|
||||
#: src/components/status.jsx:3660
|
||||
msgid "static URL"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3629
|
||||
msgid "Emojis:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3674
|
||||
msgid "Notes:"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3678
|
||||
msgid "This is static, unstyled and scriptless. You may need to apply your own styles and edit as needed."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3684
|
||||
msgid "Polls are not interactive, becomes a list with vote counts."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3689
|
||||
msgid "Media attachments can be images, videos, audios or any file types."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3695
|
||||
msgid "Post could be edited or deleted later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3701
|
||||
msgid "Preview"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/status.jsx:3710
|
||||
msgid "Note: This preview is lightly styled."
|
||||
msgstr ""
|
||||
|
||||
#. [Name] [Visibility icon] boosted
|
||||
#: src/components/status.jsx:3963
|
||||
#: src/components/status.jsx:2888
|
||||
msgid "<0/> <1/> boosted"
|
||||
msgstr "<0/> <1/> boosted"
|
||||
|
||||
#: src/components/status.jsx:4065
|
||||
msgid "Post hidden by your filters"
|
||||
msgstr "Post hidden by your filters"
|
||||
|
||||
#: src/components/status.jsx:4066
|
||||
msgid "Post pending"
|
||||
msgstr "Post pending"
|
||||
|
||||
#: src/components/status.jsx:4067
|
||||
#: src/components/status.jsx:4068
|
||||
#: src/components/status.jsx:4069
|
||||
#: src/components/status.jsx:4070
|
||||
msgid "Post unavailable"
|
||||
msgstr "Post unavailable"
|
||||
|
||||
#: src/components/thread-badge.jsx:22
|
||||
#: src/components/thread-badge.jsx:37
|
||||
#: src/components/thread-badge.jsx:52
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import mem from './mem';
|
||||
import store from './store';
|
||||
|
||||
export function getAccounts() {
|
||||
|
@ -50,6 +51,16 @@ export function getCurrentAccountID() {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Memoized version of getCurrentAccountID for performance
|
||||
export const getCurrentAccID = mem(
|
||||
() => {
|
||||
return getCurrentAccountID();
|
||||
},
|
||||
{
|
||||
maxAge: 60 * 1000, // 1 minute
|
||||
},
|
||||
);
|
||||
|
||||
export function setCurrentAccountID(id) {
|
||||
try {
|
||||
store.session.set('currentAccount', id);
|
||||
|
|
Ładowanie…
Reference in New Issue