mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-17 22:57:11 +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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 "e);
|
||||
[[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 "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
|
||||
|
||||
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 "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());
|
||||
}
|
||||
}));
|
||||
args.show->show(
|
||||
Box(DraftOptionsBox, std::move(args), replyItem, previewData));
|
||||
}
|
||||
|
||||
} // namespace HistoryView::Controls
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
Loading…
Add table
Reference in a new issue