Calls: Add peer-to-peer calls with fallback to group calls

This commit is contained in:
Alexander Zinchuk 2021-11-29 18:26:02 +01:00
parent 33258e0f45
commit 0575fc6e00
17 changed files with 346 additions and 52 deletions

View File

@ -495,10 +495,10 @@ export async function updateChatMutedState({
}
export async function createChannel({
title, about, users,
title, about = '', users,
}: {
title: string; about?: string; users: ApiUser[];
}): Promise<ApiChat | undefined> {
title: string; about?: string; users?: ApiUser[];
}, noErrorUpdate = false): Promise<ApiChat | undefined> {
const result = await invokeRequest(new GramJs.channels.CreateChannel({
broadcast: true,
title,
@ -527,10 +527,16 @@ export async function createChannel({
const channel = buildApiChatFromPreview(newChannel)!;
await invokeRequest(new GramJs.channels.InviteToChannel({
channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel,
users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[],
}));
if (users?.length) {
try {
await invokeRequest(new GramJs.channels.InviteToChannel({
channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel,
users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[],
}), true, noErrorUpdate);
} catch (err) {
// `noErrorUpdate` will cause an exception which we don't want either
}
}
return channel;
}
@ -1027,20 +1033,25 @@ export async function openChatByInvite(hash: string) {
return { chatId: chat.id };
}
export function addChatMembers(chat: ApiChat, users: ApiUser[]) {
if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') {
return invokeRequest(new GramJs.channels.InviteToChannel({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[],
}), true);
}
export function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpdate = false) {
try {
if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') {
return invokeRequest(new GramJs.channels.InviteToChannel({
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[],
}), true, noErrorUpdate);
}
return Promise.all(users.map((user) => {
return invokeRequest(new GramJs.messages.AddChatUser({
chatId: buildInputEntity(chat.id) as BigInt.BigInteger,
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
}), true);
}));
return Promise.all(users.map((user) => {
return invokeRequest(new GramJs.messages.AddChatUser({
chatId: buildInputEntity(chat.id) as BigInt.BigInteger,
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
}), true, noErrorUpdate);
}));
} catch (err) {
// `noErrorUpdate` will cause an exception which we don't want either
return undefined;
}
}
export function deleteChatMember(chat: ApiChat, user: ApiUser) {

View File

@ -42,15 +42,19 @@ export async function setChatUsername(
}
}
export async function updatePrivateLink(
{ chat }: { chat: ApiChat },
) {
export async function updatePrivateLink({
chat, usageLimit, expireDate,
}: {
chat: ApiChat; usageLimit?: number; expireDate?: number;
}) {
const result = await invokeRequest(new GramJs.messages.ExportChatInvite({
peer: buildInputPeer(chat.id, chat.accessHash),
usageLimit,
expireDate,
}));
if (!result || !(result instanceof GramJs.ChatInviteExported)) {
return;
if (!result) {
return undefined;
}
onUpdate({
@ -60,4 +64,6 @@ export async function updatePrivateLink(
inviteLink: result.link,
},
});
return result.link;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,2 +1,3 @@
export { default as GroupCall } from '../components/calls/group/GroupCall';
export { default as ActiveCallHeader } from '../components/calls/ActiveCallHeader';
export { default as CallFallbackConfirm } from '../components/calls/CallFallbackConfirm';

View File

@ -0,0 +1,15 @@
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);

View File

@ -0,0 +1,68 @@
import React, { FC, memo, useState } from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
import { GlobalActions } from '../../global/types';
import { pick } from '../../util/iteratees';
import ConfirmDialog from '../ui/ConfirmDialog';
import Checkbox from '../ui/Checkbox';
import { selectCallFallbackChannelTitle } from '../../modules/selectors/calls';
import { getUserFullName } from '../../modules/helpers';
import { selectCurrentMessageList, selectUser } from '../../modules/selectors';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
export type OwnProps = {
isOpen: boolean;
};
interface StateProps {
userFullName?: string;
channelTitle: string;
}
type DispatchProps = Pick<GlobalActions, 'closeCallFallbackConfirm' | 'inviteToCallFallback'>;
const CallFallbackConfirm: FC<OwnProps & StateProps & DispatchProps> = ({
isOpen,
channelTitle,
userFullName,
closeCallFallbackConfirm,
inviteToCallFallback,
}) => {
const [shouldRemove, setShouldRemove] = useState(true);
const renderingUserFullName = useCurrentOrPrev(userFullName, true);
return (
<ConfirmDialog
title="Start Call"
isOpen={isOpen}
confirmHandler={() => {
inviteToCallFallback({ shouldRemove });
}}
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),
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'closeCallFallbackConfirm', 'inviteToCallFallback',
]),
)(CallFallbackConfirm));

View File

@ -46,6 +46,7 @@ import HistoryCalendar from './HistoryCalendar.async';
import StickerSetModal from '../common/StickerSetModal.async';
import GroupCall from '../calls/group/GroupCall.async';
import ActiveCallHeader from '../calls/ActiveCallHeader.async';
import CallFallbackConfirm from '../calls/CallFallbackConfirm.async';
import './Main.scss';
@ -67,6 +68,7 @@ type StateProps = {
animationLevel: number;
language?: LangCode;
wasTimeFormatSetManually?: boolean;
isCallFallbackConfirmOpen: boolean;
};
type DispatchProps = Pick<GlobalActions, (
@ -100,6 +102,7 @@ const Main: FC<StateProps & DispatchProps> = ({
animationLevel,
language,
wasTimeFormatSetManually,
isCallFallbackConfirmOpen,
loadAnimatedEmojis,
loadNotificationSettings,
loadNotificationExceptions,
@ -282,6 +285,7 @@ const Main: FC<StateProps & DispatchProps> = ({
</>
)}
<DownloadManager />
<CallFallbackConfirm isOpen={isCallFallbackConfirmOpen} />
</div>
);
};
@ -333,6 +337,7 @@ export default memo(withGlobal(
animationLevel,
language,
wasTimeFormatSetManually,
isCallFallbackConfirmOpen: Boolean(global.groupCalls.isFallbackConfirmOpen),
};
},
(setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -13,7 +13,9 @@ import { IAnchorPosition } from '../../types';
import { ARE_CALLS_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { pick } from '../../util/iteratees';
import { isChatBasicGroup, isChatChannel, isChatSuperGroup } from '../../modules/helpers';
import {
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
} from '../../modules/helpers';
import {
selectChat,
selectChatBot,
@ -43,13 +45,16 @@ interface StateProps {
canRestartBot?: boolean;
canSubscribe?: boolean;
canSearch?: boolean;
canCall?: boolean;
canMute?: boolean;
canLeave?: boolean;
canEnterVoiceChat?: boolean;
canCreateVoiceChat?: boolean;
}
type DispatchProps = Pick<GlobalActions, 'joinChannel' | 'sendBotCommand' | 'openLocalTextSearch' | 'restartBot'>;
type DispatchProps = Pick<GlobalActions, (
'joinChannel' | 'sendBotCommand' | 'openLocalTextSearch' | 'restartBot' | 'openCallFallbackConfirm'
)>;
// Chrome breaks layout when focusing input during transition
const SEARCH_FOCUS_DELAY_MS = 400;
@ -63,6 +68,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
canRestartBot,
canSubscribe,
canSearch,
canCall,
canMute,
canLeave,
canEnterVoiceChat,
@ -73,6 +79,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
sendBotCommand,
openLocalTextSearch,
restartBot,
openCallFallbackConfirm,
}) => {
// eslint-disable-next-line no-null/no-null
const menuButtonRef = useRef<HTMLButtonElement>(null);
@ -170,6 +177,17 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
<i className="icon-search" />
</Button>
)}
{canCall && (
<Button
round
color="translucent"
size="smaller"
onClick={openCallFallbackConfirm}
ariaLabel="Call"
>
<i className="icon-phone" />
</Button>
)}
</>
)}
<Button
@ -197,6 +215,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
canRestartBot={canRestartBot}
canSubscribe={canSubscribe}
canSearch={canSearch}
canCall={canCall}
canMute={canMute}
canLeave={canLeave}
canEnterVoiceChat={canEnterVoiceChat}
@ -216,7 +235,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId);
const isChannel = Boolean(chat && isChatChannel(chat));
if (chat?.isRestricted || selectIsInSelectMode(global)) {
if (!chat || chat.isRestricted || selectIsInSelectMode(global)) {
return {
noMenu: true,
};
@ -231,13 +250,14 @@ export default memo(withGlobal<OwnProps>(
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
const canSubscribe = Boolean(
isMainThread && chat && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
isMainThread && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
);
const canSearch = isMainThread || isDiscussionThread;
const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot;
const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
const canLeave = isMainThread && !canSubscribe;
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && chat && chat.isCallActive;
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && chat && !chat.isCallActive
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && chat.isCallActive;
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && !chat.isCallActive
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
return {
@ -248,6 +268,7 @@ export default memo(withGlobal<OwnProps>(
canRestartBot,
canSubscribe,
canSearch,
canCall,
canMute,
canLeave,
canEnterVoiceChat,
@ -255,6 +276,6 @@ export default memo(withGlobal<OwnProps>(
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'joinChannel', 'sendBotCommand', 'openLocalTextSearch', 'restartBot',
'joinChannel', 'sendBotCommand', 'openLocalTextSearch', 'restartBot', 'openCallFallbackConfirm',
]),
)(HeaderActions));

View File

@ -28,7 +28,7 @@ import './HeaderMenuContainer.scss';
type DispatchProps = Pick<GlobalActions, (
'updateChatMutedState' | 'enterMessageSelectMode' | 'sendBotCommand' | 'restartBot' | 'openLinkedChat' |
'joinGroupCall' | 'createGroupCall' | 'addContact'
'joinGroupCall' | 'createGroupCall' | 'addContact' | 'openCallFallbackConfirm'
)>;
export type OwnProps = {
@ -42,6 +42,7 @@ export type OwnProps = {
canRestartBot?: boolean;
canSubscribe?: boolean;
canSearch?: boolean;
canCall?: boolean;
canMute?: boolean;
canLeave?: boolean;
canEnterVoiceChat?: boolean;
@ -71,6 +72,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
canRestartBot,
canSubscribe,
canSearch,
canCall,
canMute,
canLeave,
canEnterVoiceChat,
@ -93,6 +95,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
createGroupCall,
openLinkedChat,
addContact,
openCallFallbackConfirm,
}) => {
const [isMenuOpen, setIsMenuOpen] = useState(true);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -157,6 +160,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
closeMenu();
}, [closeMenu, onSubscribeChannel]);
const handleCall = useCallback(() => {
openCallFallbackConfirm();
closeMenu();
}, [closeMenu, openCallFallbackConfirm]);
const handleSearch = useCallback(() => {
onSearchClick();
closeMenu();
@ -216,6 +224,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
{lang('AddContact')}
</MenuItem>
)}
{IS_SINGLE_COLUMN_LAYOUT && canCall && (
<MenuItem
icon="phone"
onClick={handleCall}
>
{lang('Call')}
</MenuItem>
)}
{IS_SINGLE_COLUMN_LAYOUT && canSearch && (
<MenuItem
icon="search"
@ -306,5 +322,6 @@ export default memo(withGlobal<OwnProps>(
'createGroupCall',
'openLinkedChat',
'addContact',
'openCallFallbackConfirm',
]),
)(HeaderMenuContainer));

View File

@ -49,10 +49,6 @@ export default function useProfileViewportIds(
resultType, loadMoreMembers, lastSyncTime, memberIds,
);
const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems(
resultType, loadCommonChats, lastSyncTime, chatIds,
);
const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia(
'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
);
@ -73,6 +69,10 @@ export default function useProfileViewportIds(
'voice', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
);
const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems(
resultType, loadCommonChats, lastSyncTime, chatIds,
);
let viewportIds: number[] | string[] | undefined;
let getMore: AnyToVoidFunction | undefined;
let noProfileInfo = false;

View File

@ -18,6 +18,7 @@ type OwnProps = {
confirmHandler: () => void;
confirmIsDestructive?: boolean;
isButtonsInOneRow?: boolean;
children?: any;
};
const ConfirmDialog: FC<OwnProps> = ({
@ -32,6 +33,7 @@ const ConfirmDialog: FC<OwnProps> = ({
confirmHandler,
confirmIsDestructive,
isButtonsInOneRow,
children,
}) => {
const lang = useLang();
@ -48,7 +50,7 @@ const ConfirmDialog: FC<OwnProps> = ({
{text && text.split('\\n').map((textPart) => (
<p>{textPart}</p>
))}
{textParts}
{textParts || children}
<div className={isButtonsInOneRow ? 'dialog-buttons mt-2' : ''}>
<Button
className="confirm-dialog-button"

View File

@ -222,6 +222,7 @@ function updateCache() {
'shouldShowContextMenuHint',
'leftColumnWidth',
'serviceNotifications',
// TODO Support 'groupCalls'
]),
audioPlayer: {
volume: global.audioPlayer.volume,
@ -237,6 +238,7 @@ function updateCache() {
},
settings: reduceSettings(global),
chatFolders: reduceChatFolders(global),
groupCalls: reduceGroupCalls(global),
};
const json = JSON.stringify(reducedGlobal);
@ -332,6 +334,16 @@ function reduceChatFolders(global: GlobalState): GlobalState['chatFolders'] {
};
}
function reduceGroupCalls(global: GlobalState): GlobalState['groupCalls'] {
return {
...global.groupCalls,
byId: {},
activeGroupCallId: undefined,
isGroupCallPanelHidden: undefined,
isFallbackConfirmOpen: undefined,
};
}
function setupHeavyAnimationListeners() {
document.addEventListener(ANIMATION_START_EVENT, () => {
isHeavyAnimating = true;

View File

@ -48,7 +48,10 @@ import {
AudioOrigin,
} from '../types';
export type MessageListType = 'thread' | 'pinned' | 'scheduled';
export type MessageListType =
'thread'
| 'pinned'
| 'scheduled';
export interface MessageList {
chatId: string;
@ -168,6 +171,9 @@ export type GlobalState = {
byId: Record<string, ApiGroupCall>;
activeGroupCallId?: string;
isGroupCallPanelHidden?: boolean;
isFallbackConfirmOpen?: boolean;
fallbackChatId?: string;
fallbackUserIdsToRemove?: string[];
};
scheduledMessages: {
@ -551,7 +557,8 @@ export type ActionTypes = (
'joinGroupCall' | 'toggleGroupCallMute' | 'toggleGroupCallPresentation' | 'leaveGroupCall' |
'toggleGroupCallVideo' | 'requestToSpeak' | 'setGroupCallParticipantVolume' | 'toggleGroupCallPanel' |
'createGroupCall' | 'joinVoiceChatByLink' | 'subscribeToGroupCallUpdates' | 'createGroupCallInviteLink' |
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' | 'playGroupCallSound'
'loadMoreGroupCallParticipants' | 'connectToActiveGroupCall' | 'playGroupCallSound' |
'openCallFallbackConfirm' | 'closeCallFallbackConfirm' | 'inviteToCallFallback'
);
export type GlobalActions = Record<ActionTypes, (...args: any[]) => void>;

View File

@ -617,9 +617,9 @@ export function useMemo<T extends any>(resolver: () => T, dependencies: any[], d
return current;
}
export function useCallback<F extends AnyFunction>(newCallback: F, dependencies: any[]): F {
export function useCallback<F extends AnyFunction>(newCallback: F, dependencies: any[], debugKey?: string): F {
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => newCallback, dependencies);
return useMemo(() => newCallback, dependencies, debugKey);
}
export function useRef<T>(initial: T): { current: T };

View File

@ -1,3 +1,4 @@
import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
import {
joinGroupCall,
startSharingScreen,
@ -7,11 +8,15 @@ import {
setVolume,
handleUpdateGroupCallParticipants, handleUpdateGroupCallConnection,
} from '../../../lib/secret-sauce';
import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
import { ApiUpdate } from '../../../api/types';
import { GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
import { callApi } from '../../../api/gramjs';
import { selectChat, selectUser } from '../../selectors';
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
import {
selectActiveGroupCall,
selectCallFallbackChannelTitle,
selectGroupCallParticipant,
} from '../../selectors/calls';
import {
@ -20,12 +25,16 @@ import {
updateGroupCall,
updateGroupCallParticipant,
} from '../../reducers/calls';
import { ApiUpdate } from '../../../api/types';
import { GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
import { omit } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { fetchFile } from '../../../util/files';
import { getGroupCallAudioContext, getGroupCallAudioElement, removeGroupCallAudioElement } from '../ui/calls';
import { loadFullChat } from './chats';
import callFallbackAvatarPath from '../../../assets/call-fallback-avatar.png';
const FALLBACK_INVITE_EXPIRE_SECONDS = 1800; // 30 min
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
const { activeGroupCallId } = global.groupCalls;
@ -88,16 +97,25 @@ addReducer('leaveGroupCall', (global, actions, payload) => {
return;
}
global = updateActiveGroupCall(global, {
connectionState: 'disconnected',
}, groupCall.participantsCount - 1);
setGlobal(updateActiveGroupCall(global, { connectionState: 'disconnected' }, groupCall.participantsCount - 1));
(async () => {
await callApi('leaveGroupCall', {
call: groupCall,
});
let shouldResetFallbackState = false;
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', {
call: groupCall,
});
@ -116,6 +134,10 @@ addReducer('leaveGroupCall', (global, actions, payload) => {
...global.groupCalls,
isGroupCallPanelHidden: true,
activeGroupCallId: undefined,
...(shouldResetFallbackState && {
fallbackChatId: undefined,
fallbackUserIdsToRemove: undefined,
}),
},
});
@ -281,3 +303,81 @@ addReducer('connectToActiveGroupCall', (global, actions) => {
}
})();
});
addReducer('inviteToCallFallback', (global, actions, payload) => {
const { chatId } = selectCurrentMessageList(global) || {};
if (!chatId) {
return;
}
const user = selectUser(global, chatId);
if (!user) {
return;
}
const { shouldRemove } = payload;
(async () => {
const fallbackChannelTitle = selectCallFallbackChannelTitle(global);
let fallbackChannel = Object.values(global.chats.byId).find((channel) => {
return (
channel.title === fallbackChannelTitle
&& channel.isCreator
&& !channel.isRestricted
);
});
if (!fallbackChannel) {
fallbackChannel = await callApi('createChannel', {
title: fallbackChannelTitle,
users: [user],
});
if (!fallbackChannel) {
return;
}
const photo = await fetchFile(callFallbackAvatarPath, 'avatar.png');
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 inviteLink = await callApi('updatePrivateLink', {
chat: fallbackChannel,
usageLimit: 1,
expireDate: getServerTime(global.serverTimeOffset) + FALLBACK_INVITE_EXPIRE_SECONDS,
});
if (!inviteLink) {
return;
}
if (shouldRemove) {
global = getGlobal();
const fallbackUserIdsToRemove = global.groupCalls.fallbackUserIdsToRemove || [];
setGlobal({
...global,
groupCalls: {
...global.groupCalls,
fallbackChatId: fallbackChannel.id,
fallbackUserIdsToRemove: [...fallbackUserIdsToRemove, chatId],
},
});
}
actions.sendMessage({ text: `Join a call: ${inviteLink}` });
actions.openChat({ id: fallbackChannel.id });
actions.createGroupCall({ chatId: fallbackChannel.id });
actions.closeCallFallbackConfirm();
})();
});

View File

@ -61,11 +61,13 @@ async function fetchGroupCall(groupCall: Partial<ApiGroupCall>) {
const existingGroupCall = selectGroupCall(global, groupCall.id!);
global = updateGroupCall(global,
global = updateGroupCall(
global,
groupCall.id!,
omit(result.groupCall, ['connectionState']),
undefined,
existingGroupCall?.isLoaded ? undefined : result.groupCall.participantsCount);
existingGroupCall?.isLoaded ? undefined : result.groupCall.participantsCount,
);
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
@ -309,3 +311,23 @@ export function removeGroupCallAudioElement() {
audioContext = undefined;
audioElement = undefined;
}
addReducer('openCallFallbackConfirm', (global) => {
return {
...global,
groupCalls: {
...global.groupCalls,
isFallbackConfirmOpen: true,
},
};
});
addReducer('closeCallFallbackConfirm', (global) => {
return {
...global,
groupCalls: {
...global.groupCalls,
isFallbackConfirmOpen: false,
},
};
});

View File

@ -1,6 +1,7 @@
import { GlobalState } from '../../global/types';
import { selectChat } from './chats';
import { isChatBasicGroup } from '../helpers';
import { getUserFullName, isChatBasicGroup } from '../helpers';
import { selectUser } from './users';
export function selectChatGroupCall(global: GlobalState, chatId: string) {
const chat = selectChat(global, chatId);
@ -36,3 +37,9 @@ export function selectActiveGroupCall(global: GlobalState) {
return selectGroupCall(global, activeGroupCallId);
}
export function selectCallFallbackChannelTitle(global: GlobalState) {
const currentUser = selectUser(global, global.currentUserId!);
return `Calls: ${getUserFullName(currentUser!)}`;
}