forked from Ivasoft/mattermost-mobile
MM-48287 - Calls: Select and send emoji reactions (#6781)
* add emoji reactions in mobile
* use StyleSheet.create
* PR comment
* prefer no inline functions
* Revert "prefer no inline functions"
This reverts commit b3c4627dac.
This commit is contained in:
committed by
GitHub
parent
5dd3121bbc
commit
89b94d0188
@@ -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);
|
||||
|
||||
54
app/products/calls/components/emoji_button.tsx
Normal file
54
app/products/calls/components/emoji_button.tsx
Normal file
@@ -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<ViewStyle>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<Pressable
|
||||
android_ripple={androidRippleConfig}
|
||||
hitSlop={5}
|
||||
onPress={onPress}
|
||||
style={pressableStyle}
|
||||
>
|
||||
<Emoji
|
||||
emojiName={emojiName}
|
||||
size={24}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiButton;
|
||||
106
app/products/calls/components/reaction_bar.tsx
Normal file
106
app/products/calls/components/reaction_bar.tsx
Normal file
@@ -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 = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_lower_hand'}
|
||||
defaultMessage={'Lower hand'}
|
||||
style={[styles.buttonText, raisedHand ? styles.pressed : styles.unPressed]}
|
||||
/>);
|
||||
const RaiseHandText = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_raise_hand'}
|
||||
defaultMessage={'Raise hand'}
|
||||
style={[styles.buttonText, raisedHand ? styles.pressed : styles.unPressed]}
|
||||
/>);
|
||||
|
||||
const toggleRaiseHand = useCallback(() => {
|
||||
const whenRaisedHand = raisedHand || 0;
|
||||
if (whenRaisedHand > 0) {
|
||||
unraiseHand();
|
||||
} else {
|
||||
raiseHand();
|
||||
}
|
||||
}, [raisedHand]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Pressable
|
||||
style={[styles.button, Boolean(raisedHand) && styles.buttonPressed]}
|
||||
onPress={toggleRaiseHand}
|
||||
>
|
||||
<CompassIcon
|
||||
name={raisedHand ? 'hand-right-outline-off' : 'hand-right-outline'}
|
||||
size={24}
|
||||
style={[raisedHand ? styles.pressed : styles.unPressed]}
|
||||
/>
|
||||
{raisedHand ? LowerHandText : RaiseHandText}
|
||||
</Pressable>
|
||||
{
|
||||
predefinedReactions.map(([name, unified]) => (
|
||||
<EmojiButton
|
||||
key={name}
|
||||
emojiName={name}
|
||||
style={styles.button}
|
||||
onPress={() => sendReaction({name, unified})}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactionBar;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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<ViewStyle>;
|
||||
svgStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export default function RaisedHandIcon({width = 25, height = 27, style, svgStyle, ...props}: Props) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<Svg
|
||||
{...props}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox='0 0 25 27'
|
||||
style={svgStyle}
|
||||
>
|
||||
|
||||
<Path
|
||||
d='M24.6999 6.15117V21.873C24.6999 23.0918 24.2733 24.1074 23.4202 24.9199C22.5671 25.773 21.5515 26.1996 20.3733 26.1996H12.4515C11.2327 26.1996 10.1968 25.773 9.34365 24.9199L0.87334 16.2668L2.2749 14.9262C2.51865 14.723 2.80303 14.6215 3.12803 14.6215C3.37178 14.6215 3.59521 14.6824 3.79834 14.8043L8.42959 17.4855V4.5668C8.42959 4.11992 8.59209 3.73398 8.91709 3.40898C9.24209 3.08398 9.62803 2.92148 10.0749 2.92148C10.5218 2.92148 10.9077 3.08398 11.2327 3.40898C11.5577 3.73398 11.7202 4.11992 11.7202 4.5668V12.123H12.7562V1.82461C12.7562 1.37773 12.9187 1.01211 13.2437 0.727734C13.5687 0.402734 13.9546 0.240234 14.4015 0.240234C14.8483 0.240234 15.2343 0.402734 15.5593 0.727734C15.8843 1.01211 16.0468 1.37773 16.0468 1.82461V12.123H17.1437V2.92148C17.1437 2.47461 17.2858 2.08867 17.5702 1.76367C17.8952 1.43867 18.2812 1.27617 18.728 1.27617C19.1749 1.27617 19.5608 1.43867 19.8858 1.76367C20.2108 2.08867 20.3733 2.47461 20.3733 2.92148V12.123H21.4702V6.15117C21.4702 5.7043 21.6124 5.33867 21.8968 5.0543C22.2218 4.7293 22.6077 4.5668 23.0546 4.5668C23.5015 4.5668 23.8874 4.7293 24.2124 5.0543C24.5374 5.33867 24.6999 5.7043 24.6999 6.15117Z'
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ViewStyle>;
|
||||
svgStyle?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export default function UnraisedHandIcon({width = 24, height = 24, style, svgStyle, ...props}: Props) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<Svg
|
||||
{...props}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox='0 0 24 24'
|
||||
style={svgStyle}
|
||||
>
|
||||
|
||||
<Path
|
||||
d='M20.84 22.73L19.17 21.06C17.7 22.85 15.5 24 13 24C9.74 24 6.81 22 5.6 19L2.57 11.37C2.26 10.58 3 9.79 3.81 10.05L4.6 10.31C5.16 10.5 5.62 10.92 5.84 11.47L7.25 15H8V9.89L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73M14 1.25C14 .56 13.44 0 12.75 0S11.5 .56 11.5 1.25V8.3L14 10.8V1.25M21 16V5.75C21 5.06 20.44 4.5 19.75 4.5S18.5 5.06 18.5 5.75V12H17.5V2.75C17.5 2.06 16.94 1.5 16.25 1.5S15 2.06 15 2.75V11.8L20.83 17.63C20.94 17.11 21 16.56 21 16M10.5 3.25C10.5 2.56 9.94 2 9.25 2S8 2.56 8 3.25V4.8L10.5 7.3V3.25Z'
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_lower_hand'}
|
||||
defaultMessage={'Lower hand'}
|
||||
style={style.buttonText}
|
||||
/>);
|
||||
const RaiseHandText = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_raise_hand'}
|
||||
defaultMessage={'Raise hand'}
|
||||
style={style.buttonText}
|
||||
/>);
|
||||
const MuteText = (
|
||||
<FormattedText
|
||||
id={'mobile.calls_mute'}
|
||||
@@ -507,6 +467,7 @@ const CallScreen = ({
|
||||
{screenShareView}
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
<EmojiList reactionStream={currentCall.reactionStream}/>
|
||||
{showReactions && <ReactionBar raisedHand={myParticipant.raisedHand}/>}
|
||||
<View
|
||||
style={[style.buttons, isLandscape && style.buttonsLandscape, !showControls && style.buttonsLandscapeNoControls]}
|
||||
>
|
||||
@@ -550,7 +511,7 @@ const CallScreen = ({
|
||||
<CompassIcon
|
||||
name={'volume-high'}
|
||||
size={24}
|
||||
style={[style.buttonIcon, style.speakerphoneIcon, currentCall.speakerphoneOn && style.speakerphoneIconOn]}
|
||||
style={[style.buttonIcon, style.speakerphoneIcon, currentCall.speakerphoneOn && style.buttonOn]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_speaker'}
|
||||
@@ -560,16 +521,18 @@ const CallScreen = ({
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={style.button}
|
||||
onPress={toggleRaiseHand}
|
||||
onPress={toggleReactions}
|
||||
>
|
||||
<HandIcon
|
||||
fill={myParticipant.raisedHand ? 'rgb(255, 188, 66)' : theme.sidebarText}
|
||||
height={24}
|
||||
width={24}
|
||||
style={[style.buttonIcon, style.handIcon, myParticipant.raisedHand && style.handIconRaisedHand]}
|
||||
svgStyle={style.handIconSvgStyle}
|
||||
<CompassIcon
|
||||
name={'emoticon-happy-outline'}
|
||||
size={24}
|
||||
style={[style.buttonIcon, showReactions && style.buttonOn]}
|
||||
/>
|
||||
<FormattedText
|
||||
id={'mobile.calls_react'}
|
||||
defaultMessage={'React'}
|
||||
style={style.buttonText}
|
||||
/>
|
||||
{myParticipant.raisedHand ? LowerHandText : RaiseHandText}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={style.button}
|
||||
|
||||
@@ -111,6 +111,7 @@ export type CallsConnection = {
|
||||
raiseHand: () => void;
|
||||
unraiseHand: () => void;
|
||||
initializeVoiceTrack: () => void;
|
||||
sendReaction: (emoji: CallReactionEmoji) => void;
|
||||
}
|
||||
|
||||
export type ServerCallsConfig = {
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user