/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "window/window_peer_menu.h" #include "lang/lang_keys.h" #include "boxes/confirm_box.h" #include "boxes/mute_settings_box.h" #include "boxes/add_contact_box.h" #include "boxes/report_box.h" #include "boxes/create_poll_box.h" #include "boxes/peers/add_participants_box.h" #include "ui/toast/toast.h" #include "auth_session.h" #include "apiwrap.h" #include "mainwidget.h" #include "mainwindow.h" #include "observer_peer.h" #include "history/history.h" #include "window/window_controller.h" #include "support/support_helper.h" #include "info/info_memento.h" #include "info/info_controller.h" //#include "info/feed/info_feed_channels_controllers.h" // #feed #include "info/profile/info_profile_values.h" #include "data/data_session.h" #include "data/data_folder.h" #include "data/data_poll.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_drafts.h" #include "data/data_user.h" #include "dialogs/dialogs_key.h" #include "boxes/peers/edit_peer_info_box.h" #include "styles/style_boxes.h" #include "styles/style_window.h" // st::windowMinWidth namespace Window { namespace { constexpr auto kArchivedToastDuration = crl::time(3000); class Filler { public: Filler( not_null controller, not_null peer, const PeerMenuCallback &addAction, PeerMenuSource source); void fill(); private: bool showInfo(); bool showToggleArchived(); bool showTogglePin(); void addTogglePin(); void addInfo(); //void addSearch(); void addToggleUnreadMark(); void addToggleArchive(); void addUserActions(not_null user); void addBlockUser(not_null user); void addChatActions(not_null chat); void addChannelActions(not_null channel); not_null _controller; not_null _peer; const PeerMenuCallback &_addAction; PeerMenuSource _source; }; class FolderFiller { public: FolderFiller( not_null controller, not_null folder, const PeerMenuCallback &addAction, PeerMenuSource source); void fill(); private: void addToggleCollapse(); //bool showInfo(); //void addTogglePin(); //void addInfo(); //void addSearch(); //void addNotifications(); //void addUngroup(); not_null _controller; not_null _folder; const PeerMenuCallback &_addAction; PeerMenuSource _source; }; History *FindWastedPin(Data::Folder *folder) { const auto &order = Auth().data().pinnedChatsOrder(folder); for (const auto &pinned : order) { if (const auto history = pinned.history()) { if (history->peer->isChat() && history->peer->asChat()->isDeactivated() && !history->inChatList()) { return history; } } } return nullptr; } void AddChatMembers(not_null chat) { AddParticipantsBoxController::Start(chat); } bool PinnedLimitReached(Dialogs::Key key) { Expects(key.entry()->folderKnown()); const auto folder = key.entry()->folder(); const auto pinnedCount = Auth().data().pinnedChatsCount(folder); const auto pinnedMax = Auth().data().pinnedChatsLimit(folder); if (pinnedCount < pinnedMax) { return false; } // Some old chat, that was converted, maybe is still pinned. if (const auto wasted = FindWastedPin(folder)) { Auth().data().setChatPinned(wasted, false); Auth().data().setChatPinned(key, true); Auth().api().savePinnedOrder(folder); } else { auto errorText = lng_error_pinned_max( lt_count, pinnedMax); Ui::show(Box(errorText)); } return true; } void TogglePinnedDialog(Dialogs::Key key) { if (!key.entry()->folderKnown()) { return; } const auto isPinned = !key.entry()->isPinnedDialog(); if (isPinned && PinnedLimitReached(key)) { return; } Auth().data().setChatPinned(key, isPinned); const auto flags = isPinned ? MTPmessages_ToggleDialogPin::Flag::f_pinned : MTPmessages_ToggleDialogPin::Flag(0); if (const auto history = key.history()) { history->session().api().request(MTPmessages_ToggleDialogPin( MTP_flags(flags), MTP_inputDialogPeer(key.history()->peer->input) )).done([=](const MTPBool &result) { Auth().data().notifyPinnedDialogsOrderUpdated(); }).send(); } else if (const auto folder = key.folder()) { folder->session().api().request(MTPmessages_ToggleDialogPin( MTP_flags(flags), MTP_inputDialogPeerFolder(MTP_int(folder->id())) )).send(); } if (isPinned) { if (const auto main = App::main()) { main->dialogsToUp(); } } } Filler::Filler( not_null controller, not_null peer, const PeerMenuCallback &addAction, PeerMenuSource source) : _controller(controller) , _peer(peer) , _addAction(addAction) , _source(source) { } bool Filler::showInfo() { if (_source == PeerMenuSource::Profile || _source == PeerMenuSource::ChatsList || _peer->isSelf()) { return false; } else if (_controller->activeChatCurrent().peer() != _peer) { return true; } else if (!Adaptive::ThreeColumn()) { return true; } else if ( !Auth().settings().thirdSectionInfoEnabled() && !Auth().settings().tabbedReplacedWithInfo()) { return true; } return false; } bool Filler::showToggleArchived() { if (_source != PeerMenuSource::ChatsList) { return false; } const auto history = _peer->owner().historyLoaded(_peer); if (history && history->useProxyPromotion()) { return false; } else if (!_peer->isNotificationsUser() && !_peer->isSelf()) { return true; } return history && (history->folder() != nullptr); } bool Filler::showTogglePin() { if (_source != PeerMenuSource::ChatsList) { return false; } const auto history = _peer->owner().historyLoaded(_peer); return history && !history->fixedOnTopIndex(); } void Filler::addTogglePin() { auto peer = _peer; auto isPinned = false; if (auto history = peer->owner().historyLoaded(peer)) { isPinned = history->isPinnedDialog(); } auto pinText = [](bool isPinned) { return lang(isPinned ? lng_context_unpin_from_top : lng_context_pin_to_top); }; auto pinToggle = [=] { TogglePinnedDialog(peer->owner().history(peer)); }; auto pinAction = _addAction(pinText(isPinned), pinToggle); const auto lifetime = Ui::CreateChild(pinAction); Notify::PeerUpdateViewer( peer, Notify::PeerUpdate::Flag::ChatPinnedChanged ) | rpl::start_with_next([peer, pinAction, pinText] { auto isPinned = peer->owner().history(peer)->isPinnedDialog(); pinAction->setText(pinText(isPinned)); }, *lifetime); } void Filler::addInfo() { auto controller = _controller; auto peer = _peer; auto infoKey = (peer->isChat() || peer->isMegagroup()) ? lng_context_view_group : (peer->isUser() ? lng_context_view_profile : lng_context_view_channel); _addAction(lang(infoKey), [=] { controller->showPeerInfo(peer); }); } //void Filler::addSearch() { // _addAction(lang(lng_profile_search_messages), [peer = _peer] { // App::main()->searchInChat(peer->owner().history(peer)); // }); //} void Filler::addToggleUnreadMark() { const auto peer = _peer; const auto isUnread = [](not_null peer) { if (const auto history = peer->owner().historyLoaded(peer)) { return (history->chatListUnreadCount() > 0) || (history->chatListUnreadMark()); } return false; }; const auto label = [=](not_null peer) { return lang(isUnread(peer) ? lng_context_mark_read : lng_context_mark_unread); }; auto action = _addAction(label(peer), [=] { const auto markAsRead = isUnread(peer); const auto handle = [&](not_null history) { if (markAsRead) { Auth().api().readServerHistory(history); } else { Auth().api().changeDialogUnreadMark(history, !markAsRead); } }; const auto history = peer->owner().history(peer); handle(history); if (markAsRead) { if (const auto migrated = history->migrateSibling()) { handle(migrated); } } }); const auto lifetime = Ui::CreateChild(action); Notify::PeerUpdateViewer( _peer, Notify::PeerUpdate::Flag::UnreadViewChanged ) | rpl::start_with_next([=] { action->setText(label(peer)); }, *lifetime); } void Filler::addToggleArchive() { const auto peer = _peer; const auto archived = [&] { const auto history = peer->owner().historyLoaded(peer); return history && history->folder(); }(); const auto toggle = [=] { ToggleHistoryArchived( peer->owner().history(peer), !archived); }; _addAction( lang(archived ? lng_archived_remove : lng_archived_add), toggle); } void Filler::addBlockUser(not_null user) { auto blockText = [](not_null user) { return lang(user->isBlocked() ? ((user->isBot() && !user->isSupport()) ? lng_profile_restart_bot : lng_profile_unblock_user) : ((user->isBot() && !user->isSupport()) ? lng_profile_block_bot : lng_profile_block_user)); }; auto blockAction = _addAction(blockText(user), [=] { if (user->isBlocked()) { Auth().api().unblockUser(user); } else { Auth().api().blockUser(user); } }); const auto lifetime = Ui::CreateChild(blockAction); Notify::PeerUpdateViewer( _peer, Notify::PeerUpdate::Flag::UserIsBlocked ) | rpl::start_with_next([=] { blockAction->setText(blockText(user)); }, *lifetime); if (user->blockStatus() == UserData::BlockStatus::Unknown) { Auth().api().requestFullPeer(user); } } void Filler::addUserActions(not_null user) { if (_source != PeerMenuSource::ChatsList) { if (Auth().supportMode()) { _addAction("Edit support info", [=] { Auth().supportHelper().editInfo(user); }); } if (user->isContact()) { if (!user->isSelf()) { _addAction( lang(lng_info_share_contact), [user] { PeerMenuShareContactBox(user); }); _addAction( lang(lng_info_edit_contact), [user] { Ui::show(Box(user)); }); _addAction( lang(lng_info_delete_contact), [user] { PeerMenuDeleteContact(user); }); } } else if (user->canShareThisContact()) { if (!user->isSelf()) { _addAction( lang(lng_info_add_as_contact), [user] { PeerMenuAddContact(user); }); } _addAction( lang(lng_info_share_contact), [user] { PeerMenuShareContactBox(user); }); } else if (user->botInfo && !user->botInfo->cantJoinGroups) { _addAction( lang(lng_profile_invite_to_group), [user] { AddBotToGroupBoxController::Start(user); }); } if (user->canExportChatHistory()) { _addAction( lang(lng_profile_export_chat), [=] { PeerMenuExportChat(user); }); } } _addAction( lang(lng_profile_delete_conversation), DeleteAndLeaveHandler(user)); _addAction( lang(lng_profile_clear_history), ClearHistoryHandler(user)); if (!user->isInaccessible() && user != Auth().user() && _source != PeerMenuSource::ChatsList) { addBlockUser(user); } } void Filler::addChatActions(not_null chat) { if (_source != PeerMenuSource::ChatsList) { if (EditPeerInfoBox::Available(chat)) { const auto text = lang(lng_manage_group_title); _addAction(text, [=] { App::wnd()->controller()->showEditPeerBox(chat); }); } if (chat->canAddMembers()) { _addAction( lang(lng_profile_add_participant), [chat] { AddChatMembers(chat); }); } if (chat->canSendPolls()) { _addAction( lang(lng_polls_create), [=] { PeerMenuCreatePoll(chat); }); } if (chat->canExportChatHistory()) { _addAction( lang(lng_profile_export_chat), [=] { PeerMenuExportChat(chat); }); } } _addAction( lang(lng_profile_clear_and_exit), DeleteAndLeaveHandler(_peer)); _addAction( lang(lng_profile_clear_history), ClearHistoryHandler(_peer)); } void Filler::addChannelActions(not_null channel) { auto isGroup = channel->isMegagroup(); //if (!isGroup) { // #feed // const auto feed = channel->feed(); // const auto grouped = (feed != nullptr); // if (!grouped || feed->channels().size() > 1) { // _addAction( // #feed // lang(grouped ? lng_feed_ungroup : lng_feed_group), // [=] { ToggleChannelGrouping(channel, !grouped); }); // } //} if (_source != PeerMenuSource::ChatsList) { if (EditPeerInfoBox::Available(channel)) { const auto text = lang(isGroup ? lng_manage_group_title : lng_manage_channel_title); _addAction(text, [channel] { App::wnd()->controller()->showEditPeerBox(channel); }); } if (channel->canAddMembers()) { _addAction( lang(lng_channel_add_members), [channel] { PeerMenuAddChannelMembers(channel); }); } if (channel->canSendPolls()) { _addAction( lang(lng_polls_create), [=] { PeerMenuCreatePoll(channel); }); } if (channel->canExportChatHistory()) { _addAction( lang(isGroup ? lng_profile_export_chat : lng_profile_export_channel), [=] { PeerMenuExportChat(channel); }); } } if (channel->amIn()) { if (isGroup && !channel->isPublic()) { _addAction( lang(lng_profile_clear_history), ClearHistoryHandler(channel)); } auto text = lang(isGroup ? lng_profile_leave_group : lng_profile_leave_channel); _addAction(text, DeleteAndLeaveHandler(channel)); } else { auto text = lang(isGroup ? lng_profile_join_group : lng_profile_join_channel); _addAction( text, [channel] { Auth().api().joinChannel(channel); }); } if (_source != PeerMenuSource::ChatsList) { auto needReport = !channel->amCreator() && (!isGroup || channel->isPublic()); if (needReport) { _addAction(lang(lng_profile_report), [channel] { Ui::show(Box(channel)); }); } } } void Filler::fill() { if (showToggleArchived()) { addToggleArchive(); } if (showTogglePin()) { addTogglePin(); } if (showInfo()) { addInfo(); } if (_source != PeerMenuSource::Profile && !_peer->isSelf()) { PeerMenuAddMuteAction(_peer, _addAction); } if (_source == PeerMenuSource::ChatsList) { //addSearch(); addToggleUnreadMark(); } if (const auto user = _peer->asUser()) { addUserActions(user); } else if (const auto chat = _peer->asChat()) { addChatActions(chat); } else if (const auto channel = _peer->asChannel()) { addChannelActions(channel); } } FolderFiller::FolderFiller( not_null controller, not_null folder, const PeerMenuCallback &addAction, PeerMenuSource source) : _controller(controller) , _folder(folder) , _addAction(addAction) , _source(source) { } void FolderFiller::fill() { if (_source == PeerMenuSource::ChatsList) { addToggleCollapse(); } } void FolderFiller::addToggleCollapse() { if (_folder->id() != Data::Folder::kId) { return; } const auto controller = _controller; const auto hidden = controller->session().settings().archiveCollapsed(); const auto text = lang(hidden ? lng_context_archive_expand : lng_context_archive_collapse); _addAction(text, [=] { controller->session().settings().setArchiveCollapsed(!hidden); controller->session().saveSettingsDelayed(); }); } // //void FolderFiller::addInfo() { // auto controller = _controller; // auto feed = _feed; // _addAction(lang(lng_context_view_feed_info), [=] { // controller->showSection(Info::Memento( // feed, // Info::Section(Info::Section::Type::Profile))); // }); //} // //void FolderFiller::addNotifications() { // const auto feed = _feed; // _addAction(lang(lng_feed_notifications), [=] { // Info::FeedProfile::NotificationsController::Start(feed); // }); //} // //void FolderFiller::addSearch() { // const auto feed = _feed; // _addAction(lang(lng_profile_search_messages), [=] { // App::main()->searchInChat(feed); // }); //} // //void FolderFiller::addUngroup() { // const auto feed = _feed; // //_addAction(lang(lng_feed_ungroup_all), [=] { // #feed // // PeerMenuUngroupFeed(feed); // //}); //} } // namespace void PeerMenuExportChat(not_null peer) { Auth().data().startExport(peer); } void PeerMenuDeleteContact(not_null user) { const auto text = lng_sure_delete_contact( lt_contact, App::peerName(user)); const auto deleteSure = [=] { Ui::hideLayer(); user->session().api().request(MTPcontacts_DeleteContact( user->inputUser )).done([=](const MTPcontacts_Link &result) { result.match([&](const MTPDcontacts_link &data) { user->owner().processUser(data.vuser); App::feedUserLink( MTP_int(peerToUser(user->id)), data.vmy_link, data.vforeign_link); }); }).send(); }; Ui::show(Box( text, lang(lng_box_delete), deleteSure)); } void PeerMenuAddContact(not_null user) { Ui::show(Box( user->firstName, user->lastName, Auth().data().findContactPhone(user))); } void PeerMenuShareContactBox(not_null user) { const auto weak = std::make_shared>(); auto callback = [=](not_null peer) { if (!peer->canWrite()) { Ui::show(Box( lang(lng_forward_share_cant)), LayerOption::KeepOther); return; } else if (peer->isSelf()) { auto options = ApiWrap::SendOptions(peer->owner().history(peer)); Auth().api().shareContact(user, options); Ui::Toast::Show(lang(lng_share_done)); if (auto strong = *weak) { strong->closeBox(); } return; } auto recipient = peer->isUser() ? peer->name : '\xAB' + peer->name + '\xBB'; Ui::show(Box( lng_forward_share_contact(lt_recipient, recipient), lang(lng_forward_send), [peer, user] { const auto history = peer->owner().history(peer); Ui::showPeerHistory(history, ShowAtTheEndMsgId); auto options = ApiWrap::SendOptions(history); Auth().api().shareContact(user, options); }), LayerOption::KeepOther); }; *weak = Ui::show(Box( std::make_unique(std::move(callback)), [](not_null box) { box->addButton(langFactory(lng_cancel), [box] { box->closeBox(); }); })); } void PeerMenuCreatePoll(not_null peer) { const auto box = Ui::show(Box()); const auto lock = box->lifetime().make_state(false); box->submitRequests( ) | rpl::start_with_next([=](const PollData &result) { if (std::exchange(*lock, true)) { return; } auto options = ApiWrap::SendOptions(peer->owner().history(peer)); if (const auto id = App::main()->currentReplyToIdFor(options.history)) { options.replyTo = id; } if (const auto localDraft = options.history->localDraft()) { options.clearDraft = localDraft->textWithTags.text.isEmpty(); } Auth().api().createPoll(result, options, crl::guard(box, [=] { box->closeBox(); }), crl::guard(box, [=](const RPCError &error) { *lock = false; box->submitFailed(lang(lng_attach_failed)); })); }, box->lifetime()); } QPointer ShowForwardMessagesBox( MessageIdsList &&items, FnMut &&successCallback) { const auto weak = std::make_shared>(); auto callback = [ ids = std::move(items), callback = std::move(successCallback), weak ](not_null peer) mutable { if (peer->isSelf()) { auto items = Auth().data().idsToItems(ids); if (!items.empty()) { auto options = ApiWrap::SendOptions(peer->owner().history(peer)); options.generateLocal = false; Auth().api().forwardMessages(std::move(items), options, [] { Ui::Toast::Show(lang(lng_share_done)); }); } } else { App::main()->setForwardDraft(peer->id, std::move(ids)); } if (const auto strong = *weak) { strong->closeBox(); } if (callback) { callback(); } }; auto initBox = [](not_null box) { box->addButton(langFactory(lng_cancel), [box] { box->closeBox(); }); }; *weak = Ui::show(Box( std::make_unique(std::move(callback)), std::move(initBox)), LayerOption::KeepOther); return weak->data(); } void PeerMenuAddChannelMembers(not_null channel) { if (!channel->isMegagroup() && channel->membersCount() >= Global::ChatSizeMax()) { Ui::show( Box(channel), LayerOption::KeepOther); return; } auto callback = [=](const MTPchannels_ChannelParticipants &result) { Auth().api().parseChannelParticipants(channel, result, [&]( int availableCount, const QVector &list) { auto already = ( list ) | ranges::view::transform([](const MTPChannelParticipant &p) { return p.match([](const auto &data) { return data.vuser_id.v; }); }) | ranges::view::transform([](UserId userId) { return Auth().data().userLoaded(userId); }) | ranges::view::filter([](UserData *user) { return (user != nullptr); }) | ranges::to_vector; AddParticipantsBoxController::Start( channel, { already.begin(), already.end() }); }); }; Auth().api().requestChannelMembersForAdd(channel, callback); } void PeerMenuAddMuteAction( not_null peer, const PeerMenuCallback &addAction) { Auth().data().requestNotifySettings(peer); const auto muteText = [](bool isMuted) { return lang(isMuted ? lng_enable_notifications_from_tray : lng_disable_notifications_from_tray); }; const auto muteAction = addAction(QString("-"), [=] { if (!Auth().data().notifyIsMuted(peer)) { Ui::show(Box(peer)); } else { Auth().data().updateNotifySettings(peer, 0); } }); const auto lifetime = Ui::CreateChild(muteAction); Info::Profile::NotificationsEnabledValue( peer ) | rpl::start_with_next([=](bool enabled) { muteAction->setText(muteText(!enabled)); }, *lifetime); } // #feed //void PeerMenuUngroupFeed(not_null feed) { // Ui::show(Box( // lang(lng_feed_sure_ungroup_all), // lang(lng_feed_ungroup_sure), // [=] { Ui::hideLayer(); Auth().api().ungroupAllFromFeed(feed); })); //} // void ToggleHistoryArchived(not_null history, bool archived) { const auto callback = [=] { Ui::Toast::Config toast; toast.text = lang(archived ? lng_archived_added : lng_archived_removed); toast.maxWidth = st::boxWideWidth; if (archived) { toast.durationMs = kArchivedToastDuration; } Ui::Toast::Show(toast); }; history->session().api().toggleHistoryArchived( history, archived, callback); } Fn ClearHistoryHandler(not_null peer) { return [=] { Ui::show(Box(peer, true), LayerOption::KeepOther); }; } Fn DeleteAndLeaveHandler(not_null peer) { return [=] { Ui::show(Box(peer, false), LayerOption::KeepOther); }; } void FillPeerMenu( not_null controller, not_null peer, const PeerMenuCallback &callback, PeerMenuSource source) { Filler filler(controller, peer, callback, source); filler.fill(); } void FillFolderMenu( not_null controller, not_null folder, const PeerMenuCallback &callback, PeerMenuSource source) { FolderFiller filler(controller, folder, callback, source); filler.fill(); } } // namespace Window