this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Experimental Embed post

+475
+1
src/components/ICONS.jsx
··· 101 101 history: () => import('@iconify-icons/mingcute/history-2-line'), 102 102 document: () => import('@iconify-icons/mingcute/document-line'), 103 103 'arrows-right': () => import('@iconify-icons/mingcute/arrows-right-line'), 104 + code: () => import('@iconify-icons/mingcute/code-line'), 104 105 };
+91
src/components/status.css
··· 2135 2135 pointer-events: none; 2136 2136 } 2137 2137 2138 + /* EMBED */ 2139 + 2140 + #embed-post { 2141 + > main > section { 2142 + p { 2143 + margin-block: 0.5em; 2144 + } 2145 + ul { 2146 + margin: 0; 2147 + padding-inline: 1em; 2148 + } 2149 + p + ul { 2150 + margin-top: 0; 2151 + padding-top: 0; 2152 + } 2153 + } 2154 + 2155 + .embed-code { 2156 + width: 100%; 2157 + resize: vertical; 2158 + min-height: 12em; 2159 + max-height: 40vh; 2160 + font-family: var(--monospace-font); 2161 + font-size: 0.8em; 2162 + border-color: var(--link-color); 2163 + /* background-color: var(--bg-faded-color); */ 2164 + } 2165 + 2166 + .links-list { 2167 + a { 2168 + display: -webkit-box; 2169 + -webkit-line-clamp: 1; 2170 + -webkit-box-orient: vertical; 2171 + overflow: hidden; 2172 + } 2173 + } 2174 + 2175 + .embed-preview { 2176 + display: block; 2177 + max-height: 40vh; 2178 + overflow: auto; 2179 + font-size: 0.9em; 2180 + border: 2px dashed var(--link-light-color); 2181 + border-radius: 8px; 2182 + box-shadow: 0 4px 8px -4px var(--drop-shadow-color), 2183 + 0 8px 32px -8px var(--drop-shadow-color); 2184 + padding: 16px; 2185 + 2186 + /* Interactive elements */ 2187 + button, 2188 + a, 2189 + video, 2190 + audio, 2191 + input, 2192 + select, 2193 + textarea, 2194 + iframe, 2195 + object, 2196 + embed { 2197 + pointer-events: none; 2198 + } 2199 + 2200 + blockquote { 2201 + margin: 0 0 1em; 2202 + border-inline-start: 4px solid var(--outline-color); 2203 + padding-inline-start: 1em; 2204 + 2205 + > p:first-child { 2206 + margin-top: 0; 2207 + } 2208 + } 2209 + 2210 + ul, 2211 + ol { 2212 + margin-inline: 0; 2213 + padding-inline: 1em; 2214 + } 2215 + 2216 + figure { 2217 + margin-inline: 0; 2218 + 2219 + img, 2220 + video, 2221 + audio { 2222 + max-width: 100%; 2223 + height: auto; 2224 + } 2225 + } 2226 + } 2227 + } 2228 + 2138 2229 /* DELETED */ 2139 2230 2140 2231 .status-deleted {
+383
src/components/status.jsx
··· 10 10 } from '@szhsin/react-menu'; 11 11 import { decodeBlurHash, getBlurHashAverageColor } from 'fast-blurhash'; 12 12 import { shallowEqual } from 'fast-equals'; 13 + import prettify from 'html-prettify'; 13 14 import { memo } from 'preact/compat'; 14 15 import { 15 16 useCallback, ··· 451 452 ]); 452 453 453 454 const [showEdited, setShowEdited] = useState(false); 455 + const [showEmbed, setShowEmbed] = useState(false); 454 456 455 457 const spoilerContentRef = useTruncated(); 456 458 const contentRef = useTruncated(); ··· 935 937 </MenuItem> 936 938 )} 937 939 </div> 940 + {isSizeLarge && ( 941 + <MenuItem 942 + onClick={() => { 943 + setShowEmbed(true); 944 + }} 945 + > 946 + <Icon icon="code" /> 947 + <span>Embed</span> 948 + </MenuItem> 949 + )} 938 950 {(isSelf || mentionSelf) && <MenuDivider />} 939 951 {(isSelf || mentionSelf) && ( 940 952 <MenuItem ··· 1994 2006 /> 1995 2007 </Modal> 1996 2008 )} 2009 + {!!showEmbed && ( 2010 + <Modal 2011 + class="light" 2012 + onClick={(e) => { 2013 + if (e.target === e.currentTarget) { 2014 + setShowEmbed(false); 2015 + } 2016 + }} 2017 + > 2018 + <EmbedModal 2019 + post={status} 2020 + instance={instance} 2021 + onClose={() => { 2022 + setShowEmbed(false); 2023 + }} 2024 + /> 2025 + </Modal> 2026 + )} 1997 2027 </article> 1998 2028 </> 1999 2029 ); ··· 2293 2323 })} 2294 2324 </ol> 2295 2325 )} 2326 + </main> 2327 + </div> 2328 + ); 2329 + } 2330 + 2331 + function generateHTMLCode(post, instance, level = 0) { 2332 + const { 2333 + account: { 2334 + url: accountURL, 2335 + displayName, 2336 + username, 2337 + emojis: accountEmojis, 2338 + bot, 2339 + group, 2340 + }, 2341 + id, 2342 + poll, 2343 + spoilerText, 2344 + language, 2345 + editedAt, 2346 + createdAt, 2347 + content, 2348 + mediaAttachments, 2349 + url, 2350 + emojis, 2351 + } = post; 2352 + 2353 + const sKey = statusKey(id, instance); 2354 + const quotes = states.statusQuotes[sKey] || []; 2355 + const uniqueQuotes = quotes.filter( 2356 + (q, i, arr) => arr.findIndex((q2) => q2.url === q.url) === i, 2357 + ); 2358 + const quoteStatusesHTML = 2359 + uniqueQuotes.length && level <= 2 2360 + ? uniqueQuotes 2361 + .map((quote) => { 2362 + const { id, instance } = quote; 2363 + const sKey = statusKey(id, instance); 2364 + const s = states.statuses[sKey]; 2365 + if (s) { 2366 + return generateHTMLCode(s, instance, ++level); 2367 + } 2368 + }) 2369 + .join('') 2370 + : ''; 2371 + 2372 + const createdAtDate = new Date(createdAt); 2373 + // const editedAtDate = editedAt && new Date(editedAt); 2374 + 2375 + const contentHTML = 2376 + emojifyText(content, emojis) + 2377 + '\n' + 2378 + quoteStatusesHTML + 2379 + '\n' + 2380 + (poll?.options?.length 2381 + ? ` 2382 + <p>📊:</p> 2383 + <ul> 2384 + ${poll.options 2385 + .map( 2386 + (option) => ` 2387 + <li> 2388 + ${option.title} 2389 + ${option.votesCount >= 0 ? ` (${option.votesCount})` : ''} 2390 + </li> 2391 + `, 2392 + ) 2393 + .join('')} 2394 + </ul>` 2395 + : '') + 2396 + (mediaAttachments.length > 0 2397 + ? '\n' + 2398 + mediaAttachments 2399 + .map((media) => { 2400 + const { 2401 + description, 2402 + meta, 2403 + previewRemoteUrl, 2404 + previewUrl, 2405 + remoteUrl, 2406 + url, 2407 + type, 2408 + } = media; 2409 + const { original = {}, small } = meta || {}; 2410 + const width = small?.width || original?.width; 2411 + const height = small?.height || original?.height; 2412 + 2413 + // Prefer remote over original 2414 + const sourceMediaURL = remoteUrl || url; 2415 + const previewMediaURL = previewRemoteUrl || previewUrl; 2416 + const mediaURL = previewMediaURL || sourceMediaURL; 2417 + 2418 + const sourceMediaURLObj = sourceMediaURL 2419 + ? new URL(sourceMediaURL) 2420 + : null; 2421 + const isVideoMaybe = 2422 + type === 'unknown' && 2423 + sourceMediaURLObj && 2424 + /\.(mp4|m4r|m4v|mov|webm)$/i.test(sourceMediaURLObj.pathname); 2425 + const isAudioMaybe = 2426 + type === 'unknown' && 2427 + sourceMediaURLObj && 2428 + /\.(mp3|ogg|wav|m4a|m4p|m4b)$/i.test(sourceMediaURLObj.pathname); 2429 + const isImage = 2430 + type === 'image' || 2431 + (type === 'unknown' && 2432 + previewMediaURL && 2433 + !isVideoMaybe && 2434 + !isAudioMaybe); 2435 + const isVideo = type === 'gifv' || type === 'video' || isVideoMaybe; 2436 + const isAudio = type === 'audio' || isAudioMaybe; 2437 + 2438 + let mediaHTML = ''; 2439 + if (isImage) { 2440 + mediaHTML = `<img src="${mediaURL}" width="${width}" height="${height}" alt="${description}" loading="lazy" />`; 2441 + } else if (isVideo) { 2442 + mediaHTML = ` 2443 + <video src="${sourceMediaURL}" width="${width}" height="${height}" controls preload="auto" poster="${previewMediaURL}" loading="lazy"></video> 2444 + ${description ? `<figcaption>${description}</figcaption>` : ''} 2445 + `; 2446 + } else if (isAudio) { 2447 + mediaHTML = ` 2448 + <audio src="${sourceMediaURL}" controls preload="auto"></audio> 2449 + ${description ? `<figcaption>${description}</figcaption>` : ''} 2450 + `; 2451 + } else { 2452 + mediaHTML = ` 2453 + <a href="${sourceMediaURL}">📄 ${ 2454 + description || sourceMediaURL 2455 + }</a> 2456 + `; 2457 + } 2458 + 2459 + return `<figure>${mediaHTML}</figure>`; 2460 + }) 2461 + .join('\n') 2462 + : ''); 2463 + 2464 + const htmlCode = ` 2465 + <blockquote lang="${language}" cite="${url}"> 2466 + ${ 2467 + spoilerText 2468 + ? ` 2469 + <details> 2470 + <summary>${spoilerText}</summary> 2471 + ${contentHTML} 2472 + </details> 2473 + ` 2474 + : contentHTML 2475 + } 2476 + <footer> 2477 + — ${emojifyText( 2478 + displayName, 2479 + accountEmojis, 2480 + )} (@${username}) <a href="${url}"><time datetime="${createdAtDate.toISOString()}">${createdAtDate.toLocaleString()}</time></a> 2481 + </footer> 2482 + </blockquote> 2483 + `; 2484 + 2485 + return prettify(htmlCode); 2486 + } 2487 + 2488 + function EmbedModal({ post, instance, onClose }) { 2489 + const { 2490 + account: { 2491 + url: accountURL, 2492 + displayName, 2493 + username, 2494 + emojis: accountEmojis, 2495 + bot, 2496 + group, 2497 + }, 2498 + id, 2499 + poll, 2500 + spoilerText, 2501 + language, 2502 + editedAt, 2503 + createdAt, 2504 + content, 2505 + mediaAttachments, 2506 + url, 2507 + emojis, 2508 + } = post; 2509 + 2510 + const htmlCode = generateHTMLCode(post, instance); 2511 + return ( 2512 + <div id="embed-post" class="sheet"> 2513 + {!!onClose && ( 2514 + <button type="button" class="sheet-close" onClick={onClose}> 2515 + <Icon icon="x" /> 2516 + </button> 2517 + )} 2518 + <header> 2519 + <h2>Embed post</h2> 2520 + </header> 2521 + <main tabIndex="-1"> 2522 + <h3>HTML Code</h3> 2523 + <textarea 2524 + class="embed-code" 2525 + readonly 2526 + onClick={(e) => { 2527 + e.target.select(); 2528 + }} 2529 + > 2530 + {htmlCode} 2531 + </textarea> 2532 + <button 2533 + type="button" 2534 + onClick={() => { 2535 + try { 2536 + navigator.clipboard.writeText(htmlCode); 2537 + showToast('HTML code copied'); 2538 + } catch (e) { 2539 + console.error(e); 2540 + showToast('Unable to copy HTML code'); 2541 + } 2542 + }} 2543 + > 2544 + <Icon icon="clipboard" /> <span>Copy</span> 2545 + </button> 2546 + {!!mediaAttachments?.length && ( 2547 + <section> 2548 + <p>Media attachments:</p> 2549 + <ol class="links-list"> 2550 + {mediaAttachments.map((media) => { 2551 + return ( 2552 + <li key={media.id}> 2553 + <a 2554 + href={media.remoteUrl || media.url} 2555 + target="_blank" 2556 + download 2557 + > 2558 + {media.remoteUrl || media.url} 2559 + </a> 2560 + </li> 2561 + ); 2562 + })} 2563 + </ol> 2564 + </section> 2565 + )} 2566 + {!!accountEmojis?.length && ( 2567 + <section> 2568 + <p>Account Emojis:</p> 2569 + <ul class="links-list"> 2570 + {accountEmojis.map((emoji) => { 2571 + return ( 2572 + <li key={emoji.shortcode}> 2573 + <picture> 2574 + <source 2575 + srcset={emoji.staticUrl} 2576 + media="(prefers-reduced-motion: reduce)" 2577 + ></source> 2578 + <img 2579 + class="shortcode-emoji emoji" 2580 + src={emoji.url} 2581 + alt={`:${emoji.shortcode}:`} 2582 + width="16" 2583 + height="16" 2584 + loading="lazy" 2585 + decoding="async" 2586 + /> 2587 + </picture>{' '} 2588 + <code>:{emoji.shortcode}:</code> ( 2589 + <a href={emoji.url} target="_blank" download> 2590 + url 2591 + </a> 2592 + ) 2593 + {emoji.staticUrl ? ( 2594 + <> 2595 + {' '} 2596 + ( 2597 + <a href={emoji.staticUrl} target="_blank" download> 2598 + static 2599 + </a> 2600 + ) 2601 + </> 2602 + ) : null} 2603 + </li> 2604 + ); 2605 + })} 2606 + </ul> 2607 + </section> 2608 + )} 2609 + {!!emojis?.length && ( 2610 + <section> 2611 + <p>Emojis:</p> 2612 + <ul class="links-list"> 2613 + {emojis.map((emoji) => { 2614 + return ( 2615 + <li key={emoji.shortcode}> 2616 + <picture> 2617 + <source 2618 + srcset={emoji.staticUrl} 2619 + media="(prefers-reduced-motion: reduce)" 2620 + ></source> 2621 + <img 2622 + class="shortcode-emoji emoji" 2623 + src={emoji.url} 2624 + alt={`:${emoji.shortcode}:`} 2625 + width="16" 2626 + height="16" 2627 + loading="lazy" 2628 + decoding="async" 2629 + /> 2630 + </picture>{' '} 2631 + <code>:{emoji.shortcode}:</code> ( 2632 + <a href={emoji.url} target="_blank" download> 2633 + url 2634 + </a> 2635 + ) 2636 + {emoji.staticUrl ? ( 2637 + <> 2638 + {' '} 2639 + ( 2640 + <a href={emoji.staticUrl} target="_blank" download> 2641 + static 2642 + </a> 2643 + ) 2644 + </> 2645 + ) : null} 2646 + </li> 2647 + ); 2648 + })} 2649 + </ul> 2650 + </section> 2651 + )} 2652 + <section> 2653 + <small> 2654 + <p>Notes:</p> 2655 + <ul> 2656 + <li> 2657 + This is static, unstyled and scriptless. You may need to apply 2658 + your own styles and edit as needed. 2659 + </li> 2660 + <li> 2661 + Polls are not interactive, becomes a list with vote counts. 2662 + </li> 2663 + <li> 2664 + Media attachments can be images, videos, audios or any file 2665 + types. 2666 + </li> 2667 + <li>Post could be edited or deleted later.</li> 2668 + </ul> 2669 + </small> 2670 + </section> 2671 + <h3>Preview</h3> 2672 + <output 2673 + class="embed-preview" 2674 + dangerouslySetInnerHTML={{ __html: htmlCode }} 2675 + /> 2676 + <p> 2677 + <small>Note: This preview is lightly styled.</small> 2678 + </p> 2296 2679 </main> 2297 2680 </div> 2298 2681 );