Experimental Embed post
This commit is contained in:
parent
8108151fb6
commit
fcb0074f49
|
@ -101,4 +101,5 @@ export const ICONS = {
|
|||
history: () => import('@iconify-icons/mingcute/history-2-line'),
|
||||
document: () => import('@iconify-icons/mingcute/document-line'),
|
||||
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-line'),
|
||||
code: () => import('@iconify-icons/mingcute/code-line'),
|
||||
};
|
||||
|
|
|
@ -2135,6 +2135,97 @@ a.card:is(:hover, :focus):visited {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* EMBED */
|
||||
|
||||
#embed-post {
|
||||
> main > section {
|
||||
p {
|
||||
margin-block: 0.5em;
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-inline: 1em;
|
||||
}
|
||||
p + ul {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.embed-code {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
min-height: 12em;
|
||||
max-height: 40vh;
|
||||
font-family: var(--monospace-font);
|
||||
font-size: 0.8em;
|
||||
border-color: var(--link-color);
|
||||
/* background-color: var(--bg-faded-color); */
|
||||
}
|
||||
|
||||
.links-list {
|
||||
a {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.embed-preview {
|
||||
display: block;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
font-size: 0.9em;
|
||||
border: 2px dashed var(--link-light-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px -4px var(--drop-shadow-color),
|
||||
0 8px 32px -8px var(--drop-shadow-color);
|
||||
padding: 16px;
|
||||
|
||||
/* Interactive elements */
|
||||
button,
|
||||
a,
|
||||
video,
|
||||
audio,
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
iframe,
|
||||
object,
|
||||
embed {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1em;
|
||||
border-inline-start: 4px solid var(--outline-color);
|
||||
padding-inline-start: 1em;
|
||||
|
||||
> p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-inline: 0;
|
||||
padding-inline: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin-inline: 0;
|
||||
|
||||
img,
|
||||
video,
|
||||
audio {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* DELETED */
|
||||
|
||||
.status-deleted {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from '@szhsin/react-menu';
|
||||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||
import { shallowEqual } from 'fast-equals';
|
||||
import prettify from 'html-prettify';
|
||||
import { memo } from 'preact/compat';
|
||||
import {
|
||||
useCallback,
|
||||
|
@ -451,6 +452,7 @@ function Status({
|
|||
]);
|
||||
|
||||
const [showEdited, setShowEdited] = useState(false);
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
|
||||
const spoilerContentRef = useTruncated();
|
||||
const contentRef = useTruncated();
|
||||
|
@ -935,6 +937,16 @@ function Status({
|
|||
</MenuItem>
|
||||
)}
|
||||
</div>
|
||||
{isSizeLarge && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setShowEmbed(true);
|
||||
}}
|
||||
>
|
||||
<Icon icon="code" />
|
||||
<span>Embed</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{(isSelf || mentionSelf) && <MenuDivider />}
|
||||
{(isSelf || mentionSelf) && (
|
||||
<MenuItem
|
||||
|
@ -1994,6 +2006,24 @@ function Status({
|
|||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{!!showEmbed && (
|
||||
<Modal
|
||||
class="light"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowEmbed(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EmbedModal
|
||||
post={status}
|
||||
instance={instance}
|
||||
onClose={() => {
|
||||
setShowEmbed(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
|
@ -2298,6 +2328,359 @@ function EditedAtModal({
|
|||
);
|
||||
}
|
||||
|
||||
function generateHTMLCode(post, instance, level = 0) {
|
||||
const {
|
||||
account: {
|
||||
url: accountURL,
|
||||
displayName,
|
||||
username,
|
||||
emojis: accountEmojis,
|
||||
bot,
|
||||
group,
|
||||
},
|
||||
id,
|
||||
poll,
|
||||
spoilerText,
|
||||
language,
|
||||
editedAt,
|
||||
createdAt,
|
||||
content,
|
||||
mediaAttachments,
|
||||
url,
|
||||
emojis,
|
||||
} = post;
|
||||
|
||||
const sKey = statusKey(id, instance);
|
||||
const quotes = states.statusQuotes[sKey] || [];
|
||||
const uniqueQuotes = quotes.filter(
|
||||
(q, i, arr) => arr.findIndex((q2) => q2.url === q.url) === i,
|
||||
);
|
||||
const quoteStatusesHTML =
|
||||
uniqueQuotes.length && level <= 2
|
||||
? uniqueQuotes
|
||||
.map((quote) => {
|
||||
const { id, instance } = quote;
|
||||
const sKey = statusKey(id, instance);
|
||||
const s = states.statuses[sKey];
|
||||
if (s) {
|
||||
return generateHTMLCode(s, instance, ++level);
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
: '';
|
||||
|
||||
const createdAtDate = new Date(createdAt);
|
||||
// const editedAtDate = editedAt && new Date(editedAt);
|
||||
|
||||
const contentHTML =
|
||||
emojifyText(content, emojis) +
|
||||
'\n' +
|
||||
quoteStatusesHTML +
|
||||
'\n' +
|
||||
(poll?.options?.length
|
||||
? `
|
||||
<p>📊:</p>
|
||||
<ul>
|
||||
${poll.options
|
||||
.map(
|
||||
(option) => `
|
||||
<li>
|
||||
${option.title}
|
||||
${option.votesCount >= 0 ? ` (${option.votesCount})` : ''}
|
||||
</li>
|
||||
`,
|
||||
)
|
||||
.join('')}
|
||||
</ul>`
|
||||
: '') +
|
||||
(mediaAttachments.length > 0
|
||||
? '\n' +
|
||||
mediaAttachments
|
||||
.map((media) => {
|
||||
const {
|
||||
description,
|
||||
meta,
|
||||
previewRemoteUrl,
|
||||
previewUrl,
|
||||
remoteUrl,
|
||||
url,
|
||||
type,
|
||||
} = media;
|
||||
const { original = {}, small } = meta || {};
|
||||
const width = small?.width || original?.width;
|
||||
const height = small?.height || original?.height;
|
||||
|
||||
// Prefer remote over original
|
||||
const sourceMediaURL = remoteUrl || url;
|
||||
const previewMediaURL = previewRemoteUrl || previewUrl;
|
||||
const mediaURL = previewMediaURL || sourceMediaURL;
|
||||
|
||||
const sourceMediaURLObj = sourceMediaURL
|
||||
? new URL(sourceMediaURL)
|
||||
: null;
|
||||
const isVideoMaybe =
|
||||
type === 'unknown' &&
|
||||
sourceMediaURLObj &&
|
||||
/\.(mp4|m4r|m4v|mov|webm)$/i.test(sourceMediaURLObj.pathname);
|
||||
const isAudioMaybe =
|
||||
type === 'unknown' &&
|
||||
sourceMediaURLObj &&
|
||||
/\.(mp3|ogg|wav|m4a|m4p|m4b)$/i.test(sourceMediaURLObj.pathname);
|
||||
const isImage =
|
||||
type === 'image' ||
|
||||
(type === 'unknown' &&
|
||||
previewMediaURL &&
|
||||
!isVideoMaybe &&
|
||||
!isAudioMaybe);
|
||||
const isVideo = type === 'gifv' || type === 'video' || isVideoMaybe;
|
||||
const isAudio = type === 'audio' || isAudioMaybe;
|
||||
|
||||
let mediaHTML = '';
|
||||
if (isImage) {
|
||||
mediaHTML = `<img src="${mediaURL}" width="${width}" height="${height}" alt="${description}" loading="lazy" />`;
|
||||
} else if (isVideo) {
|
||||
mediaHTML = `
|
||||
<video src="${sourceMediaURL}" width="${width}" height="${height}" controls preload="auto" poster="${previewMediaURL}" loading="lazy"></video>
|
||||
${description ? `<figcaption>${description}</figcaption>` : ''}
|
||||
`;
|
||||
} else if (isAudio) {
|
||||
mediaHTML = `
|
||||
<audio src="${sourceMediaURL}" controls preload="auto"></audio>
|
||||
${description ? `<figcaption>${description}</figcaption>` : ''}
|
||||
`;
|
||||
} else {
|
||||
mediaHTML = `
|
||||
<a href="${sourceMediaURL}">📄 ${
|
||||
description || sourceMediaURL
|
||||
}</a>
|
||||
`;
|
||||
}
|
||||
|
||||
return `<figure>${mediaHTML}</figure>`;
|
||||
})
|
||||
.join('\n')
|
||||
: '');
|
||||
|
||||
const htmlCode = `
|
||||
<blockquote lang="${language}" cite="${url}">
|
||||
${
|
||||
spoilerText
|
||||
? `
|
||||
<details>
|
||||
<summary>${spoilerText}</summary>
|
||||
${contentHTML}
|
||||
</details>
|
||||
`
|
||||
: contentHTML
|
||||
}
|
||||
<footer>
|
||||
— ${emojifyText(
|
||||
displayName,
|
||||
accountEmojis,
|
||||
)} (@${username}) <a href="${url}"><time datetime="${createdAtDate.toISOString()}">${createdAtDate.toLocaleString()}</time></a>
|
||||
</footer>
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
return prettify(htmlCode);
|
||||
}
|
||||
|
||||
function EmbedModal({ post, instance, onClose }) {
|
||||
const {
|
||||
account: {
|
||||
url: accountURL,
|
||||
displayName,
|
||||
username,
|
||||
emojis: accountEmojis,
|
||||
bot,
|
||||
group,
|
||||
},
|
||||
id,
|
||||
poll,
|
||||
spoilerText,
|
||||
language,
|
||||
editedAt,
|
||||
createdAt,
|
||||
content,
|
||||
mediaAttachments,
|
||||
url,
|
||||
emojis,
|
||||
} = post;
|
||||
|
||||
const htmlCode = generateHTMLCode(post, instance);
|
||||
return (
|
||||
<div id="embed-post" class="sheet">
|
||||
{!!onClose && (
|
||||
<button type="button" class="sheet-close" onClick={onClose}>
|
||||
<Icon icon="x" />
|
||||
</button>
|
||||
)}
|
||||
<header>
|
||||
<h2>Embed post</h2>
|
||||
</header>
|
||||
<main tabIndex="-1">
|
||||
<h3>HTML Code</h3>
|
||||
<textarea
|
||||
class="embed-code"
|
||||
readonly
|
||||
onClick={(e) => {
|
||||
e.target.select();
|
||||
}}
|
||||
>
|
||||
{htmlCode}
|
||||
</textarea>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
try {
|
||||
navigator.clipboard.writeText(htmlCode);
|
||||
showToast('HTML code copied');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast('Unable to copy HTML code');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="clipboard" /> <span>Copy</span>
|
||||
</button>
|
||||
{!!mediaAttachments?.length && (
|
||||
<section>
|
||||
<p>Media attachments:</p>
|
||||
<ol class="links-list">
|
||||
{mediaAttachments.map((media) => {
|
||||
return (
|
||||
<li key={media.id}>
|
||||
<a
|
||||
href={media.remoteUrl || media.url}
|
||||
target="_blank"
|
||||
download
|
||||
>
|
||||
{media.remoteUrl || media.url}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
{!!accountEmojis?.length && (
|
||||
<section>
|
||||
<p>Account Emojis:</p>
|
||||
<ul class="links-list">
|
||||
{accountEmojis.map((emoji) => {
|
||||
return (
|
||||
<li key={emoji.shortcode}>
|
||||
<picture>
|
||||
<source
|
||||
srcset={emoji.staticUrl}
|
||||
media="(prefers-reduced-motion: reduce)"
|
||||
></source>
|
||||
<img
|
||||
class="shortcode-emoji emoji"
|
||||
src={emoji.url}
|
||||
alt={`:${emoji.shortcode}:`}
|
||||
width="16"
|
||||
height="16"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>{' '}
|
||||
<code>:{emoji.shortcode}:</code> (
|
||||
<a href={emoji.url} target="_blank" download>
|
||||
url
|
||||
</a>
|
||||
)
|
||||
{emoji.staticUrl ? (
|
||||
<>
|
||||
{' '}
|
||||
(
|
||||
<a href={emoji.staticUrl} target="_blank" download>
|
||||
static
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!!emojis?.length && (
|
||||
<section>
|
||||
<p>Emojis:</p>
|
||||
<ul class="links-list">
|
||||
{emojis.map((emoji) => {
|
||||
return (
|
||||
<li key={emoji.shortcode}>
|
||||
<picture>
|
||||
<source
|
||||
srcset={emoji.staticUrl}
|
||||
media="(prefers-reduced-motion: reduce)"
|
||||
></source>
|
||||
<img
|
||||
class="shortcode-emoji emoji"
|
||||
src={emoji.url}
|
||||
alt={`:${emoji.shortcode}:`}
|
||||
width="16"
|
||||
height="16"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>{' '}
|
||||
<code>:{emoji.shortcode}:</code> (
|
||||
<a href={emoji.url} target="_blank" download>
|
||||
url
|
||||
</a>
|
||||
)
|
||||
{emoji.staticUrl ? (
|
||||
<>
|
||||
{' '}
|
||||
(
|
||||
<a href={emoji.staticUrl} target="_blank" download>
|
||||
static
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<small>
|
||||
<p>Notes:</p>
|
||||
<ul>
|
||||
<li>
|
||||
This is static, unstyled and scriptless. You may need to apply
|
||||
your own styles and edit as needed.
|
||||
</li>
|
||||
<li>
|
||||
Polls are not interactive, becomes a list with vote counts.
|
||||
</li>
|
||||
<li>
|
||||
Media attachments can be images, videos, audios or any file
|
||||
types.
|
||||
</li>
|
||||
<li>Post could be edited or deleted later.</li>
|
||||
</ul>
|
||||
</small>
|
||||
</section>
|
||||
<h3>Preview</h3>
|
||||
<output
|
||||
class="embed-preview"
|
||||
dangerouslySetInnerHTML={{ __html: htmlCode }}
|
||||
/>
|
||||
<p>
|
||||
<small>Note: This preview is lightly styled.</small>
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusButton({
|
||||
checked,
|
||||
count,
|
||||
|
|
Loading…
Reference in a new issue