diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 5932da779..f5aa4148e 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -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 { + title: string; about?: string; users?: ApiUser[]; +}, noErrorUpdate = false): Promise { 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) { diff --git a/src/api/gramjs/methods/management.ts b/src/api/gramjs/methods/management.ts index af93f0ebf..73c98a485 100644 --- a/src/api/gramjs/methods/management.ts +++ b/src/api/gramjs/methods/management.ts @@ -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; } diff --git a/src/assets/call-fallback-avatar.png b/src/assets/call-fallback-avatar.png new file mode 100644 index 000000000..3a850f2ee Binary files /dev/null and b/src/assets/call-fallback-avatar.png differ diff --git a/src/bundles/calls.ts b/src/bundles/calls.ts index cc2f86d7f..a33bd663f 100644 --- a/src/bundles/calls.ts +++ b/src/bundles/calls.ts @@ -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'; diff --git a/src/components/calls/CallFallbackConfirm.async.tsx b/src/components/calls/CallFallbackConfirm.async.tsx new file mode 100644 index 000000000..c106cd23f --- /dev/null +++ b/src/components/calls/CallFallbackConfirm.async.tsx @@ -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 = ({ isOpen }) => { + const CallFallbackConfirm = useModuleLoader(Bundles.Calls, 'CallFallbackConfirm', !isOpen); + + return CallFallbackConfirm ? : undefined; +}; + +export default memo(CallFallbackConfirmAsync); diff --git a/src/components/calls/CallFallbackConfirm.tsx b/src/components/calls/CallFallbackConfirm.tsx new file mode 100644 index 000000000..18e2c5f9a --- /dev/null +++ b/src/components/calls/CallFallbackConfirm.tsx @@ -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; + +const CallFallbackConfirm: FC = ({ + isOpen, + channelTitle, + userFullName, + closeCallFallbackConfirm, + inviteToCallFallback, +}) => { + const [shouldRemove, setShouldRemove] = useState(true); + const renderingUserFullName = useCurrentOrPrev(userFullName, true); + + return ( + { + inviteToCallFallback({ shouldRemove }); + }} + onClose={closeCallFallbackConfirm} + > +

The call will be started in a private channel {channelTitle}.

+ +
+ ); +}; + +export default memo(withGlobal( + (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)); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 821b70e25..9be9e1e9f 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -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 = ({ animationLevel, language, wasTimeFormatSetManually, + isCallFallbackConfirmOpen, loadAnimatedEmojis, loadNotificationSettings, loadNotificationExceptions, @@ -282,6 +285,7 @@ const Main: FC = ({ )} + ); }; @@ -333,6 +337,7 @@ export default memo(withGlobal( animationLevel, language, wasTimeFormatSetManually, + isCallFallbackConfirmOpen: Boolean(global.groupCalls.isFallbackConfirmOpen), }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx index 3e98b28d3..6acf904b5 100644 --- a/src/components/middle/HeaderActions.tsx +++ b/src/components/middle/HeaderActions.tsx @@ -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; +type DispatchProps = Pick; // Chrome breaks layout when focusing input during transition const SEARCH_FOCUS_DELAY_MS = 400; @@ -63,6 +68,7 @@ const HeaderActions: FC = ({ canRestartBot, canSubscribe, canSearch, + canCall, canMute, canLeave, canEnterVoiceChat, @@ -73,6 +79,7 @@ const HeaderActions: FC = ({ sendBotCommand, openLocalTextSearch, restartBot, + openCallFallbackConfirm, }) => { // eslint-disable-next-line no-null/no-null const menuButtonRef = useRef(null); @@ -170,6 +177,17 @@ const HeaderActions: FC = ({ )} + {canCall && ( + + )} )}