refactor(livechat): Solve lint warnings (#39338)

Co-authored-by: Copilot <copilot@github.com>
playwright-1.59.1
Tasso Evangelista 2 weeks ago committed by GitHub
parent 5aff6f2436
commit f9560cb167
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 97
      eslint.config.mjs
  2. 2
      packages/ddp-client/src/livechat/LivechatClientImpl.ts
  3. 0
      packages/livechat/src/components/Avatar/Avatar.stories.tsx
  4. 0
      packages/livechat/src/components/ButtonGroup/ButtonGroup.stories.tsx
  5. 2
      packages/livechat/src/components/Composer/index.tsx
  6. 0
      packages/livechat/src/components/FilesDropTarget/FilesDropTarget.stories.tsx
  7. 2
      packages/livechat/src/components/FilesDropTarget/index.tsx
  8. 19
      packages/livechat/src/components/Footer/CharCounter.tsx
  9. 24
      packages/livechat/src/components/Footer/Footer.stories.tsx
  10. 17
      packages/livechat/src/components/Footer/Footer.tsx
  11. 17
      packages/livechat/src/components/Footer/FooterContent.tsx
  12. 16
      packages/livechat/src/components/Footer/FooterOptions.tsx
  13. 23
      packages/livechat/src/components/Footer/OptionsTrigger.tsx
  14. 25
      packages/livechat/src/components/Footer/PoweredBy.tsx
  15. 6
      packages/livechat/src/components/Footer/index.ts
  16. 64
      packages/livechat/src/components/Footer/index.tsx
  17. 184
      packages/livechat/src/components/Header/Header.stories.tsx
  18. 39
      packages/livechat/src/components/Header/Header.tsx
  19. 18
      packages/livechat/src/components/Header/HeaderAction.tsx
  20. 17
      packages/livechat/src/components/Header/HeaderActions.tsx
  21. 17
      packages/livechat/src/components/Header/HeaderContent.tsx
  22. 17
      packages/livechat/src/components/Header/HeaderCustomField.tsx
  23. 17
      packages/livechat/src/components/Header/HeaderPicture.tsx
  24. 17
      packages/livechat/src/components/Header/HeaderPost.tsx
  25. 28
      packages/livechat/src/components/Header/HeaderSubTitle.tsx
  26. 17
      packages/livechat/src/components/Header/HeaderTitle.tsx
  27. 9
      packages/livechat/src/components/Header/index.ts
  28. 111
      packages/livechat/src/components/Header/index.tsx
  29. 42
      packages/livechat/src/components/Menu/Group.stories.tsx
  30. 66
      packages/livechat/src/components/Menu/Item.stories.tsx
  31. 26
      packages/livechat/src/components/Menu/Menu.stories.tsx
  32. 18
      packages/livechat/src/components/Menu/Menu.tsx
  33. 17
      packages/livechat/src/components/Menu/MenuGroup.tsx
  34. 21
      packages/livechat/src/components/Menu/MenuItem.tsx
  35. 29
      packages/livechat/src/components/Menu/MenuPopover.tsx
  36. 34
      packages/livechat/src/components/Menu/PopoverMenu.stories.tsx
  37. 72
      packages/livechat/src/components/Menu/PopoverMenuWrapper.tsx
  38. 4
      packages/livechat/src/components/Menu/index.ts
  39. 1
      packages/livechat/src/components/Messages/AudioAttachment/index.tsx
  40. 2
      packages/livechat/src/components/Messages/ImageAttachment/index.tsx
  41. 0
      packages/livechat/src/components/Messages/Message/Message.stories.tsx
  42. 7
      packages/livechat/src/components/Messages/MessageAvatars/index.tsx
  43. 8
      packages/livechat/src/components/Messages/MessageList/index.js
  44. 6
      packages/livechat/src/components/Messages/TypingDots/index.tsx
  45. 1
      packages/livechat/src/components/Messages/VideoAttachment/index.tsx
  46. 30
      packages/livechat/src/components/Modal/AlertModal.tsx
  47. 35
      packages/livechat/src/components/Modal/ConfirmationModal.tsx
  48. 12
      packages/livechat/src/components/Modal/MessageModal.tsx
  49. 8
      packages/livechat/src/components/Modal/Modal.stories.tsx
  50. 73
      packages/livechat/src/components/Modal/Modal.tsx
  51. 33
      packages/livechat/src/components/Modal/ModalManager.tsx
  52. 95
      packages/livechat/src/components/Modal/component.js
  53. 2
      packages/livechat/src/components/Modal/index.js
  54. 5
      packages/livechat/src/components/Modal/index.ts
  55. 30
      packages/livechat/src/components/Modal/manager.js
  56. 3
      packages/livechat/src/components/Popover/Popover.stories.tsx
  57. 130
      packages/livechat/src/components/Popover/PopoverContainer.tsx
  58. 7
      packages/livechat/src/components/Popover/PopoverContext.ts
  59. 20
      packages/livechat/src/components/Popover/PopoverOverlay.tsx
  60. 17
      packages/livechat/src/components/Popover/PopoverTrigger.tsx
  61. 90
      packages/livechat/src/components/Popover/index.js
  62. 2
      packages/livechat/src/components/Popover/index.ts
  63. 2
      packages/livechat/src/components/Screen/ChatButton.tsx
  64. 48
      packages/livechat/src/components/Screen/CssVar.tsx
  65. 50
      packages/livechat/src/components/Screen/Footer.stories.tsx
  66. 68
      packages/livechat/src/components/Screen/Header.tsx
  67. 79
      packages/livechat/src/components/Screen/Screen.tsx
  68. 17
      packages/livechat/src/components/Screen/ScreenContent.tsx
  69. 28
      packages/livechat/src/components/Screen/ScreenFooter.tsx
  70. 39
      packages/livechat/src/components/Screen/ScreenProvider.tsx
  71. 3
      packages/livechat/src/components/Screen/index.ts
  72. 48
      packages/livechat/src/components/Screen/stories.tsx
  73. 1
      packages/livechat/src/components/Sound/index.js
  74. 23
      packages/livechat/src/components/Tooltip/Tooltip.stories.tsx
  75. 61
      packages/livechat/src/components/Tooltip/Tooltip.tsx
  76. 71
      packages/livechat/src/components/Tooltip/TooltipContainer.tsx
  77. 35
      packages/livechat/src/components/Tooltip/TooltipContext.ts
  78. 30
      packages/livechat/src/components/Tooltip/TooltipTrigger.tsx
  79. 127
      packages/livechat/src/components/Tooltip/index.js
  80. 3
      packages/livechat/src/components/Tooltip/index.ts
  81. 35
      packages/livechat/src/components/uiKit/message/OverflowElement/OverflowElement.tsx
  82. 41
      packages/livechat/src/components/uiKit/message/OverflowElement/OverflowOption.tsx
  83. 33
      packages/livechat/src/components/uiKit/message/OverflowElement/OverflowTrigger.tsx
  84. 1
      packages/livechat/src/components/uiKit/message/OverflowElement/index.ts
  85. 94
      packages/livechat/src/components/uiKit/message/OverflowElement/index.tsx
  86. 4
      packages/livechat/src/helpers/visibility.ts
  87. 20
      packages/livechat/src/lib/customFields.ts
  88. 2
      packages/livechat/src/lib/hooks.ts
  89. 43
      packages/livechat/src/lib/locale.js
  90. 46
      packages/livechat/src/lib/locale.ts
  91. 4
      packages/livechat/src/lib/triggerActions.ts
  92. 8
      packages/livechat/src/lib/triggerUtils.ts
  93. 25
      packages/livechat/src/lib/userPresence.ts
  94. 28
      packages/livechat/src/routes/Chat/component.js
  95. 11
      packages/livechat/src/routes/Chat/connector.tsx
  96. 18
      packages/livechat/src/routes/Chat/container.js
  97. 8
      packages/livechat/src/routes/ChatFinished/component.tsx
  98. 9
      packages/livechat/src/routes/ChatFinished/container.tsx
  99. 8
      packages/livechat/src/routes/GDPRAgreement/component.tsx
  100. 11
      packages/livechat/src/routes/GDPRAgreement/container.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,13 +1,7 @@
import { fileURLToPath } from 'node:url';
import rocketChatConfig from '@rocket.chat/eslint-config';
import youDontNeedLodashUnderscorePlugin from 'eslint-plugin-you-dont-need-lodash-underscore';
import globals from 'globals';
function getAbsolutePath(path) {
return fileURLToPath(new URL(path, import.meta.url));
}
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
...rocketChatConfig,
@ -392,98 +386,8 @@ export default [
react: {
pragma: 'h',
pragmaFrag: 'Fragment',
version: 'detect',
},
},
rules: {
'import/order': [
'error',
{
'newlines-between': 'always',
'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']],
'alphabetize': {
order: 'asc',
},
},
],
'jsx-a11y/alt-text': 'off',
'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/media-has-caption': 'off',
'jsx-a11y/no-static-element-interactions': 'off',
'jsx-quotes': ['error', 'prefer-single'],
'react/jsx-curly-brace-presence': 'off',
'react/display-name': [
'warn',
{
ignoreTranspilerName: false,
},
],
'react/jsx-fragments': ['error', 'syntax'],
'react/jsx-key': 'off',
'react/jsx-no-bind': [
'warn',
{
ignoreRefs: true,
allowFunctions: true,
allowArrowFunctions: true,
},
],
'react/jsx-no-comment-textnodes': 'error',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-target-blank': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-tag-spacing': [
'error',
{
beforeSelfClosing: 'always',
},
],
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/no-children-prop': 'error',
'react/no-danger': 'warn',
'react/no-deprecated': 'error',
'react/no-did-mount-set-state': 'error',
'react/no-did-update-set-state': 'error',
'react/no-direct-mutation-state': 'warn',
'react/no-find-dom-node': 'error',
'react/no-is-mounted': 'error',
'react/no-multi-comp': 'off',
'react/no-string-refs': 'error',
'react/no-unknown-property': ['error', { ignore: ['class'] }],
'react/prefer-es6-class': 'error',
'react/prefer-stateless-function': 'warn',
'react/require-render-return': 'error',
'react/self-closing-comp': 'error',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
'no-sequences': 'off',
},
},
{
files: ['packages/livechat/**/*.@(ts|tsx)'],
rules: {
'@typescript-eslint/naming-convention': [
'error',
{ selector: 'variableLike', format: ['camelCase'], leadingUnderscore: 'allow' },
{
selector: ['variable'],
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
leadingUnderscore: 'allowSingleOrDouble',
},
{
selector: ['function'],
format: ['camelCase', 'PascalCase'],
leadingUnderscore: 'allowSingleOrDouble',
},
{
selector: 'parameter',
format: ['camelCase'],
modifiers: ['unused'],
leadingUnderscore: 'require',
},
],
},
},
{
ignores: ['packages/node-poplib/**', 'packages/storybook-config/*.@(d.ts|js)', 'scripts/**', '.github/**', '.houston/**'],
@ -500,7 +404,6 @@ export default [
'apps/meteor/ee/server/**/*.ts',
'apps/meteor/server/**/*.ts',
'packages/fuselage-ui-kit/**/*.@(ts|tsx)',
'packages/livechat/**/*.@(ts|tsx)',
'packages/ui-client/**/*.@(ts|tsx)',
'packages/ui-voip/**/*.@(ts|tsx)',
'packages/web-ui-registration/**/*.@(ts|tsx)',

@ -40,7 +40,7 @@ declare module '../DDPSDK' {
}
export class LivechatClientImpl extends DDPSDK implements LivechatStream, LivechatEndpoints {
private token?: string;
public token?: string;
public readonly credentials: { token?: string } = { token: this.token };

@ -270,7 +270,9 @@ export class Composer extends Component<ComposerProps, ComposerState> {
{pre}
<div
ref={this.handleRef}
role='textbox'
contentEditable
tabIndex={0}
data-placeholder={placeholder}
data-qa='livechat-composer'
onInput={this.handleInput(onChange)}

@ -93,7 +93,7 @@ export const FilesDropTarget = ({
filteredFiles = filteredFiles.slice(0, 1);
}
filteredFiles.length && onUpload(filteredFiles);
if (filteredFiles.length) onUpload(filteredFiles);
};
return (

@ -0,0 +1,19 @@
import type { CSSProperties } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type CharCounterProps = {
className?: string;
style?: CSSProperties;
textLength: number;
limitTextLength: number;
};
const CharCounter = ({ className, style = {}, textLength, limitTextLength }: CharCounterProps) => (
<span className={createClassName(styles, 'footer__remainder', { highlight: textLength === limitTextLength }, [className])} style={style}>
{textLength} / {limitTextLength}
</span>
);
export default CharCounter;

@ -2,14 +2,16 @@ import { action } from '@storybook/addon-actions';
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import { Footer, FooterContent, FooterOptions, PoweredBy } from '.';
import ChangeIcon from '../../icons/change.svg';
import FinishIcon from '../../icons/finish.svg';
import RemoveIcon from '../../icons/remove.svg';
import { Composer } from '../Composer';
import Menu from '../Menu';
import { MenuGroup, MenuItem } from '../Menu';
import { PopoverContainer } from '../Popover';
import Footer from './Footer';
import FooterContent from './FooterContent';
import FooterOptions from './FooterOptions';
import PoweredBy from './PoweredBy';
import '../../i18next';
export default {
@ -46,17 +48,17 @@ export const WithComposerAndOptions: StoryFn<ComponentProps<typeof Footer>> = (a
</FooterContent>
<FooterContent>
<FooterOptions>
<Menu.Group>
<Menu.Item onClick={action('change-department')} icon={ChangeIcon}>
<MenuGroup>
<MenuItem onClick={action('change-department')} icon={ChangeIcon}>
Change department
</Menu.Item>
<Menu.Item onClick={action('remove-user-data')} icon={RemoveIcon}>
</MenuItem>
<MenuItem onClick={action('remove-user-data')} icon={RemoveIcon}>
Forget/Remove my personal data
</Menu.Item>
<Menu.Item danger onClick={action('finish-chat')} icon={FinishIcon}>
</MenuItem>
<MenuItem danger onClick={action('finish-chat')} icon={FinishIcon}>
Finish this chat
</Menu.Item>
</Menu.Group>
</MenuItem>
</MenuGroup>
</FooterOptions>
<PoweredBy />
</FooterContent>

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type FooterProps = {
children: ComponentChildren;
className?: string;
};
const Footer = ({ children, className, ...props }: FooterProps) => (
<footer className={createClassName(styles, 'footer', {}, [className])} {...props}>
{children}
</footer>
);
export default Footer;

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type FooterContentProps = {
children: ComponentChildren;
className?: string;
};
const FooterContent = ({ children, className, ...props }: FooterContentProps) => (
<div className={createClassName(styles, 'footer__content', {}, [className])} {...props}>
{children}
</div>
);
export default FooterContent;

@ -0,0 +1,16 @@
import type { ComponentChildren } from 'preact';
import { MenuPopover } from '../Menu';
import OptionsTrigger from './OptionsTrigger';
export type FooterOptionsProps = {
children: ComponentChildren;
};
const FooterOptions = ({ children }: FooterOptionsProps) => (
<MenuPopover trigger={OptionsTrigger} overlayed>
{children}
</MenuPopover>
);
export default FooterOptions;

@ -0,0 +1,23 @@
import { type MouseEventHandler } from 'preact/compat';
import { useTranslation } from 'react-i18next';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
const handleMouseUp: MouseEventHandler<HTMLButtonElement> = ({ target }) => (target as HTMLButtonElement | null)?.blur();
type OptionsTriggerProps = {
pop: () => void;
};
const OptionsTrigger = ({ pop }: OptionsTriggerProps) => {
const { t } = useTranslation();
return (
<button className={createClassName(styles, 'footer__options')} onClick={pop} onMouseUp={handleMouseUp}>
{t('options')}
</button>
);
};
export default OptionsTrigger;

@ -0,0 +1,25 @@
import { RocketChatLogo } from '@rocket.chat/logo';
import { useTranslation } from 'react-i18next';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type PoweredByProps = {
className?: string;
};
export const PoweredBy = ({ className, ...props }: PoweredByProps) => {
const { t } = useTranslation();
return (
<h3 data-qa='livechat-watermark' className={createClassName(styles, 'powered-by', {}, [className])} {...props}>
{t('powered_by_rocket_chat').split('Rocket.Chat')[0]}
<a className={createClassName(styles, 'powered-by__logo')} href='https://rocket.chat' target='_blank' rel='noopener noreferrer'>
<RocketChatLogo />
</a>
{t('powered_by_rocket_chat').split('Rocket.Chat')[1]}
</h3>
);
};
export default PoweredBy;

@ -0,0 +1,6 @@
export { default as Footer } from './Footer';
export { default as FooterContent } from './FooterContent';
export { default as PoweredBy } from './PoweredBy';
export { default as OptionsTrigger } from './OptionsTrigger';
export { default as FooterOptions } from './FooterOptions';
export { default as CharCounter } from './CharCounter';

@ -1,64 +0,0 @@
import { RocketChatLogo } from '@rocket.chat/logo';
import type { ComponentChildren } from 'preact';
import type { JSXInternal } from 'preact/src/jsx';
import { useTranslation, withTranslation } from 'react-i18next';
import { createClassName } from '../../helpers/createClassName';
import { PopoverMenu } from '../Menu';
import styles from './styles.scss';
export const Footer = ({ children, className, ...props }: { children: ComponentChildren; className?: string }) => (
<footer className={createClassName(styles, 'footer', {}, [className])} {...props}>
{children}
</footer>
);
export const FooterContent = ({ children, className, ...props }: { children: ComponentChildren; className?: string }) => (
<div className={createClassName(styles, 'footer__content', {}, [className])} {...props}>
{children}
</div>
);
export const PoweredBy = withTranslation()(({ className, t, ...props }: { className?: string; t: (translationKey: string) => string }) => (
<h3 data-qa='livechat-watermark' className={createClassName(styles, 'powered-by', {}, [className])} {...props}>
{t('powered_by_rocket_chat').split('Rocket.Chat')[0]}
<a className={createClassName(styles, 'powered-by__logo')} href='https://rocket.chat' target='_blank' rel='noopener noreferrer'>
<RocketChatLogo />
</a>
{t('powered_by_rocket_chat').split('Rocket.Chat')[1]}
</h3>
));
const handleMouseUp: JSXInternal.MouseEventHandler<HTMLButtonElement> = ({ target }: { target: EventTarget | null }) =>
(target as HTMLButtonElement)?.blur();
const OptionsTrigger = ({ pop }: { pop: () => void }) => {
const { t } = useTranslation();
return (
<button className={createClassName(styles, 'footer__options')} onClick={pop} onMouseUp={handleMouseUp}>
{t('options')}
</button>
);
};
export const FooterOptions = ({ children }: { children: ComponentChildren }) => (
<PopoverMenu trigger={OptionsTrigger} overlayed>
{children}
</PopoverMenu>
);
export const CharCounter = ({
className,
style = {},
textLength,
limitTextLength,
}: {
className?: string;
style: JSXInternal.CSSProperties;
textLength: number;
limitTextLength: number;
}) => (
<span className={createClassName(styles, 'footer__remainder', { highlight: textLength === limitTextLength }, [className])} style={style}>
{textLength} / {limitTextLength}
</span>
);

@ -2,7 +2,17 @@ import { action } from '@storybook/addon-actions';
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import Header, { Picture, Content, SubTitle, Title, Actions, Action, Post, CustomField } from '.';
import {
Header,
HeaderPicture,
HeaderContent,
HeaderSubTitle,
HeaderTitle,
HeaderActions,
HeaderAction,
HeaderPost,
HeaderCustomField,
} from '.';
import { gazzoAvatar } from '../../../.storybook/helpers';
import Arrow from '../../icons/arrowDown.svg';
import Bell from '../../icons/bell.svg';
@ -27,49 +37,49 @@ export default {
export const WithTextContent: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args}>
<Content>Need Help?</Content>
<HeaderContent>Need Help?</HeaderContent>
</Header>
);
WithTextContent.storyName = 'with text content';
export const WithLongTextContent: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args}>
<Content>{'Need Help? '.repeat(100)}</Content>
<HeaderContent>{'Need Help? '.repeat(100)}</HeaderContent>
</Header>
);
WithLongTextContent.storyName = 'with long text content';
export const WithTitleAndSubtitle: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args}>
<Content>
<Title>Rocket.Chat</Title>
<SubTitle>Livechat</SubTitle>
</Content>
<HeaderContent>
<HeaderTitle>Rocket.Chat</HeaderTitle>
<HeaderSubTitle>Livechat</HeaderSubTitle>
</HeaderContent>
</Header>
);
WithTitleAndSubtitle.storyName = 'with title and subtitle';
export const WithPicture: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args}>
<Picture>
<HeaderPicture>
<Bell width={20} height={20} />
</Picture>
<Content>Notification settings</Content>
</HeaderPicture>
<HeaderContent>Notification settings</HeaderContent>
</Header>
);
WithPicture.storyName = 'with picture';
export const WithActions: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args}>
<Content>Chat finished</Content>
<Actions>
<Action onClick={action('notifications')}>
<HeaderContent>Chat finished</HeaderContent>
<HeaderActions>
<HeaderAction onClick={action('notifications')}>
<Bell width={20} height={20} />
</Action>
<Action onClick={action('minimize')}>
</HeaderAction>
<HeaderAction onClick={action('minimize')}>
<Arrow width={20} height={20} />
</Action>
</Actions>
</HeaderAction>
</HeaderActions>
</Header>
);
WithActions.storyName = 'with actions';
@ -78,74 +88,74 @@ export const WithMultiplesAlerts: StoryFn<ComponentProps<typeof Header>> = (args
<Header
{...args}
post={
<Post>
<HeaderPost>
<Alert success>Success</Alert>
<Alert warning>Warning</Alert>
<Alert error>Error</Alert>
<Alert error color='#175CC4'>
Custom color
</Alert>
</Post>
</HeaderPost>
}
>
<Content>Chat finished</Content>
<Actions>
<Action onClick={action('notifications')}>
<HeaderContent>Chat finished</HeaderContent>
<HeaderActions>
<HeaderAction onClick={action('notifications')}>
<Bell width={20} height={20} />
</Action>
<Action onClick={action('minimize')}>
</HeaderAction>
<HeaderAction onClick={action('minimize')}>
<Arrow width={20} height={20} />
</Action>
</Actions>
</HeaderAction>
</HeaderActions>
</Header>
);
WithMultiplesAlerts.storyName = 'with multiples alerts';
export const ForUserChat: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args}>
<Picture>
<HeaderPicture>
<Avatar src={gazzoAvatar} status='busy' />
</Picture>
<Content>
<Title>@guilherme.gazzo</Title>
<SubTitle>guilherme.gazzo@rocket.chat</SubTitle>
</Content>
<Actions>
<Action onClick={action('notifications')}>
</HeaderPicture>
<HeaderContent>
<HeaderTitle>@guilherme.gazzo</HeaderTitle>
<HeaderSubTitle>guilherme.gazzo@rocket.chat</HeaderSubTitle>
</HeaderContent>
<HeaderActions>
<HeaderAction onClick={action('notifications')}>
<Bell width={20} height={20} />
</Action>
<Action onClick={action('minimize')}>
</HeaderAction>
<HeaderAction onClick={action('minimize')}>
<Arrow width={20} height={20} />
</Action>
<Action onClick={action('fullscreen')}>
</HeaderAction>
<HeaderAction onClick={action('fullscreen')}>
<NewWindow width={20} height={20} />
</Action>
</Actions>
</HeaderAction>
</HeaderActions>
</Header>
);
ForUserChat.storyName = 'for user chat';
export const WithCustomField: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args} large>
<Picture>
<HeaderPicture>
<Avatar src={gazzoAvatar} large status='away' />
</Picture>
<Content>
<Title>Guilherme Gazzo</Title>
<SubTitle>guilherme.gazzo@rocket.chat</SubTitle>
<CustomField>+ 55 42423 24242</CustomField>
</Content>
<Actions>
<Action onClick={action('notifications')}>
</HeaderPicture>
<HeaderContent>
<HeaderTitle>Guilherme Gazzo</HeaderTitle>
<HeaderSubTitle>guilherme.gazzo@rocket.chat</HeaderSubTitle>
<HeaderCustomField>+ 55 42423 24242</HeaderCustomField>
</HeaderContent>
<HeaderActions>
<HeaderAction onClick={action('notifications')}>
<Bell width={20} height={20} />
</Action>
<Action onClick={action('minimize')}>
</HeaderAction>
<HeaderAction onClick={action('minimize')}>
<Arrow width={20} height={20} />
</Action>
<Action onClick={action('fullscreen')}>
</HeaderAction>
<HeaderAction onClick={action('fullscreen')}>
<NewWindow width={20} height={20} />
</Action>
</Actions>
</HeaderAction>
</HeaderActions>
</Header>
);
WithCustomField.storyName = 'with custom field';
@ -157,31 +167,31 @@ export const WithCustomFieldAndAlert: StoryFn<ComponentProps<typeof Header>> = (
<Header
{...args}
post={
<Post>
<HeaderPost>
<Alert success>Success</Alert>
<Alert warning>Warning</Alert>
</Post>
</HeaderPost>
}
>
<Picture>
<HeaderPicture>
<Avatar src={gazzoAvatar} large status='online' />
</Picture>
<Content>
<Title>Guilherme Gazzo</Title>
<SubTitle>guilherme.gazzo@rocket.chat</SubTitle>
<CustomField>+ 55 42423 24242</CustomField>
</Content>
<Actions>
<Action onClick={action('notifications')}>
</HeaderPicture>
<HeaderContent>
<HeaderTitle>Guilherme Gazzo</HeaderTitle>
<HeaderSubTitle>guilherme.gazzo@rocket.chat</HeaderSubTitle>
<HeaderCustomField>+ 55 42423 24242</HeaderCustomField>
</HeaderContent>
<HeaderActions>
<HeaderAction onClick={action('notifications')}>
<Bell width={20} height={20} />
</Action>
<Action onClick={action('minimize')}>
</HeaderAction>
<HeaderAction onClick={action('minimize')}>
<Arrow width={20} height={20} />
</Action>
<Action onClick={action('fullscreen')}>
</HeaderAction>
<HeaderAction onClick={action('fullscreen')}>
<NewWindow width={20} height={20} />
</Action>
</Actions>
</HeaderAction>
</HeaderActions>
</Header>
);
WithCustomFieldAndAlert.storyName = 'with custom field and alert';
@ -191,25 +201,25 @@ WithCustomFieldAndAlert.args = {
export const WithTheme: StoryFn<ComponentProps<typeof Header>> = (args) => (
<Header {...args}>
<Picture>
<HeaderPicture>
<Avatar src={gazzoAvatar} large status='away' />
</Picture>
<Content>
<Title>Guilherme Gazzo</Title>
<SubTitle>guilherme.gazzo@rocket.chat</SubTitle>
<CustomField>+ 55 42423 24242</CustomField>
</Content>
<Actions>
<Action onClick={action('notifications')}>
</HeaderPicture>
<HeaderContent>
<HeaderTitle>Guilherme Gazzo</HeaderTitle>
<HeaderSubTitle>guilherme.gazzo@rocket.chat</HeaderSubTitle>
<HeaderCustomField>+ 55 42423 24242</HeaderCustomField>
</HeaderContent>
<HeaderActions>
<HeaderAction onClick={action('notifications')}>
<Bell width={20} height={20} />
</Action>
<Action onClick={action('minimize')}>
</HeaderAction>
<HeaderAction onClick={action('minimize')}>
<Arrow width={20} height={20} />
</Action>
<Action onClick={action('fullscreen')}>
</HeaderAction>
<HeaderAction onClick={action('fullscreen')}>
<NewWindow width={20} height={20} />
</Action>
</Actions>
</HeaderAction>
</HeaderActions>
</Header>
);
WithTheme.storyName = 'with theme';

@ -0,0 +1,39 @@
import type { ComponentChildren, Ref } from 'preact';
import type { MouseEventHandler } from 'preact/compat';
import type { JSXInternal } from 'preact/src/jsx';
import styles from './styles.scss';
import type { Theme } from '../../Theme';
import { createClassName } from '../../helpers/createClassName';
type HeaderProps = {
children?: ComponentChildren;
theme?: Partial<Theme>;
className?: string;
post?: ComponentChildren;
large?: boolean;
style?: JSXInternal.CSSProperties;
ref?: Ref<HTMLElement>;
onClick?: MouseEventHandler<HTMLElement>;
};
const Header = ({
children,
theme: { color: backgroundColor, fontColor: color } = {},
className,
post,
large,
style,
...props
}: HeaderProps) => (
<header
className={createClassName(styles, 'header', { large }, [className])}
style={style || backgroundColor || color ? { ...(style || {}), backgroundColor, color } : undefined}
{...props}
>
{children}
{post}
</header>
);
export default Header;

@ -0,0 +1,18 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderActionProps = {
children?: ComponentChildren;
className?: string;
onClick?: () => void;
};
const HeaderAction = ({ children, className = undefined, ...props }: HeaderActionProps) => (
<button className={createClassName(styles, 'header__action', {}, [className])} {...props}>
{children}
</button>
);
export default HeaderAction;

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderActionsProps = {
children?: ComponentChildren;
className?: string;
};
const HeaderActions = ({ children, className = undefined, ...props }: HeaderActionsProps) => (
<nav className={createClassName(styles, 'header__actions', {}, [className])} {...props}>
{children}
</nav>
);
export default HeaderActions;

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderContentProps = {
children?: ComponentChildren;
className?: string;
};
const HeaderContent = ({ children, className = undefined, ...props }: HeaderContentProps) => (
<div className={createClassName(styles, 'header__content', {}, [className])} {...props}>
{children}
</div>
);
export default HeaderContent;

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderCustomFieldProps = {
children?: ComponentChildren;
className?: string;
};
const HeaderCustomField = ({ children, className = undefined, ...props }: HeaderCustomFieldProps) => (
<div className={createClassName(styles, 'header__custom-field', {}, [className])} {...props}>
{children}
</div>
);
export default HeaderCustomField;

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderPictureProps = {
children?: ComponentChildren;
className?: string;
};
export const HeaderPicture = ({ children, className = undefined, ...props }: HeaderPictureProps) => (
<div className={createClassName(styles, 'header__picture', {}, [className])} {...props}>
{children}
</div>
);
export default HeaderPicture;

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderPostProps = {
children?: ComponentChildren;
className?: string;
};
export const HeaderPost = ({ children, className = undefined, ...props }: HeaderPostProps) => (
<div className={createClassName(styles, 'header__post', {}, [className])} {...props}>
{children}
</div>
);
export default HeaderPost;

@ -0,0 +1,28 @@
import type { ComponentChildren } from 'preact';
import { toChildArray } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderSubTitleProps = {
children?: ComponentChildren;
className?: string;
};
const HeaderSubTitle = ({ children, className = undefined, ...props }: HeaderSubTitleProps) => (
<div
className={createClassName(
styles,
'header__subtitle',
{
children: toChildArray(children).length > 0,
},
[className],
)}
{...props}
>
{children}
</div>
);
export default HeaderSubTitle;

@ -0,0 +1,17 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type HeaderTitleProps = {
children?: ComponentChildren;
className?: string;
};
const HeaderTitle = ({ children, className = undefined, ...props }: HeaderTitleProps) => (
<div className={createClassName(styles, 'header__title', {}, [className])} data-qa='header-title' {...props}>
{children}
</div>
);
export default HeaderTitle;

@ -0,0 +1,9 @@
export { default as Header } from './Header';
export { default as HeaderPicture } from './HeaderPicture';
export { default as HeaderContent } from './HeaderContent';
export { default as HeaderTitle } from './HeaderTitle';
export { default as HeaderSubTitle } from './HeaderSubTitle';
export { default as HeaderActions } from './HeaderActions';
export { default as HeaderAction } from './HeaderAction';
export { default as HeaderPost } from './HeaderPost';
export { default as HeaderCustomField } from './HeaderCustomField';

@ -1,111 +0,0 @@
import type { ComponentChildren, Ref } from 'preact';
import { toChildArray } from 'preact';
import type { JSXInternal } from 'preact/src/jsx';
import styles from './styles.scss';
import type { Theme } from '../../Theme';
import { createClassName } from '../../helpers/createClassName';
type HeaderProps = {
children?: ComponentChildren;
theme?: Partial<Theme>;
className?: string;
post?: ComponentChildren;
large?: boolean;
style?: JSXInternal.CSSProperties;
ref?: Ref<HTMLElement>;
onClick?: JSXInternal.DOMAttributes<HTMLElement>['onClick'];
};
type HeaderComponentProps = {
children?: ComponentChildren;
className?: string;
};
export const Header = ({
children,
theme: { color: backgroundColor, fontColor: color } = {},
className,
post,
large,
style,
...props
}: HeaderProps) => (
<header
className={createClassName(styles, 'header', { large }, [className])}
style={style || backgroundColor || color ? { ...(style || {}), backgroundColor, color } : undefined}
{...props}
>
{children}
{post}
</header>
);
export const Picture = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
<div className={createClassName(styles, 'header__picture', {}, [className])} {...props}>
{children}
</div>
);
export const Content = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
<div className={createClassName(styles, 'header__content', {}, [className])} {...props}>
{children}
</div>
);
export const Title = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
<div className={createClassName(styles, 'header__title', {}, [className])} data-qa='header-title' {...props}>
{children}
</div>
);
export const SubTitle = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
<div
className={createClassName(
styles,
'header__subtitle',
{
children: toChildArray(children).length > 0,
},
[className],
)}
{...props}
>
{children}
</div>
);
export const Actions = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
<nav className={createClassName(styles, 'header__actions', {}, [className])} {...props}>
{children}
</nav>
);
export const Action = ({ children, className = undefined, ...props }: HeaderComponentProps & { onClick?: () => void }) => (
<button className={createClassName(styles, 'header__action', {}, [className])} {...props}>
{children}
</button>
);
export const Post = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
<div className={createClassName(styles, 'header__post', {}, [className])} {...props}>
{children}
</div>
);
export const CustomField = ({ children, className = undefined, ...props }: HeaderComponentProps) => (
<div className={createClassName(styles, 'header__custom-field', {}, [className])} {...props}>
{children}
</div>
);
Header.Picture = Picture;
Header.Content = Content;
Header.Title = Title;
Header.SubTitle = SubTitle;
Header.Actions = Actions;
Header.Action = Action;
Header.Post = Post;
Header.CustomField = CustomField;
export default Header;

@ -1,45 +1,45 @@
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import Menu, { Group, Item } from '.';
import { Menu, MenuGroup, MenuItem } from '.';
export default {
title: 'Components/Menu/Group',
component: Group,
component: MenuGroup,
parameters: {
layout: 'centered',
},
} satisfies Meta<ComponentProps<typeof Group>>;
} satisfies Meta<ComponentProps<typeof MenuGroup>>;
export const Single: StoryFn<ComponentProps<typeof Group>> = (args) => (
export const Single: StoryFn<ComponentProps<typeof MenuGroup>> = (args) => (
<Menu>
<Group {...args}>
<Item>A menu item</Item>
<Item>Another menu item</Item>
</Group>
<MenuGroup {...args}>
<MenuItem>A menu item</MenuItem>
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
);
Single.storyName = 'single';
export const Multiple: StoryFn<ComponentProps<typeof Group>> = (args) => (
export const Multiple: StoryFn<ComponentProps<typeof MenuGroup>> = (args) => (
<Menu>
<Group {...args}>
<Item>A menu item</Item>
<Item>Another menu item</Item>
</Group>
<Group>
<Item>Report</Item>
</Group>
<MenuGroup {...args}>
<MenuItem>A menu item</MenuItem>
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
<MenuGroup>
<MenuItem>Report</MenuItem>
</MenuGroup>
</Menu>
);
Multiple.storyName = 'multiple';
export const WithTitle: StoryFn<ComponentProps<typeof Group>> = (args) => (
export const WithTitle: StoryFn<ComponentProps<typeof MenuGroup>> = (args) => (
<Menu>
<Group {...args}>
<Item>A menu item</Item>
<Item>Another menu item</Item>
</Group>
<MenuGroup {...args}>
<MenuItem>A menu item</MenuItem>
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
);
WithTitle.storyName = 'with title';

@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions';
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import Menu, { Group, Item } from '.';
import { Menu, MenuGroup, MenuItem } from '.';
import BellIcon from '../../icons/bell.svg';
import ChangeIcon from '../../icons/change.svg';
import CloseIcon from '../../icons/close.svg';
@ -10,7 +10,7 @@ import FinishIcon from '../../icons/finish.svg';
export default {
title: 'Components/Menu/Item',
component: Item,
component: MenuItem,
args: {
children: 'A menu item',
onClick: action('clicked'),
@ -18,24 +18,24 @@ export default {
parameters: {
layout: 'centered',
},
} satisfies Meta<ComponentProps<typeof Item>>;
} satisfies Meta<ComponentProps<typeof MenuItem>>;
export const Simple: StoryFn<ComponentProps<typeof Item>> = (args) => (
export const Simple: StoryFn<ComponentProps<typeof MenuItem>> = (args) => (
<Menu>
<Group>
<Item {...args} />
<Item>Another menu item</Item>
</Group>
<MenuGroup>
<MenuItem {...args} />
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
);
Simple.storyName = 'simple';
export const Primary: StoryFn<ComponentProps<typeof Item>> = (args) => (
export const Primary: StoryFn<ComponentProps<typeof MenuItem>> = (args) => (
<Menu>
<Group>
<Item {...args} />
<Item>Another menu item</Item>
</Group>
<MenuGroup>
<MenuItem {...args} />
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
);
Primary.storyName = 'primary';
@ -43,12 +43,12 @@ Primary.args = {
primary: true,
};
export const Danger: StoryFn<ComponentProps<typeof Item>> = (args) => (
export const Danger: StoryFn<ComponentProps<typeof MenuItem>> = (args) => (
<Menu>
<Group>
<Item {...args} />
<Item>Another menu item</Item>
</Group>
<MenuGroup>
<MenuItem {...args} />
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
);
Danger.storyName = 'danger';
@ -56,12 +56,12 @@ Danger.args = {
danger: true,
};
export const Disabled: StoryFn<ComponentProps<typeof Item>> = (args) => (
export const Disabled: StoryFn<ComponentProps<typeof MenuItem>> = (args) => (
<Menu>
<Group>
<Item {...args} />
<Item>Another menu item</Item>
</Group>
<MenuGroup>
<MenuItem {...args} />
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
);
Disabled.storyName = 'disabled';
@ -69,20 +69,20 @@ Disabled.args = {
disabled: true,
};
export const WithIcon: StoryFn<ComponentProps<typeof Item>> = (args) => (
export const WithIcon: StoryFn<ComponentProps<typeof MenuItem>> = (args) => (
<Menu>
<Group>
<Item {...args} />
<Item primary icon={ChangeIcon} onClick={action('clicked')}>
<MenuGroup>
<MenuItem {...args} />
<MenuItem primary icon={ChangeIcon} onClick={action('clicked')}>
Primary
</Item>
<Item danger icon={FinishIcon} onClick={action('clicked')}>
</MenuItem>
<MenuItem danger icon={FinishIcon} onClick={action('clicked')}>
Danger
</Item>
<Item disabled icon={BellIcon} onClick={action('clicked')}>
</MenuItem>
<MenuItem disabled icon={BellIcon} onClick={action('clicked')}>
Disabled
</Item>
</Group>
</MenuItem>
</MenuGroup>
</Menu>
);
WithIcon.storyName = 'with icon';

@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions';
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import Menu, { Group, Item } from '.';
import { Menu, MenuGroup, MenuItem } from '.';
import { Button } from '../Button';
export default {
@ -29,19 +29,19 @@ Empty.storyName = 'empty';
export const Simple: StoryFn<ComponentProps<typeof Menu>> = (args) => (
<Menu {...args}>
<Group>
<Item onClick={action('clicked')}>A menu item</Item>
<Item>Another menu item</Item>
</Group>
<MenuGroup>
<MenuItem onClick={action('clicked')}>A menu item</MenuItem>
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
);
Simple.storyName = 'simple';
Simple.args = {
children: (
<Group>
<Item onClick={action('clicked')}>A menu item</Item>
<Item>Another menu item</Item>
</Group>
<MenuGroup>
<MenuItem onClick={action('clicked')}>A menu item</MenuItem>
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
),
};
@ -49,10 +49,10 @@ export const Placement: StoryFn<ComponentProps<typeof Menu>> = (args) => (
<div style={{ position: 'relative' }}>
<Button>Button</Button>
<Menu {...args}>
<Group>
<Item onClick={action('clicked')}>A menu item</Item>
<Item>Another menu item</Item>
</Group>
<MenuGroup>
<MenuItem onClick={action('clicked')}>A menu item</MenuItem>
<MenuItem>Another menu item</MenuItem>
</MenuGroup>
</Menu>
</div>
);

@ -0,0 +1,18 @@
import type { HTMLAttributes } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type MenuProps = {
hidden?: boolean;
placement?: string;
ref?: any; // FIXME: remove this
} & Omit<HTMLAttributes<HTMLDivElement>, 'ref'>;
const Menu = ({ children, hidden, placement = '', ...props }: MenuProps) => (
<div className={createClassName(styles, 'menu', { hidden, placement })} {...props}>
{children}
</div>
);
export default Menu;

@ -0,0 +1,17 @@
import type { HTMLAttributes } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type GroupProps = {
title?: string;
} & HTMLAttributes<HTMLDivElement>;
const MenuGroup = ({ children, title = '', ...props }: GroupProps) => (
<div className={createClassName(styles, 'menu__group')} {...props}>
{title && <div className={createClassName(styles, 'menu__group-title')}>{title}</div>}
{children}
</div>
);
export default MenuGroup;

@ -0,0 +1,21 @@
import type { ComponentChildren } from 'preact';
import type { HTMLAttributes } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
type ItemProps = {
primary?: boolean;
danger?: boolean;
disabled?: boolean;
icon?: () => ComponentChildren;
} & HTMLAttributes<HTMLButtonElement>;
const MenuItem = ({ children, primary = false, danger = false, disabled = false, icon = undefined, ...props }: ItemProps) => (
<button className={createClassName(styles, 'menu__item', { primary, danger, disabled })} disabled={disabled} {...props}>
{icon && <div className={createClassName(styles, 'menu__item__icon')}>{icon()}</div>}
{children}
</button>
);
export default MenuItem;

@ -0,0 +1,29 @@
import type { ComponentChildren } from 'preact';
import PopoverMenuWrapper from './PopoverMenuWrapper';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
import { PopoverTrigger } from '../Popover';
type PopoverMenuProps = {
children?: ComponentChildren;
trigger: (contextValue: { pop: () => void }) => ComponentChildren;
overlayed?: boolean;
};
const MenuPopover = ({ children = null, trigger, overlayed }: PopoverMenuProps) => (
<PopoverTrigger
overlayProps={{
className: overlayed ? createClassName(styles, 'popover-menu__overlay') : null,
}}
>
{trigger}
{({ dismiss, triggerBounds, overlayBounds }) => (
<PopoverMenuWrapper dismiss={dismiss} triggerBounds={triggerBounds} overlayBounds={overlayBounds}>
{children}
</PopoverMenuWrapper>
)}
</PopoverTrigger>
);
export default MenuPopover;

@ -1,13 +1,13 @@
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import { Group, Item, PopoverMenu } from '.';
import { MenuGroup, MenuItem, MenuPopover } from '.';
import { Button } from '../Button';
import { PopoverContainer } from '../Popover';
export default {
title: 'Components/Menu/PopoverMenu',
component: PopoverMenu,
component: MenuPopover,
args: {},
decorators: [
(storyFn) => (
@ -19,25 +19,25 @@ export default {
parameters: {
layout: 'fullscreen',
},
} satisfies Meta<ComponentProps<typeof PopoverMenu>>;
} satisfies Meta<ComponentProps<typeof MenuPopover>>;
export const Default: StoryFn<ComponentProps<typeof PopoverMenu>> = (args) => (
<PopoverMenu {...args} trigger={({ pop }) => <Button onClick={pop}>More options...</Button>}>
<Group>
<Item>Reload</Item>
<Item danger>Delete...</Item>
</Group>
</PopoverMenu>
export const Default: StoryFn<ComponentProps<typeof MenuPopover>> = (args) => (
<MenuPopover {...args} trigger={({ pop }) => <Button onClick={pop}>More options...</Button>}>
<MenuGroup>
<MenuItem>Reload</MenuItem>
<MenuItem danger>Delete...</MenuItem>
</MenuGroup>
</MenuPopover>
);
Default.storyName = 'default';
export const WithOverlay: StoryFn<ComponentProps<typeof PopoverMenu>> = (args) => (
<PopoverMenu {...args} trigger={({ pop }) => <Button onClick={pop}>More options...</Button>}>
<Group>
<Item>Reload</Item>
<Item danger>Delete...</Item>
</Group>
</PopoverMenu>
export const WithOverlay: StoryFn<ComponentProps<typeof MenuPopover>> = (args) => (
<MenuPopover {...args} trigger={({ pop }) => <Button onClick={pop}>More options...</Button>}>
<MenuGroup>
<MenuItem>Reload</MenuItem>
<MenuItem danger>Delete...</MenuItem>
</MenuGroup>
</MenuPopover>
);
WithOverlay.storyName = 'with overlay';
WithOverlay.args = {

@ -1,47 +1,9 @@
import { Component, type ComponentChildren } from 'preact';
import type { HTMLAttributes, TargetedEvent } from 'preact/compat';
import type { TargetedEvent } from 'preact/compat';
import { createClassName } from '../../helpers/createClassName';
import { normalizeDOMRect } from '../../helpers/normalizeDOMRect';
import { PopoverTrigger } from '../Popover';
import Menu from './Menu';
import styles from './styles.scss';
type MenuProps = {
hidden?: boolean;
placement?: string;
ref?: any; // FIXME: remove this
} & Omit<HTMLAttributes<HTMLDivElement>, 'ref'>;
export const Menu = ({ children, hidden, placement = '', ...props }: MenuProps) => (
<div className={createClassName(styles, 'menu', { hidden, placement })} {...props}>
{children}
</div>
);
type GroupProps = {
title?: string;
} & HTMLAttributes<HTMLDivElement>;
export const Group = ({ children, title = '', ...props }: GroupProps) => (
<div className={createClassName(styles, 'menu__group')} {...props}>
{title && <div className={createClassName(styles, 'menu__group-title')}>{title}</div>}
{children}
</div>
);
type ItemProps = {
primary?: boolean;
danger?: boolean;
disabled?: boolean;
icon?: () => ComponentChildren;
} & HTMLAttributes<HTMLButtonElement>;
export const Item = ({ children, primary = false, danger = false, disabled = false, icon = undefined, ...props }: ItemProps) => (
<button className={createClassName(styles, 'menu__item', { primary, danger, disabled })} disabled={disabled} {...props}>
{icon && <div className={createClassName(styles, 'menu__item__icon')}>{icon()}</div>}
{children}
</button>
);
import { normalizeDOMRect } from '../../helpers/normalizeDOMRect';
type PopoverMenuWrapperProps = {
children?: ComponentChildren;
@ -96,7 +58,6 @@ class PopoverMenuWrapper extends Component<PopoverMenuWrapperProps, PopoverMenuW
const placement = `${menuWidth < rightSpace ? 'right' : 'left'}-${menuHeight < bottomSpace ? 'bottom' : 'top'}`;
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({
position: { left, right, top, bottom },
placement,
@ -115,29 +76,4 @@ class PopoverMenuWrapper extends Component<PopoverMenuWrapperProps, PopoverMenuW
);
}
type PopoverMenuProps = {
children?: ComponentChildren;
trigger: (contextValue: { pop: () => void }) => void;
overlayed?: boolean;
};
export const PopoverMenu = ({ children = null, trigger, overlayed }: PopoverMenuProps) => (
<PopoverTrigger
overlayProps={{
className: overlayed ? createClassName(styles, 'popover-menu__overlay') : null,
}}
>
{trigger}
{({ dismiss, triggerBounds, overlayBounds }) => (
<PopoverMenuWrapper dismiss={dismiss} triggerBounds={triggerBounds} overlayBounds={overlayBounds}>
{children}
</PopoverMenuWrapper>
)}
</PopoverTrigger>
);
Menu.Group = Group;
Menu.Item = Item;
Menu.Popover = PopoverMenu;
export default Menu;
export default PopoverMenuWrapper;

@ -0,0 +1,4 @@
export { default as Menu } from './Menu';
export { default as MenuGroup } from './MenuGroup';
export { default as MenuItem } from './MenuItem';
export { default as MenuPopover } from './MenuPopover';

@ -13,6 +13,7 @@ type AudioAttachmentProps = {
const AudioAttachment = ({ url, className, t, ...messageBubbleProps }: AudioAttachmentProps) => (
<MessageBubble nude className={createClassName(styles, 'audio-attachment', {}, [className])} {...messageBubbleProps}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<audio src={url} controls className={createClassName(styles, 'audio-attachment__inner')}>
{t('you_browser_doesn_t_support_audio_element')}
</audio>

@ -11,6 +11,6 @@ type ImageAttachmentProps = {
export const ImageAttachment = memo(({ url, className, ...messageBubbleProps }: ImageAttachmentProps) => (
<MessageBubble nude className={createClassName(styles, 'image-attachment', {}, [className])} {...messageBubbleProps}>
<img src={url} className={createClassName(styles, 'image-attachment__inner')} />
<img className={createClassName(styles, 'image-attachment__inner')} src={url} alt={url} />
</MessageBubble>
));

@ -22,7 +22,12 @@ export const MessageAvatars = memo(({ avatarResolver = () => undefined, username
return (
<div className={createClassName(styles, 'message-avatars', {}, [className])} style={style}>
{avatars.map((username) => (
<Avatar src={avatarResolver(username)} description={username} className={createClassName(styles, 'message-avatars__avatar')} />
<Avatar
key={username}
src={avatarResolver(username)}
description={username}
className={createClassName(styles, 'message-avatars__avatar')}
/>
))}
</div>
);

@ -24,7 +24,6 @@ export class MessageList extends MemoizedComponent {
static SCROLL_AT_BOTTOM_AREA = 128;
// eslint-disable-next-line no-use-before-define
scrollPosition = MessageList.SCROLL_AT_BOTTOM;
handleScroll = () => {
@ -187,12 +186,13 @@ export class MessageList extends MemoizedComponent {
};
render = ({ className, style = {} }) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onScroll={this.handleScroll}
data-qa='message-list'
className={createClassName(styles, 'message-list', {}, [className])}
onClick={this.handleClick}
style={style}
data-qa='message-list'
onClick={this.handleClick}
onScroll={this.handleScroll}
>
<ol className={createClassName(styles, 'message-list__content')}>{this.renderItems(this.props)}</ol>
</div>

@ -11,8 +11,8 @@ type TypingDotsProps = {
export const TypingDots = ({ text, className, style = {} }: TypingDotsProps) => (
<div role='status' aria-label={text} className={createClassName(styles, 'typing-dots', {}, [className])} style={style}>
<span class={createClassName(styles, 'typing-dots__dot')} />
<span class={createClassName(styles, 'typing-dots__dot')} />
<span class={createClassName(styles, 'typing-dots__dot')} />
<span className={createClassName(styles, 'typing-dots__dot')} />
<span className={createClassName(styles, 'typing-dots__dot')} />
<span className={createClassName(styles, 'typing-dots__dot')} />
</div>
);

@ -13,6 +13,7 @@ type VideoAttachmentProps = {
};
const VideoAttachment = ({ url, className, t, ...messageBubbleProps }: VideoAttachmentProps) => (
<MessageBubble nude className={createClassName(styles, 'video-attachment', {}, [className])} {...messageBubbleProps}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video src={url} controls className={createClassName(styles, 'video-attachment__inner')}>
{t('you_browser_doesn_t_support_video_element')}
</video>

@ -0,0 +1,30 @@
import type { ComponentProps } from 'preact';
import { useTranslation } from 'react-i18next';
import { Button } from '../Button';
import { ButtonGroup } from '../ButtonGroup';
import ModalMessage from './MessageModal';
import Modal from './Modal';
export type AlertModalProps = {
text: string;
buttonText?: string;
onConfirm: () => void;
} & Omit<ComponentProps<typeof Modal>, 'open' | 'onDismiss'>;
const AlertModal = ({ text, buttonText, onConfirm, ...props }: AlertModalProps) => {
const { t } = useTranslation();
return (
<Modal open animated dismissByOverlay={false} {...props}>
<ModalMessage>{text}</ModalMessage>
<ButtonGroup>
<Button secondary onClick={onConfirm}>
{buttonText || t('ok')}
</Button>
</ButtonGroup>
</Modal>
);
};
export default AlertModal;

@ -0,0 +1,35 @@
import { type ComponentProps } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '../Button';
import { ButtonGroup } from '../ButtonGroup';
import ModalMessage from './MessageModal';
import Modal from './Modal';
export type ConfirmationModalProps = {
text: string;
confirmButtonText?: string;
cancelButtonText?: string;
onConfirm: () => void;
onCancel: () => void;
} & Omit<ComponentProps<typeof Modal>, 'open' | 'onDismiss'>;
const ConfirmationModal = ({ text, confirmButtonText, cancelButtonText, onConfirm, onCancel, ...props }: ConfirmationModalProps) => {
const { t } = useTranslation();
return (
<Modal open animated dismissByOverlay={false} {...props}>
<ModalMessage>{text}</ModalMessage>
<ButtonGroup>
<Button outline secondary onClick={onCancel}>
{cancelButtonText || t('no')}
</Button>
<Button secondary danger onClick={onConfirm}>
{confirmButtonText || t('yes')}
</Button>
</ButtonGroup>
</Modal>
);
};
export default ConfirmationModal;

@ -0,0 +1,12 @@
import type { ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type ModalMessageProps = {
children: ComponentChildren;
};
const ModalMessage = ({ children }: ModalMessageProps) => <div className={createClassName(styles, 'modal__message')}>{children}</div>;
export default ModalMessage;

@ -2,7 +2,9 @@ import { action } from '@storybook/addon-actions';
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import Modal from './component';
import AlertModal from './AlertModal';
import ConfirmationModal from './ConfirmationModal';
import Modal from './Modal';
import { loremIpsum } from '../../../.storybook/helpers';
export default {
@ -64,7 +66,7 @@ DisallowDismissByOverlay.args = {
onDismiss: action('dismiss'),
};
export const Confirm: StoryFn<ComponentProps<typeof Modal.Confirm>> = (args) => <Modal.Confirm {...args} />;
export const Confirm: StoryFn<ComponentProps<typeof ConfirmationModal>> = (args) => <ConfirmationModal {...args} />;
Confirm.storyName = 'confirm';
Confirm.args = {
text: 'Are you ok?',
@ -74,7 +76,7 @@ Confirm.args = {
onCancel: action('cancel'),
};
export const Alert: StoryFn<ComponentProps<typeof Modal.Alert>> = (args) => <Modal.Alert {...args} />;
export const Alert: StoryFn<ComponentProps<typeof AlertModal>> = (args) => <AlertModal {...args} />;
Alert.storyName = 'alert';
Alert.args = {
text: 'You look great today.',

@ -0,0 +1,73 @@
import { Component } from 'preact';
import type { HTMLAttributes } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type ModalProps = {
open: boolean;
animated?: boolean;
timeout?: number;
dismissByOverlay?: boolean;
onDismiss?: () => void;
} & Omit<HTMLAttributes<HTMLDivElement>, 'onDismiss'>;
class Modal extends Component<ModalProps> {
static override defaultProps = {
dismissByOverlay: true,
};
mounted = false;
handleKeyDown = ({ key }: KeyboardEvent) => {
if (key === 'Escape') {
this.triggerDismiss();
}
};
handleTouchStart = () => {
const { dismissByOverlay } = this.props;
if (dismissByOverlay) this.triggerDismiss();
};
handleMouseDown = () => {
const { dismissByOverlay } = this.props;
if (dismissByOverlay) this.triggerDismiss();
};
triggerDismiss = () => {
const { onDismiss } = this.props;
if (this.mounted) onDismiss?.();
};
override componentDidMount() {
this.mounted = true;
window.addEventListener('keydown', this.handleKeyDown, false);
const { timeout } = this.props;
if (timeout !== undefined && Number.isFinite(timeout) && timeout > 0) {
setTimeout(() => this.triggerDismiss(), timeout);
}
}
override componentWillUnmount() {
this.mounted = false;
window.removeEventListener('keydown', this.handleKeyDown, false);
}
render = ({ children, animated, open, ...props }: ModalProps) =>
open ? (
<div
data-qa-type='modal-overlay'
role='presentation'
className={createClassName(styles, 'modal__overlay')}
onTouchStart={this.handleTouchStart}
onMouseDown={this.handleMouseDown}
>
<div className={createClassName(styles, 'modal', { animated })} {...props}>
{children}
</div>
</div>
) : null;
}
export default Modal;

@ -0,0 +1,33 @@
import { type ComponentProps } from 'preact';
import AlertModal from './AlertModal';
import ConfirmationModal from './ConfirmationModal';
import store from '../../store';
export const ModalManager = {
confirm(props: Omit<ComponentProps<typeof ConfirmationModal>, 'onConfirm' | 'onCancel'>) {
return new Promise<{ success: boolean }>((resolve) => {
const handleButton = (success: boolean) => () => {
store.setState({ modal: null });
resolve({ success });
};
store.setState({
modal: <ConfirmationModal {...props} onConfirm={handleButton(true)} onCancel={handleButton(false)} />,
});
});
},
alert(props: Omit<ComponentProps<typeof AlertModal>, 'onConfirm'>) {
return new Promise<{ success: boolean }>((resolve) => {
const handleButton = () => () => {
store.setState({ modal: null });
resolve({ success: true });
};
store.setState({
modal: <AlertModal {...props} onConfirm={handleButton()} />,
});
});
},
} as const;

@ -1,95 +0,0 @@
import { Component } from 'preact';
import { withTranslation } from 'react-i18next';
import { createClassName } from '../../helpers/createClassName';
import { Button } from '../Button';
import { ButtonGroup } from '../ButtonGroup';
import styles from './styles.scss';
export class Modal extends Component {
static defaultProps = {
dismissByOverlay: true,
};
handleKeyDown = ({ key }) => {
if (key === 'Escape') {
this.triggerDismiss();
}
};
handleTouchStart = () => {
const { dismissByOverlay } = this.props;
dismissByOverlay && this.triggerDismiss();
};
handleMouseDown = () => {
const { dismissByOverlay } = this.props;
dismissByOverlay && this.triggerDismiss();
};
triggerDismiss = () => {
const { onDismiss } = this.props;
this.mounted && onDismiss && onDismiss();
};
componentDidMount() {
this.mounted = true;
window.addEventListener('keydown', this.handleKeyDown, false);
const { timeout } = this.props;
if (Number.isFinite(timeout) && timeout > 0) {
setTimeout(() => this.triggerDismiss(), timeout);
}
}
componentWillUnmount() {
this.mounted = false;
window.removeEventListener('keydown', this.handleKeyDown, false);
}
render = ({ children, animated, open, ...props }) =>
open ? (
<div
data-qa-type='modal-overlay'
onTouchStart={this.handleTouchStart}
onMouseDown={this.handleMouseDown}
className={createClassName(styles, 'modal__overlay')}
>
<div className={createClassName(styles, 'modal', { animated })} {...props}>
{children}
</div>
</div>
) : null;
}
export const ModalMessage = ({ children }) => <div className={createClassName(styles, 'modal__message')}>{children}</div>;
export const ConfirmationModal = withTranslation()(({ text, confirmButtonText, cancelButtonText, onConfirm, onCancel, t, ...props }) => (
<Modal open animated dismissByOverlay={false} {...props}>
<Modal.Message>{text}</Modal.Message>
<ButtonGroup>
<Button outline secondary onClick={onCancel}>
{cancelButtonText || t('no')}
</Button>
<Button secondaryDanger onClick={onConfirm}>
{confirmButtonText || t('yes')}
</Button>
</ButtonGroup>
</Modal>
));
export const AlertModal = withTranslation()(({ text, buttonText, onConfirm, t, ...props }) => (
<Modal open animated dismissByOverlay={false} {...props}>
<Modal.Message>{text}</Modal.Message>
<ButtonGroup>
<Button secondary onClick={onConfirm}>
{buttonText || t('ok')}
</Button>
</ButtonGroup>
</Modal>
));
Modal.Message = ModalMessage;
Modal.Confirm = ConfirmationModal;
Modal.Alert = AlertModal;
export default Modal;

@ -1,2 +0,0 @@
export { default, Modal, ModalMessage, ConfirmationModal, AlertModal } from './component';
export { default as ModalManager } from './manager';

@ -0,0 +1,5 @@
export { default as Modal } from './Modal';
export { ModalManager } from './ModalManager';
export { default as ModalMessage } from './MessageModal';
export { default as AlertModal } from './AlertModal';
export { default as ConfirmationModal } from './ConfirmationModal';

@ -1,30 +0,0 @@
import Modal from './component';
import store from '../../store';
export default {
confirm(props = {}) {
return new Promise((resolve) => {
const handleButton = (success) => () => {
store.setState({ modal: null });
resolve({ success });
};
store.setState({
modal: <Modal.Confirm {...props} onConfirm={handleButton(true)} onCancel={handleButton(false)} />,
});
});
},
alert(props = {}) {
return new Promise((resolve) => {
const handleButton = () => () => {
store.setState({ modal: null });
resolve({ success: true });
};
store.setState({
modal: <Modal.Alert {...props} onConfirm={handleButton()} />,
});
});
},
};

@ -1,8 +1,9 @@
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import { PopoverContainer, PopoverTrigger } from '.';
import { PopoverContainer } from '.';
import { Button } from '../Button';
import PopoverTrigger from './PopoverTrigger';
export default {
title: 'Components/Popover',

@ -0,0 +1,130 @@
import { Component, type ComponentProps } from 'preact';
import { PopoverContext } from './PopoverContext';
import PopoverOverlay from './PopoverOverlay';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
import { normalizeDOMRect } from '../../helpers/normalizeDOMRect';
export type PopoverContainerProps = {
children?: any;
};
type PopoverContainerState = {
renderer:
| null
| ((options: {
dismiss: () => void;
overlayBounds?: {
left: number;
top: number;
right: number;
bottom: number;
} | null;
triggerBounds?: {
left: number;
top: number;
right: number;
bottom: number;
} | null;
}) => any);
overlayProps?: ComponentProps<typeof PopoverOverlay>;
overlayBounds?: {
left: number;
top: number;
right: number;
bottom: number;
} | null;
triggerBounds?: {
left: number;
top: number;
right: number;
bottom: number;
} | null;
};
class PopoverContainer extends Component<PopoverContainerProps, PopoverContainerState> {
override state: PopoverContainerState = {
renderer: null,
};
mounted = false;
overlayRef: any = null;
open = (
renderer: (options: {
dismiss: () => void;
overlayBounds?: { left: number; top: number; right: number; bottom: number } | null;
triggerBounds?: { left: number; top: number; right: number; bottom: number } | null;
}) => any,
props: ComponentProps<typeof PopoverOverlay>,
{ currentTarget }: { currentTarget?: HTMLElement | null } = {},
) => {
let overlayBounds;
let triggerBounds;
if (this.overlayRef) {
overlayBounds = normalizeDOMRect(this.overlayRef.base.getBoundingClientRect());
}
if (currentTarget) {
triggerBounds = normalizeDOMRect(currentTarget.getBoundingClientRect());
}
this.setState({ renderer, ...props, overlayBounds, triggerBounds });
};
dismiss = () => {
this.setState({ renderer: null, overlayBounds: null, triggerBounds: null });
};
handleOverlayGesture = ({ currentTarget, target }: Event) => {
if (currentTarget !== target) {
return;
}
this.dismiss();
};
handleKeyDown = ({ key }: KeyboardEvent) => {
if (key !== 'Escape') {
return;
}
this.dismiss();
};
handleOverlayRef = (ref: any) => {
this.overlayRef = ref;
};
override componentDidMount() {
this.mounted = true;
window.addEventListener('keydown', this.handleKeyDown, false);
}
override componentWillUnmount() {
this.mounted = false;
window.removeEventListener('keydown', this.handleKeyDown, false);
}
render = ({ children }: PopoverContainerProps, { renderer, overlayProps, overlayBounds, triggerBounds }: PopoverContainerState) => (
<PopoverContext.Provider value={{ open: this.open }}>
<div className={createClassName(styles, 'popover__container')}>
{children}
<PopoverOverlay
ref={this.handleOverlayRef}
onMouseDown={this.handleOverlayGesture}
onTouchStart={this.handleOverlayGesture}
visible={!!renderer}
{...overlayProps}
>
{renderer ? renderer({ dismiss: this.dismiss, overlayBounds, triggerBounds }) : null}
</PopoverOverlay>
</div>
</PopoverContext.Provider>
);
}
export default PopoverContainer;

@ -0,0 +1,7 @@
import { createContext } from 'preact';
export const PopoverContext = createContext<{ open: (renderer: any, props: any, options?: { currentTarget?: HTMLElement }) => void }>({
open: () => {
// noop
},
});

@ -0,0 +1,20 @@
import type { ComponentChildren } from 'preact';
import type { HTMLAttributes } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type PopoverOverlayProps = {
children?: ComponentChildren;
visible?: boolean;
className?: string;
ref?: any; // FIXME: remove this
} & HTMLAttributes<HTMLDivElement>;
const PopoverOverlay = ({ children, className, visible, ...props }: PopoverOverlayProps) => (
<div className={createClassName(styles, 'popover__overlay', { visible }, [className])} {...props}>
{children}
</div>
);
export default PopoverOverlay;

@ -0,0 +1,17 @@
import { type ComponentChildren } from 'preact';
import { PopoverContext } from './PopoverContext';
export type PopoverTriggerProps = {
children: [
trigger: (contextValue: { pop: () => void }) => ComponentChildren,
renderer: (popoverContext: { dismiss: () => void; triggerBounds: DOMRect; overlayBounds: DOMRect }) => ComponentChildren,
];
overlayProps?: any;
};
const PopoverTrigger = ({ children, ...props }: PopoverTriggerProps) => (
<PopoverContext.Consumer>{({ open }) => children[0]({ pop: open.bind(null, children[1], props) })}</PopoverContext.Consumer>
);
export default PopoverTrigger;

@ -1,90 +0,0 @@
import { Component, createContext } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
import { normalizeDOMRect } from '../../helpers/normalizeDOMRect';
const PopoverContext = createContext();
const PopoverOverlay = ({ children, className, visible, ...props }) => (
<div className={createClassName(styles, 'popover__overlay', { visible }, [className])} {...props}>
{children}
</div>
);
export class PopoverContainer extends Component {
state = {
renderer: null,
};
open = (renderer, props, { currentTarget } = {}) => {
let overlayBounds;
let triggerBounds;
if (this.overlayRef) {
overlayBounds = normalizeDOMRect(this.overlayRef.base.getBoundingClientRect());
}
if (currentTarget) {
triggerBounds = normalizeDOMRect(currentTarget.getBoundingClientRect());
}
this.setState({ renderer, ...props, overlayBounds, triggerBounds });
};
dismiss = () => {
this.setState({ renderer: null, overlayBounds: null, triggerBounds: null });
};
handleOverlayGesture = ({ currentTarget, target }) => {
if (currentTarget !== target) {
return;
}
this.dismiss();
};
handleKeyDown = ({ key }) => {
if (key !== 'Escape') {
return;
}
this.dismiss();
};
handleOverlayRef = (ref) => {
this.overlayRef = ref;
};
componentDidMount() {
this.mounted = true;
window.addEventListener('keydown', this.handleKeyDown, false);
}
componentWillUnmount() {
this.mounted = false;
window.removeEventListener('keydown', this.handleKeyDown, false);
}
render = ({ children }, { renderer, overlayProps, overlayBounds, triggerBounds }) => (
<PopoverContext.Provider value={{ open: this.open }}>
<div className={createClassName(styles, 'popover__container')}>
{children}
<PopoverOverlay
ref={this.handleOverlayRef}
onMouseDown={this.handleOverlayGesture}
onTouchStart={this.handleOverlayGesture}
visible={!!renderer}
{...overlayProps}
>
{renderer ? renderer({ dismiss: this.dismiss, overlayBounds, triggerBounds }) : null}
</PopoverOverlay>
</div>
</PopoverContext.Provider>
);
}
/** @type {function({ children: [function({ pop: function() }), function({ dismiss: any, triggerBounds?: any, overlayBounds?: any })], overlayProps?: any }): any} */
export const PopoverTrigger = ({ children, ...props }) => (
<PopoverContext.Consumer>{({ open }) => children[0]({ pop: open.bind(null, children[1], props) })}</PopoverContext.Consumer>
);

@ -0,0 +1,2 @@
export { default as PopoverContainer } from './PopoverContainer';
export { default as PopoverTrigger } from './PopoverTrigger';

@ -5,7 +5,7 @@ import { Button } from '../Button';
type ChatButtonProps = {
text: string;
minimized: boolean;
badge: number;
badge: number | undefined;
onClick: () => void;
triggered?: boolean;
className?: string;

@ -0,0 +1,48 @@
import { useEffect } from 'preact/hooks';
import type { ScreenTheme } from './ScreenProvider';
import styles from './styles.scss';
export type CssVarProps = {
theme: ScreenTheme;
};
const CssVar = ({ theme }: CssVarProps) => {
useEffect(() => {
if (window.CSS && CSS.supports('color', 'var(--color)')) {
return;
}
let mounted = true;
void (async () => {
const { default: cssVars } = await import('css-vars-ponyfill');
if (!mounted) {
return;
}
cssVars({
variables: {
'--color': theme.color,
'--font-color': theme.fontColor,
'--icon-color': theme.iconColor,
},
});
})();
return () => {
mounted = false;
};
}, [theme]);
return (
<style>{`
.${styles.screen} {
${theme.color ? `--color: ${theme.color};` : ''}
${theme.fontColor ? `--font-color: ${theme.fontColor};` : ''}
${theme.iconColor ? `--icon-color: ${theme.iconColor};` : ''}
${theme.guestBubbleBackgroundColor ? `--sender-bubble-background-color: ${theme.guestBubbleBackgroundColor};` : ''}
${theme.agentBubbleBackgroundColor ? `--receiver-bubble-background-color: ${theme.agentBubbleBackgroundColor};` : ''}
${theme.background ? `--message-list-background: ${theme.background};` : ''}
}
`}</style>
);
};
export default CssVar;

@ -3,34 +3,18 @@ import type { Meta, StoryFn } from '@storybook/preact';
import i18next from 'i18next';
import type { ComponentProps } from 'preact';
import { Screen } from '.';
import { Screen, ScreenContent, ScreenFooter } from '.';
import { screenDecorator } from '../../../.storybook/helpers';
import { FooterOptions } from '../Footer';
import Menu from '../Menu';
import { MenuGroup, MenuItem } from '../Menu';
export default {
title: 'Components/Screen/Footer',
component: Screen.Footer,
component: ScreenFooter,
decorators: [
(storyFn) => (
<Screen
theme={{
color: '',
fontColor: '',
iconColor: '',
}}
title={'Title'}
notificationsEnabled={true}
minimized={false}
expanded={false}
windowed={false}
onEnableNotifications={action('enableNotifications')}
onDisableNotifications={action('disableNotifications')}
onMinimize={action('minimize')}
onRestore={action('restore')}
onOpenWindow={action('openWindow')}
>
<Screen.Content />
<Screen title='Title'>
<ScreenContent />
{storyFn()}
</Screen>
),
@ -39,27 +23,27 @@ export default {
parameters: {
layout: 'centered',
},
} satisfies Meta<ComponentProps<typeof Screen.Footer>>;
} satisfies Meta<ComponentProps<typeof ScreenFooter>>;
export const Empty: StoryFn<ComponentProps<typeof Screen.Footer>> = () => <Screen.Footer />;
export const Empty: StoryFn<ComponentProps<typeof ScreenFooter>> = () => <ScreenFooter />;
Empty.storyName = 'empty';
export const WithChildren: StoryFn<ComponentProps<typeof Screen.Footer>> = () => (
<Screen.Footer>Lorem ipsum dolor sit amet, his id atqui repudiare.</Screen.Footer>
export const WithChildren: StoryFn<ComponentProps<typeof ScreenFooter>> = () => (
<ScreenFooter>Lorem ipsum dolor sit amet, his id atqui repudiare.</ScreenFooter>
);
WithChildren.storyName = 'with children';
export const WithOptions: StoryFn<ComponentProps<typeof Screen.Footer>> = () => (
<Screen.Footer
export const WithOptions: StoryFn<ComponentProps<typeof ScreenFooter>> = () => (
<ScreenFooter
options={
<FooterOptions>
<Menu.Group>
<Menu.Item onClick={action('changeDepartment')}>{i18next.t('change_department')}</Menu.Item>
<Menu.Item onClick={action('removeUserData')}>{i18next.t('forget_remove_my_data')}</Menu.Item>
<Menu.Item danger onClick={action('finishChat')}>
<MenuGroup>
<MenuItem onClick={action('changeDepartment')}>{i18next.t('change_department')}</MenuItem>
<MenuItem onClick={action('removeUserData')}>{i18next.t('forget_remove_my_data')}</MenuItem>
<MenuItem danger onClick={action('finishChat')}>
{i18next.t('finish_this_chat')}
</Menu.Item>
</Menu.Group>
</MenuItem>
</MenuGroup>
</FooterOptions>
}
/>

@ -2,6 +2,7 @@ import type { ComponentChildren } from 'preact';
import { useRef } from 'preact/hooks';
import { useTranslation, withTranslation } from 'react-i18next';
import type { ScreenContextValue } from './ScreenProvider';
import type { Agent } from '../../definitions/agents';
import MinimizeIcon from '../../icons/arrowDown.svg';
import RestoreIcon from '../../icons/arrowUp.svg';
@ -10,9 +11,18 @@ import NotificationsDisabledIcon from '../../icons/bellOff.svg';
import OpenWindowIcon from '../../icons/newWindow.svg';
import Alert from '../Alert';
import { Avatar } from '../Avatar';
import Header from '../Header';
import Tooltip from '../Tooltip';
import type { ScreenContextValue } from './ScreenProvider';
import {
Header,
HeaderAction,
HeaderActions,
HeaderContent,
HeaderCustomField,
HeaderPicture,
HeaderPost,
HeaderSubTitle,
HeaderTitle,
} from '../Header';
import { TooltipContainer, TooltipTrigger } from '../Tooltip';
type ScreenHeaderProps = {
alerts: { id: string; children: ComponentChildren; [key: string]: unknown }[];
@ -74,31 +84,31 @@ const ScreenHeader = ({
<Header
ref={headerRef}
post={
<Header.Post>
<HeaderPost>
{alerts?.map((alert) => (
<Alert {...alert} onDismiss={onDismissAlert}>
<Alert key={alert.id} {...alert} onDismiss={onDismissAlert}>
{alert.children}
</Alert>
))}
</Header.Post>
</HeaderPost>
}
large={largeHeader()}
>
{agent?.avatar && (
<Header.Picture>
<HeaderPicture>
<Avatar src={agent.avatar.src} description={agent.avatar.description} status={agent.status} large={largeHeader()} />
</Header.Picture>
</HeaderPicture>
)}
<Header.Content>
<Header.Title>{headerTitle()}</Header.Title>
{agent?.email && <Header.SubTitle>{agent.email}</Header.SubTitle>}
{agent?.phone && <Header.CustomField>{agent.phone}</Header.CustomField>}
</Header.Content>
<Tooltip.Container>
<Header.Actions>
<Tooltip.Trigger content={notificationsEnabled ? t('sound_is_on') : t('sound_is_off')} placement='bottom-left'>
<Header.Action
<HeaderContent>
<HeaderTitle>{headerTitle()}</HeaderTitle>
{agent?.email && <HeaderSubTitle>{agent.email}</HeaderSubTitle>}
{agent?.phone && <HeaderCustomField>{agent.phone}</HeaderCustomField>}
</HeaderContent>
<TooltipContainer>
<HeaderActions>
<TooltipTrigger content={notificationsEnabled ? t('sound_is_on') : t('sound_is_off')} placement='bottom-left'>
<HeaderAction
aria-label={notificationsEnabled ? t('disable_notifications') : t('enable_notifications')}
onClick={notificationsEnabled ? onDisableNotifications : onEnableNotifications}
>
@ -107,24 +117,24 @@ const ScreenHeader = ({
) : (
<NotificationsDisabledIcon width={20} height={20} />
)}
</Header.Action>
</Tooltip.Trigger>
</HeaderAction>
</TooltipTrigger>
{(expanded || !windowed) && (
<Tooltip.Trigger content={minimized ? t('restore_chat') : t('minimize_chat')}>
<Header.Action aria-label={minimized ? t('restore_chat') : t('minimize_chat')} onClick={minimized ? onRestore : onMinimize}>
<TooltipTrigger content={minimized ? t('restore_chat') : t('minimize_chat')}>
<HeaderAction aria-label={minimized ? t('restore_chat') : t('minimize_chat')} onClick={minimized ? onRestore : onMinimize}>
{minimized ? <RestoreIcon width={20} height={20} /> : <MinimizeIcon width={20} height={20} />}
</Header.Action>
</Tooltip.Trigger>
</HeaderAction>
</TooltipTrigger>
)}
{!hideExpandChat && !expanded && !windowed && (
<Tooltip.Trigger content={t('expand_chat')} placement='bottom-left'>
<Header.Action aria-label={t('expand_chat')} onClick={onOpenWindow}>
<TooltipTrigger content={t('expand_chat')} placement='bottom-left'>
<HeaderAction aria-label={t('expand_chat')} onClick={onOpenWindow}>
<OpenWindowIcon width={20} height={20} />
</Header.Action>
</Tooltip.Trigger>
</HeaderAction>
</TooltipTrigger>
)}
</Header.Actions>
</Tooltip.Container>
</HeaderActions>
</TooltipContainer>
</Header>
);
};

@ -1,77 +1,32 @@
import { useContext, useEffect } from 'preact/hooks';
import { useContext } from 'preact/hooks';
import { createClassName } from '../../helpers/createClassName';
import CloseIcon from '../../icons/close.svg';
import { Button } from '../Button';
import { Footer, FooterContent, PoweredBy } from '../Footer';
import { PopoverContainer } from '../Popover';
import { Sound } from '../Sound';
import { ChatButton } from './ChatButton';
import CssVar from './CssVar';
import ScreenHeader from './Header';
import { ScreenContext } from './ScreenProvider';
import styles from './styles.scss';
export const ScreenContent = ({ children, nopadding, triggered = false, full = false }) => (
<main className={createClassName(styles, 'screen__main', { nopadding, triggered, full })}>{children}</main>
);
export const ScreenFooter = ({ children, options, limit }) => {
const { hideWatermark } = useContext(ScreenContext);
return (
<Footer>
{children && <FooterContent>{children}</FooterContent>}
<FooterContent>
{options}
{limit}
{!hideWatermark && <PoweredBy />}
</FooterContent>
</Footer>
);
};
const CssVar = ({ theme }) => {
useEffect(() => {
if (window.CSS && CSS.supports('color', 'var(--color)')) {
return;
}
let mounted = true;
(async () => {
const { default: cssVars } = await import('css-vars-ponyfill');
if (!mounted) {
return;
}
cssVars({
variables: {
'--color': theme.color,
'--font-color': theme.fontColor,
'--icon-color': theme.iconColor,
},
});
})();
return () => {
mounted = false;
};
}, [theme]);
return (
<style>{`
.${styles.screen} {
${theme.color ? `--color: ${theme.color};` : ''}
${theme.fontColor ? `--font-color: ${theme.fontColor};` : ''}
${theme.iconColor ? `--icon-color: ${theme.iconColor};` : ''}
${theme.guestBubbleBackgroundColor ? `--sender-bubble-background-color: ${theme.guestBubbleBackgroundColor};` : ''}
${theme.agentBubbleBackgroundColor ? `--receiver-bubble-background-color: ${theme.agentBubbleBackgroundColor};` : ''}
${theme.background ? `--message-list-background: ${theme.background};` : ''}
}
`}</style>
);
export type ScreenProps = {
title: string;
color?: string;
agent?: any;
children?: any;
className?: string;
unread?: number;
triggered?: boolean;
queueInfo?: any;
onSoundStop?: () => void;
ref?: any; // FIXME: remove this
};
/** @type {{ (props: any) => JSX.Element; Content: (props: any) => JSX.Element; Footer: (props: any) => JSX.Element }} */
export const Screen = ({ title, color, agent, children, className, unread, triggered = false, queueInfo, onSoundStop }) => {
const Screen = ({ title, color, agent, children, className, unread, triggered = false, queueInfo, onSoundStop }: ScreenProps) => {
const {
theme = {},
theme,
livechatLogo,
notificationsEnabled,
minimized = false,
@ -133,7 +88,6 @@ export const Screen = ({ title, color, agent, children, className, unread, trigg
</div>
<ChatButton
agent={agent}
triggered={triggered}
text={title}
badge={unread}
@ -148,7 +102,4 @@ export const Screen = ({ title, color, agent, children, className, unread, trigg
);
};
Screen.Content = ScreenContent;
Screen.Footer = ScreenFooter;
export default Screen;

@ -0,0 +1,17 @@
import { type ComponentChildren } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type ScreenContentProps = {
children?: ComponentChildren;
nopadding?: boolean;
triggered?: boolean;
full?: boolean;
};
const ScreenContent = ({ children, nopadding, triggered = false, full = false }: ScreenContentProps) => (
<main className={createClassName(styles, 'screen__main', { nopadding, triggered, full })}>{children}</main>
);
export default ScreenContent;

@ -0,0 +1,28 @@
import { type ComponentChildren } from 'preact';
import { useContext } from 'preact/hooks';
import { Footer, FooterContent, PoweredBy } from '../Footer';
import { ScreenContext } from './ScreenProvider';
export type ScreenFooterProps = {
children?: ComponentChildren;
options?: ComponentChildren;
limit?: ComponentChildren;
};
const ScreenFooter = ({ children, options, limit }: ScreenFooterProps) => {
const { hideWatermark } = useContext(ScreenContext);
return (
<Footer>
{children && <FooterContent>{children}</FooterContent>}
<FooterContent>
{options}
{limit}
{!hideWatermark && <PoweredBy />}
</FooterContent>
</Footer>
);
};
export default ScreenFooter;

@ -1,4 +1,4 @@
import type { FunctionalComponent } from 'preact';
import type { ComponentChildren } from 'preact';
import { createContext } from 'preact';
import { useContext, useEffect, useState } from 'preact/hooks';
import { parse } from 'query-string';
@ -10,6 +10,19 @@ import { loadMessages } from '../../lib/room';
import Triggers from '../../lib/triggers';
import { StoreContext } from '../../store';
export type ScreenTheme = {
color: string;
fontColor: string;
iconColor: string;
position?: 'left' | 'right';
guestBubbleBackgroundColor?: string;
agentBubbleBackgroundColor?: string;
background?: string;
hideAgentAvatar: boolean;
hideGuestAvatar: boolean;
hideExpandChat: boolean;
};
export type ScreenContextValue = {
hideWatermark: boolean;
livechatLogo: { url: string } | undefined;
@ -17,7 +30,10 @@ export type ScreenContextValue = {
minimized: boolean;
expanded: boolean;
windowed: boolean;
sound: unknown;
sound: {
src: string;
play: boolean;
};
alerts: unknown;
modal: unknown;
nameDefault: string;
@ -30,18 +46,7 @@ export type ScreenContextValue = {
onOpenWindow: () => unknown;
onDismissAlert: () => unknown;
dismissNotification: () => void;
theme?: {
color?: string;
fontColor?: string;
iconColor?: string;
position?: 'left' | 'right';
guestBubbleBackgroundColor?: string;
agentBubbleBackgroundColor?: string;
background?: string;
hideGuestAvatar?: boolean;
hideAgentAvatar?: boolean;
hideExpandChat?: boolean;
};
theme: ScreenTheme;
};
export const ScreenContext = createContext<ScreenContextValue>({
@ -63,7 +68,11 @@ export const ScreenContext = createContext<ScreenContextValue>({
onOpenWindow: () => undefined,
} as ScreenContextValue);
export const ScreenProvider: FunctionalComponent = ({ children }) => {
type ScreenProviderProps = {
children: ComponentChildren;
};
export const ScreenProvider = ({ children }: ScreenProviderProps) => {
const store = useContext(StoreContext);
const { token, dispatch, config, sound, minimized = true, undocked, expanded = false, alerts, modal, iframe, customFieldsQueue } = store;
const { department, name, email } = iframe.guest || {};

@ -0,0 +1,3 @@
export { default as Screen } from './Screen';
export { default as ScreenContent } from './ScreenContent';
export { default as ScreenFooter } from './ScreenFooter';

@ -1,29 +1,14 @@
import { action } from '@storybook/addon-actions';
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import { Screen } from '.';
import { Screen, ScreenContent } from '.';
import { gazzoAvatar, screenDecorator } from '../../../.storybook/helpers';
export default {
title: 'Components/Screen',
component: Screen,
args: {
theme: {
color: '',
fontColor: '',
iconColor: '',
},
title: 'Title',
notificationsEnabled: true,
minimized: false,
expanded: false,
windowed: false,
onEnableNotifications: action('enableNotifications'),
onDisableNotifications: action('disableNotifications'),
onMinimize: action('minimize'),
onRestore: action('restore'),
onOpenWindow: action('openWindow'),
},
decorators: [screenDecorator],
parameters: {
@ -33,31 +18,13 @@ export default {
const Template: StoryFn<ComponentProps<typeof Screen>> = (args) => (
<Screen {...args}>
<Screen.Content>Content</Screen.Content>
<ScreenContent>Content</ScreenContent>
</Screen>
);
export const Normal = Template.bind({});
Normal.storyName = 'normal';
export const Minimized = Template.bind({});
Minimized.storyName = 'minimized';
Minimized.args = {
minimized: true,
};
export const Expanded = Template.bind({});
Expanded.storyName = 'expanded';
Expanded.args = {
expanded: true,
};
export const Windowed = Template.bind({});
Windowed.storyName = 'windowed';
Windowed.args = {
windowed: true,
};
export const WithAgentEmail = Template.bind({});
WithAgentEmail.storyName = 'with agent (email)';
WithAgentEmail.args = {
@ -108,14 +75,3 @@ WithHiddenAgent.args = {
hiddenInfo: true,
},
};
export const WithMultipleAlerts = Template.bind({});
WithMultipleAlerts.storyName = 'with multiple alerts';
WithMultipleAlerts.args = {
alerts: [
{ id: 1, children: 'Success alert', success: true },
{ id: 2, children: 'Warning alert', warning: true, timeout: 0 },
{ id: 3, children: 'Error alert', error: true, timeout: 1000 },
{ id: 4, children: 'Custom colored alert', color: '#000', timeout: 5000 },
],
};

@ -31,5 +31,6 @@ export class Sound extends Component {
this.handlePlayProp();
}
// eslint-disable-next-line jsx-a11y/media-has-caption
render = ({ src, onStart, onStop }) => <audio ref={this.handleRef} src={src} onPlay={onStart} onEnded={onStop} type='audio/mpeg' />;
}

@ -1,7 +1,9 @@
import type { Meta, StoryFn } from '@storybook/preact';
import type { ComponentProps } from 'preact';
import Tooltip, { withTooltip } from '.';
import Tooltip from './Tooltip';
import TooltipContainer from './TooltipContainer';
import TooltipTrigger from './TooltipTrigger';
import { Button } from '../Button';
const placements = [null, 'left', 'top', 'right', 'bottom', 'top-left', 'top-right', 'bottom-left', 'bottom-right'] as const;
@ -32,28 +34,19 @@ Inline.storyName = 'inline';
export const Placements: StoryFn<ComponentProps<typeof Tooltip>> = (args) => (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{placements.map((placement, i) => (
<Tooltip {...args} key={i} placement={placement} />
<Tooltip key={i} {...args} placement={placement} />
))}
</div>
);
Placements.storyName = 'placements';
export const ConnectedToAnotherComponent: StoryFn<ComponentProps<typeof Tooltip.Trigger>> = (args) => (
<Tooltip.Trigger {...args}>
export const ConnectedToAnotherComponent: StoryFn<ComponentProps<typeof TooltipTrigger>> = (args) => (
<TooltipTrigger {...args}>
<Button>A simple button</Button>
</Tooltip.Trigger>
</TooltipTrigger>
);
ConnectedToAnotherComponent.storyName = 'connected to another component';
ConnectedToAnotherComponent.args = {
content: 'A simple tool tip',
};
ConnectedToAnotherComponent.decorators = [(storyFn) => <Tooltip.Container>{storyFn()}</Tooltip.Container>];
const MyButton = withTooltip(Button);
export const WithTooltip: StoryFn<{ tooltip?: string }> = ({ tooltip }) => <MyButton tooltip={tooltip}>A simple button</MyButton>;
WithTooltip.storyName = 'withTooltip()';
WithTooltip.args = {
tooltip: 'A simple tool tip',
};
WithTooltip.decorators = [(storyFn) => <Tooltip.Container>{storyFn()}</Tooltip.Container>];
ConnectedToAnotherComponent.decorators = [(storyFn) => <TooltipContainer>{storyFn()}</TooltipContainer>];

@ -0,0 +1,61 @@
import type { HTMLAttributes } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
export type Placement = 'left' | 'top' | 'right' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | null;
const getPositioningStyle = (
placement: Placement,
{ left, top, right, bottom }: { left: number; top: number; right: number; bottom: number },
) => {
switch (placement) {
case 'left':
return {
left: `${left}px`,
top: `${(top + bottom) / 2}px`,
};
case 'top':
case 'top-left':
case 'top-right':
return {
left: `${(left + right) / 2}px`,
top: `${top}px`,
};
case 'right':
return {
left: `${right}px`,
top: `${(top + bottom) / 2}px`,
};
case 'bottom':
case 'bottom-left':
case 'bottom-right':
default:
return {
left: `${(left + right) / 2}px`,
top: `${bottom}px`,
};
}
};
export type TooltipProps = {
hidden?: boolean;
placement: Placement;
floating?: boolean;
triggerBounds: { left: number; top: number; right: number; bottom: number };
} & Omit<HTMLAttributes<HTMLDivElement>, 'ref'>;
const Tooltip = ({ children, hidden = false, placement, floating = false, triggerBounds, ...props }: TooltipProps) => (
<div
className={createClassName(styles, 'tooltip', { hidden, placement, floating })}
style={floating ? getPositioningStyle(placement, triggerBounds) : {}}
{...props}
>
{children}
</div>
);
export default Tooltip;

@ -0,0 +1,71 @@
import { Component, type ComponentChildren } from 'preact';
import Tooltip, { type Placement } from './Tooltip';
import { TooltipContext } from './TooltipContext';
export type TooltipContainerProps = {
children: any;
};
type TooltipContainerState = {
tooltip: any;
activeChild: number | null;
event: any;
placement: Placement;
content?: ComponentChildren;
};
class TooltipContainer extends Component<TooltipContainerProps, TooltipContainerState> {
override state: TooltipContainerState = {
tooltip: null,
activeChild: null,
event: null,
placement: null,
};
showTooltip = (
event: any,
{ content, placement = 'bottom', childIndex }: { content: any; placement?: Placement; childIndex: number | null },
) => {
const triggerBounds = event.target.getBoundingClientRect();
this.setState({
tooltip: (
<Tooltip floating placement={placement} triggerBounds={triggerBounds}>
{content}
</Tooltip>
),
activeChild: childIndex,
event,
placement,
content,
});
};
hideTooltip = () => {
this.setState({ tooltip: null });
};
UNSAFE_componentWillReceiveProps(props: TooltipContainerProps) {
if (this.state.tooltip) {
const activeChildren = props?.children?.props?.children[this.state.activeChild ?? 0];
if (activeChildren && activeChildren.props.content !== this.state.content) {
this.showTooltip(this.state.event, {
content: activeChildren.props.content,
placement: this.state.placement,
childIndex: this.state.activeChild,
});
}
}
}
render({ children }: TooltipContainerProps) {
return (
<TooltipContext.Provider value={{ ...this.state, showTooltip: this.showTooltip, hideTooltip: this.hideTooltip }}>
{children}
<TooltipContext.Consumer>{({ tooltip }) => tooltip}</TooltipContext.Consumer>
</TooltipContext.Provider>
);
}
}
export default TooltipContainer;

@ -0,0 +1,35 @@
import { type ComponentChildren, createContext } from 'preact';
import type { Placement } from './Tooltip';
export const TooltipContext = createContext<{
tooltip: any;
activeChild: number | null;
event: any;
placement: Placement;
content?: ComponentChildren;
showTooltip: (
event: any,
{
content,
placement,
childIndex,
}: {
content: any;
placement?: Placement;
childIndex: number | null;
},
) => void;
hideTooltip: () => void;
}>({
activeChild: null,
event: null,
placement: null,
showTooltip: () => {
// noop
},
hideTooltip: () => {
// noop
},
tooltip: null,
});

@ -0,0 +1,30 @@
import type { ComponentChildren, VNode } from 'preact';
import { toChildArray, cloneElement } from 'preact';
import type { FocusEvent, MouseEvent } from 'preact/compat';
import type { Placement } from './Tooltip';
import { TooltipContext } from './TooltipContext';
export type TooltipTriggerProps = {
content: ComponentChildren;
placement?: Placement;
children: ComponentChildren;
};
const TooltipTrigger = ({ children, content, placement }: TooltipTriggerProps) => (
<TooltipContext.Consumer>
{({ showTooltip, hideTooltip }) =>
toChildArray(children).map((child, index) =>
cloneElement(child as VNode, {
onMouseEnter: (event: MouseEvent<any>) => showTooltip(event, { content, placement, childIndex: index }),
onMouseLeave: () => hideTooltip(),
onFocusCapture: (event: FocusEvent<any>) => showTooltip(event, { content, placement, childIndex: index }),
onBlurCapture: () => hideTooltip(),
content,
}),
)
}
</TooltipContext.Consumer>
);
export default TooltipTrigger;

@ -1,127 +0,0 @@
import { cloneElement, Component, createContext, toChildArray } from 'preact';
import styles from './styles.scss';
import { createClassName } from '../../helpers/createClassName';
const getPositioningStyle = (placement, { left, top, right, bottom }) => {
switch (placement) {
case 'left':
return {
left: `${left}px`,
top: `${(top + bottom) / 2}px`,
};
case 'top':
case 'top-left':
case 'top-right':
return {
left: `${(left + right) / 2}px`,
top: `${top}px`,
};
case 'right':
return {
left: `${right}px`,
top: `${(top + bottom) / 2}px`,
};
case 'bottom':
case 'bottom-left':
case 'bottom-right':
default:
return {
left: `${(left + right) / 2}px`,
top: `${bottom}px`,
};
}
};
export const Tooltip = ({ children, hidden = false, placement, floating = false, triggerBounds, ...props }) => (
<div
className={createClassName(styles, 'tooltip', { hidden, placement, floating })}
style={floating ? getPositioningStyle(placement, triggerBounds) : {}}
{...props}
>
{children}
</div>
);
const TooltipContext = createContext();
export class TooltipContainer extends Component {
state = {
tooltip: null,
activeChild: null,
event: null,
placement: null,
};
showTooltip = (event, { content, placement = 'bottom', childIndex }) => {
const triggerBounds = event.target.getBoundingClientRect();
this.setState({
tooltip: (
<Tooltip floating placement={placement} triggerBounds={triggerBounds}>
{content}
</Tooltip>
),
activeChild: childIndex,
event,
placement,
content,
});
};
hideTooltip = () => {
this.setState({ tooltip: null });
};
UNSAFE_componentWillReceiveProps(props) {
if (this.state.tooltip) {
const activeChildren = props?.children?.props?.children[this.state.activeChild];
if (activeChildren && activeChildren.props.content !== this.state.content) {
this.showTooltip(this.state.event, {
content: activeChildren.props.content,
placement: this.state.placement,
childIndex: this.state.activeChild,
});
}
}
}
render({ children }) {
return (
<TooltipContext.Provider value={{ ...this.state, showTooltip: this.showTooltip, hideTooltip: this.hideTooltip }}>
{children}
<TooltipContext.Consumer>{({ tooltip }) => tooltip}</TooltipContext.Consumer>
</TooltipContext.Provider>
);
}
}
export const TooltipTrigger = ({ children, content, placement = '' }) => (
<TooltipContext.Consumer>
{({ showTooltip, hideTooltip }) =>
toChildArray(children).map((child, index) =>
cloneElement(child, {
onMouseEnter: (event) => showTooltip(event, { content, placement, childIndex: index }),
onMouseLeave: (event) => hideTooltip(event),
onFocusCapture: (event) => showTooltip(event, { content, placement, childIndex: index }),
onBlurCapture: (event) => hideTooltip(event),
content,
}),
)
}
</TooltipContext.Consumer>
);
export const withTooltip = (component) => {
const TooltipConnection = ({ tooltip, ...props }) => <Tooltip.Trigger content={tooltip}>{component(props)}</Tooltip.Trigger>;
TooltipConnection.displayName = `withTooltip(${component.displayName})`;
return TooltipConnection;
};
Tooltip.Container = TooltipContainer;
Tooltip.Trigger = TooltipTrigger;
export default Tooltip;

@ -0,0 +1,3 @@
export { default, default as Tooltip } from './Tooltip';
export { default as TooltipContainer } from './TooltipContainer';
export { default as TooltipTrigger } from './TooltipTrigger';

@ -0,0 +1,35 @@
import type * as uikit from '@rocket.chat/ui-kit';
import type { ComponentChild } from 'preact';
import type { TargetedEvent } from 'preact/compat';
import { memo, useCallback } from 'preact/compat';
import { MenuGroup, MenuPopover } from '../../../Menu';
import { usePerformAction } from '../Block';
import OverflowOption from './OverflowOption';
import OverflowTrigger from './OverflowTrigger';
type OverflowElementProps = uikit.OverflowElement & {
parser: uikit.SurfaceRenderer<ComponentChild>;
};
const OverflowElement = ({ actionId, confirm, options, parser }: OverflowElementProps) => {
const [performAction, performingAction] = usePerformAction(actionId);
const handleClick = useCallback(
async (value: TargetedEvent<HTMLElement, MouseEvent>) => {
await performAction({ value });
},
[performAction],
);
return (
<MenuPopover trigger={({ pop }) => <OverflowTrigger loading={performingAction} onClick={pop} />}>
<MenuGroup>
{Array.isArray(options) &&
options.map((option, i) => <OverflowOption key={i} {...option} confirm={confirm} parser={parser} onClick={handleClick} />)}
</MenuGroup>
</MenuPopover>
);
};
export default memo(OverflowElement);

@ -0,0 +1,41 @@
import type * as uikit from '@rocket.chat/ui-kit';
import type { ComponentChild } from 'preact';
import type { TargetedEvent } from 'preact/compat';
import { useCallback } from 'preact/compat';
import { MenuItem } from '../../../Menu';
type OverflowOptionProps = uikit.Option & {
confirm: boolean;
parser: uikit.SurfaceRenderer<ComponentChild>;
onClick: (value: string) => void;
};
const OverflowOption = ({ confirm, text, value, url, parser, onClick }: OverflowOptionProps) => {
const handleClick = useCallback(
async (event: TargetedEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();
if (confirm) {
// TODO
}
if (url) {
const newTab = window.open();
if (!newTab) {
throw new Error('Could not open new tab');
}
newTab.opener = null;
newTab.location = url;
return;
}
await onClick(value);
},
[confirm, onClick, url, value],
);
return <MenuItem onClick={handleClick}>{parser.renderTextObject(text, 0)}</MenuItem>;
};
export default OverflowOption;

@ -0,0 +1,33 @@
import type { TargetedEvent } from 'preact/compat';
import { useCallback } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../../../helpers/createClassName';
import KebabIcon from '../../../../icons/kebab.svg';
import { Button } from '../../../Button';
type OverflowTriggerProps = {
loading: boolean;
onClick: () => void;
};
const OverflowTrigger = ({ loading, onClick }: OverflowTriggerProps) => {
const handleMouseUp = useCallback(({ currentTarget }: TargetedEvent<HTMLElement>) => {
currentTarget.blur();
}, []);
return (
<Button
className={createClassName(styles, 'uikit-overflow__trigger')}
disabled={loading}
outline
secondary
onClick={onClick}
onMouseUp={handleMouseUp}
>
<KebabIcon width={20} height={20} />
</Button>
);
};
export default OverflowTrigger;

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

@ -1,94 +0,0 @@
import type * as uikit from '@rocket.chat/ui-kit';
import type { ComponentChild } from 'preact';
import type { TargetedEvent } from 'preact/compat';
import { memo, useCallback } from 'preact/compat';
import styles from './styles.scss';
import { createClassName } from '../../../../helpers/createClassName';
import KebabIcon from '../../../../icons/kebab.svg';
import { Button } from '../../../Button';
import Menu, { PopoverMenu } from '../../../Menu';
import { usePerformAction } from '../Block';
type OverflowTriggerProps = {
loading: boolean;
onClick: () => void;
};
const OverflowTrigger = ({ loading, onClick }: OverflowTriggerProps) => {
const handleMouseUp = useCallback(({ currentTarget }: TargetedEvent<HTMLElement>) => {
currentTarget.blur();
}, []);
return (
<Button
className={createClassName(styles, 'uikit-overflow__trigger')}
disabled={loading}
outline
secondary
onClick={onClick}
onMouseUp={handleMouseUp}
>
<KebabIcon width={20} height={20} />
</Button>
);
};
type OverflowOptionProps = uikit.Option & {
confirm: boolean;
parser: uikit.SurfaceRenderer<ComponentChild>;
onClick: (value: string) => void;
};
const OverflowOption = ({ confirm, text, value, url, parser, onClick }: OverflowOptionProps) => {
const handleClick = useCallback(
async (event: TargetedEvent<HTMLElement, MouseEvent>) => {
event.preventDefault();
if (confirm) {
// TODO
}
if (url) {
const newTab = window.open();
if (!newTab) {
throw new Error('Could not open new tab');
}
newTab.opener = null;
newTab.location = url;
return;
}
await onClick(value);
},
[confirm, onClick, url, value],
);
return <Menu.Item onClick={handleClick}>{parser.renderTextObject(text, 0)}</Menu.Item>;
};
type OverflowElementProps = uikit.OverflowElement & {
parser: uikit.SurfaceRenderer<ComponentChild>;
};
const OverflowElement = ({ actionId, confirm, options, parser }: OverflowElementProps) => {
const [performAction, performingAction] = usePerformAction(actionId);
const handleClick = useCallback(
async (value: TargetedEvent<HTMLElement, MouseEvent>) => {
await performAction({ value });
},
[performAction],
);
return (
<PopoverMenu trigger={({ pop }) => <OverflowTrigger loading={performingAction} onClick={pop} />}>
<Menu.Group>
{Array.isArray(options) &&
options.map((option, i) => <OverflowOption key={i} {...option} confirm={confirm} parser={parser} onClick={handleClick} />)}
</Menu.Group>
</PopoverMenu>
);
};
export default memo(OverflowElement);

@ -1,8 +1,8 @@
interface Visibility {
type Visibility = {
readonly hidden: boolean | undefined;
addListener: (f: (this: Document, ev: Event) => any) => void;
removeListener: (f: (this: Document, ev: Event) => any) => void;
}
};
export const visibility: Visibility = (() => {
if (typeof document.hidden !== 'undefined') {

@ -1,12 +1,16 @@
import { Livechat } from '../api';
import type { StoreState } from '../store';
import store from '../store';
class CustomFields {
static instance: CustomFields;
private _initiated = false;
private _started = false;
constructor() {
if (!CustomFields.instance) {
this._initiated = false;
this._started = false;
this._queue = {};
CustomFields.instance = this;
}
@ -31,7 +35,7 @@ class CustomFields {
store.off('change', this.handleStoreChange);
}
handleStoreChange([state]) {
handleStoreChange([state]: [StoreState]) {
const { user } = state;
const { _started } = CustomFields.instance;
@ -47,7 +51,7 @@ class CustomFields {
CustomFields.instance.processCustomFields();
}
addToQueue(key, value, overwrite) {
addToQueue(key: string, value: string, overwrite: boolean) {
const { customFieldsQueue } = store.state;
store.setState({
customFieldsQueue: {
@ -74,14 +78,16 @@ class CustomFields {
this.clearQueue();
}
setCustomField(key, value, overwrite = true) {
setCustomField(key: string, value: string, overwrite = true) {
if (!this._started) {
this.addToQueue(key, value, overwrite);
return;
}
const { token } = Livechat;
Livechat.sendCustomField({ token, key, value, overwrite });
if (token) {
void Livechat.sendCustomField({ token, key, value, overwrite });
}
}
}

@ -202,7 +202,7 @@ const api = {
clearWidgetData: async () => {
const { minimized, visible, undocked, expanded, businessUnit, ...initial } = initialState();
await store.setState(initial);
store.setState(initial);
},
setAgent: (agent: StoreState['defaultAgent']) => {

@ -1,43 +0,0 @@
import store from '../store';
import { supportedLocales } from '../supportedLocales';
/**
* To normalize Language String and return language code
* @param {String} languageString
*/
export const normalizeLanguageString = (languageString) => {
let [languageCode, countryCode] = languageString.split ? languageString.split(/[-_]/) : [];
if (!languageCode || languageCode.length !== 2) {
return 'en';
}
languageCode = languageCode.toLowerCase();
if (!countryCode || countryCode.length !== 2) {
countryCode = null;
} else {
countryCode = countryCode.toUpperCase();
}
return countryCode ? `${languageCode}-${countryCode}` : languageCode;
};
/**
* To get browser Language of user
*/
export const browserLanguage = () => navigator.userLanguage || navigator.language;
/**
* This is configured langauge
*/
export const configLanguage = () => {
const { config: { settings: { language } = {} } = {}, iframe: { language: iframeLanguage } = {} } = store.state;
return iframeLanguage || language;
};
export const getDateFnsLocale = () => {
let fullLanguage = configLanguage() || browserLanguage();
fullLanguage = fullLanguage.toLowerCase();
const [languageCode] = fullLanguage.split ? fullLanguage.split(/[-_]/) : [];
const locale = [fullLanguage, languageCode, 'en-US'].find((lng) => supportedLocales.indexOf(lng) > -1);
return import(`date-fns/locale/${locale}.js`).then((module) => module.default);
};

@ -0,0 +1,46 @@
import type { Locale } from 'date-fns';
import store from '../store';
import { supportedLocales } from '../supportedLocales';
/**
* To normalize Language String and return language code
*/
export const normalizeLanguageString = (languageString: string): string => {
let [languageCode, countryCode]: (string | undefined)[] = languageString.split?.(/[-_]/) ?? [];
if (languageCode?.length !== 2) {
return 'en';
}
languageCode = languageCode.toLowerCase();
if (countryCode?.length !== 2) {
countryCode = undefined;
} else {
countryCode = countryCode.toUpperCase();
}
return countryCode ? `${languageCode}-${countryCode}` : languageCode;
};
/**
* To get browser Language of user
*/
export const browserLanguage = (): string => navigator.language;
/**
* This is configured langauge
*/
export const configLanguage = (): string | undefined => {
const { iframe: { language: iframeLanguage } = {} } = store.state;
const language = (store.state.config?.settings as Record<string, unknown> | undefined)?.language as string | undefined;
return iframeLanguage || language;
};
export const getDateFnsLocale = async (): Promise<Locale> => {
let fullLanguage = configLanguage() || browserLanguage();
fullLanguage = fullLanguage.toLowerCase();
const [languageCode] = fullLanguage.split?.(/[-_]/) ?? [];
const locale = [fullLanguage, languageCode, 'en-US'].find((lng) => supportedLocales.indexOf(lng) > -1);
const { default: dateFnsLocale } = await import(`date-fns/locale/${locale}.js`);
return dateFnsLocale as Locale;
};

@ -32,7 +32,7 @@ export const sendMessageAction = async (_: string, action: ILivechatSendMessageA
}
if (agent && '_id' in agent) {
await store.setState({ agent });
store.setState({ agent });
parentCall('callback', 'assign-agent', normalizeAgent(agent));
}
@ -98,7 +98,7 @@ export const sendMessageExternalServiceAction = async (
);
if (agent && '_id' in agent) {
await store.setState({ agent });
store.setState({ agent });
parentCall('callback', 'assign-agent', normalizeAgent(agent));
}

@ -1,10 +1,10 @@
import type { ILivechatAgent, ILivechatTrigger, ILivechatTriggerAction, ILivechatTriggerType, Serialized } from '@rocket.chat/core-typings';
import { Livechat } from '../api';
import { processUnread } from './main';
import type { Agent } from '../definitions/agents';
import { upsert } from '../helpers/upsert';
import store from '../store';
import { processUnread } from './main';
type AgentPromise = { username: string } | Serialized<ILivechatAgent> | null;
@ -75,7 +75,7 @@ export const getAgent = async (triggerAction: ILivechatTriggerAction): Promise<A
};
export const upsertMessage = async (message: Record<string, unknown>) => {
await store.setState({
store.setState({
messages: upsert(
store.state.messages,
message,
@ -89,12 +89,12 @@ export const upsertMessage = async (message: Record<string, unknown>) => {
export const removeMessage = async (messageId: string) => {
const { messages } = store.state;
await store.setState({ messages: messages.filter(({ _id }) => _id !== messageId) });
store.setState({ messages: messages.filter(({ _id }) => _id !== messageId) });
};
export const removeTriggerMessage = async (messageId: string) => {
const { renderedTriggers } = store.state;
await store.setState({ renderedTriggers: renderedTriggers.filter(({ _id }) => _id !== messageId) });
store.setState({ renderedTriggers: renderedTriggers.filter(({ _id }) => _id !== messageId) });
};
export const hasTriggerCondition = (conditionName: ILivechatTriggerType) => (trigger: ILivechatTrigger) => {

@ -1,12 +1,12 @@
import { Livechat } from '../api';
import type { StoreState } from '../store';
import store from '../store';
const docActivityEvents = ['mousemove', 'mousedown', 'touchend', 'keydown'];
let timer;
const docActivityEvents = ['mousemove' as const, 'mousedown' as const, 'touchend' as const, 'keydown' as const];
let timer: ReturnType<typeof setTimeout> | undefined;
let initiated = false;
const awayTime = 300000;
let self;
let oldStatus;
let oldStatus: string | undefined;
const userPrensence = {
init() {
@ -15,7 +15,6 @@ const userPrensence = {
}
initiated = true;
self = this;
store.on('change', this.handleStoreChange);
},
@ -26,7 +25,9 @@ const userPrensence = {
},
stopTimer() {
timer && clearTimeout(timer);
if (timer) {
clearTimeout(timer);
}
},
startTimer() {
@ -34,13 +35,17 @@ const userPrensence = {
timer = setTimeout(this.setAway, awayTime);
},
handleStoreChange([state]) {
handleStoreChange([state]: [StoreState]) {
if (!initiated) {
return;
}
const { room, user } = state;
room && user ? self.startEvents() : self.stopEvents();
if (room && user) {
userPrensence.startEvents();
} else {
userPrensence.stopEvents();
}
},
startEvents() {
@ -61,7 +66,7 @@ const userPrensence = {
},
async setOnline() {
self.startTimer();
userPrensence.startTimer();
if (oldStatus === 'online') {
return;
}
@ -70,7 +75,7 @@ const userPrensence = {
},
async setAway() {
self.stopTimer();
userPrensence.stopTimer();
if (oldStatus === 'away') {
return;
}

@ -7,9 +7,9 @@ import { Button } from '../../components/Button';
import { Composer, ComposerAction, ComposerActions } from '../../components/Composer';
import { FilesDropTarget } from '../../components/FilesDropTarget';
import { FooterOptions, CharCounter } from '../../components/Footer';
import { Menu } from '../../components/Menu';
import { MenuGroup, MenuItem } from '../../components/Menu';
import { MessageList } from '../../components/Messages';
import { Screen } from '../../components/Screen';
import { Screen, ScreenContent, ScreenFooter } from '../../components/Screen';
import { createClassName } from '../../helpers/createClassName';
import ChangeIcon from '../../icons/change.svg';
import FinishIcon from '../../icons/finish.svg';
@ -149,7 +149,7 @@ class Chat extends Component {
{...props}
>
<FilesDropTarget inputRef={this.inputRef} overlayed overlayText={t('drop_here_to_upload_a_file')} onUpload={onUpload}>
<Screen.Content nopadding>
<ScreenContent nopadding>
<div className={createClassName(styles, 'chat__messages', { atBottom, loading })}>
<MessageList
ref={this.handleMessagesContainerRef}
@ -178,28 +178,28 @@ class Chat extends Component {
</Suspense>
)}
</div>
</Screen.Content>
<Screen.Footer
</ScreenContent>
<ScreenFooter
options={
options && !registrationRequired ? (
<FooterOptions>
<Menu.Group>
<MenuGroup>
{onChangeDepartment && (
<Menu.Item onClick={onChangeDepartment} icon={ChangeIcon}>
<MenuItem onClick={onChangeDepartment} icon={ChangeIcon}>
{t('change_department')}
</Menu.Item>
</MenuItem>
)}
{onRemoveUserData && (
<Menu.Item onClick={onRemoveUserData} icon={RemoveIcon}>
<MenuItem onClick={onRemoveUserData} icon={RemoveIcon}>
{t('forget_remove_my_data')}
</Menu.Item>
</MenuItem>
)}
{onFinishChat && (
<Menu.Item danger onClick={onFinishChat} icon={FinishIcon}>
<MenuItem danger onClick={onFinishChat} icon={FinishIcon}>
{t('finish_this_chat')}
</Menu.Item>
</MenuItem>
)}
</Menu.Group>
</MenuGroup>
</FooterOptions>
) : null
}
@ -244,7 +244,7 @@ class Chat extends Component {
limitTextLength={limitTextLength}
/>
)}
</Screen.Footer>
</ScreenFooter>
</FilesDropTarget>
</Screen>
);

@ -1,5 +1,5 @@
import type { TFunction } from 'i18next';
import type { FunctionalComponent } from 'preact';
import type { Ref } from 'preact';
import { useContext } from 'preact/hooks';
import { withTranslation } from 'react-i18next';
@ -9,7 +9,14 @@ import { canRenderMessage } from '../../helpers/canRenderMessage';
import { formatAgent } from '../../helpers/formatAgent';
import { StoreContext } from '../../store';
export const ChatConnector: FunctionalComponent<{ path: string; default: boolean; t: TFunction }> = ({ ref, t }) => {
type ChatConnectorProps = {
path: string;
default: boolean;
t: TFunction;
ref?: Ref<any>;
};
export const ChatConnector = ({ ref, t }: ChatConnectorProps) => {
const { theme } = useContext(ScreenContext);
const {
config: {

@ -43,7 +43,7 @@ const ChatWrapper = ({ children, rid }) => {
};
class ChatContainer extends Component {
state = {
innerState = {
room: null,
connectingAgent: false,
queueSpot: 0,
@ -53,16 +53,16 @@ class ChatContainer extends Component {
checkConnectingAgent = async () => {
const { connecting, queueInfo } = this.props;
const { connectingAgent, queueSpot, estimatedWaitTime } = this.state;
const { connectingAgent, queueSpot, estimatedWaitTime } = this.innerState;
const newConnecting = connecting;
const newQueueSpot = (queueInfo && queueInfo.spot) || 0;
const newEstimatedWaitTime = queueInfo && queueInfo.estimatedWaitTimeSeconds;
if (newConnecting !== connectingAgent || newQueueSpot !== queueSpot || newEstimatedWaitTime !== estimatedWaitTime) {
this.state.connectingAgent = newConnecting;
this.state.queueSpot = newQueueSpot;
this.state.estimatedWaitTime = newEstimatedWaitTime;
this.innerState.connectingAgent = newConnecting;
this.innerState.queueSpot = newQueueSpot;
this.innerState.estimatedWaitTime = newEstimatedWaitTime;
await this.handleQueueMessage(connecting, queueInfo);
await this.handleConnectingAgentAlert(newConnecting, await normalizeQueueAlert(queueInfo));
}
@ -70,9 +70,9 @@ class ChatContainer extends Component {
checkRoom = () => {
const { room } = this.props;
const { room: stateRoom } = this.state;
const { room: stateRoom } = this.innerState;
if (room && (!stateRoom || room._id !== stateRoom._id)) {
this.state.room = room;
this.innerState.room = room;
setTimeout(loadMessages, 500);
}
};
@ -341,14 +341,14 @@ class ChatContainer extends Component {
const { livechatQueueMessageId } = constants;
const { message: { text: msg, user: u } = {} } = queueInfo;
const { triggerQueueMessage } = this.state;
const { triggerQueueMessage } = this.innerState;
const { room } = this.props;
if (!room || !connecting || !msg || !triggerQueueMessage) {
return;
}
this.state.triggerQueueMessage = false;
this.innerState.triggerQueueMessage = false;
const { dispatch, messages } = this.props;
const ts = new Date();

@ -3,7 +3,7 @@ import { withTranslation } from 'react-i18next';
import styles from './styles.scss';
import { Button } from '../../components/Button';
import { ButtonGroup } from '../../components/ButtonGroup';
import Screen from '../../components/Screen';
import { Screen, ScreenContent, ScreenFooter } from '../../components/Screen';
import { createClassName } from '../../helpers/createClassName';
import Triggers from '../../lib/triggers';
@ -26,7 +26,7 @@ const ChatFinished = ({ title, greeting, message, onRedirectChat, t }: ChatFinis
return (
<Screen title={title} className={createClassName(styles, 'chat-finished')}>
<Screen.Content>
<ScreenContent>
<p className={createClassName(styles, 'chat-finished__greeting')}>{greeting || defaultGreeting}</p>
<p className={createClassName(styles, 'chat-finished__message')}>{message || defaultMessage}</p>
@ -35,8 +35,8 @@ const ChatFinished = ({ title, greeting, message, onRedirectChat, t }: ChatFinis
{t('new_chat')}
</Button>
</ButtonGroup>
</Screen.Content>
<Screen.Footer />
</ScreenContent>
<ScreenFooter />
</Screen>
);
};

@ -1,5 +1,4 @@
import type { TFunction } from 'i18next';
import type { FunctionalComponent } from 'preact';
import { useContext } from 'preact/hooks';
import { route } from 'preact-router';
import { withTranslation } from 'react-i18next';
@ -7,7 +6,13 @@ import { withTranslation } from 'react-i18next';
import ChatFinished from './component';
import { StoreContext } from '../../store';
const ChatFinishedContainer: FunctionalComponent<{ path: string; t: TFunction }> = ({ ref, t }) => {
type ChatFinishedContainerProps = {
ref?: any;
t: TFunction;
path: string;
};
const ChatFinishedContainer = ({ ref, t }: ChatFinishedContainerProps) => {
const {
config: {
messages: { conversationFinishedMessage: greeting, conversationFinishedText: message },

@ -6,7 +6,7 @@ import styles from './styles.scss';
import { Button } from '../../components/Button';
import { ButtonGroup } from '../../components/ButtonGroup';
import MarkdownBlock from '../../components/MarkdownBlock';
import Screen from '../../components/Screen';
import { Screen, ScreenContent, ScreenFooter } from '../../components/Screen';
import { createClassName } from '../../helpers/createClassName';
type GDPRProps = {
@ -25,7 +25,7 @@ class GDPR extends Component<GDPRProps> {
render = ({ title, consentText, instructions, t }: GDPRProps) => (
<Screen title={title} className={createClassName(styles, 'gdpr')}>
<Screen.Content>
<ScreenContent>
{consentText ? (
<p className={createClassName(styles, 'gdpr__consent-text')}>
<MarkdownBlock text={consentText} />
@ -51,8 +51,8 @@ class GDPR extends Component<GDPRProps> {
{t('i_agree')}
</Button>
</ButtonGroup>
</Screen.Content>
<Screen.Footer />
</ScreenContent>
<ScreenFooter />
</Screen>
);
}

@ -1,5 +1,5 @@
import type { TFunction } from 'i18next';
import type { FunctionalComponent } from 'preact';
import type { Ref } from 'preact';
import { useContext } from 'preact/hooks';
import { route } from 'preact-router';
import { withTranslation } from 'react-i18next';
@ -7,11 +7,16 @@ import { withTranslation } from 'react-i18next';
import GDPRAgreement from './component';
import { StoreContext } from '../../store';
const GDPRContainer: FunctionalComponent<{ t: TFunction }> = ({ ref, t }) => {
type GDPRContainerProps = {
t: TFunction;
ref?: Ref<any>;
};
const GDPRContainer = ({ ref, t }: GDPRContainerProps) => {
const { config: { messages: { dataProcessingConsentText: consentText = '' } = {} } = {}, dispatch } = useContext(StoreContext);
const handleAgree = async () => {
await dispatch({ gdpr: { accepted: true } });
dispatch({ gdpr: { accepted: true } });
route('/');
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save