Support stopping and restarting bots (#1316)
This commit is contained in:
parent
7bd89f47d8
commit
85ed696721
@ -3,7 +3,7 @@ import { ApiUser, ApiUserStatus, ApiUserType } from '../../types';
|
||||
|
||||
export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser {
|
||||
const {
|
||||
about, commonChatsCount, pinnedMsgId, botInfo,
|
||||
about, commonChatsCount, pinnedMsgId, botInfo, blocked,
|
||||
} = mtpUserFull;
|
||||
|
||||
return {
|
||||
@ -12,6 +12,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.UserFull): ApiUser {
|
||||
bio: about,
|
||||
commonChatsCount,
|
||||
pinnedMessageId: pinnedMsgId,
|
||||
isBlocked: Boolean(blocked),
|
||||
...(botInfo && { botDescription: botInfo.description }),
|
||||
},
|
||||
};
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import { buildApiChatFromPreview, getApiChatIdFromMtpPeer } from '../apiBuilders/chats';
|
||||
import { buildInputPrivacyKey, buildInputPeer, buildPeer } from '../gramjsBuilders';
|
||||
import { buildInputPrivacyKey, buildInputPeer } from '../gramjsBuilders';
|
||||
import { invokeRequest, uploadFile, getClient } from './client';
|
||||
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
@ -129,9 +129,9 @@ export async function fetchBlockedContacts() {
|
||||
};
|
||||
}
|
||||
|
||||
export function blockContact(chatOrUserId: number) {
|
||||
export function blockContact(chatOrUserId: number, accessHash?: string) {
|
||||
return invokeRequest(new GramJs.contacts.Block({
|
||||
id: buildPeer(chatOrUserId),
|
||||
id: buildInputPeer(chatOrUserId, accessHash),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ export interface ApiUser {
|
||||
}
|
||||
|
||||
export interface ApiUserFullInfo {
|
||||
isBlocked?: boolean;
|
||||
bio?: string;
|
||||
commonChatsCount?: number;
|
||||
botDescription?: string;
|
||||
|
||||
@ -7,6 +7,7 @@ import { GlobalActions } from '../../global/types';
|
||||
import { selectIsChatWithSelf, selectUser } from '../../modules/selectors';
|
||||
import {
|
||||
isChatPrivate,
|
||||
isUserBot,
|
||||
getUserFirstOrLastName,
|
||||
getPrivateChatUserId,
|
||||
isChatBasicGroup,
|
||||
@ -34,6 +35,7 @@ export type OwnProps = {
|
||||
type StateProps = {
|
||||
isChannel: boolean;
|
||||
isChatWithSelf?: boolean;
|
||||
isBot?: boolean;
|
||||
isPrivateChat: boolean;
|
||||
isBasicGroup: boolean;
|
||||
isSuperGroup: boolean;
|
||||
@ -42,7 +44,9 @@ type StateProps = {
|
||||
contactName?: string;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'leaveChannel' | 'deleteHistory' | 'deleteChannel' | 'deleteChatUser'>;
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'leaveChannel' | 'deleteHistory' | 'deleteChannel' | 'deleteChatUser' | 'blockContact'
|
||||
)>;
|
||||
|
||||
const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isOpen,
|
||||
@ -50,6 +54,7 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isChannel,
|
||||
isPrivateChat,
|
||||
isChatWithSelf,
|
||||
isBot,
|
||||
isBasicGroup,
|
||||
isSuperGroup,
|
||||
currentUserId,
|
||||
@ -61,6 +66,7 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
deleteHistory,
|
||||
deleteChannel,
|
||||
deleteChatUser,
|
||||
blockContact,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const chatTitle = getChatTitle(lang, chat);
|
||||
@ -71,6 +77,13 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
onClose();
|
||||
}, [deleteHistory, chat.id, onClose]);
|
||||
|
||||
const handleDeleteAndStop = useCallback(() => {
|
||||
deleteHistory({ chatId: chat.id, shouldDeleteForAll: true });
|
||||
blockContact({ contactId: chat.id, accessHash: chat.accessHash });
|
||||
|
||||
onClose();
|
||||
}, [deleteHistory, chat.id, chat.accessHash, blockContact, onClose]);
|
||||
|
||||
const handleDeleteChat = useCallback(() => {
|
||||
if (isPrivateChat) {
|
||||
deleteHistory({ chatId: chat.id, shouldDeleteForAll: false });
|
||||
@ -163,6 +176,11 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
>
|
||||
{renderMessage()}
|
||||
{isBot && (
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteAndStop}>
|
||||
{lang('DeleteAndStop')}
|
||||
</Button>
|
||||
)}
|
||||
{canDeleteForAll && (
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||
{contactName ? renderText(lang('ChatList.DeleteForEveryone', contactName)) : lang('DeleteForAll')}
|
||||
@ -180,14 +198,17 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { chat }): StateProps => {
|
||||
const isPrivateChat = isChatPrivate(chat.id);
|
||||
const isChatWithSelf = selectIsChatWithSelf(global, chat.id);
|
||||
const canDeleteForAll = (isPrivateChat && !isChatWithSelf);
|
||||
const contactName = chat && isChatPrivate(chat.id)
|
||||
const user = isPrivateChat && selectUser(global, getPrivateChatUserId(chat)!);
|
||||
const isBot = user && isUserBot(user) && !chat.isSupport;
|
||||
const canDeleteForAll = (isPrivateChat && !isChatWithSelf && !isBot);
|
||||
const contactName = isPrivateChat
|
||||
? getUserFirstOrLastName(selectUser(global, getPrivateChatUserId(chat)!))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
isPrivateChat,
|
||||
isChatWithSelf,
|
||||
isBot,
|
||||
isChannel: isChatChannel(chat),
|
||||
isBasicGroup: isChatBasicGroup(chat),
|
||||
isSuperGroup: isChatSuperGroup(chat),
|
||||
@ -197,5 +218,5 @@ export default memo(withGlobal<OwnProps>(
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions,
|
||||
['leaveChannel', 'deleteHistory', 'deleteChannel', 'deleteChatUser']),
|
||||
['leaveChannel', 'deleteHistory', 'deleteChannel', 'deleteChatUser', 'blockContact']),
|
||||
)(DeleteChatModal));
|
||||
|
||||
@ -16,7 +16,10 @@ import { pick } from '../../util/iteratees';
|
||||
import { isChatChannel, isChatSuperGroup } from '../../modules/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectIsChatBotNotStarted, selectIsChatWithSelf,
|
||||
selectChatBot,
|
||||
selectIsUserBlocked,
|
||||
selectIsChatBotNotStarted,
|
||||
selectIsChatWithSelf,
|
||||
selectIsInSelectMode,
|
||||
selectIsRightColumnShown,
|
||||
} from '../../modules/selectors';
|
||||
@ -36,6 +39,7 @@ interface StateProps {
|
||||
isChannel?: boolean;
|
||||
isRightColumnShown?: boolean;
|
||||
canStartBot?: boolean;
|
||||
canRestartBot?: boolean;
|
||||
canSubscribe?: boolean;
|
||||
canSearch?: boolean;
|
||||
canMute?: boolean;
|
||||
@ -43,7 +47,7 @@ interface StateProps {
|
||||
canLeave?: boolean;
|
||||
}
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'joinChannel' | 'sendBotCommand' | 'openLocalTextSearch'>;
|
||||
type DispatchProps = Pick<GlobalActions, 'joinChannel' | 'sendBotCommand' | 'openLocalTextSearch' | 'restartBot'>;
|
||||
|
||||
// Chrome breaks layout when focusing input during transition
|
||||
const SEARCH_FOCUS_DELAY_MS = 400;
|
||||
@ -54,6 +58,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
noMenu,
|
||||
isChannel,
|
||||
canStartBot,
|
||||
canRestartBot,
|
||||
canSubscribe,
|
||||
canSearch,
|
||||
canMute,
|
||||
@ -63,6 +68,7 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
joinChannel,
|
||||
sendBotCommand,
|
||||
openLocalTextSearch,
|
||||
restartBot,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
@ -91,6 +97,10 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
sendBotCommand({ command: '/start' });
|
||||
}, [sendBotCommand]);
|
||||
|
||||
const handleRestartBot = useCallback(() => {
|
||||
restartBot({ chatId });
|
||||
}, [chatId, restartBot]);
|
||||
|
||||
const handleSearchClick = useCallback(() => {
|
||||
openLocalTextSearch();
|
||||
|
||||
@ -132,6 +142,16 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
{lang('BotStart')}
|
||||
</Button>
|
||||
)}
|
||||
{!IS_SINGLE_COLUMN_LAYOUT && canRestartBot && (
|
||||
<Button
|
||||
size="tiny"
|
||||
ripple
|
||||
fluid
|
||||
onClick={handleRestartBot}
|
||||
>
|
||||
{lang('BotRestart')}
|
||||
</Button>
|
||||
)}
|
||||
{!IS_SINGLE_COLUMN_LAYOUT && canSearch && (
|
||||
<Button
|
||||
round
|
||||
@ -166,6 +186,8 @@ const HeaderActions: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isOpen={isMenuOpen}
|
||||
anchor={menuPosition}
|
||||
isChannel={isChannel}
|
||||
canStartBot={canStartBot}
|
||||
canRestartBot={canRestartBot}
|
||||
canSubscribe={canSubscribe}
|
||||
canSearch={canSearch}
|
||||
canMute={canMute}
|
||||
@ -192,12 +214,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
};
|
||||
}
|
||||
|
||||
const bot = selectChatBot(global, chatId);
|
||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
|
||||
const isDiscussionThread = messageListType === 'thread' && threadId !== MAIN_THREAD_ID;
|
||||
const isRightColumnShown = selectIsRightColumnShown(global);
|
||||
|
||||
const canStartBot = Boolean(selectIsChatBotNotStarted(global, chatId));
|
||||
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,
|
||||
);
|
||||
@ -219,6 +243,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isChannel,
|
||||
isRightColumnShown,
|
||||
canStartBot,
|
||||
canRestartBot,
|
||||
canSubscribe,
|
||||
canSearch,
|
||||
canMute,
|
||||
@ -227,6 +252,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
'joinChannel', 'sendBotCommand', 'openLocalTextSearch',
|
||||
'joinChannel', 'sendBotCommand', 'openLocalTextSearch', 'restartBot',
|
||||
]),
|
||||
)(HeaderActions));
|
||||
|
||||
@ -22,7 +22,9 @@ import DeleteChatModal from '../common/DeleteChatModal';
|
||||
|
||||
import './HeaderMenuContainer.scss';
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'updateChatMutedState' | 'enterMessageSelectMode'>;
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'updateChatMutedState' | 'enterMessageSelectMode' | 'sendBotCommand' | 'restartBot'
|
||||
)>;
|
||||
|
||||
export type OwnProps = {
|
||||
chatId: number;
|
||||
@ -30,6 +32,8 @@ export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
anchor: IAnchorPosition;
|
||||
isChannel?: boolean;
|
||||
canStartBot?: boolean;
|
||||
canRestartBot?: boolean;
|
||||
canSubscribe?: boolean;
|
||||
canSearch?: boolean;
|
||||
canMute?: boolean;
|
||||
@ -53,6 +57,8 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isOpen,
|
||||
anchor,
|
||||
isChannel,
|
||||
canStartBot,
|
||||
canRestartBot,
|
||||
canSubscribe,
|
||||
canSearch,
|
||||
canMute,
|
||||
@ -68,6 +74,8 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
onCloseAnimationEnd,
|
||||
updateChatMutedState,
|
||||
enterMessageSelectMode,
|
||||
sendBotCommand,
|
||||
restartBot,
|
||||
}) => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@ -90,6 +98,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const handleStartBot = useCallback(() => {
|
||||
sendBotCommand({ command: '/start' });
|
||||
}, [sendBotCommand]);
|
||||
|
||||
const handleRestartBot = useCallback(() => {
|
||||
restartBot({ chatId });
|
||||
}, [chatId, restartBot]);
|
||||
|
||||
const handleToggleMuteClick = useCallback(() => {
|
||||
updateChatMutedState({ chatId, isMuted: !isMuted });
|
||||
closeMenu();
|
||||
@ -127,6 +143,22 @@ const HeaderMenuContainer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
style={`left: ${x}px;top: ${y}px;`}
|
||||
onClose={closeMenu}
|
||||
>
|
||||
{IS_SINGLE_COLUMN_LAYOUT && canStartBot && (
|
||||
<MenuItem
|
||||
icon="bots"
|
||||
onClick={handleStartBot}
|
||||
>
|
||||
{lang('BotStart')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{IS_SINGLE_COLUMN_LAYOUT && canRestartBot && (
|
||||
<MenuItem
|
||||
icon="bots"
|
||||
onClick={handleRestartBot}
|
||||
>
|
||||
{lang('BotRestart')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{IS_SINGLE_COLUMN_LAYOUT && canSubscribe && (
|
||||
<MenuItem
|
||||
icon={isChannel ? 'channel' : 'group'}
|
||||
@ -200,5 +232,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
'updateChatMutedState',
|
||||
'enterMessageSelectMode',
|
||||
'sendBotCommand',
|
||||
'restartBot',
|
||||
]),
|
||||
)(HeaderMenuContainer));
|
||||
|
||||
@ -446,7 +446,7 @@ export default memo(withGlobal(
|
||||
messageListType,
|
||||
originChatId: originChat ? originChat.id : chatId,
|
||||
isPrivate: isChatPrivate(chatId),
|
||||
canPost: !isPinnedMessageList && (!chat || canPost) && (!isBotNotStarted || IS_SINGLE_COLUMN_LAYOUT),
|
||||
canPost: !isPinnedMessageList && (!chat || canPost) && !isBotNotStarted,
|
||||
isPinnedMessageList,
|
||||
isScheduledMessageList,
|
||||
currentUserBannedRights: chat && chat.currentUserBannedRights,
|
||||
|
||||
@ -487,7 +487,7 @@ export type ActionTypes = (
|
||||
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' |
|
||||
// bots
|
||||
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
||||
'resetInlineBot' |
|
||||
'resetInlineBot' | 'restartBot' |
|
||||
// misc
|
||||
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'closeAudioPlayer' | 'openPollModal' | 'closePollModal' |
|
||||
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' |
|
||||
|
||||
@ -8,9 +8,10 @@ import { InlineBotSettings } from '../../../types';
|
||||
import { RE_TME_INVITE_LINK, RE_TME_LINK } from '../../../config';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
selectChat, selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectReplyingToId, selectUser,
|
||||
selectChat, selectChatBot, selectChatMessage, selectCurrentChat, selectCurrentMessageList,
|
||||
selectReplyingToId, selectUser,
|
||||
} from '../../selectors';
|
||||
import { addChats, addUsers } from '../../reducers';
|
||||
import { addChats, addUsers, removeBlockedContact } from '../../reducers';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { debounce } from '../../../util/schedulers';
|
||||
import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots';
|
||||
@ -74,6 +75,26 @@ addReducer('sendBotCommand', (global, actions, payload) => {
|
||||
void sendBotCommand(chat, currentUserId, command);
|
||||
});
|
||||
|
||||
addReducer('restartBot', (global, actions, payload) => {
|
||||
const { chatId } = payload;
|
||||
const { currentUserId } = global;
|
||||
const chat = selectCurrentChat(global);
|
||||
const bot = currentUserId && selectChatBot(global, chatId);
|
||||
if (!currentUserId || !chat || !bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('unblockContact', bot.id, bot.accessHash);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobal(removeBlockedContact(getGlobal(), bot.id));
|
||||
void sendBotCommand(chat, currentUserId, '/start');
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('loadTopInlineBots', (global) => {
|
||||
const { serverTimeOffset } = global;
|
||||
const { hash, lastRequestedAt } = global.topInlineBots;
|
||||
|
||||
@ -217,10 +217,10 @@ addReducer('loadBlockedContacts', () => {
|
||||
});
|
||||
|
||||
addReducer('blockContact', (global, actions, payload) => {
|
||||
const { contactId } = payload!;
|
||||
const { contactId, accessHash } = payload!;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('blockContact', contactId);
|
||||
const result = await callApi('blockContact', contactId, accessHash);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
ISettings, IThemeSettings, ThemeKey, NotifyException,
|
||||
} from '../../types';
|
||||
import { ApiNotifyException } from '../../api/types';
|
||||
import { updateUserBlockedState } from './users';
|
||||
|
||||
export function replaceSettings(global: GlobalState, newSettings?: Partial<ISettings>): GlobalState {
|
||||
return {
|
||||
@ -87,6 +88,8 @@ export function updateNotifySettings(
|
||||
}
|
||||
|
||||
export function addBlockedContact(global: GlobalState, contactId: number): GlobalState {
|
||||
global = updateUserBlockedState(global, contactId, true);
|
||||
|
||||
return {
|
||||
...global,
|
||||
blocked: {
|
||||
@ -98,6 +101,8 @@ export function addBlockedContact(global: GlobalState, contactId: number): Globa
|
||||
}
|
||||
|
||||
export function removeBlockedContact(global: GlobalState, contactId: number): GlobalState {
|
||||
global = updateUserBlockedState(global, contactId, false);
|
||||
|
||||
return {
|
||||
...global,
|
||||
blocked: {
|
||||
|
||||
@ -169,3 +169,19 @@ export function updateUserSearchFetchingStatus(
|
||||
fetchingStatus: newState,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateUserBlockedState(global: GlobalState, userId: number, isBlocked: boolean) {
|
||||
const { byId } = global.users;
|
||||
const user = byId[userId];
|
||||
if (!user || !user.fullInfo) {
|
||||
return global;
|
||||
}
|
||||
|
||||
return updateUser(global, userId, {
|
||||
...user,
|
||||
fullInfo: {
|
||||
...user.fullInfo,
|
||||
isBlocked,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -5,6 +5,12 @@ export function selectUser(global: GlobalState, userId: number): ApiUser | undef
|
||||
return global.users.byId[userId];
|
||||
}
|
||||
|
||||
export function selectIsUserBlocked(global: GlobalState, userId: number) {
|
||||
const user = selectUser(global, userId);
|
||||
|
||||
return user && user.fullInfo && user.fullInfo.isBlocked;
|
||||
}
|
||||
|
||||
// Slow, not to be used in `withGlobal`
|
||||
export function selectUserByUsername(global: GlobalState, username: string) {
|
||||
const usernameLowered = username.toLowerCase();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user