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<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);
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 <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;
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 <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
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<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
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