mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +02:00
Allow choosing the link for the preview.
This commit is contained in:
parent
3b91e2dee4
commit
a197ed9e95
10 changed files with 588 additions and 351 deletions
|
@ -648,10 +648,6 @@ bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
|
||||||
return QObject::eventFilter(object, event);
|
return QObject::eventFilter(object, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rpl::variable<QStringList> &MessageLinksParser::list() const {
|
|
||||||
return _list;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MessageLinksParser::parse() {
|
void MessageLinksParser::parse() {
|
||||||
const auto &textWithTags = _field->getTextWithTags();
|
const auto &textWithTags = _field->getTextWithTags();
|
||||||
const auto &text = textWithTags.text;
|
const auto &text = textWithTags.text;
|
||||||
|
@ -781,7 +777,7 @@ void MessageLinksParser::parse() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const auto range = LinkRange {
|
const auto range = MessageLinkRange{
|
||||||
int(domainOffset),
|
int(domainOffset),
|
||||||
static_cast<int>(p - start - domainOffset),
|
static_cast<int>(p - start - domainOffset),
|
||||||
QString()
|
QString()
|
||||||
|
@ -802,7 +798,7 @@ void MessageLinksParser::parse() {
|
||||||
void MessageLinksParser::applyRanges(const QString &text) {
|
void MessageLinksParser::applyRanges(const QString &text) {
|
||||||
const auto count = int(_ranges.size());
|
const auto count = int(_ranges.size());
|
||||||
const auto current = _list.current();
|
const auto current = _list.current();
|
||||||
const auto computeLink = [&](const LinkRange &range) {
|
const auto computeLink = [&](const MessageLinkRange &range) {
|
||||||
return range.custom.isEmpty()
|
return range.custom.isEmpty()
|
||||||
? base::StringViewMid(text, range.start, range.length)
|
? base::StringViewMid(text, range.start, range.length)
|
||||||
: QStringView(range.custom);
|
: QStringView(range.custom);
|
||||||
|
|
|
@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/widgets/fields/input_field.h"
|
#include "base/qt/qt_compare.h"
|
||||||
#include "base/timer.h"
|
#include "base/timer.h"
|
||||||
#include "chat_helpers/compose/compose_features.h"
|
#include "chat_helpers/compose/compose_features.h"
|
||||||
|
#include "ui/widgets/fields/input_field.h"
|
||||||
|
|
||||||
#ifndef TDESKTOP_DISABLE_SPELLCHECK
|
#ifndef TDESKTOP_DISABLE_SPELLCHECK
|
||||||
#include "boxes/dictionaries_manager.h"
|
#include "boxes/dictionaries_manager.h"
|
||||||
|
@ -96,6 +97,19 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery(
|
||||||
not_null<const Ui::InputField*> field,
|
not_null<const Ui::InputField*> field,
|
||||||
ChatHelpers::ComposeFeatures features);
|
ChatHelpers::ComposeFeatures features);
|
||||||
|
|
||||||
|
struct MessageLinkRange {
|
||||||
|
int start = 0;
|
||||||
|
int length = 0;
|
||||||
|
QString custom;
|
||||||
|
|
||||||
|
friend inline auto operator<=>(
|
||||||
|
const MessageLinkRange&,
|
||||||
|
const MessageLinkRange&) = default;
|
||||||
|
friend inline bool operator==(
|
||||||
|
const MessageLinkRange&,
|
||||||
|
const MessageLinkRange&) = default;
|
||||||
|
};
|
||||||
|
|
||||||
class MessageLinksParser final : private QObject {
|
class MessageLinksParser final : private QObject {
|
||||||
public:
|
public:
|
||||||
MessageLinksParser(not_null<Ui::InputField*> field);
|
MessageLinksParser(not_null<Ui::InputField*> field);
|
||||||
|
@ -103,21 +117,12 @@ public:
|
||||||
void parseNow();
|
void parseNow();
|
||||||
void setDisabled(bool disabled);
|
void setDisabled(bool disabled);
|
||||||
|
|
||||||
struct LinkRange {
|
[[nodiscard]] const rpl::variable<QStringList> &list() const {
|
||||||
int start = 0;
|
return _list;
|
||||||
int length = 0;
|
}
|
||||||
QString custom;
|
[[nodiscard]] const std::vector<MessageLinkRange> &ranges() const {
|
||||||
|
return _ranges;
|
||||||
friend inline auto operator<=>(
|
}
|
||||||
const LinkRange&,
|
|
||||||
const LinkRange&) = default;
|
|
||||||
friend inline bool operator==(
|
|
||||||
const LinkRange&,
|
|
||||||
const LinkRange&) = default;
|
|
||||||
};
|
|
||||||
|
|
||||||
[[nodiscard]] const rpl::variable<QStringList> &list() const;
|
|
||||||
[[nodiscard]] const std::vector<LinkRange> &ranges() const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool eventFilter(QObject *object, QEvent *event) override;
|
bool eventFilter(QObject *object, QEvent *event) override;
|
||||||
|
@ -127,7 +132,7 @@ private:
|
||||||
|
|
||||||
not_null<Ui::InputField*> _field;
|
not_null<Ui::InputField*> _field;
|
||||||
rpl::variable<QStringList> _list;
|
rpl::variable<QStringList> _list;
|
||||||
std::vector<LinkRange> _ranges;
|
std::vector<MessageLinkRange> _ranges;
|
||||||
int _lastLength = 0;
|
int _lastLength = 0;
|
||||||
bool _disabled = false;
|
bool _disabled = false;
|
||||||
base::Timer _timer;
|
base::Timer _timer;
|
||||||
|
|
|
@ -2403,9 +2403,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||||
if (canReply) {
|
if (canReply) {
|
||||||
const auto itemId = item->fullId();
|
const auto itemId = item->fullId();
|
||||||
const auto quote = selectedQuote(item);
|
const auto quote = selectedQuote(item);
|
||||||
const auto text = quote.empty()
|
auto text = quote.empty()
|
||||||
? tr::lng_context_reply_msg(tr::now)
|
? tr::lng_context_reply_msg(tr::now)
|
||||||
: tr::lng_context_quote_and_reply(tr::now);
|
: tr::lng_context_quote_and_reply(tr::now);
|
||||||
|
text.replace('&', u"&&"_q);
|
||||||
_menu->addAction(text, [=] {
|
_menu->addAction(text, [=] {
|
||||||
if (canSendReply) {
|
if (canSendReply) {
|
||||||
_widget->replyToMessage({ itemId, quote });
|
_widget->replyToMessage({ itemId, quote });
|
||||||
|
|
|
@ -6284,21 +6284,26 @@ void HistoryWidget::editDraftOptions() {
|
||||||
}
|
}
|
||||||
_preview->apply(webpage);
|
_preview->apply(webpage);
|
||||||
};
|
};
|
||||||
|
const auto replyToId = reply.messageId;
|
||||||
const auto highlight = [=] {
|
const auto highlight = [=] {
|
||||||
controller()->showPeerHistory(
|
controller()->showPeerHistory(
|
||||||
reply.messageId.peer,
|
replyToId.peer,
|
||||||
Window::SectionShow::Way::Forward,
|
Window::SectionShow::Way::Forward,
|
||||||
reply.messageId.msg);
|
replyToId.msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
using namespace HistoryView::Controls;
|
using namespace HistoryView::Controls;
|
||||||
EditDraftOptions(
|
EditDraftOptions({
|
||||||
controller()->uiShow(),
|
.show = controller()->uiShow(),
|
||||||
history,
|
.history = history,
|
||||||
Data::Draft(_field, reply, _preview->draft()),
|
.draft = Data::Draft(_field, reply, _preview->draft()),
|
||||||
done,
|
.usedLink = _preview->link(),
|
||||||
highlight,
|
.links = _preview->links(),
|
||||||
[=] { ClearDraftReplyTo(history, reply.messageId); });
|
.resolver = _preview->resolver(),
|
||||||
|
.done = done,
|
||||||
|
.highlight = highlight,
|
||||||
|
.clearOldDraft = [=] { ClearDraftReplyTo(history, replyToId); },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryWidget::keyPressEvent(QKeyEvent *e) {
|
void HistoryWidget::keyPressEvent(QKeyEvent *e) {
|
||||||
|
|
|
@ -18,11 +18,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_thread.h"
|
#include "data/data_thread.h"
|
||||||
#include "data/data_user.h"
|
#include "data/data_user.h"
|
||||||
#include "data/data_web_page.h"
|
#include "data/data_web_page.h"
|
||||||
|
#include "history/view/controls/history_view_webpage_processor.h"
|
||||||
|
#include "history/view/history_view_element.h"
|
||||||
|
#include "history/view/history_view_cursor_state.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
#include "history/history_item.h"
|
#include "history/history_item.h"
|
||||||
#include "history/history_item_components.h"
|
#include "history/history_item_components.h"
|
||||||
#include "history/view/history_view_element.h"
|
|
||||||
#include "history/view/history_view_cursor_state.h"
|
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "settings/settings_common.h"
|
#include "settings/settings_common.h"
|
||||||
|
@ -102,6 +103,28 @@ private:
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] TextWithEntities HighlightParsedLinks(
|
||||||
|
TextWithEntities text,
|
||||||
|
const std::vector<MessageLinkRange> &links) {
|
||||||
|
auto i = text.entities.begin();
|
||||||
|
for (const auto &range : links) {
|
||||||
|
if (range.custom.isEmpty()) {
|
||||||
|
while (i != text.entities.end()) {
|
||||||
|
if (i->offset() > range.start) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
i = text.entities.insert(
|
||||||
|
i,
|
||||||
|
EntityInText(EntityType::Url, range.start, range.length));
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PreviewWrap final : public Ui::RpWidget {
|
class PreviewWrap final : public Ui::RpWidget {
|
||||||
public:
|
public:
|
||||||
PreviewWrap(
|
PreviewWrap(
|
||||||
|
@ -114,7 +137,9 @@ public:
|
||||||
const TextWithEntities "e);
|
const TextWithEntities "e);
|
||||||
[[nodiscard]] rpl::producer<QString> showLinkSelector(
|
[[nodiscard]] rpl::producer<QString> showLinkSelector(
|
||||||
const TextWithTags &message,
|
const TextWithTags &message,
|
||||||
Data::WebPageDraft webpage);
|
Data::WebPageDraft webpage,
|
||||||
|
const std::vector<MessageLinkRange> &links,
|
||||||
|
const QString &usedLink);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void paintEvent(QPaintEvent *e) override;
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
@ -125,6 +150,10 @@ private:
|
||||||
void mouseDoubleClickEvent(QMouseEvent *e) override;
|
void mouseDoubleClickEvent(QMouseEvent *e) override;
|
||||||
|
|
||||||
void initElement();
|
void initElement();
|
||||||
|
void highlightUsedLink(
|
||||||
|
const TextWithTags &message,
|
||||||
|
const QString &usedLink,
|
||||||
|
const std::vector<MessageLinkRange> &links);
|
||||||
void startSelection(TextSelectType type);
|
void startSelection(TextSelectType type);
|
||||||
[[nodiscard]] TextSelection resolveNewSelection() const;
|
[[nodiscard]] TextSelection resolveNewSelection() const;
|
||||||
|
|
||||||
|
@ -138,12 +167,15 @@ private:
|
||||||
HistoryItem *_draftItem = nullptr;
|
HistoryItem *_draftItem = nullptr;
|
||||||
std::unique_ptr<Element> _element;
|
std::unique_ptr<Element> _element;
|
||||||
rpl::variable<TextSelection> _selection;
|
rpl::variable<TextSelection> _selection;
|
||||||
|
rpl::event_stream<QString> _chosenUrl;
|
||||||
Ui::PeerUserpicView _userpic;
|
Ui::PeerUserpicView _userpic;
|
||||||
rpl::lifetime _elementLifetime;
|
rpl::lifetime _elementLifetime;
|
||||||
|
|
||||||
QPoint _position;
|
QPoint _position;
|
||||||
|
|
||||||
base::Timer _trippleClickTimer;
|
base::Timer _trippleClickTimer;
|
||||||
|
ClickHandlerPtr _link;
|
||||||
|
ClickHandlerPtr _pressedLink;
|
||||||
TextSelectType _selectType = TextSelectType::Letters;
|
TextSelectType _selectType = TextSelectType::Letters;
|
||||||
uint16 _symbol = 0;
|
uint16 _symbol = 0;
|
||||||
uint16 _selectionStartSymbol = 0;
|
uint16 _selectionStartSymbol = 0;
|
||||||
|
@ -219,6 +251,8 @@ rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
|
||||||
_selection.reset(element->selectionFromQuote(quote));
|
_selection.reset(element->selectionFromQuote(quote));
|
||||||
_element = std::move(element);
|
_element = std::move(element);
|
||||||
|
|
||||||
|
_link = _pressedLink = nullptr;
|
||||||
|
|
||||||
if (const auto was = base::take(_draftItem)) {
|
if (const auto was = base::take(_draftItem)) {
|
||||||
was->destroy();
|
was->destroy();
|
||||||
}
|
}
|
||||||
|
@ -240,7 +274,9 @@ rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
|
||||||
|
|
||||||
rpl::producer<QString> PreviewWrap::showLinkSelector(
|
rpl::producer<QString> PreviewWrap::showLinkSelector(
|
||||||
const TextWithTags &message,
|
const TextWithTags &message,
|
||||||
Data::WebPageDraft webpage) {
|
Data::WebPageDraft webpage,
|
||||||
|
const std::vector<MessageLinkRange> &links,
|
||||||
|
const QString &usedLink) {
|
||||||
_selection.reset(TextSelection());
|
_selection.reset(TextSelection());
|
||||||
|
|
||||||
_element = nullptr;
|
_element = nullptr;
|
||||||
|
@ -259,10 +295,10 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
|
||||||
base::unixtime::now(), // date
|
base::unixtime::now(), // date
|
||||||
_history->session().userPeerId(),
|
_history->session().userPeerId(),
|
||||||
QString(), // postAuthor
|
QString(), // postAuthor
|
||||||
TextWithEntities{
|
HighlightParsedLinks({
|
||||||
message.text,
|
message.text,
|
||||||
TextUtilities::ConvertTextTagsToEntities(message.tags),
|
TextUtilities::ConvertTextTagsToEntities(message.tags),
|
||||||
},
|
}, links),
|
||||||
MTP_messageMediaWebPage(
|
MTP_messageMediaWebPage(
|
||||||
MTP_flags(Flag()
|
MTP_flags(Flag()
|
||||||
| (webpage.forceLargeMedia
|
| (webpage.forceLargeMedia
|
||||||
|
@ -287,8 +323,50 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
|
||||||
_section = Section::Link;
|
_section = Section::Link;
|
||||||
|
|
||||||
initElement();
|
initElement();
|
||||||
|
highlightUsedLink(message, usedLink, links);
|
||||||
|
|
||||||
return rpl::never<QString>();
|
return _chosenUrl.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreviewWrap::highlightUsedLink(
|
||||||
|
const TextWithTags &message,
|
||||||
|
const QString &usedLink,
|
||||||
|
const std::vector<MessageLinkRange> &links) {
|
||||||
|
auto selection = TextSelection();
|
||||||
|
const auto view = QStringView(message.text);
|
||||||
|
for (const auto &range : links) {
|
||||||
|
auto text = view.mid(range.start, range.length);
|
||||||
|
if (range.custom == usedLink
|
||||||
|
|| (range.custom.isEmpty()
|
||||||
|
&& range.length == usedLink.size()
|
||||||
|
&& text == usedLink)) {
|
||||||
|
selection = {
|
||||||
|
uint16(range.start),
|
||||||
|
uint16(range.start + range.length),
|
||||||
|
};
|
||||||
|
const auto skip = [](QChar ch) {
|
||||||
|
return ch.isSpace() || Ui::Text::IsNewline(ch);
|
||||||
|
};
|
||||||
|
while (!text.isEmpty() && skip(text.front())) {
|
||||||
|
text = text.mid(1);
|
||||||
|
++selection.from;
|
||||||
|
}
|
||||||
|
while (!text.isEmpty() && skip(text.back())) {
|
||||||
|
text = text.mid(0, text.size() - 1);
|
||||||
|
--selection.to;
|
||||||
|
}
|
||||||
|
const auto basic = _element->textState(QPoint(0, 0), {
|
||||||
|
.flags = Ui::Text::StateRequest::Flag::LookupSymbol,
|
||||||
|
.onlyMessageText = true,
|
||||||
|
});
|
||||||
|
if (basic.symbol > 0) {
|
||||||
|
selection.from += basic.symbol;
|
||||||
|
selection.to += basic.symbol;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_selection = selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PreviewWrap::paintEvent(QPaintEvent *e) {
|
void PreviewWrap::paintEvent(QPaintEvent *e) {
|
||||||
|
@ -385,7 +463,10 @@ void PreviewWrap::mouseMoveEvent(QMouseEvent *e) {
|
||||||
_over = true;
|
_over = true;
|
||||||
const auto text = (_section == Section::Reply)
|
const auto text = (_section == Section::Reply)
|
||||||
&& (resolved.cursor == CursorState::Text);
|
&& (resolved.cursor == CursorState::Text);
|
||||||
const auto link = (_section == Section::Link) && resolved.link;
|
_link = (_section == Section::Link && resolved.overMessageText)
|
||||||
|
? resolved.link
|
||||||
|
: nullptr;
|
||||||
|
const auto link = (_link != nullptr) || (_pressedLink != nullptr);
|
||||||
if (_textCursor != text || _linkCursor != link) {
|
if (_textCursor != text || _linkCursor != link) {
|
||||||
_textCursor = text;
|
_textCursor = text;
|
||||||
_linkCursor = link;
|
_linkCursor = link;
|
||||||
|
@ -412,13 +493,16 @@ void PreviewWrap::mousePressEvent(QMouseEvent *e) {
|
||||||
startSelection(_trippleClickTimer.isActive()
|
startSelection(_trippleClickTimer.isActive()
|
||||||
? TextSelectType::Paragraphs
|
? TextSelectType::Paragraphs
|
||||||
: TextSelectType::Letters);
|
: TextSelectType::Letters);
|
||||||
|
} else {
|
||||||
|
_pressedLink = _link;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) {
|
void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) {
|
||||||
if (!_selecting) {
|
if (_section == Section::Reply) {
|
||||||
return;
|
if (!_selecting) {
|
||||||
} else if (_section == Section::Reply) {
|
return;
|
||||||
|
}
|
||||||
const auto result = resolveNewSelection();
|
const auto result = resolveNewSelection();
|
||||||
_selecting = false;
|
_selecting = false;
|
||||||
_selectType = TextSelectType::Letters;
|
_selectType = TextSelectType::Letters;
|
||||||
|
@ -426,6 +510,12 @@ void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) {
|
||||||
setCursor(style::cur_default);
|
setCursor(style::cur_default);
|
||||||
}
|
}
|
||||||
_selection = result;
|
_selection = result;
|
||||||
|
} else if (base::take(_pressedLink) == _link && _link) {
|
||||||
|
if (const auto url = _link->url(); !url.isEmpty()) {
|
||||||
|
_chosenUrl.fire_copy(url);
|
||||||
|
}
|
||||||
|
} else if (!_link) {
|
||||||
|
setCursor(style::cur_default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,6 +599,276 @@ Context PreviewDelegate::elementContext() {
|
||||||
return Context::History;
|
return Context::History;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AddFilledSkip(not_null<Ui::VerticalLayout*> container) {
|
||||||
|
const auto skip = container->add(object_ptr<Ui::FixedHeightWidget>(
|
||||||
|
container,
|
||||||
|
st::settingsPrivacySkipTop));
|
||||||
|
skip->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
||||||
|
QPainter(skip).fillRect(clip, st::boxBg);
|
||||||
|
}, skip->lifetime());
|
||||||
|
};
|
||||||
|
|
||||||
|
void DraftOptionsBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
EditDraftOptionsArgs &&args,
|
||||||
|
HistoryItem *replyItem,
|
||||||
|
WebPageData *previewData) {
|
||||||
|
box->setWidth(st::boxWideWidth);
|
||||||
|
|
||||||
|
const auto &draft = args.draft;
|
||||||
|
struct State {
|
||||||
|
rpl::variable<Section> shown;
|
||||||
|
rpl::lifetime shownLifetime;
|
||||||
|
rpl::variable<TextWithEntities> quote;
|
||||||
|
Data::WebPageDraft webpage;
|
||||||
|
WebPageData *preview = nullptr;
|
||||||
|
QString link;
|
||||||
|
Ui::SettingsSlider *tabs = nullptr;
|
||||||
|
PreviewWrap *wrap = nullptr;
|
||||||
|
rpl::lifetime resolveLifetime;
|
||||||
|
};
|
||||||
|
const auto state = box->lifetime().make_state<State>();
|
||||||
|
state->quote = draft.reply.quote;
|
||||||
|
state->webpage = draft.webpage;
|
||||||
|
state->preview = previewData;
|
||||||
|
state->shown = previewData ? Section::Link : Section::Reply;
|
||||||
|
if (replyItem && previewData) {
|
||||||
|
box->setNoContentMargin(true);
|
||||||
|
state->tabs = box->setPinnedToTopContent(
|
||||||
|
object_ptr<Ui::SettingsSlider>(
|
||||||
|
box.get(),
|
||||||
|
st::defaultTabsSlider));
|
||||||
|
state->tabs->resizeToWidth(st::boxWideWidth);
|
||||||
|
state->tabs->move(0, 0);
|
||||||
|
state->tabs->setRippleTopRoundRadius(st::boxRadius);
|
||||||
|
state->tabs->setSections({
|
||||||
|
tr::lng_reply_header_short(tr::now),
|
||||||
|
tr::lng_link_header_short(tr::now),
|
||||||
|
});
|
||||||
|
state->tabs->setActiveSectionFast(1);
|
||||||
|
state->tabs->sectionActivated(
|
||||||
|
) | rpl::start_with_next([=](int section) {
|
||||||
|
state->shown = section ? Section::Link : Section::Reply;
|
||||||
|
}, box->lifetime());
|
||||||
|
} else {
|
||||||
|
box->setTitle(previewData
|
||||||
|
? tr::lng_link_options_header()
|
||||||
|
: draft.reply.quote.empty()
|
||||||
|
? tr::lng_reply_options_header()
|
||||||
|
: tr::lng_reply_options_quote());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto bottom = box->setPinnedToBottomContent(
|
||||||
|
object_ptr<Ui::VerticalLayout>(box));
|
||||||
|
|
||||||
|
const auto &done = args.done;
|
||||||
|
const auto &show = args.show;
|
||||||
|
const auto &highlight = args.highlight;
|
||||||
|
const auto &clearOldDraft = args.clearOldDraft;
|
||||||
|
const auto resolveReply = [=] {
|
||||||
|
auto result = draft.reply;
|
||||||
|
result.quote = state->quote.current();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const auto finish = [=](
|
||||||
|
FullReplyTo result,
|
||||||
|
Data::WebPageDraft webpage) {
|
||||||
|
const auto weak = Ui::MakeWeak(box);
|
||||||
|
done(std::move(result), std::move(webpage));
|
||||||
|
if (const auto strong = weak.data()) {
|
||||||
|
strong->closeBox();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto setupReplyActions = [=] {
|
||||||
|
AddFilledSkip(bottom);
|
||||||
|
|
||||||
|
Settings::AddButton(
|
||||||
|
bottom,
|
||||||
|
tr::lng_reply_in_another_chat(),
|
||||||
|
st::settingsButton,
|
||||||
|
{ &st::menuIconReplace }
|
||||||
|
)->setClickedCallback([=] {
|
||||||
|
ShowReplyToChatBox(show, resolveReply(), clearOldDraft);
|
||||||
|
});
|
||||||
|
|
||||||
|
Settings::AddButton(
|
||||||
|
bottom,
|
||||||
|
tr::lng_reply_show_in_chat(),
|
||||||
|
st::settingsButton,
|
||||||
|
{ &st::menuIconShowInChat }
|
||||||
|
)->setClickedCallback(highlight);
|
||||||
|
|
||||||
|
Settings::AddButton(
|
||||||
|
bottom,
|
||||||
|
tr::lng_reply_remove(),
|
||||||
|
st::settingsAttentionButtonWithIcon,
|
||||||
|
{ &st::menuIconDeleteAttention }
|
||||||
|
)->setClickedCallback([=] {
|
||||||
|
finish({}, state->webpage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!replyItem->originalText().empty()) {
|
||||||
|
AddFilledSkip(bottom);
|
||||||
|
Settings::AddDividerText(
|
||||||
|
bottom,
|
||||||
|
tr::lng_reply_about_quote());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto setupLinkActions = [=] {
|
||||||
|
AddFilledSkip(bottom);
|
||||||
|
|
||||||
|
if (!draft.textWithTags.empty()) {
|
||||||
|
Settings::AddButton(
|
||||||
|
bottom,
|
||||||
|
(state->webpage.invert
|
||||||
|
? tr::lng_link_move_down()
|
||||||
|
: tr::lng_link_move_up()),
|
||||||
|
st::settingsButton,
|
||||||
|
{ state->webpage.invert
|
||||||
|
? &st::menuIconBelow
|
||||||
|
: &st::menuIconAbove }
|
||||||
|
)->setClickedCallback([=] {
|
||||||
|
state->webpage.invert = !state->webpage.invert;
|
||||||
|
state->webpage.manual = true;
|
||||||
|
state->shown.force_assign(Section::Link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state->preview->hasLargeMedia) {
|
||||||
|
const auto small = state->webpage.forceSmallMedia
|
||||||
|
|| (!state->webpage.forceLargeMedia
|
||||||
|
&& state->preview->computeDefaultSmallMedia());
|
||||||
|
Settings::AddButton(
|
||||||
|
bottom,
|
||||||
|
(small
|
||||||
|
? tr::lng_link_enlarge_photo()
|
||||||
|
: tr::lng_link_shrink_photo()),
|
||||||
|
st::settingsButton,
|
||||||
|
{ small ? &st::menuIconEnlarge : &st::menuIconShrink }
|
||||||
|
)->setClickedCallback([=] {
|
||||||
|
if (small) {
|
||||||
|
state->webpage.forceSmallMedia = false;
|
||||||
|
state->webpage.forceLargeMedia = true;
|
||||||
|
} else {
|
||||||
|
state->webpage.forceLargeMedia = false;
|
||||||
|
state->webpage.forceSmallMedia = true;
|
||||||
|
}
|
||||||
|
state->webpage.manual = true;
|
||||||
|
state->shown.force_assign(Section::Link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings::AddButton(
|
||||||
|
bottom,
|
||||||
|
tr::lng_link_remove(),
|
||||||
|
st::settingsAttentionButtonWithIcon,
|
||||||
|
{ &st::menuIconDeleteAttention }
|
||||||
|
)->setClickedCallback([=] {
|
||||||
|
finish(resolveReply(), { .removed = true });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (args.links.size() > 1) {
|
||||||
|
AddFilledSkip(bottom);
|
||||||
|
Settings::AddDividerText(
|
||||||
|
bottom,
|
||||||
|
tr::lng_link_about_choose());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto &resolver = args.resolver;
|
||||||
|
const auto performSwitch = [=](const QString &link, WebPageData *page) {
|
||||||
|
if (page) {
|
||||||
|
state->preview = page;
|
||||||
|
state->webpage.id = page->id;
|
||||||
|
state->webpage.url = page->url;
|
||||||
|
state->webpage.manual = true;
|
||||||
|
state->link = link;
|
||||||
|
state->shown.force_assign(Section::Link);
|
||||||
|
} else {
|
||||||
|
show->showToast(u"Could not generate preview for this link."_q);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto switchTo = [=](const QString &link) {
|
||||||
|
if (link == state->link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto value = resolver->lookup(link)) {
|
||||||
|
performSwitch(link, *value);
|
||||||
|
} else {
|
||||||
|
resolver->request(link);
|
||||||
|
state->resolveLifetime = resolver->resolved(
|
||||||
|
) | rpl::start_with_next([=](const QString &resolved) {
|
||||||
|
if (resolved == link) {
|
||||||
|
state->resolveLifetime.destroy();
|
||||||
|
performSwitch(
|
||||||
|
link,
|
||||||
|
resolver->lookup(link).value_or(nullptr));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
state->wrap = box->addRow(
|
||||||
|
object_ptr<PreviewWrap>(box, args.history),
|
||||||
|
{});
|
||||||
|
const auto &linkRanges = args.links;
|
||||||
|
state->shown.value() | rpl::start_with_next([=](Section shown) {
|
||||||
|
bottom->clear();
|
||||||
|
state->shownLifetime.destroy();
|
||||||
|
if (shown == Section::Reply) {
|
||||||
|
state->quote = state->wrap->showQuoteSelector(
|
||||||
|
replyItem,
|
||||||
|
state->quote.current());
|
||||||
|
setupReplyActions();
|
||||||
|
} else {
|
||||||
|
state->wrap->showLinkSelector(
|
||||||
|
draft.textWithTags,
|
||||||
|
state->webpage,
|
||||||
|
linkRanges,
|
||||||
|
state->link
|
||||||
|
) | rpl::start_with_next([=](QString link) {
|
||||||
|
switchTo(link);
|
||||||
|
}, state->shownLifetime);
|
||||||
|
setupLinkActions();
|
||||||
|
}
|
||||||
|
}, box->lifetime());
|
||||||
|
|
||||||
|
auto save = rpl::combine(
|
||||||
|
state->quote.value(),
|
||||||
|
state->shown.value()
|
||||||
|
) | rpl::map([=](const TextWithEntities "e, Section shown) {
|
||||||
|
return (quote.empty() || shown != Section::Reply)
|
||||||
|
? tr::lng_settings_save()
|
||||||
|
: tr::lng_reply_quote_selected();
|
||||||
|
}) | rpl::flatten_latest();
|
||||||
|
box->addButton(std::move(save), [=] {
|
||||||
|
finish(resolveReply(), state->webpage);
|
||||||
|
});
|
||||||
|
|
||||||
|
box->addButton(tr::lng_cancel(), [=] {
|
||||||
|
box->closeBox();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (replyItem) {
|
||||||
|
args.show->session().data().itemRemoved(
|
||||||
|
) | rpl::filter([=](not_null<const HistoryItem*> removed) {
|
||||||
|
return removed == replyItem;
|
||||||
|
}) | rpl::start_with_next([=] {
|
||||||
|
if (previewData) {
|
||||||
|
state->tabs = nullptr;
|
||||||
|
box->setPinnedToTopContent(
|
||||||
|
object_ptr<Ui::RpWidget>(nullptr));
|
||||||
|
box->setNoContentMargin(false);
|
||||||
|
box->setTitle(state->quote.current().empty()
|
||||||
|
? tr::lng_reply_options_header()
|
||||||
|
: tr::lng_reply_options_quote());
|
||||||
|
state->shown = Section::Link;
|
||||||
|
} else {
|
||||||
|
box->closeBox();
|
||||||
|
}
|
||||||
|
}, box->lifetime());
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ShowReplyToChatBox(
|
void ShowReplyToChatBox(
|
||||||
|
@ -603,14 +963,9 @@ void ShowReplyToChatBox(
|
||||||
) | rpl::start_with_next(std::move(callback), state->box->lifetime());
|
) | rpl::start_with_next(std::move(callback), state->box->lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditDraftOptions(
|
void EditDraftOptions(EditDraftOptionsArgs &&args) {
|
||||||
std::shared_ptr<ChatHelpers::Show> show,
|
const auto &draft = args.draft;
|
||||||
not_null<History*> history,
|
const auto session = &args.show->session();
|
||||||
Data::Draft draft,
|
|
||||||
Fn<void(FullReplyTo, Data::WebPageDraft)> done,
|
|
||||||
Fn<void()> highlight,
|
|
||||||
Fn<void()> clearOldDraft) {
|
|
||||||
const auto session = &show->session();
|
|
||||||
const auto replyItem = session->data().message(draft.reply.messageId);
|
const auto replyItem = session->data().message(draft.reply.messageId);
|
||||||
const auto previewDataRaw = draft.webpage.id
|
const auto previewDataRaw = draft.webpage.id
|
||||||
? session->data().webpage(draft.webpage.id).get()
|
? session->data().webpage(draft.webpage.id).get()
|
||||||
|
@ -623,225 +978,8 @@ void EditDraftOptions(
|
||||||
if (!replyItem && !previewData) {
|
if (!replyItem && !previewData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
show->show(Box([=](not_null<Ui::GenericBox*> box) {
|
args.show->show(
|
||||||
box->setWidth(st::boxWideWidth);
|
Box(DraftOptionsBox, std::move(args), replyItem, previewData));
|
||||||
|
|
||||||
struct State {
|
|
||||||
rpl::variable<Section> shown;
|
|
||||||
rpl::lifetime shownLifetime;
|
|
||||||
rpl::variable<TextWithEntities> quote;
|
|
||||||
Data::WebPageDraft webpage;
|
|
||||||
Ui::SettingsSlider *tabs = nullptr;
|
|
||||||
PreviewWrap *wrap = nullptr;
|
|
||||||
};
|
|
||||||
const auto state = box->lifetime().make_state<State>();
|
|
||||||
state->quote = draft.reply.quote;
|
|
||||||
state->webpage = draft.webpage;
|
|
||||||
state->shown = previewData ? Section::Link : Section::Reply;
|
|
||||||
if (replyItem && previewData) {
|
|
||||||
box->setNoContentMargin(true);
|
|
||||||
state->tabs = box->setPinnedToTopContent(
|
|
||||||
object_ptr<Ui::SettingsSlider>(
|
|
||||||
box.get(),
|
|
||||||
st::defaultTabsSlider));
|
|
||||||
state->tabs->resizeToWidth(st::boxWideWidth);
|
|
||||||
state->tabs->move(0, 0);
|
|
||||||
state->tabs->setRippleTopRoundRadius(st::boxRadius);
|
|
||||||
state->tabs->setSections({
|
|
||||||
tr::lng_reply_header_short(tr::now),
|
|
||||||
tr::lng_link_header_short(tr::now),
|
|
||||||
});
|
|
||||||
state->tabs->setActiveSectionFast(1);
|
|
||||||
state->tabs->sectionActivated(
|
|
||||||
) | rpl::start_with_next([=](int section) {
|
|
||||||
state->shown = section ? Section::Link : Section::Reply;
|
|
||||||
}, box->lifetime());
|
|
||||||
} else {
|
|
||||||
box->setTitle(previewData
|
|
||||||
? tr::lng_link_options_header()
|
|
||||||
: draft.reply.quote.empty()
|
|
||||||
? tr::lng_reply_options_header()
|
|
||||||
: tr::lng_reply_options_quote());
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto bottom = box->setPinnedToBottomContent(
|
|
||||||
object_ptr<Ui::VerticalLayout>(box));
|
|
||||||
const auto addSkip = [=] {
|
|
||||||
const auto skip = bottom->add(object_ptr<Ui::FixedHeightWidget>(
|
|
||||||
bottom,
|
|
||||||
st::settingsPrivacySkipTop));
|
|
||||||
skip->paintRequest() | rpl::start_with_next([=](QRect clip) {
|
|
||||||
QPainter(skip).fillRect(clip, st::boxBg);
|
|
||||||
}, skip->lifetime());
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto resolveReply = [=] {
|
|
||||||
auto result = draft.reply;
|
|
||||||
result.quote = state->quote.current();
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
const auto finish = [=](
|
|
||||||
FullReplyTo result,
|
|
||||||
Data::WebPageDraft webpage) {
|
|
||||||
const auto weak = Ui::MakeWeak(box);
|
|
||||||
done(std::move(result), std::move(webpage));
|
|
||||||
if (const auto strong = weak.data()) {
|
|
||||||
strong->closeBox();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto setupReplyActions = [=] {
|
|
||||||
addSkip();
|
|
||||||
|
|
||||||
Settings::AddButton(
|
|
||||||
bottom,
|
|
||||||
tr::lng_reply_in_another_chat(),
|
|
||||||
st::settingsButton,
|
|
||||||
{ &st::menuIconReplace }
|
|
||||||
)->setClickedCallback([=] {
|
|
||||||
ShowReplyToChatBox(show, resolveReply(), clearOldDraft);
|
|
||||||
});
|
|
||||||
|
|
||||||
Settings::AddButton(
|
|
||||||
bottom,
|
|
||||||
tr::lng_reply_show_in_chat(),
|
|
||||||
st::settingsButton,
|
|
||||||
{ &st::menuIconShowInChat }
|
|
||||||
)->setClickedCallback(highlight);
|
|
||||||
|
|
||||||
Settings::AddButton(
|
|
||||||
bottom,
|
|
||||||
tr::lng_reply_remove(),
|
|
||||||
st::settingsAttentionButtonWithIcon,
|
|
||||||
{ &st::menuIconDeleteAttention }
|
|
||||||
)->setClickedCallback([=] {
|
|
||||||
finish({}, state->webpage);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!replyItem->originalText().empty()) {
|
|
||||||
addSkip();
|
|
||||||
Settings::AddDividerText(
|
|
||||||
bottom,
|
|
||||||
tr::lng_reply_about_quote());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto setupLinkActions = [=] {
|
|
||||||
addSkip();
|
|
||||||
|
|
||||||
if (!draft.textWithTags.empty()) {
|
|
||||||
Settings::AddButton(
|
|
||||||
bottom,
|
|
||||||
(state->webpage.invert
|
|
||||||
? tr::lng_link_move_down()
|
|
||||||
: tr::lng_link_move_up()),
|
|
||||||
st::settingsButton,
|
|
||||||
{ state->webpage.invert
|
|
||||||
? &st::menuIconBelow
|
|
||||||
: &st::menuIconAbove }
|
|
||||||
)->setClickedCallback([=] {
|
|
||||||
state->webpage.invert = !state->webpage.invert;
|
|
||||||
state->webpage.manual = true;
|
|
||||||
state->shown.force_assign(Section::Link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewData->hasLargeMedia) {
|
|
||||||
const auto small = state->webpage.forceSmallMedia
|
|
||||||
|| (!state->webpage.forceLargeMedia
|
|
||||||
&& previewData->computeDefaultSmallMedia());
|
|
||||||
Settings::AddButton(
|
|
||||||
bottom,
|
|
||||||
(small
|
|
||||||
? tr::lng_link_enlarge_photo()
|
|
||||||
: tr::lng_link_shrink_photo()),
|
|
||||||
st::settingsButton,
|
|
||||||
{ small ? &st::menuIconEnlarge : &st::menuIconShrink }
|
|
||||||
)->setClickedCallback([=] {
|
|
||||||
if (small) {
|
|
||||||
state->webpage.forceSmallMedia = false;
|
|
||||||
state->webpage.forceLargeMedia = true;
|
|
||||||
} else {
|
|
||||||
state->webpage.forceLargeMedia = false;
|
|
||||||
state->webpage.forceSmallMedia = true;
|
|
||||||
}
|
|
||||||
state->webpage.manual = true;
|
|
||||||
state->shown.force_assign(Section::Link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Settings::AddButton(
|
|
||||||
bottom,
|
|
||||||
tr::lng_link_remove(),
|
|
||||||
st::settingsAttentionButtonWithIcon,
|
|
||||||
{ &st::menuIconDeleteAttention }
|
|
||||||
)->setClickedCallback([=] {
|
|
||||||
finish(resolveReply(), { .removed = true });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (true) {
|
|
||||||
addSkip();
|
|
||||||
Settings::AddDividerText(
|
|
||||||
bottom,
|
|
||||||
tr::lng_link_about_choose());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
state->wrap = box->addRow(
|
|
||||||
object_ptr<PreviewWrap>(box, history),
|
|
||||||
{});
|
|
||||||
state->shown.value() | rpl::start_with_next([=](Section shown) {
|
|
||||||
bottom->clear();
|
|
||||||
state->shownLifetime.destroy();
|
|
||||||
if (shown == Section::Reply) {
|
|
||||||
state->quote = state->wrap->showQuoteSelector(
|
|
||||||
replyItem,
|
|
||||||
state->quote.current());
|
|
||||||
setupReplyActions();
|
|
||||||
} else {
|
|
||||||
state->wrap->showLinkSelector(
|
|
||||||
draft.textWithTags,
|
|
||||||
state->webpage
|
|
||||||
) | rpl::start_with_next([=](QString url) {
|
|
||||||
}, state->shownLifetime);
|
|
||||||
setupLinkActions();
|
|
||||||
}
|
|
||||||
}, box->lifetime());
|
|
||||||
|
|
||||||
auto save = rpl::combine(
|
|
||||||
state->quote.value(),
|
|
||||||
state->shown.value()
|
|
||||||
) | rpl::map([=](const TextWithEntities "e, Section shown) {
|
|
||||||
return (quote.empty() || shown != Section::Reply)
|
|
||||||
? tr::lng_settings_save()
|
|
||||||
: tr::lng_reply_quote_selected();
|
|
||||||
}) | rpl::flatten_latest();
|
|
||||||
box->addButton(std::move(save), [=] {
|
|
||||||
finish(resolveReply(), state->webpage);
|
|
||||||
});
|
|
||||||
|
|
||||||
box->addButton(tr::lng_cancel(), [=] {
|
|
||||||
box->closeBox();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (replyItem) {
|
|
||||||
session->data().itemRemoved(
|
|
||||||
) | rpl::filter([=](not_null<const HistoryItem*> removed) {
|
|
||||||
return removed == replyItem;
|
|
||||||
}) | rpl::start_with_next([=] {
|
|
||||||
if (previewData) {
|
|
||||||
state->tabs = nullptr;
|
|
||||||
box->setPinnedToTopContent(
|
|
||||||
object_ptr<Ui::RpWidget>(nullptr));
|
|
||||||
box->setNoContentMargin(false);
|
|
||||||
box->setTitle(state->quote.current().empty()
|
|
||||||
? tr::lng_reply_options_header()
|
|
||||||
: tr::lng_reply_options_quote());
|
|
||||||
state->shown = Section::Link;
|
|
||||||
} else {
|
|
||||||
box->closeBox();
|
|
||||||
}
|
|
||||||
}, box->lifetime());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace HistoryView::Controls
|
} // namespace HistoryView::Controls
|
||||||
|
|
|
@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "data/data_drafts.h"
|
#include "data/data_drafts.h"
|
||||||
|
|
||||||
class History;
|
class History;
|
||||||
|
struct MessageLinkRange;
|
||||||
|
|
||||||
namespace ChatHelpers {
|
namespace ChatHelpers {
|
||||||
class Show;
|
class Show;
|
||||||
|
@ -21,13 +22,21 @@ class SessionController;
|
||||||
|
|
||||||
namespace HistoryView::Controls {
|
namespace HistoryView::Controls {
|
||||||
|
|
||||||
void EditDraftOptions(
|
class WebpageResolver;
|
||||||
std::shared_ptr<ChatHelpers::Show> show,
|
|
||||||
not_null<History*> history,
|
struct EditDraftOptionsArgs {
|
||||||
Data::Draft draft,
|
std::shared_ptr<ChatHelpers::Show> show;
|
||||||
Fn<void(FullReplyTo, Data::WebPageDraft)> done,
|
not_null<History*> history;
|
||||||
Fn<void()> highlight,
|
Data::Draft draft;
|
||||||
Fn<void()> clearOldDraft);
|
QString usedLink;
|
||||||
|
std::vector<MessageLinkRange> links;
|
||||||
|
std::shared_ptr<WebpageResolver> resolver;
|
||||||
|
Fn<void(FullReplyTo, Data::WebPageDraft)> done;
|
||||||
|
Fn<void()> highlight;
|
||||||
|
Fn<void()> clearOldDraft;
|
||||||
|
};
|
||||||
|
|
||||||
|
void EditDraftOptions(EditDraftOptionsArgs &&args);
|
||||||
|
|
||||||
void ShowReplyToChatBox(
|
void ShowReplyToChatBox(
|
||||||
std::shared_ptr<ChatHelpers::Show> show,
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
|
|
@ -110,17 +110,88 @@ WebPageText ProcessWebPageData(WebPageData *page) {
|
||||||
return previewText;
|
return previewText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WebpageResolver::WebpageResolver(not_null<Main::Session*> session)
|
||||||
|
: _session(session)
|
||||||
|
, _api(&session->mtp()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<WebPageData*> WebpageResolver::lookup(
|
||||||
|
const QString &link) const {
|
||||||
|
const auto i = _cache.find(link);
|
||||||
|
return (i == end(_cache))
|
||||||
|
? std::optional<WebPageData*>()
|
||||||
|
: (i->second && !i->second->failed)
|
||||||
|
? i->second
|
||||||
|
: nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WebpageResolver::find(not_null<WebPageData*> page) const {
|
||||||
|
for (const auto &[link, cached] : _cache) {
|
||||||
|
if (cached == page) {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebpageResolver::request(const QString &link) {
|
||||||
|
if (_requestLink == link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto done = [=](const MTPDmessageMediaWebPage &data) {
|
||||||
|
const auto page = _session->data().processWebpage(data.vwebpage());
|
||||||
|
if (page->pendingTill > 0
|
||||||
|
&& page->pendingTill < base::unixtime::now()) {
|
||||||
|
page->pendingTill = 0;
|
||||||
|
page->failed = true;
|
||||||
|
}
|
||||||
|
_cache.emplace(link, page->failed ? nullptr : page.get());
|
||||||
|
_resolved.fire_copy(link);
|
||||||
|
};
|
||||||
|
const auto fail = [=] {
|
||||||
|
_cache.emplace(link, nullptr);
|
||||||
|
_resolved.fire_copy(link);
|
||||||
|
};
|
||||||
|
_requestLink = link;
|
||||||
|
_requestId = _api.request(
|
||||||
|
MTPmessages_GetWebPagePreview(
|
||||||
|
MTP_flags(0),
|
||||||
|
MTP_string(link),
|
||||||
|
MTPVector<MTPMessageEntity>()
|
||||||
|
)).done([=](const MTPMessageMedia &result, mtpRequestId requestId) {
|
||||||
|
if (_requestId == requestId) {
|
||||||
|
_requestId = 0;
|
||||||
|
}
|
||||||
|
result.match([=](const MTPDmessageMediaWebPage &data) {
|
||||||
|
done(data);
|
||||||
|
}, [&](const auto &d) {
|
||||||
|
fail();
|
||||||
|
});
|
||||||
|
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
|
||||||
|
if (_requestId == requestId) {
|
||||||
|
_requestId = 0;
|
||||||
|
}
|
||||||
|
fail();
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebpageResolver::cancel(const QString &link) {
|
||||||
|
if (_requestLink == link) {
|
||||||
|
_api.request(base::take(_requestId)).cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WebpageProcessor::WebpageProcessor(
|
WebpageProcessor::WebpageProcessor(
|
||||||
not_null<History*> history,
|
not_null<History*> history,
|
||||||
not_null<Ui::InputField*> field)
|
not_null<Ui::InputField*> field)
|
||||||
: _history(history)
|
: _history(history)
|
||||||
, _api(&history->session().mtp())
|
, _resolver(std::make_shared<WebpageResolver>(&history->session()))
|
||||||
, _parser(field)
|
, _parser(field)
|
||||||
, _timer([=] {
|
, _timer([=] {
|
||||||
if (!ShowWebPagePreview(_data) || _link.isEmpty()) {
|
if (!ShowWebPagePreview(_data) || _link.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
request();
|
_resolver->request(_link);
|
||||||
}) {
|
}) {
|
||||||
_history->session().downloaderTaskFinished(
|
_history->session().downloaderTaskFinished(
|
||||||
) | rpl::filter([=] {
|
) | rpl::filter([=] {
|
||||||
|
@ -141,6 +212,23 @@ WebpageProcessor::WebpageProcessor(
|
||||||
_parsedLinks = std::move(parsed);
|
_parsedLinks = std::move(parsed);
|
||||||
checkPreview();
|
checkPreview();
|
||||||
}, _lifetime);
|
}, _lifetime);
|
||||||
|
|
||||||
|
_resolver->resolved() | rpl::start_with_next([=](QString link) {
|
||||||
|
if (_link != link
|
||||||
|
|| _draft.removed
|
||||||
|
|| (_draft.manual && _draft.url != link)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_data = _resolver->lookup(link).value_or(nullptr);
|
||||||
|
if (_data) {
|
||||||
|
_draft.id = _data->id;
|
||||||
|
_draft.url = _data->url;
|
||||||
|
updateFromData();
|
||||||
|
} else {
|
||||||
|
_links = QStringList();
|
||||||
|
checkPreview();
|
||||||
|
}
|
||||||
|
}, _lifetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<> WebpageProcessor::repaintRequests() const {
|
rpl::producer<> WebpageProcessor::repaintRequests() const {
|
||||||
|
@ -151,8 +239,20 @@ Data::WebPageDraft WebpageProcessor::draft() const {
|
||||||
return _draft;
|
return _draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<WebpageResolver> WebpageProcessor::resolver() const {
|
||||||
|
return _resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<MessageLinkRange> &WebpageProcessor::links() const {
|
||||||
|
return _parser.ranges();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString WebpageProcessor::link() const {
|
||||||
|
return _link;
|
||||||
|
}
|
||||||
|
|
||||||
void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
|
void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
|
||||||
_api.request(base::take(_requestId)).cancel();
|
const auto was = _link;
|
||||||
if (draft.removed) {
|
if (draft.removed) {
|
||||||
_draft = draft;
|
_draft = draft;
|
||||||
if (_parsedLinks.empty()) {
|
if (_parsedLinks.empty()) {
|
||||||
|
@ -173,14 +273,21 @@ void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
|
||||||
: nullptr;
|
: nullptr;
|
||||||
if (page && page->url == draft.url) {
|
if (page && page->url == draft.url) {
|
||||||
_data = page;
|
_data = page;
|
||||||
|
if (const auto link = _resolver->find(page); !link.isEmpty()) {
|
||||||
|
_link = link;
|
||||||
|
}
|
||||||
updateFromData();
|
updateFromData();
|
||||||
} else {
|
} else {
|
||||||
request();
|
_resolver->request(_link);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (!draft.manual && !_draft.manual) {
|
} else if (!draft.manual && !_draft.manual) {
|
||||||
_draft = draft;
|
_draft = draft;
|
||||||
checkNow(reparse);
|
checkNow(reparse);
|
||||||
}
|
}
|
||||||
|
if (_link != was) {
|
||||||
|
_resolver->cancel(was);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebpageProcessor::updateFromData() {
|
void WebpageProcessor::updateFromData() {
|
||||||
|
@ -212,56 +319,6 @@ void WebpageProcessor::updateFromData() {
|
||||||
_repaintRequests.fire({});
|
_repaintRequests.fire({});
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebpageProcessor::request() {
|
|
||||||
const auto link = _link;
|
|
||||||
const auto done = [=](const MTPDmessageMediaWebPage &data) {
|
|
||||||
const auto page = _history->owner().processWebpage(data.vwebpage());
|
|
||||||
if (page->pendingTill > 0
|
|
||||||
&& page->pendingTill < base::unixtime::now()) {
|
|
||||||
page->pendingTill = 0;
|
|
||||||
page->failed = true;
|
|
||||||
}
|
|
||||||
_cache.emplace(link, page->failed ? nullptr : page.get());
|
|
||||||
if (_link == link
|
|
||||||
&& !_draft.removed
|
|
||||||
&& (!_draft.manual || _draft.url == link)) {
|
|
||||||
_data = (page->id && !page->failed)
|
|
||||||
? page.get()
|
|
||||||
: nullptr;
|
|
||||||
_draft.id = page->id;
|
|
||||||
_draft.url = page->url;
|
|
||||||
updateFromData();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const auto fail = [=] {
|
|
||||||
_cache.emplace(link, nullptr);
|
|
||||||
if (_link == link && !_draft.removed && !_draft.manual) {
|
|
||||||
_links = QStringList();
|
|
||||||
checkPreview();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
_requestId = _api.request(
|
|
||||||
MTPmessages_GetWebPagePreview(
|
|
||||||
MTP_flags(0),
|
|
||||||
MTP_string(_link),
|
|
||||||
MTPVector<MTPMessageEntity>()
|
|
||||||
)).done([=](const MTPMessageMedia &result, mtpRequestId requestId) {
|
|
||||||
if (_requestId == requestId) {
|
|
||||||
_requestId = 0;
|
|
||||||
}
|
|
||||||
result.match([=](const MTPDmessageMediaWebPage &data) {
|
|
||||||
done(data);
|
|
||||||
}, [&](const auto &d) {
|
|
||||||
fail();
|
|
||||||
});
|
|
||||||
}).fail([=](const MTP::Error &error, mtpRequestId requestId) {
|
|
||||||
if (_requestId == requestId) {
|
|
||||||
_requestId = 0;
|
|
||||||
}
|
|
||||||
fail();
|
|
||||||
}).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebpageProcessor::setDisabled(bool disabled) {
|
void WebpageProcessor::setDisabled(bool disabled) {
|
||||||
_parser.setDisabled(disabled);
|
_parser.setDisabled(disabled);
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
|
@ -307,25 +364,21 @@ void WebpageProcessor::checkPreview() {
|
||||||
auto page = (WebPageData*)nullptr;
|
auto page = (WebPageData*)nullptr;
|
||||||
auto chosen = QString();
|
auto chosen = QString();
|
||||||
for (const auto &link : _links) {
|
for (const auto &link : _links) {
|
||||||
const auto i = _cache.find(link);
|
const auto value = _resolver->lookup(link);
|
||||||
if (i == end(_cache)) {
|
if (!value) {
|
||||||
chosen = link;
|
chosen = link;
|
||||||
break;
|
break;
|
||||||
} else if (i->second) {
|
} else if (*value) {
|
||||||
if (i->second->failed) {
|
chosen = link;
|
||||||
i->second = nullptr;
|
page = *value;
|
||||||
} else {
|
break;
|
||||||
chosen = link;
|
|
||||||
page = i->second;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (_link != chosen) {
|
if (_link != chosen) {
|
||||||
|
_resolver->cancel(_link);
|
||||||
_link = chosen;
|
_link = chosen;
|
||||||
_api.request(base::take(_requestId)).cancel();
|
|
||||||
if (!page && !_link.isEmpty()) {
|
if (!page && !_link.isEmpty()) {
|
||||||
request();
|
_resolver->request(_link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (page) {
|
if (page) {
|
||||||
|
|
|
@ -7,13 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "base/weak_ptr.h"
|
|
||||||
#include "data/data_drafts.h"
|
#include "data/data_drafts.h"
|
||||||
#include "chat_helpers/message_field.h"
|
#include "chat_helpers/message_field.h"
|
||||||
#include "mtproto/sender.h"
|
#include "mtproto/sender.h"
|
||||||
|
|
||||||
class History;
|
class History;
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class InputField;
|
class InputField;
|
||||||
} // namespace Ui
|
} // namespace Ui
|
||||||
|
@ -47,7 +50,33 @@ struct WebpageParsed {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class WebpageProcessor final : public base::has_weak_ptr {
|
class WebpageResolver final {
|
||||||
|
public:
|
||||||
|
explicit WebpageResolver(not_null<Main::Session*> session);
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<WebPageData*> lookup(
|
||||||
|
const QString &link) const;
|
||||||
|
[[nodiscard]] rpl::producer<QString> resolved() const {
|
||||||
|
return _resolved.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QString find(not_null<WebPageData*> page) const;
|
||||||
|
|
||||||
|
void request(const QString &link);
|
||||||
|
void cancel(const QString &link);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const not_null<Main::Session*> _session;
|
||||||
|
MTP::Sender _api;
|
||||||
|
base::flat_map<QString, WebPageData*> _cache;
|
||||||
|
rpl::event_stream<QString> _resolved;
|
||||||
|
|
||||||
|
QString _requestLink;
|
||||||
|
mtpRequestId _requestId = 0;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class WebpageProcessor final {
|
||||||
public:
|
public:
|
||||||
WebpageProcessor(
|
WebpageProcessor(
|
||||||
not_null<History*> history,
|
not_null<History*> history,
|
||||||
|
@ -63,6 +92,9 @@ public:
|
||||||
// unless preview was removed in the draft or manual.
|
// unless preview was removed in the draft or manual.
|
||||||
void apply(Data::WebPageDraft draft, bool reparse = true);
|
void apply(Data::WebPageDraft draft, bool reparse = true);
|
||||||
[[nodiscard]] Data::WebPageDraft draft() const;
|
[[nodiscard]] Data::WebPageDraft draft() const;
|
||||||
|
[[nodiscard]] std::shared_ptr<WebpageResolver> resolver() const;
|
||||||
|
[[nodiscard]] const std::vector<MessageLinkRange> &links() const;
|
||||||
|
[[nodiscard]] QString link() const;
|
||||||
|
|
||||||
[[nodiscard]] rpl::producer<> repaintRequests() const;
|
[[nodiscard]] rpl::producer<> repaintRequests() const;
|
||||||
[[nodiscard]] rpl::producer<WebpageParsed> parsedValue() const;
|
[[nodiscard]] rpl::producer<WebpageParsed> parsedValue() const;
|
||||||
|
@ -74,21 +106,17 @@ public:
|
||||||
private:
|
private:
|
||||||
void updateFromData();
|
void updateFromData();
|
||||||
void checkPreview();
|
void checkPreview();
|
||||||
void request();
|
|
||||||
|
|
||||||
const not_null<History*> _history;
|
const not_null<History*> _history;
|
||||||
MTP::Sender _api;
|
const std::shared_ptr<WebpageResolver> _resolver;
|
||||||
MessageLinksParser _parser;
|
MessageLinksParser _parser;
|
||||||
|
|
||||||
QStringList _parsedLinks;
|
QStringList _parsedLinks;
|
||||||
QStringList _links;
|
QStringList _links;
|
||||||
QString _link;
|
QString _link;
|
||||||
WebPageData *_data = nullptr;
|
WebPageData *_data = nullptr;
|
||||||
base::flat_map<QString, WebPageData*> _cache;
|
|
||||||
Data::WebPageDraft _draft;
|
Data::WebPageDraft _draft;
|
||||||
|
|
||||||
mtpRequestId _requestId = 0;
|
|
||||||
|
|
||||||
rpl::event_stream<> _repaintRequests;
|
rpl::event_stream<> _repaintRequests;
|
||||||
rpl::variable<WebpageParsed> _parsed;
|
rpl::variable<WebpageParsed> _parsed;
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ struct TextState {
|
||||||
FullMsgId itemId;
|
FullMsgId itemId;
|
||||||
CursorState cursor = CursorState::None;
|
CursorState cursor = CursorState::None;
|
||||||
ClickHandlerPtr link;
|
ClickHandlerPtr link;
|
||||||
|
bool overMessageText = false;
|
||||||
bool afterSymbol = false;
|
bool afterSymbol = false;
|
||||||
bool customTooltip = false;
|
bool customTooltip = false;
|
||||||
uint16 symbol = 0;
|
uint16 symbol = 0;
|
||||||
|
|
|
@ -2195,6 +2195,7 @@ TextState Message::textState(
|
||||||
if (_invertMedia) {
|
if (_invertMedia) {
|
||||||
result.symbol += visibleMediaTextLength();
|
result.symbol += visibleMediaTextLength();
|
||||||
}
|
}
|
||||||
|
result.overMessageText = true;
|
||||||
checkBottomInfoState();
|
checkBottomInfoState();
|
||||||
return result;
|
return result;
|
||||||
} else if (point.y() >= trect.y() + trect.height()) {
|
} else if (point.y() >= trect.y() + trect.height()) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue