Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat_add_gallery_support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

# Added a gallery support as per msc4274
52 changes: 44 additions & 8 deletions src/app/components/RenderMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
MNotice,
MText,
MVideo,
MGallery,
ReadPdfFile,
ReadTextFile,
RenderBody,
Expand All @@ -49,7 +50,8 @@ import { ClientSideHoverFreeze } from './ClientSideHoverFreeze';
import { CuteEventType, MCuteEvent } from './message/MCuteEvent';
import { PollEvent } from './message/PollEvent';
import { M_TEXT } from 'matrix-js-sdk';
import type { IImageInfo } from '$types/matrix/common';
import type { IImageInfo, IGalleryContent } from '$types/matrix/common';
import { GALLERY_MSGTYPE } from '$types/matrix/common';

type RenderMessageContentProps = {
displayName: string;
Expand All @@ -61,6 +63,7 @@ type RenderMessageContentProps = {
bundledPreview?: boolean;
urlPreview?: boolean;
clientUrlPreview?: boolean;
isGallery?: boolean;
showMaps?: boolean;
highlightRegex?: RegExp;
htmlReactParserOptions: HTMLReactParserOptions;
Expand Down Expand Up @@ -94,6 +97,7 @@ function RenderMessageContentInternal({
edited,
getContent,
mediaAutoLoad,
isGallery,
bundledPreview,
urlPreview,
clientUrlPreview,
Expand Down Expand Up @@ -213,8 +217,9 @@ function RenderMessageContentInternal({
if (captionPosition === CaptionPosition.Hidden || hideCaption) return null;
if (
hasCaption &&
(content as { filename?: string }).filename &&
(content as { filename?: string }).filename !== content.body
(((content as { filename?: string }).filename &&
(content as { filename?: string }).filename !== content.body) ||
msgType === GALLERY_MSGTYPE)
) {
if (captionPosition !== CaptionPosition.Inline)
return (
Expand Down Expand Up @@ -254,16 +259,19 @@ function RenderMessageContentInternal({
return null;
};

function renderCaptionedAttachment(attachment: JSX.Element): JSX.Element {
function renderCaptionedAttachment(attachment: JSX.Element, isInGallery?: boolean): JSX.Element {
return (
<div
style={{
display: 'flex',
flexDirection: attachmentDirection,
height: '100%',
width: '100%',
position: 'relative',
}}
>
<div>{attachment}</div>
{renderCaption()}
{attachment}
{!isInGallery && renderCaption()}
</div>
);
}
Expand Down Expand Up @@ -368,6 +376,7 @@ function RenderMessageContentInternal({
return renderCaptionedAttachment(
<MImage
content={content as Record<string, never> & { msgtype: MsgType.Image }}
fitParent={isGallery}
renderImageContent={(imageProps) => (
<ImageContent
{...imageProps}
Expand All @@ -386,7 +395,8 @@ function RenderMessageContentInternal({
/>
)}
outlined={outlineAttachment}
/>
/>,
isGallery
);
}

Expand Down Expand Up @@ -416,7 +426,8 @@ function RenderMessageContentInternal({
/>
)}
outlined={outlineAttachment}
/>
/>,
isGallery
);
}

Expand All @@ -429,13 +440,38 @@ function RenderMessageContentInternal({
<AudioContent {...audioProps} renderMediaControl={(p) => <MediaControl {...p} />} />
)}
outlined={outlineAttachment}
fitParent={isGallery}
/>
);
}

if (msgType === (MsgType.File as string)) return renderFile();
if (msgType === (MsgType.Location as string))
return <MLocation showMaps={showMaps} content={content} />;

if (msgType === GALLERY_MSGTYPE) {
return renderCaptionedAttachment(
<MGallery
content={content as IGalleryContent}
renderItem={(itemContent) => (
<RenderMessageContentInternal
displayName={displayName}
msgType={itemContent.msgtype as string}
ts={ts}
getContent={() => itemContent}
mediaAutoLoad={mediaAutoLoad}
urlPreview={urlPreview}
highlightRegex={highlightRegex}
htmlReactParserOptions={htmlReactParserOptions}
linkifyOpts={linkifyOpts}
outlineAttachment={outlineAttachment}
isGallery={true}
/>
)}
/>
);
}

if (msgType === 'm.bad.encrypted') return <MBadEncrypted />;

// cute events
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/media/media.css.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not like this, can we set it for gallery only somehow?

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DefaultReset } from 'folds';
export const Image = style([
DefaultReset,
{
objectFit: 'contain',
objectFit: 'cover',
width: '100%',
height: '100%',
},
Expand Down
57 changes: 57 additions & 0 deletions src/app/components/message/MGallery.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { recipe } from '@vanilla-extract/recipes';
import { style } from '@vanilla-extract/css';
import { DefaultReset, config, toRem } from 'folds';

export const GalleryHolder = style({
marginTop: config.space.S200,
});

export const GalleryImageGrid = recipe({
base: [
DefaultReset,
{
display: 'grid',
gap: '0.5rem',
maxWidth: toRem(600),
height: '100%',
width: '100%',
gridAutoColumns: toRem(100),
},
],
variants: {
type: {
ThreeItems: {
gridTemplateColumns: '1fr 1fr',
gridTemplateRows: `repeat(2, 1fr)`,
},
ThreeByThree: {
gridTemplateColumns: 'repeat(3,minmax(0,1fr))',
},
TwoByTwo: {
gridTemplateColumns: 'repeat(2,minmax(0,1fr))',
},
OneByOne: {
maxHeight: toRem(300),
gridTemplateColumns: '1fr',
},
},
},
});

export const GalleryItem = recipe({
base: [
DefaultReset,
{
borderRadius: config.radii.R300,
overflow: 'hidden',
width: '100%',
height: '100%',
aspectRatio: '1/1',
selectors: {
[`${GalleryImageGrid.classNames.variants.type.ThreeItems} &:nth-child(1)`]: {
gridRow: 'span 2',
},
},
},
],
});
127 changes: 127 additions & 0 deletions src/app/components/message/MGallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { type ReactNode } from 'react';
import { Box } from 'folds';
import { MsgType, type IContent } from 'matrix-js-sdk';
import type {
IGalleryContent,
IGalleryImageItem,
IGalleryItem,
IGalleryVideoItem,
} from '$types/matrix/common';
import * as css from './MGallery.css';

function galleryItemToContent(item: IGalleryItem): IContent {
const { itemtype, ...rest } = item;
return { ...rest, msgtype: itemtype } as IContent;
}

type MGalleryProps = {
content: IGalleryContent;
renderItem: (content: IContent, index: number) => ReactNode;
};

type PartitionedMediaItem = {
items: (IGalleryImageItem | IGalleryVideoItem)[];
type: 'ThreeByThree' | 'TwoByTwo' | 'OneByOne' | 'ThreeItems';
};

export function MGallery({ content, renderItem }: MGalleryProps) {
const items = content.itemtypes;

let mediaItems = items.filter(
(item) => item.itemtype == MsgType.Video || item.itemtype == MsgType.Image
);
const columnItems = items.filter(
(item) => item.itemtype == MsgType.File || item.itemtype == MsgType.Audio
);

let partitionedMediaItems: PartitionedMediaItem[] = [];
while (mediaItems.length > 0) {
if (mediaItems.length >= 9) {
partitionedMediaItems.unshift({
items: mediaItems.slice(-9),
type: 'ThreeByThree',
});
mediaItems = mediaItems.slice(0, mediaItems.length - 9);
continue;
}
if (mediaItems.length >= 6) {
partitionedMediaItems.unshift({
items: mediaItems.slice(-6),
type: 'ThreeByThree',
});
mediaItems = mediaItems.slice(0, mediaItems.length - 6);
continue;
}
if (mediaItems.length >= 4) {
partitionedMediaItems.unshift({
items: mediaItems.slice(-4),
type: 'TwoByTwo',
});
mediaItems = mediaItems.slice(0, mediaItems.length - 4);
continue;
}
if (mediaItems.length > 3) {
partitionedMediaItems.unshift({
items: mediaItems.slice(-3),
type: 'ThreeByThree',
});
mediaItems = mediaItems.slice(0, mediaItems.length - 3);
continue;
}
if (mediaItems.length == 3) {
partitionedMediaItems.unshift({
items: mediaItems.slice(-3),
type: 'ThreeItems',
});
mediaItems = mediaItems.slice(0, mediaItems.length - 3);
continue;
}
if (mediaItems.length >= 2) {
partitionedMediaItems.unshift({
items: mediaItems.slice(-2),
type: 'TwoByTwo',
});
mediaItems = mediaItems.slice(0, mediaItems.length - 2);
continue;
}
if (mediaItems.length >= 1) {
partitionedMediaItems.unshift({
items: mediaItems.slice(-1),
type: 'OneByOne',
});
mediaItems = mediaItems.slice(0, mediaItems.length - 1);
continue;
}
}
return (
<Box display="Flex" direction="Column" gap="400">
{partitionedMediaItems.length > 0 && (
<Box direction="Column" gap="200">
{partitionedMediaItems.map((item) => (
<div
key={`${item.type}-${item.items[0]?.url ?? item.items[0]?.file?.url}`}
className={css.GalleryImageGrid({
type: item.type,
})}
>
{item.items.map((mediaItem, index) => (
<div key={mediaItem.url ?? mediaItem.file?.url} className={css.GalleryItem()}>
{renderItem(galleryItemToContent(mediaItem), index)}
</div>
))}
</div>
))}
</Box>
)}
{columnItems.length > 0 && (
<Box alignItems="Start" gap="200" direction="Column">
{columnItems.map((item, index) => (
<div key={item.url ?? item.file?.url ?? index}>
{renderItem(galleryItemToContent(item), index)}
</div>
))}
</Box>
)}
</Box>
);
}
Loading
Loading