diff --git a/packages/app/src/Element/Text.css b/packages/app/src/Element/Text.css index 9201a171..4a458edb 100644 --- a/packages/app/src/Element/Text.css +++ b/packages/app/src/Element/Text.css @@ -90,3 +90,26 @@ border-left: 2px solid var(--font-secondary-color); padding-left: 12px; } + +.gallery { + grid-template-columns: repeat(4, 1fr); + gap: 2px; + display: grid; + list-style: none; + padding: 0; + -webkit-overflow-scrolling: touch; + margin: 10px 0; +} + +.gallery-item { + display: block; + position: relative; + margin: 0; +} + +.gallery-item img { + object-fit: cover; + width: 100%; + height: 100%; + display: block; +} diff --git a/packages/app/src/Element/Text.tsx b/packages/app/src/Element/Text.tsx index 5d3cbb64..0cbacf51 100644 --- a/packages/app/src/Element/Text.tsx +++ b/packages/app/src/Element/Text.tsx @@ -27,6 +27,18 @@ export interface TextProps { onClick?: (e: React.MouseEvent) => void; } +const gridConfigMap = new Map([ + [1, [[4, 3]]], + [2, [[2, 2], [2, 2]]], + [3, [[2, 2], [2, 1], [2, 1]]], + [4, [[2, 1], [2, 1], [2, 1], [2, 1]]], + [5, [[2, 1], [2, 1], [2, 1], [1, 1], [1, 1]]], + [6, [[2, 2], [1, 1], [1, 1], [2, 2], [1, 1], [1, 1]]], +]); + +const ROW_HEIGHT = 140; +const GRID_GAP = 2; + export default function Text({ id, content, @@ -77,67 +89,149 @@ export default function Text({ ); } + const DisableMedia = ({ content }: { content: string }) => ( + e.stopPropagation()} + target="_blank" + rel="noreferrer" + className="ext" + > + {content} + + ); + + const RevealMediaInstance = ({ content }: { content: string }) => ( + { + if (!disableMediaSpotlight) { + e.stopPropagation(); + e.preventDefault(); + setShowSpotlight(true); + const selected = images.findIndex((b) => b === content); + setImageIdx(selected === -1 ? 0 : selected); + } + }} + /> + ); + const renderContent = () => { let lenCtr = 0; - function renderChunk(a: ParsedFragment) { + + let chunks: (JSX.Element | null)[] = []; + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + if (truncate) { if (lenCtr > truncate) { - return null; - } else if (lenCtr + a.content.length > truncate) { - lenCtr += a.content.length; - return
{a.content.slice(0, truncate - lenCtr)}...
; + continue; + } else if (lenCtr + element.content.length > truncate) { + lenCtr += element.content.length; + chunks = chunks.concat(
{element.content.slice(0, truncate - lenCtr)}...
); } else { - lenCtr += a.content.length; + lenCtr += element.content.length; } } - if (a.type === "media" && !a.mimeType?.startsWith("unknown")) { + if (element.type === "media" && element.mimeType?.startsWith("image")) { if (disableMedia ?? false) { - return ( - e.stopPropagation()} target="_blank" rel="noreferrer" className="ext"> - {a.content} - - ); - } - return ( - { - if (!disableMediaSpotlight) { - e.stopPropagation(); - e.preventDefault(); - setShowSpotlight(true); - const selected = images.findIndex(b => b === a.content); - setImageIdx(selected === -1 ? 0 : selected); + chunks = chunks.concat(); + } else { + let galleryImages: ParsedFragment[] = [element]; + // If the current element is of type media and mimeType starts with image, + // we verify if the next elements are of the same type and mimeType and push to the galleryImages + // Whenever one of the next elements is not longer of the type we are looking for, we break the loop + for (let j = i; j < elements.length; j++) { + const nextElement = elements[j + 1]; + + if ( + nextElement && + nextElement.type === "media" && + nextElement.mimeType?.startsWith("image") + ) { + galleryImages = galleryImages.concat(nextElement); + i++; + } else { + break; + } + } + + // We build a grid layout to render the grouped images + const imagesWithGridConfig = galleryImages.map((gi, index) => { + const config = gridConfigMap.get(galleryImages.length); + let height = ROW_HEIGHT; + + if (config && config[index][1] > 1) { + height = (config[index][1] * ROW_HEIGHT) + GRID_GAP; + } + + return { + content: gi.content, + gridColumn: config ? config[index][0] : 1, + gridRow: config ? config[index][1] : 1, + height, + } + }); + const gallery = ( +
+ { + imagesWithGridConfig.map((img) => ( +
+ +
+ )) } - }} - /> - ); - } else { - switch (a.type) { - case "invoice": - return ; - case "hashtag": - return ; - case "cashu": - return ; - case "media": - case "link": - return ; - case "custom_emoji": - return ; - default: - return ( -
- {highlighText ? renderContentWithHighlightedText(a.content, highlighText) : a.content} -
- ); +
+ ); + chunks = chunks.concat(gallery); } } + + if (element.type === "media" && (element.mimeType?.startsWith("audio") || element.mimeType?.startsWith("video"))) { + if (disableMedia ?? false) { + chunks = chunks.concat(); + } else { + chunks = chunks.concat(); + } + } + if (element.type === "invoice") { + chunks = chunks.concat(); + } + if (element.type === "hashtag") { + chunks = chunks.concat(); + } + if (element.type === "cashu") { + chunks = chunks.concat(); + } + if(element.type === "link" || (element.type === 'media' && element.mimeType?.startsWith('unknown'))) { + chunks = chunks.concat(); + } + if (element.type === "custom_emoji") { + chunks = chunks.concat(); + } + if (element.type === "text") { + chunks = chunks.concat( +
+ {highlighText + ? renderContentWithHighlightedText(element.content, highlighText) + : element.content} +
, + ); + + } } - - return elements.map(a => renderChunk(a)); + return chunks; }; return ( diff --git a/packages/system/src/text.ts b/packages/system/src/text.ts index edd4502d..89b9514f 100644 --- a/packages/system/src/text.ts +++ b/packages/system/src/text.ts @@ -189,7 +189,7 @@ export function transformText(body: string, tags: Array>) { fragments = fragments .map(a => { if (typeof a === "string") { - if (a.length > 0) { + if (a.trim().length > 0) { return { type: "text", content: a } as ParsedFragment; } } else {