diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts index 214a835f47..e30aea07c0 100644 --- a/app/products/calls/actions/calls.ts +++ b/app/products/calls/actions/calls.ts @@ -35,6 +35,7 @@ import type { ApiResp, Call, CallParticipant, + CallReactionEmoji, CallsConnection, ServerCallState, ServerChannelState, @@ -291,6 +292,12 @@ export const unraiseHand = () => { } }; +export const sendReaction = (emoji: CallReactionEmoji) => { + if (connection) { + connection.sendReaction(emoji); + } +}; + export const setSpeakerphoneOn = (speakerphoneOn: boolean) => { InCallManager.setSpeakerphoneOn(speakerphoneOn); setSpeakerPhone(speakerphoneOn); diff --git a/app/products/calls/components/emoji_button.tsx b/app/products/calls/components/emoji_button.tsx new file mode 100644 index 0000000000..b25127348a --- /dev/null +++ b/app/products/calls/components/emoji_button.tsx @@ -0,0 +1,54 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import { + Platform, + Pressable, + PressableAndroidRippleConfig, + PressableStateCallbackType, + StyleProp, + ViewStyle, +} from 'react-native'; + +import Emoji from '@components/emoji'; + +type Props = { + emojiName: string; + onPress: () => void; + style?: StyleProp; +} + +const pressedStyle = ({pressed}: PressableStateCallbackType) => { + let opacity = 1; + if (Platform.OS === 'ios' && pressed) { + opacity = 0.5; + } + + return [{opacity}]; +}; + +const androidRippleConfig: PressableAndroidRippleConfig = {borderless: true, radius: 24, color: '#FFF'}; + +const EmojiButton = ({emojiName, onPress, style}: Props) => { + const pressableStyle = useCallback((pressed: PressableStateCallbackType) => ([ + pressedStyle(pressed), + style, + ]), [style]); + + return ( + + + + ); +}; + +export default EmojiButton; diff --git a/app/products/calls/components/reaction_bar.tsx b/app/products/calls/components/reaction_bar.tsx new file mode 100644 index 0000000000..d4046262bf --- /dev/null +++ b/app/products/calls/components/reaction_bar.tsx @@ -0,0 +1,106 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {useCallback} from 'react'; +import {Pressable, StyleSheet, View} from 'react-native'; + +import {raiseHand, unraiseHand} from '@calls/actions'; +import {sendReaction} from '@calls/actions/calls'; +import EmojiButton from '@calls/components/emoji_button'; +import CompassIcon from '@components/compass_icon'; +import FormattedText from '@components/formatted_text'; + +const styles = StyleSheet.create({ + container: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'space-between', + backgroundColor: 'rgba(255,255,255,0.16)', + width: '100%', + height: 64, + paddingLeft: 16, + paddingRight: 16, + }, + button: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + backgroundColor: 'rgba(255,255,255,0.08)', + borderRadius: 30, + height: 48, + maxWidth: 160, + paddingLeft: 10, + paddingRight: 10, + }, + buttonPressed: { + backgroundColor: 'rgba(245, 171, 0, 0.24)', + }, + unPressed: { + color: 'white', + }, + pressed: { + color: '#F5AB00', + }, + buttonText: { + marginLeft: 8, + }, +}); + +const predefinedReactions = [['+1', '1F44D'], ['clap', '1F44F'], ['joy', '1F602'], ['heart', '2764-FE0F']]; + +interface Props { + raisedHand: number; +} + +const ReactionBar = ({raisedHand}: Props) => { + const LowerHandText = ( + ); + const RaiseHandText = ( + ); + + const toggleRaiseHand = useCallback(() => { + const whenRaisedHand = raisedHand || 0; + if (whenRaisedHand > 0) { + unraiseHand(); + } else { + raiseHand(); + } + }, [raisedHand]); + + return ( + + + + {raisedHand ? LowerHandText : RaiseHandText} + + { + predefinedReactions.map(([name, unified]) => ( + sendReaction({name, unified})} + /> + )) + } + + ); +}; + +export default ReactionBar; diff --git a/app/products/calls/connection/connection.ts b/app/products/calls/connection/connection.ts index f0a71f04a0..e6bb5de328 100644 --- a/app/products/calls/connection/connection.ts +++ b/app/products/calls/connection/connection.ts @@ -21,7 +21,7 @@ import {logError, logDebug, logWarning} from '@utils/log'; import Peer from './simple-peer'; import {WebSocketClient, wsReconnectionTimeoutErr} from './websocket_client'; -import type {CallsConnection} from '@calls/types/calls'; +import type {CallReactionEmoji, CallsConnection} from '@calls/types/calls'; const peerConnectTimeout = 5000; @@ -166,6 +166,14 @@ export async function newConnection( } }; + const sendReaction = (emoji: CallReactionEmoji) => { + if (ws) { + ws.send('react', { + data: JSON.stringify(emoji), + }); + } + }; + ws.on('error', (err: Event) => { logDebug('calls: ws error', err); if (err === wsReconnectionTimeoutErr) { @@ -281,6 +289,7 @@ export async function newConnection( waitForPeerConnection, raiseHand, unraiseHand, + sendReaction, initializeVoiceTrack, }; diff --git a/app/products/calls/icons/raised_hand_icon.tsx b/app/products/calls/icons/raised_hand_icon.tsx deleted file mode 100644 index fcd5d6fda9..0000000000 --- a/app/products/calls/icons/raised_hand_icon.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; -import Svg, {Path} from 'react-native-svg'; - -type Props = { - className?: string; - width?: number; - height?: number; - fill?: string; - style?: StyleProp; - svgStyle?: StyleProp; -} - -export default function RaisedHandIcon({width = 25, height = 27, style, svgStyle, ...props}: Props) { - return ( - - - - - - - ); -} - diff --git a/app/products/calls/icons/unraised_hand_icon.tsx b/app/products/calls/icons/unraised_hand_icon.tsx deleted file mode 100644 index 4544bcb682..0000000000 --- a/app/products/calls/icons/unraised_hand_icon.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; -import Svg, {Path} from 'react-native-svg'; - -type Props = { - className?: string; - width?: number; - height?: number; - fill?: string; - style?: StyleProp; - svgStyle?: StyleProp; -} - -export default function UnraisedHandIcon({width = 24, height = 24, style, svgStyle, ...props}: Props) { - return ( - - - - - - - ); -} - diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index c17d3984dc..4a9f92ef3a 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -18,22 +18,14 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {RTCView} from 'react-native-webrtc'; import {appEntry} from '@actions/remote/entry'; -import { - leaveCall, - muteMyself, - raiseHand, - setSpeakerphoneOn, - unmuteMyself, - unraiseHand, -} from '@calls/actions'; +import {leaveCall, muteMyself, setSpeakerphoneOn, unmuteMyself} from '@calls/actions'; import CallAvatar from '@calls/components/call_avatar'; import CallDuration from '@calls/components/call_duration'; import EmojiList from '@calls/components/emoji_list'; import PermissionErrorBar from '@calls/components/permission_error_bar'; +import ReactionBar from '@calls/components/reaction_bar'; import UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper'; import {usePermissionsChecker} from '@calls/hooks'; -import RaisedHandIcon from '@calls/icons/raised_hand_icon'; -import UnraisedHandIcon from '@calls/icons/unraised_hand_icon'; import {CallParticipant, CurrentCall} from '@calls/types/calls'; import {sortParticipants} from '@calls/utils'; import CompassIcon from '@components/compass_icon'; @@ -175,37 +167,22 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ mute: { flexDirection: 'column', alignItems: 'center', - padding: 30, + padding: 24, backgroundColor: '#3DB887', borderRadius: 20, marginBottom: 10, marginTop: 20, - marginLeft: 10, - marginRight: 10, + marginLeft: 16, + marginRight: 16, }, muteMuted: { backgroundColor: 'rgba(255,255,255,0.16)', }, - handIcon: { - borderRadius: 34, - padding: 34, - margin: 10, - overflow: 'hidden', - backgroundColor: 'rgba(255,255,255,0.12)', - }, - handIconRaisedHand: { - backgroundColor: 'rgba(255, 188, 66, 0.16)', - }, - handIconSvgStyle: { - position: 'relative', - top: -12, - right: 13, - }, speakerphoneIcon: { color: theme.sidebarText, backgroundColor: 'rgba(255,255,255,0.12)', }, - speakerphoneIconOn: { + buttonOn: { color: 'black', backgroundColor: 'white', }, @@ -276,6 +253,7 @@ const CallScreen = ({ const {width, height} = useWindowDimensions(); usePermissionsChecker(micPermissionsGranted); const [showControlsInLandscape, setShowControlsInLandscape] = useState(false); + const [showReactions, setShowReactions] = useState(false); const style = getStyleSheet(theme); const isLandscape = width > height; @@ -308,14 +286,9 @@ const CallScreen = ({ } }, [myParticipant?.muted]); - const toggleRaiseHand = useCallback(() => { - const raisedHand = myParticipant?.raisedHand || 0; - if (raisedHand > 0) { - unraiseHand(); - } else { - raiseHand(); - } - }, [myParticipant?.raisedHand]); + const toggleReactions = useCallback(() => { + setShowReactions((prev) => !prev); + }, [setShowReactions]); const toggleSpeakerPhone = useCallback(() => { setSpeakerphoneOn(!currentCall?.speakerphoneOn); @@ -458,19 +431,6 @@ const CallScreen = ({ ); } - const HandIcon = myParticipant.raisedHand ? UnraisedHandIcon : RaisedHandIcon; - const LowerHandText = ( - ); - const RaiseHandText = ( - ); const MuteText = ( } + {showReactions && } @@ -550,7 +511,7 @@ const CallScreen = ({ - + - {myParticipant.raisedHand ? LowerHandText : RaiseHandText} void; unraiseHand: () => void; initializeVoiceTrack: () => void; + sendReaction: (emoji: CallReactionEmoji) => void; } export type ServerCallsConfig = { diff --git a/package-lock.json b/package-lock.json index f58a8b054f..15c25b6ced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@formatjs/intl-numberformat": "8.2.0", "@formatjs/intl-pluralrules": "5.1.4", "@formatjs/intl-relativetimeformat": "11.1.4", - "@mattermost/compass-icons": "0.1.30", + "@mattermost/compass-icons": "0.1.31", "@mattermost/react-native-emm": "1.3.3", "@mattermost/react-native-network-client": "1.0.0", "@mattermost/react-native-paste-input": "0.5.1", @@ -3176,9 +3176,9 @@ } }, "node_modules/@mattermost/compass-icons": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.30.tgz", - "integrity": "sha512-PmY0ak1IySORkBMgXqUMjdOjEF935FVo8/d42m1pz2CVt4oz02PQHoMsnlfU3Cf+w4J83fbA4JCywIGz7LZnLg==" + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.31.tgz", + "integrity": "sha512-/2oY/JhRAAiQXsc9QFt/i/zFQHMICG7nBfOZiyYfmJOTFIwn28IxZA66ohz+qrQlCzLOXIJuwPJLmx/7CMiSHw==" }, "node_modules/@mattermost/react-native-emm": { "version": "1.3.3", @@ -24008,9 +24008,9 @@ } }, "@mattermost/compass-icons": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.30.tgz", - "integrity": "sha512-PmY0ak1IySORkBMgXqUMjdOjEF935FVo8/d42m1pz2CVt4oz02PQHoMsnlfU3Cf+w4J83fbA4JCywIGz7LZnLg==" + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/@mattermost/compass-icons/-/compass-icons-0.1.31.tgz", + "integrity": "sha512-/2oY/JhRAAiQXsc9QFt/i/zFQHMICG7nBfOZiyYfmJOTFIwn28IxZA66ohz+qrQlCzLOXIJuwPJLmx/7CMiSHw==" }, "@mattermost/react-native-emm": { "version": "1.3.3", diff --git a/package.json b/package.json index 01b6b97ccc..d3024cb734 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@formatjs/intl-numberformat": "8.2.0", "@formatjs/intl-pluralrules": "5.1.4", "@formatjs/intl-relativetimeformat": "11.1.4", - "@mattermost/compass-icons": "0.1.30", + "@mattermost/compass-icons": "0.1.31", "@mattermost/react-native-emm": "1.3.3", "@mattermost/react-native-network-client": "1.0.0", "@mattermost/react-native-paste-input": "0.5.1",