Skip to content

Commit

Permalink
feat: add emoji picker to in-call reactions
Browse files Browse the repository at this point in the history
  • Loading branch information
waseemansar committed Feb 6, 2025
1 parent 3d8ed24 commit 497e7f8
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 83 deletions.
2 changes: 2 additions & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@
"callReactionButtonsAriaLabel": "Emoji selection bar",
"callReactions": "Reactions",
"callReactionsAriaLabel": "Emoji {emoji} from {from}",
"callReactionEmojiPickerButtonAriaLabel": "Open emoji picker",
"callReactionEmojiPickerAriaLabel": "Emoji picker",
"callStateCbr": "Constant Bit Rate",
"callStateConnecting": "Connecting…",
"callStateIncoming": "Calling…",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,58 +21,133 @@ import {CSSObject} from '@emotion/react';

import {media} from '@wireapp/react-ui-kit';

export const emojisBarWrapperStyles: CSSObject = {
position: 'absolute',
bottom: '130%',
left: '50%',
display: 'grid',
padding: '0.4rem',
borderRadius: '12px',
backgroundColor: 'var(--inactive-call-button-bg)',
boxShadow: '0px 7px 15px 0 #0000004d',
gap: '0.5rem',
gridTemplateColumns: 'repeat(3, 1fr)',
transform: 'translateX(-50%)',

[media.tablet]: {
transform: 'none',
left: 'auto',
right: 0,
},

'&::after': {
export const styles: {
emojisBar: CSSObject;
button: CSSObject;
picker: CSSObject;
} = {
emojisBar: {
position: 'absolute',
bottom: '-0.5rem',
bottom: '130%',
left: '50%',
width: 0,
height: 0,
borderTop: '0.5rem solid var(--inactive-call-button-bg)',
borderRight: '0.5rem solid transparent',
borderLeft: '0.5rem solid transparent',
content: '""',
transform: 'translateX(-50%)',
display: 'grid',
gap: '0.5rem',
gridTemplateColumns: 'repeat(3, 1fr)',
borderRadius: '12px',
backgroundColor: 'var(--inactive-call-button-bg)',
boxShadow: '0px 7px 15px 0 #0000004d',
padding: '0.5rem',

[media.tablet]: {
transform: 'none',
left: 'auto',
right: '0.625rem',
right: 0,
},

'&::after': {
position: 'absolute',
bottom: '-0.5rem',
left: '50%',
width: 0,
height: 0,
borderTop: '0.5rem solid var(--inactive-call-button-bg)',
borderRight: '0.5rem solid transparent',
borderLeft: '0.5rem solid transparent',
content: '""',
transform: 'translateX(-50%)',

[media.tablet]: {
transform: 'none',
left: 'auto',
right: '0.625rem',
},
},
},
};
button: {
backgroundColor: 'transparent',
border: 0,
padding: '0.5rem',
borderRadius: '1rem',
fontSize: '1.5rem',

export const emojisBarButtonStyles: CSSObject = {
backgroundColor: 'transparent',
border: 0,
padding: '0.5rem',
borderRadius: '1rem',
fontSize: '1.5rem',
'&:disabled': {
cursor: 'not-allowed',
opacity: 0.5,
},

'&:disabled': {
cursor: 'not-allowed',
opacity: 0.5,
'&:hover': {
backgroundColor: 'var(--inactive-call-button-hover-bg)',
},
},
picker: {
position: 'absolute',
bottom: '130%',
right: 0,
transform: 'translateX(15%)',
borderRadius: '12px',
backgroundColor: 'var(--inactive-call-button-bg)',
boxShadow: '0px 7px 15px 0 #0000004d',
padding: '0.5rem',

[media.tablet]: {
transform: 'none',
},

'&:hover': {
backgroundColor: 'var(--inactive-call-button-hover-bg)',
[media.mobile]: {
transform: 'translateX(26%)',
},

'&::after': {
position: 'absolute',
bottom: '-0.5rem',
right: '18%',
width: 0,
height: 0,
borderTop: '0.5rem solid var(--inactive-call-button-bg)',
borderRight: '0.5rem solid transparent',
borderLeft: '0.5rem solid transparent',
content: '""',

[media.tablet]: {
right: '0.625rem',
},

[media.mobile]: {
right: '30%',
},
},

'& .EmojiPickerReact': {
borderStyle: 'none !important',
backgroundColor: 'var(--message-actions-background) !important',
boxShadow: 'none',

'body.theme-dark &': {
boxShadow: 'none',
},
},

'& .EmojiPickerReact .epr-preview': {
borderTop: '1px solid var(--message-actions-border-hover)',
},

'& .EmojiPickerReact li.epr-emoji-category > .epr-emoji-category-label': {
backgroundColor: 'var(--message-actions-background)',
},

'& .EmojiPickerReact .epr-search-container input': {
'body.theme-dark &': {
border: '1px solid var(--gray-70)',
borderRadius: '12px',
background: 'var(--gray-100)',
},
},

'& .EmojiPickerReact button.epr-emoji': {
'&:hover > *, &:focus > *, &:focus-visible > *': {
backgroundColor: 'var(--message-actions-background-hover)',
},
},
},
};
107 changes: 79 additions & 28 deletions src/script/components/calling/VideoControls/EmojisBar/EmojisBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,108 @@
*
*/

import React, {useState} from 'react';
import {useEffect, useRef, useState} from 'react';

import EmojiPicker, {EmojiClickData, EmojiStyle} from 'emoji-picker-react';

import {CallingRepository} from 'src/script/calling/CallingRepository';
import {t} from 'Util/LocalizerUtil';

import {emojisBarButtonStyles, emojisBarWrapperStyles} from './EmojisBar.styles';
import {styles} from './EmojisBar.styles';

const EMOJIS_LIST = ['👍', '🎉', '❤️', '😂', '😮', '👏', '🤔', '😢', '👎'];
const EMOJIS_LIST = ['👍', '🎉', '❤️', '😂', '😮', '👏', '🤔', '😢'];

export interface EmojisBarProps {
onEmojiClick: (emoji: string) => void;
ref: React.RefObject<HTMLDivElement>;
onPickerEmojiClick: () => void;
detachedWindow?: Window | null;
}

export const EmojisBar = ({onEmojiClick, ref}: EmojisBarProps) => {
export const EmojisBar = ({onEmojiClick, onPickerEmojiClick, detachedWindow}: EmojisBarProps) => {
const emojisBarRef = useRef<HTMLDivElement>(null);

const [disabledEmojis, setDisabledEmojis] = useState<string[]>([]);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);

const handleEmojiClick = (selectedEmoji: string) => {
setDisabledEmojis(prev => [...prev, selectedEmoji]);

onEmojiClick(selectedEmoji);

setTimeout(() => {
setDisabledEmojis(prev => [...prev].filter(emoji => emoji !== selectedEmoji));
setDisabledEmojis(prev => prev.filter(emoji => emoji !== selectedEmoji));
}, CallingRepository.EMOJI_TIME_OUT_DURATION);
};

const handlePickerEmojiClick = (emojiData: EmojiClickData) => {
onEmojiClick(emojiData.emoji);
onPickerEmojiClick();
};

const handleClickOutside = (event: MouseEvent) => {
if (emojisBarRef.current && !emojisBarRef.current.contains(event.target as Node)) {
onPickerEmojiClick();
}
};

useEffect(() => {
if (detachedWindow) {
detachedWindow.document.addEventListener('mousedown', handleClickOutside);
return () => {
detachedWindow.document.removeEventListener('mousedown', handleClickOutside);
};
}

return () => {};
}, [detachedWindow]);

return (
<div
ref={ref}
role="toolbar"
data-uie-name="video-controls-emojis-bar"
aria-label={t('callReactionButtonsAriaLabel')}
css={emojisBarWrapperStyles}
>
{EMOJIS_LIST.map(emoji => {
const isDisabled = disabledEmojis.includes(emoji);
return (
<div ref={emojisBarRef}>
{showEmojiPicker ? (
<div
role="dialog"
data-uie-name="video-controls-emojis-picker"
aria-label={t('callReactionEmojiPickerAriaLabel')}
css={styles.picker}
>
<EmojiPicker emojiStyle={EmojiStyle.NATIVE} onEmojiClick={handlePickerEmojiClick} />
</div>
) : (
<div
role="toolbar"
data-uie-name="video-controls-emojis-bar"
aria-label={t('callReactionButtonsAriaLabel')}
css={styles.emojisBar}
>
{EMOJIS_LIST.map(emoji => {
const isDisabled = disabledEmojis.includes(emoji);
return (
<button
aria-label={t('callReactionButtonAriaLabel', {emoji})}
data-uie-name="video-controls-emoji"
data-uie-value={emoji}
key={emoji}
disabled={isDisabled}
onClick={() => handleEmojiClick(emoji)}
css={styles.button}
>
{emoji}
</button>
);
})}
<button
aria-label={t('callReactionButtonAriaLabel', {emoji})}
data-uie-name="video-controls-emoji"
data-uie-value={emoji}
key={emoji}
disabled={isDisabled}
onClick={() => handleEmojiClick(emoji)}
css={emojisBarButtonStyles}
>
{emoji}
</button>
);
})}
aria-label={t('callReactionEmojiPickerButtonAriaLabel')}
data-uie-name="call-reaction-emoji-picker-button"
data-uie-value="open-emoji-picker"
className="icon-more font-size-sm"
onClick={event => {
event.stopPropagation();
setShowEmojiPicker(prev => !prev);
}}
css={styles.button}
></button>
</div>
)}
</div>
);
};
27 changes: 16 additions & 11 deletions src/script/components/calling/VideoControls/VideoControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,18 @@
*
*/

import React, {useRef, useState} from 'react';
import React, {useState} from 'react';

import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums';
import classNames from 'classnames';
import {container} from 'tsyringe';

import {CALL_TYPE} from '@wireapp/avs';
import {EmojiIcon, GridIcon, QUERY, RaiseHandIcon, MoreIcon} from '@wireapp/react-ui-kit';
import {EmojiIcon, GridIcon, MoreIcon, QUERY, RaiseHandIcon} from '@wireapp/react-ui-kit';
import {WebAppEvents} from '@wireapp/webapp-events';

import * as Icon from 'Components/Icon';
import {useActiveWindowMatchMedia} from 'Hooks/useActiveWindowMatchMedia';
import {useClickOutside} from 'Hooks/useClickOutside';
import {useUserPropertyValue} from 'Hooks/useUserProperty';
import {Call} from 'src/script/calling/Call';
import {CallingViewMode, CallState} from 'src/script/calling/CallState';
Expand Down Expand Up @@ -135,9 +134,6 @@ export const VideoControls: React.FC<VideoControlsProps> = ({

const {is1to1: is1to1Conversation} = useKoSubscribableChildren(conversation, ['is1to1']);

const emojiBarRef = useRef<HTMLDivElement | null>(null);
const emojiBarToggleButtonRef = useRef<HTMLButtonElement | null>(null);

const {blurredVideoStream} = useKoSubscribableChildren(selfParticipant, ['blurredVideoStream']);
const hasBlurredBackground = !!blurredVideoStream;

Expand All @@ -147,8 +143,6 @@ export const VideoControls: React.FC<VideoControlsProps> = ({

const {viewMode, detachedWindow} = useKoSubscribableChildren(callState, ['viewMode', 'detachedWindow']);

useClickOutside(emojiBarRef, () => setShowEmojisBar(false), emojiBarToggleButtonRef, detachedWindow?.document);

const {isVideoCallingEnabled} = useKoSubscribableChildren(teamState, ['isVideoCallingEnabled']);

const {
Expand Down Expand Up @@ -648,7 +642,13 @@ export const VideoControls: React.FC<VideoControlsProps> = ({
<div css={moreControlsWrapperStyles}>
{!isDesktop && (
<li className="video-controls__item">
{showEmojisBar && <EmojisBar onEmojiClick={handleEmojiClick} ref={emojiBarRef} />}
{showEmojisBar && (
<EmojisBar
onEmojiClick={handleEmojiClick}
onPickerEmojiClick={() => setShowEmojisBar(false)}
detachedWindow={detachedWindow}
/>
)}
<button
title={t('callMenuMoreInteractions')}
className={classNames(
Expand Down Expand Up @@ -750,9 +750,14 @@ export const VideoControls: React.FC<VideoControlsProps> = ({

{isInCallReactionsEnable && (
<li className="video-controls__item">
{showEmojisBar && <EmojisBar onEmojiClick={handleEmojiClick} ref={emojiBarRef} />}
{showEmojisBar && (
<EmojisBar
onEmojiClick={handleEmojiClick}
onPickerEmojiClick={() => setShowEmojisBar(false)}
detachedWindow={detachedWindow}
/>
)}
<button
ref={emojiBarToggleButtonRef}
title={t('callReactions')}
className={classNames('video-controls__button_primary', {active: showEmojisBar})}
onClick={() => setShowEmojisBar(prev => !prev)}
Expand Down
Loading

0 comments on commit 497e7f8

Please sign in to comment.