mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-04 22:40:29 +03:00
Add Tenor GIF search feature
This commit is contained in:
parent
fbd7e0a14b
commit
397e71adc3
5 changed files with 440 additions and 54 deletions
4
public/res/ic/outlined/gif.svg
Normal file
4
public/res/ic/outlined/gif.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.5 3.75C2.84315 3.75 1.5 5.09315 1.5 6.75V17.25C1.5 18.9069 2.84315 20.25 4.5 20.25H19.5C21.1569 20.25 22.5 18.9069 22.5 17.25V6.75C22.5 5.09315 21.1569 3.75 19.5 3.75H4.5ZM13.5 8.25C13.5 7.83579 13.1642 7.5 12.75 7.5C12.3358 7.5 12 7.83579 12 8.25V15.75C12 16.1642 12.3358 16.5 12.75 16.5C13.1642 16.5 13.5 16.1642 13.5 15.75V8.25ZM15 8.25C15 7.83579 15.3358 7.5 15.75 7.5H18.75C19.1642 7.5 19.5 7.83579 19.5 8.25C19.5 8.66421 19.1642 9 18.75 9H16.5V11.25H18C18.4142 11.25 18.75 11.5858 18.75 12C18.75 12.4142 18.4142 12.75 18 12.75H16.5V15.75C16.5 16.1642 16.1642 16.5 15.75 16.5C15.3358 16.5 15 16.1642 15 15.75V8.25ZM6.63565 9.77966C7.03978 9.20475 7.5033 9 7.88604 9C8.26878 9 8.7323 9.20475 9.13643 9.77966C9.37463 10.1185 9.84244 10.2001 10.1813 9.96192C10.5202 9.72372 10.6018 9.25591 10.3636 8.91704C9.73827 8.02748 8.85254 7.5 7.88604 7.5C6.91953 7.5 6.03381 8.02748 5.4085 8.91704C4.7885 9.79905 4.5 10.9173 4.5 12C4.5 13.0827 4.7885 14.201 5.4085 15.083C6.03381 15.9725 6.91953 16.5 7.88604 16.5C8.85254 16.5 9.73827 15.9725 10.3636 15.083C10.4524 14.9567 10.5 14.806 10.5 14.6517V12C10.5 11.5858 10.1642 11.25 9.75 11.25H8.25C7.83579 11.25 7.5 11.5858 7.5 12C7.5 12.4142 7.83579 12.75 8.25 12.75H9V14.3981C8.6305 14.8381 8.22634 15 7.88604 15C7.5033 15 7.03978 14.7953 6.63565 14.2203C6.22622 13.6379 6 12.8367 6 12C6 11.1633 6.22622 10.3621 6.63565 9.77966Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -134,3 +134,43 @@ export const StickerImg = style([
|
|||
objectFit: 'contain',
|
||||
},
|
||||
]);
|
||||
|
||||
export const GifContainer = style({
|
||||
columnCount: 3,
|
||||
columnGap: toRem(8),
|
||||
padding: toRem(16),
|
||||
|
||||
'@media': {
|
||||
'(max-width: 768px)': {
|
||||
columnCount: 2,
|
||||
},
|
||||
'(max-width: 480px)': {
|
||||
columnCount: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const GifItem = style([
|
||||
DefaultReset,
|
||||
FocusOutline,
|
||||
{
|
||||
width: '100%',
|
||||
marginBottom: toRem(8),
|
||||
breakInside: 'avoid',
|
||||
borderRadius: config.radii.R400,
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
display: 'block',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: color.Surface.ContainerHover,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const GifImg = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
borderRadius: config.radii.R400,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import React, {
|
|||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Badge,
|
||||
|
|
@ -32,6 +33,7 @@ import { isKeyHotkey } from 'is-hotkey';
|
|||
import classNames from 'classnames';
|
||||
import { MatrixClient, Room } from 'matrix-js-sdk';
|
||||
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import cons from '../../../client/state/cons';
|
||||
|
||||
import * as css from './EmojiBoard.css';
|
||||
import { EmojiGroupId, IEmoji, IEmojiGroup, emojiGroups, emojis } from '../../plugins/emoji';
|
||||
|
|
@ -52,20 +54,34 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
|||
import { ImagePack, ImageUsage, PackImageReader } from '../../plugins/custom-emoji';
|
||||
import { getEmoticonSearchStr } from '../../plugins/utils';
|
||||
|
||||
import GifIC from '../../../../public/res/ic/outlined/gif.svg';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
|
||||
const RECENT_GROUP_ID = 'recent_group';
|
||||
const SEARCH_GROUP_ID = 'search_group';
|
||||
|
||||
export enum EmojiBoardTab {
|
||||
Emoji = 'Emoji',
|
||||
Sticker = 'Sticker',
|
||||
Gif = 'Gif',
|
||||
}
|
||||
|
||||
enum EmojiType {
|
||||
Emoji = 'emoji',
|
||||
CustomEmoji = 'customEmoji',
|
||||
Sticker = 'sticker',
|
||||
Gif = 'gif',
|
||||
}
|
||||
|
||||
export type GifData = {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
preview_url?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export type EmojiItemInfo = {
|
||||
type: EmojiType;
|
||||
data: string;
|
||||
|
|
@ -176,6 +192,18 @@ function EmojiBoardTabs({
|
|||
}) {
|
||||
return (
|
||||
<Box gap="100">
|
||||
<Badge
|
||||
className={css.EmojiBoardTab}
|
||||
as="button"
|
||||
variant="Secondary"
|
||||
fill={tab === EmojiBoardTab.Gif ? 'Solid' : 'None'}
|
||||
size="500"
|
||||
onClick={() => onTabChange(EmojiBoardTab.Gif)}
|
||||
>
|
||||
<Text as="span" size="L400">
|
||||
GIF
|
||||
</Text>
|
||||
</Badge>
|
||||
<Badge
|
||||
className={css.EmojiBoardTab}
|
||||
as="button"
|
||||
|
|
@ -334,6 +362,40 @@ export function StickerItem({
|
|||
);
|
||||
}
|
||||
|
||||
export function GifItem({
|
||||
label,
|
||||
type,
|
||||
data,
|
||||
shortcode,
|
||||
gif,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
type: EmojiType;
|
||||
data: string;
|
||||
shortcode: string;
|
||||
gif?: GifData;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
as="button"
|
||||
className={css.GifItem}
|
||||
type="button"
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
title={label}
|
||||
aria-label={`${label} gif`}
|
||||
data-emoji-type={type}
|
||||
data-emoji-data={data}
|
||||
data-emoji-shortcode={shortcode}
|
||||
data-gif-data={gif ? JSON.stringify(gif) : undefined}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function RecentEmojiSidebarStack({ onItemClick }: { onItemClick: (id: string) => void }) {
|
||||
const activeGroupId = useAtomValue(activeGroupIdAtom);
|
||||
|
||||
|
|
@ -637,6 +699,85 @@ export const NativeEmojiGroups = memo(
|
|||
)
|
||||
);
|
||||
|
||||
export const GifGroups = memo(
|
||||
({ gifs, loading, error }: { gifs: GifData[]; loading: boolean; error: string | null }) => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Box
|
||||
style={{ padding: `${toRem(60)} ${config.space.S500}` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
direction="Column"
|
||||
gap="300"
|
||||
>
|
||||
<Text>Loading GIFs...</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box
|
||||
style={{ padding: `${toRem(60)} ${config.space.S500}` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
direction="Column"
|
||||
gap="300"
|
||||
>
|
||||
<Text>Error: {error}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (gifs.length === 0) {
|
||||
return (
|
||||
<Box
|
||||
style={{ padding: `${toRem(60)} ${config.space.S500}` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
direction="Column"
|
||||
gap="300"
|
||||
>
|
||||
<RawIcon size="large" src={GifIC} />
|
||||
<Box direction="Inherit">
|
||||
<Text align="Center">No GIFs found!</Text>
|
||||
<Text priority="300" align="Center" size="T200">
|
||||
Try searching for something else.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Text className={css.EmojiGroupLabel} size="O400">
|
||||
GIFs
|
||||
</Text>
|
||||
<div className={css.GifContainer}>
|
||||
{gifs.map((gif) => (
|
||||
<GifItem
|
||||
key={gif.id}
|
||||
label={gif.title}
|
||||
type={EmojiType.Gif}
|
||||
data={gif.url}
|
||||
shortcode={gif.title}
|
||||
gif={gif}
|
||||
>
|
||||
<img
|
||||
loading="lazy"
|
||||
className={css.GifImg}
|
||||
alt={gif.title}
|
||||
src={gif.preview_url || gif.url}
|
||||
/>
|
||||
</GifItem>
|
||||
))}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const SEARCH_OPTIONS: UseAsyncSearchOptions = {
|
||||
limit: 1000,
|
||||
matchOptions: {
|
||||
|
|
@ -653,6 +794,7 @@ export function EmojiBoard({
|
|||
onEmojiSelect,
|
||||
onCustomEmojiSelect,
|
||||
onStickerSelect,
|
||||
onGifSelect,
|
||||
allowTextCustomEmoji,
|
||||
addToRecentEmoji = true,
|
||||
}: {
|
||||
|
|
@ -664,11 +806,13 @@ export function EmojiBoard({
|
|||
onEmojiSelect?: (unicode: string, shortcode: string) => void;
|
||||
onCustomEmojiSelect?: (mxc: string, shortcode: string) => void;
|
||||
onStickerSelect?: (mxc: string, shortcode: string, label: string) => void;
|
||||
onGifSelect?: (gif: GifData) => void;
|
||||
allowTextCustomEmoji?: boolean;
|
||||
addToRecentEmoji?: boolean;
|
||||
}) {
|
||||
const emojiTab = tab === EmojiBoardTab.Emoji;
|
||||
const stickerTab = tab === EmojiBoardTab.Sticker;
|
||||
const gifTab = tab === EmojiBoardTab.Gif;
|
||||
const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker;
|
||||
|
||||
const setActiveGroupId = useSetAtom(activeGroupIdAtom);
|
||||
|
|
@ -698,14 +842,116 @@ export function EmojiBoard({
|
|||
|
||||
const searchedItems = result?.items.slice(0, 100);
|
||||
|
||||
function useGifSearch() {
|
||||
const [gifs, setGifs] = useState<GifData[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const parseTenorResult = useCallback((tenorResult: any): GifData => {
|
||||
const SIZE_LIMIT = 3 * 1024 * 1024; // 3MB
|
||||
|
||||
const formats = tenorResult.media_formats || {};
|
||||
const preview = formats.tinygif || formats.nanogif || formats.mediumgif;
|
||||
|
||||
// Start with full resolution GIF
|
||||
let fullRes = formats.gif;
|
||||
// If full res is too large and medium exists, use medium instead
|
||||
if (fullRes && fullRes.size > SIZE_LIMIT && formats.mediumgif) {
|
||||
fullRes = formats.mediumgif;
|
||||
}
|
||||
|
||||
// Fallback if no suitable format found
|
||||
if (!fullRes) {
|
||||
fullRes = formats.mediumgif || formats.gif || preview;
|
||||
}
|
||||
|
||||
// Get dimensions from the selected full resolution format
|
||||
const dimensions = fullRes?.dims || preview?.dims || [0, 0];
|
||||
|
||||
// Convert URLs to use proxy
|
||||
const convertUrl = (url: string): string => {
|
||||
if (!url) return '';
|
||||
try {
|
||||
const originalUrl = new URL(url);
|
||||
const proxyUrl = new URL(cons.api.GIF_PROXY_URL);
|
||||
proxyUrl.pathname = `/proxy/tenor/media${originalUrl.pathname}`;
|
||||
return proxyUrl.toString();
|
||||
} catch {
|
||||
// Return original URL as fallback
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
id: tenorResult.id,
|
||||
title: tenorResult.content_description || tenorResult.h1_title || 'GIF',
|
||||
url: convertUrl(fullRes?.url || ''),
|
||||
preview_url: convertUrl(preview?.url || fullRes?.url || ''),
|
||||
width: dimensions[0] || 0,
|
||||
height: dimensions[1] || 0,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const searchGifs = useCallback(
|
||||
async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
setGifs([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const url = new URL(cons.api.GIF_PROXY_URL);
|
||||
url.pathname = '/proxy/tenor/api/v2/search';
|
||||
url.searchParams.set('q', query);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
const results = data.results as any[] | undefined;
|
||||
|
||||
if (results) {
|
||||
const gifData: GifData[] = results.map(parseTenorResult);
|
||||
setGifs(gifData);
|
||||
} else {
|
||||
setGifs([]);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to search GIFs');
|
||||
setGifs([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[parseTenorResult]
|
||||
);
|
||||
|
||||
return { gifs, loading, error, searchGifs };
|
||||
}
|
||||
|
||||
const { gifs, loading: gifsLoading, error: gifsError, searchGifs } = useGifSearch();
|
||||
|
||||
const handleOnChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
|
||||
useCallback(
|
||||
(evt) => {
|
||||
const term = evt.target.value;
|
||||
if (term) search(term);
|
||||
else resetSearch();
|
||||
if (gifTab) {
|
||||
if (term) {
|
||||
searchGifs(term);
|
||||
}
|
||||
} else if (term) {
|
||||
search(term);
|
||||
} else {
|
||||
resetSearch();
|
||||
}
|
||||
},
|
||||
[search, resetSearch]
|
||||
[search, resetSearch, searchGifs, gifTab]
|
||||
),
|
||||
{ wait: 200 }
|
||||
);
|
||||
|
|
@ -751,6 +997,12 @@ export function EmojiBoard({
|
|||
onStickerSelect?.(emojiInfo.data, emojiInfo.shortcode, emojiInfo.label);
|
||||
if (!evt.altKey && !evt.shiftKey) requestClose();
|
||||
}
|
||||
if (emojiInfo.type === EmojiType.Gif) {
|
||||
const gifDataStr = targetEl.getAttribute('data-gif-data');
|
||||
const gifData = gifDataStr ? JSON.parse(gifDataStr) : null;
|
||||
onGifSelect?.(gifData);
|
||||
if (!evt.altKey && !evt.shiftKey) requestClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmojiPreview = useCallback(
|
||||
|
|
@ -823,10 +1075,12 @@ export function EmojiBoard({
|
|||
data-emoji-board-search
|
||||
variant="SurfaceVariant"
|
||||
size="400"
|
||||
placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'}
|
||||
placeholder={
|
||||
allowTextCustomEmoji && !gifTab ? 'Search or Text Reaction ' : 'Search'
|
||||
}
|
||||
maxLength={50}
|
||||
after={
|
||||
allowTextCustomEmoji && result?.query ? (
|
||||
allowTextCustomEmoji && result?.query && !gifTab ? (
|
||||
<Chip
|
||||
variant="Primary"
|
||||
radii="Pill"
|
||||
|
|
@ -855,28 +1109,30 @@ export function EmojiBoard({
|
|||
</Header>
|
||||
}
|
||||
sidebar={
|
||||
<Sidebar>
|
||||
{emojiTab && recentEmojis.length > 0 && (
|
||||
<RecentEmojiSidebarStack onItemClick={handleScrollToGroup} />
|
||||
)}
|
||||
{imagePacks.length > 0 && (
|
||||
<ImagePackSidebarStack
|
||||
mx={mx}
|
||||
usage={usage}
|
||||
packs={imagePacks}
|
||||
onItemClick={handleScrollToGroup}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
)}
|
||||
{emojiTab && (
|
||||
<NativeEmojiSidebarStack
|
||||
groups={emojiGroups}
|
||||
icons={emojiGroupIcons}
|
||||
labels={emojiGroupLabels}
|
||||
onItemClick={handleScrollToGroup}
|
||||
/>
|
||||
)}
|
||||
</Sidebar>
|
||||
!gifTab ? (
|
||||
<Sidebar>
|
||||
{emojiTab && recentEmojis.length > 0 && (
|
||||
<RecentEmojiSidebarStack onItemClick={handleScrollToGroup} />
|
||||
)}
|
||||
{imagePacks.length > 0 && (
|
||||
<ImagePackSidebarStack
|
||||
mx={mx}
|
||||
usage={usage}
|
||||
packs={imagePacks}
|
||||
onItemClick={handleScrollToGroup}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
)}
|
||||
{emojiTab && (
|
||||
<NativeEmojiSidebarStack
|
||||
groups={emojiGroups}
|
||||
icons={emojiGroupIcons}
|
||||
labels={emojiGroupLabels}
|
||||
onItemClick={handleScrollToGroup}
|
||||
/>
|
||||
)}
|
||||
</Sidebar>
|
||||
) : undefined
|
||||
}
|
||||
footer={
|
||||
emojiTab ? (
|
||||
|
|
@ -895,7 +1151,8 @@ export function EmojiBoard({
|
|||
</Text>
|
||||
</Footer>
|
||||
) : (
|
||||
imagePacks.length > 0 && (
|
||||
imagePacks.length > 0 &&
|
||||
!gifTab && (
|
||||
<Footer>
|
||||
<Text ref={emojiPreviewTextRef} size="H5" truncate>
|
||||
:smiley:
|
||||
|
|
@ -920,30 +1177,40 @@ export function EmojiBoard({
|
|||
direction="Column"
|
||||
gap="200"
|
||||
>
|
||||
{searchedItems && (
|
||||
<SearchEmojiGroup
|
||||
mx={mx}
|
||||
tab={tab}
|
||||
id={SEARCH_GROUP_ID}
|
||||
label={searchedItems.length ? 'Search Results' : 'No Results found'}
|
||||
emojis={searchedItems}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
{gifTab ? (
|
||||
<GifGroups gifs={gifs} loading={gifsLoading} error={gifsError} />
|
||||
) : (
|
||||
<>
|
||||
{searchedItems && (
|
||||
<SearchEmojiGroup
|
||||
mx={mx}
|
||||
tab={tab}
|
||||
id={SEARCH_GROUP_ID}
|
||||
label={searchedItems.length ? 'Search Results' : 'No Results found'}
|
||||
emojis={searchedItems}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
)}
|
||||
{emojiTab && recentEmojis.length > 0 && (
|
||||
<RecentEmojiGroup id={RECENT_GROUP_ID} label="Recent" emojis={recentEmojis} />
|
||||
)}
|
||||
{emojiTab && (
|
||||
<CustomEmojiGroups
|
||||
mx={mx}
|
||||
groups={imagePacks}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
)}
|
||||
{stickerTab && (
|
||||
<StickerGroups
|
||||
mx={mx}
|
||||
groups={imagePacks}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
)}
|
||||
{emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />}
|
||||
</>
|
||||
)}
|
||||
{emojiTab && recentEmojis.length > 0 && (
|
||||
<RecentEmojiGroup id={RECENT_GROUP_ID} label="Recent" emojis={recentEmojis} />
|
||||
)}
|
||||
{emojiTab && (
|
||||
<CustomEmojiGroups
|
||||
mx={mx}
|
||||
groups={imagePacks}
|
||||
useAuthentication={useAuthentication}
|
||||
/>
|
||||
)}
|
||||
{stickerTab && (
|
||||
<StickerGroups mx={mx} groups={imagePacks} useAuthentication={useAuthentication} />
|
||||
)}
|
||||
{emojiTab && <NativeEmojiGroups groups={emojiGroups} labels={emojiGroupLabels} />}
|
||||
</Box>
|
||||
</Scroll>
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ import {
|
|||
trimCommand,
|
||||
getMentions,
|
||||
} from '../../components/editor';
|
||||
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board';
|
||||
import { EmojiBoard, EmojiBoardTab, GifData } from '../../components/emoji-board';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import {
|
||||
TUploadContent,
|
||||
|
|
@ -112,6 +112,8 @@ import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
|
|||
import { powerLevelAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import { useIsDirectRoom } from '../../hooks/useRoom';
|
||||
import GifIC from '../../../../public/res/ic/outlined/gif.svg';
|
||||
import RawIcon from '../../atoms/system-icons/RawIcon';
|
||||
|
||||
interface RoomInputProps {
|
||||
editor: Editor;
|
||||
|
|
@ -430,6 +432,57 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
});
|
||||
};
|
||||
|
||||
const handleGifSelect = async (gif: GifData) => {
|
||||
// Download the GIF data
|
||||
const response = await fetch(gif.url);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Failed to fetch GIF: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(data);
|
||||
|
||||
// Create a File object for the GIF
|
||||
const filename = `${gif.title}.gif`;
|
||||
const file = new File([uint8Array], filename, { type: 'image/gif' });
|
||||
|
||||
// Upload to Matrix
|
||||
const uploadResponse = await mx.uploadContent(file, {
|
||||
name: filename,
|
||||
type: 'image/gif',
|
||||
});
|
||||
const mxcUrl = uploadResponse.content_uri;
|
||||
|
||||
const content: IContent = {
|
||||
body: filename,
|
||||
url: mxcUrl,
|
||||
info: {
|
||||
w: gif.width,
|
||||
h: gif.height,
|
||||
mimetype: 'image/gif',
|
||||
size: data.byteLength,
|
||||
},
|
||||
};
|
||||
|
||||
// Handle replies if there's a reply draft
|
||||
if (replyDraft) {
|
||||
content['m.relates_to'] = {
|
||||
'm.in_reply_to': {
|
||||
event_id: replyDraft.eventId,
|
||||
},
|
||||
};
|
||||
if (replyDraft.relation?.rel_type === RelationType.Thread) {
|
||||
content['m.relates_to'].event_id = replyDraft.relation.event_id;
|
||||
content['m.relates_to'].rel_type = RelationType.Thread;
|
||||
content['m.relates_to'].is_falling_back = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the gif as sticker event.
|
||||
await mx.sendEvent(roomId, EventType.Sticker, content);
|
||||
setReplyDraft(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{selectedFiles.length > 0 && (
|
||||
|
|
@ -607,6 +660,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
onEmojiSelect={handleEmoticonSelect}
|
||||
onCustomEmojiSelect={handleEmoticonSelect}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
onGifSelect={handleGifSelect}
|
||||
requestClose={() => {
|
||||
setEmojiBoardTab((t) => {
|
||||
if (t) {
|
||||
|
|
@ -619,6 +673,18 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
/>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Gif}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Gif)}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
>
|
||||
<RawIcon
|
||||
src={GifIC}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
{!hideStickerBtn && (
|
||||
<IconButton
|
||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
|
||||
|
|
@ -636,7 +702,10 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
<IconButton
|
||||
ref={emojiBtnRef}
|
||||
aria-pressed={
|
||||
hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
hideStickerBtn ?
|
||||
(emojiBoardTab === EmojiBoardTab.Emoji
|
||||
|| emojiBoardTab === EmojiBoardTab.Gif) :
|
||||
emojiBoardTab === EmojiBoardTab.Emoji
|
||||
}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
||||
variant="SurfaceVariant"
|
||||
|
|
@ -646,7 +715,10 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
<Icon
|
||||
src={Icons.Smile}
|
||||
filled={
|
||||
hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
hideStickerBtn ?
|
||||
(emojiBoardTab === EmojiBoardTab.Emoji
|
||||
|| emojiBoardTab === EmojiBoardTab.Gif) :
|
||||
emojiBoardTab === EmojiBoardTab.Emoji
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ const cons = {
|
|||
REUSABLE_DIALOG_OPENED: 'REUSABLE_DIALOG_OPENED',
|
||||
},
|
||||
},
|
||||
api: {
|
||||
GIF_PROXY_URL: 'https://proxy.commet.chat',
|
||||
},
|
||||
};
|
||||
|
||||
Object.freeze(cons);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue