forked from Ivasoft/mattermost-mobile
MM-48286 - Calls: Emoji reactions, display incoming reactions (#6780)
This commit is contained in:
committed by
GitHub
parent
2ebbd299ff
commit
a300cc651e
@@ -18,6 +18,7 @@ import {
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserMuted,
|
||||
handleCallUserRaiseHand,
|
||||
handleCallUserReacted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserUnraiseHand,
|
||||
handleCallUserVoiceOff,
|
||||
@@ -393,6 +394,9 @@ export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
|
||||
case WebsocketEvents.CALLS_CALL_END:
|
||||
handleCallEnded(serverUrl, msg);
|
||||
break;
|
||||
case WebsocketEvents.CALLS_USER_REACTED:
|
||||
handleCallUserReacted(serverUrl, msg);
|
||||
break;
|
||||
|
||||
case WebsocketEvents.GROUP_RECEIVED:
|
||||
handleGroupReceivedEvent(serverUrl, msg);
|
||||
|
||||
@@ -14,4 +14,7 @@ const RequiredServer = {
|
||||
|
||||
const PluginId = 'com.mattermost.calls';
|
||||
|
||||
export default {RequiredServer, RefreshConfigMillis, PluginId};
|
||||
export const REACTION_TIMEOUT = 10000;
|
||||
export const REACTION_LIMIT = 20;
|
||||
|
||||
export default {RequiredServer, RefreshConfigMillis, PluginId, REACTION_TIMEOUT};
|
||||
|
||||
@@ -66,6 +66,7 @@ const WebsocketEvents = {
|
||||
CALLS_SCREEN_OFF: `custom_${Calls.PluginId}_user_screen_off`,
|
||||
CALLS_USER_RAISE_HAND: `custom_${Calls.PluginId}_user_raise_hand`,
|
||||
CALLS_USER_UNRAISE_HAND: `custom_${Calls.PluginId}_user_unraise_hand`,
|
||||
CALLS_USER_REACTED: `custom_${Calls.PluginId}_user_reacted`,
|
||||
GROUP_RECEIVED: 'received_group',
|
||||
GROUP_MEMBER_ADD: 'group_member_add',
|
||||
GROUP_MEMBER_DELETE: 'group_member_delete',
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
import React, {useMemo} from 'react';
|
||||
import {View, StyleSheet, Text, Platform} from 'react-native';
|
||||
|
||||
import {CallReactionEmoji} from '@calls/types/calls';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Emoji from '@components/emoji';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
@@ -16,6 +18,7 @@ type Props = {
|
||||
muted?: boolean;
|
||||
sharingScreen?: boolean;
|
||||
raisedHand?: boolean;
|
||||
reaction?: CallReactionEmoji;
|
||||
size?: 'm' | 'l';
|
||||
}
|
||||
|
||||
@@ -77,49 +80,49 @@ const getStyleSheet = ({volume, muted, size}: { volume: number; muted?: boolean;
|
||||
},
|
||||
),
|
||||
},
|
||||
raisedHand: {
|
||||
reaction: {
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
top: 0,
|
||||
right: -5,
|
||||
backgroundColor: 'black',
|
||||
borderColor: 'black',
|
||||
borderRadius,
|
||||
padding,
|
||||
borderWidth: 2,
|
||||
width: widthHeight,
|
||||
height: widthHeight,
|
||||
borderRadius,
|
||||
padding,
|
||||
backgroundColor: 'black',
|
||||
borderColor: 'black',
|
||||
borderWidth: 2,
|
||||
fontSize: smallIcon ? 10 : 12,
|
||||
},
|
||||
raisedHand: {
|
||||
backgroundColor: 'white',
|
||||
right: -5,
|
||||
...Platform.select(
|
||||
{
|
||||
android: {
|
||||
paddingLeft: 4,
|
||||
paddingTop: 2,
|
||||
paddingLeft: 5,
|
||||
paddingTop: 3,
|
||||
color: 'rgb(255, 188, 66)',
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
screenSharing: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: -5,
|
||||
width: widthHeight,
|
||||
height: widthHeight,
|
||||
borderRadius,
|
||||
padding: padding + 1,
|
||||
backgroundColor: '#D24B4E',
|
||||
borderColor: 'black',
|
||||
borderWidth: 2,
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
textAlignVertical: 'center',
|
||||
overflow: 'hidden',
|
||||
paddingTop: Platform.select({ios: 3}),
|
||||
},
|
||||
emoji: {
|
||||
paddingLeft: 1,
|
||||
paddingTop: Platform.select({ios: 2, default: 1}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const CallAvatar = ({userModel, volume, serverUrl, sharingScreen, size, muted, raisedHand}: Props) => {
|
||||
const CallAvatar = ({userModel, volume, serverUrl, sharingScreen, size, muted, raisedHand, reaction}: Props) => {
|
||||
const style = useMemo(() => getStyleSheet({volume, muted, size}), [volume, muted, size]);
|
||||
const profileSize = size === 'm' || !size ? 40 : 72;
|
||||
const iconSize = size === 'm' || !size ? 12 : 16;
|
||||
@@ -132,17 +135,29 @@ const CallAvatar = ({userModel, volume, serverUrl, sharingScreen, size, muted, r
|
||||
<CompassIcon
|
||||
name={'monitor'}
|
||||
size={iconSize}
|
||||
style={style.screenSharing}
|
||||
style={[style.reaction, style.screenSharing]}
|
||||
/>
|
||||
);
|
||||
} else if (raisedHand) {
|
||||
topRightIcon = (
|
||||
<Text style={style.raisedHand}>
|
||||
<Text style={[style.reaction, style.raisedHand]}>
|
||||
{'✋'}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
// An emoji will override the top right indicator.
|
||||
if (reaction) {
|
||||
topRightIcon = (
|
||||
<View style={[style.reaction, style.emoji]}>
|
||||
<Emoji
|
||||
emojiName={reaction.name}
|
||||
size={iconSize - 3}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const profile = userModel ? (
|
||||
<ProfilePicture
|
||||
author={userModel}
|
||||
|
||||
66
app/products/calls/components/emoji_list.tsx
Normal file
66
app/products/calls/components/emoji_list.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import EmojiPill from '@calls/components/emoji_pill';
|
||||
import {ReactionStreamEmoji} from '@calls/types/calls';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: 48,
|
||||
},
|
||||
emojiList: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
height: '100%',
|
||||
},
|
||||
gradient: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const gradient = {
|
||||
start: {x: 0.75, y: 0},
|
||||
end: {x: 1, y: 0},
|
||||
colors: ['#00000000', '#000000'],
|
||||
};
|
||||
|
||||
interface Props {
|
||||
reactionStream: ReactionStreamEmoji[];
|
||||
}
|
||||
|
||||
const EmojiList = ({reactionStream}: Props) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.emojiList}>
|
||||
{reactionStream.map((e) => (
|
||||
<EmojiPill
|
||||
key={e.latestTimestamp}
|
||||
name={e.name}
|
||||
count={e.count}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<LinearGradient
|
||||
start={gradient.start}
|
||||
end={gradient.end}
|
||||
colors={gradient.colors}
|
||||
style={styles.gradient}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiList;
|
||||
46
app/products/calls/components/emoji_pill.tsx
Normal file
46
app/products/calls/components/emoji_pill.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
pill: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: 'rgba(255,255,255,0.16)',
|
||||
borderRadius: 20,
|
||||
height: 30,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
marginLeft: 6,
|
||||
},
|
||||
count: {
|
||||
...typography('Body', 75, 'SemiBold'),
|
||||
color: 'white',
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const EmojiPill = ({name, count}: Props) => {
|
||||
return (
|
||||
<View style={styles.pill}>
|
||||
<Emoji
|
||||
emojiName={name}
|
||||
size={18}
|
||||
/>
|
||||
<Text style={styles.count}>{count}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiPill;
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
setUserVoiceOn,
|
||||
userJoinedCall,
|
||||
userLeftCall,
|
||||
userReacted,
|
||||
} from '@calls/state';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
@@ -93,3 +94,7 @@ export const handleCallUserRaiseHand = (serverUrl: string, msg: WebSocketMessage
|
||||
export const handleCallUserUnraiseHand = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
setRaisedHand(serverUrl, msg.broadcast.channel_id, msg.data.userID, msg.data.raised_hand);
|
||||
};
|
||||
|
||||
export const handleCallUserReacted = (serverUrl: string, msg: WebSocketMessage) => {
|
||||
userReacted(serverUrl, msg.broadcast.channel_id, msg.data);
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} 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 UnavailableIconWrapper from '@calls/components/unavailable_icon_wrapper';
|
||||
import {usePermissionsChecker} from '@calls/hooks';
|
||||
@@ -439,6 +440,7 @@ const CallScreen = ({
|
||||
muted={user.muted}
|
||||
sharingScreen={user.id === currentCall.screenOn}
|
||||
raisedHand={Boolean(user.raisedHand)}
|
||||
reaction={user.reaction?.emoji}
|
||||
size={currentCall.screenOn ? 'm' : 'l'}
|
||||
serverUrl={currentCall.serverUrl}
|
||||
/>
|
||||
@@ -504,6 +506,7 @@ const CallScreen = ({
|
||||
{usersList}
|
||||
{screenShareView}
|
||||
{micPermissionsError && <PermissionErrorBar/>}
|
||||
<EmojiList reactionStream={currentCall.reactionStream}/>
|
||||
<View
|
||||
style={[style.buttons, isLandscape && style.buttonsLandscape, !showControls && style.buttonsLandscapeNoControls]}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
useCallsState,
|
||||
useChannelsWithCalls,
|
||||
useCurrentCall,
|
||||
useGlobalCallsState,
|
||||
useGlobalCallsState, userReacted,
|
||||
} from '@calls/state';
|
||||
import {
|
||||
setCalls,
|
||||
@@ -815,4 +815,77 @@ describe('useCallsState', () => {
|
||||
act(() => setPluginEnabled('server1', false));
|
||||
assert.deepEqual(result.current, {...newConfig, pluginEnabled: false});
|
||||
});
|
||||
|
||||
it('user reactions', () => {
|
||||
const initialCallsState = {
|
||||
...DefaultCallsState,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
calls: {'channel-1': call1, 'channel-2': call2},
|
||||
};
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
const expectedCurrentCallState: CurrentCall = {
|
||||
...initialCurrentCallState,
|
||||
reactionStream: [
|
||||
{name: 'smile', latestTimestamp: 202, count: 1},
|
||||
{name: '+1', latestTimestamp: 145, count: 2},
|
||||
],
|
||||
participants: {
|
||||
...initialCurrentCallState.participants,
|
||||
'user-1': {
|
||||
...initialCurrentCallState.participants['user-1'],
|
||||
reaction: {
|
||||
user_id: 'user-1',
|
||||
emoji: {name: 'smile', unified: 'something'},
|
||||
timestamp: 202,
|
||||
},
|
||||
},
|
||||
'user-2': {
|
||||
...initialCurrentCallState.participants['user-2'],
|
||||
reaction: {
|
||||
user_id: 'user-2',
|
||||
emoji: {name: '+1', unified: 'something'},
|
||||
timestamp: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall()];
|
||||
});
|
||||
act(() => {
|
||||
setCallsState('server1', initialCallsState);
|
||||
setCurrentCall(initialCurrentCallState);
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], initialCurrentCallState);
|
||||
|
||||
// test
|
||||
act(() => {
|
||||
userReacted('server1', 'channel-1', {
|
||||
user_id: 'user-2',
|
||||
emoji: {name: '+1', unified: 'something'},
|
||||
timestamp: 123,
|
||||
});
|
||||
userReacted('server1', 'channel-1', {
|
||||
user_id: 'user-1',
|
||||
emoji: {name: '+1', unified: 'something'},
|
||||
timestamp: 145,
|
||||
});
|
||||
userReacted('server1', 'channel-1', {
|
||||
user_id: 'user-1',
|
||||
emoji: {name: 'smile', unified: 'something'},
|
||||
timestamp: 202,
|
||||
});
|
||||
});
|
||||
assert.deepEqual(result.current[0], initialCallsState);
|
||||
assert.deepEqual(result.current[1], expectedCurrentCallState);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,16 @@ import {
|
||||
setCurrentCall,
|
||||
setGlobalCallsState,
|
||||
} from '@calls/state';
|
||||
import {Call, CallsConfig, ChannelsWithCalls, DefaultCall, DefaultCurrentCall} from '@calls/types/calls';
|
||||
import {
|
||||
Call,
|
||||
CallReaction,
|
||||
CallsConfig,
|
||||
ChannelsWithCalls,
|
||||
CurrentCall,
|
||||
DefaultCall,
|
||||
DefaultCurrentCall, ReactionStreamEmoji,
|
||||
} from '@calls/types/calls';
|
||||
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls';
|
||||
|
||||
export const setCalls = (serverUrl: string, myUserId: string, calls: Dictionary<Call>, enabled: Dictionary<boolean>) => {
|
||||
const channelsWithCalls = Object.keys(calls).reduce(
|
||||
@@ -409,3 +418,78 @@ export const setMicPermissionsErrorDismissed = () => {
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
export const userReacted = (serverUrl: string, channelId: string, reaction: CallReaction) => {
|
||||
// Note: Simplification for performance:
|
||||
// If you are not in the call with the reaction, ignore it. There could be many calls ongoing in your
|
||||
// servers, do we want to be tracking reactions and setting timeouts for all those calls? No.
|
||||
// The downside of this approach: when you join/rejoin a call, you will not see the current reactions.
|
||||
// When you leave a call, you will lose the reactions you were tracking.
|
||||
// We can revisit this if it causes UX issues.
|
||||
const currentCall = getCurrentCall();
|
||||
if (currentCall?.channelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the reaction stream.
|
||||
const newReactionStream = [...currentCall.reactionStream];
|
||||
const idx = newReactionStream.findIndex((e) => e.name === reaction.emoji.name);
|
||||
if (idx > -1) {
|
||||
const [newReaction] = newReactionStream.splice(idx, 1);
|
||||
newReaction.count += 1;
|
||||
newReaction.latestTimestamp = reaction.timestamp;
|
||||
newReactionStream.splice(0, 0, newReaction);
|
||||
} else {
|
||||
const newReaction: ReactionStreamEmoji = {
|
||||
name: reaction.emoji.name,
|
||||
count: 1,
|
||||
latestTimestamp: reaction.timestamp,
|
||||
};
|
||||
newReactionStream.splice(0, 0, newReaction);
|
||||
}
|
||||
if (newReactionStream.length > REACTION_LIMIT) {
|
||||
newReactionStream.pop();
|
||||
}
|
||||
|
||||
// Update the participant.
|
||||
const nextParticipants = {...currentCall.participants};
|
||||
if (nextParticipants[reaction.user_id]) {
|
||||
const nextUser = {...nextParticipants[reaction.user_id], reaction};
|
||||
nextParticipants[reaction.user_id] = nextUser;
|
||||
}
|
||||
|
||||
const nextCurrentCall: CurrentCall = {
|
||||
...currentCall,
|
||||
reactionStream: newReactionStream,
|
||||
participants: nextParticipants,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
|
||||
setTimeout(() => {
|
||||
userReactionTimeout(serverUrl, channelId, reaction);
|
||||
}, REACTION_TIMEOUT);
|
||||
};
|
||||
|
||||
const userReactionTimeout = (serverUrl: string, channelId: string, reaction: CallReaction) => {
|
||||
const currentCall = getCurrentCall();
|
||||
if (currentCall?.channelId !== channelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the reaction only if it was the last time that emoji was used.
|
||||
const newReactions = currentCall.reactionStream.filter((e) => e.latestTimestamp !== reaction.timestamp);
|
||||
|
||||
const nextParticipants = {...currentCall.participants};
|
||||
if (nextParticipants[reaction.user_id] && nextParticipants[reaction.user_id].reaction?.timestamp === reaction.timestamp) {
|
||||
const nextUser = {...nextParticipants[reaction.user_id]};
|
||||
delete nextUser.reaction;
|
||||
nextParticipants[reaction.user_id] = nextUser;
|
||||
}
|
||||
|
||||
const nextCurrentCall: CurrentCall = {
|
||||
...currentCall,
|
||||
reactionStream: newReactions,
|
||||
participants: nextParticipants,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {CallsConfig, DefaultCallsConfig} from '@calls/types/calls';
|
||||
|
||||
const callsConfigSubjects = {} as Dictionary<BehaviorSubject<CallsConfig>>;
|
||||
const callsConfigSubjects: Dictionary<BehaviorSubject<CallsConfig>> = {};
|
||||
|
||||
const getCallsConfigSubject = (serverUrl: string) => {
|
||||
if (!callsConfigSubjects[serverUrl]) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {CallsState, DefaultCallsState} from '@calls/types/calls';
|
||||
|
||||
const callsStateSubjects = {} as Dictionary<BehaviorSubject<CallsState>>;
|
||||
const callsStateSubjects: Dictionary<BehaviorSubject<CallsState>> = {};
|
||||
|
||||
const getCallsStateSubject = (serverUrl: string) => {
|
||||
if (!callsStateSubjects[serverUrl]) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
import {ChannelsWithCalls} from '@calls/types/calls';
|
||||
|
||||
const channelsWithCallsSubject = {} as Dictionary<BehaviorSubject<ChannelsWithCalls>>;
|
||||
const channelsWithCallsSubject: Dictionary<BehaviorSubject<ChannelsWithCalls>> = {};
|
||||
|
||||
const getChannelsWithCallsSubject = (serverUrl: string) => {
|
||||
if (!channelsWithCallsSubject[serverUrl]) {
|
||||
|
||||
@@ -22,8 +22,8 @@ export type CallsState = {
|
||||
export const DefaultCallsState: CallsState = {
|
||||
serverUrl: '',
|
||||
myUserId: '',
|
||||
calls: {} as Dictionary<Call>,
|
||||
enabled: {} as Dictionary<boolean>,
|
||||
calls: {},
|
||||
enabled: {},
|
||||
};
|
||||
|
||||
export type Call = {
|
||||
@@ -36,7 +36,7 @@ export type Call = {
|
||||
}
|
||||
|
||||
export const DefaultCall: Call = {
|
||||
participants: {} as Dictionary<CallParticipant>,
|
||||
participants: {},
|
||||
channelId: '',
|
||||
startTime: 0,
|
||||
screenOn: '',
|
||||
@@ -52,6 +52,7 @@ export type CurrentCall = Call & {
|
||||
speakerphoneOn: boolean;
|
||||
voiceOn: Dictionary<boolean>;
|
||||
micPermissionsErrorDismissed: boolean;
|
||||
reactionStream: ReactionStreamEmoji[];
|
||||
}
|
||||
|
||||
export const DefaultCurrentCall: CurrentCall = {
|
||||
@@ -68,6 +69,7 @@ export const DefaultCurrentCall: CurrentCall = {
|
||||
speakerphoneOn: false,
|
||||
voiceOn: {},
|
||||
micPermissionsErrorDismissed: false,
|
||||
reactionStream: [],
|
||||
};
|
||||
|
||||
export type CallParticipant = {
|
||||
@@ -75,6 +77,7 @@ export type CallParticipant = {
|
||||
muted: boolean;
|
||||
raisedHand: number;
|
||||
userModel?: UserModel;
|
||||
reaction?: CallReaction;
|
||||
}
|
||||
|
||||
export type ChannelsWithCalls = Dictionary<boolean>;
|
||||
@@ -100,11 +103,6 @@ export type ServerCallState = {
|
||||
owner_id: string;
|
||||
}
|
||||
|
||||
export type VoiceEventData = {
|
||||
channelId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export type CallsConnection = {
|
||||
disconnect: () => void;
|
||||
mute: () => void;
|
||||
@@ -149,3 +147,21 @@ export type ApiResp = {
|
||||
detailed_error?: string;
|
||||
status_code: number;
|
||||
}
|
||||
|
||||
export type CallReactionEmoji = {
|
||||
name: string;
|
||||
skin?: string;
|
||||
unified: string;
|
||||
}
|
||||
|
||||
export type CallReaction = {
|
||||
user_id: string;
|
||||
emoji: CallReactionEmoji;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type ReactionStreamEmoji = {
|
||||
name: string;
|
||||
latestTimestamp: number;
|
||||
count: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user