mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 14:30: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 { DefaultReset, FocusOutline, color, config, toRem } from 'folds';
 | 
			
		||||
import { toRem, color, config, DefaultReset, FocusOutline } from 'folds';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Layout
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const Base = style({
 | 
			
		||||
  maxWidth: toRem(432),
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +17,15 @@ export const Base = style({
 | 
			
		|||
  overflow: 'hidden',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const Header = style({
 | 
			
		||||
  padding: config.space.S300,
 | 
			
		||||
  paddingBottom: 0,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Sidebar
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const Sidebar = style({
 | 
			
		||||
  width: toRem(54),
 | 
			
		||||
  backgroundColor: color.Surface.Container,
 | 
			
		||||
| 
						 | 
				
			
			@ -29,26 +42,21 @@ export const SidebarStack = style({
 | 
			
		|||
  backgroundColor: color.Surface.Container,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const NativeEmojiSidebarStack = style({
 | 
			
		||||
  position: 'sticky',
 | 
			
		||||
  bottom: '-67%',
 | 
			
		||||
  zIndex: 1,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const SidebarDivider = style({
 | 
			
		||||
  width: toRem(18),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const Header = style({
 | 
			
		||||
  padding: config.space.S300,
 | 
			
		||||
  paddingBottom: 0,
 | 
			
		||||
export const SidebarBtnImg = style({
 | 
			
		||||
  width: toRem(24),
 | 
			
		||||
  height: toRem(24),
 | 
			
		||||
  objectFit: 'contain',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const EmojiBoardTab = style({
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
});
 | 
			
		||||
/**
 | 
			
		||||
 * Preview
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const Footer = style({
 | 
			
		||||
export const Preview = style({
 | 
			
		||||
  padding: config.space.S200,
 | 
			
		||||
  margin: config.space.S300,
 | 
			
		||||
  marginTop: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -59,7 +67,30 @@ export const Footer = style({
 | 
			
		|||
  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({
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  padding: `${config.space.S300} 0`,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,15 +113,9 @@ export const EmojiGroupContent = style([
 | 
			
		|||
  },
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export const EmojiPreview = style([
 | 
			
		||||
  DefaultReset,
 | 
			
		||||
  {
 | 
			
		||||
    width: toRem(32),
 | 
			
		||||
    height: toRem(32),
 | 
			
		||||
    fontSize: toRem(32),
 | 
			
		||||
    lineHeight: toRem(32),
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
/**
 | 
			
		||||
 * Item
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const EmojiItem = style([
 | 
			
		||||
  DefaultReset,
 | 
			
		||||
| 
						 | 
				
			
			@ -1 +1,2 @@
 | 
			
		|||
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 React, { ReactNode } from 'react';
 | 
			
		||||
import { CompactLayout, ModernLayout } from '..';
 | 
			
		||||
import { BubbleLayout, CompactLayout, ModernLayout } from '..';
 | 
			
		||||
import { MessageLayout } from '../../../state/settings';
 | 
			
		||||
 | 
			
		||||
export type EventContentProps = {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,9 +30,15 @@ export function EventContent({ messageLayout, time, iconSrc, content }: EventCon
 | 
			
		|||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return messageLayout === MessageLayout.Compact ? (
 | 
			
		||||
    <CompactLayout before={beforeJSX}>{msgContentJSX}</CompactLayout>
 | 
			
		||||
  ) : (
 | 
			
		||||
    <ModernLayout before={beforeJSX}>{msgContentJSX}</ModernLayout>
 | 
			
		||||
  );
 | 
			
		||||
  if (messageLayout === MessageLayout.Compact) {
 | 
			
		||||
    return <CompactLayout before={beforeJSX}>{msgContentJSX}</CompactLayout>;
 | 
			
		||||
  }
 | 
			
		||||
  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 { Box, as } from 'folds';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { Box, ContainerColor, as, color } from 'folds';
 | 
			
		||||
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 = {
 | 
			
		||||
  hideBubble?: boolean;
 | 
			
		||||
  before?: ReactNode;
 | 
			
		||||
  header?: ReactNode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const BubbleLayout = as<'div', BubbleLayoutProps>(({ before, children, ...props }, ref) => (
 | 
			
		||||
  <Box gap="300" {...props} ref={ref}>
 | 
			
		||||
    <Box className={css.BubbleBefore} shrink="No">
 | 
			
		||||
      {before}
 | 
			
		||||
export const BubbleLayout = as<'div', BubbleLayoutProps>(
 | 
			
		||||
  ({ hideBubble, before, header, children, ...props }, ref) => (
 | 
			
		||||
    <Box gap="300" {...props} ref={ref}>
 | 
			
		||||
      <Box className={css.BubbleBefore} shrink="No">
 | 
			
		||||
        {before}
 | 
			
		||||
      </Box>
 | 
			
		||||
      <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}
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Box>
 | 
			
		||||
        )}
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Box>
 | 
			
		||||
    <Box className={css.BubbleContent} direction="Column">
 | 
			
		||||
      {children}
 | 
			
		||||
    </Box>
 | 
			
		||||
  </Box>
 | 
			
		||||
));
 | 
			
		||||
  )
 | 
			
		||||
);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -120,6 +120,7 @@ export const CompactHeader = style([
 | 
			
		|||
export const AvatarBase = style({
 | 
			
		||||
  paddingTop: toRem(4),
 | 
			
		||||
  transition: 'transform 200ms cubic-bezier(0, 0.8, 0.67, 0.97)',
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignSelf: 'start',
 | 
			
		||||
 | 
			
		||||
  selectors: {
 | 
			
		||||
| 
						 | 
				
			
			@ -133,14 +134,31 @@ export const ModernBefore = style({
 | 
			
		|||
  minWidth: toRem(36),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const BubbleBefore = style([ModernBefore]);
 | 
			
		||||
export const BubbleBefore = style({
 | 
			
		||||
  minWidth: toRem(36),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const BubbleContent = style({
 | 
			
		||||
  maxWidth: toRem(800),
 | 
			
		||||
  padding: config.space.S200,
 | 
			
		||||
  backgroundColor: color.SurfaceVariant.Container,
 | 
			
		||||
  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({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -723,6 +723,7 @@ export const Message = as<'div', MessageProps>(
 | 
			
		|||
    const mx = useMatrixClient();
 | 
			
		||||
    const useAuthentication = useMediaAuthentication();
 | 
			
		||||
    const senderId = mEvent.getSender() ?? '';
 | 
			
		||||
 | 
			
		||||
    const [hover, setHover] = useState(false);
 | 
			
		||||
    const { hoverProps } = useHover({ onHoverChange: setHover });
 | 
			
		||||
    const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover });
 | 
			
		||||
| 
						 | 
				
			
			@ -790,7 +791,9 @@ export const Message = as<'div', MessageProps>(
 | 
			
		|||
    );
 | 
			
		||||
 | 
			
		||||
    const avatarJSX = !collapse && messageLayout !== MessageLayout.Compact && (
 | 
			
		||||
      <AvatarBase>
 | 
			
		||||
      <AvatarBase
 | 
			
		||||
        className={messageLayout === MessageLayout.Bubble ? css.BubbleAvatarBase : undefined}
 | 
			
		||||
      >
 | 
			
		||||
        <Avatar
 | 
			
		||||
          className={css.MessageAvatar}
 | 
			
		||||
          as="button"
 | 
			
		||||
| 
						 | 
				
			
			@ -875,7 +878,9 @@ export const Message = as<'div', MessageProps>(
 | 
			
		|||
 | 
			
		||||
    return (
 | 
			
		||||
      <MessageBase
 | 
			
		||||
        className={classNames(css.MessageBase, className)}
 | 
			
		||||
        className={classNames(css.MessageBase, className, {
 | 
			
		||||
          [css.MessageBaseBubbleCollapsed]: messageLayout === MessageLayout.Bubble && collapse,
 | 
			
		||||
        })}
 | 
			
		||||
        tabIndex={0}
 | 
			
		||||
        space={messageSpacing}
 | 
			
		||||
        collapse={collapse}
 | 
			
		||||
| 
						 | 
				
			
			@ -1132,8 +1137,7 @@ export const Message = as<'div', MessageProps>(
 | 
			
		|||
          </CompactLayout>
 | 
			
		||||
        )}
 | 
			
		||||
        {messageLayout === MessageLayout.Bubble && (
 | 
			
		||||
          <BubbleLayout before={avatarJSX} onContextMenu={handleContextMenu}>
 | 
			
		||||
            {headerJSX}
 | 
			
		||||
          <BubbleLayout before={avatarJSX} header={headerJSX} onContextMenu={handleContextMenu}>
 | 
			
		||||
            {msgContentJSX}
 | 
			
		||||
          </BubbleLayout>
 | 
			
		||||
        )}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,9 @@ import { DefaultReset, config, toRem } from 'folds';
 | 
			
		|||
export const MessageBase = style({
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
});
 | 
			
		||||
export const MessageBaseBubbleCollapsed = style({
 | 
			
		||||
  paddingTop: 0,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const MessageOptionsBase = style([
 | 
			
		||||
  DefaultReset,
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +24,10 @@ export const MessageOptionsBar = style([
 | 
			
		|||
  },
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
export const BubbleAvatarBase = style({
 | 
			
		||||
  paddingTop: 0,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const MessageAvatar = style({
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
import { style } from '@vanilla-extract/css';
 | 
			
		||||
import { recipe } from '@vanilla-extract/recipes';
 | 
			
		||||
import { color, config, DefaultReset, toRem } from 'folds';
 | 
			
		||||
import { ContainerColor } from './ContainerColor.css';
 | 
			
		||||
 | 
			
		||||
export const MarginSpaced = style({
 | 
			
		||||
  marginBottom: config.space.S200,
 | 
			
		||||
| 
						 | 
				
			
			@ -92,11 +93,14 @@ export const CodeBlock = style([
 | 
			
		|||
    overflow: 'hidden',
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
export const CodeBlockHeader = style({
 | 
			
		||||
  padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
 | 
			
		||||
  borderBottomWidth: config.borderWidth.B300,
 | 
			
		||||
  gap: config.space.S200,
 | 
			
		||||
});
 | 
			
		||||
export const CodeBlockHeader = style([
 | 
			
		||||
  ContainerColor({ variant: 'Surface' }),
 | 
			
		||||
  {
 | 
			
		||||
    padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
 | 
			
		||||
    borderBottomWidth: config.borderWidth.B300,
 | 
			
		||||
    gap: config.space.S200,
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
export const CodeBlockInternal = style([
 | 
			
		||||
  CodeFont,
 | 
			
		||||
  {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue