diff --git a/ts/components/conversation/MessageBody.stories.tsx b/ts/components/conversation/MessageBody.stories.tsx index 916a6bfd5..7d6a750b7 100644 --- a/ts/components/conversation/MessageBody.stories.tsx +++ b/ts/components/conversation/MessageBody.stories.tsx @@ -278,7 +278,19 @@ export function FormattingSpoiler(): JSX.Element { bodyRanges: [ { start: 8, - length: 89, + length: 60, + style: BodyRange.Style.SPOILER, + }, + // This is touching, but not overlapping; they should not reveal together + { + start: 68, + length: 29, + style: BodyRange.Style.SPOILER, + }, + // Note: in overlaps, the last spoiler wins + { + start: 94, + length: 6, style: BodyRange.Style.SPOILER, }, { @@ -322,7 +334,11 @@ export function FormattingSpoiler(): JSX.Element {

- + null} + isSpoilerExpanded={{}} + />
diff --git a/ts/components/conversation/MessageTextRenderer.tsx b/ts/components/conversation/MessageTextRenderer.tsx index a4be2200c..f815a66bb 100644 --- a/ts/components/conversation/MessageTextRenderer.tsx +++ b/ts/components/conversation/MessageTextRenderer.tsx @@ -75,8 +75,22 @@ export function MessageTextRenderer({ // Create range tree, dropping bodyRanges that don't apply. Read More means truncated // strings. + let spoilerCount = 0; const tree = sortedRanges.reduce>( (acc, range) => { + if ( + BodyRange.isFormatting(range) && + range.style === BodyRange.Style.SPOILER + ) { + spoilerCount += 1; + return insertRange( + { + ...range, + spoilerId: spoilerCount, + }, + acc + ); + } if (range.start < textLength) { return insertRange(range, acc); } @@ -139,7 +153,7 @@ function renderNode({ if (node.isSpoiler && node.spoilerChildren?.length) { const isSpoilerHidden = Boolean( - node.isSpoiler && !isSpoilerExpanded[node.spoilerIndex || 0] + node.isSpoiler && !isSpoilerExpanded[node.spoilerId || 0] ); const content = node.spoilerChildren?.map(spoilerNode => renderNode({ @@ -193,7 +207,7 @@ function renderNode({ event.stopPropagation(); onExpandSpoiler({ ...isSpoilerExpanded, - [node.spoilerIndex || 0]: true, + [node.spoilerId || 0]: true, }); } } @@ -209,7 +223,7 @@ function renderNode({ event.stopPropagation(); onExpandSpoiler?.({ ...isSpoilerExpanded, - [node.spoilerIndex || 0]: true, + [node.spoilerId || 0]: true, }); } } diff --git a/ts/types/BodyRange.ts b/ts/types/BodyRange.ts index 09dc1e039..46b80ef28 100644 --- a/ts/types/BodyRange.ts +++ b/ts/types/BodyRange.ts @@ -44,6 +44,7 @@ export namespace BodyRange { }; export type Formatting = { style: Style; + spoilerId?: number; }; export type DisplayOnly = { displayStyle: DisplayStyle; @@ -356,8 +357,8 @@ export type DisplayNode = { // DisplayOnly isKeywordHighlight?: boolean; - // Only for spoilers, only to represent contiguous groupings - spoilerIndex?: number; + // Only for spoilers, only to make sure we honor original spoiler breakdown + spoilerId?: number; spoilerChildren?: ReadonlyArray; }; type PartialDisplayNode = Omit< @@ -381,7 +382,7 @@ function rangeToPartialNode( return { isMonospace: true }; } if (range.style === BodyRange.Style.SPOILER) { - return { isSpoiler: true }; + return { isSpoiler: true, spoilerId: range.spoilerId }; } if (range.style === BodyRange.Style.STRIKETHROUGH) { return { isStrikethrough: true }; @@ -482,25 +483,29 @@ export function groupContiguousSpoilers( const result: Array = []; let spoilerContainer: DisplayNode | undefined; - let spoilerIndex = 0; nodes.forEach(node => { if (node.isSpoiler) { - if (!spoilerContainer) { - spoilerContainer = { - ...node, - spoilerIndex, - isSpoiler: true, - spoilerChildren: [], - }; - spoilerIndex += 1; - result.push(spoilerContainer); - } - if (spoilerContainer) { + if ( + spoilerContainer && + isNumber(spoilerContainer.spoilerId) && + spoilerContainer.spoilerId === node.spoilerId + ) { spoilerContainer.spoilerChildren = [ ...(spoilerContainer.spoilerChildren || []), node, ]; + } else { + spoilerContainer = undefined; + } + + if (!spoilerContainer) { + spoilerContainer = { + ...node, + isSpoiler: true, + spoilerChildren: [node], + }; + result.push(spoilerContainer); } } else { spoilerContainer = undefined;