feat: add Giphy support with embed component and power levels state management

This commit is contained in:
2026-04-21 21:35:47 +10:00
parent 7c824392b7
commit 9a463facce
14 changed files with 218 additions and 11 deletions

View File

@@ -25,7 +25,7 @@ import {
VideoContent,
VideoFrameThumbnail,
} from './message';
import { UrlPreviewCard, UrlPreviewHolder, YouTubeEmbed, isYouTubeUrl, TikTokEmbed, isTikTokUrl } from './url-preview';
import { UrlPreviewCard, UrlPreviewHolder, YouTubeEmbed, isYouTubeUrl, TikTokEmbed, isTikTokUrl, GiphyEmbed, isGiphyUrl } from './url-preview';
import { Image, MediaControl, Video } from './media';
import { ImageViewer } from './image-viewer';
import { PdfViewer } from './Pdf-viewer';
@@ -71,7 +71,8 @@ export function RenderMessageContent({
// Separate special URLs from generic ones
const youtubeUrls = filteredUrls.filter(isYouTubeUrl);
const tiktokUrls = filteredUrls.filter(isTikTokUrl);
const otherUrls = filteredUrls.filter((url) => !isYouTubeUrl(url) && !isTikTokUrl(url));
const giphyUrls = filteredUrls.filter(isGiphyUrl);
const otherUrls = filteredUrls.filter((url) => !isYouTubeUrl(url) && !isTikTokUrl(url) && !isGiphyUrl(url));
return (
<>
@@ -83,6 +84,10 @@ export function RenderMessageContent({
{tiktokUrls.map((url) => (
<TikTokEmbed key={url} url={url} />
))}
{/* Render Giphy embeds */}
{giphyUrls.map((url) => (
<GiphyEmbed key={url} url={url} />
))}
{/* Render standard URL previews for other links */}
{otherUrls.length > 0 && (
<UrlPreviewHolder>
@@ -247,6 +252,7 @@ export function RenderMessageContent({
mimeType={mimeType}
url={url}
encInfo={encInfo}
autoPlay={mediaAutoLoad}
{...props}
renderThumbnail={
mediaAutoLoad

View File

@@ -82,6 +82,16 @@ export const createRoomEncryptionState = () => ({
},
});
export const createRoomPowerLevelsState = () => ({
type: StateEvent.RoomPowerLevels,
state_key: '',
content: {
events: {
'org.matrix.msc3401.call.member': 0,
},
},
});
export type CreateRoomData = {
version: string;
type?: RoomType;
@@ -98,6 +108,8 @@ export type CreateRoomData = {
export const createRoom = async (mx: MatrixClient, data: CreateRoomData): Promise<string> => {
const initialState: ICreateRoomStateEvent[] = [];
initialState.push(createRoomPowerLevelsState());
if (data.encryption) {
initialState.push(createRoomEncryptionState());
}

View File

@@ -96,7 +96,14 @@ export const Video = forwardRef<HTMLVideoElement, VideoProps>(
if (disableControls) {
return (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video className={classNames(css.Video, className)} {...props} ref={videoRef} />
<video
className={classNames(css.Video, className)}
{...props}
ref={videoRef}
loop={props.autoPlay}
muted={props.autoPlay}
playsInline
/>
);
}
@@ -108,7 +115,14 @@ export const Video = forwardRef<HTMLVideoElement, VideoProps>(
onMouseLeave={() => setShowControls(false)}
>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video className={classNames(css.Video, className)} {...props} ref={videoRef} />
<video
className={classNames(css.Video, className)}
{...props}
ref={videoRef}
loop={props.autoPlay}
muted={props.autoPlay}
playsInline
/>
{showControls && (
<Box className={videoCss.VideoControlsOverlay} gap="200">

View File

@@ -243,7 +243,7 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
const filename = content.filename ?? content.body ?? 'Video';
return (
<Attachment outlined={outlined}>
<Attachment outlined={outlined} transparent>
<AttachmentHeader>
<FileHeader
body={filename}

View File

@@ -16,6 +16,11 @@ export const Attachment = recipe({
boxShadow: `inset 0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`,
},
},
transparent: {
true: {
backgroundColor: 'transparent',
},
},
},
});

View File

@@ -4,11 +4,11 @@ import classNames from 'classnames';
import * as css from './Attachment.css';
export const Attachment = as<'div', css.AttachmentVariants>(
({ className, outlined, ...props }, ref) => (
({ className, outlined, transparent, ...props }, ref) => (
<Box
display="InlineFlex"
direction="Column"
className={classNames(css.Attachment({ outlined }), className)}
className={classNames(css.Attachment({ outlined, transparent }), className)}
{...props}
ref={ref}
/>

View File

@@ -0,0 +1,22 @@
import { style } from '@vanilla-extract/css';
import { DefaultReset, toRem } from 'folds';
export const GiphyEmbed = style([
DefaultReset,
{
width: toRem(480),
maxWidth: '100%',
display: 'block',
},
]);
export const GiphyVideo = style([
DefaultReset,
{
width: '100%',
height: 'auto',
border: 'none',
display: 'block',
backgroundColor: 'transparent',
},
]);

View File

@@ -0,0 +1,64 @@
import React, { useEffect, useState } from 'react';
import { as } from 'folds';
import * as css from './GiphyEmbed.css';
const GIPHY_URL_PATTERNS = [
/(?:https?:\/\/)?(?:www\.)?giphy\.com\/gifs\/(?:[\w-]+-)([a-zA-Z0-9]+)(?:\?|$)/,
/(?:https?:\/\/)?(?:www\.)?giphy\.com\/embed\/([a-zA-Z0-9]+)(?:\?|$)/,
/(?:https?:\/\/)?(?:media\.)?giphy\.com\/media\/([a-zA-Z0-9]+)/,
];
export function isGiphyUrl(url: string): boolean {
return GIPHY_URL_PATTERNS.some((pattern) => pattern.test(url));
}
export function extractGiphyId(url: string): string | null {
let gifId: string | null = null;
GIPHY_URL_PATTERNS.some((pattern) => {
const match = url.match(pattern);
if (match) {
const [, id] = match;
if (id) {
gifId = id;
return true;
}
}
return false;
});
return gifId;
}
type GiphyEmbedProps = {
url: string;
};
export const GiphyEmbed = as<'div', GiphyEmbedProps>(({ url, ...props }, ref) => {
const gifId = extractGiphyId(url);
const [mediaUrl, setMediaUrl] = useState<string | null>(null);
useEffect(() => {
if (!gifId) return;
// Try to get the direct media URL from Giphy
// Format: https://i.giphy.com/media/{ID}/giphy.mp4 or giphy.gif
const mp4Url = `https://i.giphy.com/media/${gifId}/giphy.mp4`;
setMediaUrl(mp4Url);
}, [gifId]);
if (!gifId || !mediaUrl) {
return null;
}
return (
<div className={css.GiphyEmbed} {...props} ref={ref}>
<video
className={css.GiphyVideo}
src={mediaUrl}
autoPlay
loop
muted
playsInline
/>
</div>
);
});

View File

@@ -2,3 +2,4 @@ export * from './UrlPreview';
export * from './UrlPreviewCard';
export * from './YouTubeEmbed';
export * from './TikTokEmbed';
export * from './GiphyEmbed';