From 997375b307f7fd59a58084f5b489617f13c21b9f Mon Sep 17 00:00:00 2001 From: heaven Date: Thu, 7 May 2026 21:24:50 +0300 Subject: [PATCH] feat(timeline): square image+video bubbles with username overlay, reactions outside bubble, edge-anchored mobile rail, horizontal day divider --- src/app/components/RenderMessageContent.tsx | 149 ++++++--- .../components/message/MsgTypeRenderers.tsx | 4 +- .../message/attachment/StreamMedia.css.ts | 108 +++++++ .../message/attachment/StreamMediaImage.tsx | 61 ++++ .../message/attachment/StreamMediaShell.tsx | 105 ++++++ .../message/attachment/StreamMediaVideo.tsx | 65 ++++ .../components/message/attachment/index.ts | 3 + .../message/attachment/streamMediaDebug.ts | 123 +++++++ src/app/components/message/layout/Stream.tsx | 98 +++--- .../components/message/layout/layout.css.ts | 305 ++++++++---------- src/app/features/room/RoomTimeline.tsx | 2 + src/app/features/room/message/Message.tsx | 90 ++++-- 12 files changed, 836 insertions(+), 277 deletions(-) create mode 100644 src/app/components/message/attachment/StreamMedia.css.ts create mode 100644 src/app/components/message/attachment/StreamMediaImage.tsx create mode 100644 src/app/components/message/attachment/StreamMediaShell.tsx create mode 100644 src/app/components/message/attachment/StreamMediaVideo.tsx create mode 100644 src/app/components/message/attachment/streamMediaDebug.ts diff --git a/src/app/components/RenderMessageContent.tsx b/src/app/components/RenderMessageContent.tsx index 4cfcb7dc..71700e82 100644 --- a/src/app/components/RenderMessageContent.tsx +++ b/src/app/components/RenderMessageContent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { MouseEventHandler, createContext, useContext } from 'react'; import { MsgType } from 'matrix-js-sdk'; import { HTMLReactParserOptions } from 'html-react-parser'; import { Opts } from 'linkifyjs'; @@ -20,6 +20,11 @@ import { ReadPdfFile, ReadTextFile, RenderBody, + RenderImageContentProps, + RenderVideoContentProps, + StreamMediaCaption, + StreamMediaImage, + StreamMediaVideo, ThumbnailContent, UnsupportedContent, VideoContent, @@ -31,6 +36,22 @@ import { PdfViewer } from './Pdf-viewer'; import { TextViewer } from './text-viewer'; import { testMatrixTo } from '../plugins/matrix-to'; import { IImageContent } from '../../types/matrix/common'; +import { logMedia } from './message/attachment/streamMediaDebug'; + +// Threads the StreamLayout's mediaMode info from Message.tsx down to the +// image / video rendering branches below. Non-null only for media messages +// in the timeline; pin-menu / message-search leave it null and fall back +// to the legacy MImage / MVideo Attachment chrome. +export type StreamMediaContextValue = { + own: boolean; + username: string; + senderId: string; + onUsernameClick: MouseEventHandler; + onUsernameContextMenu: MouseEventHandler; +}; +export const StreamMediaContext = createContext(null); +export const useStreamMediaContext = (): StreamMediaContextValue | null => + useContext(StreamMediaContext); type RenderMessageContentProps = { displayName: string; @@ -58,6 +79,7 @@ export function RenderMessageContent({ linkifyOpts, outlineAttachment, }: RenderMessageContentProps) { + const streamMedia = useStreamMediaContext(); const renderUrlsPreview = (urls: string[]) => { const filteredUrls = urls.filter((url) => !testMatrixTo(url)); if (filteredUrls.length === 0) return undefined; @@ -72,9 +94,9 @@ export function RenderMessageContent({ const renderCaption = () => { const content: IImageContent = getContent(); if (content.filename && content.filename !== content.body) { - return ( + const captionNode = ( ( @@ -88,6 +110,10 @@ export function RenderMessageContent({ renderUrlsPreview={urlPreview ? renderUrlsPreview : undefined} /> ); + if (streamMedia) { + return
{captionNode}
; + } + return captionNode; } return null; }; @@ -184,53 +210,90 @@ export function RenderMessageContent({ } if (msgType === MsgType.Image) { + logMedia('RenderMessageContent', { + msgType, + streamMediaPresent: !!streamMedia, + branch: streamMedia ? 'StreamMediaImage' : 'MImage(legacy)', + }); + const renderImageInside = (props: RenderImageContentProps) => ( + } + renderViewer={(p) => } + /> + ); return ( <> - ( - } - renderViewer={(p) => } - /> - )} - outlined={outlineAttachment} - /> + {streamMedia ? ( + + ) : ( + + )} {renderCaption()} ); } if (msgType === MsgType.Video) { + logMedia('RenderMessageContent', { + msgType, + streamMediaPresent: !!streamMedia, + branch: streamMedia ? 'StreamMediaVideo' : 'MVideo(legacy)', + }); + const renderVideoInside = ({ body, info, ...props }: RenderVideoContentProps) => ( + ( + ( + {body} + )} + /> + ) + : undefined + } + renderVideo={(p) =>