mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-02-24 09:16:57 +00:00
Added initial ability to reply with left swipe.
This commit is contained in:
parent
c9f7da6e82
commit
7f70ee1227
@ -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<const HistoryItem*> 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<Window::SessionController*> 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<EnumItemsDirection::BottomToTop>([&](
|
||||
not_null<Element*> 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);
|
||||
|
@ -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 <QtGui/QPainterPath>
|
||||
@ -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<Ui::PopupMenu> _menu;
|
||||
|
164
Telegram/SourceFiles/history/history_view_swipe.cpp
Normal file
164
Telegram/SourceFiles/history/history_view_swipe.cpp
Normal file
@ -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 <QtWidgets/QApplication>
|
||||
|
||||
namespace HistoryView {
|
||||
|
||||
void SetupSwipeHandler(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<Ui::ScrollArea*> scroll,
|
||||
Fn<void(ChatPaintGestureHorizontalData)> update,
|
||||
Fn<SwipeHandlerFinishData(int)> generateFinishByTop) {
|
||||
constexpr auto kThresholdWidth = 50;
|
||||
const auto threshold = style::ConvertFloatScale(kThresholdWidth);
|
||||
struct State {
|
||||
base::unique_qptr<QObject> filter;
|
||||
Ui::Animations::Simple animationEnd;
|
||||
SwipeHandlerFinishData finishByTopData;
|
||||
std::optional<Qt::Orientation> orientation;
|
||||
QPointF startAt;
|
||||
QPointF lastAt;
|
||||
int cursorTop = 0;
|
||||
bool reached = false;
|
||||
|
||||
rpl::lifetime lifetime;
|
||||
};
|
||||
const auto state = widget->lifetime().make_state<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<Qt::Orientation> &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<QEvent*> e) {
|
||||
if (e->type() == QEvent::Leave && state->orientation) {
|
||||
processEnd(nullptr);
|
||||
}
|
||||
if (e->type() == QEvent::MouseMove && state->orientation) {
|
||||
const auto m = static_cast<QMouseEvent*>(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<QTouchEvent*>(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<QObject>(
|
||||
base::install_event_filter(widget, filter));
|
||||
}
|
||||
|
||||
} // namespace HistoryView
|
30
Telegram/SourceFiles/history/history_view_swipe.h
Normal file
30
Telegram/SourceFiles/history/history_view_swipe.h
Normal file
@ -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<void(void)> callback;
|
||||
int64 msgBareId = 0;
|
||||
};
|
||||
|
||||
void SetupSwipeHandler(
|
||||
not_null<Ui::RpWidget*> widget,
|
||||
not_null<Ui::ScrollArea*> scroll,
|
||||
Fn<void(ChatPaintGestureHorizontalData)> update,
|
||||
Fn<SwipeHandlerFinishData(int)> generateFinishByTop);
|
||||
|
||||
} // namespace HistoryView
|
19
Telegram/SourceFiles/history/history_view_swipe_data.h
Normal file
19
Telegram/SourceFiles/history/history_view_swipe_data.h
Normal file
@ -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
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 601c20431cc3f91de01e1b13a033e0a41cd36353
|
||||
Subproject commit 547e7f2914d9b5548dd17e70a3a7bf5d6606afc3
|
Loading…
Reference in New Issue
Block a user