From 7f70ee122779634c1a53b03a3b2f296b7dc71c54 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Mon, 2 Sep 2024 20:14:33 +0300 Subject: [PATCH] Added initial ability to reply with left swipe. --- .../history/history_inner_widget.cpp | 81 ++++++++- .../history/history_inner_widget.h | 3 + .../history/history_view_swipe.cpp | 164 ++++++++++++++++++ .../SourceFiles/history/history_view_swipe.h | 30 ++++ .../history/history_view_swipe_data.h | 19 ++ .../history/view/history_view_element.cpp | 7 + .../history/view/history_view_message.cpp | 41 +++++ .../history_view_reactions_button.cpp | 4 + Telegram/SourceFiles/ui/chat/chat_style.h | 2 + Telegram/cmake/td_ui.cmake | 3 + Telegram/lib_base | 2 +- 11 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 Telegram/SourceFiles/history/history_view_swipe.cpp create mode 100644 Telegram/SourceFiles/history/history_view_swipe.h create mode 100644 Telegram/SourceFiles/history/history_view_swipe_data.h diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index d682fc81c..370a70b0e 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_emoji_interactions.h" #include "history/history_item_components.h" #include "history/history_item_text.h" +#include "history/history_view_swipe.h" #include "payments/payments_reaction_process.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/menu/menu_multiline_action.h" @@ -123,6 +124,15 @@ int BinarySearchBlocksOrItems(const T &list, int edge) { return start; } +[[nodiscard]] bool CanSendReply(not_null item) { + const auto peer = item->history()->peer; + const auto topic = item->topic(); + return topic + ? Data::CanSendAnything(topic) + : (Data::CanSendAnything(peer) + && (!peer->isChannel() || peer->asChannel()->amIn())); +} + void FillSponsoredMessagesMenu( not_null controller, FullMsgId itemId, @@ -373,6 +383,59 @@ HistoryInner::HistoryInner( _migrated->delegateMixin()->setCurrent(this); _migrated->translateTo(_history->translatedTo()); } + HistoryView::SetupSwipeHandler(this, _scroll, [=]( + HistoryView::ChatPaintGestureHorizontalData data) { + _gestureHorizontal = data; + update(); + }, [=, show = controller->uiShow()](int cursorTop) { + auto result = HistoryView::SwipeHandlerFinishData(); + if (inSelectionMode()) { + return result; + } + enumerateItems([&]( + not_null view, + int itemtop, + int itembottom) { + if ((cursorTop < itemtop) + || (cursorTop > itembottom) + || !view->data()->isRegular() + || view->data()->isService()) { + return true; + } + const auto item = view->data(); + const auto canSendReply = CanSendReply(item); + const auto canReply = (canSendReply || item->allowsForward()); + if (!canReply) { + return true; + } + result.msgBareId = item->fullId().msg.bare; + result.callback = [=, itemId = item->fullId()] { + const auto still = show->session().data().message(itemId); + const auto selected = selectedQuote(still); + const auto replyToItemId = (selected.item + ? selected.item + : still)->fullId(); + if (canSendReply) { + _widget->replyToMessage({ + .messageId = replyToItemId, + .quote = selected.text, + .quoteOffset = selected.offset, + }); + if (!selected.text.empty()) { + _widget->clearSelected(); + } + } else { + HistoryView::Controls::ShowReplyToChatBox(show, { + .messageId = replyToItemId, + .quote = selected.text, + .quoteOffset = selected.offset, + }); + } + }; + return false; + }); + return result; + }); Window::ChatThemeValueFromPeer( controller, @@ -944,6 +1007,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) { auto clip = e->rect(); auto context = preparePaintContext(clip); + context.gestureHorizontal = _gestureHorizontal; context.highlightPathCache = &_highlightPathCache; _pathGradient->startFrame( 0, @@ -1157,6 +1221,11 @@ void HistoryInner::paintEvent(QPaintEvent *e) { // paint the userpic if it intersects the painted rect if (userpicTop + st::msgPhotoSize > clip.top()) { const auto item = view->data(); + const auto hasTranslation = _gestureHorizontal.ratio + && (_gestureHorizontal.msgBareId == item->fullId().msg.bare); + if (hasTranslation) { + p.translate(_gestureHorizontal.translation, 0); + } if (const auto from = item->displayFrom()) { Dialogs::Ui::PaintUserpic( p, @@ -1192,6 +1261,9 @@ void HistoryInner::paintEvent(QPaintEvent *e) { } else { Unexpected("Corrupt forwarded information in message."); } + if (hasTranslation) { + p.translate(-_gestureHorizontal.translation, 0); + } } return true; }); @@ -2461,14 +2533,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (!item || !item->isRegular()) { return; } - const auto canSendReply = [&] { - const auto peer = item->history()->peer; - const auto topic = item->topic(); - return topic - ? Data::CanSendAnything(topic) - : (Data::CanSendAnything(peer) - && (!peer->isChannel() || peer->asChannel()->amIn())); - }(); + const auto canSendReply = CanSendReply(item); const auto canReply = canSendReply || item->allowsForward(); if (canReply) { const auto selected = selectedQuote(item); diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 5a11b2944..2507a0921 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/dragging_scroll_manager.h" #include "ui/widgets/tooltip.h" #include "ui/widgets/scroll_area.h" +#include "history/history_view_swipe_data.h" #include "history/view/history_view_top_bar_widget.h" #include @@ -526,6 +527,8 @@ private: crl::time _touchTime = 0; base::Timer _touchScrollTimer; + HistoryView::ChatPaintGestureHorizontalData _gestureHorizontal; + // _menu must be destroyed before _whoReactedMenuLifetime. rpl::lifetime _whoReactedMenuLifetime; base::unique_qptr _menu; diff --git a/Telegram/SourceFiles/history/history_view_swipe.cpp b/Telegram/SourceFiles/history/history_view_swipe.cpp new file mode 100644 index 000000000..7a254c51a --- /dev/null +++ b/Telegram/SourceFiles/history/history_view_swipe.cpp @@ -0,0 +1,164 @@ +/* +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 "history/history_view_swipe.h" + +#include "base/event_filter.h" +#include "base/platform/base_platform_haptic.h" +#include "history/history_view_swipe_data.h" +#include "ui/chat/chat_style.h" +#include "ui/ui_utility.h" +#include "ui/widgets/scroll_area.h" + +#include + +namespace HistoryView { + +void SetupSwipeHandler( + not_null widget, + not_null scroll, + Fn update, + Fn generateFinishByTop) { + constexpr auto kThresholdWidth = 50; + const auto threshold = style::ConvertFloatScale(kThresholdWidth); + struct State { + base::unique_qptr filter; + Ui::Animations::Simple animationEnd; + SwipeHandlerFinishData finishByTopData; + std::optional orientation; + QPointF startAt; + QPointF lastAt; + int cursorTop = 0; + bool reached = false; + + rpl::lifetime lifetime; + }; + const auto state = widget->lifetime().make_state(); + const auto updateRatio = [=](float64 ratio) { + update({ + .ratio = std::clamp(ratio, 0., 1.), + .translation = (-std::clamp(ratio, 0., 1.5) * threshold), + .msgBareId = state->finishByTopData.msgBareId, + .cursorTop = state->cursorTop, + }); + }; + const auto setOrientation = [=](const std::optional &o) { + state->orientation = o; + const auto isHorizontal = o.value_or(Qt::Vertical) == Qt::Horizontal; + scroll->viewport()->setAttribute( + Qt::WA_AcceptTouchEvents, + !isHorizontal); + scroll->disableScroll(isHorizontal); + }; + const auto processEnd = [=](QTouchEvent *t) { + if (state->orientation) { + if ((*state->orientation) == Qt::Horizontal) { + if (t && t->touchPoints().size() > 0) { + state->lastAt = t->touchPoints().at(0).pos(); + } + const auto delta = state->startAt - state->lastAt; + const auto ratio = delta.x() / threshold; + if ((ratio >= 1) && state->finishByTopData.callback) { + Ui::PostponeCall( + widget, + state->finishByTopData.callback); + } + state->animationEnd.stop(); + state->animationEnd.start( + updateRatio, + ratio, + 0., + st::slideWrapDuration); + } + } + setOrientation(std::nullopt); + state->startAt = QPointF(); + state->reached = false; + }; + scroll->scrolls() | rpl::start_with_next([=] { + processEnd(nullptr); + }, state->lifetime); + const auto filter = [=](not_null e) { + if (e->type() == QEvent::Leave && state->orientation) { + processEnd(nullptr); + } + if (e->type() == QEvent::MouseMove && state->orientation) { + const auto m = static_cast(e.get()); + if (std::abs(m->pos().y() - state->cursorTop) + > QApplication::startDragDistance()) { + processEnd(nullptr); + } + } + if (e->type() == QEvent::TouchBegin + || e->type() == QEvent::TouchUpdate + || e->type() == QEvent::TouchEnd + || e->type() == QEvent::TouchCancel) { + const auto t = static_cast(e.get()); + const auto &touches = t->touchPoints(); + const auto anyReleased = (touches.size() == 2) + ? ((touches.at(0).state() & Qt::TouchPointReleased) + + (touches.at(1).state() & Qt::TouchPointReleased)) + : (touches.size() == 1) + ? (touches.at(0).state() & Qt::TouchPointReleased) + : 0; + if (touches.size() == 2) { + if ((e->type() == QEvent::TouchBegin) + || (e->type() == QEvent::TouchUpdate)) { + if (state->startAt.isNull()) { + state->startAt = touches.at(0).pos(); + state->cursorTop = widget->mapFromGlobal( + QCursor::pos()).y(); + state->finishByTopData = generateFinishByTop( + state->cursorTop); + if (!state->finishByTopData.callback) { + setOrientation(Qt::Vertical); + } + } else if (state->orientation) { + if ((*state->orientation) == Qt::Horizontal) { + state->lastAt = touches.at(0).pos(); + const auto delta = state->startAt - state->lastAt; + const auto ratio = delta.x() / threshold; + updateRatio(ratio); + constexpr auto kResetReachedOn = 0.95; + if (!state->reached && ratio >= 1.) { + state->reached = true; + base::Platform::Haptic(); + } else if (state->reached + && ratio < kResetReachedOn) { + state->reached = false; + } + } + } else { + state->lastAt = touches.at(0).pos(); + const auto delta = state->startAt - state->lastAt; + const auto diffXtoY = std::abs(delta.x()) + - std::abs(delta.y()); + if (diffXtoY > 0) { + setOrientation(Qt::Horizontal); + } else if (diffXtoY < 0) { + setOrientation(Qt::Vertical); + } else { + setOrientation(std::nullopt); + } + } + } + } + if ((e->type() == QEvent::TouchEnd) + || touches.empty() + || anyReleased + || (touches.size() > 2)) { + processEnd(t); + } + return base::EventFilterResult::Cancel; + } + return base::EventFilterResult::Continue; + }; + state->filter = base::make_unique_q( + base::install_event_filter(widget, filter)); +} + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/history_view_swipe.h b/Telegram/SourceFiles/history/history_view_swipe.h new file mode 100644 index 000000000..e7fb2ea4a --- /dev/null +++ b/Telegram/SourceFiles/history/history_view_swipe.h @@ -0,0 +1,30 @@ +/* +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 +*/ +#pragma once + +namespace Ui { +class RpWidget; +class ScrollArea; +} // namespace Ui + +namespace HistoryView { + +struct ChatPaintGestureHorizontalData; + +struct SwipeHandlerFinishData { + Fn callback; + int64 msgBareId = 0; +}; + +void SetupSwipeHandler( + not_null widget, + not_null scroll, + Fn update, + Fn generateFinishByTop); + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/history_view_swipe_data.h b/Telegram/SourceFiles/history/history_view_swipe_data.h new file mode 100644 index 000000000..c42b146b8 --- /dev/null +++ b/Telegram/SourceFiles/history/history_view_swipe_data.h @@ -0,0 +1,19 @@ +/* +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 +*/ +#pragma once + +namespace HistoryView { + +struct ChatPaintGestureHorizontalData { + float64 ratio = 0.; + float64 translation = 0.; + int64 msgBareId = 0; + int cursorTop = 0; +}; + +} // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_element.cpp b/Telegram/SourceFiles/history/view/history_view_element.cpp index d36c32a2c..883b51a5a 100644 --- a/Telegram/SourceFiles/history/view/history_view_element.cpp +++ b/Telegram/SourceFiles/history/view/history_view_element.cpp @@ -315,6 +315,10 @@ void UnreadBar::paint( int y, int w, bool chatWide) const { + const auto previousTranslation = p.transform().dx(); + if (previousTranslation != 0) { + p.translate(-previousTranslation, 0); + } const auto st = context.st; const auto bottom = y + height(); y += marginTop(); @@ -350,6 +354,9 @@ void UnreadBar::paint( (w - width) / 2, y + (skip / 2) + st::historyUnreadBarFont->ascent, text); + if (previousTranslation != 0) { + p.translate(previousTranslation, 0); + } } void DateBadge::init(const QString &date) { diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp index 146f86894..b5b0dea66 100644 --- a/Telegram/SourceFiles/history/view/history_view_message.cpp +++ b/Telegram/SourceFiles/history/view/history_view_message.cpp @@ -1091,6 +1091,12 @@ void Message::draw(Painter &p, const PaintContext &context) const { const auto item = data(); const auto media = this->media(); + const auto hasGesture = context.gestureHorizontal.ratio + && (context.gestureHorizontal.msgBareId == item->fullId().msg.bare); + if (hasGesture) { + p.translate(context.gestureHorizontal.translation, 0); + } + if (item->hasUnrequestedFactcheck()) { item->history()->session().factchecks().requestFor(item); } @@ -1481,6 +1487,41 @@ void Message::draw(Painter &p, const PaintContext &context) const { } } } + if (hasGesture) { + p.translate(-context.gestureHorizontal.translation, 0); + + constexpr auto kShiftRatio = 1.5; + const auto size = st::historyFastShareSize; + const auto rect = QRect( + width() - (size * kShiftRatio) * context.gestureHorizontal.ratio, + g.y() + (g.height() - size) / 2, + size, + size); + const auto center = rect::center(rect); + const auto spanAngle = -context.gestureHorizontal.ratio + * arc::kFullLength; + const auto strokeWidth = style::ConvertFloatScale(2.); + auto pen = QPen(context.st->msgServiceBg()); + pen.setWidthF(strokeWidth); + const auto arcRect = rect - Margins(strokeWidth); + p.save(); + { + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + p.setBrush(context.st->msgServiceBg()); + p.setOpacity(context.gestureHorizontal.ratio); + p.drawEllipse(rect); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawArc(arcRect, arc::kQuarterLength, spanAngle); + p.drawArc(arcRect, arc::kQuarterLength, spanAngle); + p.translate(center); + p.scale(-1., 1.); + p.translate(-center); + context.st->historyFastShareIcon().paintInCenter(p, rect); + } + p.restore(); + } } void Message::paintCommentsButton( diff --git a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp index 4464e9546..9f2759f36 100644 --- a/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp +++ b/Telegram/SourceFiles/history/view/reactions/history_view_reactions_button.cpp @@ -488,6 +488,10 @@ void Manager::paint(QPainter &p, const PaintContext &context) { paintButton(p, context, button.get()); } if (const auto current = _button.get()) { + if (context.gestureHorizontal.ratio) { + current->applyState(ButtonState::Hidden); + _buttonHiding.push_back(std::move(_button)); + } paintButton(p, context, current); } diff --git a/Telegram/SourceFiles/ui/chat/chat_style.h b/Telegram/SourceFiles/ui/chat/chat_style.h index a00fe941a..b66cf814b 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.h +++ b/Telegram/SourceFiles/ui/chat/chat_style.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/message_bubble.h" #include "ui/chat/chat_style_radius.h" #include "ui/style/style_core_palette.h" +#include "history/history_view_swipe_data.h" #include "layout/layout_selection.h" #include "styles/style_basic.h" @@ -164,6 +165,7 @@ struct ChatPaintContext { QPainterPath *highlightPathCache = nullptr; mutable QRect highlightInterpolateTo; crl::time now = 0; + HistoryView::ChatPaintGestureHorizontalData gestureHorizontal; void translate(int x, int y) { viewport.translate(x, y); diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 00871161d..2eef94d8e 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -127,6 +127,9 @@ PRIVATE history/admin_log/history_admin_log_filter_value.h history/history_view_top_toast.cpp history/history_view_top_toast.h + history/history_view_swipe.cpp + history/history_view_swipe.h + history/history_view_swipe_data.h history/view/controls/history_view_characters_limit.cpp history/view/controls/history_view_characters_limit.h history/view/controls/history_view_voice_record_button.cpp diff --git a/Telegram/lib_base b/Telegram/lib_base index 601c20431..547e7f291 160000 --- a/Telegram/lib_base +++ b/Telegram/lib_base @@ -1 +1 @@ -Subproject commit 601c20431cc3f91de01e1b13a033e0a41cd36353 +Subproject commit 547e7f2914d9b5548dd17e70a3a7bf5d6606afc3