From 23529106b3b06448c7f51ec5ecbe6ae5f680b354 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 11 Jun 2021 01:19:03 +0300 Subject: [PATCH] Voice Chats: Indicators and service messages (#1134) --- src/api/gramjs/apiBuilders/chats.ts | 2 + src/api/gramjs/apiBuilders/messages.ts | 7 ++ src/api/gramjs/methods/chats.ts | 2 + src/api/types/chats.ts | 20 +++++ src/components/left/main/Chat.scss | 5 ++ src/components/left/main/Chat.tsx | 20 +++-- src/components/left/main/ChatCallStatus.scss | 80 ++++++++++++++++++++ src/components/left/main/ChatCallStatus.tsx | 26 +++++++ src/lib/gramjs/tl/api.d.ts | 6 +- 9 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 src/components/left/main/ChatCallStatus.scss create mode 100644 src/components/left/main/ChatCallStatus.tsx diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 8a463ecb9..c5c758e5d 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -40,6 +40,8 @@ function buildApiChatFieldsFromPeerEntity( && { username: peerEntity.username } ), ...(('verified' in peerEntity) && { isVerified: peerEntity.verified }), + ...(('callActive' in peerEntity) && { isCallActive: peerEntity.callActive }), + ...(('callNotEmpty' in peerEntity) && { isCallNotEmpty: peerEntity.callNotEmpty }), ...((peerEntity instanceof GramJs.Chat || peerEntity instanceof GramJs.Channel) && { ...(peerEntity.participantsCount && { membersCount: peerEntity.participantsCount }), joinDate: peerEntity.date, diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 4dbab8a89..659179856 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -599,6 +599,13 @@ function buildAction( const currencySign = getCurrencySign(action.currency); const amount = (Number(action.totalAmount) / 100).toFixed(2); text = `You successfully transferred ${currencySign}${amount} to shop for %product%`; + } else if (action instanceof GramJs.MessageActionGroupCall) { + if (action.duration) { + const mins = Math.max(Math.round(action.duration / 60), 1); + text = `Voice chat ended (${mins} min${mins > 1 ? 's' : ''})`; + } else { + text = 'Voice chat started'; + } } else { text = '%ACTION_NOT_IMPLEMENTED%'; } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 84f5234d4..bce3c2d07 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -355,6 +355,7 @@ async function getFullChannelInfo( canViewParticipants, linkedChatId, hiddenPrehistory, + call, } = result.fullChat; const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported @@ -387,6 +388,7 @@ async function getFullChannelInfo( members, kickedMembers, adminMembers, + groupCallId: call ? call.id.toString() : undefined, linkedChatId: linkedChatId ? getApiChatIdFromMtpPeer({ chatId: linkedChatId } as GramJs.TypePeer) : undefined, }, users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])], diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 3bd2033c5..534b8267e 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -30,6 +30,10 @@ export interface ApiChat { isSupport?: boolean; photos?: ApiPhoto[]; + // Calls + isCallActive?: boolean; + isCallNotEmpty?: boolean; + // Current user permissions isNotJoined?: boolean; isCreator?: boolean; @@ -56,6 +60,21 @@ export interface ApiTypingStatus { timestamp: number; } +export interface ApiTypeGroupCall { + joinMuted?: true; + canChangeJoinMuted?: true; + joinDateAsc?: true; + scheduleStartSubscribed?: true; + id: number; + participantsCount: number; + params?: any; + title?: string; + streamDcId?: number; + recordStartDate?: number; + scheduleDate?: number; + version: number; +} + export interface ApiChatFullInfo { about?: string; onlineCount?: number; @@ -65,6 +84,7 @@ export interface ApiChatFullInfo { canViewMembers?: boolean; isPreHistoryHidden?: boolean; inviteLink?: string; + groupCallId?: string; slowMode?: { seconds: number; nextSendDate?: number; diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index 01d4f96c7..68408d93b 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -52,6 +52,11 @@ } } + .status { + position: relative; + flex-shrink: 0; + } + .info { .title, .subtitle { padding-right: .125rem; diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index ca8120ad1..fac91e8dc 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -40,6 +40,7 @@ import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useChatContextActions from '../../../hooks/useChatContextActions'; import useFlag from '../../../hooks/useFlag'; import useMedia from '../../../hooks/useMedia'; +import ChatCallStatus from './ChatCallStatus'; import { ChatAnimationTypes } from './hooks'; import Avatar from '../../common/Avatar'; @@ -248,13 +249,18 @@ const Chat: FC = ({ contextActions={contextActions} onClick={handleClick} > - +
+ + {chat.isCallActive && ( + + )} +

{renderText(getChatTitle(lang, chat, privateChatUser))}

diff --git a/src/components/left/main/ChatCallStatus.scss b/src/components/left/main/ChatCallStatus.scss new file mode 100644 index 000000000..2873f3cb3 --- /dev/null +++ b/src/components/left/main/ChatCallStatus.scss @@ -0,0 +1,80 @@ + +@keyframes bar-animation-transform-1 { + 0% { transform: scaleY(0.33); } + 12.5% { transform: scaleY(1.66); } + 25% { transform: scaleY(0.33); } + 37.5% { transform: scaleY(1); } + 50% { transform: scaleY(0.33); } + 62.5% { transform: scaleY(1.66); } + 75% { transform: scaleY(0.33); } + 87.5% { transform: scaleY(1.66); } + 100% { transform: scaleY(0.33); } +} + +@keyframes bar-animation-transform-2 { + 0% { transform: scaleY(1); } + 12.5% { transform: scaleY(0.33); } + 25% { transform: scaleY(1.66); } + 37.5% { transform: scaleY(0.33); } + 50% { transform: scaleY(1); } + 62.5% { transform: scaleY(0.33); } + 75% { transform: scaleY(1.66); } + 87.5% { transform: scaleY(0.33); } + 100% { transform: scaleY(1); } +} + + +.ChatCallStatus { + position: absolute; + right: 6px; + bottom: 0; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: #0ac630; + border: 2px solid var(--color-background); + overflow: hidden; + + .indicator { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + & > div { + width: 2px; + height: 6px; + background: var(--color-background); + border-radius: 1px; + margin: 1px; + will-change: transform; + transform: translateZ(0); + } + & > div:nth-child(odd) { + transform: scaleY(0.8); + } + & > div:nth-child(even) { + transform: scaleY(1.33); + } + } + + &.selected { + background-color: var(--color-white); + border-color: var(--color-chat-active); + .indicator div{ + background-color: var(--color-chat-active); + } + } + + &.active .indicator { + div:nth-child(odd) { + animation: bar-animation-transform-2 3.2s normal infinite; + } + + div:nth-child(even) { + animation: bar-animation-transform-1 3.2s normal infinite; + } + } + +} diff --git a/src/components/left/main/ChatCallStatus.tsx b/src/components/left/main/ChatCallStatus.tsx new file mode 100644 index 000000000..6d1478ad3 --- /dev/null +++ b/src/components/left/main/ChatCallStatus.tsx @@ -0,0 +1,26 @@ +import React, { FC, memo } from '../../../lib/teact/teact'; +import buildClassName from '../../../util/buildClassName'; + +import './ChatCallStatus.scss'; + +type OwnProps = { + isSelected?: boolean; + isActive?: boolean; +}; + +const ChatCallStatus: FC = ({ + isSelected, + isActive, +}) => { + return ( +
+
+
+
+
+
+
+ ); +}; + +export default memo(ChatCallStatus); diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 65c2d9cf7..6c1582fe5 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -285,7 +285,7 @@ namespace Api { export type TypeAccessPointRule = AccessPointRule; export type TypeTlsClientHello = TlsClientHello; export type TypeTlsBlock = TlsBlockString | TlsBlockRandom | TlsBlockZero | TlsBlockDomain | TlsBlockGrease | TlsBlockScope; - + export namespace storage { export type TypeFileType = storage.FileUnknown | storage.FilePartial | storage.FileJpeg | storage.FileGif | storage.FilePng | storage.FilePdf | storage.FileMp3 | storage.FileMov | storage.FileMp4 | storage.FileWebp; @@ -7001,7 +7001,7 @@ namespace Api { }> { entries: Api.TypeTlsBlock[]; }; - + export namespace storage { export class FileUnknown extends VirtualClass {}; @@ -8500,7 +8500,7 @@ namespace Api { }>, Api.TypeDestroySessionRes> { sessionId: long; }; - + export namespace auth { export class SendCode extends Request