Allow choosing the link for the preview.

This commit is contained in:
John Preston 2023-10-26 11:30:36 +04:00
parent 3b91e2dee4
commit a197ed9e95
10 changed files with 588 additions and 351 deletions

View file

@ -648,10 +648,6 @@ bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
return QObject::eventFilter(object, event);
}
const rpl::variable<QStringList> &MessageLinksParser::list() const {
return _list;
}
void MessageLinksParser::parse() {
const auto &textWithTags = _field->getTextWithTags();
const auto &text = textWithTags.text;
@ -781,7 +777,7 @@ void MessageLinksParser::parse() {
continue;
}
}
const auto range = LinkRange {
const auto range = MessageLinkRange{
int(domainOffset),
static_cast<int>(p - start - domainOffset),
QString()
@ -802,7 +798,7 @@ void MessageLinksParser::parse() {
void MessageLinksParser::applyRanges(const QString &text) {
const auto count = int(_ranges.size());
const auto current = _list.current();
const auto computeLink = [&](const LinkRange &range) {
const auto computeLink = [&](const MessageLinkRange &range) {
return range.custom.isEmpty()
? base::StringViewMid(text, range.start, range.length)
: QStringView(range.custom);

View file

@ -7,9 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/widgets/fields/input_field.h"
#include "base/qt/qt_compare.h"
#include "base/timer.h"
#include "chat_helpers/compose/compose_features.h"
#include "ui/widgets/fields/input_field.h"
#ifndef TDESKTOP_DISABLE_SPELLCHECK
#include "boxes/dictionaries_manager.h"
@ -96,6 +97,19 @@ AutocompleteQuery ParseMentionHashtagBotCommandQuery(
not_null<const Ui::InputField*> field,
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 {
public:
MessageLinksParser(not_null<Ui::InputField*> field);
@ -103,21 +117,12 @@ public:
void parseNow();
void setDisabled(bool disabled);
struct LinkRange {
int start = 0;
int length = 0;
QString custom;
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;
[[nodiscard]] const rpl::variable<QStringList> &list() const {
return _list;
}
[[nodiscard]] const std::vector<MessageLinkRange> &ranges() const {
return _ranges;
}
private:
bool eventFilter(QObject *object, QEvent *event) override;
@ -127,7 +132,7 @@ private:
not_null<Ui::InputField*> _field;
rpl::variable<QStringList> _list;
std::vector<LinkRange> _ranges;
std::vector<MessageLinkRange> _ranges;
int _lastLength = 0;
bool _disabled = false;
base::Timer _timer;

View file

@ -2403,9 +2403,10 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
if (canReply) {
const auto itemId = item->fullId();
const auto quote = selectedQuote(item);
const auto text = quote.empty()
auto text = quote.empty()
? tr::lng_context_reply_msg(tr::now)
: tr::lng_context_quote_and_reply(tr::now);
text.replace('&', u"&&"_q);
_menu->addAction(text, [=] {
if (canSendReply) {
_widget->replyToMessage({ itemId, quote });

View file

@ -6284,21 +6284,26 @@ void HistoryWidget::editDraftOptions() {
}
_preview->apply(webpage);
};
const auto replyToId = reply.messageId;
const auto highlight = [=] {
controller()->showPeerHistory(
reply.messageId.peer,
replyToId.peer,
Window::SectionShow::Way::Forward,
reply.messageId.msg);
replyToId.msg);
};
using namespace HistoryView::Controls;
EditDraftOptions(
controller()->uiShow(),
history,
Data::Draft(_field, reply, _preview->draft()),
done,
highlight,
[=] { ClearDraftReplyTo(history, reply.messageId); });
EditDraftOptions({
.show = controller()->uiShow(),
.history = history,
.draft = Data::Draft(_field, reply, _preview->draft()),
.usedLink = _preview->link(),
.links = _preview->links(),
.resolver = _preview->resolver(),
.done = done,
.highlight = highlight,
.clearOldDraft = [=] { ClearDraftReplyTo(history, replyToId); },
});
}
void HistoryWidget::keyPressEvent(QKeyEvent *e) {

View file

@ -18,11 +18,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_thread.h"
#include "data/data_user.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_item.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 "main/main_session.h"
#include "settings/settings_common.h"
@ -102,6 +103,28 @@ private:
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 {
public:
PreviewWrap(
@ -114,7 +137,9 @@ public:
const TextWithEntities &quote);
[[nodiscard]] rpl::producer<QString> showLinkSelector(
const TextWithTags &message,
Data::WebPageDraft webpage);
Data::WebPageDraft webpage,
const std::vector<MessageLinkRange> &links,
const QString &usedLink);
private:
void paintEvent(QPaintEvent *e) override;
@ -125,6 +150,10 @@ private:
void mouseDoubleClickEvent(QMouseEvent *e) override;
void initElement();
void highlightUsedLink(
const TextWithTags &message,
const QString &usedLink,
const std::vector<MessageLinkRange> &links);
void startSelection(TextSelectType type);
[[nodiscard]] TextSelection resolveNewSelection() const;
@ -138,12 +167,15 @@ private:
HistoryItem *_draftItem = nullptr;
std::unique_ptr<Element> _element;
rpl::variable<TextSelection> _selection;
rpl::event_stream<QString> _chosenUrl;
Ui::PeerUserpicView _userpic;
rpl::lifetime _elementLifetime;
QPoint _position;
base::Timer _trippleClickTimer;
ClickHandlerPtr _link;
ClickHandlerPtr _pressedLink;
TextSelectType _selectType = TextSelectType::Letters;
uint16 _symbol = 0;
uint16 _selectionStartSymbol = 0;
@ -219,6 +251,8 @@ rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
_selection.reset(element->selectionFromQuote(quote));
_element = std::move(element);
_link = _pressedLink = nullptr;
if (const auto was = base::take(_draftItem)) {
was->destroy();
}
@ -240,7 +274,9 @@ rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
rpl::producer<QString> PreviewWrap::showLinkSelector(
const TextWithTags &message,
Data::WebPageDraft webpage) {
Data::WebPageDraft webpage,
const std::vector<MessageLinkRange> &links,
const QString &usedLink) {
_selection.reset(TextSelection());
_element = nullptr;
@ -259,10 +295,10 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
base::unixtime::now(), // date
_history->session().userPeerId(),
QString(), // postAuthor
TextWithEntities{
HighlightParsedLinks({
message.text,
TextUtilities::ConvertTextTagsToEntities(message.tags),
},
}, links),
MTP_messageMediaWebPage(
MTP_flags(Flag()
| (webpage.forceLargeMedia
@ -287,8 +323,50 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
_section = Section::Link;
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) {
@ -385,7 +463,10 @@ void PreviewWrap::mouseMoveEvent(QMouseEvent *e) {
_over = true;
const auto text = (_section == Section::Reply)
&& (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) {
_textCursor = text;
_linkCursor = link;
@ -412,13 +493,16 @@ void PreviewWrap::mousePressEvent(QMouseEvent *e) {
startSelection(_trippleClickTimer.isActive()
? TextSelectType::Paragraphs
: TextSelectType::Letters);
} else {
_pressedLink = _link;
}
}
void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) {
if (!_selecting) {
return;
} else if (_section == Section::Reply) {
if (_section == Section::Reply) {
if (!_selecting) {
return;
}
const auto result = resolveNewSelection();
_selecting = false;
_selectType = TextSelectType::Letters;
@ -426,6 +510,12 @@ void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) {
setCursor(style::cur_default);
}
_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;
}
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 &quote, 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
void ShowReplyToChatBox(
@ -603,14 +963,9 @@ void ShowReplyToChatBox(
) | rpl::start_with_next(std::move(callback), state->box->lifetime());
}
void EditDraftOptions(
std::shared_ptr<ChatHelpers::Show> show,
not_null<History*> history,
Data::Draft draft,
Fn<void(FullReplyTo, Data::WebPageDraft)> done,
Fn<void()> highlight,
Fn<void()> clearOldDraft) {
const auto session = &show->session();
void EditDraftOptions(EditDraftOptionsArgs &&args) {
const auto &draft = args.draft;
const auto session = &args.show->session();
const auto replyItem = session->data().message(draft.reply.messageId);
const auto previewDataRaw = draft.webpage.id
? session->data().webpage(draft.webpage.id).get()
@ -623,225 +978,8 @@ void EditDraftOptions(
if (!replyItem && !previewData) {
return;
}
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setWidth(st::boxWideWidth);
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 &quote, 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());
}
}));
args.show->show(
Box(DraftOptionsBox, std::move(args), replyItem, previewData));
}
} // namespace HistoryView::Controls

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_drafts.h"
class History;
struct MessageLinkRange;
namespace ChatHelpers {
class Show;
@ -21,13 +22,21 @@ class SessionController;
namespace HistoryView::Controls {
void EditDraftOptions(
std::shared_ptr<ChatHelpers::Show> show,
not_null<History*> history,
Data::Draft draft,
Fn<void(FullReplyTo, Data::WebPageDraft)> done,
Fn<void()> highlight,
Fn<void()> clearOldDraft);
class WebpageResolver;
struct EditDraftOptionsArgs {
std::shared_ptr<ChatHelpers::Show> show;
not_null<History*> history;
Data::Draft draft;
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(
std::shared_ptr<ChatHelpers::Show> show,

View file

@ -110,17 +110,88 @@ WebPageText ProcessWebPageData(WebPageData *page) {
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(
not_null<History*> history,
not_null<Ui::InputField*> field)
: _history(history)
, _api(&history->session().mtp())
, _resolver(std::make_shared<WebpageResolver>(&history->session()))
, _parser(field)
, _timer([=] {
if (!ShowWebPagePreview(_data) || _link.isEmpty()) {
return;
}
request();
_resolver->request(_link);
}) {
_history->session().downloaderTaskFinished(
) | rpl::filter([=] {
@ -141,6 +212,23 @@ WebpageProcessor::WebpageProcessor(
_parsedLinks = std::move(parsed);
checkPreview();
}, _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 {
@ -151,8 +239,20 @@ Data::WebPageDraft WebpageProcessor::draft() const {
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) {
_api.request(base::take(_requestId)).cancel();
const auto was = _link;
if (draft.removed) {
_draft = draft;
if (_parsedLinks.empty()) {
@ -173,14 +273,21 @@ void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
: nullptr;
if (page && page->url == draft.url) {
_data = page;
if (const auto link = _resolver->find(page); !link.isEmpty()) {
_link = link;
}
updateFromData();
} else {
request();
_resolver->request(_link);
return;
}
} else if (!draft.manual && !_draft.manual) {
_draft = draft;
checkNow(reparse);
}
if (_link != was) {
_resolver->cancel(was);
}
}
void WebpageProcessor::updateFromData() {
@ -212,56 +319,6 @@ void WebpageProcessor::updateFromData() {
_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) {
_parser.setDisabled(disabled);
if (disabled) {
@ -307,25 +364,21 @@ void WebpageProcessor::checkPreview() {
auto page = (WebPageData*)nullptr;
auto chosen = QString();
for (const auto &link : _links) {
const auto i = _cache.find(link);
if (i == end(_cache)) {
const auto value = _resolver->lookup(link);
if (!value) {
chosen = link;
break;
} else if (i->second) {
if (i->second->failed) {
i->second = nullptr;
} else {
chosen = link;
page = i->second;
break;
}
} else if (*value) {
chosen = link;
page = *value;
break;
}
}
if (_link != chosen) {
_resolver->cancel(_link);
_link = chosen;
_api.request(base::take(_requestId)).cancel();
if (!page && !_link.isEmpty()) {
request();
_resolver->request(_link);
}
}
if (page) {

View file

@ -7,13 +7,16 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "data/data_drafts.h"
#include "chat_helpers/message_field.h"
#include "mtproto/sender.h"
class History;
namespace Main {
class Session;
} // namespace Main
namespace Ui {
class InputField;
} // 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:
WebpageProcessor(
not_null<History*> history,
@ -63,6 +92,9 @@ public:
// unless preview was removed in the draft or manual.
void apply(Data::WebPageDraft draft, bool reparse = true);
[[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<WebpageParsed> parsedValue() const;
@ -74,21 +106,17 @@ public:
private:
void updateFromData();
void checkPreview();
void request();
const not_null<History*> _history;
MTP::Sender _api;
const std::shared_ptr<WebpageResolver> _resolver;
MessageLinksParser _parser;
QStringList _parsedLinks;
QStringList _links;
QString _link;
WebPageData *_data = nullptr;
base::flat_map<QString, WebPageData*> _cache;
Data::WebPageDraft _draft;
mtpRequestId _requestId = 0;
rpl::event_stream<> _repaintRequests;
rpl::variable<WebpageParsed> _parsed;

View file

@ -49,6 +49,7 @@ struct TextState {
FullMsgId itemId;
CursorState cursor = CursorState::None;
ClickHandlerPtr link;
bool overMessageText = false;
bool afterSymbol = false;
bool customTooltip = false;
uint16 symbol = 0;

View file

@ -2195,6 +2195,7 @@ TextState Message::textState(
if (_invertMedia) {
result.symbol += visibleMediaTextLength();
}
result.overMessageText = true;
checkBottomInfoState();
return result;
} else if (point.y() >= trect.y() + trect.height()) {