snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
import baseConfig from '@rocket.chat/storybook-config/main';
export default baseConfig();

View File

@@ -0,0 +1,11 @@
import { parameters, decorators } from '@rocket.chat/storybook-config/preview';
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
...parameters,
},
decorators: [...decorators],
};
export default preview;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
import client from '@rocket.chat/jest-presets/client';
import type { Config } from 'jest';
export default {
preset: client.preset,
setupFilesAfterEnv: [...client.setupFilesAfterEnv],
moduleNameMapper: {
'^react($|/.+)': '<rootDir>/../../node_modules/react$1',
'^react-dom($|/.+)': '<rootDir>/../../node_modules/react-dom$1',
'^react-i18next($|/.+)': '<rootDir>/../../node_modules/react-i18next$1',
},
} satisfies Config;

View File

@@ -0,0 +1,65 @@
{
"name": "@rocket.chat/ui-video-conf",
"version": "34.0.0",
"private": true,
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.build.json",
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"storybook": "storybook dev -p 6006",
"test": "jest",
"testunit": "jest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@rocket.chat/emitter": "workspace:~"
},
"devDependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/css-in-js": "^0.33.1",
"@rocket.chat/fuselage": "^0.89.0",
"@rocket.chat/fuselage-hooks": "^0.43.1",
"@rocket.chat/fuselage-tokens": "^0.34.0",
"@rocket.chat/icons": "^0.49.0",
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/storybook-config": "workspace:~",
"@rocket.chat/styled": "~0.34.1",
"@rocket.chat/tsconfig": "workspace:*",
"@rocket.chat/ui-avatar": "workspace:^",
"@rocket.chat/ui-contexts": "workspace:^",
"@storybook/react": "^9.1.20",
"@types/jest": "~30.0.0",
"@types/jest-axe": "~3.5.9",
"@types/react": "~19.2.18",
"@types/react-dom": "~19.2.5",
"eslint": "~9.39.5",
"jest": "~30.2.0",
"jest-axe": "~10.0.0",
"react": "~19.2.8",
"react-dom": "~19.2.8",
"react-virtuoso": "~4.18.12",
"storybook": "^9.1.20",
"typescript": "~5.9.3",
"webpack": "~5.104.1"
},
"peerDependencies": {
"@rocket.chat/css-in-js": "*",
"@rocket.chat/fuselage": "*",
"@rocket.chat/fuselage-hooks": "*",
"@rocket.chat/icons": "*",
"@rocket.chat/styled": "*",
"@rocket.chat/ui-avatar": "workspace:^",
"@rocket.chat/ui-contexts": "workspace:^",
"react": "*",
"react-dom": "*"
},
"volta": {
"extends": "../../package.json"
}
}

View File

@@ -0,0 +1,19 @@
import { composeStories } from '@storybook/react';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import * as stories from './VideoConfButton.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

View File

@@ -0,0 +1,12 @@
import type { Meta, StoryObj } from '@storybook/react';
import VideoConfButton from './VideoConfButton';
export default {
component: VideoConfButton,
decorators: [],
} satisfies Meta<typeof VideoConfButton>;
export const Default: StoryObj<typeof VideoConfButton> = {
render: (args) => <VideoConfButton {...args}>Button</VideoConfButton>,
};

View File

@@ -0,0 +1,20 @@
import { Button } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { ReactNode, ButtonHTMLAttributes } from 'react';
export type VideoConfButtonProps = {
icon?: IconName;
primary?: boolean;
secondary?: boolean;
danger?: boolean;
disabled?: boolean;
children: ReactNode;
} & Omit<ButtonHTMLAttributes<HTMLElement>, 'ref' | 'is' | 'className' | 'size' | 'elevation'>;
const VideoConfButton = ({ primary, secondary, danger, disabled, icon, children, ...props }: VideoConfButtonProps) => (
<Button icon={icon} width='100%' primary={primary} danger={danger} secondary={secondary} disabled={disabled} {...props}>
{children}
</Button>
);
export default VideoConfButton;

View File

@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`renders Default without crashing 1`] = `
<body>
<div>
<button
class="rcx-box rcx-box--full rcx-button rcx-css-1qcz93u"
type="button"
>
<span
class="rcx-button--content"
>
Button
</span>
</button>
</div>
</body>
`;

View File

@@ -0,0 +1 @@
export { default } from './VideoConfButton';

View File

@@ -0,0 +1,28 @@
import type { IRoom, DirectCallData, ProviderCapabilities, CallPreferences } from '@rocket.chat/core-typings';
import { createContext } from 'react';
export type VideoConfPopupPayload = {
id: string;
rid: IRoom['_id'];
isReceiving?: boolean;
};
export type VideoConfContextValue = {
dispatchOutgoing: (options: Omit<VideoConfPopupPayload, 'id'>) => void;
dismissOutgoing: () => void;
startCall: (rid: IRoom['_id'], title?: string) => void;
acceptCall: (callId: string) => void;
joinCall: (callId: string) => void;
dismissCall: (callId: string) => void;
rejectIncomingCall: (callId: string) => void;
abortCall: () => void;
setPreferences: (prefs: { mic?: boolean; cam?: boolean }) => void;
loadCapabilities: () => Promise<void>;
queryIncomingCalls: () => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => DirectCallData[]];
queryRinging: () => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean];
queryCalling: () => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean];
queryCapabilities: () => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ProviderCapabilities];
queryPreferences: () => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => CallPreferences];
};
export const VideoConfContext = createContext<VideoConfContextValue | undefined>(undefined);

View File

@@ -0,0 +1,19 @@
import { composeStories } from '@storybook/react';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import * as stories from './VideoConfController.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

View File

@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react';
import VideoConfController from './VideoConfController';
export default {
component: VideoConfController,
} satisfies Meta<typeof VideoConfController>;
export const Default: StoryObj<typeof VideoConfController> = {
args: {
'icon': 'info',
'aria-label': 'info',
},
};

View File

@@ -0,0 +1,25 @@
import { IconButton } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import type { ButtonHTMLAttributes } from 'react';
export type VideoConfControllerProps = {
icon: IconName;
active?: boolean;
secondary?: boolean;
disabled?: boolean;
small?: boolean;
} & Omit<ButtonHTMLAttributes<HTMLElement>, 'ref' | 'is' | 'className' | 'size' | 'elevation'>;
const VideoConfController = ({ icon, active, secondary, disabled, small = true, ...props }: VideoConfControllerProps) => (
<IconButton
aria-live='assertive'
small={small}
icon={icon}
info={active}
disabled={disabled}
secondary={secondary || active || disabled}
{...props}
/>
);
export default VideoConfController;

View File

@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`renders Default without crashing 1`] = `
<body>
<div>
<button
aria-label="info"
aria-live="assertive"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-info rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</body>
`;

View File

@@ -0,0 +1 @@
export { default } from './VideoConfController';

View File

@@ -0,0 +1,19 @@
import { composeStories } from '@storybook/react';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import * as stories from './VideoConfMessage.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

View File

@@ -0,0 +1,171 @@
import {
MessageDivider,
Message,
MessageLeftContainer,
MessageContainer,
MessageHeader,
MessageName,
MessageUsername,
MessageRole,
MessageTimestamp,
MessageBody,
Avatar,
Box,
} from '@rocket.chat/fuselage';
import type { Meta, StoryObj } from '@storybook/react';
import { VideoConfMessage, VideoConfMessageIcon, VideoConfMessageRow, VideoConfMessageText } from '.';
import VideoConfMessageAction from './VideoConfMessageAction';
import VideoConfMessageActions from './VideoConfMessageActions';
import VideoConfMessageButton from './VideoConfMessageButton';
import VideoConfMessageContent from './VideoConfMessageContent';
import VideoConfMessageFooter from './VideoConfMessageFooter';
import VideoConfMessageFooterText from './VideoConfMessageFooterText';
import VideoConfMessageSkeleton from './VideoConfMessageSkeleton';
import VideoConfMessageUserStack from './VideoConfMessageUserStack';
export default {
component: VideoConfMessage,
decorators: [
(Story) => (
<Box>
<MessageDivider>May, 24, 2020</MessageDivider>
<Message className='customclass'>
<MessageLeftContainer>
<Avatar alt='' url={avatarUrl} size='x36' />
</MessageLeftContainer>
<MessageContainer>
<MessageHeader>
<MessageName>Haylie George</MessageName>
<MessageUsername>@haylie.george</MessageUsername>
<MessageRole>Admin</MessageRole>
<MessageRole>User</MessageRole>
<MessageRole>Owner</MessageRole>
<MessageTimestamp>12:00 PM</MessageTimestamp>
</MessageHeader>
<MessageBody>
<Story />
</MessageBody>
</MessageContainer>
</Message>
</Box>
),
],
} satisfies Meta<typeof VideoConfMessage>;
const avatarUrl =
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z';
const fakeUsers = Array.from({ length: 10 }).map((_, i) => ({
username: `user${i}`,
ts: new Date().toISOString(),
name: `User ${i}`,
_id: `id${i}`,
avatarETag: '',
}));
export const CallingDM: StoryObj<typeof VideoConfMessage> = {
render: () => (
<VideoConfMessage>
<VideoConfMessageRow>
<VideoConfMessageContent>
<VideoConfMessageIcon variant='incoming' />
<VideoConfMessageText>Calling...</VideoConfMessageText>
</VideoConfMessageContent>
<VideoConfMessageActions>
<VideoConfMessageAction aria-label='info' icon='info' />
</VideoConfMessageActions>
</VideoConfMessageRow>
<VideoConfMessageFooter>
<VideoConfMessageButton primary>Join</VideoConfMessageButton>
<VideoConfMessageFooterText>Waiting for answer</VideoConfMessageFooterText>
</VideoConfMessageFooter>
</VideoConfMessage>
),
};
export const CallEndedDM: StoryObj<typeof VideoConfMessage> = {
render: () => (
<VideoConfMessage>
<VideoConfMessageRow>
<VideoConfMessageContent>
<VideoConfMessageIcon />
<VideoConfMessageText>Call ended</VideoConfMessageText>
</VideoConfMessageContent>
<VideoConfMessageActions>
<VideoConfMessageAction aria-label='info' icon='info' />
</VideoConfMessageActions>
</VideoConfMessageRow>
<VideoConfMessageFooter>
<VideoConfMessageButton>Call Back</VideoConfMessageButton>
<VideoConfMessageFooterText>Call was not answered</VideoConfMessageFooterText>
</VideoConfMessageFooter>
</VideoConfMessage>
),
};
export const CallOngoing: StoryObj<typeof VideoConfMessage> = {
render: () => (
<VideoConfMessage>
<VideoConfMessageRow>
<VideoConfMessageContent>
<VideoConfMessageIcon variant='outgoing' />
<VideoConfMessageText>Call ongoing</VideoConfMessageText>
</VideoConfMessageContent>
<VideoConfMessageActions>
<VideoConfMessageAction aria-label='info' icon='info' />
</VideoConfMessageActions>
</VideoConfMessageRow>
<VideoConfMessageFooter>
<VideoConfMessageButton primary>Join</VideoConfMessageButton>
<VideoConfMessageUserStack users={fakeUsers} />
<VideoConfMessageFooterText>joined</VideoConfMessageFooterText>
</VideoConfMessageFooter>
</VideoConfMessage>
),
};
export const CallEnded: StoryObj<typeof VideoConfMessage> = {
render: () => (
<VideoConfMessage>
<VideoConfMessageRow>
<VideoConfMessageContent>
<VideoConfMessageIcon />
<VideoConfMessageText>Call ended</VideoConfMessageText>
</VideoConfMessageContent>
<VideoConfMessageActions>
<VideoConfMessageAction aria-label='info' icon='info' />
</VideoConfMessageActions>
</VideoConfMessageRow>
<VideoConfMessageFooter>
<VideoConfMessageUserStack users={fakeUsers} />
<VideoConfMessageFooterText>joined</VideoConfMessageFooterText>
</VideoConfMessageFooter>
</VideoConfMessage>
),
};
export const Loading: StoryObj<typeof VideoConfMessage> = {
render: () => <VideoConfMessageSkeleton />,
};
export const NoAvatars: StoryObj<typeof VideoConfMessage> = {
render: () => (
<VideoConfMessage>
<VideoConfMessageRow>
<VideoConfMessageContent>
<VideoConfMessageIcon variant='outgoing' />
<VideoConfMessageText>Call ongoing</VideoConfMessageText>
</VideoConfMessageContent>
<VideoConfMessageActions>
<VideoConfMessageAction aria-label='info' icon='info' />
</VideoConfMessageActions>
</VideoConfMessageRow>
<VideoConfMessageFooter>
<VideoConfMessageButton primary>Join</VideoConfMessageButton>
<VideoConfMessageUserStack users={fakeUsers} />
<VideoConfMessageFooterText>{fakeUsers.length} joined</VideoConfMessageFooterText>
</VideoConfMessageFooter>
</VideoConfMessage>
),
};

View File

@@ -0,0 +1,20 @@
import { Box } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
export type VideoConfMessageProps = Omit<AllHTMLAttributes<HTMLDivElement>, 'is'>;
const VideoConfMessage = (props: VideoConfMessageProps) => (
<Box
marginBlockStart={4}
color='default'
maxWidth='345px'
backgroundColor='surface-light'
borderWidth={1}
borderColor='extra-light'
borderRadius='x4'
rcx-videoconf-message-block
{...props}
/>
);
export default VideoConfMessage;

View File

@@ -0,0 +1,7 @@
import { IconButton } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const VideoConfMessageAction = ({ icon = 'info', ...props }: ComponentProps<typeof IconButton>) => (
<IconButton {...props} icon={icon} small />
);
export default VideoConfMessageAction;

View File

@@ -0,0 +1,10 @@
import { ButtonGroup } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const VideoConfMessageActions = ({ children, ...props }: ComponentProps<typeof ButtonGroup>) => (
<ButtonGroup {...props} align='end'>
{children}
</ButtonGroup>
);
export default VideoConfMessageActions;

View File

@@ -0,0 +1,9 @@
import { Button } from '@rocket.chat/fuselage';
import type { ButtonHTMLAttributes } from 'react';
export type VideoConfMessageButtonProps = { primary?: boolean } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'is'>;
const VideoConfMessageButton = ({ primary, ...props }: VideoConfMessageButtonProps) => (
<Button {...props} marginInline={4} size='small' variant={primary ? 'primary' : undefined} />
);
export default VideoConfMessageButton;

View File

@@ -0,0 +1,8 @@
import { Box } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
export type VideoConfMessageContentProps = Omit<AllHTMLAttributes<HTMLDivElement>, 'is'>;
const VideoConfMessageContent = (props: VideoConfMessageContentProps) => <Box display='flex' alignItems='center' {...props} />;
export default VideoConfMessageContent;

View File

@@ -0,0 +1,16 @@
import { Box } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
import VideoConfMessageRow from './VideoConfMessageRow';
export type VideoConfMessageFooterProps = Omit<AllHTMLAttributes<HTMLDivElement>, 'is'>;
const VideoConfMessageFooter = ({ children, ...props }: VideoConfMessageFooterProps) => (
<VideoConfMessageRow backgroundColor='tint' {...props}>
<Box marginInline='neg-x4' display='flex' alignItems='center'>
{children}
</Box>
</VideoConfMessageRow>
);
export default VideoConfMessageFooter;

View File

@@ -0,0 +1,11 @@
import { Box } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
export type VideoConfMessageFooterTextProps = Omit<AllHTMLAttributes<HTMLParagraphElement>, 'is'>;
const VideoConfMessageFooterText = ({ children, ...props }: VideoConfMessageFooterTextProps) => (
<Box {...props} is='p' fontScale='micro' marginInline={4}>
{children}
</Box>
);
export default VideoConfMessageFooterText;

View File

@@ -0,0 +1,38 @@
import { Box, Icon } from '@rocket.chat/fuselage';
export type VideoConfMessageIconProps = {
variant?: keyof typeof styles;
};
const styles = {
ended: {
icon: 'phone-off',
color: 'hint',
backgroundColor: 'surface-neutral',
},
incoming: {
icon: 'phone-in',
color: 'status-font-on-info',
backgroundColor: 'status-background-info',
},
outgoing: {
icon: 'phone',
color: 'status-font-on-success',
backgroundColor: 'status-background-success',
},
} as const;
const VideoConfMessageIcon = ({ variant = 'ended' }: VideoConfMessageIconProps) => (
<Box
size='x28'
alignItems='center'
justifyContent='center'
display='flex'
borderRadius='x4'
backgroundColor={styles[variant].backgroundColor}
>
<Icon size='x20' name={styles[variant].icon} color={styles[variant].color} />
</Box>
);
export default VideoConfMessageIcon;

View File

@@ -0,0 +1,10 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
export type VideoConfMessageRowProps = ComponentProps<typeof Box>;
const VideoConfMessageRow = (props: VideoConfMessageRowProps) => (
<Box padding={16} display='flex' justifyContent='space-between' {...props} />
);
export default VideoConfMessageRow;

View File

@@ -0,0 +1,20 @@
import { Skeleton } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
import VideoConfMessage from './VideoConfMessage';
import VideoConfMessageRow from './VideoConfMessageRow';
export type VideoConfMessageSkeletonProps = Omit<AllHTMLAttributes<HTMLDivElement>, 'is'>;
const VideoConfMessageSkeleton = (props: VideoConfMessageSkeletonProps) => (
<VideoConfMessage {...props}>
<VideoConfMessageRow>
<Skeleton width='full' paddingBlock={4} />
</VideoConfMessageRow>
<VideoConfMessageRow backgroundColor='tint'>
<Skeleton width='full' paddingBlock={4} />
</VideoConfMessageRow>
</VideoConfMessage>
);
export default VideoConfMessageSkeleton;

View File

@@ -0,0 +1,8 @@
import { Box } from '@rocket.chat/fuselage';
import type { AllHTMLAttributes } from 'react';
export type VideoConfMessageTextProps = AllHTMLAttributes<HTMLParagraphElement>;
const VideoConfMessageText = (props: VideoConfMessageTextProps) => <Box {...props} is='p' fontScale='c2' marginInlineStart={8} />;
export default VideoConfMessageText;

View File

@@ -0,0 +1,39 @@
import type { IVideoConferenceUser, Serialized } from '@rocket.chat/core-typings';
import { getUserDisplayName } from '@rocket.chat/core-typings';
import { Avatar, Box, Icon } from '@rocket.chat/fuselage';
import { useSetting, useUserAvatarPath, useUserPreference } from '@rocket.chat/ui-contexts';
import { memo } from 'react';
const MAX_USERS = 3;
export type VideoConfMessageUserStackProps = {
users: Serialized<IVideoConferenceUser>[];
};
const VideoConfMessageUserStack = ({ users }: VideoConfMessageUserStackProps) => {
const displayAvatars = useUserPreference<boolean>('displayAvatars');
const showRealName = useSetting('UI_Use_Real_Name', false);
const getUserAvatarPath = useUserAvatarPath();
return (
<Box marginInline={4}>
{displayAvatars && (
<Box display='flex' alignItems='center' marginInline='neg-x2'>
{users.slice(0, MAX_USERS).map(({ name, username }, index) => (
<Box marginInline={2} key={index}>
<Avatar
size='x28'
alt={username || ''}
title={getUserDisplayName(name, username, showRealName)}
url={getUserAvatarPath(username)}
/>
</Box>
))}
</Box>
)}
{!displayAvatars && <Icon size='x20' name='user' />}
</Box>
);
};
export default memo(VideoConfMessageUserStack);

View File

@@ -0,0 +1,993 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`renders CallEnded without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full"
>
<div
class="rcx-message-divider"
role="separator"
>
<div
class="rcx-message-divider__bar"
/>
<div
class="rcx-message-divider__wrapper"
>
May, 24, 2020
</div>
<div
class="rcx-message-divider__bar"
/>
</div>
<div
class="rcx-message customclass"
>
<div
class="rcx-box rcx-box--full rcx-message-container rcx-message-container--left"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z"
/>
</figure>
</div>
<div
class="rcx-box rcx-box--full rcx-message-container"
>
<div
class="rcx-box rcx-box--full rcx-message-header"
>
<div
class="rcx-box rcx-box--full rcx-message-header__wrapper"
>
<span
class="rcx-box rcx-box--full rcx-message-header__name"
>
Haylie George
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__username"
>
@haylie.george
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Admin
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
User
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Owner
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__time"
>
12:00 PM
</span>
</div>
</div>
<div
class="rcx-message-body"
>
<div
class="rcx-box rcx-box--full rcx-videoconf-message-block rcx-css-170vvmf"
>
<div
class="rcx-box rcx-box--full rcx-css-1s5wk2a"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<div
class="rcx-box rcx-box--full rcx-css-1gl6eu2"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-phone-off rcx-icon rcx-css-568yc1"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-hmgb4y"
>
Call ended
</p>
</div>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
>
<button
aria-label="info"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-info rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1aqp1io"
>
<div
class="rcx-box rcx-box--full rcx-css-1gj3xb"
>
<div
class="rcx-box rcx-box--full rcx-css-1eogw2f"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-user rcx-icon rcx-css-4pvxx3"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-weo5d9"
>
joined
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
exports[`renders CallEndedDM without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full"
>
<div
class="rcx-message-divider"
role="separator"
>
<div
class="rcx-message-divider__bar"
/>
<div
class="rcx-message-divider__wrapper"
>
May, 24, 2020
</div>
<div
class="rcx-message-divider__bar"
/>
</div>
<div
class="rcx-message customclass"
>
<div
class="rcx-box rcx-box--full rcx-message-container rcx-message-container--left"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z"
/>
</figure>
</div>
<div
class="rcx-box rcx-box--full rcx-message-container"
>
<div
class="rcx-box rcx-box--full rcx-message-header"
>
<div
class="rcx-box rcx-box--full rcx-message-header__wrapper"
>
<span
class="rcx-box rcx-box--full rcx-message-header__name"
>
Haylie George
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__username"
>
@haylie.george
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Admin
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
User
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Owner
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__time"
>
12:00 PM
</span>
</div>
</div>
<div
class="rcx-message-body"
>
<div
class="rcx-box rcx-box--full rcx-videoconf-message-block rcx-css-170vvmf"
>
<div
class="rcx-box rcx-box--full rcx-css-1s5wk2a"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<div
class="rcx-box rcx-box--full rcx-css-1gl6eu2"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-phone-off rcx-icon rcx-css-568yc1"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-hmgb4y"
>
Call ended
</p>
</div>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
>
<button
aria-label="info"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-info rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1aqp1io"
>
<div
class="rcx-box rcx-box--full rcx-css-1gj3xb"
>
<button
class="rcx-box rcx-box--full rcx-button--small rcx-button rcx-css-1eogw2f"
type="button"
>
<span
class="rcx-button--content"
>
Call Back
</span>
</button>
<p
class="rcx-box rcx-box--full rcx-css-weo5d9"
>
Call was not answered
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
exports[`renders CallOngoing without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full"
>
<div
class="rcx-message-divider"
role="separator"
>
<div
class="rcx-message-divider__bar"
/>
<div
class="rcx-message-divider__wrapper"
>
May, 24, 2020
</div>
<div
class="rcx-message-divider__bar"
/>
</div>
<div
class="rcx-message customclass"
>
<div
class="rcx-box rcx-box--full rcx-message-container rcx-message-container--left"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z"
/>
</figure>
</div>
<div
class="rcx-box rcx-box--full rcx-message-container"
>
<div
class="rcx-box rcx-box--full rcx-message-header"
>
<div
class="rcx-box rcx-box--full rcx-message-header__wrapper"
>
<span
class="rcx-box rcx-box--full rcx-message-header__name"
>
Haylie George
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__username"
>
@haylie.george
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Admin
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
User
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Owner
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__time"
>
12:00 PM
</span>
</div>
</div>
<div
class="rcx-message-body"
>
<div
class="rcx-box rcx-box--full rcx-videoconf-message-block rcx-css-170vvmf"
>
<div
class="rcx-box rcx-box--full rcx-css-1s5wk2a"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<div
class="rcx-box rcx-box--full rcx-css-ehyzbd"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-phone rcx-icon rcx-css-1inbyew"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-hmgb4y"
>
Call ongoing
</p>
</div>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
>
<button
aria-label="info"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-info rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1aqp1io"
>
<div
class="rcx-box rcx-box--full rcx-css-1gj3xb"
>
<button
class="rcx-box rcx-box--full rcx-button--small rcx-button--primary rcx-button rcx-css-1eogw2f"
type="button"
>
<span
class="rcx-button--content"
>
Join
</span>
</button>
<div
class="rcx-box rcx-box--full rcx-css-1eogw2f"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-user rcx-icon rcx-css-4pvxx3"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-weo5d9"
>
joined
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
exports[`renders CallingDM without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full"
>
<div
class="rcx-message-divider"
role="separator"
>
<div
class="rcx-message-divider__bar"
/>
<div
class="rcx-message-divider__wrapper"
>
May, 24, 2020
</div>
<div
class="rcx-message-divider__bar"
/>
</div>
<div
class="rcx-message customclass"
>
<div
class="rcx-box rcx-box--full rcx-message-container rcx-message-container--left"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z"
/>
</figure>
</div>
<div
class="rcx-box rcx-box--full rcx-message-container"
>
<div
class="rcx-box rcx-box--full rcx-message-header"
>
<div
class="rcx-box rcx-box--full rcx-message-header__wrapper"
>
<span
class="rcx-box rcx-box--full rcx-message-header__name"
>
Haylie George
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__username"
>
@haylie.george
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Admin
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
User
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Owner
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__time"
>
12:00 PM
</span>
</div>
</div>
<div
class="rcx-message-body"
>
<div
class="rcx-box rcx-box--full rcx-videoconf-message-block rcx-css-170vvmf"
>
<div
class="rcx-box rcx-box--full rcx-css-1s5wk2a"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<div
class="rcx-box rcx-box--full rcx-css-1xqpvsb"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-phone-in rcx-icon rcx-css-2mjsik"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-hmgb4y"
>
Calling...
</p>
</div>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
>
<button
aria-label="info"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-info rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1aqp1io"
>
<div
class="rcx-box rcx-box--full rcx-css-1gj3xb"
>
<button
class="rcx-box rcx-box--full rcx-button--small rcx-button--primary rcx-button rcx-css-1eogw2f"
type="button"
>
<span
class="rcx-button--content"
>
Join
</span>
</button>
<p
class="rcx-box rcx-box--full rcx-css-weo5d9"
>
Waiting for answer
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
exports[`renders Loading without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full"
>
<div
class="rcx-message-divider"
role="separator"
>
<div
class="rcx-message-divider__bar"
/>
<div
class="rcx-message-divider__wrapper"
>
May, 24, 2020
</div>
<div
class="rcx-message-divider__bar"
/>
</div>
<div
class="rcx-message customclass"
>
<div
class="rcx-box rcx-box--full rcx-message-container rcx-message-container--left"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z"
/>
</figure>
</div>
<div
class="rcx-box rcx-box--full rcx-message-container"
>
<div
class="rcx-box rcx-box--full rcx-message-header"
>
<div
class="rcx-box rcx-box--full rcx-message-header__wrapper"
>
<span
class="rcx-box rcx-box--full rcx-message-header__name"
>
Haylie George
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__username"
>
@haylie.george
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Admin
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
User
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Owner
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__time"
>
12:00 PM
</span>
</div>
</div>
<div
class="rcx-message-body"
>
<div
class="rcx-box rcx-box--full rcx-videoconf-message-block rcx-css-170vvmf"
>
<div
class="rcx-box rcx-box--full rcx-css-1s5wk2a"
>
<span
class="rcx-skeleton rcx-skeleton--text rcx-css-el6yni"
/>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1aqp1io"
>
<span
class="rcx-skeleton rcx-skeleton--text rcx-css-el6yni"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;
exports[`renders NoAvatars without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full"
>
<div
class="rcx-message-divider"
role="separator"
>
<div
class="rcx-message-divider__bar"
/>
<div
class="rcx-message-divider__wrapper"
>
May, 24, 2020
</div>
<div
class="rcx-message-divider__bar"
/>
</div>
<div
class="rcx-message customclass"
>
<div
class="rcx-box rcx-box--full rcx-message-container rcx-message-container--left"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z"
/>
</figure>
</div>
<div
class="rcx-box rcx-box--full rcx-message-container"
>
<div
class="rcx-box rcx-box--full rcx-message-header"
>
<div
class="rcx-box rcx-box--full rcx-message-header__wrapper"
>
<span
class="rcx-box rcx-box--full rcx-message-header__name"
>
Haylie George
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__username"
>
@haylie.george
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Admin
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
User
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-tag rcx-box rcx-box--full rcx-message-header__role"
>
<span
class="rcx-tag__inner"
>
Owner
</span>
</span>
<span
class="rcx-box rcx-box--full rcx-message-header__time"
>
12:00 PM
</span>
</div>
</div>
<div
class="rcx-message-body"
>
<div
class="rcx-box rcx-box--full rcx-videoconf-message-block rcx-css-170vvmf"
>
<div
class="rcx-box rcx-box--full rcx-css-1s5wk2a"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<div
class="rcx-box rcx-box--full rcx-css-ehyzbd"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-phone rcx-icon rcx-css-1inbyew"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-hmgb4y"
>
Call ongoing
</p>
</div>
<div
class="rcx-button-group rcx-button-group--align-end"
role="group"
>
<button
aria-label="info"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-info rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1aqp1io"
>
<div
class="rcx-box rcx-box--full rcx-css-1gj3xb"
>
<button
class="rcx-box rcx-box--full rcx-button--small rcx-button--primary rcx-button rcx-css-1eogw2f"
type="button"
>
<span
class="rcx-button--content"
>
Join
</span>
</button>
<div
class="rcx-box rcx-box--full rcx-css-1eogw2f"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-user rcx-icon rcx-css-4pvxx3"
>
</i>
</div>
<p
class="rcx-box rcx-box--full rcx-css-weo5d9"
>
10
joined
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;

View File

@@ -0,0 +1,27 @@
import VideoConfMessage from './VideoConfMessage';
import VideoConfMessageAction from './VideoConfMessageAction';
import VideoConfMessageActions from './VideoConfMessageActions';
import VideoConfMessageButton from './VideoConfMessageButton';
import VideoConfMessageContent from './VideoConfMessageContent';
import VideoConfMessageFooter from './VideoConfMessageFooter';
import VideoConfMessageFooterText from './VideoConfMessageFooterText';
import VideoConfMessageIcon from './VideoConfMessageIcon';
import VideoConfMessageRow from './VideoConfMessageRow';
import VideoConfMessageSkeleton from './VideoConfMessageSkeleton';
import VideoConfMessageText from './VideoConfMessageText';
import VideoConfMessageUserStack from './VideoConfMessageUserStack';
export {
VideoConfMessage,
VideoConfMessageIcon,
VideoConfMessageRow,
VideoConfMessageContent,
VideoConfMessageButton,
VideoConfMessageText,
VideoConfMessageSkeleton,
VideoConfMessageFooter,
VideoConfMessageAction,
VideoConfMessageActions,
VideoConfMessageUserStack,
VideoConfMessageFooterText,
};

View File

@@ -0,0 +1,19 @@
import { composeStories } from '@storybook/react';
import { render } from '@testing-library/react';
import { axe } from 'jest-axe';
import * as stories from './VideoConfPopup.stories';
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]);
test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const { baseElement } = render(<Story />);
expect(baseElement).toMatchSnapshot();
});
test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});

View File

@@ -0,0 +1,60 @@
import { Avatar, Icon } from '@rocket.chat/fuselage';
import type { Meta, StoryObj } from '@storybook/react';
import { action } from 'storybook/actions';
import VideoConfButton from '../VideoConfButton';
import VideoConfController from '../VideoConfController';
import VideoConfPopup from './VideoConfPopup';
import VideoConfPopupBackdrop from './VideoConfPopupBackdrop';
import VideoConfPopupContent from './VideoConfPopupContent';
import VideoConfPopupControllers from './VideoConfPopupControllers';
import VideoConfPopupFooter from './VideoConfPopupFooter';
import VideoConfPopupFooterButtons from './VideoConfPopupFooterButtons';
import VideoConfPopupHeader from './VideoConfPopupHeader';
import VideoConfPopupInfo from './VideoConfPopupInfo';
import VideoConfPopupSkeleton from './VideoConfPopupSkeleton';
import VideoConfPopupTitle from './VideoConfPopupTitle';
export default {
component: VideoConfPopup,
decorators: [
(Story) => (
<VideoConfPopupBackdrop>
<Story />
</VideoConfPopupBackdrop>
),
],
} satisfies Meta<typeof VideoConfPopup>;
const avatarUrl =
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z';
export const StartCall: StoryObj<typeof VideoConfPopup> = {
render: () => (
<VideoConfPopup aria-label='Start Call'>
<VideoConfPopupHeader>
<VideoConfPopupTitle text='Start Call' />
<VideoConfPopupControllers>
<VideoConfController active={false} aria-label='video' icon='video' onClick={action('click')} />
<VideoConfController active={true} aria-label='mic' icon='mic' onClick={action('click')} />
</VideoConfPopupControllers>
</VideoConfPopupHeader>
<VideoConfPopupContent>
<VideoConfPopupInfo avatar={<Avatar alt='' url={avatarUrl} />} icon={<Icon name='hash' />}>
Room Name
</VideoConfPopupInfo>
</VideoConfPopupContent>
<VideoConfPopupFooter>
<VideoConfPopupFooterButtons>
<VideoConfButton primary onClick={action('click')}>
Start Call
</VideoConfButton>
</VideoConfPopupFooterButtons>
</VideoConfPopupFooter>
</VideoConfPopup>
),
};
export const Skeleton: StoryObj<typeof VideoConfPopup> = {
render: () => <VideoConfPopupSkeleton />,
};

View File

@@ -0,0 +1,34 @@
import { Box, Palette } from '@rocket.chat/fuselage';
import styled from '@rocket.chat/styled';
import { forwardRef } from 'react';
import type { ReactNode, HTMLAttributes } from 'react';
export const VideoConfPopupContainer = styled('div', ({ position: _position, ...props }: { position?: number }) => props)`
width: 100%;
position: relative;
box-shadow:
0px 0px 1px 0px ${Palette.shadow['shadow-elevation-2x'].toString()},
0px 0px 12px 0px ${Palette.shadow['shadow-elevation-2y'].toString()};
background-color: ${Palette.surface['surface-light'].toString()};
border: 1px solid ${Palette.stroke['stroke-extra-light'].toString()};
top: ${(p): string => (p.position ? `${p.position}px` : '0')};
left: -${(p): string => (p.position ? `${p.position}px` : '0')};
border-radius: 0.25rem;
`;
export type VideoConfPopupProps = {
children: ReactNode;
position?: number;
} & HTMLAttributes<HTMLElement>;
const VideoConfPopup = forwardRef<HTMLDivElement, VideoConfPopupProps>(function VideoConfPopup({ children, position, ...props }, ref) {
return (
<VideoConfPopupContainer role='dialog' ref={ref} position={position} {...props}>
<Box padding={24} maxWidth='x276' color='default'>
{children}
</Box>
</VideoConfPopupContainer>
);
});
export default VideoConfPopup;

View File

@@ -0,0 +1,25 @@
import { css } from '@rocket.chat/css-in-js';
import { Box } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
const backdropStyle = css`
position: fixed;
top: 0;
min-width: 276px;
[dir='ltr'] & {
right: 0;
}
[dir='rtl'] & {
left: 0;
}
`;
export type VideoConfPopupBackdropProps = { children: ReactNode };
const VideoConfPopupBackdrop = ({ children }: VideoConfPopupBackdropProps) => (
<Box margin={40} zIndex={99} className={backdropStyle}>
{children}
</Box>
);
export default VideoConfPopupBackdrop;

View File

@@ -0,0 +1,12 @@
import { Box } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type VideoConfPopupContentProps = { children: ReactNode };
const VideoConfPopupContent = ({ children }: VideoConfPopupContentProps) => (
<Box display='flex' flexDirection='column' marginBlockStart={8}>
{children}
</Box>
);
export default VideoConfPopupContent;

View File

@@ -0,0 +1,6 @@
import { ButtonGroup } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const VideoConfPopupControllers = (props: ComponentProps<typeof ButtonGroup>) => <ButtonGroup {...props} />;
export default VideoConfPopupControllers;

View File

@@ -0,0 +1,6 @@
import { Box } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const VideoConfPopupFooter = (props: ComponentProps<typeof Box>) => <Box marginBlockStart={28} {...props} />;
export default VideoConfPopupFooter;

View File

@@ -0,0 +1,6 @@
import { ButtonGroup } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
const VideoConfPopupFooterButtons = (props: ComponentProps<typeof ButtonGroup>) => <ButtonGroup stretch {...props} />;
export default VideoConfPopupFooterButtons;

View File

@@ -0,0 +1,12 @@
import { Box } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type VideoConfPopupHeaderProps = { children: ReactNode };
const VideoConfPopupHeader = ({ children }: VideoConfPopupHeaderProps) => (
<Box display='flex' minHeight='x28' justifyContent='space-between'>
{children}
</Box>
);
export default VideoConfPopupHeader;

View File

@@ -0,0 +1,12 @@
import { Box } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type VideoConfPopupIconProps = { children: ReactNode };
const VideoConfPopupIcon = ({ children }: VideoConfPopupIconProps) => (
<Box display='flex' flexShrink={0} alignItems='center' size='x18' overflow='hidden' justifyContent='center'>
{children}
</Box>
);
export default VideoConfPopupIcon;

View File

@@ -0,0 +1,24 @@
import { Box } from '@rocket.chat/fuselage';
import type { ReactNode } from 'react';
export type VideoConfPopupInfoProps = {
avatar?: ReactNode;
icon?: ReactNode;
children: ReactNode;
};
const VideoConfPopupInfo = ({ avatar, icon, children }: VideoConfPopupInfoProps) => (
<Box display='flex' alignItems='center'>
{avatar}
{(icon || children) && (
<Box display='flex' flexGrow={1} flexShrink={1} flexBasis='0%' alignItems='center' marginInlineStart={8} withTruncatedText>
{icon}
<Box marginInlineStart={8} fontScale='h4' withTruncatedText>
{children}
</Box>
</Box>
)}
</Box>
);
export default VideoConfPopupInfo;

View File

@@ -0,0 +1,28 @@
import { Box, Skeleton } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
import VideoConfPopup from './VideoConfPopup';
import VideoConfPopupContent from './VideoConfPopupContent';
import VideoConfPopupFooter from './VideoConfPopupFooter';
import VideoConfPopupHeader from './VideoConfPopupHeader';
export type VideoConfPopupSkeletonProps = Omit<ComponentProps<typeof VideoConfPopup>, 'children'>;
const VideoConfPopupSkeleton = (props: VideoConfPopupSkeletonProps) => (
<VideoConfPopup aria-label='Loading' {...props}>
<VideoConfPopupHeader>
<Skeleton width='100%' />
</VideoConfPopupHeader>
<VideoConfPopupContent>
<Box display='flex' alignItems='center'>
<Skeleton variant='rect' height='x36' width='x36' />
<Skeleton marginInlineStart={8} variant='rect' height='x24' width='x120' />
</Box>
</VideoConfPopupContent>
<VideoConfPopupFooter>
<Skeleton variant='rect' height='x40' />
</VideoConfPopupFooter>
</VideoConfPopup>
);
export default VideoConfPopupSkeleton;

View File

@@ -0,0 +1,15 @@
import { Box, Throbber } from '@rocket.chat/fuselage';
export type VideoConfPopupTitleProps = {
text: string;
counter?: boolean;
};
const VideoConfPopupTitle = ({ text, counter = false }: VideoConfPopupTitleProps) => (
<Box display='flex' alignItems='center'>
<Box fontScale='p2b'>{text}</Box>
{counter && <Throbber size='x8' marginInlineStart={4} />}
</Box>
);
export default VideoConfPopupTitle;

View File

@@ -0,0 +1,166 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`renders Skeleton without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full rcx-css-11d1wak rcx-css-veoob9"
>
<div
aria-label="Loading"
class="rcx-css-4z2gcx"
role="dialog"
>
<div
class="rcx-box rcx-box--full rcx-css-th6bpk"
>
<div
class="rcx-box rcx-box--full rcx-css-135wgx"
>
<span
class="rcx-skeleton rcx-skeleton--text rcx-css-1qcz93u"
/>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1rtfgno"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<span
class="rcx-skeleton rcx-skeleton--rect rcx-css-1etla80"
/>
<span
class="rcx-skeleton rcx-skeleton--rect rcx-css-dzx8a8"
/>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-4ka9e7"
>
<span
class="rcx-skeleton rcx-skeleton--rect rcx-css-15ie4i5"
/>
</div>
</div>
</div>
</div>
</div>
</body>
`;
exports[`renders StartCall without crashing 1`] = `
<body>
<div>
<div
class="rcx-box rcx-box--full rcx-css-11d1wak rcx-css-veoob9"
>
<div
aria-label="Start Call"
class="rcx-css-4z2gcx"
role="dialog"
>
<div
class="rcx-box rcx-box--full rcx-css-th6bpk"
>
<div
class="rcx-box rcx-box--full rcx-css-135wgx"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<div
class="rcx-box rcx-box--full rcx-css-1rfo3kt"
>
Start Call
</div>
</div>
<div
class="rcx-button-group rcx-button-group--align-start"
role="group"
>
<button
aria-label="video"
aria-live="assertive"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-video rcx-icon rcx-css-4pvxx3"
>
❲
</i>
</button>
<button
aria-label="mic"
aria-live="assertive"
class="rcx-box rcx-box--full rcx-button--small-square rcx-button--icon-secondary-info rcx-button--square rcx-button--icon rcx-button"
type="button"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-mic rcx-icon rcx-css-4pvxx3"
>
</i>
</button>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1rtfgno"
>
<div
class="rcx-box rcx-box--full rcx-css-127j9mz"
>
<figure
class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x36"
>
<img
alt=""
class="rcx-avatar__element rcx-avatar__element--x36"
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z"
/>
</figure>
<div
class="rcx-box rcx-box--full rcx-css-1iqy4x3"
>
<i
aria-hidden="true"
class="rcx-box rcx-box--full rcx-icon--name-hash rcx-icon"
>
</i>
<div
class="rcx-box rcx-box--full rcx-css-rnm2z7"
>
Room Name
</div>
</div>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-4ka9e7"
>
<div
class="rcx-button-group rcx-button-group--stretch rcx-button-group--align-start"
role="group"
>
<button
class="rcx-box rcx-box--full rcx-button--primary rcx-button rcx-css-1qcz93u"
type="button"
>
<span
class="rcx-button--content"
>
Start Call
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
`;

View File

@@ -0,0 +1,25 @@
import VideoConfPopup from './VideoConfPopup';
import VideoConfPopupBackdrop from './VideoConfPopupBackdrop';
import VideoConfPopupContent from './VideoConfPopupContent';
import VideoConfPopupControllers from './VideoConfPopupControllers';
import VideoConfPopupFooter from './VideoConfPopupFooter';
import VideoConfPopupFooterButtons from './VideoConfPopupFooterButtons';
import VideoConfPopupHeader from './VideoConfPopupHeader';
import VideoConfPopupIcon from './VideoConfPopupIcon';
import VideoConfPopupInfo from './VideoConfPopupInfo';
import VideoConfPopupSkeleton from './VideoConfPopupSkeleton';
import VideoConfPopupTitle from './VideoConfPopupTitle';
export {
VideoConfPopup,
VideoConfPopupHeader,
VideoConfPopupIcon,
VideoConfPopupInfo,
VideoConfPopupContent,
VideoConfPopupTitle,
VideoConfPopupBackdrop,
VideoConfPopupControllers,
VideoConfPopupFooter,
VideoConfPopupFooterButtons,
VideoConfPopupSkeleton,
};

View File

@@ -0,0 +1,2 @@
export * from './useVideoConfControllers';
export * from './useVideoConfContext';

View File

@@ -0,0 +1,58 @@
import { useContext, useMemo, useSyncExternalStore } from 'react';
import { VideoConfContext } from '../VideoConfContext';
const useVideoConfContext = () => {
const context = useContext(VideoConfContext);
if (!context) {
throw new Error('Must be running in VideoConf Context');
}
return context;
};
export const useVideoConfDispatchOutgoing = () => useVideoConfContext().dispatchOutgoing;
export const useVideoConfDismissOutgoing = () => useVideoConfContext().dismissOutgoing;
export const useVideoConfStartCall = () => useVideoConfContext().startCall;
export const useVideoConfAcceptCall = () => useVideoConfContext().acceptCall;
export const useVideoConfJoinCall = () => useVideoConfContext().joinCall;
export const useVideoConfDismissCall = () => useVideoConfContext().dismissCall;
export const useVideoConfAbortCall = () => useVideoConfContext().abortCall;
export const useVideoConfRejectIncomingCall = () => useVideoConfContext().rejectIncomingCall;
export const useVideoConfSetPreferences = () => useVideoConfContext().setPreferences;
export const useVideoConfLoadCapabilities = () => useVideoConfContext().loadCapabilities;
export const useVideoConfIncomingCalls = () => {
const { queryIncomingCalls } = useVideoConfContext();
const [subscribe, getSnapshot] = useMemo(() => queryIncomingCalls(), [queryIncomingCalls]);
return useSyncExternalStore(subscribe, getSnapshot);
};
export const useVideoConfIsRinging = () => {
const { queryRinging } = useVideoConfContext();
const [subscribe, getSnapshot] = useMemo(() => queryRinging(), [queryRinging]);
return useSyncExternalStore(subscribe, getSnapshot);
};
export const useVideoConfIsCalling = () => {
const { queryCalling } = useVideoConfContext();
const [subscribe, getSnapshot] = useMemo(() => queryCalling(), [queryCalling]);
return useSyncExternalStore(subscribe, getSnapshot);
};
export const useVideoConfCapabilities = () => {
const { queryCapabilities } = useVideoConfContext();
const [subscribe, getSnapshot] = useMemo(() => queryCapabilities(), [queryCapabilities]);
return useSyncExternalStore(subscribe, getSnapshot);
};
export const useVideoConfPreferences = () => {
const { queryPreferences } = useVideoConfContext();
const [subscribe, getSnapshot] = useMemo(() => queryPreferences(), [queryPreferences]);
return useSyncExternalStore(subscribe, getSnapshot);
};

View File

@@ -0,0 +1,22 @@
import { useCallback, useState } from 'react';
type controllersConfigProps = {
mic?: boolean;
cam?: boolean;
};
export const useVideoConfControllers = (
initialPreferences: controllersConfigProps = { mic: true, cam: false },
): { controllersConfig: controllersConfigProps; handleToggleMic: () => void; handleToggleCam: () => void } => {
const [controllersConfig, setControllersConfig] = useState(initialPreferences);
const handleToggleMic = useCallback(() => setControllersConfig((prevState) => ({ ...prevState, mic: !prevState.mic })), []);
const handleToggleCam = useCallback(() => setControllersConfig((prevState) => ({ ...prevState, cam: !prevState.cam })), []);
return {
controllersConfig,
handleToggleMic,
handleToggleCam,
};
};

View File

@@ -0,0 +1,8 @@
import VideoConfButton from './VideoConfButton';
import VideoConfController from './VideoConfController';
export * from './VideoConfContext';
export * from './VideoConfMessage';
export * from './VideoConfPopup';
export * from './hooks';
export { VideoConfButton, VideoConfController };

View File

@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["./src/**/*"],
"exclude": ["./src/**/*.stories.tsx", "./src/**/*.spec.ts", "./src/**/*.spec.tsx", "./.storybook/**/*", "./jest.config.ts"]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@rocket.chat/tsconfig/client.json",
"compilerOptions": {
"rootDirs": ["./src", "./"],
"outDir": "./dist",
},
"include": ["./src/**/*", "./jest.config.ts", "./.storybook/**/*"],
}