mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-04 22:40:29 +03:00
Merge branch 'dev' into fix-257
This commit is contained in:
commit
19096c3543
19 changed files with 1084 additions and 852 deletions
File diff suppressed because it is too large
Load diff
34
src/app/components/emoji-board/components/Group.tsx
Normal file
34
src/app/components/emoji-board/components/Group.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { as, Box, Text } from 'folds';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
export const getDOMGroupId = (id: string): string => `EmojiBoardGroup-${id}`;
|
||||||
|
|
||||||
|
export const EmojiGroup = as<
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
>(({ className, id, label, children, ...props }, ref) => (
|
||||||
|
<Box
|
||||||
|
id={getDOMGroupId(id)}
|
||||||
|
data-group-id={id}
|
||||||
|
className={classNames(css.EmojiGroup, className)}
|
||||||
|
direction="Column"
|
||||||
|
gap="200"
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Text id={`EmojiGroup-${id}-label`} as="label" className={css.EmojiGroupLabel} size="O400">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<div aria-labelledby={`EmojiGroup-${id}-label`} className={css.EmojiGroupContent}>
|
||||||
|
<Box wrap="Wrap" justifyContent="Center">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
105
src/app/components/emoji-board/components/Item.tsx
Normal file
105
src/app/components/emoji-board/components/Item.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box } from 'folds';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import { EmojiItemInfo, EmojiType } from '../types';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
import { PackImageReader } from '../../../plugins/custom-emoji';
|
||||||
|
import { IEmoji } from '../../../plugins/emoji';
|
||||||
|
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
|
|
||||||
|
export const getEmojiItemInfo = (element: Element): EmojiItemInfo | undefined => {
|
||||||
|
const label = element.getAttribute('title');
|
||||||
|
const type = element.getAttribute('data-emoji-type') as EmojiType | undefined;
|
||||||
|
const data = element.getAttribute('data-emoji-data');
|
||||||
|
const shortcode = element.getAttribute('data-emoji-shortcode');
|
||||||
|
|
||||||
|
if (type && data && shortcode && label)
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
shortcode,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmojiItemProps = {
|
||||||
|
emoji: IEmoji;
|
||||||
|
};
|
||||||
|
export function EmojiItem({ emoji }: EmojiItemProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
className={css.EmojiItem}
|
||||||
|
title={emoji.label}
|
||||||
|
aria-label={`${emoji.label} emoji`}
|
||||||
|
data-emoji-type={EmojiType.Emoji}
|
||||||
|
data-emoji-data={emoji.unicode}
|
||||||
|
data-emoji-shortcode={emoji.shortcode}
|
||||||
|
>
|
||||||
|
{emoji.unicode}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomEmojiItemProps = {
|
||||||
|
mx: MatrixClient;
|
||||||
|
useAuthentication?: boolean;
|
||||||
|
image: PackImageReader;
|
||||||
|
};
|
||||||
|
export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiItemProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
className={css.EmojiItem}
|
||||||
|
title={image.body || image.shortcode}
|
||||||
|
aria-label={`${image.body || image.shortcode} emoji`}
|
||||||
|
data-emoji-type={EmojiType.CustomEmoji}
|
||||||
|
data-emoji-data={image.url}
|
||||||
|
data-emoji-shortcode={image.shortcode}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
className={css.CustomEmojiImg}
|
||||||
|
alt={image.body || image.shortcode}
|
||||||
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type StickerItemProps = {
|
||||||
|
mx: MatrixClient;
|
||||||
|
useAuthentication?: boolean;
|
||||||
|
image: PackImageReader;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
as="button"
|
||||||
|
type="button"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
className={css.StickerItem}
|
||||||
|
title={image.body || image.shortcode}
|
||||||
|
aria-label={`${image.body || image.shortcode} emoji`}
|
||||||
|
data-emoji-type={EmojiType.Sticker}
|
||||||
|
data-emoji-data={image.url}
|
||||||
|
data-emoji-shortcode={image.shortcode}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
className={css.StickerImg}
|
||||||
|
alt={image.body || image.shortcode}
|
||||||
|
src={mxcUrlToHttp(mx, image.url, useAuthentication) ?? image.url}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/app/components/emoji-board/components/Layout.tsx
Normal file
30
src/app/components/emoji-board/components/Layout.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { as, Box, Line } from 'folds';
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
export const EmojiBoardLayout = as<
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
header: ReactNode;
|
||||||
|
sidebar?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
>(({ className, header, sidebar, children, ...props }, ref) => (
|
||||||
|
<Box
|
||||||
|
display="InlineFlex"
|
||||||
|
className={classNames(css.Base, className)}
|
||||||
|
direction="Row"
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Box direction="Column" grow="Yes">
|
||||||
|
<Box className={css.Header} direction="Column" shrink="No">
|
||||||
|
{header}
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
<Line size="300" direction="Vertical" />
|
||||||
|
{sidebar}
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
22
src/app/components/emoji-board/components/NoStickerPacks.tsx
Normal file
22
src/app/components/emoji-board/components/NoStickerPacks.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, toRem, config, Icons, Icon, Text } from 'folds';
|
||||||
|
|
||||||
|
export function NoStickerPacks() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{ padding: `${toRem(60)} ${config.space.S500}` }}
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
direction="Column"
|
||||||
|
gap="300"
|
||||||
|
>
|
||||||
|
<Icon size="600" src={Icons.Sticker} />
|
||||||
|
<Box direction="Inherit">
|
||||||
|
<Text align="Center">No Sticker Packs!</Text>
|
||||||
|
<Text priority="300" align="Center" size="T200">
|
||||||
|
Add stickers from user, room or space settings.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
src/app/components/emoji-board/components/Preview.tsx
Normal file
53
src/app/components/emoji-board/components/Preview.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Box, Text } from 'folds';
|
||||||
|
import React from 'react';
|
||||||
|
import { Atom, atom, useAtomValue } from 'jotai';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
|
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||||
|
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
|
|
||||||
|
export type PreviewData = {
|
||||||
|
key: string;
|
||||||
|
shortcode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPreviewDataAtom = (initial?: PreviewData) =>
|
||||||
|
atom<PreviewData | undefined>(initial);
|
||||||
|
|
||||||
|
type PreviewProps = {
|
||||||
|
previewAtom: Atom<PreviewData | undefined>;
|
||||||
|
};
|
||||||
|
export function Preview({ previewAtom }: PreviewProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
|
|
||||||
|
const { key, shortcode } = useAtomValue(previewAtom) ?? {};
|
||||||
|
|
||||||
|
if (!shortcode) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box shrink="No" className={css.Preview} gap="300" alignItems="Center">
|
||||||
|
{key && (
|
||||||
|
<Box
|
||||||
|
display="InlineFlex"
|
||||||
|
className={css.PreviewEmoji}
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
>
|
||||||
|
{key.startsWith('mxc://') ? (
|
||||||
|
<img
|
||||||
|
className={css.PreviewImg}
|
||||||
|
src={mxcUrlToHttp(mx, key, useAuthentication) ?? key}
|
||||||
|
alt={shortcode}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
key
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Text size="H5" truncate>
|
||||||
|
:{shortcode}:
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/app/components/emoji-board/components/SearchInput.tsx
Normal file
51
src/app/components/emoji-board/components/SearchInput.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React, { ChangeEventHandler, useRef } from 'react';
|
||||||
|
import { Input, Chip, Icon, Icons, Text } from 'folds';
|
||||||
|
import { mobileOrTablet } from '../../../utils/user-agent';
|
||||||
|
|
||||||
|
type SearchInputProps = {
|
||||||
|
query?: string;
|
||||||
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
allowTextCustomEmoji?: boolean;
|
||||||
|
onTextCustomEmojiSelect?: (text: string) => void;
|
||||||
|
};
|
||||||
|
export function SearchInput({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
allowTextCustomEmoji,
|
||||||
|
onTextCustomEmojiSelect,
|
||||||
|
}: SearchInputProps) {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleReact = () => {
|
||||||
|
const textEmoji = inputRef.current?.value.trim();
|
||||||
|
if (!textEmoji) return;
|
||||||
|
onTextCustomEmojiSelect?.(textEmoji);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
size="400"
|
||||||
|
placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'}
|
||||||
|
maxLength={50}
|
||||||
|
after={
|
||||||
|
allowTextCustomEmoji && query ? (
|
||||||
|
<Chip
|
||||||
|
variant="Primary"
|
||||||
|
radii="Pill"
|
||||||
|
after={<Icon src={Icons.ArrowRight} size="50" />}
|
||||||
|
outlined
|
||||||
|
onClick={handleReact}
|
||||||
|
>
|
||||||
|
<Text size="L400">React</Text>
|
||||||
|
</Chip>
|
||||||
|
) : (
|
||||||
|
<Icon src={Icons.Search} size="50" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
autoFocus={!mobileOrTablet()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
src/app/components/emoji-board/components/Sidebar.tsx
Normal file
130
src/app/components/emoji-board/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Scroll,
|
||||||
|
Line,
|
||||||
|
as,
|
||||||
|
TooltipProvider,
|
||||||
|
Tooltip,
|
||||||
|
Text,
|
||||||
|
IconButton,
|
||||||
|
Icon,
|
||||||
|
IconSrc,
|
||||||
|
Icons,
|
||||||
|
} from 'folds';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as css from './styles.css';
|
||||||
|
|
||||||
|
export function Sidebar({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Box className={css.Sidebar} shrink="No">
|
||||||
|
<Scroll size="0">
|
||||||
|
<Box className={css.SidebarContent} direction="Column" alignItems="Center" gap="100">
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</Scroll>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarStack = as<'div'>(({ className, children, ...props }, ref) => (
|
||||||
|
<Box
|
||||||
|
className={classNames(css.SidebarStack, className)}
|
||||||
|
direction="Column"
|
||||||
|
alignItems="Center"
|
||||||
|
gap="100"
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
export function SidebarDivider() {
|
||||||
|
return <Line className={css.SidebarDivider} size="300" variant="Surface" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarBtn<T extends string>({
|
||||||
|
active,
|
||||||
|
label,
|
||||||
|
id,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
label: string;
|
||||||
|
id: T;
|
||||||
|
onClick: (id: T) => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider
|
||||||
|
delay={500}
|
||||||
|
position="Left"
|
||||||
|
tooltip={
|
||||||
|
<Tooltip id={`SidebarStackItem-${id}-label`}>
|
||||||
|
<Text size="T300">{label}</Text>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(ref) => (
|
||||||
|
<IconButton
|
||||||
|
aria-pressed={active}
|
||||||
|
aria-labelledby={`SidebarStackItem-${id}-label`}
|
||||||
|
ref={ref}
|
||||||
|
onClick={() => onClick(id)}
|
||||||
|
size="400"
|
||||||
|
radii="300"
|
||||||
|
variant="Surface"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupIconProps<T extends string> = {
|
||||||
|
active: boolean;
|
||||||
|
id: T;
|
||||||
|
label: string;
|
||||||
|
icon: IconSrc;
|
||||||
|
onClick: (id: T) => void;
|
||||||
|
};
|
||||||
|
export function GroupIcon<T extends string>({
|
||||||
|
active,
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
onClick,
|
||||||
|
}: GroupIconProps<T>) {
|
||||||
|
return (
|
||||||
|
<SidebarBtn active={active} id={id} label={label} onClick={onClick}>
|
||||||
|
<Icon src={icon} filled={active} />
|
||||||
|
</SidebarBtn>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageGroupIconProps<T extends string> = {
|
||||||
|
active: boolean;
|
||||||
|
id: T;
|
||||||
|
label: string;
|
||||||
|
url?: string;
|
||||||
|
onClick: (id: T) => void;
|
||||||
|
};
|
||||||
|
export function ImageGroupIcon<T extends string>({
|
||||||
|
active,
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
url,
|
||||||
|
onClick,
|
||||||
|
}: ImageGroupIconProps<T>) {
|
||||||
|
return (
|
||||||
|
<SidebarBtn active={active} id={id} label={label} onClick={onClick}>
|
||||||
|
{url ? (
|
||||||
|
<img className={css.SidebarBtnImg} src={url} alt={label} />
|
||||||
|
) : (
|
||||||
|
<Icon src={Icons.Photo} filled={active} />
|
||||||
|
)}
|
||||||
|
</SidebarBtn>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
src/app/components/emoji-board/components/Tabs.tsx
Normal file
44
src/app/components/emoji-board/components/Tabs.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import React, { CSSProperties } from 'react';
|
||||||
|
import { Badge, Box, Text } from 'folds';
|
||||||
|
import { EmojiBoardTab } from '../types';
|
||||||
|
|
||||||
|
const styles: CSSProperties = {
|
||||||
|
cursor: 'pointer',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EmojiBoardTabs({
|
||||||
|
tab,
|
||||||
|
onTabChange,
|
||||||
|
}: {
|
||||||
|
tab: EmojiBoardTab;
|
||||||
|
onTabChange: (tab: EmojiBoardTab) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box gap="100">
|
||||||
|
<Badge
|
||||||
|
style={styles}
|
||||||
|
as="button"
|
||||||
|
variant="Secondary"
|
||||||
|
fill={tab === EmojiBoardTab.Sticker ? 'Solid' : 'None'}
|
||||||
|
size="500"
|
||||||
|
onClick={() => onTabChange(EmojiBoardTab.Sticker)}
|
||||||
|
>
|
||||||
|
<Text as="span" size="L400">
|
||||||
|
Sticker
|
||||||
|
</Text>
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
style={styles}
|
||||||
|
as="button"
|
||||||
|
variant="Secondary"
|
||||||
|
fill={tab === EmojiBoardTab.Emoji ? 'Solid' : 'None'}
|
||||||
|
size="500"
|
||||||
|
onClick={() => onTabChange(EmojiBoardTab.Emoji)}
|
||||||
|
>
|
||||||
|
<Text as="span" size="L400">
|
||||||
|
Emoji
|
||||||
|
</Text>
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/app/components/emoji-board/components/index.tsx
Normal file
8
src/app/components/emoji-board/components/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export * from './SearchInput';
|
||||||
|
export * from './Tabs';
|
||||||
|
export * from './Sidebar';
|
||||||
|
export * from './NoStickerPacks';
|
||||||
|
export * from './Preview';
|
||||||
|
export * from './Item';
|
||||||
|
export * from './Group';
|
||||||
|
export * from './Layout';
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
import { DefaultReset, FocusOutline, color, config, toRem } from 'folds';
|
import { toRem, color, config, DefaultReset, FocusOutline } from 'folds';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout
|
||||||
|
*/
|
||||||
|
|
||||||
export const Base = style({
|
export const Base = style({
|
||||||
maxWidth: toRem(432),
|
maxWidth: toRem(432),
|
||||||
|
|
@ -13,6 +17,15 @@ export const Base = style({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const Header = style({
|
||||||
|
padding: config.space.S300,
|
||||||
|
paddingBottom: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sidebar
|
||||||
|
*/
|
||||||
|
|
||||||
export const Sidebar = style({
|
export const Sidebar = style({
|
||||||
width: toRem(54),
|
width: toRem(54),
|
||||||
backgroundColor: color.Surface.Container,
|
backgroundColor: color.Surface.Container,
|
||||||
|
|
@ -29,26 +42,21 @@ export const SidebarStack = style({
|
||||||
backgroundColor: color.Surface.Container,
|
backgroundColor: color.Surface.Container,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const NativeEmojiSidebarStack = style({
|
|
||||||
position: 'sticky',
|
|
||||||
bottom: '-67%',
|
|
||||||
zIndex: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SidebarDivider = style({
|
export const SidebarDivider = style({
|
||||||
width: toRem(18),
|
width: toRem(18),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Header = style({
|
export const SidebarBtnImg = style({
|
||||||
padding: config.space.S300,
|
width: toRem(24),
|
||||||
paddingBottom: 0,
|
height: toRem(24),
|
||||||
|
objectFit: 'contain',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EmojiBoardTab = style({
|
/**
|
||||||
cursor: 'pointer',
|
* Preview
|
||||||
});
|
*/
|
||||||
|
|
||||||
export const Footer = style({
|
export const Preview = style({
|
||||||
padding: config.space.S200,
|
padding: config.space.S200,
|
||||||
margin: config.space.S300,
|
margin: config.space.S300,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
|
|
@ -59,7 +67,30 @@ export const Footer = style({
|
||||||
color: color.SurfaceVariant.OnContainer,
|
color: color.SurfaceVariant.OnContainer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const PreviewEmoji = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
width: toRem(32),
|
||||||
|
height: toRem(32),
|
||||||
|
fontSize: toRem(32),
|
||||||
|
lineHeight: toRem(32),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
export const PreviewImg = style([
|
||||||
|
DefaultReset,
|
||||||
|
{
|
||||||
|
width: toRem(32),
|
||||||
|
height: toRem(32),
|
||||||
|
objectFit: 'contain',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group
|
||||||
|
*/
|
||||||
|
|
||||||
export const EmojiGroup = style({
|
export const EmojiGroup = style({
|
||||||
|
position: 'relative',
|
||||||
padding: `${config.space.S300} 0`,
|
padding: `${config.space.S300} 0`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -82,15 +113,9 @@ export const EmojiGroupContent = style([
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const EmojiPreview = style([
|
/**
|
||||||
DefaultReset,
|
* Item
|
||||||
{
|
*/
|
||||||
width: toRem(32),
|
|
||||||
height: toRem(32),
|
|
||||||
fontSize: toRem(32),
|
|
||||||
lineHeight: toRem(32),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const EmojiItem = style([
|
export const EmojiItem = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export * from './EmojiBoard';
|
export * from './EmojiBoard';
|
||||||
|
export * from './types';
|
||||||
|
|
|
||||||
17
src/app/components/emoji-board/types.ts
Normal file
17
src/app/components/emoji-board/types.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
export enum EmojiBoardTab {
|
||||||
|
Emoji = 'Emoji',
|
||||||
|
Sticker = 'Sticker',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EmojiType {
|
||||||
|
Emoji = 'emoji',
|
||||||
|
CustomEmoji = 'customEmoji',
|
||||||
|
Sticker = 'sticker',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EmojiItemInfo = {
|
||||||
|
type: EmojiType;
|
||||||
|
data: string;
|
||||||
|
shortcode: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, Icon, IconSrc } from 'folds';
|
import { Box, Icon, IconSrc } from 'folds';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { CompactLayout, ModernLayout } from '..';
|
import { BubbleLayout, CompactLayout, ModernLayout } from '..';
|
||||||
import { MessageLayout } from '../../../state/settings';
|
import { MessageLayout } from '../../../state/settings';
|
||||||
|
|
||||||
export type EventContentProps = {
|
export type EventContentProps = {
|
||||||
|
|
@ -30,9 +30,15 @@ export function EventContent({ messageLayout, time, iconSrc, content }: EventCon
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
return messageLayout === MessageLayout.Compact ? (
|
if (messageLayout === MessageLayout.Compact) {
|
||||||
<CompactLayout before={beforeJSX}>{msgContentJSX}</CompactLayout>
|
return <CompactLayout before={beforeJSX}>{msgContentJSX}</CompactLayout>;
|
||||||
) : (
|
}
|
||||||
<ModernLayout before={beforeJSX}>{msgContentJSX}</ModernLayout>
|
if (messageLayout === MessageLayout.Bubble) {
|
||||||
|
return (
|
||||||
|
<BubbleLayout hideBubble before={beforeJSX}>
|
||||||
|
{msgContentJSX}
|
||||||
|
</BubbleLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return <ModernLayout before={beforeJSX}>{msgContentJSX}</ModernLayout>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,63 @@
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Box, as } from 'folds';
|
import classNames from 'classnames';
|
||||||
|
import { Box, ContainerColor, as, color } from 'folds';
|
||||||
import * as css from './layout.css';
|
import * as css from './layout.css';
|
||||||
|
|
||||||
|
type BubbleArrowProps = {
|
||||||
|
variant: ContainerColor;
|
||||||
|
};
|
||||||
|
function BubbleLeftArrow({ variant }: BubbleArrowProps) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={css.BubbleLeftArrow}
|
||||||
|
width="9"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 9 8"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M9.00004 8V0H4.82847C3.04666 0 2.15433 2.15428 3.41426 3.41421L8.00004 8H9.00004Z"
|
||||||
|
fill={color[variant].Container}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type BubbleLayoutProps = {
|
type BubbleLayoutProps = {
|
||||||
|
hideBubble?: boolean;
|
||||||
before?: ReactNode;
|
before?: ReactNode;
|
||||||
|
header?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BubbleLayout = as<'div', BubbleLayoutProps>(({ before, children, ...props }, ref) => (
|
export const BubbleLayout = as<'div', BubbleLayoutProps>(
|
||||||
|
({ hideBubble, before, header, children, ...props }, ref) => (
|
||||||
<Box gap="300" {...props} ref={ref}>
|
<Box gap="300" {...props} ref={ref}>
|
||||||
<Box className={css.BubbleBefore} shrink="No">
|
<Box className={css.BubbleBefore} shrink="No">
|
||||||
{before}
|
{before}
|
||||||
</Box>
|
</Box>
|
||||||
<Box className={css.BubbleContent} direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
{header}
|
||||||
|
{hideBubble ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
className={
|
||||||
|
hideBubble
|
||||||
|
? undefined
|
||||||
|
: classNames(css.BubbleContent, before ? css.BubbleContentArrowLeft : undefined)
|
||||||
|
}
|
||||||
|
direction="Column"
|
||||||
|
>
|
||||||
|
{before ? <BubbleLeftArrow variant="SurfaceVariant" /> : null}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
));
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export const CompactHeader = style([
|
||||||
export const AvatarBase = style({
|
export const AvatarBase = style({
|
||||||
paddingTop: toRem(4),
|
paddingTop: toRem(4),
|
||||||
transition: 'transform 200ms cubic-bezier(0, 0.8, 0.67, 0.97)',
|
transition: 'transform 200ms cubic-bezier(0, 0.8, 0.67, 0.97)',
|
||||||
|
display: 'flex',
|
||||||
alignSelf: 'start',
|
alignSelf: 'start',
|
||||||
|
|
||||||
selectors: {
|
selectors: {
|
||||||
|
|
@ -133,14 +134,31 @@ export const ModernBefore = style({
|
||||||
minWidth: toRem(36),
|
minWidth: toRem(36),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const BubbleBefore = style([ModernBefore]);
|
export const BubbleBefore = style({
|
||||||
|
minWidth: toRem(36),
|
||||||
|
});
|
||||||
|
|
||||||
export const BubbleContent = style({
|
export const BubbleContent = style({
|
||||||
maxWidth: toRem(800),
|
maxWidth: toRem(800),
|
||||||
padding: config.space.S200,
|
padding: config.space.S200,
|
||||||
backgroundColor: color.SurfaceVariant.Container,
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
color: color.SurfaceVariant.OnContainer,
|
color: color.SurfaceVariant.OnContainer,
|
||||||
borderRadius: config.radii.R400,
|
borderRadius: config.radii.R500,
|
||||||
|
position: 'relative',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BubbleContentArrowLeft = style({
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BubbleLeftArrow = style({
|
||||||
|
width: toRem(9),
|
||||||
|
height: toRem(8),
|
||||||
|
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: toRem(-8),
|
||||||
|
zIndex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Username = style({
|
export const Username = style({
|
||||||
|
|
|
||||||
|
|
@ -723,6 +723,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const senderId = mEvent.getSender() ?? '';
|
const senderId = mEvent.getSender() ?? '';
|
||||||
|
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
const { hoverProps } = useHover({ onHoverChange: setHover });
|
const { hoverProps } = useHover({ onHoverChange: setHover });
|
||||||
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
|
const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
|
||||||
|
|
@ -790,7 +791,9 @@ export const Message = as<'div', MessageProps>(
|
||||||
);
|
);
|
||||||
|
|
||||||
const avatarJSX = !collapse && messageLayout !== MessageLayout.Compact && (
|
const avatarJSX = !collapse && messageLayout !== MessageLayout.Compact && (
|
||||||
<AvatarBase>
|
<AvatarBase
|
||||||
|
className={messageLayout === MessageLayout.Bubble ? css.BubbleAvatarBase : undefined}
|
||||||
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
className={css.MessageAvatar}
|
className={css.MessageAvatar}
|
||||||
as="button"
|
as="button"
|
||||||
|
|
@ -875,7 +878,9 @@ export const Message = as<'div', MessageProps>(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageBase
|
<MessageBase
|
||||||
className={classNames(css.MessageBase, className)}
|
className={classNames(css.MessageBase, className, {
|
||||||
|
[css.MessageBaseBubbleCollapsed]: messageLayout === MessageLayout.Bubble && collapse,
|
||||||
|
})}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
space={messageSpacing}
|
space={messageSpacing}
|
||||||
collapse={collapse}
|
collapse={collapse}
|
||||||
|
|
@ -1132,8 +1137,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
</CompactLayout>
|
</CompactLayout>
|
||||||
)}
|
)}
|
||||||
{messageLayout === MessageLayout.Bubble && (
|
{messageLayout === MessageLayout.Bubble && (
|
||||||
<BubbleLayout before={avatarJSX} onContextMenu={handleContextMenu}>
|
<BubbleLayout before={avatarJSX} header={headerJSX} onContextMenu={handleContextMenu}>
|
||||||
{headerJSX}
|
|
||||||
{msgContentJSX}
|
{msgContentJSX}
|
||||||
</BubbleLayout>
|
</BubbleLayout>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { DefaultReset, config, toRem } from 'folds';
|
||||||
export const MessageBase = style({
|
export const MessageBase = style({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
});
|
});
|
||||||
|
export const MessageBaseBubbleCollapsed = style({
|
||||||
|
paddingTop: 0,
|
||||||
|
});
|
||||||
|
|
||||||
export const MessageOptionsBase = style([
|
export const MessageOptionsBase = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
|
|
@ -21,6 +24,10 @@ export const MessageOptionsBar = style([
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const BubbleAvatarBase = style({
|
||||||
|
paddingTop: 0,
|
||||||
|
});
|
||||||
|
|
||||||
export const MessageAvatar = style({
|
export const MessageAvatar = style({
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
import { recipe } from '@vanilla-extract/recipes';
|
import { recipe } from '@vanilla-extract/recipes';
|
||||||
import { color, config, DefaultReset, toRem } from 'folds';
|
import { color, config, DefaultReset, toRem } from 'folds';
|
||||||
|
import { ContainerColor } from './ContainerColor.css';
|
||||||
|
|
||||||
export const MarginSpaced = style({
|
export const MarginSpaced = style({
|
||||||
marginBottom: config.space.S200,
|
marginBottom: config.space.S200,
|
||||||
|
|
@ -92,11 +93,14 @@ export const CodeBlock = style([
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
export const CodeBlockHeader = style({
|
export const CodeBlockHeader = style([
|
||||||
|
ContainerColor({ variant: 'Surface' }),
|
||||||
|
{
|
||||||
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
|
||||||
borderBottomWidth: config.borderWidth.B300,
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
gap: config.space.S200,
|
gap: config.space.S200,
|
||||||
});
|
},
|
||||||
|
]);
|
||||||
export const CodeBlockInternal = style([
|
export const CodeBlockInternal = style([
|
||||||
CodeFont,
|
CodeFont,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue