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({
|
export async function createChannel({
|
||||||
title, about, users,
|
title, about = '', users,
|
||||||
}: {
|
}: {
|
||||||
title: string; about?: string; users: ApiUser[];
|
title: string; about?: string; users?: ApiUser[];
|
||||||
}): Promise<ApiChat | undefined> {
|
}, noErrorUpdate = false): Promise<ApiChat | undefined> {
|
||||||
const result = await invokeRequest(new GramJs.channels.CreateChannel({
|
const result = await invokeRequest(new GramJs.channels.CreateChannel({
|
||||||
broadcast: true,
|
broadcast: true,
|
||||||
title,
|
title,
|
||||||
@ -527,10 +527,16 @@ export async function createChannel({
|
|||||||
|
|
||||||
const channel = buildApiChatFromPreview(newChannel)!;
|
const channel = buildApiChatFromPreview(newChannel)!;
|
||||||
|
|
||||||
await invokeRequest(new GramJs.channels.InviteToChannel({
|
if (users?.length) {
|
||||||
channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel,
|
try {
|
||||||
users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[],
|
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;
|
return channel;
|
||||||
}
|
}
|
||||||
@ -1027,20 +1033,25 @@ export async function openChatByInvite(hash: string) {
|
|||||||
return { chatId: chat.id };
|
return { chatId: chat.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addChatMembers(chat: ApiChat, users: ApiUser[]) {
|
export function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpdate = false) {
|
||||||
if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') {
|
try {
|
||||||
return invokeRequest(new GramJs.channels.InviteToChannel({
|
if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') {
|
||||||
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
return invokeRequest(new GramJs.channels.InviteToChannel({
|
||||||
users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[],
|
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||||
}), true);
|
users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[],
|
||||||
}
|
}), true, noErrorUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.all(users.map((user) => {
|
return Promise.all(users.map((user) => {
|
||||||
return invokeRequest(new GramJs.messages.AddChatUser({
|
return invokeRequest(new GramJs.messages.AddChatUser({
|
||||||
chatId: buildInputEntity(chat.id) as BigInt.BigInteger,
|
chatId: buildInputEntity(chat.id) as BigInt.BigInteger,
|
||||||
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
||||||
}), true);
|
}), true, noErrorUpdate);
|
||||||
}));
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
// `noErrorUpdate` will cause an exception which we don't want either
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteChatMember(chat: ApiChat, user: ApiUser) {
|
export function deleteChatMember(chat: ApiChat, user: ApiUser) {
|
||||||
|
|||||||
@ -42,15 +42,19 @@ export async function setChatUsername(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePrivateLink(
|
export async function updatePrivateLink({
|
||||||
{ chat }: { chat: ApiChat },
|
chat, usageLimit, expireDate,
|
||||||
) {
|
}: {
|
||||||
|
chat: ApiChat; usageLimit?: number; expireDate?: number;
|
||||||
|
}) {
|
||||||
const result = await invokeRequest(new GramJs.messages.ExportChatInvite({
|
const result = await invokeRequest(new GramJs.messages.ExportChatInvite({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
usageLimit,
|
||||||
|
expireDate,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!result || !(result instanceof GramJs.ChatInviteExported)) {
|
if (!result) {
|
||||||
return;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@ -60,4 +64,6 @@ export async function updatePrivateLink(
|
|||||||
inviteLink: result.link,
|
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 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';
|
||||||
|
|||||||
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 StickerSetModal from '../common/StickerSetModal.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 './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -67,6 +68,7 @@ type StateProps = {
|
|||||||
animationLevel: number;
|
animationLevel: number;
|
||||||
language?: LangCode;
|
language?: LangCode;
|
||||||
wasTimeFormatSetManually?: boolean;
|
wasTimeFormatSetManually?: boolean;
|
||||||
|
isCallFallbackConfirmOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
@ -100,6 +102,7 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
animationLevel,
|
animationLevel,
|
||||||
language,
|
language,
|
||||||
wasTimeFormatSetManually,
|
wasTimeFormatSetManually,
|
||||||
|
isCallFallbackConfirmOpen,
|
||||||
loadAnimatedEmojis,
|
loadAnimatedEmojis,
|
||||||
loadNotificationSettings,
|
loadNotificationSettings,
|
||||||
loadNotificationExceptions,
|
loadNotificationExceptions,
|
||||||
@ -282,6 +285,7 @@ const Main: FC<StateProps & DispatchProps> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DownloadManager />
|
<DownloadManager />
|
||||||
|
<CallFallbackConfirm isOpen={isCallFallbackConfirmOpen} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -333,6 +337,7 @@ export default memo(withGlobal(
|
|||||||
animationLevel,
|
animationLevel,
|
||||||
language,
|
language,
|
||||||
wasTimeFormatSetManually,
|
wasTimeFormatSetManually,
|
||||||
|
isCallFallbackConfirmOpen: Boolean(global.groupCalls.isFallbackConfirmOpen),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(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 { ARE_CALLS_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
import { pick } from '../../util/iteratees';
|
import { pick } from '../../util/iteratees';
|
||||||
import { isChatBasicGroup, isChatChannel, isChatSuperGroup } from '../../modules/helpers';
|
import {
|
||||||
|
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
|
||||||
|
} from '../../modules/helpers';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
selectChatBot,
|
selectChatBot,
|
||||||
@ -43,13 +45,16 @@ interface StateProps {
|
|||||||
canRestartBot?: boolean;
|
canRestartBot?: boolean;
|
||||||
canSubscribe?: boolean;
|
canSubscribe?: boolean;
|
||||||
canSearch?: boolean;
|
canSearch?: boolean;
|
||||||
|
canCall?: boolean;
|
||||||
canMute?: boolean;
|
canMute?: boolean;
|
||||||
canLeave?: boolean;
|
canLeave?: boolean;
|
||||||
canEnterVoiceChat?: boolean;
|
canEnterVoiceChat?: boolean;
|
||||||
canCreateVoiceChat?: 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
|
// Chrome breaks layout when focusing input during transition
|
||||||
const SEARCH_FOCUS_DELAY_MS = 400;
|
const SEARCH_FOCUS_DELAY_MS = 400;
|
||||||
@ -63,6 +68,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canRestartBot,
|
canRestartBot,
|
||||||
canSubscribe,
|
canSubscribe,
|
||||||
canSearch,
|
canSearch,
|
||||||
|
canCall,
|
||||||
canMute,
|
canMute,
|
||||||
canLeave,
|
canLeave,
|
||||||
canEnterVoiceChat,
|
canEnterVoiceChat,
|
||||||
@ -73,6 +79,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
sendBotCommand,
|
sendBotCommand,
|
||||||
openLocalTextSearch,
|
openLocalTextSearch,
|
||||||
restartBot,
|
restartBot,
|
||||||
|
openCallFallbackConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
// 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);
|
||||||
@ -170,6 +177,17 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
<i className="icon-search" />
|
<i className="icon-search" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{canCall && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
onClick={openCallFallbackConfirm}
|
||||||
|
ariaLabel="Call"
|
||||||
|
>
|
||||||
|
<i className="icon-phone" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@ -197,6 +215,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canRestartBot={canRestartBot}
|
canRestartBot={canRestartBot}
|
||||||
canSubscribe={canSubscribe}
|
canSubscribe={canSubscribe}
|
||||||
canSearch={canSearch}
|
canSearch={canSearch}
|
||||||
|
canCall={canCall}
|
||||||
canMute={canMute}
|
canMute={canMute}
|
||||||
canLeave={canLeave}
|
canLeave={canLeave}
|
||||||
canEnterVoiceChat={canEnterVoiceChat}
|
canEnterVoiceChat={canEnterVoiceChat}
|
||||||
@ -216,7 +235,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const isChannel = Boolean(chat && isChatChannel(chat));
|
const isChannel = Boolean(chat && isChatChannel(chat));
|
||||||
|
|
||||||
if (chat?.isRestricted || selectIsInSelectMode(global)) {
|
if (!chat || chat.isRestricted || selectIsInSelectMode(global)) {
|
||||||
return {
|
return {
|
||||||
noMenu: true,
|
noMenu: true,
|
||||||
};
|
};
|
||||||
@ -231,13 +250,14 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
|
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
|
||||||
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
||||||
const canSubscribe = Boolean(
|
const canSubscribe = Boolean(
|
||||||
isMainThread && chat && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
|
isMainThread && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
|
||||||
);
|
);
|
||||||
const canSearch = isMainThread || isDiscussionThread;
|
const canSearch = isMainThread || isDiscussionThread;
|
||||||
|
const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot;
|
||||||
const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
|
const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
|
||||||
const canLeave = isMainThread && !canSubscribe;
|
const canLeave = isMainThread && !canSubscribe;
|
||||||
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && chat && chat.isCallActive;
|
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && chat.isCallActive;
|
||||||
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && chat && !chat.isCallActive
|
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && !chat.isCallActive
|
||||||
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
|
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -248,6 +268,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canRestartBot,
|
canRestartBot,
|
||||||
canSubscribe,
|
canSubscribe,
|
||||||
canSearch,
|
canSearch,
|
||||||
|
canCall,
|
||||||
canMute,
|
canMute,
|
||||||
canLeave,
|
canLeave,
|
||||||
canEnterVoiceChat,
|
canEnterVoiceChat,
|
||||||
@ -255,6 +276,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
'joinChannel', 'sendBotCommand', 'openLocalTextSearch', 'restartBot',
|
'joinChannel', 'sendBotCommand', 'openLocalTextSearch', 'restartBot', 'openCallFallbackConfirm',
|
||||||
]),
|
]),
|
||||||
)(HeaderActions));
|
)(HeaderActions));
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import './HeaderMenuContainer.scss';
|
|||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'updateChatMutedState' | 'enterMessageSelectMode' | 'sendBotCommand' | 'restartBot' | 'openLinkedChat' |
|
'updateChatMutedState' | 'enterMessageSelectMode' | 'sendBotCommand' | 'restartBot' | 'openLinkedChat' |
|
||||||
'joinGroupCall' | 'createGroupCall' | 'addContact'
|
'joinGroupCall' | 'createGroupCall' | 'addContact' | 'openCallFallbackConfirm'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
@ -42,6 +42,7 @@ export type OwnProps = {
|
|||||||
canRestartBot?: boolean;
|
canRestartBot?: boolean;
|
||||||
canSubscribe?: boolean;
|
canSubscribe?: boolean;
|
||||||
canSearch?: boolean;
|
canSearch?: boolean;
|
||||||
|
canCall?: boolean;
|
||||||
canMute?: boolean;
|
canMute?: boolean;
|
||||||
canLeave?: boolean;
|
canLeave?: boolean;
|
||||||
canEnterVoiceChat?: boolean;
|
canEnterVoiceChat?: boolean;
|
||||||
@ -71,6 +72,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
canRestartBot,
|
canRestartBot,
|
||||||
canSubscribe,
|
canSubscribe,
|
||||||
canSearch,
|
canSearch,
|
||||||
|
canCall,
|
||||||
canMute,
|
canMute,
|
||||||
canLeave,
|
canLeave,
|
||||||
canEnterVoiceChat,
|
canEnterVoiceChat,
|
||||||
@ -93,6 +95,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
createGroupCall,
|
createGroupCall,
|
||||||
openLinkedChat,
|
openLinkedChat,
|
||||||
addContact,
|
addContact,
|
||||||
|
openCallFallbackConfirm,
|
||||||
}) => {
|
}) => {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
@ -157,6 +160,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}, [closeMenu, onSubscribeChannel]);
|
}, [closeMenu, onSubscribeChannel]);
|
||||||
|
|
||||||
|
const handleCall = useCallback(() => {
|
||||||
|
openCallFallbackConfirm();
|
||||||
|
closeMenu();
|
||||||
|
}, [closeMenu, openCallFallbackConfirm]);
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
const handleSearch = useCallback(() => {
|
||||||
onSearchClick();
|
onSearchClick();
|
||||||
closeMenu();
|
closeMenu();
|
||||||
@ -216,6 +224,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{lang('AddContact')}
|
{lang('AddContact')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{IS_SINGLE_COLUMN_LAYOUT && canCall && (
|
||||||
|
<MenuItem
|
||||||
|
icon="phone"
|
||||||
|
onClick={handleCall}
|
||||||
|
>
|
||||||
|
{lang('Call')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{IS_SINGLE_COLUMN_LAYOUT && canSearch && (
|
{IS_SINGLE_COLUMN_LAYOUT && canSearch && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="search"
|
icon="search"
|
||||||
@ -306,5 +322,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
'createGroupCall',
|
'createGroupCall',
|
||||||
'openLinkedChat',
|
'openLinkedChat',
|
||||||
'addContact',
|
'addContact',
|
||||||
|
'openCallFallbackConfirm',
|
||||||
]),
|
]),
|
||||||
)(HeaderMenuContainer));
|
)(HeaderMenuContainer));
|
||||||
|
|||||||
@ -49,10 +49,6 @@ export default function useProfileViewportIds(
|
|||||||
resultType, loadMoreMembers, lastSyncTime, memberIds,
|
resultType, loadMoreMembers, lastSyncTime, memberIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems(
|
|
||||||
resultType, loadCommonChats, lastSyncTime, chatIds,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia(
|
const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia(
|
||||||
'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
|
'media', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
|
||||||
);
|
);
|
||||||
@ -73,6 +69,10 @@ export default function useProfileViewportIds(
|
|||||||
'voice', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
|
'voice', resultType, searchMessages, lastSyncTime, chatMessages, foundIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems(
|
||||||
|
resultType, loadCommonChats, lastSyncTime, chatIds,
|
||||||
|
);
|
||||||
|
|
||||||
let viewportIds: number[] | string[] | undefined;
|
let viewportIds: number[] | string[] | undefined;
|
||||||
let getMore: AnyToVoidFunction | undefined;
|
let getMore: AnyToVoidFunction | undefined;
|
||||||
let noProfileInfo = false;
|
let noProfileInfo = false;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ type OwnProps = {
|
|||||||
confirmHandler: () => void;
|
confirmHandler: () => void;
|
||||||
confirmIsDestructive?: boolean;
|
confirmIsDestructive?: boolean;
|
||||||
isButtonsInOneRow?: boolean;
|
isButtonsInOneRow?: boolean;
|
||||||
|
children?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConfirmDialog: FC<OwnProps> = ({
|
const ConfirmDialog: FC<OwnProps> = ({
|
||||||
@ -32,6 +33,7 @@ const ConfirmDialog: FC<OwnProps> = ({
|
|||||||
confirmHandler,
|
confirmHandler,
|
||||||
confirmIsDestructive,
|
confirmIsDestructive,
|
||||||
isButtonsInOneRow,
|
isButtonsInOneRow,
|
||||||
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
@ -48,7 +50,7 @@ const ConfirmDialog: FC<OwnProps> = ({
|
|||||||
{text && text.split('\\n').map((textPart) => (
|
{text && text.split('\\n').map((textPart) => (
|
||||||
<p>{textPart}</p>
|
<p>{textPart}</p>
|
||||||
))}
|
))}
|
||||||
{textParts}
|
{textParts || children}
|
||||||
<div className={isButtonsInOneRow ? 'dialog-buttons mt-2' : ''}>
|
<div className={isButtonsInOneRow ? 'dialog-buttons mt-2' : ''}>
|
||||||
<Button
|
<Button
|
||||||
className="confirm-dialog-button"
|
className="confirm-dialog-button"
|
||||||
|
|||||||
@ -222,6 +222,7 @@ function updateCache() {
|
|||||||
'shouldShowContextMenuHint',
|
'shouldShowContextMenuHint',
|
||||||
'leftColumnWidth',
|
'leftColumnWidth',
|
||||||
'serviceNotifications',
|
'serviceNotifications',
|
||||||
|
// TODO Support 'groupCalls'
|
||||||
]),
|
]),
|
||||||
audioPlayer: {
|
audioPlayer: {
|
||||||
volume: global.audioPlayer.volume,
|
volume: global.audioPlayer.volume,
|
||||||
@ -237,6 +238,7 @@ function updateCache() {
|
|||||||
},
|
},
|
||||||
settings: reduceSettings(global),
|
settings: reduceSettings(global),
|
||||||
chatFolders: reduceChatFolders(global),
|
chatFolders: reduceChatFolders(global),
|
||||||
|
groupCalls: reduceGroupCalls(global),
|
||||||
};
|
};
|
||||||
|
|
||||||
const json = JSON.stringify(reducedGlobal);
|
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() {
|
function setupHeavyAnimationListeners() {
|
||||||
document.addEventListener(ANIMATION_START_EVENT, () => {
|
document.addEventListener(ANIMATION_START_EVENT, () => {
|
||||||
isHeavyAnimating = true;
|
isHeavyAnimating = true;
|
||||||
|
|||||||
@ -48,7 +48,10 @@ import {
|
|||||||
AudioOrigin,
|
AudioOrigin,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export type MessageListType = 'thread' | 'pinned' | 'scheduled';
|
export type MessageListType =
|
||||||
|
'thread'
|
||||||
|
| 'pinned'
|
||||||
|
| 'scheduled';
|
||||||
|
|
||||||
export interface MessageList {
|
export interface MessageList {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -168,6 +171,9 @@ export type GlobalState = {
|
|||||||
byId: Record<string, ApiGroupCall>;
|
byId: Record<string, ApiGroupCall>;
|
||||||
activeGroupCallId?: string;
|
activeGroupCallId?: string;
|
||||||
isGroupCallPanelHidden?: boolean;
|
isGroupCallPanelHidden?: boolean;
|
||||||
|
isFallbackConfirmOpen?: boolean;
|
||||||
|
fallbackChatId?: string;
|
||||||
|
fallbackUserIdsToRemove?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
scheduledMessages: {
|
scheduledMessages: {
|
||||||
@ -551,7 +557,8 @@ export type ActionTypes = (
|
|||||||
'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' | 'playGroupCallSound' |
|
||||||
|
'openCallFallbackConfirm' | 'closeCallFallbackConfirm' | 'inviteToCallFallback'
|
||||||
);
|
);
|
||||||
|
|
||||||
export type GlobalActions = Record<ActionTypes, (...args: any[]) => void>;
|
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;
|
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
|
// 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 };
|
export function useRef<T>(initial: T): { current: T };
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
|
||||||
import {
|
import {
|
||||||
joinGroupCall,
|
joinGroupCall,
|
||||||
startSharingScreen,
|
startSharingScreen,
|
||||||
@ -7,11 +8,15 @@ import {
|
|||||||
setVolume,
|
setVolume,
|
||||||
handleUpdateGroupCallParticipants, handleUpdateGroupCallConnection,
|
handleUpdateGroupCallParticipants, handleUpdateGroupCallConnection,
|
||||||
} from '../../../lib/secret-sauce';
|
} 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 { callApi } from '../../../api/gramjs';
|
||||||
import { selectChat, selectUser } from '../../selectors';
|
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
|
||||||
import {
|
import {
|
||||||
selectActiveGroupCall,
|
selectActiveGroupCall,
|
||||||
|
selectCallFallbackChannelTitle,
|
||||||
selectGroupCallParticipant,
|
selectGroupCallParticipant,
|
||||||
} from '../../selectors/calls';
|
} from '../../selectors/calls';
|
||||||
import {
|
import {
|
||||||
@ -20,12 +25,16 @@ import {
|
|||||||
updateGroupCall,
|
updateGroupCall,
|
||||||
updateGroupCallParticipant,
|
updateGroupCallParticipant,
|
||||||
} from '../../reducers/calls';
|
} from '../../reducers/calls';
|
||||||
import { ApiUpdate } from '../../../api/types';
|
|
||||||
import { GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
|
||||||
import { omit } from '../../../util/iteratees';
|
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
|
||||||
|
|
||||||
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
||||||
const { activeGroupCallId } = global.groupCalls;
|
const { activeGroupCallId } = global.groupCalls;
|
||||||
|
|
||||||
@ -88,16 +97,25 @@ addReducer('leaveGroupCall', (global, actions, payload) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
global = updateActiveGroupCall(global, {
|
setGlobal(updateActiveGroupCall(global, { connectionState: 'disconnected' }, groupCall.participantsCount - 1));
|
||||||
connectionState: 'disconnected',
|
|
||||||
}, groupCall.participantsCount - 1);
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await callApi('leaveGroupCall', {
|
await callApi('leaveGroupCall', {
|
||||||
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,
|
||||||
});
|
});
|
||||||
@ -116,6 +134,10 @@ addReducer('leaveGroupCall', (global, actions, payload) => {
|
|||||||
...global.groupCalls,
|
...global.groupCalls,
|
||||||
isGroupCallPanelHidden: true,
|
isGroupCallPanelHidden: true,
|
||||||
activeGroupCallId: undefined,
|
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!);
|
const existingGroupCall = selectGroupCall(global, groupCall.id!);
|
||||||
|
|
||||||
global = updateGroupCall(global,
|
global = updateGroupCall(
|
||||||
|
global,
|
||||||
groupCall.id!,
|
groupCall.id!,
|
||||||
omit(result.groupCall, ['connectionState']),
|
omit(result.groupCall, ['connectionState']),
|
||||||
undefined,
|
undefined,
|
||||||
existingGroupCall?.isLoaded ? undefined : result.groupCall.participantsCount);
|
existingGroupCall?.isLoaded ? undefined : result.groupCall.participantsCount,
|
||||||
|
);
|
||||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||||
|
|
||||||
@ -309,3 +311,23 @@ export function removeGroupCallAudioElement() {
|
|||||||
audioContext = undefined;
|
audioContext = undefined;
|
||||||
audioElement = 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 { GlobalState } from '../../global/types';
|
||||||
import { selectChat } from './chats';
|
import { selectChat } from './chats';
|
||||||
import { isChatBasicGroup } from '../helpers';
|
import { getUserFullName, isChatBasicGroup } from '../helpers';
|
||||||
|
import { selectUser } from './users';
|
||||||
|
|
||||||
export function selectChatGroupCall(global: GlobalState, chatId: string) {
|
export function selectChatGroupCall(global: GlobalState, chatId: string) {
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
@ -36,3 +37,9 @@ export function selectActiveGroupCall(global: GlobalState) {
|
|||||||
|
|
||||||
return selectGroupCall(global, activeGroupCallId);
|
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