/* 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 "mainwidget.h" #include "api/api_updates.h" #include "data/data_photo.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_document_resolver.h" #include "data/data_wall_paper.h" #include "data/data_web_page.h" #include "data/data_game.h" #include "data/data_peer_values.h" #include "data/data_drafts.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_media_types.h" #include "data/data_folder.h" #include "data/data_channel.h" #include "data/data_chat.h" #include "data/data_user.h" #include "data/data_chat_filters.h" #include "data/data_scheduled_messages.h" #include "data/data_file_origin.h" #include "data/data_histories.h" #include "data/stickers/data_stickers.h" #include "api/api_text_entities.h" #include "ui/special_buttons.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" #include "ui/toasts/common_toasts.h" #include "ui/widgets/dropdown_menu.h" #include "ui/image/image.h" #include "ui/focus_persister.h" #include "ui/resize_area.h" #include "ui/text/text_options.h" #include "ui/emoji_config.h" #include "ui/ui_utility.h" #include "window/section_memento.h" #include "window/section_widget.h" #include "window/window_connecting_widget.h" #include "window/window_top_bar_wrap.h" #include "window/notifications_manager.h" #include "window/window_slide_animation.h" #include "window/window_session_controller.h" #include "window/window_history_hider.h" #include "window/window_controller.h" #include "window/themes/window_theme.h" #include "chat_helpers/tabbed_selector.h" // TabbedSelector::refreshStickers #include "chat_helpers/message_field.h" #include "info/info_memento.h" #include "info/info_controller.h" #include "apiwrap.h" #include "dialogs/dialogs_widget.h" #include "dialogs/dialogs_key.h" #include "history/history.h" #include "history/history_widget.h" #include "history/history_message.h" #include "history/view/media/history_view_media.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_element.h" #include "lang/lang_keys.h" #include "lang/lang_cloud_manager.h" #include "boxes/add_contact_box.h" #include "mainwindow.h" #include "inline_bots/inline_bot_layout_item.h" #include "boxes/confirm_box.h" #include "boxes/sticker_set_box.h" #include "boxes/mute_settings_box.h" #include "boxes/peer_list_controllers.h" #include "boxes/download_path_box.h" #include "boxes/connection_box.h" #include "storage/storage_account.h" #include "media/audio/media_audio.h" #include "media/player/media_player_panel.h" #include "media/player/media_player_widget.h" #include "media/player/media_player_volume_controller.h" #include "media/player/media_player_instance.h" #include "media/player/media_player_float.h" #include "base/qthelp_regex.h" #include "base/qthelp_url.h" #include "base/flat_set.h" #include "mtproto/mtproto_dc_options.h" #include "core/file_utilities.h" #include "core/update_checker.h" #include "core/shortcuts.h" #include "core/application.h" #include "core/changelogs.h" #include "base/unixtime.h" #include "calls/calls_call.h" #include "calls/calls_instance.h" #include "calls/calls_top_bar.h" #include "calls/group/calls_group_call.h" #include "export/export_settings.h" #include "export/export_manager.h" #include "export/view/export_view_top_bar.h" #include "export/view/export_view_panel_controller.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "main/main_account.h" #include "support/support_helper.h" #include "storage/storage_facade.h" #include "storage/storage_shared_media.h" #include "storage/storage_user_photos.h" #include "facades.h" #include "styles/style_dialogs.h" #include "styles/style_chat.h" #include "styles/style_boxes.h" #include #include namespace { // Send channel views each second. constexpr auto kSendViewsTimeout = crl::time(1000); } // namespace enum StackItemType { HistoryStackItem, SectionStackItem, }; class StackItem { public: StackItem(PeerData *peer) : _peer(peer) { } PeerData *peer() const { return _peer; } void setThirdSectionMemento( std::shared_ptr memento); std::shared_ptr takeThirdSectionMemento() { return std::move(_thirdSectionMemento); } void setThirdSectionWeak(QPointer section) { _thirdSectionWeak = section; } QPointer thirdSectionWeak() const { return _thirdSectionWeak; } virtual StackItemType type() const = 0; virtual ~StackItem() = default; private: PeerData *_peer = nullptr; QPointer _thirdSectionWeak; std::shared_ptr _thirdSectionMemento; }; class StackItemHistory : public StackItem { public: StackItemHistory( not_null history, MsgId msgId, QList replyReturns) : StackItem(history->peer) , history(history) , msgId(msgId) , replyReturns(replyReturns) { } StackItemType type() const override { return HistoryStackItem; } not_null history; MsgId msgId; QList replyReturns; }; class StackItemSection : public StackItem { public: StackItemSection( std::shared_ptr memento); StackItemType type() const override { return SectionStackItem; } std::shared_ptr takeMemento() { return std::move(_memento); } private: std::shared_ptr _memento; }; void StackItem::setThirdSectionMemento( std::shared_ptr memento) { _thirdSectionMemento = std::move(memento); } StackItemSection::StackItemSection( std::shared_ptr memento) : StackItem(nullptr) , _memento(std::move(memento)) { } struct MainWidget::SettingBackground { explicit SettingBackground(const Data::WallPaper &data); Data::WallPaper data; std::shared_ptr dataMedia; base::binary_guard generating; }; MainWidget::SettingBackground::SettingBackground( const Data::WallPaper &data) : data(data) { } MainWidget::MainWidget( QWidget *parent, not_null controller) : RpWidget(parent) , _controller(controller) , _api(&controller->session().mtp()) , _dialogsWidth(st::columnMinimalWidthLeft) , _thirdColumnWidth(st::columnMinimalWidthThird) , _sideShadow(this) , _dialogs(this, _controller) , _history(this, _controller) , _playerPlaylist(this, _controller) , _viewsIncrementTimer([=] { viewsIncrement(); }) , _changelogs(Core::Changelogs::Create(&controller->session())) { setupConnectingWidget(); connect(_dialogs, SIGNAL(cancelled()), this, SLOT(dialogsCancelled())); _history->cancelRequests( ) | rpl::start_with_next([=] { handleHistoryBack(); }, lifetime()); Core::App().calls().currentCallValue( ) | rpl::start_with_next([=](Calls::Call *call) { setCurrentCall(call); }, lifetime()); Core::App().calls().currentGroupCallValue( ) | rpl::start_with_next([=](Calls::GroupCall *call) { setCurrentGroupCall(call); }, lifetime()); if (_callTopBar) { _callTopBar->finishAnimating(); } Core::App().setDefaultFloatPlayerDelegate(floatPlayerDelegate()); Core::App().floatPlayerClosed( ) | rpl::start_with_next([=](FullMsgId itemId) { floatPlayerClosed(itemId); }, lifetime()); Core::App().exportManager().currentView( ) | rpl::start_with_next([=](Export::View::PanelController *view) { setCurrentExportView(view); }, lifetime()); if (_exportTopBar) { _exportTopBar->finishAnimating(); } Media::Player::instance()->updatedNotifier( ) | rpl::start_with_next([=](const Media::Player::TrackState &state) { handleAudioUpdate(state); }, lifetime()); handleAudioUpdate(Media::Player::instance()->getState(AudioMsgId::Type::Song)); handleAudioUpdate(Media::Player::instance()->getState(AudioMsgId::Type::Voice)); if (_player) { _player->finishAnimating(); } subscribe(_controller->dialogsListFocused(), [this](bool) { updateDialogsWidthAnimated(); }); subscribe(_controller->dialogsListDisplayForced(), [this](bool) { updateDialogsWidthAnimated(); }); rpl::merge( Core::App().settings().dialogsWidthRatioChanges() | rpl::to_empty, Core::App().settings().thirdColumnWidthChanges() | rpl::to_empty ) | rpl::start_with_next([=] { updateControlsGeometry(); }, lifetime()); session().changes().historyUpdates( Data::HistoryUpdate::Flag::MessageSent | Data::HistoryUpdate::Flag::LocalDraftSet ) | rpl::start_with_next([=](const Data::HistoryUpdate &update) { const auto history = update.history; if (update.flags & Data::HistoryUpdate::Flag::MessageSent) { history->forgetScrollState(); if (const auto from = history->peer->migrateFrom()) { auto &owner = history->owner(); if (const auto migrated = owner.historyLoaded(from)) { migrated->forgetScrollState(); } } } if (update.flags & Data::HistoryUpdate::Flag::LocalDraftSet) { const auto opened = (_history->peer() == history->peer.get()); if (opened) { _history->applyDraft(); } else { Ui::showPeerHistory(history, ShowAtUnreadMsgId); } Ui::hideLayer(); } }, lifetime()); // MSVC BUG + REGRESSION rpl::mappers::tuple :( using namespace rpl::mappers; _controller->activeChatValue( ) | rpl::map([](Dialogs::Key key) { const auto peer = key.peer(); auto canWrite = peer ? Data::CanWriteValue(peer) : rpl::single(false); return std::move( canWrite ) | rpl::map([=](bool can) { return std::make_tuple(key, can); }); }) | rpl::flatten_latest( ) | rpl::start_with_next([this](Dialogs::Key key, bool canWrite) { updateThirdColumnToCurrentChat(key, canWrite); }, lifetime()); QCoreApplication::instance()->installEventFilter(this); subscribe(Media::Player::instance()->playerWidgetOver(), [this](bool over) { if (over) { if (_playerPlaylist->isHidden()) { auto position = mapFromGlobal(QCursor::pos()).x(); auto bestPosition = _playerPlaylist->bestPositionFor(position); if (rtl()) bestPosition = position + 2 * (position - bestPosition) - _playerPlaylist->width(); updateMediaPlaylistPosition(bestPosition); } _playerPlaylist->showFromOther(); } else { _playerPlaylist->hideFromOther(); } }); subscribe(Media::Player::instance()->tracksFinishedNotifier(), [this](AudioMsgId::Type type) { if (type == AudioMsgId::Type::Voice) { const auto songState = Media::Player::instance()->getState(AudioMsgId::Type::Song); if (!songState.id || IsStoppedOrStopping(songState.state)) { closeBothPlayers(); } } else if (type == AudioMsgId::Type::Song) { const auto songState = Media::Player::instance()->getState(AudioMsgId::Type::Song); if (!songState.id) { closeBothPlayers(); } } }); _controller->adaptive().changes( ) | rpl::start_with_next([=] { handleAdaptiveLayoutUpdate(); }, lifetime()); _dialogs->show(); if (isOneColumn()) { _history->hide(); } else { _history->show(); } orderWidgets(); if (!Core::UpdaterDisabled()) { Core::UpdateChecker checker; checker.start(); } cSetOtherOnline(0); _history->start(); } MainWidget::~MainWidget() = default; Main::Session &MainWidget::session() const { return _controller->session(); } not_null MainWidget::controller() const { return _controller; } void MainWidget::setupConnectingWidget() { using namespace rpl::mappers; _connecting = std::make_unique( this, &session().account(), _controller->adaptive().oneColumnValue() | rpl::map(!_1)); } not_null MainWidget::floatPlayerDelegate() { return static_cast(this); } not_null MainWidget::floatPlayerWidget() { return this; } auto MainWidget::floatPlayerGetSection(Window::Column column) -> not_null { if (isThreeColumn()) { if (column == Window::Column::First) { return _dialogs; } else if (column == Window::Column::Second || !_thirdSection) { if (_mainSection) { return _mainSection; } return _history; } return _thirdSection; } else if (isNormalColumn()) { if (column == Window::Column::First) { return _dialogs; } else if (_mainSection) { return _mainSection; } return _history; } if (isOneColumn() && selectingPeer()) { return _dialogs; } else if (_mainSection) { return _mainSection; } else if (!isOneColumn() || _history->peer()) { return _history; } return _dialogs; } void MainWidget::floatPlayerEnumerateSections(Fn widget, Window::Column widgetColumn)> callback) { if (isThreeColumn()) { callback(_dialogs, Window::Column::First); if (_mainSection) { callback(_mainSection, Window::Column::Second); } else { callback(_history, Window::Column::Second); } if (_thirdSection) { callback(_thirdSection, Window::Column::Third); } } else if (isNormalColumn()) { callback(_dialogs, Window::Column::First); if (_mainSection) { callback(_mainSection, Window::Column::Second); } else { callback(_history, Window::Column::Second); } } else { if (isOneColumn() && selectingPeer()) { callback(_dialogs, Window::Column::First); } else if (_mainSection) { callback(_mainSection, Window::Column::Second); } else if (!isOneColumn() || _history->peer()) { callback(_history, Window::Column::Second); } else { callback(_dialogs, Window::Column::First); } } } bool MainWidget::floatPlayerIsVisible(not_null item) { auto isVisible = false; session().data().queryItemVisibility().notify({ item, &isVisible }, true); return isVisible; } void MainWidget::floatPlayerClosed(FullMsgId itemId) { if (_player) { const auto voiceData = Media::Player::instance()->current( AudioMsgId::Type::Voice); if (voiceData.contextId() == itemId) { stopAndClosePlayer(); } } } void MainWidget::floatPlayerDoubleClickEvent( not_null item) { _controller->showPeerHistoryAtItem(item); } bool MainWidget::setForwardDraft(PeerId peerId, MessageIdsList &&items) { Expects(peerId != 0); const auto peer = session().data().peer(peerId); const auto error = GetErrorTextForSending( peer, session().data().idsToItems(items), true); if (!error.isEmpty()) { Ui::show(Box(error), Ui::LayerOption::KeepOther); return false; } peer->owner().history(peer)->setForwardDraft(std::move(items)); _controller->showPeerHistory( peer, SectionShow::Way::Forward, ShowAtUnreadMsgId); _history->cancelReply(); return true; } bool MainWidget::shareUrl( PeerId peerId, const QString &url, const QString &text) { Expects(peerId != 0); const auto peer = session().data().peer(peerId); if (!peer->canWrite()) { Ui::show(Box(tr::lng_share_cant(tr::now))); return false; } TextWithTags textWithTags = { url + '\n' + text, TextWithTags::Tags() }; MessageCursor cursor = { url.size() + 1, url.size() + 1 + text.size(), QFIXED_MAX }; auto history = peer->owner().history(peer); history->setLocalDraft(std::make_unique( textWithTags, 0, cursor, Data::PreviewState::Allowed)); history->clearLocalEditDraft(); history->session().changes().historyUpdated( history, Data::HistoryUpdate::Flag::LocalDraftSet); return true; } bool MainWidget::inlineSwitchChosen(PeerId peerId, const QString &botAndQuery) { Expects(peerId != 0); const auto peer = session().data().peer(peerId); if (!peer->canWrite()) { Ui::show(Box(tr::lng_inline_switch_cant(tr::now))); return false; } const auto h = peer->owner().history(peer); TextWithTags textWithTags = { botAndQuery, TextWithTags::Tags() }; MessageCursor cursor = { botAndQuery.size(), botAndQuery.size(), QFIXED_MAX }; h->setLocalDraft(std::make_unique( textWithTags, 0, cursor, Data::PreviewState::Allowed)); h->clearLocalEditDraft(); h->session().changes().historyUpdated( h, Data::HistoryUpdate::Flag::LocalDraftSet); return true; } bool MainWidget::sendPaths(PeerId peerId) { Expects(peerId != 0); auto peer = session().data().peer(peerId); if (!peer->canWrite()) { Ui::show(Box(tr::lng_forward_send_files_cant(tr::now))); return false; } else if (const auto error = Data::RestrictionError( peer, ChatRestriction::SendMedia)) { Ui::show(Box(*error)); return false; } Ui::showPeerHistory(peer, ShowAtTheEndMsgId); return _history->confirmSendingFiles(cSendPaths()); } void MainWidget::onFilesOrForwardDrop( const PeerId &peerId, const QMimeData *data) { Expects(peerId != 0); if (data->hasFormat(qsl("application/x-td-forward"))) { if (!setForwardDraft(peerId, session().data().takeMimeForwardIds())) { // We've already released the mouse button, so the forwarding is cancelled. if (_hider) { _hider->startHide(); clearHider(_hider); } } } else { auto peer = session().data().peer(peerId); if (!peer->canWrite()) { Ui::show(Box(tr::lng_forward_send_files_cant(tr::now))); return; } Ui::showPeerHistory(peer, ShowAtTheEndMsgId); _history->confirmSendingFiles(data); } } bool MainWidget::notify_switchInlineBotButtonReceived(const QString &query, UserData *samePeerBot, MsgId samePeerReplyTo) { return _history->notify_switchInlineBotButtonReceived(query, samePeerBot, samePeerReplyTo); } void MainWidget::clearHider(not_null instance) { if (_hider != instance) { return; } _hider.release(); controller()->setSelectingPeer(false); if (isOneColumn()) { if (_mainSection || (_history->peer() && _history->peer()->id)) { auto animationParams = ([=] { if (_mainSection) { return prepareMainSectionAnimation(_mainSection); } return prepareHistoryAnimation(_history->peer() ? _history->peer()->id : 0); })(); _dialogs->hide(); if (_mainSection) { _mainSection->showAnimated(Window::SlideDirection::FromRight, animationParams); } else { _history->showAnimated(Window::SlideDirection::FromRight, animationParams); } floatPlayerCheckVisibility(); } else { _dialogs->updateForwardBar(); } } } void MainWidget::hiderLayer(base::unique_qptr hider) { if (controller()->window().locked()) { return; } _hider = std::move(hider); controller()->setSelectingPeer(true); _dialogs->closeForwardBarRequests( ) | rpl::start_with_next([=] { _hider->startHide(); }, _hider->lifetime()); _hider->setParent(this); _hider->hidden( ) | rpl::start_with_next([=, instance = _hider.get()] { clearHider(instance); instance->hide(); instance->deleteLater(); }, _hider->lifetime()); _hider->confirmed( ) | rpl::start_with_next([=] { _dialogs->onCancelSearch(); }, _hider->lifetime()); if (isOneColumn()) { dialogsToUp(); _hider->hide(); auto animationParams = prepareDialogsAnimation(); if (_mainSection) { _mainSection->hide(); } else { _history->hide(); } if (_dialogs->isHidden()) { _dialogs->show(); updateControlsGeometry(); _dialogs->showAnimated(Window::SlideDirection::FromLeft, animationParams); } } else { _hider->show(); updateControlsGeometry(); _dialogs->setInnerFocus(); } floatPlayerCheckVisibility(); } void MainWidget::showForwardLayer(MessageIdsList &&items) { auto callback = [=, items = std::move(items)](PeerId peer) mutable { return setForwardDraft(peer, std::move(items)); }; hiderLayer(base::make_unique_q( this, tr::lng_forward_choose(tr::now), std::move(callback), _controller->adaptive().oneColumnValue())); } void MainWidget::showSendPathsLayer() { hiderLayer(base::make_unique_q( this, tr::lng_forward_choose(tr::now), [=](PeerId peer) { return sendPaths(peer); }, _controller->adaptive().oneColumnValue())); if (_hider) { connect(_hider, &QObject::destroyed, [] { cSetSendPaths(QStringList()); }); } } void MainWidget::deletePhotoLayer(PhotoData *photo) { if (!photo) return; Ui::show(Box(tr::lng_delete_photo_sure(tr::now), tr::lng_box_delete(tr::now), crl::guard(this, [=] { session().api().clearPeerPhoto(photo); Ui::hideLayer(); }))); } void MainWidget::shareUrlLayer(const QString &url, const QString &text) { // Don't allow to insert an inline bot query by share url link. if (url.trimmed().startsWith('@')) { return; } auto callback = [=](PeerId peer) { return shareUrl(peer, url, text); }; hiderLayer(base::make_unique_q( this, tr::lng_forward_choose(tr::now), std::move(callback), _controller->adaptive().oneColumnValue())); } void MainWidget::inlineSwitchLayer(const QString &botAndQuery) { auto callback = [=](PeerId peer) { return inlineSwitchChosen(peer, botAndQuery); }; hiderLayer(base::make_unique_q( this, tr::lng_inline_switch_choose(tr::now), std::move(callback), _controller->adaptive().oneColumnValue())); } bool MainWidget::selectingPeer() const { return _hider ? true : false; } crl::time MainWidget::highlightStartTime(not_null item) const { return _history->highlightStartTime(item); } void MainWidget::sendBotCommand(Bot::SendCommandRequest request) { const auto type = _mainSection ? _mainSection->sendBotCommand(request) : Window::SectionActionResult::Fallback; if (type == Window::SectionActionResult::Fallback) { ui_showPeerHistory( request.peer->id, SectionShow::Way::ClearStack, ShowAtTheEndMsgId); _history->sendBotCommand(request); } } void MainWidget::hideSingleUseKeyboard(PeerData *peer, MsgId replyTo) { _history->hideSingleUseKeyboard(peer, replyTo); } bool MainWidget::insertBotCommand(const QString &cmd) { return _history->insertBotCommand(cmd); } void MainWidget::searchMessages(const QString &query, Dialogs::Key inChat) { _dialogs->searchMessages(query, inChat); if (isOneColumn()) { Ui::showChatsList(&session()); } else { _dialogs->setInnerFocus(); } } void MainWidget::handleAudioUpdate(const Media::Player::TrackState &state) { using State = Media::Player::State; const auto document = state.id.audio(); if (!Media::Player::IsStoppedOrStopping(state.state)) { createPlayer(); } else if (state.state == State::StoppedAtStart) { closeBothPlayers(); } if (const auto item = session().data().message(state.id.contextId())) { session().data().requestItemRepaint(item); } if (document) { if (const auto items = InlineBots::Layout::documentItems()) { if (const auto i = items->find(document); i != items->end()) { for (const auto item : i->second) { item->update(); } } } } } void MainWidget::closeBothPlayers() { if (_player) { _player->hide(anim::type::normal); } _playerVolume.destroyDelayed(); _playerPlaylist->hideIgnoringEnterEvents(); Media::Player::instance()->stop(AudioMsgId::Type::Voice); Media::Player::instance()->stop(AudioMsgId::Type::Song); Shortcuts::ToggleMediaShortcuts(false); } void MainWidget::stopAndClosePlayer() { if (_player) { _player->entity()->stopAndClose(); } } void MainWidget::createPlayer() { if (!_player) { _player.create( this, object_ptr(this, &session()), _controller->adaptive().oneColumnValue()); rpl::merge( _player->heightValue() | rpl::map_to(true), _player->shownValue() ) | rpl::start_with_next( [this] { playerHeightUpdated(); }, _player->lifetime()); _player->entity()->setCloseCallback([=] { closeBothPlayers(); }); _player->entity()->setShowItemCallback([=]( not_null item) { _controller->showPeerHistoryAtItem(item); }); _playerVolume.create(this, _controller); _player->entity()->volumeWidgetCreated(_playerVolume); orderWidgets(); if (_a_show.animating()) { _player->show(anim::type::instant); _player->setVisible(false); Shortcuts::ToggleMediaShortcuts(true); } else { _player->hide(anim::type::instant); } } if (_player && !_player->toggled()) { if (!_a_show.animating()) { _player->show(anim::type::normal); _playerHeight = _contentScrollAddToY = _player->contentHeight(); updateControlsGeometry(); Shortcuts::ToggleMediaShortcuts(true); } } } void MainWidget::playerHeightUpdated() { if (!_player) { // Player could be already "destroyDelayed", but still handle events. return; } auto playerHeight = _player->contentHeight(); if (playerHeight != _playerHeight) { _contentScrollAddToY += playerHeight - _playerHeight; _playerHeight = playerHeight; updateControlsGeometry(); } if (!_playerHeight && _player->isHidden()) { const auto state = Media::Player::instance()->getState(Media::Player::instance()->getActiveType()); if (!state.id || Media::Player::IsStoppedOrStopping(state.state)) { _playerVolume.destroyDelayed(); _player.destroyDelayed(); } } } void MainWidget::setCurrentCall(Calls::Call *call) { if (!call && _currentGroupCall) { return; } _currentCallLifetime.destroy(); _currentCall = call; if (_currentCall) { _callTopBar.destroy(); _currentCall->stateValue( ) | rpl::start_with_next([=](Calls::Call::State state) { using State = Calls::Call::State; if (state != State::Established) { destroyCallTopBar(); } else if (!_callTopBar) { createCallTopBar(); } }, _currentCallLifetime); } else { destroyCallTopBar(); } } void MainWidget::setCurrentGroupCall(Calls::GroupCall *call) { if (!call && _currentCall) { return; } _currentCallLifetime.destroy(); _currentGroupCall = call; if (_currentGroupCall) { _callTopBar.destroy(); _currentGroupCall->stateValue( ) | rpl::start_with_next([=](Calls::GroupCall::State state) { using State = Calls::GroupCall::State; if (state != State::Creating && state != State::Waiting && state != State::Joining && state != State::Joined && state != State::Connecting) { destroyCallTopBar(); } else if (!_callTopBar) { createCallTopBar(); } }, _currentCallLifetime); } else { destroyCallTopBar(); } } void MainWidget::createCallTopBar() { Expects(_currentCall != nullptr || _currentGroupCall != nullptr); _callTopBar.create( this, (_currentCall ? object_ptr(this, _currentCall) : object_ptr(this, _currentGroupCall))); _callTopBar->entity()->initBlobsUnder(this, _callTopBar->geometryValue()); _callTopBar->heightValue( ) | rpl::start_with_next([this](int value) { callTopBarHeightUpdated(value); }, lifetime()); orderWidgets(); if (_a_show.animating()) { _callTopBar->show(anim::type::instant); _callTopBar->setVisible(false); } else { _callTopBar->hide(anim::type::instant); _callTopBar->show(anim::type::normal); _callTopBarHeight = _contentScrollAddToY = _callTopBar->height(); updateControlsGeometry(); } } void MainWidget::destroyCallTopBar() { if (_callTopBar) { _callTopBar->hide(anim::type::normal); } } void MainWidget::callTopBarHeightUpdated(int callTopBarHeight) { if (!callTopBarHeight && !_currentCall && !_currentGroupCall) { _callTopBar.destroyDelayed(); } if (callTopBarHeight != _callTopBarHeight) { _contentScrollAddToY += callTopBarHeight - _callTopBarHeight; _callTopBarHeight = callTopBarHeight; updateControlsGeometry(); } } void MainWidget::setCurrentExportView(Export::View::PanelController *view) { _currentExportView = view; if (_currentExportView) { _currentExportView->progressState( ) | rpl::start_with_next([=](Export::View::Content &&data) { if (!data.rows.empty() && data.rows[0].id == Export::View::Content::kDoneId) { LOG(("Export Info: Destroy top bar by Done.")); destroyExportTopBar(); } else if (!_exportTopBar) { LOG(("Export Info: Create top bar by State.")); createExportTopBar(std::move(data)); } else { _exportTopBar->entity()->updateData(std::move(data)); } }, _exportViewLifetime); } else { _exportViewLifetime.destroy(); LOG(("Export Info: Destroy top bar by controller removal.")); destroyExportTopBar(); } } void MainWidget::createExportTopBar(Export::View::Content &&data) { _exportTopBar.create( this, object_ptr(this, std::move(data)), _controller->adaptive().oneColumnValue()); _exportTopBar->entity()->clicks( ) | rpl::start_with_next([=] { if (_currentExportView) { _currentExportView->activatePanel(); } }, _exportTopBar->lifetime()); orderWidgets(); if (_a_show.animating()) { _exportTopBar->show(anim::type::instant); _exportTopBar->setVisible(false); } else { _exportTopBar->hide(anim::type::instant); _exportTopBar->show(anim::type::normal); _exportTopBarHeight = _contentScrollAddToY = _exportTopBar->contentHeight(); updateControlsGeometry(); } rpl::merge( _exportTopBar->heightValue() | rpl::map_to(true), _exportTopBar->shownValue() ) | rpl::start_with_next([=] { exportTopBarHeightUpdated(); }, _exportTopBar->lifetime()); } void MainWidget::destroyExportTopBar() { if (_exportTopBar) { _exportTopBar->hide(anim::type::normal); } } void MainWidget::exportTopBarHeightUpdated() { if (!_exportTopBar) { // Player could be already "destroyDelayed", but still handle events. return; } const auto exportTopBarHeight = _exportTopBar->contentHeight(); if (exportTopBarHeight != _exportTopBarHeight) { _contentScrollAddToY += exportTopBarHeight - _exportTopBarHeight; _exportTopBarHeight = exportTopBarHeight; updateControlsGeometry(); } if (!_exportTopBarHeight && _exportTopBar->isHidden()) { _exportTopBar.destroyDelayed(); } } void MainWidget::inlineResultLoadProgress(FileLoader *loader) { //InlineBots::Result *result = InlineBots::resultFromLoader(loader); //if (!result) return; //result->loaded(); //Ui::repaintInlineItem(); } void MainWidget::inlineResultLoadFailed(FileLoader *loader, bool started) { //InlineBots::Result *result = InlineBots::resultFromLoader(loader); //if (!result) return; //result->loaded(); //Ui::repaintInlineItem(); } bool MainWidget::sendExistingDocument(not_null document) { return _history->sendExistingDocument(document, Api::SendOptions()); } void MainWidget::dialogsCancelled() { if (_hider) { _hider->startHide(); clearHider(_hider); } _history->activate(); } void MainWidget::setChatBackground( const Data::WallPaper &background, QImage &&image) { using namespace Window::Theme; if (isReadyChatBackground(background, image)) { setReadyChatBackground(background, std::move(image)); return; } _background = std::make_unique(background); if (const auto document = _background->data.document()) { _background->dataMedia = document->createMediaView(); _background->dataMedia->thumbnailWanted( _background->data.fileOrigin()); } _background->data.loadDocument(); checkChatBackground(); const auto tile = Data::IsLegacy1DefaultWallPaper(background); Window::Theme::Background()->downloadingStarted(tile); } bool MainWidget::isReadyChatBackground( const Data::WallPaper &background, const QImage &image) const { return !image.isNull() || !background.document(); } void MainWidget::setReadyChatBackground( const Data::WallPaper &background, QImage &&image) { using namespace Window::Theme; if (image.isNull() && !background.document() && background.localThumbnail()) { image = background.localThumbnail()->original(); } const auto resetToDefault = image.isNull() && !background.document() && background.backgroundColors().empty() && !Data::IsLegacy1DefaultWallPaper(background); const auto ready = resetToDefault ? Data::DefaultWallPaper() : background; Background()->set(ready, std::move(image)); const auto tile = Data::IsLegacy1DefaultWallPaper(ready); Background()->setTile(tile); Ui::ForceFullRepaint(this); } bool MainWidget::chatBackgroundLoading() { return (_background != nullptr); } float64 MainWidget::chatBackgroundProgress() const { if (_background) { if (_background->generating) { return 1.; } else if (const auto document = _background->data.document()) { return _background->dataMedia->progress(); } } return 1.; } void MainWidget::checkChatBackground() { if (!_background || _background->generating) { return; } const auto &media = _background->dataMedia; Assert(media != nullptr); if (!media->loaded()) { return; } const auto document = _background->data.document(); Assert(document != nullptr); const auto generateCallback = [=](QImage &&image) { const auto background = base::take(_background); const auto ready = image.isNull() ? Data::DefaultWallPaper() : background->data; setReadyChatBackground(ready, std::move(image)); }; _background->generating = Data::ReadImageAsync( media.get(), Window::Theme::PreprocessBackgroundImage, generateCallback); } Image *MainWidget::newBackgroundThumb() { return !_background ? nullptr : _background->data.localThumbnail() ? _background->data.localThumbnail() : _background->dataMedia ? _background->dataMedia->thumbnail() : nullptr; } void MainWidget::setInnerFocus() { if (_hider || !_history->peer()) { if (!_hider && _mainSection) { _mainSection->setInnerFocus(); } else if (!_hider && _thirdSection) { _thirdSection->setInnerFocus(); } else { _dialogs->setInnerFocus(); } } else if (_mainSection) { _mainSection->setInnerFocus(); } else if (_history->peer() || !_thirdSection) { _history->setInnerFocus(); } else { _thirdSection->setInnerFocus(); } } void MainWidget::scheduleViewIncrement(HistoryItem *item) { PeerData *peer = item->history()->peer; auto i = _viewsIncremented.find(peer); if (i != _viewsIncremented.cend()) { if (i->second.contains(item->id)) return; } else { i = _viewsIncremented.emplace(peer).first; } i->second.emplace(item->id); auto j = _viewsToIncrement.find(peer); if (j == _viewsToIncrement.cend()) { j = _viewsToIncrement.emplace(peer).first; _viewsIncrementTimer.callOnce(kSendViewsTimeout); } j->second.emplace(item->id); } void MainWidget::viewsIncrement() { for (auto i = _viewsToIncrement.begin(); i != _viewsToIncrement.cend();) { if (_viewsIncrementRequests.contains(i->first)) { ++i; continue; } QVector ids; ids.reserve(i->second.size()); for (const auto msgId : i->second) { ids.push_back(MTP_int(msgId)); } const auto requestId = _api.request(MTPmessages_GetMessagesViews( i->first->input, MTP_vector(ids), MTP_bool(true) )).done([=](const MTPmessages_MessageViews &result, mtpRequestId requestId) { viewsIncrementDone(ids, result, requestId); }).fail([=](const MTP::Error &error, mtpRequestId requestId) { viewsIncrementFail(error, requestId); }).afterDelay(5).send(); _viewsIncrementRequests.emplace(i->first, requestId); i = _viewsToIncrement.erase(i); } } void MainWidget::viewsIncrementDone( QVector ids, const MTPmessages_MessageViews &result, mtpRequestId requestId) { const auto &data = result.c_messages_messageViews(); session().data().processUsers(data.vusers()); session().data().processChats(data.vchats()); auto &v = data.vviews().v; if (ids.size() == v.size()) { for (auto i = _viewsIncrementRequests.begin(); i != _viewsIncrementRequests.cend(); ++i) { if (i->second == requestId) { const auto peer = i->first; const auto channel = peerToChannel(peer->id); for (int32 j = 0, l = ids.size(); j < l; ++j) { if (const auto item = session().data().message(channel, ids[j].v)) { v[j].match([&](const MTPDmessageViews &data) { if (const auto views = data.vviews()) { item->setViewsCount(views->v); } if (const auto forwards = data.vforwards()) { item->setForwardsCount(forwards->v); } if (const auto replies = data.vreplies()) { item->setReplies(*replies); } }); } } _viewsIncrementRequests.erase(i); break; } } } if (!_viewsToIncrement.empty() && !_viewsIncrementTimer.isActive()) { _viewsIncrementTimer.callOnce(kSendViewsTimeout); } } void MainWidget::viewsIncrementFail(const MTP::Error &error, mtpRequestId requestId) { for (auto i = _viewsIncrementRequests.begin(); i != _viewsIncrementRequests.cend(); ++i) { if (i->second == requestId) { _viewsIncrementRequests.erase(i); break; } } if (!_viewsToIncrement.empty() && !_viewsIncrementTimer.isActive()) { _viewsIncrementTimer.callOnce(kSendViewsTimeout); } } void MainWidget::choosePeer(PeerId peerId, MsgId showAtMsgId) { if (selectingPeer()) { _hider->offerPeer(peerId); } else if (peerId) { Ui::showPeerHistory(session().data().peer(peerId), showAtMsgId); } else { Ui::showChatsList(&session()); } } void MainWidget::clearBotStartToken(PeerData *peer) { if (peer && peer->isUser() && peer->asUser()->isBot()) { peer->asUser()->botInfo->startToken = QString(); } } void MainWidget::ctrlEnterSubmitUpdated() { _history->updateFieldSubmitSettings(); } void MainWidget::showChooseReportMessages( not_null peer, Ui::ReportReason reason, Fn done) { _history->setChooseReportMessagesDetails(reason, std::move(done)); ui_showPeerHistory( peer->id, SectionShow::Way::Forward, ShowForChooseMessagesMsgId); Ui::ShowMultilineToast({ .text = { tr::lng_report_please_select_messages(tr::now) }, }); } void MainWidget::clearChooseReportMessages() { _history->setChooseReportMessagesDetails({}, nullptr); } void MainWidget::ui_showPeerHistory( PeerId peerId, const SectionShow ¶ms, MsgId showAtMsgId) { if (auto peer = session().data().peerLoaded(peerId)) { if (peer->migrateTo()) { peer = peer->migrateTo(); peerId = peer->id; if (showAtMsgId > 0) showAtMsgId = -showAtMsgId; } const auto unavailable = peer->computeUnavailableReason(); if (!unavailable.isEmpty()) { if (params.activation != anim::activation::background) { Ui::show(Box(unavailable)); } return; } } if (IsServerMsgId(showAtMsgId) && _mainSection && _mainSection->showMessage(peerId, params, showAtMsgId)) { return; } if (!(_history->peer() && _history->peer()->id == peerId) && preventsCloseSection( [=] { ui_showPeerHistory(peerId, params, showAtMsgId); }, params)) { return; } using OriginMessage = SectionShow::OriginMessage; if (const auto origin = std::get_if(¶ms.origin)) { if (const auto returnTo = session().data().message(origin->id)) { if (returnTo->history()->peer->id == peerId) { _history->pushReplyReturn(returnTo); } } } _controller->dialogsListFocused().set(false, true); _a_dialogsWidth.stop(); using Way = SectionShow::Way; auto way = params.way; bool back = (way == Way::Backward || !peerId); bool foundInStack = !peerId; if (foundInStack || (way == Way::ClearStack)) { for (const auto &item : _stack) { clearBotStartToken(item->peer()); } _stack.clear(); } else { for (auto i = 0, s = int(_stack.size()); i < s; ++i) { if (_stack.at(i)->type() == HistoryStackItem && _stack.at(i)->peer()->id == peerId) { foundInStack = true; while (int(_stack.size()) > i + 1) { clearBotStartToken(_stack.back()->peer()); _stack.pop_back(); } _stack.pop_back(); if (!back) { back = true; } break; } } if (const auto activeChat = _controller->activeChatCurrent()) { if (const auto peer = activeChat.peer()) { if (way == Way::Forward && peer->id == peerId) { way = _mainSection ? Way::Backward : Way::ClearStack; } } } } const auto wasActivePeer = _controller->activeChatCurrent().peer(); if (params.activation != anim::activation::background) { Ui::hideSettingsAndLayer(); } if (_hider) { _hider->startHide(); _hider.release(); controller()->setSelectingPeer(false); } auto animatedShow = [&] { if (_a_show.animating() || Core::App().passcodeLocked() || (params.animated == anim::type::instant)) { return false; } if (!peerId) { if (isOneColumn()) { return _dialogs->isHidden(); } else { return false; } } if (_history->isHidden()) { if (!isOneColumn() && way == Way::ClearStack) { return false; } return (_mainSection != nullptr) || (isOneColumn() && !_dialogs->isHidden()); } if (back || way == Way::Forward) { return true; } return false; }; auto animationParams = animatedShow() ? prepareHistoryAnimation(peerId) : Window::SectionSlideParams(); if (!back && (way != Way::ClearStack)) { // This may modify the current section, for example remove its contents. saveSectionInStack(); } if (_history->peer() && _history->peer()->id != peerId && way != Way::Forward) { clearBotStartToken(_history->peer()); } _history->showHistory(peerId, showAtMsgId); auto noPeer = !_history->peer(); auto onlyDialogs = noPeer && isOneColumn(); _mainSection.destroy(); updateControlsGeometry(); if (noPeer) { _controller->setActiveChatEntry(Dialogs::Key()); } if (onlyDialogs) { _history->hide(); if (!_a_show.animating()) { if (animationParams) { auto direction = back ? Window::SlideDirection::FromLeft : Window::SlideDirection::FromRight; _dialogs->showAnimated(direction, animationParams); } else { _dialogs->showFast(); } } } else { const auto nowActivePeer = _controller->activeChatCurrent().peer(); if (nowActivePeer && nowActivePeer != wasActivePeer) { _viewsIncremented.remove(nowActivePeer); } if (isOneColumn() && !_dialogs->isHidden()) { _dialogs->hide(); } if (!_a_show.animating()) { if (!animationParams.oldContentCache.isNull()) { _history->showAnimated( back ? Window::SlideDirection::FromLeft : Window::SlideDirection::FromRight, animationParams); } else { _history->show(); crl::on_main(this, [=] { _controller->widget()->setInnerFocus(); }); } } } if (!_dialogs->isHidden()) { if (!back) { if (const auto history = _history->history()) { _dialogs->scrollToEntry(Dialogs::RowDescriptor( history, FullMsgId(history->channelId(), showAtMsgId))); } } _dialogs->update(); } floatPlayerCheckVisibility(); } PeerData *MainWidget::peer() { return _history->peer(); } void MainWidget::saveSectionInStack() { if (_mainSection) { if (auto memento = _mainSection->createMemento()) { _stack.push_back(std::make_unique( std::move(memento))); _stack.back()->setThirdSectionWeak(_thirdSection.data()); } } else if (const auto history = _history->history()) { _stack.push_back(std::make_unique( history, _history->msgId(), _history->replyReturns())); _stack.back()->setThirdSectionWeak(_thirdSection.data()); } } void MainWidget::showSection( std::shared_ptr memento, const SectionShow ¶ms) { if (_mainSection && _mainSection->showInternal( memento.get(), params)) { if (const auto entry = _mainSection->activeChat(); entry.key) { _controller->setActiveChatEntry(entry); } return; // // Now third section handles only its own showSection() requests. // General showSection() should show layer or main_section instead. // //} else if (_thirdSection && _thirdSection->showInternal( // &memento, // params)) { // return; } if (preventsCloseSection( [=] { showSection(memento, params); }, params)) { return; } // If the window was not resized, but we've enabled // tabbedSelectorSectionEnabled or thirdSectionInfoEnabled // we need to update adaptive layout to Adaptive::ThirdColumn(). updateColumnLayout(); showNewSection(std::move(memento), params); } void MainWidget::updateColumnLayout() { updateWindowAdaptiveLayout(); } Window::SectionSlideParams MainWidget::prepareThirdSectionAnimation(Window::SectionWidget *section) { Expects(_thirdSection != nullptr); Window::SectionSlideParams result; result.withTopBarShadow = section->hasTopBarShadow(); if (!_thirdSection->hasTopBarShadow()) { result.withTopBarShadow = false; } floatPlayerHideAll(); result.oldContentCache = _thirdSection->grabForShowAnimation(result); floatPlayerShowVisible(); return result; } Window::SectionSlideParams MainWidget::prepareShowAnimation( bool willHaveTopBarShadow) { Window::SectionSlideParams result; result.withTopBarShadow = willHaveTopBarShadow; if (selectingPeer() && isOneColumn()) { result.withTopBarShadow = false; } else if (_mainSection) { if (!_mainSection->hasTopBarShadow()) { result.withTopBarShadow = false; } } else if (!_history->peer()) { result.withTopBarShadow = false; } floatPlayerHideAll(); if (_player) { _player->hideShadow(); } auto playerVolumeVisible = _playerVolume && !_playerVolume->isHidden(); if (playerVolumeVisible) { _playerVolume->hide(); } auto playerPlaylistVisible = !_playerPlaylist->isHidden(); if (playerPlaylistVisible) { _playerPlaylist->hide(); } auto sectionTop = getMainSectionTop(); if (selectingPeer() && isOneColumn()) { result.oldContentCache = Ui::GrabWidget(this, QRect( 0, sectionTop, _dialogsWidth, height() - sectionTop)); } else if (_mainSection) { result.oldContentCache = _mainSection->grabForShowAnimation(result); } else if (!isOneColumn() || !_history->isHidden()) { result.oldContentCache = _history->grabForShowAnimation(result); } else { result.oldContentCache = Ui::GrabWidget(this, QRect( 0, sectionTop, _dialogsWidth, height() - sectionTop)); } if (playerVolumeVisible) { _playerVolume->show(); } if (playerPlaylistVisible) { _playerPlaylist->show(); } if (_player) { _player->showShadow(); } floatPlayerShowVisible(); return result; } Window::SectionSlideParams MainWidget::prepareMainSectionAnimation(Window::SectionWidget *section) { return prepareShowAnimation(section->hasTopBarShadow()); } Window::SectionSlideParams MainWidget::prepareHistoryAnimation(PeerId historyPeerId) { return prepareShowAnimation(historyPeerId != 0); } Window::SectionSlideParams MainWidget::prepareDialogsAnimation() { return prepareShowAnimation(false); } void MainWidget::showNewSection( std::shared_ptr memento, const SectionShow ¶ms) { using Column = Window::Column; auto saveInStack = (params.way == SectionShow::Way::Forward); const auto thirdSectionTop = getThirdSectionTop(); const auto newThirdGeometry = QRect( width() - st::columnMinimalWidthThird, thirdSectionTop, st::columnMinimalWidthThird, height() - thirdSectionTop); auto newThirdSection = (isThreeColumn() && params.thirdColumn) ? memento->createWidget( this, _controller, Column::Third, newThirdGeometry) : nullptr; const auto layerRect = parentWidget()->rect(); if (newThirdSection) { saveInStack = false; } else if (auto layer = memento->createLayer(_controller, layerRect)) { if (params.activation != anim::activation::background) { Ui::hideLayer(anim::type::instant); } _controller->showSpecialLayer(std::move(layer)); return; } if (params.activation != anim::activation::background) { Ui::hideSettingsAndLayer(); } _controller->dialogsListFocused().set(false, true); _a_dialogsWidth.stop(); auto mainSectionTop = getMainSectionTop(); auto newMainGeometry = QRect( _history->x(), mainSectionTop, _history->width(), height() - mainSectionTop); auto newMainSection = newThirdSection ? nullptr : memento->createWidget( this, _controller, isOneColumn() ? Column::First : Column::Second, newMainGeometry); Assert(newMainSection || newThirdSection); auto animatedShow = [&] { if (_a_show.animating() || Core::App().passcodeLocked() || (params.animated == anim::type::instant) || memento->instant()) { return false; } if (!isOneColumn() && params.way == SectionShow::Way::ClearStack) { return false; } else if (isOneColumn() || (newThirdSection && _thirdSection) || (newMainSection && isMainSectionShown())) { return true; } return false; }(); auto animationParams = animatedShow ? (newThirdSection ? prepareThirdSectionAnimation(newThirdSection) : prepareMainSectionAnimation(newMainSection)) : Window::SectionSlideParams(); setFocus(); // otherwise dialogs widget could be focused. if (saveInStack) { // This may modify the current section, for example remove its contents. saveSectionInStack(); } auto &settingSection = newThirdSection ? _thirdSection : _mainSection; if (newThirdSection) { _thirdSection = std::move(newThirdSection); if (!_thirdShadow) { _thirdShadow.create(this); _thirdShadow->show(); orderWidgets(); } updateControlsGeometry(); } else { _mainSection = std::move(newMainSection); updateControlsGeometry(); _history->finishAnimating(); _history->showHistory(0, 0); _history->hide(); if (isOneColumn()) _dialogs->hide(); } if (animationParams) { auto back = (params.way == SectionShow::Way::Backward); auto direction = (back || settingSection->forceAnimateBack()) ? Window::SlideDirection::FromLeft : Window::SlideDirection::FromRight; if (isOneColumn()) { _controller->removeLayerBlackout(); } settingSection->showAnimated(direction, animationParams); } else { settingSection->showFast(); } if (settingSection.data() == _mainSection.data()) { if (const auto entry = _mainSection->activeChat(); entry.key) { _controller->setActiveChatEntry(entry); } } floatPlayerCheckVisibility(); orderWidgets(); } void MainWidget::checkMainSectionToLayer() { if (!_mainSection) { return; } Ui::FocusPersister persister(this); if (auto layer = _mainSection->moveContentToLayer(rect())) { dropMainSection(_mainSection); _controller->showSpecialLayer( std::move(layer), anim::type::instant); } } void MainWidget::dropMainSection(Window::SectionWidget *widget) { if (_mainSection != widget) { return; } _mainSection.destroy(); _controller->showBackFromStack( SectionShow( anim::type::instant, anim::activation::background)); } bool MainWidget::isMainSectionShown() const { return _mainSection || _history->peer(); } bool MainWidget::isThirdSectionShown() const { return _thirdSection != nullptr; } bool MainWidget::stackIsEmpty() const { return _stack.empty(); } bool MainWidget::preventsCloseSection(Fn callback) const { if (Core::App().passcodeLocked()) { return false; } auto copy = callback; return (_mainSection && _mainSection->preventsClose(std::move(copy))) || (_history && _history->preventsClose(std::move(callback))); } bool MainWidget::preventsCloseSection( Fn callback, const SectionShow ¶ms) const { return params.thirdColumn ? false : preventsCloseSection(std::move(callback)); } void MainWidget::showBackFromStack( const SectionShow ¶ms) { if (preventsCloseSection([=] { showBackFromStack(params); }, params)) { return; } if (selectingPeer()) { return; } else if (_stack.empty()) { _controller->clearSectionStack(params); crl::on_main(this, [=] { _controller->widget()->setInnerFocus(); }); return; } auto item = std::move(_stack.back()); _stack.pop_back(); if (auto currentHistoryPeer = _history->peer()) { clearBotStartToken(currentHistoryPeer); } _thirdSectionFromStack = item->takeThirdSectionMemento(); if (item->type() == HistoryStackItem) { auto historyItem = static_cast(item.get()); _controller->showPeerHistory( historyItem->peer()->id, params.withWay(SectionShow::Way::Backward), ShowAtUnreadMsgId); _history->setReplyReturns(historyItem->peer()->id, historyItem->replyReturns); } else if (item->type() == SectionStackItem) { auto sectionItem = static_cast(item.get()); showNewSection( sectionItem->takeMemento(), params.withWay(SectionShow::Way::Backward)); } if (_thirdSectionFromStack && _thirdSection) { _controller->showSection( base::take(_thirdSectionFromStack), SectionShow( SectionShow::Way::ClearStack, anim::type::instant, anim::activation::background)); } } void MainWidget::orderWidgets() { _dialogs->raise(); if (_player) { _player->raise(); } if (_exportTopBar) { _exportTopBar->raise(); } if (_callTopBar) { _callTopBar->raise(); } if (_playerVolume) { _playerVolume->raise(); } _sideShadow->raise(); if (_thirdShadow) { _thirdShadow->raise(); } if (_firstColumnResizeArea) { _firstColumnResizeArea->raise(); } if (_thirdColumnResizeArea) { _thirdColumnResizeArea->raise(); } _connecting->raise(); _playerPlaylist->raise(); floatPlayerRaiseAll(); if (_hider) _hider->raise(); } QRect MainWidget::historyRect() const { QRect r(_history->historyRect()); r.moveLeft(r.left() + _history->x()); r.moveTop(r.top() + _history->y()); return r; } QPixmap MainWidget::grabForShowAnimation(const Window::SectionSlideParams ¶ms) { QPixmap result; floatPlayerHideAll(); if (_player) { _player->hideShadow(); } auto playerVolumeVisible = _playerVolume && !_playerVolume->isHidden(); if (playerVolumeVisible) { _playerVolume->hide(); } auto playerPlaylistVisible = !_playerPlaylist->isHidden(); if (playerPlaylistVisible) { _playerPlaylist->hide(); } auto sectionTop = getMainSectionTop(); if (isOneColumn()) { result = Ui::GrabWidget(this, QRect( 0, sectionTop, _dialogsWidth, height() - sectionTop)); } else { _sideShadow->hide(); if (_thirdShadow) { _thirdShadow->hide(); } result = Ui::GrabWidget(this, QRect( _dialogsWidth, sectionTop, width() - _dialogsWidth, height() - sectionTop)); _sideShadow->show(); if (_thirdShadow) { _thirdShadow->show(); } } if (playerVolumeVisible) { _playerVolume->show(); } if (playerPlaylistVisible) { _playerPlaylist->show(); } if (_player) { _player->showShadow(); } floatPlayerShowVisible(); return result; } void MainWidget::windowShown() { _history->windowShown(); } void MainWidget::dialogsToUp() { _dialogs->jumpToTop(); } void MainWidget::checkHistoryActivation() { _history->checkHistoryActivation(); } void MainWidget::showAnimated(const QPixmap &bgAnimCache, bool back) { _showBack = back; (_showBack ? _cacheOver : _cacheUnder) = bgAnimCache; _a_show.stop(); showAll(); floatPlayerHideAll(); (_showBack ? _cacheUnder : _cacheOver) = Ui::GrabWidget(this); hideAll(); floatPlayerShowVisible(); _a_show.start( [this] { animationCallback(); }, 0., 1., st::slideDuration, Window::SlideAnimation::transition()); show(); } void MainWidget::animationCallback() { update(); if (!_a_show.animating()) { _cacheUnder = _cacheOver = QPixmap(); showAll(); activate(); } } void MainWidget::paintEvent(QPaintEvent *e) { if (_background) { checkChatBackground(); } Painter p(this); auto progress = _a_show.value(1.); if (_a_show.animating()) { auto coordUnder = _showBack ? anim::interpolate(-st::slideShift, 0, progress) : anim::interpolate(0, -st::slideShift, progress); auto coordOver = _showBack ? anim::interpolate(0, width(), progress) : anim::interpolate(width(), 0, progress); auto shadow = _showBack ? (1. - progress) : progress; if (coordOver > 0) { p.drawPixmap(QRect(0, 0, coordOver, height()), _cacheUnder, QRect(-coordUnder * cRetinaFactor(), 0, coordOver * cRetinaFactor(), height() * cRetinaFactor())); p.setOpacity(shadow); p.fillRect(0, 0, coordOver, height(), st::slideFadeOutBg); p.setOpacity(1); } p.drawPixmap(coordOver, 0, _cacheOver); p.setOpacity(shadow); st::slideShadow.fill(p, QRect(coordOver - st::slideShadow.width(), 0, st::slideShadow.width(), height())); } } int MainWidget::getMainSectionTop() const { return _callTopBarHeight + _exportTopBarHeight + _playerHeight; } int MainWidget::getThirdSectionTop() const { return 0; } void MainWidget::hideAll() { _dialogs->hide(); _history->hide(); if (_mainSection) { _mainSection->hide(); } if (_thirdSection) { _thirdSection->hide(); } _sideShadow->hide(); if (_thirdShadow) { _thirdShadow->hide(); } if (_player) { _player->setVisible(false); _playerHeight = 0; } if (_callTopBar) { _callTopBar->setVisible(false); _callTopBarHeight = 0; } } void MainWidget::showAll() { if (cPasswordRecovered()) { cSetPasswordRecovered(false); Ui::show(Box(tr::lng_signin_password_removed(tr::now))); } if (isOneColumn()) { _sideShadow->hide(); if (_hider) { _hider->hide(); } if (selectingPeer()) { _dialogs->showFast(); _history->hide(); if (_mainSection) _mainSection->hide(); } else if (_mainSection) { _mainSection->show(); } else if (_history->peer()) { _history->show(); _history->updateControlsGeometry(); } else { _dialogs->showFast(); _history->hide(); } if (!selectingPeer()) { if (_mainSection) { _dialogs->hide(); } else if (isMainSectionShown()) { _dialogs->hide(); } } } else { _sideShadow->show(); if (_hider) { _hider->show(); } _dialogs->showFast(); if (_mainSection) { _mainSection->show(); } else { _history->show(); _history->updateControlsGeometry(); } if (_thirdSection) { _thirdSection->show(); } if (_thirdShadow) { _thirdShadow->show(); } } if (_player) { _player->setVisible(true); _playerHeight = _player->contentHeight(); } if (_callTopBar) { _callTopBar->setVisible(true); // show() could've send pending resize event that would update // the height value and destroy the top bar if it was hiding. if (_callTopBar) { _callTopBarHeight = _callTopBar->height(); } } updateControlsGeometry(); floatPlayerCheckVisibility(); _controller->widget()->checkHistoryActivation(); } void MainWidget::resizeEvent(QResizeEvent *e) { updateControlsGeometry(); } void MainWidget::updateControlsGeometry() { updateWindowAdaptiveLayout(); if (Core::App().settings().dialogsWidthRatio() > 0) { _a_dialogsWidth.stop(); } if (!_a_dialogsWidth.animating()) { _dialogs->stopWidthAnimation(); } if (isThreeColumn()) { if (!_thirdSection && !_controller->takeThirdSectionFromLayer()) { auto params = Window::SectionShow( Window::SectionShow::Way::ClearStack, anim::type::instant, anim::activation::background); const auto active = _controller->activeChatCurrent(); if (const auto peer = active.peer()) { if (Core::App().settings().tabbedSelectorSectionEnabled()) { if (_mainSection) { _mainSection->pushTabbedSelectorToThirdSection(peer, params); } else { _history->pushTabbedSelectorToThirdSection(peer, params); } } else if (Core::App().settings().thirdSectionInfoEnabled()) { _controller->showSection( Info::Memento::Default(peer), params.withThirdColumn()); } } } } else { _thirdSection.destroy(); _thirdShadow.destroy(); } auto mainSectionTop = getMainSectionTop(); auto dialogsWidth = qRound(_a_dialogsWidth.value(_dialogsWidth)); if (isOneColumn()) { if (_callTopBar) { _callTopBar->resizeToWidth(dialogsWidth); _callTopBar->moveToLeft(0, 0); } if (_exportTopBar) { _exportTopBar->resizeToWidth(dialogsWidth); _exportTopBar->moveToLeft(0, _callTopBarHeight); } if (_player) { _player->resizeToWidth(dialogsWidth); _player->moveToLeft(0, _callTopBarHeight + _exportTopBarHeight); } auto mainSectionGeometry = QRect( 0, mainSectionTop, dialogsWidth, height() - mainSectionTop); _dialogs->setGeometryWithTopMoved(mainSectionGeometry, _contentScrollAddToY); _history->setGeometryWithTopMoved(mainSectionGeometry, _contentScrollAddToY); if (_hider) _hider->setGeometry(0, 0, dialogsWidth, height()); } else { auto thirdSectionWidth = _thirdSection ? _thirdColumnWidth : 0; if (_thirdSection) { auto thirdSectionTop = getThirdSectionTop(); _thirdSection->setGeometry( width() - thirdSectionWidth, thirdSectionTop, thirdSectionWidth, height() - thirdSectionTop); } accumulate_min(dialogsWidth, width() - st::columnMinimalWidthMain); auto mainSectionWidth = width() - dialogsWidth - thirdSectionWidth; _dialogs->setGeometryToLeft(0, 0, dialogsWidth, height()); const auto shadowTop = _controller->window().verticalShadowTop(); const auto shadowHeight = height() - shadowTop; _sideShadow->setGeometryToLeft( dialogsWidth, shadowTop, st::lineWidth, shadowHeight); if (_thirdShadow) { _thirdShadow->setGeometryToLeft( width() - thirdSectionWidth - st::lineWidth, shadowTop, st::lineWidth, shadowHeight); } if (_callTopBar) { _callTopBar->resizeToWidth(mainSectionWidth); _callTopBar->moveToLeft(dialogsWidth, 0); } if (_exportTopBar) { _exportTopBar->resizeToWidth(mainSectionWidth); _exportTopBar->moveToLeft(dialogsWidth, _callTopBarHeight); } if (_player) { _player->resizeToWidth(mainSectionWidth); _player->moveToLeft( dialogsWidth, _callTopBarHeight + _exportTopBarHeight); } _history->setGeometryWithTopMoved({ dialogsWidth, mainSectionTop, mainSectionWidth, height() - mainSectionTop }, _contentScrollAddToY); if (_hider) { _hider->setGeometryToLeft(dialogsWidth, 0, mainSectionWidth, height()); } } if (_mainSection) { auto mainSectionGeometry = QRect(_history->x(), mainSectionTop, _history->width(), height() - mainSectionTop); _mainSection->setGeometryWithTopMoved(mainSectionGeometry, _contentScrollAddToY); } refreshResizeAreas(); updateMediaPlayerPosition(); updateMediaPlaylistPosition(_playerPlaylist->x()); _contentScrollAddToY = 0; floatPlayerUpdatePositions(); } void MainWidget::refreshResizeAreas() { if (!isOneColumn()) { ensureFirstColumnResizeAreaCreated(); _firstColumnResizeArea->setGeometryToLeft( _history->x(), 0, st::historyResizeWidth, height()); } else if (_firstColumnResizeArea) { _firstColumnResizeArea.destroy(); } if (isThreeColumn() && _thirdSection) { ensureThirdColumnResizeAreaCreated(); _thirdColumnResizeArea->setGeometryToLeft( _thirdSection->x(), 0, st::historyResizeWidth, height()); } else if (_thirdColumnResizeArea) { _thirdColumnResizeArea.destroy(); } } template void MainWidget::createResizeArea( object_ptr &area, MoveCallback &&moveCallback, FinishCallback &&finishCallback) { area.create(this); area->show(); area->addMoveLeftCallback( std::forward(moveCallback)); area->addMoveFinishedCallback( std::forward(finishCallback)); orderWidgets(); } void MainWidget::ensureFirstColumnResizeAreaCreated() { if (_firstColumnResizeArea) { return; } auto moveLeftCallback = [=](int globalLeft) { auto newWidth = globalLeft - mapToGlobal(QPoint(0, 0)).x(); auto newRatio = (newWidth < st::columnMinimalWidthLeft / 2) ? 0. : float64(newWidth) / width(); Core::App().settings().setDialogsWidthRatio(newRatio); }; auto moveFinishedCallback = [=] { if (isOneColumn()) { return; } if (Core::App().settings().dialogsWidthRatio() > 0) { Core::App().settings().setDialogsWidthRatio( float64(_dialogsWidth) / width()); } Core::App().saveSettingsDelayed(); }; createResizeArea( _firstColumnResizeArea, std::move(moveLeftCallback), std::move(moveFinishedCallback)); } void MainWidget::ensureThirdColumnResizeAreaCreated() { if (_thirdColumnResizeArea) { return; } auto moveLeftCallback = [=](int globalLeft) { auto newWidth = mapToGlobal(QPoint(width(), 0)).x() - globalLeft; Core::App().settings().setThirdColumnWidth(newWidth); }; auto moveFinishedCallback = [=] { if (!isThreeColumn() || !_thirdSection) { return; } Core::App().settings().setThirdColumnWidth(std::clamp( Core::App().settings().thirdColumnWidth(), st::columnMinimalWidthThird, st::columnMaximalWidthThird)); Core::App().saveSettingsDelayed(); }; createResizeArea( _thirdColumnResizeArea, std::move(moveLeftCallback), std::move(moveFinishedCallback)); } void MainWidget::updateDialogsWidthAnimated() { if (Core::App().settings().dialogsWidthRatio() > 0) { return; } auto dialogsWidth = _dialogsWidth; updateWindowAdaptiveLayout(); if (!Core::App().settings().dialogsWidthRatio() && (_dialogsWidth != dialogsWidth || _a_dialogsWidth.animating())) { _dialogs->startWidthAnimation(); _a_dialogsWidth.start( [this] { updateControlsGeometry(); }, dialogsWidth, _dialogsWidth, st::dialogsWidthDuration, anim::easeOutCirc); updateControlsGeometry(); } } bool MainWidget::saveThirdSectionToStackBack() const { return !_stack.empty() && _thirdSection != nullptr && _stack.back()->thirdSectionWeak() == _thirdSection.data(); } auto MainWidget::thirdSectionForCurrentMainSection( Dialogs::Key key) -> std::shared_ptr { if (_thirdSectionFromStack) { return std::move(_thirdSectionFromStack); } else if (const auto peer = key.peer()) { return std::make_shared( peer, Info::Memento::DefaultSection(peer)); } Unexpected("Key in MainWidget::thirdSectionForCurrentMainSection()."); } void MainWidget::updateThirdColumnToCurrentChat( Dialogs::Key key, bool canWrite) { auto saveOldThirdSection = [&] { if (saveThirdSectionToStackBack()) { _stack.back()->setThirdSectionMemento( _thirdSection->createMemento()); _thirdSection.destroy(); } }; auto &settings = Core::App().settings(); auto params = Window::SectionShow( Window::SectionShow::Way::ClearStack, anim::type::instant, anim::activation::background); auto switchInfoFast = [&] { saveOldThirdSection(); // // Like in _controller->showPeerInfo() // if (isThreeColumn() && !settings.thirdSectionInfoEnabled()) { settings.setThirdSectionInfoEnabled(true); Core::App().saveSettingsDelayed(); } _controller->showSection( thirdSectionForCurrentMainSection(key), params.withThirdColumn()); }; auto switchTabbedFast = [&](not_null peer) { saveOldThirdSection(); return _mainSection ? _mainSection->pushTabbedSelectorToThirdSection(peer, params) : _history->pushTabbedSelectorToThirdSection(peer, params); }; if (isThreeColumn() && settings.tabbedSelectorSectionEnabled() && key) { if (!canWrite) { switchInfoFast(); settings.setTabbedSelectorSectionEnabled(true); settings.setTabbedReplacedWithInfo(true); } else if (settings.tabbedReplacedWithInfo() && key.history() && switchTabbedFast(key.history()->peer)) { settings.setTabbedReplacedWithInfo(false); } } else { settings.setTabbedReplacedWithInfo(false); if (!key) { if (_thirdSection) { _thirdSection.destroy(); _thirdShadow.destroy(); updateControlsGeometry(); } } else if (isThreeColumn() && settings.thirdSectionInfoEnabled()) { switchInfoFast(); } } } void MainWidget::updateMediaPlayerPosition() { if (_player && _playerVolume) { auto relativePosition = _player->entity()->getPositionForVolumeWidget(); auto playerMargins = _playerVolume->getMargin(); _playerVolume->moveToLeft(_player->x() + relativePosition.x() - playerMargins.left(), _player->y() + relativePosition.y() - playerMargins.top()); } } void MainWidget::updateMediaPlaylistPosition(int x) { if (_player) { auto playlistLeft = x; auto playlistWidth = _playerPlaylist->width(); auto playlistTop = _player->y() + _player->height(); auto rightEdge = width(); if (playlistLeft + playlistWidth > rightEdge) { playlistLeft = rightEdge - playlistWidth; } else if (playlistLeft < 0) { playlistLeft = 0; } _playerPlaylist->move(playlistLeft, playlistTop); } } void MainWidget::returnTabbedSelector() { if (!_mainSection || !_mainSection->returnTabbedSelector()) { _history->returnTabbedSelector(); } } void MainWidget::keyPressEvent(QKeyEvent *e) { } bool MainWidget::eventFilter(QObject *o, QEvent *e) { if (e->type() == QEvent::FocusIn) { if (const auto widget = qobject_cast(o)) { if (_history == widget || _history->isAncestorOf(widget) || (_mainSection && (_mainSection == widget || _mainSection->isAncestorOf(widget))) || (_thirdSection && (_thirdSection == widget || _thirdSection->isAncestorOf(widget)))) { _controller->dialogsListFocused().set(false); } else if (_dialogs == widget || _dialogs->isAncestorOf(widget)) { _controller->dialogsListFocused().set(true); } } } else if (e->type() == QEvent::MouseButtonPress) { if (static_cast(e)->button() == Qt::BackButton) { if (!Core::App().hideMediaView()) { handleHistoryBack(); } return true; } } else if (e->type() == QEvent::Wheel) { if (const auto result = floatPlayerFilterWheelEvent(o, e)) { return *result; } } return RpWidget::eventFilter(o, e); } void MainWidget::handleAdaptiveLayoutUpdate() { showAll(); _sideShadow->setVisible(!isOneColumn()); if (_player) { _player->updateAdaptiveLayout(); } } void MainWidget::handleHistoryBack() { const auto historyFromFolder = _history->history() ? _history->history()->folder() : nullptr; const auto openedFolder = _controller->openedFolder().current(); if (!openedFolder || historyFromFolder == openedFolder || _dialogs->isHidden()) { _controller->showBackFromStack(); _dialogs->setInnerFocus(); } else { _controller->closeFolder(); } } void MainWidget::updateWindowAdaptiveLayout() { auto layout = _controller->computeColumnLayout(); auto dialogsWidthRatio = Core::App().settings().dialogsWidthRatio(); // Check if we are in a single-column layout in a wide enough window // for the normal layout. If so, switch to the normal layout. if (layout.windowLayout == Window::Adaptive::WindowLayout::OneColumn) { auto chatWidth = layout.chatWidth; //if (session().settings().tabbedSelectorSectionEnabled() // && chatWidth >= _history->minimalWidthForTabbedSelectorSection()) { // chatWidth -= _history->tabbedSelectorSectionWidth(); //} auto minimalNormalWidth = st::columnMinimalWidthLeft + st::columnMinimalWidthMain; if (chatWidth >= minimalNormalWidth) { // Switch layout back to normal in a wide enough window. layout.windowLayout = Window::Adaptive::WindowLayout::Normal; layout.dialogsWidth = st::columnMinimalWidthLeft; layout.chatWidth = layout.bodyWidth - layout.dialogsWidth; dialogsWidthRatio = float64(layout.dialogsWidth) / layout.bodyWidth; } } // Check if we are going to create the third column and shrink the // dialogs widget to provide a wide enough chat history column. // Don't shrink the column on the first call, when window is inited. if (layout.windowLayout == Window::Adaptive::WindowLayout::ThreeColumn && _controller->widget()->positionInited()) { //auto chatWidth = layout.chatWidth; //if (_history->willSwitchToTabbedSelectorWithWidth(chatWidth)) { // auto thirdColumnWidth = _history->tabbedSelectorSectionWidth(); // auto twoColumnsWidth = (layout.bodyWidth - thirdColumnWidth); // auto sameRatioChatWidth = twoColumnsWidth - qRound(dialogsWidthRatio * twoColumnsWidth); // auto desiredChatWidth = qMax(sameRatioChatWidth, HistoryView::WideChatWidth()); // chatWidth -= thirdColumnWidth; // auto extendChatBy = desiredChatWidth - chatWidth; // accumulate_min(extendChatBy, layout.dialogsWidth - st::columnMinimalWidthLeft); // if (extendChatBy > 0) { // layout.dialogsWidth -= extendChatBy; // layout.chatWidth += extendChatBy; // dialogsWidthRatio = float64(layout.dialogsWidth) / layout.bodyWidth; // } //} } Core::App().settings().setDialogsWidthRatio(dialogsWidthRatio); auto useSmallColumnWidth = !isOneColumn() && !dialogsWidthRatio && !_controller->forceWideDialogs(); _dialogsWidth = useSmallColumnWidth ? _controller->dialogsSmallColumnWidth() : layout.dialogsWidth; _thirdColumnWidth = layout.thirdWidth; _controller->adaptive().setWindowLayout(layout.windowLayout); } int MainWidget::backgroundFromY() const { return -getMainSectionTop(); } void MainWidget::searchInChat(Dialogs::Key chat) { if (_controller->openedFolder().current()) { _controller->closeFolder(); } _dialogs->searchInChat(chat); if (isOneColumn()) { Ui::showChatsList(&session()); } else { _dialogs->setInnerFocus(); } } bool MainWidget::contentOverlapped(const QRect &globalRect) { return (_history->contentOverlapped(globalRect) || _playerPlaylist->overlaps(globalRect) || (_playerVolume && _playerVolume->overlaps(globalRect))); } void MainWidget::activate() { if (_a_show.animating()) { return; } else if (!_mainSection) { if (_hider) { _dialogs->setInnerFocus(); } else if (!Ui::isLayerShown()) { if (!cSendPaths().isEmpty()) { const auto interpret = qstr("interpret://"); const auto path = cSendPaths()[0]; if (path.startsWith(interpret)) { cSetSendPaths(QStringList()); const auto error = Support::InterpretSendPath( _controller, path.mid(interpret.size())); if (!error.isEmpty()) { Ui::show(Box(error)); } } else { showSendPathsLayer(); } } else if (_history->peer()) { _history->activate(); } else { _dialogs->setInnerFocus(); } } } _controller->widget()->fixOrder(); } bool MainWidget::isActive() const { return isVisible() && !_a_show.animating() && !session().updates().isIdle(); } bool MainWidget::doWeMarkAsRead() const { return isActive() && !_mainSection; } int32 MainWidget::dlgsWidth() const { return _dialogs->width(); } void MainWidget::saveFieldToHistoryLocalDraft() { _history->saveFieldToHistoryLocalDraft(); } bool MainWidget::isOneColumn() const { return _controller->adaptive().isOneColumn(); } bool MainWidget::isNormalColumn() const { return _controller->adaptive().isNormal(); } bool MainWidget::isThreeColumn() const { return _controller->adaptive().isThreeColumn(); } namespace App { MainWidget *main() { if (const auto window = wnd()) { return window->sessionContent(); } return nullptr; } } // namespace App