Calls: Introduce peer-to-peer calls (#1791)
This commit is contained in:
parent
075893c37e
commit
1da41443ca
@ -24,6 +24,12 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugin/stylelint-group-selectors": [true, { "severity": "warning" }],
|
"plugin/stylelint-group-selectors": [true, { "severity": "warning" }],
|
||||||
"plugin/whole-pixel": [true, { "ignoreList": ["letter-spacing"] }]
|
"plugin/whole-pixel": [true, { "ignoreList": ["letter-spacing"] }],
|
||||||
|
"selector-pseudo-class-no-unknown": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"ignorePseudoClasses": ["global"]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/call_busy.mp3
Normal file
BIN
public/call_busy.mp3
Normal file
Binary file not shown.
BIN
public/call_connect.mp3
Normal file
BIN
public/call_connect.mp3
Normal file
Binary file not shown.
BIN
public/call_end.mp3
Normal file
BIN
public/call_end.mp3
Normal file
Binary file not shown.
BIN
public/call_incoming.mp3
Normal file
BIN
public/call_incoming.mp3
Normal file
Binary file not shown.
BIN
public/call_ringing.mp3
Normal file
BIN
public/call_ringing.mp3
Normal file
Binary file not shown.
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@ -1,5 +1,7 @@
|
|||||||
declare const process: NodeJS.Process;
|
declare const process: NodeJS.Process;
|
||||||
|
|
||||||
|
declare module '*.module.scss';
|
||||||
|
|
||||||
declare const APP_REVISION: string;
|
declare const APP_REVISION: string;
|
||||||
|
|
||||||
declare namespace React {
|
declare namespace React {
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import { GroupCallParticipant, GroupCallParticipantVideo, SsrcGroup } from '../../../lib/secret-sauce';
|
import type {
|
||||||
|
ApiCallProtocol,
|
||||||
|
ApiPhoneCallConnection,
|
||||||
|
GroupCallParticipant,
|
||||||
|
GroupCallParticipantVideo,
|
||||||
|
SsrcGroup,
|
||||||
|
} from '../../../lib/secret-sauce';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import { ApiGroupCall } from '../../types';
|
import { ApiGroupCall, ApiPhoneCall } from '../../types';
|
||||||
import { getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
import { getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
||||||
|
|
||||||
export function buildApiGroupCallParticipant(participant: GramJs.GroupCallParticipant): GroupCallParticipant {
|
export function buildApiGroupCallParticipant(participant: GramJs.GroupCallParticipant): GroupCallParticipant {
|
||||||
@ -96,3 +102,139 @@ export function buildApiGroupCall(groupCall: GramJs.TypeGroupCall): ApiGroupCall
|
|||||||
export function getGroupCallId(groupCall: GramJs.TypeInputGroupCall) {
|
export function getGroupCallId(groupCall: GramJs.TypeInputGroupCall) {
|
||||||
return groupCall.id.toString();
|
return groupCall.id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildPhoneCall(call: GramJs.TypePhoneCall): ApiPhoneCall {
|
||||||
|
const { id } = call;
|
||||||
|
|
||||||
|
let phoneCall: ApiPhoneCall = {
|
||||||
|
id: id.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (call instanceof GramJs.PhoneCallAccepted
|
||||||
|
|| call instanceof GramJs.PhoneCallWaiting
|
||||||
|
|| call instanceof GramJs.PhoneCall
|
||||||
|
|| call instanceof GramJs.PhoneCallRequested) {
|
||||||
|
const {
|
||||||
|
accessHash, adminId, date, video, participantId, protocol,
|
||||||
|
} = call;
|
||||||
|
|
||||||
|
phoneCall = {
|
||||||
|
...phoneCall,
|
||||||
|
accessHash: accessHash.toString(),
|
||||||
|
adminId: adminId.toString(),
|
||||||
|
participantId: participantId.toString(),
|
||||||
|
date,
|
||||||
|
isVideo: video,
|
||||||
|
protocol: buildApiCallProtocol(protocol),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call instanceof GramJs.PhoneCall) {
|
||||||
|
const {
|
||||||
|
p2pAllowed, gAOrB, keyFingerprint, connections, startDate,
|
||||||
|
} = call;
|
||||||
|
|
||||||
|
phoneCall = {
|
||||||
|
...phoneCall,
|
||||||
|
state: 'active',
|
||||||
|
gAOrB: Array.from(gAOrB),
|
||||||
|
keyFingerprint: keyFingerprint.toString(),
|
||||||
|
startDate,
|
||||||
|
p2pAllowed,
|
||||||
|
connections: connections.map(buildApiCallConnection).filter(Boolean) as ApiPhoneCallConnection[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call instanceof GramJs.PhoneCallDiscarded) {
|
||||||
|
phoneCall = {
|
||||||
|
...phoneCall,
|
||||||
|
state: 'discarded',
|
||||||
|
duration: call.duration,
|
||||||
|
reason: buildApiCallDiscardReason(call.reason),
|
||||||
|
needRating: call.needRating,
|
||||||
|
needDebug: call.needDebug,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call instanceof GramJs.PhoneCallWaiting) {
|
||||||
|
phoneCall = {
|
||||||
|
...phoneCall,
|
||||||
|
state: 'waiting',
|
||||||
|
receiveDate: call.receiveDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call instanceof GramJs.PhoneCallAccepted) {
|
||||||
|
phoneCall = {
|
||||||
|
...phoneCall,
|
||||||
|
state: 'accepted',
|
||||||
|
gB: Array.from(call.gB),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (call instanceof GramJs.PhoneCallRequested) {
|
||||||
|
phoneCall = {
|
||||||
|
...phoneCall,
|
||||||
|
state: 'requested',
|
||||||
|
gAHash: Array.from(call.gAHash),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return phoneCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiCallDiscardReason(discardReason?: GramJs.TypePhoneCallDiscardReason) {
|
||||||
|
if (discardReason instanceof GramJs.PhoneCallDiscardReasonMissed) {
|
||||||
|
return 'missed';
|
||||||
|
} else if (discardReason instanceof GramJs.PhoneCallDiscardReasonBusy) {
|
||||||
|
return 'busy';
|
||||||
|
} else if (discardReason instanceof GramJs.PhoneCallDiscardReasonHangup) {
|
||||||
|
return 'hangup';
|
||||||
|
} else {
|
||||||
|
return 'disconnect';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildApiCallConnection(connection: GramJs.TypePhoneConnection): ApiPhoneCallConnection | undefined {
|
||||||
|
if (connection instanceof GramJs.PhoneConnectionWebrtc) {
|
||||||
|
const {
|
||||||
|
username, password, turn, stun, ip, ipv6, port,
|
||||||
|
} = connection;
|
||||||
|
|
||||||
|
return {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
isTurn: turn,
|
||||||
|
isStun: stun,
|
||||||
|
ip,
|
||||||
|
ipv6,
|
||||||
|
port,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildApiCallProtocol(protocol: GramJs.PhoneCallProtocol): ApiCallProtocol {
|
||||||
|
const {
|
||||||
|
libraryVersions, minLayer, maxLayer, udpP2p, udpReflector,
|
||||||
|
} = protocol;
|
||||||
|
|
||||||
|
return {
|
||||||
|
libraryVersions,
|
||||||
|
minLayer,
|
||||||
|
maxLayer,
|
||||||
|
isUdpP2p: udpP2p,
|
||||||
|
isUdpReflector: udpReflector,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCallProtocol() {
|
||||||
|
return new GramJs.PhoneCallProtocol({
|
||||||
|
libraryVersions: ['4.0.0'],
|
||||||
|
minLayer: 92,
|
||||||
|
maxLayer: 92,
|
||||||
|
udpReflector: true,
|
||||||
|
udpP2p: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
ApiUser,
|
ApiUser,
|
||||||
ApiLocation,
|
ApiLocation,
|
||||||
ApiGame,
|
ApiGame,
|
||||||
|
PhoneCallAction,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -50,6 +51,7 @@ import { interpolateArray } from '../../../util/waveform';
|
|||||||
import { buildPeer } from '../gramjsBuilders';
|
import { buildPeer } from '../gramjsBuilders';
|
||||||
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
|
||||||
|
import { buildApiCallDiscardReason } from './calls';
|
||||||
|
|
||||||
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
|
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
|
||||||
const INPUT_WAVEFORM_LENGTH = 63;
|
const INPUT_WAVEFORM_LENGTH = 63;
|
||||||
@ -785,6 +787,7 @@ function buildAction(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let phoneCall: PhoneCallAction | undefined;
|
||||||
let call: Partial<ApiGroupCall> | undefined;
|
let call: Partial<ApiGroupCall> | undefined;
|
||||||
let amount: number | undefined;
|
let amount: number | undefined;
|
||||||
let currency: string | undefined;
|
let currency: string | undefined;
|
||||||
@ -871,6 +874,13 @@ function buildAction(
|
|||||||
const mins = Math.max(Math.round(action.duration! / 60), 1);
|
const mins = Math.max(Math.round(action.duration! / 60), 1);
|
||||||
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
phoneCall = {
|
||||||
|
isOutgoing,
|
||||||
|
isVideo: action.video,
|
||||||
|
duration: action.duration,
|
||||||
|
reason: buildApiCallDiscardReason(action.reason),
|
||||||
|
};
|
||||||
} else if (action instanceof GramJs.MessageActionInviteToGroupCall) {
|
} else if (action instanceof GramJs.MessageActionInviteToGroupCall) {
|
||||||
text = 'Notification.VoiceChatInvitation';
|
text = 'Notification.VoiceChatInvitation';
|
||||||
call = {
|
call = {
|
||||||
@ -933,6 +943,7 @@ function buildAction(
|
|||||||
currency,
|
currency,
|
||||||
translationValues,
|
translationValues,
|
||||||
call,
|
call,
|
||||||
|
phoneCall,
|
||||||
score,
|
score,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
ApiMessageEntity,
|
ApiMessageEntity,
|
||||||
ApiMessageEntityTypes,
|
ApiMessageEntityTypes,
|
||||||
ApiNewPoll,
|
ApiNewPoll, ApiPhoneCall,
|
||||||
ApiReportReason,
|
ApiReportReason,
|
||||||
ApiSendMessageAction,
|
ApiSendMessageAction,
|
||||||
ApiSticker,
|
ApiSticker,
|
||||||
@ -465,3 +465,10 @@ export function buildInputGroupCall(groupCall: Partial<ApiGroupCall>) {
|
|||||||
accessHash: BigInt(groupCall.accessHash!),
|
accessHash: BigInt(groupCall.accessHash!),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildInputPhoneCall({ id, accessHash }: ApiPhoneCall) {
|
||||||
|
return new GramJs.InputPhoneCall({
|
||||||
|
id: BigInt(id),
|
||||||
|
accessHash: BigInt(accessHash!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import { JoinGroupCallPayload } from '../../../lib/secret-sauce';
|
import BigInt from 'big-integer';
|
||||||
|
import type { JoinGroupCallPayload } from '../../../lib/secret-sauce';
|
||||||
import {
|
import {
|
||||||
ApiChat, ApiUser, OnApiUpdate, ApiGroupCall,
|
ApiChat, ApiUser, OnApiUpdate, ApiGroupCall, ApiPhoneCall,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
import { buildInputGroupCall, buildInputPeer, generateRandomInt } from '../gramjsBuilders';
|
|
||||||
import {
|
import {
|
||||||
|
buildInputGroupCall, buildInputPeer, buildInputPhoneCall, generateRandomInt,
|
||||||
|
} from '../gramjsBuilders';
|
||||||
|
import {
|
||||||
|
buildCallProtocol,
|
||||||
buildApiGroupCall,
|
buildApiGroupCall,
|
||||||
buildApiGroupCallParticipant,
|
buildApiGroupCallParticipant, buildPhoneCall,
|
||||||
|
|
||||||
} from '../apiBuilders/calls';
|
} from '../apiBuilders/calls';
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
@ -234,3 +238,131 @@ export function leaveGroupCallPresentation({
|
|||||||
call: buildInputGroupCall(call),
|
call: buildInputGroupCall(call),
|
||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDhConfig() {
|
||||||
|
const dhConfig = await invokeRequest(new GramJs.messages.GetDhConfig({}));
|
||||||
|
|
||||||
|
if (!dhConfig || dhConfig instanceof GramJs.messages.DhConfigNotModified) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
g: dhConfig.g,
|
||||||
|
p: Array.from(dhConfig.p),
|
||||||
|
random: Array.from(dhConfig.random),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function discardCall({
|
||||||
|
call, isBusy,
|
||||||
|
}: {
|
||||||
|
call: ApiPhoneCall; isBusy?: boolean;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.phone.DiscardCall({
|
||||||
|
peer: buildInputPhoneCall(call),
|
||||||
|
reason: isBusy ? new GramJs.PhoneCallDiscardReasonBusy() : new GramJs.PhoneCallDiscardReasonHangup(),
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestCall({
|
||||||
|
user, gAHash, isVideo,
|
||||||
|
}: {
|
||||||
|
user: ApiUser; gAHash: number[]; isVideo?: boolean;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.phone.RequestCall({
|
||||||
|
randomId: generateRandomInt(),
|
||||||
|
userId: buildInputPeer(user.id, user.accessHash),
|
||||||
|
gAHash: Buffer.from(gAHash),
|
||||||
|
...(isVideo && { video: true }),
|
||||||
|
protocol: buildCallProtocol(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const call = buildPhoneCall(result.phoneCall);
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updatePhoneCall',
|
||||||
|
call,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCallRating({
|
||||||
|
call, rating, comment,
|
||||||
|
}: {
|
||||||
|
call: ApiPhoneCall; rating: number; comment: string;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.phone.SetCallRating({
|
||||||
|
rating,
|
||||||
|
peer: buildInputPhoneCall(call),
|
||||||
|
comment,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function receivedCall({
|
||||||
|
call,
|
||||||
|
}: {
|
||||||
|
call: ApiPhoneCall;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.phone.ReceivedCall({
|
||||||
|
peer: buildInputPhoneCall(call),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function acceptCall({
|
||||||
|
call, gB,
|
||||||
|
}: {
|
||||||
|
call: ApiPhoneCall; gB: number[];
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.phone.AcceptCall({
|
||||||
|
peer: buildInputPhoneCall(call),
|
||||||
|
gB: Buffer.from(gB),
|
||||||
|
protocol: buildCallProtocol(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
call = buildPhoneCall(result.phoneCall);
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updatePhoneCall',
|
||||||
|
call,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function confirmCall({
|
||||||
|
call, gA, keyFingerprint,
|
||||||
|
}: {
|
||||||
|
call: ApiPhoneCall; gA: number[]; keyFingerprint: string;
|
||||||
|
}) {
|
||||||
|
const result = await invokeRequest(new GramJs.phone.ConfirmCall({
|
||||||
|
peer: buildInputPhoneCall(call),
|
||||||
|
gA: Buffer.from(gA),
|
||||||
|
keyFingerprint: BigInt(keyFingerprint),
|
||||||
|
protocol: buildCallProtocol(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
call = buildPhoneCall(result.phoneCall);
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updatePhoneCall',
|
||||||
|
call,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendSignalingData({
|
||||||
|
data, call,
|
||||||
|
}: {
|
||||||
|
data: number[]; call: ApiPhoneCall;
|
||||||
|
}) {
|
||||||
|
return invokeRequest(new GramJs.phone.SendSignalingData({
|
||||||
|
data: Buffer.from(data),
|
||||||
|
peer: buildInputPhoneCall(call),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export {
|
|||||||
getGroupCall, joinGroupCall, discardGroupCall, createGroupCall,
|
getGroupCall, joinGroupCall, discardGroupCall, createGroupCall,
|
||||||
editGroupCallTitle, editGroupCallParticipant, exportGroupCallInvite, fetchGroupCallParticipants,
|
editGroupCallTitle, editGroupCallParticipant, exportGroupCallInvite, fetchGroupCallParticipants,
|
||||||
joinGroupCallPresentation, leaveGroupCall, leaveGroupCallPresentation, toggleGroupCallStartSubscription,
|
joinGroupCallPresentation, leaveGroupCall, leaveGroupCallPresentation, toggleGroupCallStartSubscription,
|
||||||
|
requestCall, getDhConfig, confirmCall, sendSignalingData, acceptCall, discardCall, setCallRating, receivedCall,
|
||||||
} from './calls';
|
} from './calls';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -78,3 +79,8 @@ export {
|
|||||||
} from './reactions';
|
} from './reactions';
|
||||||
|
|
||||||
export { fetchChannelStatistics, fetchGroupStatistics, fetchStatisticsAsyncGraph } from './statistics';
|
export { fetchChannelStatistics, fetchGroupStatistics, fetchStatisticsAsyncGraph } from './statistics';
|
||||||
|
|
||||||
|
export {
|
||||||
|
acceptPhoneCall, confirmPhoneCall, requestPhoneCall, decodePhoneCallData, createPhoneCallState,
|
||||||
|
destroyPhoneCallState, encodePhoneCallData,
|
||||||
|
} from './phoneCallState';
|
||||||
|
|||||||
184
src/api/gramjs/methods/phoneCallState.ts
Normal file
184
src/api/gramjs/methods/phoneCallState.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import BigInt from 'big-integer';
|
||||||
|
import type bigInt from 'big-integer';
|
||||||
|
import MTProtoState from '../../../lib/gramjs/network/MTProtoState';
|
||||||
|
import Logger from '../../../lib/gramjs/extensions/Logger';
|
||||||
|
import Helpers from '../../../lib/gramjs/Helpers';
|
||||||
|
import AuthKey from '../../../lib/gramjs/crypto/AuthKey';
|
||||||
|
|
||||||
|
type DhConfig = {
|
||||||
|
p: number[];
|
||||||
|
g: number;
|
||||||
|
random: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentPhoneCallState: PhoneCallState | undefined;
|
||||||
|
|
||||||
|
class PhoneCallState {
|
||||||
|
private state?: MTProtoState;
|
||||||
|
|
||||||
|
private seq = 0;
|
||||||
|
|
||||||
|
private gA?: bigInt.BigInteger;
|
||||||
|
|
||||||
|
private gB: any;
|
||||||
|
|
||||||
|
private p?: bigInt.BigInteger;
|
||||||
|
|
||||||
|
private random?: bigInt.BigInteger;
|
||||||
|
|
||||||
|
private waitForState: Promise<void>;
|
||||||
|
|
||||||
|
private resolveState?: VoidFunction;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private isOutgoing: boolean,
|
||||||
|
) {
|
||||||
|
this.waitForState = new Promise<void>((resolve) => {
|
||||||
|
this.resolveState = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestCall({ p, g, random }: DhConfig) {
|
||||||
|
const pBN = Helpers.readBigIntFromBuffer(Buffer.from(p), false);
|
||||||
|
const randomBN = Helpers.readBigIntFromBuffer(Buffer.from(random), false);
|
||||||
|
|
||||||
|
const gA = Helpers.modExp(BigInt(g), randomBN, pBN);
|
||||||
|
|
||||||
|
this.gA = gA;
|
||||||
|
this.p = pBN;
|
||||||
|
this.random = randomBN;
|
||||||
|
|
||||||
|
const gAHash: Buffer = await Helpers.sha256(Helpers.getByteArray(gA));
|
||||||
|
return Array.from(gAHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptCall({ p, g, random }: DhConfig) {
|
||||||
|
const pLast = Helpers.readBigIntFromBuffer(p, false);
|
||||||
|
const randomLast = Helpers.readBigIntFromBuffer(random, false);
|
||||||
|
|
||||||
|
const gB = Helpers.modExp(BigInt(g), randomLast, pLast);
|
||||||
|
this.gB = gB;
|
||||||
|
this.p = pLast;
|
||||||
|
this.random = randomLast;
|
||||||
|
|
||||||
|
return Array.from(Helpers.getByteArray(gB));
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmCall(gAOrB: number[], emojiData: Uint16Array, emojiOffsets: number[]) {
|
||||||
|
if (this.isOutgoing) {
|
||||||
|
this.gB = Helpers.readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
||||||
|
} else {
|
||||||
|
this.gA = Helpers.readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
||||||
|
}
|
||||||
|
const authKey = Helpers.modExp(
|
||||||
|
!this.isOutgoing ? this.gA : this.gB,
|
||||||
|
this.random,
|
||||||
|
this.p,
|
||||||
|
);
|
||||||
|
const fingerprint: Buffer = await Helpers.sha1(Helpers.getByteArray(authKey));
|
||||||
|
const keyFingerprint = Helpers.readBigIntFromBuffer(fingerprint.slice(-8).reverse(), false);
|
||||||
|
|
||||||
|
const emojis = await generateEmojiFingerprint(
|
||||||
|
Helpers.getByteArray(authKey),
|
||||||
|
Helpers.getByteArray(this.gA),
|
||||||
|
emojiData,
|
||||||
|
emojiOffsets,
|
||||||
|
);
|
||||||
|
|
||||||
|
const key = new AuthKey();
|
||||||
|
await key.setKey(Helpers.getByteArray(authKey));
|
||||||
|
this.state = new MTProtoState(key, new Logger(), true, this.isOutgoing);
|
||||||
|
this.resolveState!();
|
||||||
|
|
||||||
|
return { gA: Array.from(Helpers.getByteArray(this.gA)), keyFingerprint: keyFingerprint.toString(), emojis };
|
||||||
|
}
|
||||||
|
|
||||||
|
async encode(data: string) {
|
||||||
|
if (!this.state) return undefined;
|
||||||
|
|
||||||
|
const seqArray = new Uint32Array(1);
|
||||||
|
seqArray[0] = this.seq++;
|
||||||
|
const encodedData = await this.state.encryptMessageData(
|
||||||
|
Buffer.concat([Helpers.convertToLittle(seqArray), Buffer.from(data)]),
|
||||||
|
);
|
||||||
|
return Array.from(encodedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decode(data: number[]): Promise<any> {
|
||||||
|
if (!this.state) {
|
||||||
|
return this.waitForState.then(() => {
|
||||||
|
return this.decode(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await this.state.decryptMessageData(Buffer.from(data));
|
||||||
|
|
||||||
|
return JSON.parse(message.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/TelegramV/App/blob/ead52320975362139cabad18cf8346f98c349a22/src/js/MTProto/Calls/Internal.js#L72
|
||||||
|
function computeEmojiIndex(bytes: Uint8Array) {
|
||||||
|
return ((BigInt(bytes[0]).and(0x7F)).shiftLeft(56))
|
||||||
|
.or((BigInt(bytes[1]).shiftLeft(48)))
|
||||||
|
.or((BigInt(bytes[2]).shiftLeft(40)))
|
||||||
|
.or((BigInt(bytes[3]).shiftLeft(32)))
|
||||||
|
.or((BigInt(bytes[4]).shiftLeft(24)))
|
||||||
|
.or((BigInt(bytes[5]).shiftLeft(16)))
|
||||||
|
.or((BigInt(bytes[6]).shiftLeft(8)))
|
||||||
|
.or((BigInt(bytes[7])));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateEmojiFingerprint(
|
||||||
|
authKey: Uint8Array, gA: Uint8Array, emojiData: Uint16Array, emojiOffsets: number[],
|
||||||
|
) {
|
||||||
|
const hash = await Helpers.sha256(Buffer.concat([new Uint8Array(authKey), new Uint8Array(gA)]));
|
||||||
|
const result = [];
|
||||||
|
const emojiCount = emojiOffsets.length - 1;
|
||||||
|
const kPartSize = 8;
|
||||||
|
for (let partOffset = 0; partOffset !== hash.byteLength; partOffset += kPartSize) {
|
||||||
|
const value = computeEmojiIndex(hash.subarray(partOffset, partOffset + kPartSize));
|
||||||
|
const index = value.modPow(1, emojiCount).toJSNumber();
|
||||||
|
const offset = emojiOffsets[index];
|
||||||
|
const size = emojiOffsets[index + 1] - offset;
|
||||||
|
result.push(String.fromCharCode(...emojiData.subarray(offset, offset + size)));
|
||||||
|
}
|
||||||
|
return result.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPhoneCallState(params: ConstructorParameters<typeof PhoneCallState>) {
|
||||||
|
currentPhoneCallState = new PhoneCallState(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyPhoneCallState() {
|
||||||
|
currentPhoneCallState = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FunctionPropertyOf<T> = {
|
||||||
|
[P in keyof T]: T[P] extends Function
|
||||||
|
? P
|
||||||
|
: never
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
type ParamsOf<T extends FunctionPropertyOf<PhoneCallState>> = Parameters<PhoneCallState[T]>;
|
||||||
|
type ReturnTypeOf<T extends FunctionPropertyOf<PhoneCallState>> = ReturnType<PhoneCallState[T]>;
|
||||||
|
|
||||||
|
export function encodePhoneCallData(params: ParamsOf<'encode'>): ReturnTypeOf<'encode'> {
|
||||||
|
return currentPhoneCallState!.encode(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decodePhoneCallData(params: ParamsOf<'decode'>): ReturnTypeOf<'decode'> {
|
||||||
|
return currentPhoneCallState!.decode(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirmPhoneCall(params: ParamsOf<'confirmCall'>): ReturnTypeOf<'confirmCall'> {
|
||||||
|
return currentPhoneCallState!.confirmCall(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acceptPhoneCall(params: ParamsOf<'acceptCall'>): ReturnTypeOf<'acceptCall'> {
|
||||||
|
return currentPhoneCallState!.acceptCall(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestPhoneCall(params: ParamsOf<'requestCall'>): ReturnTypeOf<'requestCall'> {
|
||||||
|
return currentPhoneCallState!.requestCall(...params);
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { GroupCallConnectionData } from '../../lib/secret-sauce';
|
import type { GroupCallConnectionData } from '../../lib/secret-sauce';
|
||||||
import { Api as GramJs, connection } from '../../lib/gramjs';
|
import { Api as GramJs, connection } from '../../lib/gramjs';
|
||||||
import { ApiMessage, ApiUpdateConnectionStateType, OnApiUpdate } from '../types';
|
import { ApiMessage, ApiUpdateConnectionStateType, OnApiUpdate } from '../types';
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './a
|
|||||||
import { buildApiPhoto } from './apiBuilders/common';
|
import { buildApiPhoto } from './apiBuilders/common';
|
||||||
import {
|
import {
|
||||||
buildApiGroupCall,
|
buildApiGroupCall,
|
||||||
buildApiGroupCallParticipant,
|
buildApiGroupCallParticipant, buildPhoneCall,
|
||||||
getGroupCallId,
|
getGroupCallId,
|
||||||
} from './apiBuilders/calls';
|
} from './apiBuilders/calls';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
|
||||||
@ -897,6 +897,24 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
recentRequesterIds: update.recentRequesters.map((id) => buildApiPeerId(id, 'user')),
|
recentRequesterIds: update.recentRequesters.map((id) => buildApiPeerId(id, 'user')),
|
||||||
requestsPending: update.requestsPending,
|
requestsPending: update.requestsPending,
|
||||||
});
|
});
|
||||||
|
} else if (update instanceof GramJs.UpdatePhoneCall) {
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
const entities = update._entities;
|
||||||
|
if (entities) {
|
||||||
|
addEntitiesWithPhotosToLocalDb(entities);
|
||||||
|
dispatchUserAndChatUpdates(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updatePhoneCall',
|
||||||
|
call: buildPhoneCall(update.phoneCall),
|
||||||
|
});
|
||||||
|
} else if (update instanceof GramJs.UpdatePhoneCallSignalingData) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updatePhoneCallSignalingData',
|
||||||
|
callId: update.phoneCallId.toString(),
|
||||||
|
data: Array.from(update.data),
|
||||||
|
});
|
||||||
} else if (DEBUG) {
|
} else if (DEBUG) {
|
||||||
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import { GroupCallParticipant, GroupCallConnectionState } from '../../lib/secret-sauce';
|
import type {
|
||||||
|
GroupCallParticipant,
|
||||||
|
GroupCallConnectionState,
|
||||||
|
ApiPhoneCallConnection,
|
||||||
|
ApiCallProtocol, VideoState, VideoRotation,
|
||||||
|
} from '../../lib/secret-sauce';
|
||||||
|
|
||||||
export interface ApiGroupCall {
|
export interface ApiGroupCall {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
@ -24,3 +29,45 @@ export interface ApiGroupCall {
|
|||||||
connectionState: GroupCallConnectionState;
|
connectionState: GroupCallConnectionState;
|
||||||
isSpeakerDisabled?: boolean;
|
isSpeakerDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PhoneCallAction {
|
||||||
|
isOutgoing: boolean;
|
||||||
|
isVideo?: boolean;
|
||||||
|
duration?: number;
|
||||||
|
reason?: 'missed' | 'disconnect' | 'hangup' | 'busy';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiPhoneCall {
|
||||||
|
state?: 'active' | 'waiting' | 'discarded' | 'requested' | 'accepted' | 'requesting';
|
||||||
|
isConnected?: boolean;
|
||||||
|
id: string;
|
||||||
|
accessHash?: string;
|
||||||
|
adminId?: string;
|
||||||
|
participantId?: string;
|
||||||
|
isVideo?: boolean;
|
||||||
|
date?: number;
|
||||||
|
startDate?: number;
|
||||||
|
receiveDate?: number;
|
||||||
|
p2pAllowed?: boolean;
|
||||||
|
connections?: ApiPhoneCallConnection[];
|
||||||
|
protocol?: ApiCallProtocol;
|
||||||
|
needRating?: boolean;
|
||||||
|
needDebug?: boolean;
|
||||||
|
reason?: 'missed' | 'disconnect' | 'hangup' | 'busy';
|
||||||
|
duration?: number;
|
||||||
|
|
||||||
|
emojis?: string;
|
||||||
|
gA?: number[];
|
||||||
|
gB?: number[];
|
||||||
|
pLast?: number[];
|
||||||
|
randomLast?: number[];
|
||||||
|
gAOrB?: number[];
|
||||||
|
gAHash?: number[];
|
||||||
|
keyFingerprint?: string;
|
||||||
|
|
||||||
|
isMuted?: boolean;
|
||||||
|
videoState?: VideoState;
|
||||||
|
videoRotation?: VideoRotation;
|
||||||
|
screencastState?: VideoState;
|
||||||
|
isBatteryLow?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ApiGroupCall } from './calls';
|
import { ApiGroupCall, PhoneCallAction } from './calls';
|
||||||
|
|
||||||
export interface ApiDimensions {
|
export interface ApiDimensions {
|
||||||
width: number;
|
width: number;
|
||||||
@ -203,6 +203,7 @@ export interface ApiAction {
|
|||||||
currency?: string;
|
currency?: string;
|
||||||
translationValues: string[];
|
translationValues: string[];
|
||||||
call?: Partial<ApiGroupCall>;
|
call?: Partial<ApiGroupCall>;
|
||||||
|
phoneCall?: PhoneCallAction;
|
||||||
score?: number;
|
score?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import { GroupCallConnectionData, GroupCallParticipant, GroupCallConnectionState } from '../../lib/secret-sauce';
|
import type {
|
||||||
|
GroupCallConnectionData,
|
||||||
|
GroupCallParticipant,
|
||||||
|
GroupCallConnectionState,
|
||||||
|
VideoState, VideoRotation,
|
||||||
|
} from '../../lib/secret-sauce';
|
||||||
import {
|
import {
|
||||||
ApiChat,
|
ApiChat,
|
||||||
ApiChatFullInfo,
|
ApiChatFullInfo,
|
||||||
@ -15,7 +20,7 @@ import {
|
|||||||
ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData,
|
ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData,
|
||||||
} from './misc';
|
} from './misc';
|
||||||
import {
|
import {
|
||||||
ApiGroupCall,
|
ApiGroupCall, ApiPhoneCall,
|
||||||
} from './calls';
|
} from './calls';
|
||||||
|
|
||||||
export type ApiUpdateReady = {
|
export type ApiUpdateReady = {
|
||||||
@ -456,6 +461,31 @@ export type ApiUpdateGroupCallConnectionState = {
|
|||||||
isSpeakerDisabled?: boolean;
|
isSpeakerDisabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdatePhoneCall = {
|
||||||
|
'@type': 'updatePhoneCall';
|
||||||
|
call: ApiPhoneCall;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdatePhoneCallSignalingData = {
|
||||||
|
'@type': 'updatePhoneCallSignalingData';
|
||||||
|
callId: string;
|
||||||
|
data: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdatePhoneCallMediaState = {
|
||||||
|
'@type': 'updatePhoneCallMediaState';
|
||||||
|
isMuted: boolean;
|
||||||
|
videoState: VideoState;
|
||||||
|
videoRotation: VideoRotation;
|
||||||
|
screencastState: VideoState;
|
||||||
|
isBatteryLow: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdatePhoneCallConnectionState = {
|
||||||
|
'@type': 'updatePhoneCallConnectionState';
|
||||||
|
connectionState: RTCPeerConnectionState;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiUpdate = (
|
export type ApiUpdate = (
|
||||||
ApiUpdateReady | ApiUpdateSession |
|
ApiUpdateReady | ApiUpdateSession |
|
||||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||||
@ -476,7 +506,9 @@ export type ApiUpdate = (
|
|||||||
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
|
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
|
||||||
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
|
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
|
||||||
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
|
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
|
||||||
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted
|
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |
|
||||||
|
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
|
||||||
|
ApiUpdatePhoneCallConnectionState
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
Binary file not shown.
@ -1,3 +1,4 @@
|
|||||||
export { default as GroupCall } from '../components/calls/group/GroupCall';
|
export { default as GroupCall } from '../components/calls/group/GroupCall';
|
||||||
export { default as ActiveCallHeader } from '../components/calls/ActiveCallHeader';
|
export { default as ActiveCallHeader } from '../components/calls/ActiveCallHeader';
|
||||||
export { default as CallFallbackConfirm } from '../components/calls/CallFallbackConfirm';
|
export { default as PhoneCall } from '../components/calls/phone/PhoneCall';
|
||||||
|
export { default as RatePhoneCallModal } from '../components/calls/phone/RatePhoneCallModal';
|
||||||
|
|||||||
@ -3,12 +3,12 @@ import useModuleLoader from '../../hooks/useModuleLoader';
|
|||||||
import { Bundles } from '../../util/moduleLoader';
|
import { Bundles } from '../../util/moduleLoader';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
groupCallId?: string;
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActiveCallHeaderAsync: FC<OwnProps> = (props) => {
|
const ActiveCallHeaderAsync: FC<OwnProps> = (props) => {
|
||||||
const { groupCallId } = props;
|
const { isActive } = props;
|
||||||
const ActiveCallHeader = useModuleLoader(Bundles.Calls, 'ActiveCallHeader', !groupCallId);
|
const ActiveCallHeader = useModuleLoader(Bundles.Calls, 'ActiveCallHeader', !isActive);
|
||||||
|
|
||||||
return ActiveCallHeader ? <ActiveCallHeader /> : undefined;
|
return ActiveCallHeader ? <ActiveCallHeader /> : undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,51 +1,50 @@
|
|||||||
import { GroupCallParticipant } from '../../lib/secret-sauce';
|
|
||||||
import React, {
|
import React, {
|
||||||
FC, memo, useEffect,
|
FC, memo, useEffect,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import { ApiGroupCall } from '../../api/types';
|
import { ApiGroupCall, ApiUser } from '../../api/types';
|
||||||
|
|
||||||
import { selectActiveGroupCall, selectGroupCallParticipant } from '../../global/selectors/calls';
|
import { selectActiveGroupCall, selectPhoneCallUser } from '../../global/selectors/calls';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import './ActiveCallHeader.scss';
|
import './ActiveCallHeader.scss';
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
isGroupCallPanelHidden?: boolean;
|
isCallPanelVisible?: boolean;
|
||||||
meParticipant: GroupCallParticipant;
|
|
||||||
groupCall?: ApiGroupCall;
|
groupCall?: ApiGroupCall;
|
||||||
|
phoneCallUser?: ApiUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ActiveCallHeader: FC<StateProps> = ({
|
const ActiveCallHeader: FC<StateProps> = ({
|
||||||
groupCall,
|
groupCall,
|
||||||
meParticipant,
|
phoneCallUser,
|
||||||
isGroupCallPanelHidden,
|
isCallPanelVisible,
|
||||||
}) => {
|
}) => {
|
||||||
const { toggleGroupCallPanel } = getActions();
|
const { toggleGroupCallPanel } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.classList.toggle('has-group-call-header', isGroupCallPanelHidden);
|
document.body.classList.toggle('has-call-header', Boolean(isCallPanelVisible));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.classList.toggle('has-group-call-header', false);
|
document.body.classList.toggle('has-call-header', false);
|
||||||
};
|
};
|
||||||
}, [isGroupCallPanelHidden]);
|
}, [isCallPanelVisible]);
|
||||||
|
|
||||||
if (!groupCall || !meParticipant) return undefined;
|
if (!groupCall && !phoneCallUser) return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'ActiveCallHeader',
|
'ActiveCallHeader',
|
||||||
isGroupCallPanelHidden && 'open',
|
isCallPanelVisible && 'open',
|
||||||
)}
|
)}
|
||||||
onClick={toggleGroupCallPanel}
|
onClick={toggleGroupCallPanel}
|
||||||
>
|
>
|
||||||
<span className="title">{groupCall.title || lang('VoipGroupVoiceChat')}</span>
|
<span className="title">{phoneCallUser?.firstName || groupCall?.title || lang('VoipGroupVoiceChat')}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -54,8 +53,8 @@ export default memo(withGlobal(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
return {
|
return {
|
||||||
groupCall: selectActiveGroupCall(global),
|
groupCall: selectActiveGroupCall(global),
|
||||||
isGroupCallPanelHidden: global.groupCalls.isGroupCallPanelHidden,
|
isCallPanelVisible: global.isCallPanelVisible,
|
||||||
meParticipant: selectGroupCallParticipant(global, global.groupCalls.activeGroupCallId!, global.currentUserId!),
|
phoneCallUser: selectPhoneCallUser(global),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ActiveCallHeader));
|
)(ActiveCallHeader));
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
import React, { FC, memo } from '../../lib/teact/teact';
|
|
||||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
|
||||||
import { Bundles } from '../../util/moduleLoader';
|
|
||||||
|
|
||||||
type OwnProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CallFallbackConfirmAsync: FC<OwnProps> = ({ isOpen }) => {
|
|
||||||
const CallFallbackConfirm = useModuleLoader(Bundles.Calls, 'CallFallbackConfirm', !isOpen);
|
|
||||||
|
|
||||||
return CallFallbackConfirm ? <CallFallbackConfirm isOpen={isOpen} /> : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(CallFallbackConfirmAsync);
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import React, {
|
|
||||||
FC, memo, useCallback, useState,
|
|
||||||
} from '../../lib/teact/teact';
|
|
||||||
import { getActions, withGlobal } from '../../global';
|
|
||||||
|
|
||||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
|
||||||
import Checkbox from '../ui/Checkbox';
|
|
||||||
import { selectCallFallbackChannelTitle } from '../../global/selectors/calls';
|
|
||||||
import { getUserFullName } from '../../global/helpers';
|
|
||||||
import { selectCurrentMessageList, selectUser } from '../../global/selectors';
|
|
||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
|
||||||
isOpen: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface StateProps {
|
|
||||||
userFullName?: string;
|
|
||||||
channelTitle: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CallFallbackConfirm: FC<OwnProps & StateProps> = ({
|
|
||||||
isOpen,
|
|
||||||
channelTitle,
|
|
||||||
userFullName,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
closeCallFallbackConfirm,
|
|
||||||
inviteToCallFallback,
|
|
||||||
} = getActions();
|
|
||||||
|
|
||||||
const [shouldRemove, setShouldRemove] = useState(true);
|
|
||||||
const renderingUserFullName = useCurrentOrPrev(userFullName, true);
|
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
|
||||||
inviteToCallFallback({ shouldRemove });
|
|
||||||
}, [inviteToCallFallback, shouldRemove]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Start Call"
|
|
||||||
isOpen={isOpen}
|
|
||||||
confirmHandler={handleConfirm}
|
|
||||||
onClose={closeCallFallbackConfirm}
|
|
||||||
>
|
|
||||||
<p>The call will be started in a private channel <b>{channelTitle}</b>.</p>
|
|
||||||
<Checkbox
|
|
||||||
label={`Remove ${renderingUserFullName} from this channel after the call`}
|
|
||||||
checked={shouldRemove}
|
|
||||||
onCheck={setShouldRemove}
|
|
||||||
/>
|
|
||||||
</ConfirmDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
|
||||||
(global): StateProps => {
|
|
||||||
const { chatId } = selectCurrentMessageList(global) || {};
|
|
||||||
const user = chatId ? selectUser(global, chatId) : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
userFullName: user ? getUserFullName(user) : undefined,
|
|
||||||
channelTitle: selectCallFallbackChannelTitle(global),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
)(CallFallbackConfirm));
|
|
||||||
@ -47,7 +47,7 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
isGroupCallPanelHidden: boolean;
|
isCallPanelVisible: boolean;
|
||||||
connectionState: GroupCallConnectionState;
|
connectionState: GroupCallConnectionState;
|
||||||
title?: string;
|
title?: string;
|
||||||
meParticipant?: TypeGroupCallParticipant;
|
meParticipant?: TypeGroupCallParticipant;
|
||||||
@ -59,7 +59,7 @@ type StateProps = {
|
|||||||
|
|
||||||
const GroupCall: FC<OwnProps & StateProps> = ({
|
const GroupCall: FC<OwnProps & StateProps> = ({
|
||||||
groupCallId,
|
groupCallId,
|
||||||
isGroupCallPanelHidden,
|
isCallPanelVisible,
|
||||||
connectionState,
|
connectionState,
|
||||||
isSpeakerEnabled,
|
isSpeakerEnabled,
|
||||||
title,
|
title,
|
||||||
@ -246,7 +246,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={!isGroupCallPanelHidden && !isLeaving}
|
isOpen={!isCallPanelVisible && !isLeaving}
|
||||||
onClose={toggleGroupCallPanel}
|
onClose={toggleGroupCallPanel}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'GroupCall',
|
'GroupCall',
|
||||||
@ -287,7 +287,7 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand && (
|
{IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="share-screen"
|
icon="share-screen-outlined"
|
||||||
onClick={toggleGroupCallPresentation}
|
onClick={toggleGroupCallPresentation}
|
||||||
>
|
>
|
||||||
{lang(hasPresentation ? 'VoipChatStopScreenCapture' : 'VoipChatStartScreenCapture')}
|
{lang(hasPresentation ? 'VoipChatStopScreenCapture' : 'VoipChatStartScreenCapture')}
|
||||||
@ -405,7 +405,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isSpeakerEnabled: !isSpeakerDisabled,
|
isSpeakerEnabled: !isSpeakerDisabled,
|
||||||
participantsCount,
|
participantsCount,
|
||||||
meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!),
|
meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!),
|
||||||
isGroupCallPanelHidden: Boolean(global.groupCalls.isGroupCallPanelHidden),
|
isCallPanelVisible: Boolean(global.isCallPanelVisible),
|
||||||
isAdmin: selectIsAdminInActiveGroupCall(global),
|
isAdmin: selectIsAdminInActiveGroupCall(global),
|
||||||
participants,
|
participants,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -51,7 +51,7 @@ const MicrophoneButton: FC<StateProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (prevShouldRaiseHand && !shouldRaiseHand) {
|
if (prevShouldRaiseHand && !shouldRaiseHand) {
|
||||||
playGroupCallSound('allowTalk');
|
playGroupCallSound({ sound: 'allowTalk' });
|
||||||
}
|
}
|
||||||
}, [playGroupCallSound, prevShouldRaiseHand, shouldRaiseHand]);
|
}, [playGroupCallSound, prevShouldRaiseHand, shouldRaiseHand]);
|
||||||
|
|
||||||
|
|||||||
16
src/components/calls/phone/PhoneCall.async.tsx
Normal file
16
src/components/calls/phone/PhoneCall.async.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React, { FC, memo } from '../../../lib/teact/teact';
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PhoneCallAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isActive } = props;
|
||||||
|
const PhoneCall = useModuleLoader(Bundles.Calls, 'PhoneCall', !isActive);
|
||||||
|
|
||||||
|
return PhoneCall ? <PhoneCall /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(PhoneCallAsync);
|
||||||
179
src/components/calls/phone/PhoneCall.module.scss
Normal file
179
src/components/calls/phone/PhoneCall.module.scss
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
.root {
|
||||||
|
:global(.modal-dialog) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
:global(.modal-content) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 80vh;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.Avatar) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
z-index: -1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
|
||||||
|
:global(.Avatar__img) {
|
||||||
|
border-radius: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.blurred :global(.Avatar__img) {
|
||||||
|
filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.single-column {
|
||||||
|
:global(.modal-dialog) {
|
||||||
|
max-width: 100% !important;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.modal-content) {
|
||||||
|
height: calc(var(--vh) * 100);
|
||||||
|
max-height: calc(var(--vh) * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
:global(.Button) {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojis-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: 0.25s ease-in-out background-color;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emojis {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 1rem;
|
||||||
|
height: 3rem;
|
||||||
|
transition: 0.25s ease-in-out transform;
|
||||||
|
top: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: scale(2) translateY(3rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-tooltip {
|
||||||
|
user-select: none;
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 10rem;
|
||||||
|
color: white;
|
||||||
|
max-width: 20rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.25s ease-in-out opacity;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 4rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
margin-bottom: auto;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, transparent 100%);
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leave {
|
||||||
|
background: #ff595a !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #d24646 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept {
|
||||||
|
background: #5CC85E !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #4eab50 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accept-icon {
|
||||||
|
transform: rotate(-135deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-video {
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.second-video {
|
||||||
|
z-index: -1;
|
||||||
|
position: absolute;
|
||||||
|
width: 9rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transform: translateY(calc(100% + 1rem));
|
||||||
|
transition: 0.25s ease-in-out transform;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
transform: translateY(-5.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fullscreen {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
362
src/components/calls/phone/PhoneCall.tsx
Normal file
362
src/components/calls/phone/PhoneCall.tsx
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useEffect, useMemo, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
import '../../../global/actions/calls';
|
||||||
|
|
||||||
|
import { ApiPhoneCall, ApiUser } from '../../../api/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IS_ANDROID,
|
||||||
|
IS_IOS,
|
||||||
|
IS_REQUEST_FULLSCREEN_SUPPORTED,
|
||||||
|
IS_SINGLE_COLUMN_LAYOUT,
|
||||||
|
} from '../../../util/environment';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { selectPhoneCallUser } from '../../../global/selectors/calls';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||||
|
import {
|
||||||
|
getStreams, IS_SCREENSHARE_SUPPORTED, switchCameraInputP2p, toggleStreamP2p,
|
||||||
|
} from '../../../lib/secret-sauce';
|
||||||
|
import useInterval from '../../../hooks/useInterval';
|
||||||
|
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||||
|
|
||||||
|
import Modal from '../../ui/Modal';
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import PhoneCallButton from './PhoneCallButton';
|
||||||
|
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||||
|
|
||||||
|
import styles from './PhoneCall.module.scss';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
user?: ApiUser;
|
||||||
|
phoneCall?: ApiPhoneCall;
|
||||||
|
isOutgoing: boolean;
|
||||||
|
isCallPanelVisible?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PhoneCall: FC<StateProps> = ({
|
||||||
|
user,
|
||||||
|
isOutgoing,
|
||||||
|
phoneCall,
|
||||||
|
isCallPanelVisible,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
const {
|
||||||
|
hangUp, acceptCall, playGroupCallSound, toggleGroupCallPanel, connectToActivePhoneCall,
|
||||||
|
} = getActions();
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [isFullscreen, openFullscreen, closeFullscreen] = useFlag();
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
if (isFullscreen) {
|
||||||
|
closeFullscreen();
|
||||||
|
} else {
|
||||||
|
openFullscreen();
|
||||||
|
}
|
||||||
|
}, [closeFullscreen, isFullscreen, openFullscreen]);
|
||||||
|
|
||||||
|
const handleToggleFullscreen = useCallback(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
|
if (isFullscreen) {
|
||||||
|
document.exitFullscreen().then(closeFullscreen);
|
||||||
|
} else {
|
||||||
|
containerRef.current.requestFullscreen().then(openFullscreen);
|
||||||
|
}
|
||||||
|
}, [closeFullscreen, isFullscreen, openFullscreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!IS_REQUEST_FULLSCREEN_SUPPORTED) return undefined;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return undefined;
|
||||||
|
|
||||||
|
container.addEventListener('fullscreenchange', toggleFullscreen);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('fullscreenchange', toggleFullscreen);
|
||||||
|
};
|
||||||
|
}, [toggleFullscreen]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
toggleGroupCallPanel();
|
||||||
|
if (isFullscreen) {
|
||||||
|
closeFullscreen();
|
||||||
|
}
|
||||||
|
}, [closeFullscreen, isFullscreen, toggleGroupCallPanel]);
|
||||||
|
|
||||||
|
const isDiscarded = phoneCall?.state === 'discarded';
|
||||||
|
const isBusy = phoneCall?.reason === 'busy';
|
||||||
|
|
||||||
|
const isIncomingRequested = phoneCall?.state === 'requested' && !isOutgoing;
|
||||||
|
const isOutgoingRequested = (phoneCall?.state === 'requested' || phoneCall?.state === 'waiting') && isOutgoing;
|
||||||
|
const isActive = phoneCall?.state === 'active';
|
||||||
|
const isConnected = phoneCall?.isConnected;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isIncomingRequested) {
|
||||||
|
playGroupCallSound({ sound: 'incoming' });
|
||||||
|
} else if (isBusy) {
|
||||||
|
playGroupCallSound({ sound: 'busy' });
|
||||||
|
} else if (isDiscarded) {
|
||||||
|
playGroupCallSound({ sound: 'end' });
|
||||||
|
} else if (isOutgoingRequested) {
|
||||||
|
playGroupCallSound({ sound: 'ringing' });
|
||||||
|
} else if (isConnected) {
|
||||||
|
playGroupCallSound({ sound: 'connect' });
|
||||||
|
}
|
||||||
|
}, [isBusy, isDiscarded, isIncomingRequested, isOutgoingRequested, isConnected, playGroupCallSound]);
|
||||||
|
|
||||||
|
const [isHangingUp, startHangingUp, stopHangingUp] = useFlag();
|
||||||
|
const handleHangUp = useCallback(() => {
|
||||||
|
startHangingUp();
|
||||||
|
hangUp();
|
||||||
|
}, [hangUp, startHangingUp]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (phoneCall?.id) {
|
||||||
|
stopHangingUp();
|
||||||
|
} else {
|
||||||
|
connectToActivePhoneCall();
|
||||||
|
}
|
||||||
|
}, [connectToActivePhoneCall, phoneCall?.id, stopHangingUp]);
|
||||||
|
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
forceUpdate();
|
||||||
|
}, isConnected ? 1000 : undefined);
|
||||||
|
|
||||||
|
const callStatus = useMemo(() => {
|
||||||
|
const state = phoneCall?.state;
|
||||||
|
if (isHangingUp) {
|
||||||
|
return lang('lng_call_status_hanging');
|
||||||
|
}
|
||||||
|
if (isBusy) return 'busy';
|
||||||
|
if (state === 'requesting') {
|
||||||
|
return lang('lng_call_status_requesting');
|
||||||
|
} else if (state === 'requested') {
|
||||||
|
return isOutgoing ? lang('lng_call_status_ringing') : lang('lng_call_status_incoming');
|
||||||
|
} else if (state === 'waiting') {
|
||||||
|
return lang('lng_call_status_waiting');
|
||||||
|
} else if (state === 'active' && isConnected) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return lang('lng_call_status_exchanging');
|
||||||
|
}
|
||||||
|
}, [isBusy, isConnected, isHangingUp, isOutgoing, lang, phoneCall?.state]);
|
||||||
|
|
||||||
|
const hasVideo = phoneCall?.videoState === 'active';
|
||||||
|
const hasPresentation = phoneCall?.screencastState === 'active';
|
||||||
|
|
||||||
|
const streams = getStreams();
|
||||||
|
const hasOwnAudio = streams?.ownAudio?.getTracks()[0].enabled;
|
||||||
|
const hasOwnPresentation = streams?.ownPresentation?.getTracks()[0].enabled;
|
||||||
|
const hasOwnVideo = streams?.ownVideo?.getTracks()[0].enabled;
|
||||||
|
|
||||||
|
const [isHidingPresentation, startHidingPresentation, stopHidingPresentation] = useFlag();
|
||||||
|
const [isHidingVideo, startHidingVideo, stopHidingVideo] = useFlag();
|
||||||
|
|
||||||
|
const handleTogglePresentation = useCallback(() => {
|
||||||
|
if (hasOwnPresentation) {
|
||||||
|
startHidingPresentation();
|
||||||
|
}
|
||||||
|
if (hasOwnVideo) {
|
||||||
|
startHidingVideo();
|
||||||
|
}
|
||||||
|
setTimeout(async () => {
|
||||||
|
await toggleStreamP2p('presentation');
|
||||||
|
stopHidingPresentation();
|
||||||
|
stopHidingVideo();
|
||||||
|
}, 250);
|
||||||
|
}, [
|
||||||
|
hasOwnPresentation, hasOwnVideo, startHidingPresentation, startHidingVideo, stopHidingPresentation, stopHidingVideo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleToggleVideo = useCallback(() => {
|
||||||
|
if (hasOwnVideo) {
|
||||||
|
startHidingVideo();
|
||||||
|
}
|
||||||
|
if (hasOwnPresentation) {
|
||||||
|
startHidingPresentation();
|
||||||
|
}
|
||||||
|
setTimeout(async () => {
|
||||||
|
await toggleStreamP2p('video');
|
||||||
|
stopHidingPresentation();
|
||||||
|
stopHidingVideo();
|
||||||
|
}, 250);
|
||||||
|
}, [
|
||||||
|
hasOwnPresentation, hasOwnVideo, startHidingPresentation, startHidingVideo, stopHidingPresentation, stopHidingVideo,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleToggleAudio = useCallback(() => {
|
||||||
|
void toggleStreamP2p('audio');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [isEmojiOpen, openEmoji, closeEmoji] = useFlag();
|
||||||
|
|
||||||
|
const [isFlipping, startFlipping, stopFlipping] = useFlag();
|
||||||
|
|
||||||
|
const handleFlipCamera = useCallback(() => {
|
||||||
|
startFlipping();
|
||||||
|
switchCameraInputP2p();
|
||||||
|
setTimeout(stopFlipping, 250);
|
||||||
|
}, [startFlipping, stopFlipping]);
|
||||||
|
|
||||||
|
const timeElapsed = phoneCall?.startDate && (Number(new Date()) / 1000 - phoneCall.startDate);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (phoneCall?.state === 'discarded') {
|
||||||
|
setTimeout(hangUp, 250);
|
||||||
|
}
|
||||||
|
}, [hangUp, phoneCall?.reason, phoneCall?.state]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={phoneCall && phoneCall?.state !== 'discarded' && !isCallPanelVisible}
|
||||||
|
onClose={handleClose}
|
||||||
|
className={buildClassName(
|
||||||
|
styles.root,
|
||||||
|
IS_SINGLE_COLUMN_LAYOUT && styles.singleColumn,
|
||||||
|
)}
|
||||||
|
dialogRef={containerRef}
|
||||||
|
>
|
||||||
|
<Avatar user={user} size="jumbo" className={hasVideo || hasPresentation ? styles.blurred : ''} />
|
||||||
|
{phoneCall?.screencastState === 'active' && streams?.presentation
|
||||||
|
&& <video className={styles.mainVideo} muted autoPlay playsInline srcObject={streams.presentation} />}
|
||||||
|
{phoneCall?.videoState === 'active' && streams?.video
|
||||||
|
&& <video className={styles.mainVideo} muted autoPlay playsInline srcObject={streams.video} />}
|
||||||
|
<video
|
||||||
|
className={buildClassName(
|
||||||
|
styles.secondVideo,
|
||||||
|
!isHidingPresentation && hasOwnPresentation && styles.visible,
|
||||||
|
isFullscreen && styles.fullscreen,
|
||||||
|
)}
|
||||||
|
muted
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
srcObject={streams?.ownPresentation}
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
className={buildClassName(
|
||||||
|
styles.secondVideo,
|
||||||
|
!isHidingVideo && hasOwnVideo && styles.visible,
|
||||||
|
isFullscreen && styles.fullscreen,
|
||||||
|
)}
|
||||||
|
muted
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
srcObject={streams?.ownVideo}
|
||||||
|
/>
|
||||||
|
<div className={styles.header}>
|
||||||
|
{IS_REQUEST_FULLSCREEN_SUPPORTED && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
onClick={handleToggleFullscreen}
|
||||||
|
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
|
||||||
|
>
|
||||||
|
<i className={isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen'} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
onClick={handleClose}
|
||||||
|
className={styles.closeButton}
|
||||||
|
>
|
||||||
|
<i className="icon-close" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.emojisBackdrop, isEmojiOpen && styles.open)}
|
||||||
|
onClick={!isEmojiOpen ? openEmoji : closeEmoji}
|
||||||
|
>
|
||||||
|
<div className={buildClassName(styles.emojis, isEmojiOpen && styles.open)}>
|
||||||
|
{phoneCall?.isConnected && phoneCall?.emojis && renderText(phoneCall.emojis, ['emoji'])}
|
||||||
|
</div>
|
||||||
|
<div className={buildClassName(styles.emojiTooltip, isEmojiOpen && styles.open)}>
|
||||||
|
{lang('CallEmojiKeyTooltip', user?.firstName).replace('%%', '%')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.userInfo}>
|
||||||
|
<h1>{user?.firstName}</h1>
|
||||||
|
<span className={styles.status}>{callStatus || formatMediaDuration(timeElapsed || 0)}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<PhoneCallButton
|
||||||
|
onClick={handleToggleAudio}
|
||||||
|
icon="microphone"
|
||||||
|
isDisabled={!isActive}
|
||||||
|
isActive={hasOwnAudio}
|
||||||
|
label={lang(hasOwnAudio ? 'lng_call_mute_audio' : 'lng_call_unmute_audio')}
|
||||||
|
/>
|
||||||
|
<PhoneCallButton
|
||||||
|
onClick={handleToggleVideo}
|
||||||
|
icon="video"
|
||||||
|
isDisabled={!isActive}
|
||||||
|
isActive={hasOwnVideo}
|
||||||
|
label={lang(hasOwnVideo ? 'lng_call_stop_video' : 'lng_call_start_video')}
|
||||||
|
/>
|
||||||
|
{hasOwnVideo && (IS_ANDROID || IS_IOS) && (
|
||||||
|
<PhoneCallButton
|
||||||
|
onClick={handleFlipCamera}
|
||||||
|
customIcon={
|
||||||
|
<AnimatedIcon name="CameraFlip" playSegment={!isFlipping ? [0, 1] : [0, 10]} size={32} />
|
||||||
|
}
|
||||||
|
isDisabled={!isActive}
|
||||||
|
label={lang('VoipFlip')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{IS_SCREENSHARE_SUPPORTED && (
|
||||||
|
<PhoneCallButton
|
||||||
|
onClick={handleTogglePresentation}
|
||||||
|
icon="share-screen"
|
||||||
|
isDisabled={!isActive}
|
||||||
|
isActive={hasOwnPresentation}
|
||||||
|
label={lang('lng_call_screencast')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isIncomingRequested && (
|
||||||
|
<PhoneCallButton
|
||||||
|
onClick={acceptCall}
|
||||||
|
icon="phone-discard"
|
||||||
|
isDisabled={isDiscarded}
|
||||||
|
label={lang('lng_call_accept')}
|
||||||
|
className={styles.accept}
|
||||||
|
iconClassName={styles.acceptIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<PhoneCallButton
|
||||||
|
onClick={handleHangUp}
|
||||||
|
icon="phone-discard"
|
||||||
|
isDisabled={isDiscarded}
|
||||||
|
label={lang(isIncomingRequested ? 'lng_call_decline' : 'lng_call_end_call')}
|
||||||
|
className={styles.leave}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal(
|
||||||
|
(global): StateProps => {
|
||||||
|
const { phoneCall, currentUserId } = global;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isCallPanelVisible: Boolean(global.isCallPanelVisible),
|
||||||
|
user: selectPhoneCallUser(global),
|
||||||
|
isOutgoing: phoneCall?.adminId === currentUserId,
|
||||||
|
phoneCall,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(PhoneCall));
|
||||||
36
src/components/calls/phone/PhoneCallButton.module.scss
Normal file
36
src/components/calls/phone/PhoneCallButton.module.scss
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.root {
|
||||||
|
width: 5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #fff !important;
|
||||||
|
color: var(--color-text-secondary) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #ddd !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: lowercase;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
45
src/components/calls/phone/PhoneCallButton.tsx
Normal file
45
src/components/calls/phone/PhoneCallButton.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React, { FC, memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
|
import styles from './PhoneCallButton.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
onClick: VoidFunction;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
iconClassName?: string;
|
||||||
|
customIcon?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
isActive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PhoneCallButton: FC<OwnProps> = ({
|
||||||
|
onClick,
|
||||||
|
label,
|
||||||
|
customIcon,
|
||||||
|
icon,
|
||||||
|
iconClassName,
|
||||||
|
className,
|
||||||
|
isDisabled,
|
||||||
|
isActive,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
className={buildClassName(className, styles.button, isActive && styles.active)}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={isDisabled}
|
||||||
|
>
|
||||||
|
{customIcon || <i className={buildClassName(iconClassName, `icon-${icon}`)} />}
|
||||||
|
</Button>
|
||||||
|
<div className={styles.buttonText}>{label}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(PhoneCallButton);
|
||||||
16
src/components/calls/phone/RatePhoneCallModal.async.tsx
Normal file
16
src/components/calls/phone/RatePhoneCallModal.async.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React, { FC, memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { OwnProps } from './RatePhoneCallModal';
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const RatePhoneCallModalAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const RatePhoneCallModal = useModuleLoader(Bundles.Calls, 'RatePhoneCallModal', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return RatePhoneCallModal ? <RatePhoneCallModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(RatePhoneCallModalAsync);
|
||||||
28
src/components/calls/phone/RatePhoneCallModal.module.scss
Normal file
28
src/components/calls/phone/RatePhoneCallModal.module.scss
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
.stars {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isFilled {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin-top: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:not(.visible) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/components/calls/phone/RatePhoneCallModal.tsx
Normal file
77
src/components/calls/phone/RatePhoneCallModal.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useRef, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
import Modal from '../../ui/Modal';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import InputText from '../../ui/InputText';
|
||||||
|
|
||||||
|
import styles from './RatePhoneCallModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RatePhoneCallModal: FC<OwnProps> = ({
|
||||||
|
isOpen,
|
||||||
|
}) => {
|
||||||
|
const { closeCallRatingModal, setCallRating } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const [rating, setRating] = useState<number | undefined>();
|
||||||
|
|
||||||
|
function handleSend() {
|
||||||
|
if (!rating) {
|
||||||
|
closeCallRatingModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCallRating({
|
||||||
|
rating: rating + 1,
|
||||||
|
comment: inputRef.current?.value || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickStar(index: number) {
|
||||||
|
return () => setRating(rating === index ? undefined : index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={lang('lng_call_rate_label')} className="narrow" onClose={closeCallRatingModal} isOpen={isOpen}>
|
||||||
|
<div className={styles.stars}>
|
||||||
|
{new Array(5).fill(undefined).map((_, i) => {
|
||||||
|
const isFilled = rating !== undefined && rating >= i;
|
||||||
|
return (
|
||||||
|
<i
|
||||||
|
className={buildClassName(
|
||||||
|
isFilled ? 'icon-favorite-filled' : 'icon-favorite',
|
||||||
|
isFilled && styles.isFilled,
|
||||||
|
styles.star,
|
||||||
|
)}
|
||||||
|
onClick={handleClickStar(i)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<InputText
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder={lang('lng_call_rate_comment')}
|
||||||
|
className={buildClassName(styles.comment, rating !== 4 && rating !== undefined && styles.visible)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* eslint-disable-next-line react/jsx-no-bind */}
|
||||||
|
<Button className="confirm-dialog-button" isText onClick={handleSend}>
|
||||||
|
{lang('Send')}
|
||||||
|
</Button>
|
||||||
|
<Button className="confirm-dialog-button" isText onClick={closeCallRatingModal}>{lang('Cancel')}</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(RatePhoneCallModal);
|
||||||
@ -58,11 +58,12 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
const isReplies = user && isChatWithRepliesBot(user.id);
|
const isReplies = user && isChatWithRepliesBot(user.id);
|
||||||
let imageHash: string | undefined;
|
let imageHash: string | undefined;
|
||||||
|
|
||||||
|
const shouldFetchBig = size === 'jumbo';
|
||||||
if (!isSavedMessages && !isDeleted) {
|
if (!isSavedMessages && !isDeleted) {
|
||||||
if (user) {
|
if (user) {
|
||||||
imageHash = getChatAvatarHash(user);
|
imageHash = getChatAvatarHash(user, shouldFetchBig ? 'big' : undefined);
|
||||||
} else if (chat) {
|
} else if (chat) {
|
||||||
imageHash = getChatAvatarHash(chat);
|
imageHash = getChatAvatarHash(chat, shouldFetchBig ? 'big' : undefined);
|
||||||
} else if (photo) {
|
} else if (photo) {
|
||||||
imageHash = `photo${photo.id}?size=m`;
|
imageHash = `photo${photo.id}?size=m`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
}, [lastSyncTime, profileId, loadProfilePhotos]);
|
}, [lastSyncTime, profileId, loadProfilePhotos]);
|
||||||
|
|
||||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Main);
|
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.Main);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastSyncTime) {
|
if (lastSyncTime) {
|
||||||
loadAuthorizations();
|
loadAuthorizations();
|
||||||
|
|||||||
@ -18,8 +18,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.has-group-call-header {
|
.has-call-header {
|
||||||
--group-call-header-height: 2rem;
|
--call-header-height: 2rem;
|
||||||
#LeftColumn, #MiddleColumn, #RightColumn-wrapper {
|
#LeftColumn, #MiddleColumn, #RightColumn-wrapper {
|
||||||
height: calc(100% - 2rem);
|
height: calc(100% - 2rem);
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
|
|||||||
@ -49,8 +49,9 @@ import SafeLinkModal from './SafeLinkModal.async';
|
|||||||
import HistoryCalendar from './HistoryCalendar.async';
|
import HistoryCalendar from './HistoryCalendar.async';
|
||||||
import GroupCall from '../calls/group/GroupCall.async';
|
import GroupCall from '../calls/group/GroupCall.async';
|
||||||
import ActiveCallHeader from '../calls/ActiveCallHeader.async';
|
import ActiveCallHeader from '../calls/ActiveCallHeader.async';
|
||||||
import CallFallbackConfirm from '../calls/CallFallbackConfirm.async';
|
import PhoneCall from '../calls/phone/PhoneCall.async';
|
||||||
import NewContactModal from './NewContactModal.async';
|
import NewContactModal from './NewContactModal.async';
|
||||||
|
import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -74,12 +75,13 @@ type StateProps = {
|
|||||||
animationLevel: number;
|
animationLevel: number;
|
||||||
language?: LangCode;
|
language?: LangCode;
|
||||||
wasTimeFormatSetManually?: boolean;
|
wasTimeFormatSetManually?: boolean;
|
||||||
isCallFallbackConfirmOpen: boolean;
|
isPhoneCallActive?: boolean;
|
||||||
addedSetIds?: string[];
|
addedSetIds?: string[];
|
||||||
newContactUserId?: string;
|
newContactUserId?: string;
|
||||||
newContactByPhoneNumber?: boolean;
|
newContactByPhoneNumber?: boolean;
|
||||||
openedGame?: GlobalState['openedGame'];
|
openedGame?: GlobalState['openedGame'];
|
||||||
gameTitle?: string;
|
gameTitle?: string;
|
||||||
|
isRatePhoneCallModalOpen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NOTIFICATION_INTERVAL = 1000;
|
const NOTIFICATION_INTERVAL = 1000;
|
||||||
@ -109,12 +111,13 @@ const Main: FC<StateProps> = ({
|
|||||||
animationLevel,
|
animationLevel,
|
||||||
language,
|
language,
|
||||||
wasTimeFormatSetManually,
|
wasTimeFormatSetManually,
|
||||||
isCallFallbackConfirmOpen,
|
|
||||||
addedSetIds,
|
addedSetIds,
|
||||||
|
isPhoneCallActive,
|
||||||
newContactUserId,
|
newContactUserId,
|
||||||
newContactByPhoneNumber,
|
newContactByPhoneNumber,
|
||||||
openedGame,
|
openedGame,
|
||||||
gameTitle,
|
gameTitle,
|
||||||
|
isRatePhoneCallModalOpen,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
sync,
|
sync,
|
||||||
@ -335,12 +338,8 @@ const Main: FC<StateProps> = ({
|
|||||||
onClose={handleStickerSetModalClose}
|
onClose={handleStickerSetModalClose}
|
||||||
stickerSetShortName={openedStickerSetShortName}
|
stickerSetShortName={openedStickerSetShortName}
|
||||||
/>
|
/>
|
||||||
{activeGroupCallId && (
|
{activeGroupCallId && <GroupCall groupCallId={activeGroupCallId} />}
|
||||||
<>
|
<ActiveCallHeader isActive={Boolean(activeGroupCallId || isPhoneCallActive)} />
|
||||||
<GroupCall groupCallId={activeGroupCallId} />
|
|
||||||
<ActiveCallHeader groupCallId={activeGroupCallId} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<NewContactModal
|
<NewContactModal
|
||||||
isOpen={Boolean(newContactUserId || newContactByPhoneNumber)}
|
isOpen={Boolean(newContactUserId || newContactByPhoneNumber)}
|
||||||
userId={newContactUserId}
|
userId={newContactUserId}
|
||||||
@ -348,8 +347,9 @@ const Main: FC<StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
||||||
<DownloadManager />
|
<DownloadManager />
|
||||||
<CallFallbackConfirm isOpen={isCallFallbackConfirmOpen} />
|
<PhoneCall isActive={isPhoneCallActive} />
|
||||||
<UnreadCount isForAppBadge />
|
<UnreadCount isForAppBadge />
|
||||||
|
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -406,12 +406,13 @@ export default memo(withGlobal(
|
|||||||
animationLevel,
|
animationLevel,
|
||||||
language,
|
language,
|
||||||
wasTimeFormatSetManually,
|
wasTimeFormatSetManually,
|
||||||
isCallFallbackConfirmOpen: Boolean(global.groupCalls.isFallbackConfirmOpen),
|
isPhoneCallActive: Boolean(global.phoneCall),
|
||||||
addedSetIds: global.stickers.added.setIds,
|
addedSetIds: global.stickers.added.setIds,
|
||||||
newContactUserId: global.newContact?.userId,
|
newContactUserId: global.newContact?.userId,
|
||||||
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
|
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
|
||||||
openedGame,
|
openedGame,
|
||||||
gameTitle,
|
gameTitle,
|
||||||
|
isRatePhoneCallModalOpen: Boolean(global.ratingPhoneCall),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Main));
|
)(Main));
|
||||||
|
|||||||
@ -84,10 +84,9 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
sendBotCommand,
|
sendBotCommand,
|
||||||
openLocalTextSearch,
|
openLocalTextSearch,
|
||||||
restartBot,
|
restartBot,
|
||||||
openCallFallbackConfirm,
|
requestCall,
|
||||||
requestNextManagementScreen,
|
requestNextManagementScreen,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
@ -140,6 +139,10 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [openLocalTextSearch]);
|
}, [openLocalTextSearch]);
|
||||||
|
|
||||||
|
function handleRequestCall() {
|
||||||
|
requestCall({ userId: chatId });
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!canSearch) {
|
if (!canSearch) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -214,7 +217,8 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
round
|
round
|
||||||
color="translucent"
|
color="translucent"
|
||||||
size="smaller"
|
size="smaller"
|
||||||
onClick={openCallFallbackConfirm}
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={handleRequestCall}
|
||||||
ariaLabel="Call"
|
ariaLabel="Call"
|
||||||
>
|
>
|
||||||
<i className="icon-phone" />
|
<i className="icon-phone" />
|
||||||
|
|||||||
@ -110,10 +110,9 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
createGroupCall,
|
createGroupCall,
|
||||||
openLinkedChat,
|
openLinkedChat,
|
||||||
openAddContactDialog,
|
openAddContactDialog,
|
||||||
openCallFallbackConfirm,
|
requestCall,
|
||||||
toggleStatistics,
|
toggleStatistics,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const { x, y } = anchor;
|
const { x, y } = anchor;
|
||||||
@ -177,10 +176,15 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}, [closeMenu, onSubscribeChannel]);
|
}, [closeMenu, onSubscribeChannel]);
|
||||||
|
|
||||||
const handleCall = useCallback(() => {
|
const handleVideoCall = useCallback(() => {
|
||||||
openCallFallbackConfirm();
|
requestCall({ userId: chatId, isVideo: true });
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}, [closeMenu, openCallFallbackConfirm]);
|
}, [chatId, closeMenu, requestCall]);
|
||||||
|
|
||||||
|
const handleCall = useCallback(() => {
|
||||||
|
requestCall({ userId: chatId });
|
||||||
|
closeMenu();
|
||||||
|
}, [chatId, closeMenu, requestCall]);
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
const handleSearch = useCallback(() => {
|
||||||
onSearchClick();
|
onSearchClick();
|
||||||
@ -276,6 +280,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
{lang('Call')}
|
{lang('Call')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{canCall && (
|
||||||
|
<MenuItem
|
||||||
|
icon="video-outlined"
|
||||||
|
onClick={handleVideoCall}
|
||||||
|
>
|
||||||
|
{lang('VideoCall')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{IS_SINGLE_COLUMN_LAYOUT && canSearch && (
|
{IS_SINGLE_COLUMN_LAYOUT && canSearch && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="search"
|
icon="search"
|
||||||
|
|||||||
@ -122,7 +122,12 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
senderGroupIndex,
|
senderGroupIndex,
|
||||||
senderGroupsArray,
|
senderGroupsArray,
|
||||||
) => {
|
) => {
|
||||||
if (senderGroup.length === 1 && !isAlbum(senderGroup[0]) && isActionMessage(senderGroup[0])) {
|
if (
|
||||||
|
senderGroup.length === 1
|
||||||
|
&& !isAlbum(senderGroup[0])
|
||||||
|
&& isActionMessage(senderGroup[0])
|
||||||
|
&& !senderGroup[0].content.action?.phoneCall
|
||||||
|
) {
|
||||||
const message = senderGroup[0];
|
const message = senderGroup[0];
|
||||||
const isLastInList = (
|
const isLastInList = (
|
||||||
senderGroupIndex === senderGroupsArray.length - 1
|
senderGroupIndex === senderGroupsArray.length - 1
|
||||||
|
|||||||
@ -114,6 +114,7 @@ import CommentButton from './CommentButton';
|
|||||||
import Reactions from './Reactions';
|
import Reactions from './Reactions';
|
||||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
import LocalAnimatedEmoji from '../../common/LocalAnimatedEmoji';
|
import LocalAnimatedEmoji from '../../common/LocalAnimatedEmoji';
|
||||||
|
import MessagePhoneCall from './MessagePhoneCall';
|
||||||
|
|
||||||
import './Message.scss';
|
import './Message.scss';
|
||||||
|
|
||||||
@ -354,9 +355,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
&& forwardInfo.fromMessageId
|
&& forwardInfo.fromMessageId
|
||||||
));
|
));
|
||||||
|
|
||||||
const withCommentButton = threadInfo && !isInDocumentGroupNotLast && messageListType === 'thread' && !noComments;
|
|
||||||
const withQuickReactionButton = !IS_TOUCH_ENV && !isInSelectMode && defaultReaction && !isInDocumentGroupNotLast;
|
|
||||||
|
|
||||||
const selectMessage = useCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
const selectMessage = useCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
|
||||||
toggleMessageSelection({
|
toggleMessageSelection({
|
||||||
messageId,
|
messageId,
|
||||||
@ -462,9 +460,15 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, game,
|
text, photo, video, audio, voice, document, sticker, contact, poll, webPage, invoice, location, action, game,
|
||||||
} = getMessageContent(message);
|
} = getMessageContent(message);
|
||||||
|
|
||||||
|
const { phoneCall } = action || {};
|
||||||
|
|
||||||
|
const withCommentButton = threadInfo && !isInDocumentGroupNotLast && messageListType === 'thread' && !noComments;
|
||||||
|
const withQuickReactionButton = !IS_TOUCH_ENV && !phoneCall && !isInSelectMode && defaultReaction
|
||||||
|
&& !isInDocumentGroupNotLast;
|
||||||
|
|
||||||
const contentClassName = buildContentClassName(message, {
|
const contentClassName = buildContentClassName(message, {
|
||||||
hasReply,
|
hasReply,
|
||||||
customShape,
|
customShape,
|
||||||
@ -482,7 +486,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
const textParts = renderMessageText(message, highlight, isEmojiOnlyMessage(customShape));
|
const textParts = renderMessageText(message, highlight, isEmojiOnlyMessage(customShape));
|
||||||
|
|
||||||
let metaPosition!: MetaPosition;
|
let metaPosition!: MetaPosition;
|
||||||
if (isInDocumentGroupNotLast) {
|
if (phoneCall) {
|
||||||
|
metaPosition = 'none';
|
||||||
|
} else if (isInDocumentGroupNotLast) {
|
||||||
metaPosition = 'none';
|
metaPosition = 'none';
|
||||||
} else if (textParts && !hasAnimatedEmoji && !webPage) {
|
} else if (textParts && !hasAnimatedEmoji && !webPage) {
|
||||||
metaPosition = 'in-text';
|
metaPosition = 'in-text';
|
||||||
@ -680,6 +686,13 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
onMediaClick={handleAlbumMediaClick}
|
onMediaClick={handleAlbumMediaClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{phoneCall && (
|
||||||
|
<MessagePhoneCall
|
||||||
|
message={message}
|
||||||
|
phoneCall={phoneCall}
|
||||||
|
chatId={chatId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!isAlbum && photo && (
|
{!isAlbum && photo && (
|
||||||
<Photo
|
<Photo
|
||||||
message={message}
|
message={message}
|
||||||
|
|||||||
49
src/components/middle/message/MessagePhoneCall.module.scss
Normal file
49
src/components/middle/message/MessagePhoneCall.module.scss
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
color: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin-inline-end: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotateZ(-45deg);
|
||||||
|
color: #4fae4e;
|
||||||
|
|
||||||
|
&.incoming {
|
||||||
|
transform: rotateZ(135deg);
|
||||||
|
|
||||||
|
&.missed {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
margin-inline-start: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.own) .duration {
|
||||||
|
color: var(--color-message-meta-own);
|
||||||
|
}
|
||||||
89
src/components/middle/message/MessagePhoneCall.tsx
Normal file
89
src/components/middle/message/MessagePhoneCall.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React, {
|
||||||
|
FC, memo, useCallback, useMemo,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import { ApiMessage, PhoneCallAction } from '../../../api/types';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatTimeDuration, formatTime } from '../../../util/dateFormat';
|
||||||
|
import { ARE_CALLS_SUPPORTED } from '../../../util/environment';
|
||||||
|
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
|
import styles from './MessagePhoneCall.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
phoneCall: PhoneCallAction;
|
||||||
|
message: ApiMessage;
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MessagePhoneCall: FC<OwnProps> = ({
|
||||||
|
phoneCall,
|
||||||
|
message,
|
||||||
|
chatId,
|
||||||
|
}) => {
|
||||||
|
const { requestCall } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
const { isOutgoing, isVideo, reason } = phoneCall;
|
||||||
|
const isMissed = reason === 'missed';
|
||||||
|
const isCancelled = reason === 'busy' && !isOutgoing;
|
||||||
|
|
||||||
|
const handleCall = useCallback(() => {
|
||||||
|
requestCall({ isVideo, userId: chatId });
|
||||||
|
}, [chatId, isVideo, requestCall]);
|
||||||
|
|
||||||
|
const reasonText = useMemo(() => {
|
||||||
|
if (isVideo) {
|
||||||
|
if (isCancelled) return 'CallMessageVideoIncomingDeclined';
|
||||||
|
if (isMissed) return isOutgoing ? 'CallMessageVideoOutgoingMissed' : 'CallMessageVideoIncomingMissed';
|
||||||
|
|
||||||
|
return isOutgoing ? 'CallMessageVideoOutgoing' : 'CallMessageVideoIncoming';
|
||||||
|
} else {
|
||||||
|
if (isCancelled) return 'CallMessageIncomingDeclined';
|
||||||
|
if (isMissed) return isOutgoing ? 'CallMessageOutgoingMissed' : 'CallMessageIncomingMissed';
|
||||||
|
|
||||||
|
return isOutgoing ? 'CallMessageOutgoing' : 'CallMessageIncoming';
|
||||||
|
}
|
||||||
|
}, [isCancelled, isMissed, isOutgoing, isVideo]);
|
||||||
|
|
||||||
|
const duration = useMemo(() => {
|
||||||
|
return phoneCall.duration ? formatTimeDuration(lang, phoneCall.duration) : undefined;
|
||||||
|
}, [lang, phoneCall.duration]);
|
||||||
|
|
||||||
|
const timeFormatted = formatTime(lang, message.date * 1000);
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<Button
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
round
|
||||||
|
ripple
|
||||||
|
onClick={handleCall}
|
||||||
|
className={styles.button}
|
||||||
|
disabled={!ARE_CALLS_SUPPORTED}
|
||||||
|
ariaLabel={lang(isOutgoing ? 'CallAgain' : 'CallBack')}
|
||||||
|
>
|
||||||
|
<i className={isVideo ? 'icon-video-outlined' : 'icon-phone'} />
|
||||||
|
</Button>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.reason}>{lang(reasonText)}</div>
|
||||||
|
<div className={styles.meta}>
|
||||||
|
<i
|
||||||
|
className={buildClassName(
|
||||||
|
'icon-arrow-right', styles.arrow, isMissed && styles.missed, !isOutgoing && styles.incoming,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className={styles.duration}>
|
||||||
|
{duration ? lang('CallMessageWithDuration', [timeFormatted, duration]) : timeFormatted}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MessagePhoneCall);
|
||||||
@ -2,7 +2,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
transform: translateY(calc(5rem - var(--group-call-header-height, 0rem)));
|
transform: translateY(calc(5rem - var(--call-header-height, 0rem)));
|
||||||
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
body.animation-level-0 & {
|
body.animation-level-0 & {
|
||||||
@ -10,6 +10,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.revealed {
|
&.revealed {
|
||||||
transform: translateY(calc(0rem - var(--group-call-header-height, 0rem)));
|
transform: translateY(calc(0rem - var(--call-header-height, 0rem)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,87 +5,22 @@ import {
|
|||||||
leaveGroupCall,
|
leaveGroupCall,
|
||||||
toggleStream,
|
toggleStream,
|
||||||
isStreamEnabled,
|
isStreamEnabled,
|
||||||
setVolume,
|
setVolume, stopPhoneCall,
|
||||||
handleUpdateGroupCallParticipants, handleUpdateGroupCallConnection,
|
|
||||||
} from '../../../lib/secret-sauce';
|
} from '../../../lib/secret-sauce';
|
||||||
|
|
||||||
import { GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
import { GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
|
import { selectChat, selectUser } from '../../selectors';
|
||||||
import {
|
import {
|
||||||
selectActiveGroupCall,
|
selectActiveGroupCall, selectPhoneCallUser,
|
||||||
selectCallFallbackChannelTitle,
|
|
||||||
selectGroupCallParticipant,
|
|
||||||
} from '../../selectors/calls';
|
} from '../../selectors/calls';
|
||||||
import {
|
import {
|
||||||
removeGroupCall,
|
removeGroupCall,
|
||||||
updateActiveGroupCall,
|
updateActiveGroupCall,
|
||||||
updateGroupCall,
|
|
||||||
updateGroupCallParticipant,
|
|
||||||
} from '../../reducers/calls';
|
} from '../../reducers/calls';
|
||||||
import { omit } from '../../../util/iteratees';
|
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
|
||||||
import { fetchFile } from '../../../util/files';
|
|
||||||
import { getGroupCallAudioContext, getGroupCallAudioElement, removeGroupCallAudioElement } from '../ui/calls';
|
import { getGroupCallAudioContext, getGroupCallAudioElement, removeGroupCallAudioElement } from '../ui/calls';
|
||||||
import { loadFullChat } from './chats';
|
import { loadFullChat } from './chats';
|
||||||
|
|
||||||
import callFallbackAvatarPath from '../../../assets/call-fallback-avatar.png';
|
|
||||||
|
|
||||||
const FALLBACK_INVITE_EXPIRE_SECONDS = 1800; // 30 min
|
|
||||||
|
|
||||||
addActionHandler('apiUpdate', (global, actions, update) => {
|
|
||||||
const { activeGroupCallId } = global.groupCalls;
|
|
||||||
|
|
||||||
switch (update['@type']) {
|
|
||||||
case 'updateGroupCallLeavePresentation': {
|
|
||||||
actions.toggleGroupCallPresentation({ value: false });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'updateGroupCallStreams': {
|
|
||||||
if (!update.userId || !activeGroupCallId) break;
|
|
||||||
if (!selectGroupCallParticipant(global, activeGroupCallId, update.userId)) break;
|
|
||||||
|
|
||||||
return updateGroupCallParticipant(global, activeGroupCallId, update.userId, omit(update, ['@type', 'userId']));
|
|
||||||
}
|
|
||||||
case 'updateGroupCallConnectionState': {
|
|
||||||
if (!activeGroupCallId) break;
|
|
||||||
|
|
||||||
if (update.connectionState === 'disconnected') {
|
|
||||||
actions.leaveGroupCall({ isFromLibrary: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateGroupCall(global, activeGroupCallId, {
|
|
||||||
connectionState: update.connectionState,
|
|
||||||
isSpeakerDisabled: update.isSpeakerDisabled,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
case 'updateGroupCallParticipants': {
|
|
||||||
const { groupCallId, participants } = update;
|
|
||||||
if (activeGroupCallId === groupCallId) {
|
|
||||||
void handleUpdateGroupCallParticipants(participants);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'updateGroupCallConnection': {
|
|
||||||
if (update.data.stream) {
|
|
||||||
actions.showNotification({ message: 'Big live streams are not yet supported' });
|
|
||||||
actions.leaveGroupCall();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
void handleUpdateGroupCallConnection(update.data, update.presentation);
|
|
||||||
|
|
||||||
const groupCall = selectActiveGroupCall(global);
|
|
||||||
if (groupCall?.participants && Object.keys(groupCall.participants).length > 0) {
|
|
||||||
void handleUpdateGroupCallParticipants(Object.values(groupCall.participants));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('leaveGroupCall', async (global, actions, payload) => {
|
addActionHandler('leaveGroupCall', async (global, actions, payload) => {
|
||||||
const {
|
const {
|
||||||
isFromLibrary, shouldDiscard, shouldRemove, rejoin,
|
isFromLibrary, shouldDiscard, shouldRemove, rejoin,
|
||||||
@ -101,18 +36,7 @@ addActionHandler('leaveGroupCall', async (global, actions, payload) => {
|
|||||||
call: groupCall,
|
call: groupCall,
|
||||||
});
|
});
|
||||||
|
|
||||||
let shouldResetFallbackState = false;
|
|
||||||
if (shouldDiscard) {
|
if (shouldDiscard) {
|
||||||
global = getGlobal();
|
|
||||||
|
|
||||||
if (global.groupCalls.fallbackChatId === groupCall.chatId) {
|
|
||||||
shouldResetFallbackState = true;
|
|
||||||
|
|
||||||
global.groupCalls.fallbackUserIdsToRemove?.forEach((userId) => {
|
|
||||||
actions.deleteChatMember({ chatId: global.groupCalls.fallbackChatId, userId });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await callApi('discardGroupCall', {
|
await callApi('discardGroupCall', {
|
||||||
call: groupCall,
|
call: groupCall,
|
||||||
});
|
});
|
||||||
@ -129,13 +53,9 @@ addActionHandler('leaveGroupCall', async (global, actions, payload) => {
|
|||||||
...global,
|
...global,
|
||||||
groupCalls: {
|
groupCalls: {
|
||||||
...global.groupCalls,
|
...global.groupCalls,
|
||||||
isGroupCallPanelHidden: true,
|
|
||||||
activeGroupCallId: undefined,
|
activeGroupCallId: undefined,
|
||||||
...(shouldResetFallbackState && {
|
|
||||||
fallbackChatId: undefined,
|
|
||||||
fallbackUserIdsToRemove: undefined,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
|
isCallPanelVisible: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isFromLibrary) {
|
if (!isFromLibrary) {
|
||||||
@ -292,79 +212,106 @@ addActionHandler('connectToActiveGroupCall', async (global, actions) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('inviteToCallFallback', async (global, actions, payload) => {
|
addActionHandler('connectToActivePhoneCall', async (global) => {
|
||||||
const { chatId } = selectCurrentMessageList(global) || {};
|
const { phoneCall } = global;
|
||||||
if (!chatId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = selectUser(global, chatId);
|
if (!phoneCall) return;
|
||||||
if (!user) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { shouldRemove } = payload;
|
const user = selectPhoneCallUser(global);
|
||||||
|
|
||||||
const fallbackChannelTitle = selectCallFallbackChannelTitle(global);
|
if (!user) return;
|
||||||
|
|
||||||
let fallbackChannel = Object.values(global.chats.byId).find((channel) => {
|
const dhConfig = await callApi('getDhConfig');
|
||||||
return (
|
|
||||||
channel.title === fallbackChannelTitle
|
|
||||||
&& channel.isCreator
|
|
||||||
&& !channel.isRestricted
|
|
||||||
&& !channel.isForbidden
|
|
||||||
);
|
|
||||||
});
|
|
||||||
if (!fallbackChannel) {
|
|
||||||
fallbackChannel = await callApi('createChannel', {
|
|
||||||
title: fallbackChannelTitle,
|
|
||||||
users: [user],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fallbackChannel) {
|
if (!dhConfig) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const photo = await fetchFile(callFallbackAvatarPath, 'avatar.png');
|
await callApi('createPhoneCallState', [true]);
|
||||||
void callApi('editChatPhoto', {
|
|
||||||
chatId: fallbackChannel.id,
|
|
||||||
accessHash: fallbackChannel.accessHash,
|
|
||||||
photo,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
actions.updateChatMemberBannedRights({
|
|
||||||
chatId: fallbackChannel.id,
|
|
||||||
userId: chatId,
|
|
||||||
bannedRights: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
void callApi('addChatMembers', fallbackChannel, [user], true);
|
const gAHash = await callApi('requestPhoneCall', [dhConfig])!;
|
||||||
}
|
|
||||||
|
|
||||||
const inviteLink = await callApi('updatePrivateLink', {
|
await callApi('requestCall', { user, gAHash, isVideo: phoneCall.isVideo });
|
||||||
chat: fallbackChannel,
|
});
|
||||||
usageLimit: 1,
|
|
||||||
expireDate: getServerTime(global.serverTimeOffset) + FALLBACK_INVITE_EXPIRE_SECONDS,
|
addActionHandler('acceptCall', async (global) => {
|
||||||
});
|
const { phoneCall } = global;
|
||||||
if (!inviteLink) {
|
|
||||||
return;
|
if (!phoneCall) return;
|
||||||
}
|
|
||||||
|
const dhConfig = await callApi('getDhConfig');
|
||||||
if (shouldRemove) {
|
if (!dhConfig) return;
|
||||||
global = getGlobal();
|
|
||||||
const fallbackUserIdsToRemove = global.groupCalls.fallbackUserIdsToRemove || [];
|
await callApi('createPhoneCallState', [false]);
|
||||||
setGlobal({
|
|
||||||
...global,
|
const gB = await callApi('acceptPhoneCall', [dhConfig])!;
|
||||||
groupCalls: {
|
callApi('acceptCall', { call: phoneCall, gB });
|
||||||
...global.groupCalls,
|
});
|
||||||
fallbackChatId: fallbackChannel.id,
|
|
||||||
fallbackUserIdsToRemove: [...fallbackUserIdsToRemove, chatId],
|
addActionHandler('sendSignalingData', (global, actions, payload) => {
|
||||||
},
|
const { phoneCall } = global;
|
||||||
});
|
if (!phoneCall) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
actions.sendMessage({ text: `Join a call: ${inviteLink}` });
|
|
||||||
actions.openChat({ id: fallbackChannel.id });
|
const data = JSON.stringify(payload);
|
||||||
actions.createGroupCall({ chatId: fallbackChannel.id });
|
|
||||||
actions.closeCallFallbackConfirm();
|
(async () => {
|
||||||
|
const encodedData = await callApi('encodePhoneCallData', [data]);
|
||||||
|
|
||||||
|
if (!encodedData) return;
|
||||||
|
|
||||||
|
callApi('sendSignalingData', { data: encodedData, call: phoneCall });
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeCallRatingModal', (global) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
ratingPhoneCall: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('setCallRating', (global, actions, payload) => {
|
||||||
|
const { ratingPhoneCall } = global;
|
||||||
|
if (!ratingPhoneCall) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rating, comment } = payload;
|
||||||
|
|
||||||
|
callApi('setCallRating', { call: ratingPhoneCall, rating, comment });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
ratingPhoneCall: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('hangUp', (global) => {
|
||||||
|
const { phoneCall } = global;
|
||||||
|
|
||||||
|
if (!phoneCall) return undefined;
|
||||||
|
|
||||||
|
if (phoneCall.state === 'discarded') {
|
||||||
|
callApi('destroyPhoneCallState');
|
||||||
|
stopPhoneCall();
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
phoneCall: undefined,
|
||||||
|
isCallPanelVisible: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
callApi('destroyPhoneCallState');
|
||||||
|
stopPhoneCall();
|
||||||
|
callApi('discardCall', { call: phoneCall });
|
||||||
|
|
||||||
|
if (phoneCall.state === 'requesting') {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
phoneCall: undefined,
|
||||||
|
isCallPanelVisible: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|||||||
202
src/global/actions/apiUpdaters/calls.async.ts
Normal file
202
src/global/actions/apiUpdaters/calls.async.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
|
import { selectActiveGroupCall, selectGroupCallParticipant, selectPhoneCallUser } from '../../selectors/calls';
|
||||||
|
import { updateGroupCall, updateGroupCallParticipant } from '../../reducers/calls';
|
||||||
|
import { omit } from '../../../util/iteratees';
|
||||||
|
import {
|
||||||
|
ApiCallProtocol,
|
||||||
|
handleUpdateGroupCallConnection,
|
||||||
|
handleUpdateGroupCallParticipants,
|
||||||
|
joinPhoneCall, processSignalingMessage,
|
||||||
|
} from '../../../lib/secret-sauce';
|
||||||
|
import { ApiPhoneCall } from '../../../api/types';
|
||||||
|
import { ARE_CALLS_SUPPORTED } from '../../../util/environment';
|
||||||
|
import { callApi } from '../../../api/gramjs';
|
||||||
|
import * as langProvider from '../../../util/langProvider';
|
||||||
|
import { EMOJI_DATA, EMOJI_OFFSETS } from '../../../util/phoneCallEmojiConstants';
|
||||||
|
|
||||||
|
addActionHandler('apiUpdate', (global, actions, update) => {
|
||||||
|
const { activeGroupCallId } = global.groupCalls;
|
||||||
|
|
||||||
|
switch (update['@type']) {
|
||||||
|
case 'updateGroupCallLeavePresentation': {
|
||||||
|
actions.toggleGroupCallPresentation({ value: false });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'updateGroupCallStreams': {
|
||||||
|
if (!update.userId || !activeGroupCallId) break;
|
||||||
|
if (!selectGroupCallParticipant(global, activeGroupCallId, update.userId)) break;
|
||||||
|
|
||||||
|
return updateGroupCallParticipant(global, activeGroupCallId, update.userId, omit(update, ['@type', 'userId']));
|
||||||
|
}
|
||||||
|
case 'updateGroupCallConnectionState': {
|
||||||
|
if (!activeGroupCallId) break;
|
||||||
|
|
||||||
|
if (update.connectionState === 'disconnected') {
|
||||||
|
actions.leaveGroupCall({ isFromLibrary: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateGroupCall(global, activeGroupCallId, {
|
||||||
|
connectionState: update.connectionState,
|
||||||
|
isSpeakerDisabled: update.isSpeakerDisabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case 'updateGroupCallParticipants': {
|
||||||
|
const { groupCallId, participants } = update;
|
||||||
|
if (activeGroupCallId === groupCallId) {
|
||||||
|
void handleUpdateGroupCallParticipants(participants);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'updateGroupCallConnection': {
|
||||||
|
if (update.data.stream) {
|
||||||
|
actions.showNotification({ message: 'Big live streams are not yet supported' });
|
||||||
|
actions.leaveGroupCall();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
void handleUpdateGroupCallConnection(update.data, update.presentation);
|
||||||
|
|
||||||
|
const groupCall = selectActiveGroupCall(global);
|
||||||
|
if (groupCall?.participants && Object.keys(groupCall.participants).length > 0) {
|
||||||
|
void handleUpdateGroupCallParticipants(Object.values(groupCall.participants));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'updatePhoneCallMediaState':
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
phoneCall: {
|
||||||
|
...global.phoneCall,
|
||||||
|
...omit(update, ['@type']),
|
||||||
|
} as ApiPhoneCall,
|
||||||
|
};
|
||||||
|
case 'updatePhoneCall': {
|
||||||
|
if (!ARE_CALLS_SUPPORTED) return undefined;
|
||||||
|
const { phoneCall, currentUserId } = global;
|
||||||
|
|
||||||
|
const call: ApiPhoneCall = {
|
||||||
|
...phoneCall,
|
||||||
|
...update.call,
|
||||||
|
};
|
||||||
|
|
||||||
|
const isOutgoing = phoneCall?.adminId === currentUserId;
|
||||||
|
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
phoneCall: call,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (phoneCall && phoneCall.id && call.id !== phoneCall.id) {
|
||||||
|
if (call.state !== 'discarded') {
|
||||||
|
callApi('discardCall', {
|
||||||
|
call,
|
||||||
|
isBusy: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
accessHash, state, connections, gB,
|
||||||
|
} = call;
|
||||||
|
|
||||||
|
if (state === 'active' || state === 'accepted') {
|
||||||
|
if (!verifyPhoneCallProtocol(call.protocol)) {
|
||||||
|
const user = selectPhoneCallUser(global);
|
||||||
|
actions.hangUp();
|
||||||
|
actions.showNotification({ message: langProvider.getTranslation('VoipPeerIncompatible', user?.firstName) });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'discarded') {
|
||||||
|
// Discarded from other device
|
||||||
|
if (!phoneCall) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
...(call.needRating && { ratingPhoneCall: call }),
|
||||||
|
isCallPanelVisible: undefined,
|
||||||
|
};
|
||||||
|
} else if (state === 'accepted' && accessHash && gB) {
|
||||||
|
(async () => {
|
||||||
|
const { gA, keyFingerprint, emojis } = await callApi('confirmPhoneCall', [gB, EMOJI_DATA, EMOJI_OFFSETS])!;
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
const newCall = {
|
||||||
|
...global.phoneCall,
|
||||||
|
emojis,
|
||||||
|
} as ApiPhoneCall;
|
||||||
|
|
||||||
|
setGlobal({
|
||||||
|
...global,
|
||||||
|
phoneCall: newCall,
|
||||||
|
});
|
||||||
|
|
||||||
|
callApi('confirmCall', {
|
||||||
|
call, gA, keyFingerprint,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
} else if (state === 'active' && connections && phoneCall?.state !== 'active') {
|
||||||
|
if (!isOutgoing) {
|
||||||
|
callApi('receivedCall', { call });
|
||||||
|
(async () => {
|
||||||
|
const { emojis } = await callApi('confirmPhoneCall', [call!.gAOrB!, EMOJI_DATA, EMOJI_OFFSETS])!;
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
const newCall = {
|
||||||
|
...global.phoneCall,
|
||||||
|
emojis,
|
||||||
|
} as ApiPhoneCall;
|
||||||
|
|
||||||
|
setGlobal({
|
||||||
|
...global,
|
||||||
|
phoneCall: newCall,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
void joinPhoneCall(
|
||||||
|
connections, actions.sendSignalingData, isOutgoing, Boolean(call?.isVideo), actions.apiUpdate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
case 'updatePhoneCallConnectionState': {
|
||||||
|
const { connectionState } = update;
|
||||||
|
|
||||||
|
if (!global.phoneCall) return global;
|
||||||
|
|
||||||
|
if (connectionState === 'closed' || connectionState === 'disconnected' || connectionState === 'failed') {
|
||||||
|
actions.hangUp();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
phoneCall: {
|
||||||
|
...global.phoneCall,
|
||||||
|
isConnected: connectionState === 'connected',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case 'updatePhoneCallSignalingData': {
|
||||||
|
const { phoneCall } = global;
|
||||||
|
|
||||||
|
if (!phoneCall) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
callApi('decodePhoneCallData', [update.data])?.then(processSignalingMessage);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
function verifyPhoneCallProtocol(protocol?: ApiCallProtocol) {
|
||||||
|
return protocol?.libraryVersions.some((version) => {
|
||||||
|
return version === '4.0.0' || version === '4.0.1';
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -3,6 +3,10 @@ import { removeGroupCall, updateGroupCall, updateGroupCallParticipant } from '..
|
|||||||
import { omit } from '../../../util/iteratees';
|
import { omit } from '../../../util/iteratees';
|
||||||
import { selectChat } from '../../selectors';
|
import { selectChat } from '../../selectors';
|
||||||
import { updateChat } from '../../reducers';
|
import { updateChat } from '../../reducers';
|
||||||
|
import { ARE_CALLS_SUPPORTED } from '../../../util/environment';
|
||||||
|
import { notifyAboutCall } from '../../../util/notifications';
|
||||||
|
import { selectPhoneCallUser } from '../../selectors/calls';
|
||||||
|
import { initializeSoundsForSafari } from '../ui/calls';
|
||||||
|
|
||||||
addActionHandler('apiUpdate', (global, actions, update) => {
|
addActionHandler('apiUpdate', (global, actions, update) => {
|
||||||
switch (update['@type']) {
|
switch (update['@type']) {
|
||||||
@ -54,6 +58,32 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
}
|
}
|
||||||
return global;
|
return global;
|
||||||
}
|
}
|
||||||
|
case 'updatePhoneCall': {
|
||||||
|
if (!ARE_CALLS_SUPPORTED) return undefined;
|
||||||
|
|
||||||
|
const {
|
||||||
|
phoneCall,
|
||||||
|
currentUserId,
|
||||||
|
} = global;
|
||||||
|
|
||||||
|
if (phoneCall) return undefined;
|
||||||
|
|
||||||
|
const { call } = update;
|
||||||
|
const isOutgoing = call?.adminId === currentUserId;
|
||||||
|
|
||||||
|
if (!isOutgoing && call.state === 'requested') {
|
||||||
|
notifyAboutCall({
|
||||||
|
call,
|
||||||
|
user: selectPhoneCallUser(global)!,
|
||||||
|
});
|
||||||
|
void initializeSoundsForSafari();
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
phoneCall: call,
|
||||||
|
isCallPanelVisible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
import './api/calls.async';
|
import './api/calls.async';
|
||||||
|
import './apiUpdaters/calls.async';
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
import { selectActiveGroupCall, selectChatGroupCall, selectGroupCall } from '../../selectors/calls';
|
import { selectActiveGroupCall, selectChatGroupCall, selectGroupCall } from '../../selectors/calls';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { selectChat } from '../../selectors';
|
import { selectChat, selectUser } from '../../selectors';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
import { ApiGroupCall } from '../../../api/types';
|
import { ApiGroupCall } from '../../../api/types';
|
||||||
import { updateGroupCall } from '../../reducers/calls';
|
import { updateGroupCall } from '../../reducers/calls';
|
||||||
@ -11,29 +11,43 @@ import { fetchChatByUsername, loadFullChat } from '../api/chats';
|
|||||||
import safePlay from '../../../util/safePlay';
|
import safePlay from '../../../util/safePlay';
|
||||||
import { ARE_CALLS_SUPPORTED } from '../../../util/environment';
|
import { ARE_CALLS_SUPPORTED } from '../../../util/environment';
|
||||||
import * as langProvider from '../../../util/langProvider';
|
import * as langProvider from '../../../util/langProvider';
|
||||||
|
import { CallSound } from '../../types';
|
||||||
|
|
||||||
// Workaround for Safari not playing audio without user interaction
|
// Workaround for Safari not playing audio without user interaction
|
||||||
let audioElement: HTMLAudioElement | undefined;
|
let audioElement: HTMLAudioElement | undefined;
|
||||||
let audioContext: AudioContext | undefined;
|
let audioContext: AudioContext | undefined;
|
||||||
|
|
||||||
const joinAudio = new Audio('./voicechat_join.mp3');
|
let sounds: Record<CallSound, HTMLAudioElement>;
|
||||||
const connectingAudio = new Audio('./voicechat_connecting.mp3');
|
|
||||||
connectingAudio.loop = true;
|
|
||||||
const leaveAudio = new Audio('./voicechat_leave.mp3');
|
|
||||||
const allowTalkAudio = new Audio('./voicechat_onallowtalk.mp3');
|
|
||||||
|
|
||||||
const sounds: Record<string, HTMLAudioElement> = {
|
|
||||||
join: joinAudio,
|
|
||||||
allowTalk: allowTalkAudio,
|
|
||||||
leave: leaveAudio,
|
|
||||||
connecting: connectingAudio,
|
|
||||||
};
|
|
||||||
|
|
||||||
let initializationPromise: Promise<void> | undefined = Promise.resolve();
|
let initializationPromise: Promise<void> | undefined = Promise.resolve();
|
||||||
|
|
||||||
const initializeSoundsForSafari = () => {
|
export const initializeSoundsForSafari = () => {
|
||||||
if (!initializationPromise) return Promise.resolve();
|
if (!initializationPromise) return Promise.resolve();
|
||||||
|
|
||||||
|
const joinAudio = new Audio('./voicechat_join.mp3');
|
||||||
|
const connectingAudio = new Audio('./voicechat_connecting.mp3');
|
||||||
|
connectingAudio.loop = true;
|
||||||
|
const leaveAudio = new Audio('./voicechat_leave.mp3');
|
||||||
|
const allowTalkAudio = new Audio('./voicechat_onallowtalk.mp3');
|
||||||
|
const busyAudio = new Audio('./call_busy.mp3');
|
||||||
|
const connectAudio = new Audio('./call_connect.mp3');
|
||||||
|
const endAudio = new Audio('./call_end.mp3');
|
||||||
|
const incomingAudio = new Audio('./call_incoming.mp3');
|
||||||
|
incomingAudio.loop = true;
|
||||||
|
const ringingAudio = new Audio('./call_ringing.mp3');
|
||||||
|
ringingAudio.loop = true;
|
||||||
|
|
||||||
|
sounds = {
|
||||||
|
join: joinAudio,
|
||||||
|
allowTalk: allowTalkAudio,
|
||||||
|
leave: leaveAudio,
|
||||||
|
connecting: connectingAudio,
|
||||||
|
incoming: incomingAudio,
|
||||||
|
end: endAudio,
|
||||||
|
connect: connectAudio,
|
||||||
|
busy: busyAudio,
|
||||||
|
ringing: ringingAudio,
|
||||||
|
};
|
||||||
|
|
||||||
initializationPromise = Promise.all(Object.values(sounds).map((l) => {
|
initializationPromise = Promise.all(Object.values(sounds).map((l) => {
|
||||||
l.muted = true;
|
l.muted = true;
|
||||||
l.volume = 0.0001;
|
l.volume = 0.0001;
|
||||||
@ -95,10 +109,7 @@ async function fetchGroupCallParticipants(groupCall: Partial<ApiGroupCall>, next
|
|||||||
addActionHandler('toggleGroupCallPanel', (global) => {
|
addActionHandler('toggleGroupCallPanel', (global) => {
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
groupCalls: {
|
isCallPanelVisible: !global.isCallPanelVisible,
|
||||||
...global.groupCalls,
|
|
||||||
isGroupCallPanelHidden: !global.groupCalls.isGroupCallPanelHidden,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -194,6 +205,11 @@ addActionHandler('joinVoiceChatByLink', async (global, actions, payload) => {
|
|||||||
addActionHandler('joinGroupCall', async (global, actions, payload) => {
|
addActionHandler('joinGroupCall', async (global, actions, payload) => {
|
||||||
if (!ARE_CALLS_SUPPORTED) return undefined;
|
if (!ARE_CALLS_SUPPORTED) return undefined;
|
||||||
|
|
||||||
|
if (global.phoneCall) {
|
||||||
|
actions.toggleGroupCallPanel();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
chatId, id, accessHash, inviteHash,
|
chatId, id, accessHash, inviteHash,
|
||||||
} = payload;
|
} = payload;
|
||||||
@ -246,8 +262,8 @@ addActionHandler('joinGroupCall', async (global, actions, payload) => {
|
|||||||
groupCalls: {
|
groupCalls: {
|
||||||
...global.groupCalls,
|
...global.groupCalls,
|
||||||
activeGroupCallId: groupCall.id,
|
activeGroupCallId: groupCall.id,
|
||||||
isGroupCallPanelHidden: false,
|
|
||||||
},
|
},
|
||||||
|
isCallPanelVisible: false,
|
||||||
};
|
};
|
||||||
return global;
|
return global;
|
||||||
});
|
});
|
||||||
@ -259,15 +275,23 @@ addActionHandler('playGroupCallSound', (global, actions, payload) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initializationPromise) {
|
const doPlay = () => {
|
||||||
initializationPromise.then(() => {
|
|
||||||
safePlay(sounds[sound]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (sound !== 'connecting') {
|
if (sound !== 'connecting') {
|
||||||
sounds.connecting.pause();
|
sounds.connecting.pause();
|
||||||
}
|
}
|
||||||
|
if (sound !== 'incoming') {
|
||||||
|
sounds.incoming.pause();
|
||||||
|
}
|
||||||
|
if (sound !== 'ringing') {
|
||||||
|
sounds.ringing.pause();
|
||||||
|
}
|
||||||
safePlay(sounds[sound]);
|
safePlay(sounds[sound]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (initializationPromise) {
|
||||||
|
initializationPromise.then(doPlay);
|
||||||
|
} else {
|
||||||
|
doPlay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -280,6 +304,35 @@ addActionHandler('loadMoreGroupCallParticipants', (global) => {
|
|||||||
void fetchGroupCallParticipants(groupCall, groupCall.nextOffset);
|
void fetchGroupCallParticipants(groupCall, groupCall.nextOffset);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('requestCall', async (global, actions, payload) => {
|
||||||
|
const { userId, isVideo } = payload;
|
||||||
|
|
||||||
|
if (global.phoneCall) {
|
||||||
|
actions.toggleGroupCallPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = selectUser(global, userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await initializeSoundsForSafari();
|
||||||
|
|
||||||
|
setGlobal({
|
||||||
|
...getGlobal(),
|
||||||
|
phoneCall: {
|
||||||
|
id: '',
|
||||||
|
state: 'requesting',
|
||||||
|
participantId: userId,
|
||||||
|
isVideo,
|
||||||
|
adminId: global.currentUserId,
|
||||||
|
},
|
||||||
|
isCallPanelVisible: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function createAudioContext() {
|
function createAudioContext() {
|
||||||
return (new (window.AudioContext || (window as any).webkitAudioContext)());
|
return (new (window.AudioContext || (window as any).webkitAudioContext)());
|
||||||
}
|
}
|
||||||
@ -312,23 +365,3 @@ export function removeGroupCallAudioElement() {
|
|||||||
audioContext = undefined;
|
audioContext = undefined;
|
||||||
audioElement = undefined;
|
audioElement = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
addActionHandler('openCallFallbackConfirm', (global) => {
|
|
||||||
return {
|
|
||||||
...global,
|
|
||||||
groupCalls: {
|
|
||||||
...global.groupCalls,
|
|
||||||
isFallbackConfirmOpen: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('closeCallFallbackConfirm', (global) => {
|
|
||||||
return {
|
|
||||||
...global,
|
|
||||||
groupCalls: {
|
|
||||||
...global.groupCalls,
|
|
||||||
isFallbackConfirmOpen: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@ -276,6 +276,7 @@ function updateCache() {
|
|||||||
chatFolders: reduceChatFolders(global),
|
chatFolders: reduceChatFolders(global),
|
||||||
groupCalls: reduceGroupCalls(global),
|
groupCalls: reduceGroupCalls(global),
|
||||||
availableReactions: reduceAvailableReactions(global),
|
availableReactions: reduceAvailableReactions(global),
|
||||||
|
isCallPanelVisible: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const json = JSON.stringify(reducedGlobal);
|
const json = JSON.stringify(reducedGlobal);
|
||||||
@ -389,8 +390,6 @@ function reduceGroupCalls(global: GlobalState): GlobalState['groupCalls'] {
|
|||||||
...global.groupCalls,
|
...global.groupCalls,
|
||||||
byId: {},
|
byId: {},
|
||||||
activeGroupCallId: undefined,
|
activeGroupCallId: undefined,
|
||||||
isGroupCallPanelHidden: undefined,
|
|
||||||
isFallbackConfirmOpen: undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,8 @@ export function getMessageOriginalId(message: ApiMessage) {
|
|||||||
|
|
||||||
export function getMessageText(message: ApiMessage) {
|
export function getMessageText(message: ApiMessage) {
|
||||||
const {
|
const {
|
||||||
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location, game,
|
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
|
||||||
|
game, action,
|
||||||
} = message.content;
|
} = message.content;
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
@ -52,7 +53,7 @@ export function getMessageText(message: ApiMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sticker || photo || video || audio || voice || document
|
if (sticker || photo || video || audio || voice || document
|
||||||
|| contact || poll || webPage || invoice || location || game) {
|
|| contact || poll || webPage || invoice || location || game || action?.phoneCall) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { GlobalState } from '../types';
|
import { GlobalState } from '../types';
|
||||||
import { selectChat } from './chats';
|
import { selectChat } from './chats';
|
||||||
import { getUserFullName, isChatBasicGroup } from '../helpers';
|
import { isChatBasicGroup } from '../helpers';
|
||||||
import { selectUser } from './users';
|
import { selectUser } from './users';
|
||||||
|
|
||||||
export function selectChatGroupCall(global: GlobalState, chatId: string) {
|
export function selectChatGroupCall(global: GlobalState, chatId: string) {
|
||||||
@ -38,8 +38,12 @@ export function selectActiveGroupCall(global: GlobalState) {
|
|||||||
return selectGroupCall(global, activeGroupCallId);
|
return selectGroupCall(global, activeGroupCallId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectCallFallbackChannelTitle(global: GlobalState) {
|
export function selectPhoneCallUser(global: GlobalState) {
|
||||||
const currentUser = selectUser(global, global.currentUserId!);
|
const { phoneCall, currentUserId } = global;
|
||||||
|
if (!phoneCall || !phoneCall.participantId || !phoneCall.adminId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return `Calls: ${getUserFullName(currentUser!)}`;
|
const id = phoneCall.adminId === currentUserId ? phoneCall.participantId : phoneCall.adminId;
|
||||||
|
return selectUser(global, id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
ApiPaymentFormNativeParams,
|
ApiPaymentFormNativeParams,
|
||||||
ApiUpdate,
|
ApiUpdate,
|
||||||
ApiKeyboardButton,
|
ApiKeyboardButton,
|
||||||
|
ApiPhoneCall,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import {
|
import {
|
||||||
FocusDirection,
|
FocusDirection,
|
||||||
@ -58,6 +59,7 @@ import {
|
|||||||
ManagementState,
|
ManagementState,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { typify } from '../lib/teact/teactn';
|
import { typify } from '../lib/teact/teactn';
|
||||||
|
import type { P2pMessage } from '../lib/secret-sauce';
|
||||||
|
|
||||||
export type MessageListType =
|
export type MessageListType =
|
||||||
'thread'
|
'thread'
|
||||||
@ -200,12 +202,12 @@ export type GlobalState = {
|
|||||||
groupCalls: {
|
groupCalls: {
|
||||||
byId: Record<string, ApiGroupCall>;
|
byId: Record<string, ApiGroupCall>;
|
||||||
activeGroupCallId?: string;
|
activeGroupCallId?: string;
|
||||||
isGroupCallPanelHidden?: boolean;
|
|
||||||
isFallbackConfirmOpen?: boolean;
|
|
||||||
fallbackChatId?: string;
|
|
||||||
fallbackUserIdsToRemove?: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isCallPanelVisible?: boolean;
|
||||||
|
phoneCall?: ApiPhoneCall;
|
||||||
|
ratingPhoneCall?: ApiPhoneCall;
|
||||||
|
|
||||||
scheduledMessages: {
|
scheduledMessages: {
|
||||||
byChatId: Record<string, {
|
byChatId: Record<string, {
|
||||||
byId: Record<number, ApiMessage>;
|
byId: Record<number, ApiMessage>;
|
||||||
@ -541,6 +543,10 @@ export type GlobalState = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CallSound = (
|
||||||
|
'join' | 'allowTalk' | 'leave' | 'connecting' | 'incoming' | 'end' | 'connect' | 'busy' | 'ringing'
|
||||||
|
);
|
||||||
|
|
||||||
export interface ActionPayloads {
|
export interface ActionPayloads {
|
||||||
// Initial
|
// Initial
|
||||||
signOut: { forceInitApi?: boolean } | undefined;
|
signOut: { forceInitApi?: boolean } | undefined;
|
||||||
@ -673,6 +679,24 @@ export interface ActionPayloads {
|
|||||||
isQuiz?: boolean;
|
isQuiz?: boolean;
|
||||||
};
|
};
|
||||||
closePollModal: {};
|
closePollModal: {};
|
||||||
|
|
||||||
|
// Calls
|
||||||
|
requestCall: {
|
||||||
|
userId: string;
|
||||||
|
isVideo?: boolean;
|
||||||
|
};
|
||||||
|
sendSignalingData: P2pMessage;
|
||||||
|
hangUp: {};
|
||||||
|
acceptCall: {};
|
||||||
|
setCallRating: {
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
closeCallRatingModal: {};
|
||||||
|
playGroupCallSound: {
|
||||||
|
sound: CallSound;
|
||||||
|
};
|
||||||
|
connectToActivePhoneCall: {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NonTypedActionNames = (
|
export type NonTypedActionNames = (
|
||||||
@ -769,8 +793,7 @@ export type NonTypedActionNames = (
|
|||||||
'joinGroupCall' | 'toggleGroupCallMute' | 'toggleGroupCallPresentation' | 'leaveGroupCall' |
|
'joinGroupCall' | 'toggleGroupCallMute' | 'toggleGroupCallPresentation' | 'leaveGroupCall' |
|
||||||
'toggleGroupCallVideo' | 'requestToSpeak' | 'setGroupCallParticipantVolume' | 'toggleGroupCallPanel' |
|
'toggleGroupCallVideo' | 'requestToSpeak' | 'setGroupCallParticipantVolume' | 'toggleGroupCallPanel' |
|
||||||
'createGroupCall' | 'joinVoiceChatByLink' | 'subscribeToGroupCallUpdates' | 'createGroupCallInviteLink' |
|
'createGroupCall' | 'joinVoiceChatByLink' | 'subscribeToGroupCallUpdates' | 'createGroupCallInviteLink' |
|
||||||
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' | 'playGroupCallSound' |
|
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' |
|
||||||
'openCallFallbackConfirm' | 'closeCallFallbackConfirm' | 'inviteToCallFallback' |
|
|
||||||
// stats
|
// stats
|
||||||
'loadStatistics' | 'loadStatisticsAsyncGraph'
|
'loadStatistics' | 'loadStatisticsAsyncGraph'
|
||||||
);
|
);
|
||||||
|
|||||||
@ -38,7 +38,6 @@ function toSignedLittleBuffer(big, number = 8) {
|
|||||||
return Buffer.from(byteArray);
|
return Buffer.from(byteArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* converts a big int to a buffer
|
* converts a big int to a buffer
|
||||||
* @param bigInt {bigInt.BigInteger}
|
* @param bigInt {bigInt.BigInteger}
|
||||||
@ -205,7 +204,6 @@ function sha1(data) {
|
|||||||
return shaSum.digest();
|
return shaSum.digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the SHA256 digest for the given data
|
* Calculates the SHA256 digest for the given data
|
||||||
* @param data
|
* @param data
|
||||||
@ -241,10 +239,9 @@ function modExp(a, b, n) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the arbitrary-length byte array corresponding to the given integer
|
* Gets the arbitrary-length byte array corresponding to the given integer
|
||||||
* @param integer {number,BigInteger}
|
* @param integer {any}
|
||||||
* @param signed {boolean}
|
* @param signed {boolean}
|
||||||
* @returns {Buffer}
|
* @returns {Buffer}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
const BigInt = require('big-integer');
|
const BigInt = require('big-integer');
|
||||||
|
const aes = require('@cryptography/aes');
|
||||||
|
|
||||||
const Helpers = require('../Helpers');
|
const Helpers = require('../Helpers');
|
||||||
const IGE = require('../crypto/IGE');
|
const IGE = require('../crypto/IGE');
|
||||||
@ -39,10 +40,14 @@ class MTProtoState {
|
|||||||
authentication process, at which point the `MTProtoPlainSender` is better
|
authentication process, at which point the `MTProtoPlainSender` is better
|
||||||
* @param authKey
|
* @param authKey
|
||||||
* @param loggers
|
* @param loggers
|
||||||
|
* @param isCall
|
||||||
|
* @param isOutgoing
|
||||||
*/
|
*/
|
||||||
constructor(authKey, loggers) {
|
constructor(authKey, loggers, isCall = false, isOutgoing = false) {
|
||||||
this.authKey = authKey;
|
this.authKey = authKey;
|
||||||
this._log = loggers;
|
this._log = loggers;
|
||||||
|
this._isCall = isCall;
|
||||||
|
this._isOutgoing = isOutgoing;
|
||||||
this.timeOffset = 0;
|
this.timeOffset = 0;
|
||||||
this.salt = 0;
|
this.salt = 0;
|
||||||
|
|
||||||
@ -81,12 +86,20 @@ class MTProtoState {
|
|||||||
* @returns {{iv: Buffer, key: Buffer}}
|
* @returns {{iv: Buffer, key: Buffer}}
|
||||||
*/
|
*/
|
||||||
async _calcKey(authKey, msgKey, client) {
|
async _calcKey(authKey, msgKey, client) {
|
||||||
const x = client === true ? 0 : 8;
|
const x = (this._isCall ? 128 + ((this._isOutgoing ^ client) ? 8 : 0) : (client === true ? 0 : 8));
|
||||||
const [sha256a, sha256b] = await Promise.all([
|
const [sha256a, sha256b] = await Promise.all([
|
||||||
Helpers.sha256(Buffer.concat([msgKey, authKey.slice(x, x + 36)])),
|
Helpers.sha256(Buffer.concat([msgKey, authKey.slice(x, x + 36)])),
|
||||||
Helpers.sha256(Buffer.concat([authKey.slice(x + 40, x + 76), msgKey])),
|
Helpers.sha256(Buffer.concat([authKey.slice(x + 40, x + 76), msgKey])),
|
||||||
]);
|
]);
|
||||||
const key = Buffer.concat([sha256a.slice(0, 8), sha256b.slice(8, 24), sha256a.slice(24, 32)]);
|
const key = Buffer.concat([sha256a.slice(0, 8), sha256b.slice(8, 24), sha256a.slice(24, 32)]);
|
||||||
|
if (this._isCall) {
|
||||||
|
const iv = Buffer.concat([sha256b.slice(0, 4), sha256a.slice(8, 16), sha256b.slice(24, 28)]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
iv,
|
||||||
|
};
|
||||||
|
}
|
||||||
const iv = Buffer.concat([sha256b.slice(0, 8), sha256a.slice(8, 24), sha256b.slice(24, 32)]);
|
const iv = Buffer.concat([sha256b.slice(0, 8), sha256a.slice(8, 24), sha256b.slice(24, 32)]);
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
@ -133,24 +146,48 @@ class MTProtoState {
|
|||||||
*/
|
*/
|
||||||
async encryptMessageData(data) {
|
async encryptMessageData(data) {
|
||||||
await this.authKey.waitForKey();
|
await this.authKey.waitForKey();
|
||||||
const s = toSignedLittleBuffer(this.salt, 8);
|
if (this._isCall) {
|
||||||
const i = toSignedLittleBuffer(this.id, 8);
|
const x = 128 + (this._isOutgoing ? 0 : 8);
|
||||||
data = Buffer.concat([Buffer.concat([s, i]), data]);
|
const lengthStart = data.length;
|
||||||
const padding = Helpers.generateRandomBytes(Helpers.mod(-(data.length + 12), 16) + 12);
|
|
||||||
// Being substr(what, offset, length); x = 0 for client
|
|
||||||
// "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)"
|
|
||||||
const msgKeyLarge = await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
|
||||||
.slice(88, 88 + 32), data, padding]));
|
|
||||||
// "msg_key = substr (msg_key_large, 8, 16)"
|
|
||||||
const msgKey = msgKeyLarge.slice(8, 24);
|
|
||||||
|
|
||||||
const {
|
data = Buffer.from(data);
|
||||||
iv,
|
if (lengthStart % 4 !== 0) {
|
||||||
key,
|
data = Buffer.concat([data, Buffer.from(new Array(4 - (lengthStart % 4)).fill(0x20))]);
|
||||||
} = await this._calcKey(this.authKey.getKey(), msgKey, true);
|
}
|
||||||
|
|
||||||
const keyId = Helpers.readBufferFromBigInt(this.authKey.keyId, 8);
|
const msgKeyLarge = await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||||
return Buffer.concat([keyId, msgKey, new IGE(key, iv).encryptIge(Buffer.concat([data, padding]))]);
|
.slice(88 + x, 88 + x + 32), Buffer.from(data)]));
|
||||||
|
|
||||||
|
const msgKey = msgKeyLarge.slice(8, 24);
|
||||||
|
|
||||||
|
const {
|
||||||
|
iv,
|
||||||
|
key,
|
||||||
|
} = await this._calcKey(this.authKey.getKey(), msgKey, true);
|
||||||
|
|
||||||
|
data = Helpers.convertToLittle(new aes.CTR(key, iv).encrypt(data));
|
||||||
|
// data = data.slice(0, lengthStart)
|
||||||
|
return Buffer.concat([msgKey, data]);
|
||||||
|
} else {
|
||||||
|
const s = toSignedLittleBuffer(this.salt, 8);
|
||||||
|
const i = toSignedLittleBuffer(this.id, 8);
|
||||||
|
data = Buffer.concat([Buffer.concat([s, i]), data]);
|
||||||
|
const padding = Helpers.generateRandomBytes(Helpers.mod(-(data.length + 12), 16) + 12);
|
||||||
|
// Being substr(what, offset, length); x = 0 for client
|
||||||
|
// "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)"
|
||||||
|
const msgKeyLarge = await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||||
|
.slice(88, 88 + 32), data, padding]));
|
||||||
|
// "msg_key = substr (msg_key_large, 8, 16)"
|
||||||
|
const msgKey = msgKeyLarge.slice(8, 24);
|
||||||
|
|
||||||
|
const {
|
||||||
|
iv,
|
||||||
|
key,
|
||||||
|
} = await this._calcKey(this.authKey.getKey(), msgKey, true);
|
||||||
|
|
||||||
|
const keyId = Helpers.readBufferFromBigInt(this.authKey.keyId, 8);
|
||||||
|
return Buffer.concat([keyId, msgKey, new IGE(key, iv).encryptIge(Buffer.concat([data, padding]))]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -164,65 +201,87 @@ class MTProtoState {
|
|||||||
if (body.length < 0) { // length needs to be positive
|
if (body.length < 0) { // length needs to be positive
|
||||||
throw new SecurityError('Server replied with negative length');
|
throw new SecurityError('Server replied with negative length');
|
||||||
}
|
}
|
||||||
if (body.length % 4 !== 0) {
|
if (body.length % 4 !== 0 && !this._isCall) {
|
||||||
throw new SecurityError('Server replied with length not divisible by 4');
|
throw new SecurityError('Server replied with length not divisible by 4');
|
||||||
}
|
}
|
||||||
// TODO Check salt,sessionId, and sequenceNumber
|
// TODO Check salt,sessionId, and sequenceNumber
|
||||||
const keyId = Helpers.readBigIntFromBuffer(body.slice(0, 8));
|
if (!this._isCall) {
|
||||||
if (keyId.neq(this.authKey.keyId)) {
|
const keyId = Helpers.readBigIntFromBuffer(body.slice(0, 8));
|
||||||
throw new SecurityError('Server replied with an invalid auth key');
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgKey = body.slice(8, 24);
|
if (keyId.neq(this.authKey.keyId)) {
|
||||||
|
throw new SecurityError('Server replied with an invalid auth key');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const msgKey = this._isCall ? body.slice(0, 16) : body.slice(8, 24);
|
||||||
|
|
||||||
|
const x = this._isCall ? 128 + (this.isOutgoing ? 8 : 0) : undefined;
|
||||||
const {
|
const {
|
||||||
iv,
|
iv,
|
||||||
key,
|
key,
|
||||||
} = await this._calcKey(this.authKey.getKey(), msgKey, false);
|
} = await this._calcKey(this.authKey.getKey(), msgKey, false);
|
||||||
body = new IGE(key, iv).decryptIge(body.slice(24));
|
|
||||||
|
|
||||||
|
if (this._isCall) {
|
||||||
|
body = body.slice(16);
|
||||||
|
const lengthStart = body.length;
|
||||||
|
|
||||||
|
body = Buffer.concat([body, Buffer.from(new Array(4 - (lengthStart % 4)).fill(0))]);
|
||||||
|
|
||||||
|
body = Helpers.convertToLittle(new aes.CTR(key, iv).decrypt(body));
|
||||||
|
|
||||||
|
body = body.slice(0, lengthStart);
|
||||||
|
} else {
|
||||||
|
body = new IGE(key, iv).decryptIge(this._isCall ? body.slice(16) : body.slice(24));
|
||||||
|
}
|
||||||
// https://core.telegram.org/mtproto/security_guidelines
|
// https://core.telegram.org/mtproto/security_guidelines
|
||||||
// Sections "checking sha256 hash" and "message length"
|
// Sections "checking sha256 hash" and "message length"
|
||||||
|
|
||||||
const ourKey = await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
const ourKey = this._isCall
|
||||||
.slice(96, 96 + 32), body]));
|
? await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||||
|
.slice(88 + x, 88 + x + 32), body]))
|
||||||
|
: await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||||
|
.slice(96, 96 + 32), body]));
|
||||||
|
|
||||||
if (!msgKey.equals(ourKey.slice(8, 24))) {
|
if (!this._isCall && !msgKey.equals(ourKey.slice(8, 24))) {
|
||||||
throw new SecurityError('Received msg_key doesn\'t match with expected one');
|
throw new SecurityError('Received msg_key doesn\'t match with expected one');
|
||||||
}
|
}
|
||||||
|
|
||||||
const reader = new BinaryReader(body);
|
const reader = new BinaryReader(body);
|
||||||
reader.readLong(); // removeSalt
|
|
||||||
const serverId = reader.readLong();
|
|
||||||
if (!serverId.eq(this.id)) {
|
|
||||||
throw new SecurityError('Server replied with a wrong session ID');
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteMsgId = reader.readLong();
|
if (this._isCall) {
|
||||||
// if we get a duplicate message id we should ignore it.
|
// Seq
|
||||||
if (this.msgIds.includes(remoteMsgId.toString())) {
|
reader.readInt(false);
|
||||||
throw new SecurityError('Duplicate msgIds');
|
return reader.read(body.length - 4);
|
||||||
}
|
} else {
|
||||||
// we only store the latest 500 message ids from the server
|
reader.readLong(); // removeSalt
|
||||||
if (this.msgIds.length > 500) {
|
const serverId = reader.readLong();
|
||||||
this.msgIds.shift();
|
if (!serverId.eq(this.id)) {
|
||||||
}
|
throw new SecurityError('Server replied with a wrong session ID');
|
||||||
this.msgIds.push(remoteMsgId.toString());
|
}
|
||||||
|
|
||||||
const remoteSequence = reader.readInt();
|
const remoteMsgId = reader.readLong();
|
||||||
const containerLen = reader.readInt(); // msgLen for the inner object, padding ignored
|
// if we get a duplicate message id we should ignore it.
|
||||||
const diff = body.length - containerLen;
|
if (this.msgIds.includes(remoteMsgId.toString())) {
|
||||||
// We want to check if it's between 12 and 1024
|
throw new SecurityError('Duplicate msgIds');
|
||||||
// https://core.telegram.org/mtproto/security_guidelines#checking-message-length
|
}
|
||||||
if (diff < 12 || diff > 1024) {
|
// we only store the latest 500 message ids from the server
|
||||||
throw new SecurityError('Server replied with the wrong message padding');
|
if (this.msgIds.length > 500) {
|
||||||
|
this.msgIds.shift();
|
||||||
|
}
|
||||||
|
this.msgIds.push(remoteMsgId.toString());const remoteSequence = reader.readInt();
|
||||||
|
const containerLen = reader.readInt(); // msgLen for the inner object, padding ignored
|
||||||
|
const diff = body.length - containerLen;
|
||||||
|
// We want to check if it's between 12 and 1024
|
||||||
|
// https://core.telegram.org/mtproto/security_guidelines#checking-message-length
|
||||||
|
if (diff < 12 || diff > 1024) {
|
||||||
|
throw new SecurityError('Server replied with the wrong message padding');
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could read msg_len bytes and use those in a new reader to read
|
||||||
|
// the next TLObject without including the padding, but since the
|
||||||
|
// reader isn't used for anything else after this, it's unnecessary.
|
||||||
|
const obj = reader.tgReadObject();
|
||||||
|
|
||||||
|
return new TLMessage(remoteMsgId, remoteSequence, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We could read msg_len bytes and use those in a new reader to read
|
|
||||||
// the next TLObject without including the padding, but since the
|
|
||||||
// reader isn't used for anything else after this, it's unnecessary.
|
|
||||||
const obj = reader.tgReadObject();
|
|
||||||
|
|
||||||
return new TLMessage(remoteMsgId, remoteSequence, obj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1154,6 +1154,14 @@ payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.Payment
|
|||||||
payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;
|
payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer msg_id:int info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;
|
||||||
payments.sendPaymentForm#30c3bc9d flags:# form_id:long peer:InputPeer msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult;
|
payments.sendPaymentForm#30c3bc9d flags:# form_id:long peer:InputPeer msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult;
|
||||||
payments.getSavedInfo#227d824b = payments.SavedInfo;
|
payments.getSavedInfo#227d824b = payments.SavedInfo;
|
||||||
|
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||||
|
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||||
|
phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||||
|
phone.receivedCall#17d54f61 peer:InputPhoneCall = Bool;
|
||||||
|
phone.discardCall#b2cbc1c0 flags:# video:flags.0?true peer:InputPhoneCall duration:int reason:PhoneCallDiscardReason connection_id:long = Updates;
|
||||||
|
phone.setCallRating#59ead627 flags:# user_initiative:flags.0?true peer:InputPhoneCall rating:int comment:string = Updates;
|
||||||
|
phone.saveCallDebug#277add7e peer:InputPhoneCall debug:DataJSON = Bool;
|
||||||
|
phone.sendSignalingData#ff7a9383 peer:InputPhoneCall data:bytes = Bool;
|
||||||
phone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates;
|
phone.createGroupCall#48cdc6d8 flags:# rtmp_stream:flags.2?true peer:InputPeer random_id:int title:flags.0?string schedule_date:flags.1?int = Updates;
|
||||||
phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates;
|
phone.joinGroupCall#b132ff7b flags:# muted:flags.0?true video_stopped:flags.2?true call:InputGroupCall join_as:InputPeer invite_hash:flags.1?string params:DataJSON = Updates;
|
||||||
phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates;
|
phone.leaveGroupCall#500377f9 call:InputGroupCall source:int = Updates;
|
||||||
|
|||||||
@ -203,6 +203,14 @@
|
|||||||
"phone.toggleGroupCallStartSubscription",
|
"phone.toggleGroupCallStartSubscription",
|
||||||
"phone.joinGroupCallPresentation",
|
"phone.joinGroupCallPresentation",
|
||||||
"phone.leaveGroupCallPresentation",
|
"phone.leaveGroupCallPresentation",
|
||||||
|
"phone.requestCall",
|
||||||
|
"phone.acceptCall",
|
||||||
|
"phone.confirmCall",
|
||||||
|
"phone.receivedCall",
|
||||||
|
"phone.discardCall",
|
||||||
|
"phone.setCallRating",
|
||||||
|
"phone.saveCallDebug",
|
||||||
|
"phone.sendSignalingData",
|
||||||
"messages.sendReaction",
|
"messages.sendReaction",
|
||||||
"messages.getMessagesReactions",
|
"messages.getMessagesReactions",
|
||||||
"messages.getMessageReactionsList",
|
"messages.getMessageReactionsList",
|
||||||
|
|||||||
2
src/lib/secret-sauce/buildSdp.d.ts
vendored
2
src/lib/secret-sauce/buildSdp.d.ts
vendored
@ -17,5 +17,5 @@ export declare type Ssrc = {
|
|||||||
isPresentation?: boolean;
|
isPresentation?: boolean;
|
||||||
sourceGroups: SsrcGroup[];
|
sourceGroups: SsrcGroup[];
|
||||||
};
|
};
|
||||||
declare const _default: (conference: Conference, isAnswer?: boolean, isPresentation?: boolean) => string;
|
declare const _default: (conference: Conference, isAnswer?: boolean, isPresentation?: boolean, isP2p?: boolean) => string;
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
2
src/lib/secret-sauce/index.d.ts
vendored
2
src/lib/secret-sauce/index.d.ts
vendored
@ -1,3 +1,5 @@
|
|||||||
export { handleUpdateGroupCallConnection, startSharingScreen, joinGroupCall, getDevices, getUserStreams, setVolume, isStreamEnabled, toggleStream, leaveGroupCall, handleUpdateGroupCallParticipants, switchCameraInput, toggleSpeaker, toggleNoiseSuppression, } from './secretsauce';
|
export { handleUpdateGroupCallConnection, startSharingScreen, joinGroupCall, getDevices, getUserStreams, setVolume, isStreamEnabled, toggleStream, leaveGroupCall, handleUpdateGroupCallParticipants, switchCameraInput, toggleSpeaker, toggleNoiseSuppression, } from './secretsauce';
|
||||||
|
export { joinPhoneCall, processSignalingMessage, getStreams, toggleStreamP2p, stopPhoneCall, switchCameraInputP2p, } from './p2p';
|
||||||
|
export * from './p2pMessage';
|
||||||
export { IS_SCREENSHARE_SUPPORTED, THRESHOLD, } from './utils';
|
export { IS_SCREENSHARE_SUPPORTED, THRESHOLD, } from './utils';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
16
src/lib/secret-sauce/p2p.d.ts
vendored
Normal file
16
src/lib/secret-sauce/p2p.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ApiPhoneCallConnection } from './types';
|
||||||
|
import { P2pMessage } from './p2pMessage';
|
||||||
|
import { StreamType } from './secretsauce';
|
||||||
|
export declare function getStreams(): {
|
||||||
|
video?: MediaStream | undefined;
|
||||||
|
audio?: MediaStream | undefined;
|
||||||
|
presentation?: MediaStream | undefined;
|
||||||
|
ownAudio?: MediaStream | undefined;
|
||||||
|
ownVideo?: MediaStream | undefined;
|
||||||
|
ownPresentation?: MediaStream | undefined;
|
||||||
|
} | undefined;
|
||||||
|
export declare function switchCameraInputP2p(): Promise<void>;
|
||||||
|
export declare function toggleStreamP2p(streamType: StreamType, value?: boolean | undefined): Promise<void>;
|
||||||
|
export declare function joinPhoneCall(connections: ApiPhoneCallConnection[], emitSignalingData: (data: P2pMessage) => void, isOutgoing: boolean, shouldStartVideo: boolean, onUpdate: (...args: any[]) => void): Promise<void>;
|
||||||
|
export declare function stopPhoneCall(): void;
|
||||||
|
export declare function processSignalingMessage(message: P2pMessage): Promise<void>;
|
||||||
47
src/lib/secret-sauce/p2pMessage.d.ts
vendored
Normal file
47
src/lib/secret-sauce/p2pMessage.d.ts
vendored
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Fingerprint, RTCPFeedbackParam, RTPExtension } from './types';
|
||||||
|
export declare type VideoState = 'inactive' | 'active' | 'suspended';
|
||||||
|
export declare type VideoRotation = 0 | 90 | 180 | 270;
|
||||||
|
export declare type MediaStateMessage = {
|
||||||
|
'@type': 'MediaState';
|
||||||
|
isMuted: boolean;
|
||||||
|
videoState: VideoState;
|
||||||
|
videoRotation: VideoRotation;
|
||||||
|
screencastState: VideoState;
|
||||||
|
isBatteryLow: boolean;
|
||||||
|
};
|
||||||
|
declare type CandidatesMessage = {
|
||||||
|
'@type': 'Candidates';
|
||||||
|
candidates: P2pCandidate[];
|
||||||
|
};
|
||||||
|
export declare type InitialSetupMessage = {
|
||||||
|
'@type': 'InitialSetup';
|
||||||
|
ufrag: string;
|
||||||
|
pwd: string;
|
||||||
|
fingerprints: Fingerprint[];
|
||||||
|
audio?: MediaContent;
|
||||||
|
video?: MediaContent;
|
||||||
|
screencast?: MediaContent;
|
||||||
|
};
|
||||||
|
export declare type MediaContent = {
|
||||||
|
ssrc: string;
|
||||||
|
ssrcGroups: P2pSsrcGroup[];
|
||||||
|
payloadTypes: P2PPayloadType[];
|
||||||
|
rtpExtensions: RTPExtension[];
|
||||||
|
};
|
||||||
|
export interface P2PPayloadType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
clockrate: number;
|
||||||
|
channels: number;
|
||||||
|
parameters?: Record<string, string>;
|
||||||
|
feedbackTypes?: RTCPFeedbackParam[];
|
||||||
|
}
|
||||||
|
declare type P2pSsrcGroup = {
|
||||||
|
semantics: string;
|
||||||
|
ssrcs: number[];
|
||||||
|
};
|
||||||
|
declare type P2pCandidate = {
|
||||||
|
sdpString: string;
|
||||||
|
};
|
||||||
|
export declare type P2pMessage = CandidatesMessage | InitialSetupMessage | MediaStateMessage;
|
||||||
|
export {};
|
||||||
2
src/lib/secret-sauce/parseSdp.d.ts
vendored
2
src/lib/secret-sauce/parseSdp.d.ts
vendored
@ -1,3 +1,3 @@
|
|||||||
import { JoinGroupCallPayload } from './types';
|
import { JoinGroupCallPayload } from './types';
|
||||||
declare const _default: (sessionDescription: RTCSessionDescriptionInit) => JoinGroupCallPayload;
|
declare const _default: (sessionDescription: RTCSessionDescriptionInit, isP2p?: boolean) => JoinGroupCallPayload;
|
||||||
export default _default;
|
export default _default;
|
||||||
|
|||||||
3
src/lib/secret-sauce/secretsauce.d.ts
vendored
3
src/lib/secret-sauce/secretsauce.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
import { GroupCallConnectionData, GroupCallParticipant, JoinGroupCallPayload } from './types';
|
import { GroupCallConnectionData, GroupCallParticipant, JoinGroupCallPayload } from './types';
|
||||||
declare type StreamType = 'audio' | 'video' | 'presentation';
|
export declare type StreamType = 'audio' | 'video' | 'presentation';
|
||||||
export declare function getDevices(streamType: StreamType, isInput?: boolean): Promise<MediaDeviceInfo[]>;
|
export declare function getDevices(streamType: StreamType, isInput?: boolean): Promise<MediaDeviceInfo[]>;
|
||||||
export declare function toggleSpeaker(): void;
|
export declare function toggleSpeaker(): void;
|
||||||
export declare function toggleNoiseSuppression(): void;
|
export declare function toggleNoiseSuppression(): void;
|
||||||
@ -17,4 +17,3 @@ export declare function handleUpdateGroupCallParticipants(updatedParticipants: G
|
|||||||
export declare function handleUpdateGroupCallConnection(data: GroupCallConnectionData, isPresentation: boolean): Promise<void>;
|
export declare function handleUpdateGroupCallConnection(data: GroupCallConnectionData, isPresentation: boolean): Promise<void>;
|
||||||
export declare function startSharingScreen(): Promise<JoinGroupCallPayload | undefined>;
|
export declare function startSharingScreen(): Promise<JoinGroupCallPayload | undefined>;
|
||||||
export declare function joinGroupCall(myId: string, audioContext: AudioContext, audioElement: HTMLAudioElement, onUpdate: (...args: any[]) => void): Promise<JoinGroupCallPayload>;
|
export declare function joinGroupCall(myId: string, audioContext: AudioContext, audioElement: HTMLAudioElement, onUpdate: (...args: any[]) => void): Promise<JoinGroupCallPayload>;
|
||||||
export {};
|
|
||||||
|
|||||||
26
src/lib/secret-sauce/types.d.ts
vendored
26
src/lib/secret-sauce/types.d.ts
vendored
@ -1,3 +1,4 @@
|
|||||||
|
import { P2PPayloadType } from './p2pMessage';
|
||||||
export interface GroupCallParticipant {
|
export interface GroupCallParticipant {
|
||||||
isSelf?: boolean;
|
isSelf?: boolean;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
@ -52,6 +53,7 @@ export declare type Candidate = {
|
|||||||
network: string;
|
network: string;
|
||||||
'rel-addr': string;
|
'rel-addr': string;
|
||||||
'rel-port': string;
|
'rel-port': string;
|
||||||
|
sdpString?: string;
|
||||||
};
|
};
|
||||||
export declare type JoinGroupCallPayload = {
|
export declare type JoinGroupCallPayload = {
|
||||||
ufrag: string;
|
ufrag: string;
|
||||||
@ -60,6 +62,14 @@ export declare type JoinGroupCallPayload = {
|
|||||||
ssrc?: number;
|
ssrc?: number;
|
||||||
'ssrc-groups'?: SsrcGroup[];
|
'ssrc-groups'?: SsrcGroup[];
|
||||||
};
|
};
|
||||||
|
export declare type P2pParsedSdp = JoinGroupCallPayload & {
|
||||||
|
audioExtmap: RTPExtension[];
|
||||||
|
videoExtmap: RTPExtension[];
|
||||||
|
screencastExtmap: RTPExtension[];
|
||||||
|
audioPayloadTypes: P2PPayloadType[];
|
||||||
|
videoPayloadTypes: P2PPayloadType[];
|
||||||
|
screencastPayloadTypes: P2PPayloadType[];
|
||||||
|
};
|
||||||
export interface RTPExtension {
|
export interface RTPExtension {
|
||||||
id: number;
|
id: number;
|
||||||
uri: string;
|
uri: string;
|
||||||
@ -98,3 +108,19 @@ export interface GroupCallConnectionData {
|
|||||||
};
|
};
|
||||||
stream?: boolean;
|
stream?: boolean;
|
||||||
}
|
}
|
||||||
|
export interface ApiPhoneCallConnection {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
isTurn?: boolean;
|
||||||
|
isStun?: boolean;
|
||||||
|
ip: string;
|
||||||
|
ipv6: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
export interface ApiCallProtocol {
|
||||||
|
libraryVersions: string[];
|
||||||
|
minLayer: number;
|
||||||
|
maxLayer: number;
|
||||||
|
isUdpP2p?: boolean;
|
||||||
|
isUdpReflector?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
7
src/lib/secret-sauce/utils.d.ts
vendored
7
src/lib/secret-sauce/utils.d.ts
vendored
@ -1,11 +1,10 @@
|
|||||||
|
import { P2PPayloadType } from './p2pMessage';
|
||||||
|
import { PayloadType } from './types';
|
||||||
export declare function toTelegramSource(source: number): number;
|
export declare function toTelegramSource(source: number): number;
|
||||||
export declare function fromTelegramSource(source: number): number;
|
export declare function fromTelegramSource(source: number): number;
|
||||||
export declare function getAmplitude(array: Uint8Array, scale?: number): number;
|
export declare function getAmplitude(array: Uint8Array, scale?: number): number;
|
||||||
export declare function getPlatform(): "Windows" | "macOS" | "iOS" | "Android" | "Linux" | undefined;
|
export declare function p2pPayloadTypeToConference(p: P2PPayloadType): PayloadType;
|
||||||
export declare const THRESHOLD = 0.1;
|
export declare const THRESHOLD = 0.1;
|
||||||
export declare const PLATFORM_ENV: "Windows" | "macOS" | "iOS" | "Android" | "Linux" | undefined;
|
|
||||||
export declare const IS_MAC_OS: boolean;
|
|
||||||
export declare const IS_IOS: boolean;
|
|
||||||
export declare const IS_SCREENSHARE_SUPPORTED: boolean;
|
export declare const IS_SCREENSHARE_SUPPORTED: boolean;
|
||||||
export declare const IS_ECHO_CANCELLATION_SUPPORTED: boolean | undefined;
|
export declare const IS_ECHO_CANCELLATION_SUPPORTED: boolean | undefined;
|
||||||
export declare const IS_NOISE_SUPPRESSION_SUPPORTED: any;
|
export declare const IS_NOISE_SUPPRESSION_SUPPORTED: any;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -51,6 +51,15 @@
|
|||||||
.icon-volume-3:before {
|
.icon-volume-3:before {
|
||||||
content: "\e991";
|
content: "\e991";
|
||||||
}
|
}
|
||||||
|
.icon-favorite-filled:before {
|
||||||
|
content: "\e998";
|
||||||
|
}
|
||||||
|
.icon-share-screen:before {
|
||||||
|
content: "\e97a";
|
||||||
|
}
|
||||||
|
.icon-video-outlined:before {
|
||||||
|
content: "\e997";
|
||||||
|
}
|
||||||
.icon-stats:before {
|
.icon-stats:before {
|
||||||
content: "\e996";
|
content: "\e996";
|
||||||
}
|
}
|
||||||
@ -84,7 +93,7 @@
|
|||||||
.icon-stop-raising-hand:before {
|
.icon-stop-raising-hand:before {
|
||||||
content: "\e985";
|
content: "\e985";
|
||||||
}
|
}
|
||||||
.icon-share-screen:before {
|
.icon-share-screen-outlined:before {
|
||||||
content: "\e986";
|
content: "\e986";
|
||||||
}
|
}
|
||||||
.icon-voice-chat:before {
|
.icon-voice-chat:before {
|
||||||
|
|||||||
@ -121,6 +121,47 @@ export function formatLastUpdated(lang: LangFn, currentTime: number, lastUpdated
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DurationType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks';
|
||||||
|
|
||||||
|
export function formatTimeDuration(lang: LangFn, duration: number, showLast = 2) {
|
||||||
|
if (!duration) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationRecords: { duration: number; type: DurationType }[] = [];
|
||||||
|
const labels = [
|
||||||
|
{ multiplier: 1, type: 'Seconds' },
|
||||||
|
{ multiplier: 60, type: 'Minutes' },
|
||||||
|
{ multiplier: 60, type: 'Hours' },
|
||||||
|
{ multiplier: 24, type: 'Days' },
|
||||||
|
{ multiplier: 7, type: 'Weeks' },
|
||||||
|
] as Array<{ multiplier: number; type: DurationType }>;
|
||||||
|
let t = 1;
|
||||||
|
labels.forEach((label, idx) => {
|
||||||
|
t *= label.multiplier;
|
||||||
|
|
||||||
|
if (duration < t) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modulus = labels[idx === (labels.length - 1) ? idx : idx + 1].multiplier!;
|
||||||
|
durationRecords.push({
|
||||||
|
duration: Math.floor((duration / t) % modulus),
|
||||||
|
type: label.type,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = durationRecords.slice(-showLast).reverse();
|
||||||
|
for (let i = out.length - 1; i >= 0; --i) {
|
||||||
|
if (out[i].duration === 0) {
|
||||||
|
out.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO In arabic we don't use "," as delimiter rather we use "and" each time
|
||||||
|
return out.map((l) => lang(l.type, l.duration, 'i')).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
export function formatHumanDate(
|
export function formatHumanDate(
|
||||||
lang: LangFn,
|
lang: LangFn,
|
||||||
datetime: number | Date,
|
datetime: number | Date,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { callApi } from '../api/gramjs';
|
import { callApi } from '../api/gramjs';
|
||||||
import {
|
import {
|
||||||
ApiChat, ApiMediaFormat, ApiMessage, ApiUser, ApiUserReaction,
|
ApiChat, ApiMediaFormat, ApiMessage, ApiPhoneCall, ApiUser, ApiUserReaction,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
||||||
import { DEBUG, IS_TEST } from '../config';
|
import { DEBUG, IS_TEST } from '../config';
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
getMessageRecentReaction,
|
getMessageRecentReaction,
|
||||||
getMessageSenderName,
|
getMessageSenderName,
|
||||||
getMessageSummaryText,
|
getMessageSummaryText,
|
||||||
getPrivateChatUserId,
|
getPrivateChatUserId, getUserFullName,
|
||||||
isActionMessage,
|
isActionMessage,
|
||||||
isChatChannel,
|
isChatChannel,
|
||||||
selectIsChatMuted,
|
selectIsChatMuted,
|
||||||
@ -322,7 +322,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAvatar(chat: ApiChat) {
|
async function getAvatar(chat: ApiChat | ApiUser) {
|
||||||
const imageHash = getChatAvatarHash(chat);
|
const imageHash = getChatAvatarHash(chat);
|
||||||
if (!imageHash) return undefined;
|
if (!imageHash) return undefined;
|
||||||
let mediaData = mediaLoader.getFromMemory(imageHash);
|
let mediaData = mediaLoader.getFromMemory(imageHash);
|
||||||
@ -333,6 +333,39 @@ async function getAvatar(chat: ApiChat) {
|
|||||||
return mediaData;
|
return mediaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function notifyAboutCall({
|
||||||
|
call, user,
|
||||||
|
}: {
|
||||||
|
call: ApiPhoneCall; user: ApiUser;
|
||||||
|
}) {
|
||||||
|
const { hasWebNotifications } = await loadNotificationSettings();
|
||||||
|
if (document.hasFocus() || !hasWebNotifications) return;
|
||||||
|
const areNotificationsSupported = checkIfNotificationsSupported();
|
||||||
|
if (!areNotificationsSupported) return;
|
||||||
|
|
||||||
|
const icon = await getAvatar(user);
|
||||||
|
|
||||||
|
const options: NotificationOptions = {
|
||||||
|
body: getUserFullName(user),
|
||||||
|
icon,
|
||||||
|
badge: icon,
|
||||||
|
tag: `call_${call.id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('vibrate' in navigator) {
|
||||||
|
options.vibrate = [200, 100, 200];
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = new Notification(getTranslation('VoipIncoming'), options);
|
||||||
|
|
||||||
|
notification.onclick = () => {
|
||||||
|
notification.close();
|
||||||
|
if (window.focus) {
|
||||||
|
window.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function notifyAboutMessage({
|
export async function notifyAboutMessage({
|
||||||
chat,
|
chat,
|
||||||
message,
|
message,
|
||||||
|
|||||||
88
src/util/phoneCallEmojiConstants.ts
Normal file
88
src/util/phoneCallEmojiConstants.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
export const EMOJI_DATA = new Uint16Array([
|
||||||
|
0xd83d, 0xde09, 0xd83d, 0xde0d, 0xd83d, 0xde1b, 0xd83d, 0xde2d, 0xd83d, 0xde31, 0xd83d, 0xde21,
|
||||||
|
0xd83d, 0xde0e, 0xd83d, 0xde34, 0xd83d, 0xde35, 0xd83d, 0xde08, 0xd83d, 0xde2c, 0xd83d, 0xde07,
|
||||||
|
0xd83d, 0xde0f, 0xd83d, 0xdc6e, 0xd83d, 0xdc77, 0xd83d, 0xdc82, 0xd83d, 0xdc76, 0xd83d, 0xdc68,
|
||||||
|
0xd83d, 0xdc69, 0xd83d, 0xdc74, 0xd83d, 0xdc75, 0xd83d, 0xde3b, 0xd83d, 0xde3d, 0xd83d, 0xde40,
|
||||||
|
0xd83d, 0xdc7a, 0xd83d, 0xde48, 0xd83d, 0xde49, 0xd83d, 0xde4a, 0xd83d, 0xdc80, 0xd83d, 0xdc7d,
|
||||||
|
0xd83d, 0xdca9, 0xd83d, 0xdd25, 0xd83d, 0xdca5, 0xd83d, 0xdca4, 0xd83d, 0xdc42, 0xd83d, 0xdc40,
|
||||||
|
0xd83d, 0xdc43, 0xd83d, 0xdc45, 0xd83d, 0xdc44, 0xd83d, 0xdc4d, 0xd83d, 0xdc4e, 0xd83d, 0xdc4c,
|
||||||
|
0xd83d, 0xdc4a, 0x270c, 0x270b, 0xd83d, 0xdc50, 0xd83d, 0xdc46, 0xd83d, 0xdc47, 0xd83d, 0xdc49,
|
||||||
|
0xd83d, 0xdc48, 0xd83d, 0xde4f, 0xd83d, 0xdc4f, 0xd83d, 0xdcaa, 0xd83d, 0xdeb6, 0xd83c, 0xdfc3,
|
||||||
|
0xd83d, 0xdc83, 0xd83d, 0xdc6b, 0xd83d, 0xdc6a, 0xd83d, 0xdc6c, 0xd83d, 0xdc6d, 0xd83d, 0xdc85,
|
||||||
|
0xd83c, 0xdfa9, 0xd83d, 0xdc51, 0xd83d, 0xdc52, 0xd83d, 0xdc5f, 0xd83d, 0xdc5e, 0xd83d, 0xdc60,
|
||||||
|
0xd83d, 0xdc55, 0xd83d, 0xdc57, 0xd83d, 0xdc56, 0xd83d, 0xdc59, 0xd83d, 0xdc5c, 0xd83d, 0xdc53,
|
||||||
|
0xd83c, 0xdf80, 0xd83d, 0xdc84, 0xd83d, 0xdc9b, 0xd83d, 0xdc99, 0xd83d, 0xdc9c, 0xd83d, 0xdc9a,
|
||||||
|
0xd83d, 0xdc8d, 0xd83d, 0xdc8e, 0xd83d, 0xdc36, 0xd83d, 0xdc3a, 0xd83d, 0xdc31, 0xd83d, 0xdc2d,
|
||||||
|
0xd83d, 0xdc39, 0xd83d, 0xdc30, 0xd83d, 0xdc38, 0xd83d, 0xdc2f, 0xd83d, 0xdc28, 0xd83d, 0xdc3b,
|
||||||
|
0xd83d, 0xdc37, 0xd83d, 0xdc2e, 0xd83d, 0xdc17, 0xd83d, 0xdc34, 0xd83d, 0xdc11, 0xd83d, 0xdc18,
|
||||||
|
0xd83d, 0xdc3c, 0xd83d, 0xdc27, 0xd83d, 0xdc25, 0xd83d, 0xdc14, 0xd83d, 0xdc0d, 0xd83d, 0xdc22,
|
||||||
|
0xd83d, 0xdc1b, 0xd83d, 0xdc1d, 0xd83d, 0xdc1c, 0xd83d, 0xdc1e, 0xd83d, 0xdc0c, 0xd83d, 0xdc19,
|
||||||
|
0xd83d, 0xdc1a, 0xd83d, 0xdc1f, 0xd83d, 0xdc2c, 0xd83d, 0xdc0b, 0xd83d, 0xdc10, 0xd83d, 0xdc0a,
|
||||||
|
0xd83d, 0xdc2b, 0xd83c, 0xdf40, 0xd83c, 0xdf39, 0xd83c, 0xdf3b, 0xd83c, 0xdf41, 0xd83c, 0xdf3e,
|
||||||
|
0xd83c, 0xdf44, 0xd83c, 0xdf35, 0xd83c, 0xdf34, 0xd83c, 0xdf33, 0xd83c, 0xdf1e, 0xd83c, 0xdf1a,
|
||||||
|
0xd83c, 0xdf19, 0xd83c, 0xdf0e, 0xd83c, 0xdf0b, 0x26a1, 0x2614, 0x2744, 0x26c4, 0xd83c, 0xdf00,
|
||||||
|
0xd83c, 0xdf08, 0xd83c, 0xdf0a, 0xd83c, 0xdf93, 0xd83c, 0xdf86, 0xd83c, 0xdf83, 0xd83d, 0xdc7b,
|
||||||
|
0xd83c, 0xdf85, 0xd83c, 0xdf84, 0xd83c, 0xdf81, 0xd83c, 0xdf88, 0xd83d, 0xdd2e, 0xd83c, 0xdfa5,
|
||||||
|
0xd83d, 0xdcf7, 0xd83d, 0xdcbf, 0xd83d, 0xdcbb, 0x260e, 0xd83d, 0xdce1, 0xd83d, 0xdcfa, 0xd83d,
|
||||||
|
0xdcfb, 0xd83d, 0xdd09, 0xd83d, 0xdd14, 0x23f3, 0x23f0, 0x231a, 0xd83d, 0xdd12, 0xd83d, 0xdd11,
|
||||||
|
0xd83d, 0xdd0e, 0xd83d, 0xdca1, 0xd83d, 0xdd26, 0xd83d, 0xdd0c, 0xd83d, 0xdd0b, 0xd83d, 0xdebf,
|
||||||
|
0xd83d, 0xdebd, 0xd83d, 0xdd27, 0xd83d, 0xdd28, 0xd83d, 0xdeaa, 0xd83d, 0xdeac, 0xd83d, 0xdca3,
|
||||||
|
0xd83d, 0xdd2b, 0xd83d, 0xdd2a, 0xd83d, 0xdc8a, 0xd83d, 0xdc89, 0xd83d, 0xdcb0, 0xd83d, 0xdcb5,
|
||||||
|
0xd83d, 0xdcb3, 0x2709, 0xd83d, 0xdceb, 0xd83d, 0xdce6, 0xd83d, 0xdcc5, 0xd83d, 0xdcc1, 0x2702,
|
||||||
|
0xd83d, 0xdccc, 0xd83d, 0xdcce, 0x2712, 0x270f, 0xd83d, 0xdcd0, 0xd83d, 0xdcda, 0xd83d, 0xdd2c,
|
||||||
|
0xd83d, 0xdd2d, 0xd83c, 0xdfa8, 0xd83c, 0xdfac, 0xd83c, 0xdfa4, 0xd83c, 0xdfa7, 0xd83c, 0xdfb5,
|
||||||
|
0xd83c, 0xdfb9, 0xd83c, 0xdfbb, 0xd83c, 0xdfba, 0xd83c, 0xdfb8, 0xd83d, 0xdc7e, 0xd83c, 0xdfae,
|
||||||
|
0xd83c, 0xdccf, 0xd83c, 0xdfb2, 0xd83c, 0xdfaf, 0xd83c, 0xdfc8, 0xd83c, 0xdfc0, 0x26bd, 0x26be,
|
||||||
|
0xd83c, 0xdfbe, 0xd83c, 0xdfb1, 0xd83c, 0xdfc9, 0xd83c, 0xdfb3, 0xd83c, 0xdfc1, 0xd83c, 0xdfc7,
|
||||||
|
0xd83c, 0xdfc6, 0xd83c, 0xdfca, 0xd83c, 0xdfc4, 0x2615, 0xd83c, 0xdf7c, 0xd83c, 0xdf7a, 0xd83c,
|
||||||
|
0xdf77, 0xd83c, 0xdf74, 0xd83c, 0xdf55, 0xd83c, 0xdf54, 0xd83c, 0xdf5f, 0xd83c, 0xdf57, 0xd83c,
|
||||||
|
0xdf71, 0xd83c, 0xdf5a, 0xd83c, 0xdf5c, 0xd83c, 0xdf61, 0xd83c, 0xdf73, 0xd83c, 0xdf5e, 0xd83c,
|
||||||
|
0xdf69, 0xd83c, 0xdf66, 0xd83c, 0xdf82, 0xd83c, 0xdf70, 0xd83c, 0xdf6a, 0xd83c, 0xdf6b, 0xd83c,
|
||||||
|
0xdf6d, 0xd83c, 0xdf6f, 0xd83c, 0xdf4e, 0xd83c, 0xdf4f, 0xd83c, 0xdf4a, 0xd83c, 0xdf4b, 0xd83c,
|
||||||
|
0xdf52, 0xd83c, 0xdf47, 0xd83c, 0xdf49, 0xd83c, 0xdf53, 0xd83c, 0xdf51, 0xd83c, 0xdf4c, 0xd83c,
|
||||||
|
0xdf50, 0xd83c, 0xdf4d, 0xd83c, 0xdf46, 0xd83c, 0xdf45, 0xd83c, 0xdf3d, 0xd83c, 0xdfe1, 0xd83c,
|
||||||
|
0xdfe5, 0xd83c, 0xdfe6, 0x26ea, 0xd83c, 0xdff0, 0x26fa, 0xd83c, 0xdfed, 0xd83d, 0xddfb, 0xd83d,
|
||||||
|
0xddfd, 0xd83c, 0xdfa0, 0xd83c, 0xdfa1, 0x26f2, 0xd83c, 0xdfa2, 0xd83d, 0xdea2, 0xd83d, 0xdea4,
|
||||||
|
0x2693, 0xd83d, 0xde80, 0x2708, 0xd83d, 0xde81, 0xd83d, 0xde82, 0xd83d, 0xde8b, 0xd83d, 0xde8e,
|
||||||
|
0xd83d, 0xde8c, 0xd83d, 0xde99, 0xd83d, 0xde97, 0xd83d, 0xde95, 0xd83d, 0xde9b, 0xd83d, 0xdea8,
|
||||||
|
0xd83d, 0xde94, 0xd83d, 0xde92, 0xd83d, 0xde91, 0xd83d, 0xdeb2, 0xd83d, 0xdea0, 0xd83d, 0xde9c,
|
||||||
|
0xd83d, 0xdea6, 0x26a0, 0xd83d, 0xdea7, 0x26fd, 0xd83c, 0xdfb0, 0xd83d, 0xddff, 0xd83c, 0xdfaa,
|
||||||
|
0xd83c, 0xdfad, 0xd83c, 0xddef, 0xd83c, 0xddf5, 0xd83c, 0xddf0, 0xd83c, 0xddf7, 0xd83c, 0xdde9,
|
||||||
|
0xd83c, 0xddea, 0xd83c, 0xdde8, 0xd83c, 0xddf3, 0xd83c, 0xddfa, 0xd83c, 0xddf8, 0xd83c, 0xddeb,
|
||||||
|
0xd83c, 0xddf7, 0xd83c, 0xddea, 0xd83c, 0xddf8, 0xd83c, 0xddee, 0xd83c, 0xddf9, 0xd83c, 0xddf7,
|
||||||
|
0xd83c, 0xddfa, 0xd83c, 0xddec, 0xd83c, 0xdde7, 0x0031, 0x20e3, 0x0032, 0x20e3, 0x0033, 0x20e3,
|
||||||
|
0x0034, 0x20e3, 0x0035, 0x20e3, 0x0036, 0x20e3, 0x0037, 0x20e3, 0x0038, 0x20e3, 0x0039, 0x20e3,
|
||||||
|
0x0030, 0x20e3, 0xd83d, 0xdd1f, 0x2757, 0x2753, 0x2665, 0x2666, 0xd83d, 0xdcaf, 0xd83d, 0xdd17,
|
||||||
|
0xd83d, 0xdd31, 0xd83d, 0xdd34, 0xd83d, 0xdd35, 0xd83d, 0xdd36, 0xd83d, 0xdd37,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const EMOJI_OFFSETS = [
|
||||||
|
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22,
|
||||||
|
24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46,
|
||||||
|
48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70,
|
||||||
|
72, 74, 76, 78, 80, 82, 84, 86, 87, 88, 90, 92,
|
||||||
|
94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116,
|
||||||
|
118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140,
|
||||||
|
142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164,
|
||||||
|
166, 168, 170, 172, 174, 176, 178, 180, 182, 184, 186, 188,
|
||||||
|
190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212,
|
||||||
|
214, 216, 218, 220, 222, 224, 226, 228, 230, 232, 234, 236,
|
||||||
|
238, 240, 242, 244, 246, 248, 250, 252, 254, 256, 258, 259,
|
||||||
|
260, 261, 262, 264, 266, 268, 270, 272, 274, 276, 278, 280,
|
||||||
|
282, 284, 286, 288, 290, 292, 294, 295, 297, 299, 301, 303,
|
||||||
|
305, 306, 307, 308, 310, 312, 314, 316, 318, 320, 322, 324,
|
||||||
|
326, 328, 330, 332, 334, 336, 338, 340, 342, 344, 346, 348,
|
||||||
|
350, 351, 353, 355, 357, 359, 360, 362, 364, 365, 366, 368,
|
||||||
|
370, 372, 374, 376, 378, 380, 382, 384, 386, 388, 390, 392,
|
||||||
|
394, 396, 398, 400, 402, 404, 406, 407, 408, 410, 412, 414,
|
||||||
|
416, 418, 420, 422, 424, 426, 427, 429, 431, 433, 435, 437,
|
||||||
|
439, 441, 443, 445, 447, 449, 451, 453, 455, 457, 459, 461,
|
||||||
|
463, 465, 467, 469, 471, 473, 475, 477, 479, 481, 483, 485,
|
||||||
|
487, 489, 491, 493, 495, 497, 499, 501, 503, 505, 507, 508,
|
||||||
|
510, 511, 513, 515, 517, 519, 521, 522, 524, 526, 528, 529,
|
||||||
|
531, 532, 534, 536, 538, 540, 542, 544, 546, 548, 550, 552,
|
||||||
|
554, 556, 558, 560, 562, 564, 566, 567, 569, 570, 572, 574,
|
||||||
|
576, 578, 582, 586, 590, 594, 598, 602, 606, 610, 614, 618,
|
||||||
|
620, 622, 624, 626, 628, 630, 632, 634, 636, 638, 640, 641,
|
||||||
|
642, 643, 644, 646, 648, 650, 652, 654, 656, 658,
|
||||||
|
];
|
||||||
@ -80,7 +80,16 @@ module.exports = (env = {}, argv = {}) => {
|
|||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
use: [
|
use: [
|
||||||
MiniCssExtractPlugin.loader,
|
MiniCssExtractPlugin.loader,
|
||||||
'css-loader',
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: {
|
||||||
|
exportLocalsConvention: 'camelCase',
|
||||||
|
auto: true,
|
||||||
|
localIdentName: argv['optimize-minimize'] ? '[hash:base64]' : '[path][name]__[local]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
'postcss-loader',
|
'postcss-loader',
|
||||||
'sass-loader',
|
'sass-loader',
|
||||||
],
|
],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user