2022-03-19 21:19:14 +01:00

335 lines
8.2 KiB
TypeScript

import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { selectActiveGroupCall, selectChatGroupCall, selectGroupCall } from '../../selectors/calls';
import { callApi } from '../../../api/gramjs';
import { selectChat } from '../../selectors';
import { copyTextToClipboard } from '../../../util/clipboard';
import { ApiGroupCall } from '../../../api/types';
import { updateGroupCall } from '../../reducers/calls';
import { buildCollectionByKey, omit } from '../../../util/iteratees';
import { addChats, addUsers } from '../../reducers';
import { fetchChatByUsername, loadFullChat } from '../api/chats';
import safePlay from '../../../util/safePlay';
import { ARE_CALLS_SUPPORTED } from '../../../util/environment';
import * as langProvider from '../../../util/langProvider';
// Workaround for Safari not playing audio without user interaction
let audioElement: HTMLAudioElement | undefined;
let audioContext: AudioContext | undefined;
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 sounds: Record<string, HTMLAudioElement> = {
join: joinAudio,
allowTalk: allowTalkAudio,
leave: leaveAudio,
connecting: connectingAudio,
};
let initializationPromise: Promise<void> | undefined = Promise.resolve();
const initializeSoundsForSafari = () => {
if (!initializationPromise) return Promise.resolve();
initializationPromise = Promise.all(Object.values(sounds).map((l) => {
l.muted = true;
l.volume = 0.0001;
return l.play().then(() => {
l.pause();
l.volume = 1;
l.currentTime = 0;
l.muted = false;
});
})).then(() => {
initializationPromise = undefined;
});
return initializationPromise;
};
async function fetchGroupCall(groupCall: Partial<ApiGroupCall>) {
const result = await callApi('getGroupCall', {
call: groupCall,
});
if (!result) return undefined;
let global = getGlobal();
const existingGroupCall = selectGroupCall(global, groupCall.id!);
global = updateGroupCall(
global,
groupCall.id!,
omit(result.groupCall, ['connectionState']),
undefined,
existingGroupCall?.isLoaded ? undefined : result.groupCall.participantsCount,
);
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
setGlobal(global);
return result.groupCall;
}
async function fetchGroupCallParticipants(groupCall: Partial<ApiGroupCall>, nextOffset?: string) {
const result = await callApi('fetchGroupCallParticipants', {
call: groupCall as ApiGroupCall,
offset: nextOffset,
});
if (!result) return;
let global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
setGlobal(global);
}
addActionHandler('toggleGroupCallPanel', (global) => {
return {
...global,
groupCalls: {
...global.groupCalls,
isGroupCallPanelHidden: !global.groupCalls.isGroupCallPanelHidden,
},
};
});
addActionHandler('subscribeToGroupCallUpdates', async (global, actions, payload) => {
const { subscribed, id } = payload!;
const groupCall = selectGroupCall(global, id);
if (!groupCall) return;
if (subscribed) {
await fetchGroupCall(groupCall);
await fetchGroupCallParticipants(groupCall);
}
await callApi('toggleGroupCallStartSubscription', {
subscribed,
call: groupCall,
});
});
addActionHandler('createGroupCall', async (global, actions, payload) => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const result = await callApi('createGroupCall', {
peer: chat,
});
if (!result) return;
global = getGlobal();
setGlobal(updateGroupCall(global, result.id, {
...result,
chatId,
}));
actions.joinGroupCall({ id: result.id, accessHash: result.accessHash });
});
addActionHandler('createGroupCallInviteLink', async (global, actions) => {
const groupCall = selectActiveGroupCall(global);
if (!groupCall || !groupCall.chatId) {
return;
}
const chat = selectChat(global, groupCall.chatId);
if (!chat) {
return;
}
const canInvite = Boolean(chat.username);
let { inviteLink } = chat.fullInfo!;
if (canInvite) {
inviteLink = await callApi('exportGroupCallInvite', {
call: groupCall,
canSelfUnmute: false,
});
}
if (!inviteLink) {
return;
}
copyTextToClipboard(inviteLink);
actions.showNotification({
message: 'Link copied to clipboard',
});
});
addActionHandler('joinVoiceChatByLink', async (global, actions, payload) => {
const { username, inviteHash } = payload!;
const chat = await fetchChatByUsername(username);
if (!chat) {
actions.showNotification({ message: langProvider.getTranslation('NoUsernameFound') });
return;
}
const full = await loadFullChat(chat);
if (full?.groupCall) {
actions.joinGroupCall({ id: full.groupCall.id, accessHash: full.groupCall.accessHash, inviteHash });
}
});
addActionHandler('joinGroupCall', async (global, actions, payload) => {
if (!ARE_CALLS_SUPPORTED) return undefined;
const {
chatId, id, accessHash, inviteHash,
} = payload;
createAudioElement();
await initializeSoundsForSafari();
const { groupCalls: { activeGroupCallId } } = global;
let groupCall = id ? selectGroupCall(global, id) : selectChatGroupCall(global, chatId);
if (groupCall?.id === activeGroupCallId) {
actions.toggleGroupCallPanel();
return undefined;
}
if (activeGroupCallId) {
actions.leaveGroupCall({
rejoin: payload,
});
return undefined;
}
if (groupCall && activeGroupCallId === groupCall.id) {
actions.toggleGroupCallPanel();
return undefined;
}
if (!groupCall && (!id || !accessHash)) {
groupCall = await fetchGroupCall({
id,
accessHash,
});
}
if (!groupCall) return undefined;
global = getGlobal();
global = updateGroupCall(
global,
groupCall.id,
{
...groupCall,
inviteHash,
},
undefined,
groupCall.participantsCount + 1,
);
global = {
...global,
groupCalls: {
...global.groupCalls,
activeGroupCallId: groupCall.id,
isGroupCallPanelHidden: false,
},
};
return global;
});
addActionHandler('playGroupCallSound', (global, actions, payload) => {
const { sound } = payload!;
if (!sounds[sound]) {
return;
}
if (initializationPromise) {
initializationPromise.then(() => {
safePlay(sounds[sound]);
});
} else {
if (sound !== 'connecting') {
sounds.connecting.pause();
}
safePlay(sounds[sound]);
}
});
addActionHandler('loadMoreGroupCallParticipants', (global) => {
const groupCall = selectActiveGroupCall(global);
if (!groupCall) {
return;
}
void fetchGroupCallParticipants(groupCall, groupCall.nextOffset);
});
function createAudioContext() {
return (new (window.AudioContext || (window as any).webkitAudioContext)());
}
const silence = (ctx: AudioContext) => {
const oscillator = ctx.createOscillator();
const dst = oscillator.connect(ctx.createMediaStreamDestination());
oscillator.start();
return new MediaStream([Object.assign((dst as any).stream.getAudioTracks()[0], { enabled: false })]);
};
function createAudioElement() {
const ctx = createAudioContext();
audioElement = new Audio();
audioContext = ctx;
audioElement.srcObject = silence(ctx);
safePlay(audioElement);
}
export function getGroupCallAudioElement() {
return audioElement;
}
export function getGroupCallAudioContext() {
return audioContext;
}
export function removeGroupCallAudioElement() {
audioElement?.pause();
audioContext = undefined;
audioElement = undefined;
}
addActionHandler('openCallFallbackConfirm', (global) => {
return {
...global,
groupCalls: {
...global.groupCalls,
isFallbackConfirmOpen: true,
},
};
});
addActionHandler('closeCallFallbackConfirm', (global) => {
return {
...global,
groupCalls: {
...global.groupCalls,
isFallbackConfirmOpen: false,
},
};
});