Add "ctrl-click-chat-new-window" option.

This commit is contained in:
John Preston 2022-06-07 22:05:37 +04:00
parent 896d39bc6a
commit a780fbd09b
22 changed files with 192 additions and 104 deletions

View File

@ -66,7 +66,9 @@ void ShowPeerInfoSync(not_null<PeerData*> peer) {
// we can safely use activeWindow.
if (const auto window = Core::App().activeWindow()) {
if (const auto controller = window->sessionController()) {
controller->showPeerInfo(peer);
if (&controller->session() == &peer->session()) {
controller->showPeerInfo(peer);
}
}
}
}

View File

@ -1198,7 +1198,7 @@ base::unique_qptr<Ui::PopupMenu> Members::Controller::createRowContextMenu(
if (const auto window = Core::App().separateWindowForPeer(
participantPeer)) {
return window->sessionController();
} else if (const auto window = Core::App().activeWindow()) {
} else if (const auto window = Core::App().primaryWindow()) {
if (const auto controller = window->sessionController()) {
if (&controller->session() == session) {
return controller;

View File

@ -1264,7 +1264,7 @@ object_ptr<TabbedSelector::InnerFooter> StickersListWidget::createFooter() {
_footer->openSettingsRequests(
) | rpl::start_with_next([=] {
const auto onlyFeatured = _footer->hasOnlyFeaturedSets();
Ui::show(Box<StickersBox>(
controller()->show(Box<StickersBox>(
controller(),
(onlyFeatured
? StickersBox::Section::Featured

View File

@ -323,7 +323,7 @@ void Application::run() {
DEBUG_LOG(("Application Info: window created..."));
// Depend on activeWindow() for now :(
// Depend on primaryWindow() for now :(
startShortcuts();
startDomain();
@ -1170,6 +1170,26 @@ Window::Controller *Application::activeWindow() const {
return _lastActiveWindow;
}
void Application::closeWindow(not_null<Window::Controller*> window) {
for (auto i = begin(_secondaryWindows); i != end(_secondaryWindows);) {
if (i->second.get() == window) {
if (_lastActiveWindow == window) {
_lastActiveWindow = _primaryWindow.get();
}
i = _secondaryWindows.erase(i);
} else {
++i;
}
}
}
void Application::windowActivated(not_null<Window::Controller*> window) {
_lastActiveWindow = window;
if (_mediaView && !_mediaView->isHidden()) {
_mediaView->activate();
}
}
bool Application::closeActiveWindow() {
if (hideMediaView()) {
return true;
@ -1198,11 +1218,12 @@ bool Application::minimizeActiveWindow() {
}
QWidget *Application::getFileDialogParent() {
return (_mediaView && !_mediaView->isHidden())
? static_cast<QWidget*>(_mediaView->widget())
: activeWindow()
? static_cast<QWidget*>(activeWindow()->widget())
: nullptr;
if (const auto view = _mediaView.get(); view && !view->isHidden()) {
return view->widget();
} else if (const auto active = activeWindow()) {
return active->widget();
}
return nullptr;
}
void Application::notifyFileDialogShown(bool shown) {
@ -1211,12 +1232,6 @@ void Application::notifyFileDialogShown(bool shown) {
}
}
void Application::checkMediaViewActivation() {
if (_mediaView && !_mediaView->isHidden()) {
_mediaView->activate();
}
}
QPoint Application::getPointForCallPanelCenter() const {
if (const auto window = activeWindow()) {
return window->getPointForCallPanelCenter();

View File

@ -162,6 +162,8 @@ public:
Window::Controller *ensureSeparateWindowForPeer(
not_null<PeerData*> peer,
MsgId showAtMsgId);
void closeWindow(not_null<Window::Controller*> window);
void windowActivated(not_null<Window::Controller*> window);
bool closeActiveWindow();
bool minimizeActiveWindow();
[[nodiscard]] QWidget *getFileDialogParent();
@ -170,7 +172,6 @@ public:
[[nodiscard]] bool isActiveForTrayMenu() const;
// Media view interface.
void checkMediaViewActivation();
bool hideMediaView();
[[nodiscard]] QPoint getPointForCallPanelCenter() const;

View File

@ -44,8 +44,6 @@ enum class OrderMode;
namespace Core {
struct WindowPosition {
WindowPosition() = default;
int32 moncrc = 0;
int maximized = 0;
int scale = 0;

View File

@ -35,6 +35,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/stickers/data_stickers.h"
#include "data/data_send_action.h"
#include "base/unixtime.h"
#include "base/options.h"
#include "lang/lang_keys.h"
#include "mainwindow.h"
#include "mainwidget.h"
@ -64,6 +65,13 @@ namespace {
constexpr auto kHashtagResultsLimit = 5;
constexpr auto kStartReorderThreshold = 30;
base::options::toggle TabbedPanelShowOnClick({
.id = kOptionCtrlClickChatNewWindow,
.name = "New chat window by Ctrl+Click",
.description = "Open chat in a new window by Ctrl+Click "
"(Cmd+Click on macOS).",
});
int FixedOnTopDialogsCount(not_null<Dialogs::IndexedList*> list) {
auto result = 0;
for (const auto &row : *list) {
@ -92,6 +100,8 @@ int PinnedDialogsCount(
} // namespace
const char kOptionCtrlClickChatNewWindow[] = "ctrl-click-chat-new-window";
struct InnerWidget::CollapsedRow {
CollapsedRow(Data::Folder *folder) : folder(folder) {
}
@ -2801,9 +2811,9 @@ bool InnerWidget::chooseRow(Qt::KeyboardModifiers modifiers) {
const auto modifyChosenRow = [](
ChosenRow row,
Qt::KeyboardModifiers modifiers) {
#ifdef _DEBUG
row.newWindow = (modifiers & Qt::ControlModifier);
#endif
if (TabbedPanelShowOnClick.value()) {
row.newWindow = (modifiers & Qt::ControlModifier);
}
return row;
};
const auto chosen = modifyChosenRow(computeChosenRow(), modifiers);

View File

@ -45,6 +45,8 @@ class VideoUserpic;
namespace Dialogs {
extern const char kOptionCtrlClickChatNewWindow[];
class Row;
class FakeRow;
class IndexedList;

View File

@ -479,7 +479,11 @@ void Manager::applyListFilters() {
if (icon.premium
&& !_allowSendingPremium
&& !_buttonAlreadyList.contains(emoji)) {
showPremiumLock = &icon;
if (_premiumPossible) {
showPremiumLock = &icon;
} else {
clearStateForHidden(icon);
}
} else {
icon.premiumLock = false;
if (emoji == _favorite) {
@ -572,7 +576,10 @@ void Manager::showButtonDelayed() {
void Manager::applyList(
const std::vector<Data::Reaction> &list,
const QString &favorite) {
const QString &favorite,
bool premiumPossible) {
const auto possibleChanged = (_premiumPossible != premiumPossible);
_premiumPossible = premiumPossible;
const auto proj = [](const auto &obj) {
return std::tie(
obj.emoji,
@ -585,7 +592,7 @@ void Manager::applyList(
_favorite = favorite;
}
if (ranges::equal(_list, list, ranges::equal_to(), proj, proj)) {
if (favoriteChanged) {
if (favoriteChanged || possibleChanged) {
applyListFilters();
}
return;
@ -1659,7 +1666,8 @@ void SetupManagerList(
) | rpl::start_with_next([=] {
manager->applyList(
reactions->list(Data::Reactions::Type::Active),
reactions->favorite());
reactions->favorite(),
session->premiumPossible());
}, manager->lifetime());
std::move(

View File

@ -147,7 +147,8 @@ public:
void applyList(
const std::vector<Data::Reaction> &list,
const QString &favorite);
const QString &favorite,
bool premiumPossible);
void updateAllowedSublist(AllowedSublist filter);
void updateAllowSendingPremium(bool allow);
[[nodiscard]] const AllowedSublist &allowedSublist() const;
@ -348,6 +349,7 @@ private:
rpl::lifetime _loadCacheLifetime;
bool _showingAll = false;
bool _allowSendingPremium = false;
bool _premiumPossible = false;
mutable int _selectedIcon = -1;
std::optional<ButtonParameters> _scheduledParameters;

View File

@ -84,6 +84,7 @@ TopBarWidget::TopBarWidget(
not_null<Window::SessionController*> controller)
: RpWidget(parent)
, _controller(controller)
, _primaryWindow(controller->isPrimary())
, _clear(this, tr::lng_selected_clear(), st::topBarClearButton)
, _forward(this, tr::lng_selected_forward(), st::defaultActiveButton)
, _sendNow(this, tr::lng_selected_send_now(), st::defaultActiveButton)
@ -846,10 +847,10 @@ void TopBarWidget::updateControlsGeometry() {
_leftTaken = smallDialogsColumn ? (width() - _back->width()) / 2 : 0;
_back->moveToLeft(_leftTaken, otherButtonsTop);
_leftTaken += _back->width();
if (_info && !_info->isHidden()) {
_info->moveToLeft(_leftTaken, otherButtonsTop);
_leftTaken += _info->width();
}
}
if (_info && !_info->isHidden()) {
_info->moveToLeft(_leftTaken, otherButtonsTop);
_leftTaken += _info->width();
}
_rightTaken = 0;
@ -908,7 +909,8 @@ void TopBarWidget::updateControlsVisibility() {
_back->setVisible(backVisible && !_chooseForReportReason);
_cancelChoose->setVisible(_chooseForReportReason.has_value());
if (_info) {
_info->setVisible(isOneColumn && !_chooseForReportReason);
_info->setVisible((isOneColumn || !_primaryWindow)
&& !_chooseForReportReason);
}
if (_unreadBadge) {
_unreadBadge->setVisible(!_chooseForReportReason);

View File

@ -153,6 +153,7 @@ private:
[[nodiscard]] bool showSelectedActions() const;
const not_null<Window::SessionController*> _controller;
const bool _primaryWindow = false;
ActiveChat _activeChat;
QString _customTitleText;
std::unique_ptr<EmojiInteractionSeenAnimation> _emojiInteractionSeen;

View File

@ -1912,7 +1912,7 @@ QPixmap MainWidget::grabForShowAnimation(const Window::SectionSlideParams &param
result = Ui::GrabWidget(this, QRect(
0,
sectionTop,
_dialogsWidth,
width(),
height() - sectionTop));
} else {
if (_sideShadow) {
@ -2163,7 +2163,7 @@ void MainWidget::updateControlsGeometry() {
auto dialogsWidth = _dialogs
? qRound(_a_dialogsWidth.value(_dialogsWidth))
: isOneColumn()
? _dialogsWidth
? width()
: 0;
if (isOneColumn()) {
if (_callTopBar) {
@ -2276,7 +2276,7 @@ void MainWidget::updateControlsGeometry() {
}
void MainWidget::refreshResizeAreas() {
if (!isOneColumn()) {
if (!isOneColumn() && _dialogs) {
ensureFirstColumnResizeAreaCreated();
_firstColumnResizeArea->setGeometryToLeft(
_history->x(),
@ -2314,6 +2314,8 @@ void MainWidget::createResizeArea(
}
void MainWidget::ensureFirstColumnResizeAreaCreated() {
Expects(_dialogs != nullptr);
if (_firstColumnResizeArea) {
return;
}

View File

@ -127,20 +127,22 @@ void MainWindow::applyInitialWorkMode() {
const auto workMode = Core::App().settings().workMode();
workmodeUpdated(workMode);
if (Core::App().settings().windowPosition().maximized) {
DEBUG_LOG(("Window Pos: First show, setting maximized."));
setWindowState(Qt::WindowMaximized);
}
if (cStartInTray()
|| (cLaunchMode() == LaunchModeAutoStart
&& cStartMinimized()
&& !Core::App().passcodeLocked())) {
DEBUG_LOG(("Window Pos: First show, setting minimized after."));
if (workMode == Core::Settings::WorkMode::TrayOnly
|| workMode == Core::Settings::WorkMode::WindowAndTray) {
hide();
} else {
setWindowState(windowState() | Qt::WindowMinimized);
if (controller().isPrimary()) {
if (Core::App().settings().windowPosition().maximized) {
DEBUG_LOG(("Window Pos: First show, setting maximized."));
setWindowState(Qt::WindowMaximized);
}
if (!cStartInTray()
|| (cLaunchMode() == LaunchModeAutoStart
&& cStartMinimized()
&& !Core::App().passcodeLocked())) {
DEBUG_LOG(("Window Pos: First show, setting minimized after."));
if (workMode == Core::Settings::WorkMode::TrayOnly
|| workMode == Core::Settings::WorkMode::WindowAndTray) {
hide();
} else {
setWindowState(windowState() | Qt::WindowMinimized);
}
}
}
setPositionInited();
@ -645,22 +647,28 @@ void MainWindow::closeEvent(QCloseEvent *e) {
if (Core::Sandbox::Instance().isSavingSession() || Core::Quitting()) {
e->accept();
Core::Quit();
} else {
e->ignore();
const auto hasAuth = [&] {
if (!Core::App().domain().started()) {
return false;
}
for (const auto &[_, account] : Core::App().domain().accounts()) {
if (account->sessionExists()) {
return true;
}
}
return;
} else if (!isPrimary()) {
e->accept();
crl::on_main(this, [=] {
Core::App().closeWindow(&controller());
});
return;
}
e->ignore();
const auto hasAuth = [&] {
if (!Core::App().domain().started()) {
return false;
}();
if (!hasAuth || !hideNoQuit()) {
Core::Quit();
}
for (const auto &[_, account] : Core::App().domain().accounts()) {
if (account->sessionExists()) {
return true;
}
}
return false;
}();
if (!hasAuth || !hideNoQuit()) {
Core::Quit();
}
}

View File

@ -768,7 +768,7 @@ TimeId CalculateOnlineTill(not_null<PeerData*> peer) {
return;
}
const auto active = Core::App().activeWindow();
const auto active = Core::App().primaryWindow();
const auto controller = active ? active->sessionController() : nullptr;
const auto openFolder = [=] {
const auto folder = _session->data().folderLoaded(Data::Folder::kId);

View File

@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "base/options.h"
#include "core/application.h"
#include "chat_helpers/tabbed_panel.h"
#include "dialogs/dialogs_inner_widget.h"
#include "history/history_widget.h"
#include "lang/lang_keys.h"
#include "media/player/media_player_instance.h"
@ -134,6 +135,7 @@ void SetupExperimental(
addToggle(ChatHelpers::kOptionTabbedPanelShowOnClick);
addToggle(Window::kOptionViewProfileInChatsListContextMenu);
addToggle(Dialogs::kOptionCtrlClickChatNewWindow);
addToggle(Ui::GL::kOptionAllowLinuxNvidiaOpenGL);
addToggle(Media::Player::kOptionDisableAutoplayNext);
addToggle(Settings::kOptionMonoSettingsIcons);

View File

@ -489,7 +489,7 @@ void MainWindow::handleStateChanged(Qt::WindowState state) {
void MainWindow::handleActiveChanged() {
if (isActiveWindow()) {
Core::App().checkMediaViewActivation();
Core::App().windowActivated(&controller());
}
}
@ -737,10 +737,9 @@ void MainWindow::initGeometry() {
if (initGeometryFromSystem()) {
return;
}
// #TODO windows
const auto geometry = countInitialGeometry(isPrimary()
? positionFromSettings()
: Core::WindowPosition());
: SecondaryInitPosition());
DEBUG_LOG(("Window Pos: Setting first %1, %2, %3, %4"
).arg(geometry.x()
).arg(geometry.y()
@ -836,30 +835,7 @@ void MainWindow::savePosition(Qt::WindowState state) {
realPosition.moncrc = 0;
DEBUG_LOG(("Window Pos: Saving non-maximized position: %1, %2, %3, %4").arg(realPosition.x).arg(realPosition.y).arg(realPosition.w).arg(realPosition.h));
auto centerX = realPosition.x + realPosition.w / 2;
auto centerY = realPosition.y + realPosition.h / 2;
int minDelta = 0;
QScreen *chosen = nullptr;
const auto screens = QGuiApplication::screens();
for (auto screen : screens) {
auto delta = (screen->geometry().center() - QPoint(centerX, centerY)).manhattanLength();
if (!chosen || delta < minDelta) {
minDelta = delta;
chosen = screen;
}
}
if (chosen) {
auto screenGeometry = chosen->geometry();
DEBUG_LOG(("Window Pos: Screen found, geometry: %1, %2, %3, %4"
).arg(screenGeometry.x()
).arg(screenGeometry.y()
).arg(screenGeometry.width()
).arg(screenGeometry.height()));
realPosition.x -= screenGeometry.x();
realPosition.y -= screenGeometry.y();
realPosition.moncrc = screenNameChecksum(chosen->name());
}
realPosition = withScreenInPosition(realPosition);
}
if (realPosition.w >= st::windowMinWidth && realPosition.h >= st::windowMinHeight) {
if (realPosition.x != savedPosition.x
@ -882,6 +858,51 @@ void MainWindow::savePosition(Qt::WindowState state) {
}
}
Core::WindowPosition MainWindow::withScreenInPosition(
Core::WindowPosition position) const {
auto centerX = position.x + position.w / 2;
auto centerY = position.y + position.h / 2;
int minDelta = 0;
QScreen *chosen = nullptr;
const auto screens = QGuiApplication::screens();
for (auto screen : screens) {
auto delta = (screen->geometry().center() - QPoint(centerX, centerY)).manhattanLength();
if (!chosen || delta < minDelta) {
minDelta = delta;
chosen = screen;
}
}
if (!chosen) {
return position;
}
auto screenGeometry = chosen->geometry();
DEBUG_LOG(("Window Pos: Screen found, geometry: %1, %2, %3, %4"
).arg(screenGeometry.x()
).arg(screenGeometry.y()
).arg(screenGeometry.width()
).arg(screenGeometry.height()));
position.x -= screenGeometry.x();
position.y -= screenGeometry.y();
position.moncrc = screenNameChecksum(chosen->name());
return position;
}
Core::WindowPosition MainWindow::SecondaryInitPosition() {
const auto active = Core::App().activeWindow();
if (!active) {
return {};
}
const auto geometry = active->widget()->geometry();
const auto skip = st::windowMinWidth / 6;
return active->widget()->withScreenInPosition({
.scale = cScale(),
.x = geometry.x() + skip,
.y = geometry.y() + skip,
.w = st::windowMinWidth,
.h = st::windowDefaultHeight,
});
}
bool MainWindow::minimizeToTray() {
if (Core::Quitting()/* || !hasTrayIcon()*/) {
return false;

View File

@ -75,6 +75,9 @@ public:
void activate();
[[nodiscard]] QRect desktopRect() const;
[[nodiscard]] Core::WindowPosition withScreenInPosition(
Core::WindowPosition position) const;
[[nodiscard]] static Core::WindowPosition SecondaryInitPosition();
void init();

View File

@ -96,6 +96,10 @@ void Controller::showAccount(
_account->sessionValue(
) | rpl::start_with_next([=](Main::Session *session) {
if (!session && !isPrimary()) {
Core::App().closeWindow(this);
return;
}
const auto was = base::take(_sessionController);
_sessionController = session
? std::make_unique<SessionController>(session, this)
@ -119,9 +123,6 @@ void Controller::showAccount(
}, _sessionController->lifetime());
widget()->setInnerFocus();
} else if (!isPrimary()) {
// #TODO windows test
close();
} else {
setupIntro();
_widget.updateGlobalMenu();
@ -230,7 +231,7 @@ void Controller::showTermsDelete() {
if (const auto session = account().maybeSession()) {
session->termsDeleteNow();
} else {
Ui::hideLayer();
hideLayer();
}
};
show(
@ -327,6 +328,10 @@ void Controller::showRightColumn(object_ptr<TWidget> widget) {
_widget.showRightColumn(std::move(widget));
}
void Controller::hideLayer(anim::type animated) {
_widget.ui_showBox({ nullptr }, Ui::LayerOption::CloseOther, animated);
}
void Controller::hideSettingsAndLayer(anim::type animated) {
_widget.ui_hideSettingsAndLayer(animated);
}

View File

@ -83,6 +83,7 @@ public:
void showRightColumn(object_ptr<TWidget> widget);
void hideLayer(anim::type animated = anim::type::normal);
void hideSettingsAndLayer(anim::type animated = anim::type::normal);
void activate();

View File

@ -243,7 +243,7 @@ void SessionNavigation::resolveDone(
const MTPcontacts_ResolvedPeer &result,
Fn<void(not_null<PeerData*>)> done) {
_resolveRequestId = 0;
Ui::hideLayer();
parentController()->hideLayer();
result.match([&](const MTPDcontacts_resolvedPeer &data) {
_session->data().processUsers(data.vusers());
_session->data().processChats(data.vchats());
@ -598,6 +598,7 @@ SessionController::SessionController(
, _window(window)
, _emojiInteractions(
std::make_unique<ChatHelpers::EmojiInteractions>(session))
, _isPrimary(window->isPrimary())
, _sendingAnimation(
std::make_unique<Ui::MessageSendingAnimationController>(this))
, _tabbedSelector(
@ -700,7 +701,7 @@ PeerData *SessionController::singlePeer() const {
}
bool SessionController::isPrimary() const {
return _window->isPrimary();
return _isPrimary;
}
not_null<::MainWindow*> SessionController::widget() const {
@ -762,7 +763,7 @@ void SessionController::initSupportMode() {
}
void SessionController::toggleFiltersMenu(bool enabled) {
if (!isPrimary() || (!enabled == !_filters)) {
if (!_isPrimary || (!enabled == !_filters)) {
return;
} else if (enabled) {
_filters = std::make_unique<FiltersMenu>(
@ -1009,7 +1010,7 @@ int SessionController::dialogsSmallColumnWidth() const {
}
int SessionController::minimalThreeColumnWidth() const {
return st::columnMinimalWidthLeft
return (_isPrimary ? st::columnMinimalWidthLeft : 0)
+ st::columnMinimalWidthMain
+ st::columnMinimalWidthThird;
}
@ -1032,7 +1033,7 @@ auto SessionController::computeColumnLayout() const -> ColumnLayout {
auto useOneColumnLayout = [&] {
auto minimalNormal = st::columnMinimalWidthLeft
+ st::columnMinimalWidthMain;
if (bodyWidth < minimalNormal) {
if (_isPrimary && bodyWidth < minimalNormal) {
return true;
}
return false;
@ -1074,6 +1075,9 @@ auto SessionController::computeColumnLayout() const -> ColumnLayout {
}
int SessionController::countDialogsWidthFromRatio(int bodyWidth) const {
if (!_isPrimary) {
return 0;
}
auto result = qRound(bodyWidth * Core::App().settings().dialogsWidthRatio());
accumulate_max(result, st::columnMinimalWidthLeft);
// accumulate_min(result, st::columnMaximalWidthLeft);
@ -1102,8 +1106,8 @@ SessionController::ShrinkResult SessionController::shrinkDialogsAndThirdColumns(
if (thirdWidthNew < st::columnMinimalWidthThird) {
thirdWidthNew = st::columnMinimalWidthThird;
dialogsWidthNew = bodyWidth - thirdWidthNew - chatWidth;
Assert(dialogsWidthNew >= st::columnMinimalWidthLeft);
} else if (dialogsWidthNew < st::columnMinimalWidthLeft) {
Assert(!_isPrimary || dialogsWidthNew >= st::columnMinimalWidthLeft);
} else if (_isPrimary && dialogsWidthNew < st::columnMinimalWidthLeft) {
dialogsWidthNew = st::columnMinimalWidthLeft;
thirdWidthNew = bodyWidth - dialogsWidthNew - chatWidth;
Assert(thirdWidthNew >= st::columnMinimalWidthThird);
@ -1240,8 +1244,8 @@ void SessionController::startOrJoinGroupCall(
const auto askConfirmation = [&](QString text, QString button) {
show(Ui::MakeConfirmBox({
.text = text,
.confirmed = crl::guard(this, [=, hash = args.joinHash] {
Ui::hideLayer();
.confirmed = crl::guard(this,[=, hash = args.joinHash] {
hideLayer();
startOrJoinGroupCall(peer, { hash, JoinConfirm::None });
}),
.confirmText = button,
@ -1597,7 +1601,7 @@ QPointer<Ui::BoxContent> SessionController::show(
}
void SessionController::hideLayer(anim::type animated) {
show({ nullptr }, Ui::LayerOption::CloseOther, animated);
_window->hideLayer(animated);
}
void SessionController::openPhoto(

View File

@ -554,6 +554,7 @@ private:
const not_null<Controller*> _window;
const std::unique_ptr<ChatHelpers::EmojiInteractions> _emojiInteractions;
const bool _isPrimary = false;
using SendingAnimation = Ui::MessageSendingAnimationController;
const std::unique_ptr<SendingAnimation> _sendingAnimation;