mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-02 22:26:57 +01:00
Experimental Embed post
This commit is contained in:
parent
8108151fb6
commit
fcb0074f49
3 changed files with 475 additions and 0 deletions
|
@ -101,4 +101,5 @@ export const ICONS = {
|
||||||
history: () => import('@iconify-icons/mingcute/history-2-line'),
|
history: () => import('@iconify-icons/mingcute/history-2-line'),
|
||||||
document: () => import('@iconify-icons/mingcute/document-line'),
|
document: () => import('@iconify-icons/mingcute/document-line'),
|
||||||
'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-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;
|
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 */
|
/* DELETED */
|
||||||
|
|
||||||
.status-deleted {
|
.status-deleted {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from '@szhsin/react-menu';
|
} from '@szhsin/react-menu';
|
||||||
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { shallowEqual } from 'fast-equals';
|
import { shallowEqual } from 'fast-equals';
|
||||||
|
import prettify from 'html-prettify';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -451,6 +452,7 @@ function Status({
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [showEdited, setShowEdited] = useState(false);
|
const [showEdited, setShowEdited] = useState(false);
|
||||||
|
const [showEmbed, setShowEmbed] = useState(false);
|
||||||
|
|
||||||
const spoilerContentRef = useTruncated();
|
const spoilerContentRef = useTruncated();
|
||||||
const contentRef = useTruncated();
|
const contentRef = useTruncated();
|
||||||
|
@ -935,6 +937,16 @@ function Status({
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isSizeLarge && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setShowEmbed(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="code" />
|
||||||
|
<span>Embed</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{(isSelf || mentionSelf) && <MenuDivider />}
|
{(isSelf || mentionSelf) && <MenuDivider />}
|
||||||
{(isSelf || mentionSelf) && (
|
{(isSelf || mentionSelf) && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
@ -1994,6 +2006,24 @@ function Status({
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
{!!showEmbed && (
|
||||||
|
<Modal
|
||||||
|
class="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
setShowEmbed(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EmbedModal
|
||||||
|
post={status}
|
||||||
|
instance={instance}
|
||||||
|
onClose={() => {
|
||||||
|
setShowEmbed(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</article>
|
</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({
|
function StatusButton({
|
||||||
checked,
|
checked,
|
||||||
count,
|
count,
|
||||||
|
|
Loading…
Reference in a new issue