diff --git a/src/components/ICONS.jsx b/src/components/ICONS.jsx
index bb654b7d..242bd6fb 100644
--- a/src/components/ICONS.jsx
+++ b/src/components/ICONS.jsx
@@ -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'),
};
diff --git a/src/components/status.css b/src/components/status.css
index 706c25ab..9d05f3df 100644
--- a/src/components/status.css
+++ b/src/components/status.css
@@ -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 {
diff --git a/src/components/status.jsx b/src/components/status.jsx
index d69a0883..92425e0f 100644
--- a/src/components/status.jsx
+++ b/src/components/status.jsx
@@ -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({
)}
+ {isSizeLarge && (
+
+ )}
{(isSelf || mentionSelf) && }
{(isSelf || mentionSelf) && (
)}
+ {!!showEmbed && (
+ {
+ if (e.target === e.currentTarget) {
+ setShowEmbed(false);
+ }
+ }}
+ >
+ {
+ setShowEmbed(false);
+ }}
+ />
+
+ )}
>
);
@@ -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
+ ? `
+
📊:
+
+ ${poll.options
+ .map(
+ (option) => `
+ -
+ ${option.title}
+ ${option.votesCount >= 0 ? ` (${option.votesCount})` : ''}
+
+ `,
+ )
+ .join('')}
+
`
+ : '') +
+ (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 = `
`;
+ } else if (isVideo) {
+ mediaHTML = `
+
+ ${description ? `${description}` : ''}
+ `;
+ } else if (isAudio) {
+ mediaHTML = `
+
+ ${description ? `${description}` : ''}
+ `;
+ } else {
+ mediaHTML = `
+ 📄 ${
+ description || sourceMediaURL
+ }
+ `;
+ }
+
+ return `${mediaHTML}`;
+ })
+ .join('\n')
+ : '');
+
+ const htmlCode = `
+
+ ${
+ spoilerText
+ ? `
+
+ ${spoilerText}
+ ${contentHTML}
+
+ `
+ : contentHTML
+ }
+
+
+ `;
+
+ 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 (
+
+ {!!onClose && (
+
+ )}
+
+
+ HTML Code
+
+
+ {!!mediaAttachments?.length && (
+
+ )}
+ {!!accountEmojis?.length && (
+
+ Account Emojis:
+
+ {accountEmojis.map((emoji) => {
+ return (
+ -
+
+
+
+ {' '}
+ :{emoji.shortcode}:
(
+
+ url
+
+ )
+ {emoji.staticUrl ? (
+ <>
+ {' '}
+ (
+
+ static
+
+ )
+ >
+ ) : null}
+
+ );
+ })}
+
+
+ )}
+ {!!emojis?.length && (
+
+ Emojis:
+
+ {emojis.map((emoji) => {
+ return (
+ -
+
+
+
+ {' '}
+ :{emoji.shortcode}:
(
+
+ url
+
+ )
+ {emoji.staticUrl ? (
+ <>
+ {' '}
+ (
+
+ static
+
+ )
+ >
+ ) : null}
+
+ );
+ })}
+
+
+ )}
+
+
+ Notes:
+
+ -
+ This is static, unstyled and scriptless. You may need to apply
+ your own styles and edit as needed.
+
+ -
+ Polls are not interactive, becomes a list with vote counts.
+
+ -
+ Media attachments can be images, videos, audios or any file
+ types.
+
+ - Post could be edited or deleted later.
+
+
+
+ Preview
+
+
+ Note: This preview is lightly styled.
+
+
+
+ );
+}
+
function StatusButton({
checked,
count,