mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-21 21:01:29 +01:00
Calls: Add peer-to-peer calls with fallback to group calls
This commit is contained in:
parent
33258e0f45
commit
0575fc6e00
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
BIN
src/assets/call-fallback-avatar.png
Normal file
BIN
src/assets/call-fallback-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -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';
|
||||
|
15
src/components/calls/CallFallbackConfirm.async.tsx
Normal file
15
src/components/calls/CallFallbackConfirm.async.tsx
Normal 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);
|
68
src/components/calls/CallFallbackConfirm.tsx
Normal file
68
src/components/calls/CallFallbackConfirm.tsx
Normal 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));
|
@ -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, [
|
||||
|
@ -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));
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
@ -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 };
|
||||
|
@ -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();
|
||||
})();
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -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!)}`;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user