From fcb0074f4992fcb78c55eae5a7518e9df3fcf0d9 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Sat, 2 Mar 2024 18:55:05 +0800 Subject: [PATCH] Experimental Embed post --- src/components/ICONS.jsx | 1 + src/components/status.css | 91 +++++++++ src/components/status.jsx | 383 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 475 insertions(+) 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 && ( + { + setShowEmbed(true); + }} + > + + Embed + + )} {(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 = `${description}`; + } 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 && ( + + )} +
+

Embed post

+
+
+

HTML Code

+ + + {!!mediaAttachments?.length && ( +
+

Media attachments:

+ +
+ )} + {!!accountEmojis?.length && ( +
+

Account Emojis:

+ +
+ )} + {!!emojis?.length && ( +
+

Emojis:

+ +
+ )} +
+ +

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,