Edit reply / webpage options together.

This commit is contained in:
John Preston 2023-10-25 21:23:54 +04:00
parent 1409d38ac3
commit 17578be4b9
17 changed files with 1525 additions and 1182 deletions

View File

@ -651,16 +651,18 @@ PRIVATE
history/view/controls/history_view_compose_controls.h
history/view/controls/history_view_compose_search.cpp
history/view/controls/history_view_compose_search.h
history/view/controls/history_view_draft_options.cpp
history/view/controls/history_view_draft_options.h
history/view/controls/history_view_forward_panel.cpp
history/view/controls/history_view_forward_panel.h
history/view/controls/history_view_reply_options.cpp
history/view/controls/history_view_reply_options.h
history/view/controls/history_view_ttl_button.cpp
history/view/controls/history_view_ttl_button.h
history/view/controls/history_view_voice_record_bar.cpp
history/view/controls/history_view_voice_record_bar.h
history/view/controls/history_view_voice_record_button.cpp
history/view/controls/history_view_voice_record_button.h
history/view/controls/history_view_webpage_processor.cpp
history/view/controls/history_view_webpage_processor.h
history/view/media/history_view_call.cpp
history/view/media/history_view_call.h
history/view/media/history_view_contact.cpp

View File

@ -37,6 +37,7 @@ WebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) {
.forceSmallMedia = !!(previewFlags & PageFlag::ForceSmallMedia),
.invert = item->invertMedia(),
.manual = !!(previewFlags & PageFlag::Manual),
.removed = !previewPage,
};
}

View File

@ -355,3 +355,30 @@ QString WebPageData::displayedSiteName() const {
? tr::lng_media_color_theme(tr::now)
: siteName;
}
bool WebPageData::computeDefaultSmallMedia() const {
if (!collage.items.empty()) {
return false;
} else if (siteName.isEmpty()
&& title.isEmpty()
&& description.empty()
&& author.isEmpty()) {
return false;
} else if (!document
&& photo
&& type != WebPageType::Photo
&& type != WebPageType::Document
&& type != WebPageType::Story
&& type != WebPageType::Video) {
if (type == WebPageType::Profile) {
return true;
} else if (siteName == u"Twitter"_q
|| siteName == u"Facebook"_q
|| type == WebPageType::ArticleWithIV) {
return false;
} else {
return true;
}
}
return false;
}

View File

@ -89,6 +89,7 @@ struct WebPageData {
const MTPmessages_Messages &result);
[[nodiscard]] QString displayedSiteName() const;
[[nodiscard]] bool computeDefaultSmallMedia() const;
const WebPageId id = 0;
WebPageType type = WebPageType::None;

View File

@ -14,7 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/view/controls/history_view_forward_panel.h"
#include "history/view/controls/history_view_reply_options.h"
#include "history/view/controls/history_view_draft_options.h"
#include "history/view/media/history_view_media.h"
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_web_page.h"

View File

@ -82,9 +82,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_unread_things.h"
#include "history/view/controls/history_view_compose_search.h"
#include "history/view/controls/history_view_forward_panel.h"
#include "history/view/controls/history_view_reply_options.h"
#include "history/view/controls/history_view_draft_options.h"
#include "history/view/controls/history_view_voice_record_bar.h"
#include "history/view/controls/history_view_ttl_button.h"
#include "history/view/controls/history_view_webpage_processor.h"
#include "history/view/reactions/history_view_reactions_button.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/history_view_service_message.h"
@ -206,7 +207,6 @@ HistoryWidget::HistoryWidget(
, _api(&controller->session().mtp())
, _updateEditTimeLeftDisplay([=] { updateField(); })
, _fieldBarCancel(this, st::historyReplyCancel)
, _previewTimer([=] { requestPreview(); })
, _topBar(this, controller)
, _scroll(
this,
@ -440,12 +440,6 @@ HistoryWidget::HistoryWidget(
if (_supportAutocomplete) {
supportInitAutocomplete();
}
_fieldLinksParser = std::make_unique<MessageLinksParser>(_field);
_fieldLinksParser->list().changes(
) | rpl::start_with_next([=](QStringList &&parsed) {
_parsedLinks = std::move(parsed);
checkPreview();
}, lifetime());
_field->rawTextEdit()->installEventFilter(this);
_field->rawTextEdit()->installEventFilter(_fieldAutocomplete);
_field->setMimeDataHook([=](
@ -566,13 +560,6 @@ HistoryWidget::HistoryWidget(
});
}, lifetime());
session().data().webPageUpdates(
) | rpl::filter([=](not_null<WebPageData*> page) {
return (_previewData == page.get());
}) | rpl::start_with_next([=] {
updatePreview();
}, lifetime());
session().data().channelDifferenceTooLong(
) | rpl::filter([=](not_null<ChannelData*> channel) {
return _peer == channel.get();
@ -723,9 +710,9 @@ HistoryWidget::HistoryWidget(
return update.flags;
}) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) {
if (flags & PeerUpdateFlag::Rights) {
checkPreview();
updateStickersByEmoji();
updateFieldPlaceholder();
_preview->checkNow(false);
}
if (flags & PeerUpdateFlag::Migration) {
handlePeerMigration();
@ -1600,7 +1587,9 @@ void HistoryWidget::fieldChanged() {
updateSendButtonType();
if (!HasSendText(_field)) {
_previewDraft = {};
if (_preview) {
_preview->apply({});
}
_fieldIsEmpty = true;
} else if (_fieldIsEmpty) {
_fieldIsEmpty = false;
@ -1664,14 +1653,14 @@ void HistoryWidget::saveFieldToHistoryLocalDraft() {
.messageId = FullMsgId(_history->peer->id, _editMsgId),
.topicRootId = topicRootId,
},
_previewDraft,
_preview->draft(),
_saveEditMsgRequestId));
} else {
if (_replyTo || !_field->empty()) {
_history->setLocalDraft(std::make_unique<Data::Draft>(
_field,
_replyTo,
_previewDraft));
_preview->draft()));
} else {
_history->clearLocalDraft(topicRootId);
}
@ -1897,6 +1886,9 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
_processingReplyItem = _replyEditMsg = nullptr;
_processingReplyTo = _replyTo = FullReplyTo();
setEditMsgId(0);
if (_preview) {
_preview->apply({});
}
if (fieldWillBeHiddenAfterEdit) {
updateControlsVisibility();
updateControlsGeometry();
@ -1930,18 +1922,9 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
processReply();
}
// Save links from _field to _parsedLinks without generating preview.
_previewDraft = { .removed = true };
if (_editMsgId) {
_fieldLinksParser->setDisabled(!_replyEditMsg
|| (_replyEditMsg->media()
&& !_replyEditMsg->media()->webpage()));
if (_preview) {
_preview->apply(draft->webpage, !_editMsgId);
}
_fieldLinksParser->parseNow();
_parsedLinks = _fieldLinksParser->list().current();
_previewDraft = draft->webpage;
checkPreview();
return true;
}
@ -2149,9 +2132,6 @@ void HistoryWidget::showHistory(
_canReplaceMedia = false;
_photoEditMedia = nullptr;
updateReplaceMediaButton();
_previewData = nullptr;
_previewDraft = {};
_previewCache.clear();
_fieldBarCancel->hide();
_membersDropdownShowTimer.cancel();
@ -2409,10 +2389,45 @@ void HistoryWidget::setHistory(History *history) {
_history = history;
_migrated = _history ? _history->migrateFrom() : nullptr;
registerDraftSource();
if (_history) {
setupPreview();
} else {
_previewDrawPreview = nullptr;
_preview = nullptr;
}
}
refreshAttachBotsMenu();
}
void HistoryWidget::setupPreview() {
Expects(_history != nullptr);
using namespace HistoryView::Controls;
_preview = std::make_unique<WebpageProcessor>(_history, _field);
_preview->repaintRequests() | rpl::start_with_next([=] {
updateField();
}, _preview->lifetime());
_preview->parsedValue(
) | rpl::start_with_next([=](WebpageParsed value) {
_previewTitle.setText(
st::msgNameStyle,
value.title,
Ui::NameTextOptions());
_previewDescription.setText(
st::defaultTextStyle,
value.description,
Ui::DialogTextOptions());
const auto changed = (!_previewDrawPreview != !value.drawPreview);
_previewDrawPreview = value.drawPreview;
if (changed) {
updateControlsGeometry();
updateControlsVisibility();
}
updateField();
}, _preview->lifetime());
}
void HistoryWidget::injectSponsoredMessages() const {
session().data().sponsoredMessages().inject(
_history,
@ -2468,7 +2483,7 @@ void HistoryWidget::registerDraftSource() {
? FullReplyTo{ FullMsgId(peerId, editMsgId) }
: _replyTo),
_field->getTextWithTags(),
_previewDraft,
_preview->draft(),
};
};
auto draftSource = Storage::MessageDraftSource{
@ -2486,9 +2501,6 @@ void HistoryWidget::registerDraftSource() {
void HistoryWidget::setEditMsgId(MsgId msgId) {
unregisterDraftSources();
_editMsgId = msgId;
if (_fieldLinksParser && !_editMsgId) {
_fieldLinksParser->setDisabled(false);
}
if (!msgId) {
_canReplaceMedia = false;
}
@ -2906,7 +2918,7 @@ void HistoryWidget::updateControlsVisibility() {
if (_editMsgId
|| _replyTo
|| readyToForward()
|| (_previewData && !_previewData->failed)
|| _previewDrawPreview
|| _kbReplyTo) {
if (_fieldBarCancel->isHidden()) {
_fieldBarCancel->show();
@ -3832,7 +3844,7 @@ void HistoryWidget::saveEditMsg() {
_saveEditMsgRequestId = Api::EditTextMessage(
item,
sending,
_previewDraft,
_preview->draft(),
options,
done,
fail);
@ -3918,7 +3930,7 @@ void HistoryWidget::send(Api::SendOptions options) {
auto message = Api::MessageToSend(prepareSendAction(options));
message.textWithTags = _field->getTextWithAppliedMarkdown();
message.webPage = _previewDraft;
message.webPage = _preview->draft();
const auto ignoreSlowmodeCountdown = (options.scheduled != 0);
if (showSendMessageError(
@ -3935,9 +3947,6 @@ void HistoryWidget::send(Api::SendOptions options) {
hideSelectorControlsAnimated();
if (_previewData && _previewData->pendingTill) {
previewCancel();
}
setInnerFocus();
if (!_keyboard->hasMarkup() && _keyboard->forceReply() && !_kbReplyTo) {
@ -4333,7 +4342,7 @@ void HistoryWidget::updateOverStates(QPoint pos) {
_field->y() - st::historySendPadding - st::historyReplyHeight,
width() - skip - _fieldBarCancel->width(),
st::historyReplyHeight);
const auto hasWebPage = _previewData && !_previewData->failed;
const auto hasWebPage = !!_previewDrawPreview;
const auto inDetails = detailsRect.contains(pos)
&& (_editMsgId || replyTo() || isReadyToForward || hasWebPage);
const auto inPhotoEdit = inDetails
@ -4595,9 +4604,7 @@ bool HistoryWidget::showRecordButton() const {
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
&& (!_previewData
|| _previewData->failed
|| _previewData->pendingTill)
&& !_previewDrawPreview
&& !readyToForward()
&& !_editMsgId;
}
@ -4781,7 +4788,7 @@ void HistoryWidget::toggleKeyboard(bool manual) {
_kbReplyTo = nullptr;
if (!readyToForward()
&& (!_previewData || _previewData->failed)
&& !_previewDrawPreview
&& !_editMsgId
&& !_replyTo) {
_fieldBarCancel->hide();
@ -5771,7 +5778,7 @@ void HistoryWidget::updateHistoryGeometry(
if (_editMsgId
|| replyTo()
|| readyToForward()
|| (_previewData && !_previewData->failed)) {
|| _previewDrawPreview) {
newScrollHeight -= st::historyReplyHeight;
}
if (_kbShown) {
@ -6074,7 +6081,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) {
_kbShown = false;
_kbReplyTo = nullptr;
if (!readyToForward()
&& (!_previewData || _previewData->failed)
&& !_previewDrawPreview
&& !_replyTo) {
_fieldBarCancel->hide();
updateMouseTracking();
@ -6092,7 +6099,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) {
_kbShown = false;
_kbReplyTo = nullptr;
if (!readyToForward()
&& (!_previewData || _previewData->failed)
&& !_previewDrawPreview
&& !_replyTo
&& !_editMsgId) {
_fieldBarCancel->hide();
@ -6139,7 +6146,7 @@ int HistoryWidget::computeMaxFieldHeight() const {
- ((_editMsgId
|| replyTo()
|| readyToForward()
|| (_previewData && !_previewData->failed))
|| _previewDrawPreview)
? st::historyReplyHeight
: 0)
- (2 * st::historySendPadding)
@ -6219,44 +6226,21 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) {
crl::guard(_list, [=] { cancelEdit(); }));
} else if (!_inDetails) {
return;
} else if (_previewData
&& !_previewData->failed
&& !_previewData->pendingTill) {
const auto history = _history;
using namespace HistoryView::Controls;
EditWebPageOptions(
controller()->uiShow(),
_previewData,
_previewDraft,
[=](Data::WebPageDraft draft) { applyPreview(draft); });
} else if (_previewDrawPreview) {
editDraftOptions();
} else if (isReadyToForward) {
if (e->button() != Qt::LeftButton) {
_forwardPanel->editToNextOption();
} else {
_forwardPanel->editOptions(controller()->uiShow());
}
} else if (const auto reply = replyTo()) {
const auto done = [=](FullReplyTo replyTo) {
if (replyTo) {
replyToMessage(replyTo);
} else {
cancelReply();
}
};
const auto highlight = [=] {
controller()->showPeerHistory(
reply.messageId.peer,
Window::SectionShow::Way::Forward,
reply.messageId.msg);
};
const auto history = _history;
using namespace HistoryView::Controls;
EditReplyOptions(
controller()->uiShow(),
reply,
done,
highlight,
[=] { ClearDraftReplyTo(history, reply.messageId); });
} else if (_replyTo) {
editDraftOptions();
} else if (_kbReplyTo) {
controller()->showPeerHistory(
_kbReplyTo->history()->peer->id,
Window::SectionShow::Way::Forward,
_kbReplyTo->id);
} else if (_editMsgId) {
controller()->showPeerHistory(
_peer,
@ -6265,6 +6249,42 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) {
}
}
void HistoryWidget::editDraftOptions() {
Expects(_history != nullptr);
const auto history = _history;
const auto reply = _replyTo;
const auto webpage = _preview->draft();
const auto done = [=](
FullReplyTo replyTo,
Data::WebPageDraft webpage) {
if (replyTo) {
replyToMessage(replyTo);
} else {
cancelReply();
}
if (_preview->draft() != webpage) {
_preview->apply(webpage);
}
};
const auto highlight = [=] {
controller()->showPeerHistory(
reply.messageId.peer,
Window::SectionShow::Way::Forward,
reply.messageId.msg);
};
using namespace HistoryView::Controls;
EditDraftOptions(
controller()->uiShow(),
history,
Data::Draft(_field, reply, _preview->draft()),
done,
highlight,
[=] { ClearDraftReplyTo(history, reply.messageId); });
}
void HistoryWidget::keyPressEvent(QKeyEvent *e) {
if (!_history) return;
@ -7075,9 +7095,8 @@ void HistoryWidget::setFieldText(
_textUpdateEvents = TextUpdateEvent::SaveDraft
| TextUpdateEvent::SendTyping;
if (!_previewDraft.manual) {
previewCancel();
_previewDraft = {};
if (_preview) {
_preview->checkNow(false);
}
}
@ -7239,7 +7258,7 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
_history->setLocalDraft(std::make_unique<Data::Draft>(
_field,
_replyTo,
_previewDraft));
_preview->draft()));
} else {
_history->clearLocalDraft({});
}
@ -7259,13 +7278,6 @@ void HistoryWidget::editMessage(not_null<HistoryItem*> item) {
previewDraft));
applyDraft();
_previewData = previewDraft.id
? session().data().webpage(previewDraft.id).get()
: nullptr;
if (_previewData) {
updatePreview();
}
updateBotKeyboard();
if (fieldOrDisabledShown()) {
@ -7332,7 +7344,7 @@ bool HistoryWidget::cancelReply(bool lastKeyboardUsed) {
_processingReplyTo = _replyTo = FullReplyTo();
mouseMoveEvent(0);
if (!readyToForward()
&& (!_previewData || _previewData->failed)
&& !_previewDrawPreview
&& !_kbReplyTo) {
_fieldBarCancel->hide();
updateMouseTracking();
@ -7405,7 +7417,7 @@ void HistoryWidget::cancelEdit() {
mouseMoveEvent(nullptr);
if (!readyToForward()
&& (!_previewData || _previewData->failed)
&& !_previewDrawPreview
&& !replyTo()) {
_fieldBarCancel->hide();
updateMouseTracking();
@ -7427,8 +7439,8 @@ void HistoryWidget::cancelEdit() {
void HistoryWidget::cancelFieldAreaState() {
controller()->hideLayer();
_replyForwardPressed = false;
if (_previewData && !_previewData->failed) {
applyPreview({ .removed = true });
if (_previewDrawPreview) {
_preview->apply({ .removed = true });
} else if (_editMsgId) {
cancelEdit();
} else if (readyToForward()) {
@ -7440,165 +7452,6 @@ void HistoryWidget::cancelFieldAreaState() {
}
}
void HistoryWidget::applyPreview(Data::WebPageDraft draft) {
_previewDraft = draft;
if (draft.removed) {
previewCancel();
} else if (draft.id) {
_previewData = session().data().webpage(draft.id).get();
requestPreview();
}
_saveDraftText = true;
_saveDraftStart = crl::now();
saveDraft();
}
void HistoryWidget::previewCancel() {
_api.request(base::take(_previewRequest)).cancel();
_previewData = nullptr;
_previewLinks.clear();
updatePreview();
}
void HistoryWidget::checkPreview() {
const auto previewRestricted = [&] {
return _peer && _peer->amRestricted(ChatRestriction::EmbedLinks);
}();
if (_previewDraft.removed || previewRestricted) {
previewCancel();
return;
} else if (_previewDraft.manual) {
return;
}
const auto links = _parsedLinks.join(' ');
if (_previewLinks != links) {
_api.request(base::take(_previewRequest)).cancel();
_previewLinks = links;
if (_previewLinks.isEmpty()) {
if (_previewData && !_previewData->failed) {
previewCancel();
}
} else {
const auto i = _previewCache.constFind(links);
if (i == _previewCache.cend()) {
_previewRequest = _api.request(MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(links),
MTPVector<MTPMessageEntity>()
)).done([=](const MTPMessageMedia &result, mtpRequestId requestId) {
gotPreview(links, result, requestId);
}).send();
} else if (i.value()) {
_previewData = session().data().webpage(i.value());
_previewDraft.id = _previewData->id;
_previewDraft.url = _previewData->url;
updatePreview();
} else if (_previewData && !_previewData->failed) {
previewCancel();
}
}
}
}
void HistoryWidget::requestPreview() {
if (!_previewData || _previewData->failed || _previewLinks.isEmpty()) {
return;
}
const auto links = _previewLinks;
_previewRequest = _api.request(MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(links),
MTPVector<MTPMessageEntity>()
)).done([=](const MTPMessageMedia &result, mtpRequestId requestId) {
gotPreview(links, result, requestId);
}).send();
}
void HistoryWidget::gotPreview(
QString links,
const MTPMessageMedia &result,
mtpRequestId req) {
if (req == _previewRequest) {
_previewRequest = 0;
}
if (result.type() == mtpc_messageMediaWebPage) {
const auto &data = result.c_messageMediaWebPage().vwebpage();
const auto page = session().data().processWebpage(data);
_previewCache.insert(links, page->id);
if (page->pendingTill > 0
&& page->pendingTill <= base::unixtime::now()) {
page->pendingTill = 0;
page->failed = true;
}
if (links == _previewLinks && !_previewDraft.removed) {
_previewData = (page->id && !page->failed)
? page.get()
: nullptr;
if (_previewData) {
_previewDraft.id = _previewData->id;
_previewDraft.url = _previewData->url;
} else {
_previewDraft = {};
}
updatePreview();
}
session().data().sendWebPageGamePollNotifications();
} else if (result.type() == mtpc_messageMediaEmpty) {
_previewCache.insert(links, 0);
if (links == _previewLinks && !_previewDraft.removed) {
_previewData = nullptr;
_previewDraft = {};
updatePreview();
}
}
}
void HistoryWidget::updatePreview() {
_previewTimer.cancel();
if (_previewData && !_previewData->failed) {
_fieldBarCancel->show();
updateMouseTracking();
if (_previewData->pendingTill) {
_previewTitle.setText(
st::msgNameStyle,
tr::lng_preview_loading(tr::now),
Ui::NameTextOptions());
auto linkText = QStringView(_previewLinks).split(' ').at(0).toString();
_previewDescription.setText(
st::defaultTextStyle,
linkText,
Ui::DialogTextOptions());
const auto timeout = (_previewData->pendingTill - base::unixtime::now());
_previewTimer.callOnce(std::max(timeout, 0) * crl::time(1000));
} else {
auto preview =
HistoryView::TitleAndDescriptionFromWebPage(_previewData);
if (preview.title.isEmpty()) {
if (_previewData->document) {
preview.title = tr::lng_attach_file(tr::now);
} else if (_previewData->photo) {
preview.title = tr::lng_attach_photo(tr::now);
}
}
_previewTitle.setText(
st::msgNameStyle,
preview.title,
Ui::NameTextOptions());
_previewDescription.setText(
st::defaultTextStyle,
preview.description,
Ui::DialogTextOptions());
}
} else if (!readyToForward() && !replyTo() && !_editMsgId) {
_fieldBarCancel->hide();
updateMouseTracking();
}
updateControlsGeometry();
update();
}
void HistoryWidget::fullInfoUpdated() {
auto refresh = false;
if (_list) {
@ -7960,12 +7813,11 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) {
} else if (hasForward) {
backy -= st::historyReplyHeight;
backh += st::historyReplyHeight;
} else if (_previewData && !_previewData->failed) {
} else if (_previewDrawPreview) {
backy -= st::historyReplyHeight;
backh += st::historyReplyHeight;
}
auto drawWebPagePreview = (_previewData && !_previewData->failed)
&& !_replyForwardPressed;
auto drawWebPagePreview = _previewDrawPreview && !_replyForwardPressed;
p.setInactive(
controller()->isGifPausedAtLeastFor(Window::GifPauseReason::Any));
p.fillRect(myrtlrect(0, backy, width(), backh), st::historyReplyBg);
@ -8072,7 +7924,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) {
backy + (st::historyReplyHeight - st::historyReplyPreview) / 2,
st::historyReplyPreview,
st::historyReplyPreview);
if (HistoryView::DrawWebPageDataPreview(p, _previewData, _peer, to)) {
if (_previewDrawPreview(p, to)) {
previewLeft += st::historyReplyPreview + st::msgReplyBarSkip;
}
p.setPen(st::historyReplyNameFg);

View File

@ -94,13 +94,15 @@ class Element;
class PinnedTracker;
class TranslateBar;
class ComposeSearch;
namespace Controls {
} // namespace HistoryView
namespace HistoryView::Controls {
class RecordLock;
class VoiceRecordBar;
class ForwardPanel;
class TTLButton;
} // namespace Controls
} // namespace HistoryView
class WebpageProcessor;
} // namespace HistoryView::Controls
class BotKeyboard;
class HistoryInner;
@ -200,9 +202,6 @@ public:
[[nodiscard]] QVector<FullMsgId> replyReturns() const;
void setReplyReturns(PeerId peer, QVector<FullMsgId> replyReturns);
void updatePreview();
void previewCancel();
void escape();
void sendBotCommand(const Bot::SendCommandRequest &request);
@ -406,7 +405,6 @@ private:
void startBotCommand();
void hidePinnedMessage();
void cancelFieldAreaState();
void applyPreview(Data::WebPageDraft draft);
void unblockUser();
void sendBotStartCommand();
void joinChannel();
@ -540,9 +538,9 @@ private:
void saveEditMsg();
void checkPreview();
void requestPreview();
void gotPreview(QString links, const MTPMessageMedia &media, mtpRequestId req);
void setupPreview();
void editDraftOptions();
void messagesReceived(not_null<PeerData*> peer, const MTPmessages_Messages &messages, int requestId);
void messagesFailed(const MTP::Error &error, int requestId);
void addMessagesToFront(not_null<PeerData*> peer, const QVector<MTPMessage> &messages);
@ -674,16 +672,10 @@ private:
mtpRequestId _saveEditMsgRequestId = 0;
QStringList _parsedLinks;
QString _previewLinks;
WebPageData *_previewData = nullptr;
typedef QMap<QString, WebPageId> PreviewCache;
PreviewCache _previewCache;
mtpRequestId _previewRequest = 0;
std::unique_ptr<HistoryView::Controls::WebpageProcessor> _preview;
Fn<bool(QPainter &p, QRect to)> _previewDrawPreview;
Ui::Text::String _previewTitle;
Ui::Text::String _previewDescription;
base::Timer _previewTimer;
Data::WebPageDraft _previewDraft;
bool _replyForwardPressed = false;
@ -725,7 +717,6 @@ private:
const object_ptr<FieldAutocomplete> _fieldAutocomplete;
object_ptr<Support::Autocomplete> _supportAutocomplete;
std::unique_ptr<MessageLinksParser> _fieldLinksParser;
UserData *_inlineBot = nullptr;
QString _inlineBotUsername;

View File

@ -47,9 +47,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_item.h"
#include "history/view/controls/history_view_forward_panel.h"
#include "history/view/controls/history_view_reply_options.h"
#include "history/view/controls/history_view_draft_options.h"
#include "history/view/controls/history_view_voice_record_bar.h"
#include "history/view/controls/history_view_ttl_button.h"
#include "history/view/controls/history_view_webpage_processor.h"
#include "history/view/history_view_webpage_preview.h"
#include "inline_bots/bot_attach_web_view.h"
#include "inline_bots/inline_results_widget.h"
@ -120,222 +121,6 @@ WebPageText ProcessWebPageData(WebPageData *page) {
} // namespace
class WebpageProcessor final {
public:
WebpageProcessor(
not_null<History*> history,
not_null<Ui::InputField*> field);
void cancel();
void checkPreview();
[[nodiscard]] Data::WebPageDraft draft() const;
void setAllowed(bool allowed);
void refreshDraft(Data::WebPageDraft draft, bool disable);
[[nodiscard]] rpl::producer<> paintRequests() const;
[[nodiscard]] rpl::producer<QString> titleChanges() const;
[[nodiscard]] rpl::producer<QString> descriptionChanges() const;
[[nodiscard]] rpl::producer<WebPageData*> pageDataChanges() const;
private:
void updatePreview();
void getWebPagePreview();
const not_null<History*> _history;
MTP::Sender _api;
MessageLinksParser _fieldLinksParser;
Data::WebPageDraft _previewDraft;
QStringList _parsedLinks;
QString _previewLinks;
WebPageData *_previewData = nullptr;
std::map<QString, WebPageId> _previewCache;
mtpRequestId _previewRequest = 0;
rpl::event_stream<> _paintRequests;
rpl::event_stream<QString> _titleChanges;
rpl::event_stream<QString> _descriptionChanges;
rpl::event_stream<WebPageData*> _pageDataChanges;
base::Timer _timer;
rpl::lifetime _lifetime;
};
WebpageProcessor::WebpageProcessor(
not_null<History*> history,
not_null<Ui::InputField*> field)
: _history(history)
, _api(&history->session().mtp())
, _fieldLinksParser(field)
, _timer([=] {
if (!ShowWebPagePreview(_previewData)
|| _previewLinks.isEmpty()) {
return;
}
getWebPagePreview();
}) {
_history->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _previewData
&& (_previewData->document || _previewData->photo);
}) | rpl::start_with_next([=] {
_paintRequests.fire({});
}, _lifetime);
_history->owner().webPageUpdates(
) | rpl::filter([=](not_null<WebPageData*> page) {
return (_previewData == page.get());
}) | rpl::start_with_next([=] {
updatePreview();
}, _lifetime);
_fieldLinksParser.list().changes(
) | rpl::start_with_next([=](QStringList &&parsed) {
_parsedLinks = std::move(parsed);
checkPreview();
}, _lifetime);
}
rpl::producer<> WebpageProcessor::paintRequests() const {
return _paintRequests.events();
}
Data::WebPageDraft WebpageProcessor::draft() const {
return _previewDraft;
}
void WebpageProcessor::setAllowed(bool allowed) {
_previewDraft.removed = !allowed;
}
void WebpageProcessor::refreshDraft(
Data::WebPageDraft draft,
bool disable) {
// Save links from _field to _parsedLinks without generating preview.
_previewDraft = { .removed = true };
_fieldLinksParser.setDisabled(disable);
_fieldLinksParser.parseNow();
_parsedLinks = _fieldLinksParser.list().current();
_previewDraft = draft;
checkPreview();
}
void WebpageProcessor::cancel() {
_api.request(base::take(_previewRequest)).cancel();
_previewData = nullptr;
_previewLinks.clear();
updatePreview();
}
void WebpageProcessor::updatePreview() {
_timer.cancel();
auto t = QString();
auto d = QString();
if (ShowWebPagePreview(_previewData)) {
if (const auto till = _previewData->pendingTill) {
t = tr::lng_preview_loading(tr::now);
d = QStringView(_previewLinks).split(' ').at(0).toString();
const auto timeout = till - base::unixtime::now();
_timer.callOnce(
std::max(timeout, 0) * crl::time(1000));
} else {
const auto preview = ProcessWebPageData(_previewData);
t = preview.title;
d = preview.description;
}
}
_titleChanges.fire_copy(t);
_descriptionChanges.fire_copy(d);
_pageDataChanges.fire_copy(_previewData);
_paintRequests.fire({});
}
void WebpageProcessor::getWebPagePreview() {
const auto links = _previewLinks;
_previewRequest = _api.request(
MTPmessages_GetWebPagePreview(
MTP_flags(0),
MTP_string(links),
MTPVector<MTPMessageEntity>()
)).done([=](const MTPMessageMedia &result) {
_previewRequest = 0;
result.match([=](const MTPDmessageMediaWebPage &d) {
const auto page = _history->owner().processWebpage(d.vwebpage());
_previewCache.insert({ links, page->id });
if (page->pendingTill > 0
&& page->pendingTill <= base::unixtime::now()) {
page->pendingTill = 0;
page->failed = true;
}
if (links == _previewLinks && !_previewDraft.removed) {
_previewData = (page->id && !page->failed)
? page.get()
: nullptr;
updatePreview();
}
}, [=](const MTPDmessageMediaEmpty &d) {
_previewCache.insert({ links, 0 });
if (links == _previewLinks && !_previewDraft.removed) {
_previewData = nullptr;
updatePreview();
}
}, [](const auto &d) {
});
}).fail([=] {
_previewRequest = 0;
}).send();
}
void WebpageProcessor::checkPreview() {
const auto previewRestricted = _history->peer
&& _history->peer->amRestricted(ChatRestriction::EmbedLinks);
if (_previewDraft.removed || previewRestricted) {
cancel();
return;
}
const auto newLinks = _parsedLinks.join(' ');
if (_previewLinks == newLinks) {
return;
}
_api.request(base::take(_previewRequest)).cancel();
_previewLinks = newLinks;
if (_previewLinks.isEmpty()) {
if (ShowWebPagePreview(_previewData)) {
cancel();
}
} else {
const auto i = _previewCache.find(_previewLinks);
if (i == _previewCache.end()) {
getWebPagePreview();
} else if (i->second) {
_previewData = _history->owner().webpage(i->second);
updatePreview();
} else if (ShowWebPagePreview(_previewData)) {
cancel();
}
}
}
rpl::producer<QString> WebpageProcessor::titleChanges() const {
return _titleChanges.events();
}
rpl::producer<QString> WebpageProcessor::descriptionChanges() const {
return _descriptionChanges.events();
}
rpl::producer<WebPageData*> WebpageProcessor::pageDataChanges() const {
return _pageDataChanges.events();
}
class FieldHeader final : public Ui::RpWidget {
public:
FieldHeader(
@ -350,10 +135,7 @@ public:
void updateForwarding(
Data::Thread *thread,
Data::ResolvedForwardDraft items);
void previewRequested(
rpl::producer<QString> title,
rpl::producer<QString> description,
rpl::producer<WebPageData*> page);
void previewReady(rpl::producer<Controls::WebpageParsed> parsed);
void previewUnregister();
[[nodiscard]] bool isDisplayed() const;
@ -366,7 +148,6 @@ public:
[[nodiscard]] rpl::producer<FullMsgId> scrollToItemRequests() const;
[[nodiscard]] rpl::producer<> editPhotoRequests() const;
[[nodiscard]] MessageToEdit queryToEdit();
[[nodiscard]] Data::WebPageDraft webPageDraft() const;
[[nodiscard]] FullReplyTo getDraftReply() const;
[[nodiscard]] rpl::producer<> editCancelled() const {
@ -399,18 +180,14 @@ private:
bool hasPreview() const;
struct Preview {
WebPageData *data = nullptr;
Data::WebPageDraft draft;
Controls::WebpageParsed parsed;
Ui::Text::String title;
Ui::Text::String description;
bool cancelled = false;
};
const std::shared_ptr<ChatHelpers::Show> _show;
History *_history = nullptr;
MsgId _topicRootId = 0;
rpl::variable<QString> _title;
rpl::variable<QString> _description;
Preview _preview;
rpl::event_stream<> _editCancelled;
@ -493,7 +270,7 @@ void FieldHeader::init() {
st::historyReplyIcon.paint(p, position, width());
}
(ShowWebPagePreview(_preview.data) && !*leftIconPressed)
(_preview.parsed && !*leftIconPressed)
? paintWebPage(
p,
_history ? _history->peer : _data->session().user())
@ -548,22 +325,6 @@ void FieldHeader::init() {
update();
});
_title.value(
) | rpl::start_with_next([=](const auto &t) {
_preview.title.setText(
st::msgNameStyle,
t,
Ui::NameTextOptions());
}, lifetime());
_description.value(
) | rpl::start_with_next([=](const auto &d) {
_preview.description.setText(
st::messageTextStyle,
d,
Ui::DialogTextOptions());
}, lifetime());
setMouseTracking(true);
events(
) | rpl::filter([=](not_null<QEvent*> event) {
@ -619,28 +380,32 @@ void FieldHeader::init() {
if (!isEditingMessage() && readyToForward()) {
_forwardPanel->editOptions(_show);
} else if (!isEditingMessage() && reply) {
using namespace Controls;
const auto highlight = [=] {
_scrollToItemRequests.fire_copy(reply.messageId);
};
const auto history = _history;
const auto topicRootId = _topicRootId;
const auto done = [=](FullReplyTo replyTo) {
if (replyTo) {
replyToMessage(replyTo);
} else {
_replyCancelled.fire({});
}
};
const auto clearOldReplyTo = [=, id = reply.messageId] {
ClearDraftReplyTo(history, topicRootId, id);
};
EditReplyOptions(
_show,
reply,
done,
highlight,
clearOldReplyTo);
//using namespace Controls;
//const auto highlight = [=] {
// _scrollToItemRequests.fire_copy(reply.messageId);
//};
//const auto history = _history;
//const auto topicRootId = _topicRootId;
//const auto done = [=](
// FullReplyTo replyTo,
// Data::WebPageDraft webpage) {
// if (replyTo) {
// replyToMessage(replyTo);
// } else {
// _replyCancelled.fire({});
// }
//};
//const auto clearOldReplyTo = [=, id = reply.messageId] {
// ClearDraftReplyTo(history, topicRootId, id);
//};
//EditDraftOptions(
// _show,
// _history,
// Data::Draft( reply,
// done,
// highlight,
// clearOldReplyTo);
} else {
auto id = isEditingMessage()
? _editMsgId.current()
@ -683,17 +448,17 @@ void FieldHeader::setShownMessage(HistoryItem *item) {
_shownMessage = item;
if (item) {
updateShownMessageText();
if (item->fullId() == _editMsgId.current()) {
_preview = {};
if (const auto media = item->media()) {
if (const auto page = media->webpage()) {
const auto preview = ProcessWebPageData(page);
_title = preview.title;
_description = preview.description;
_preview.data = page;
}
}
}
//if (item->fullId() == _editMsgId.current()) {
// _preview = {};
// if (const auto media = item->media()) {
// if (const auto page = media->webpage()) {
// const auto preview = ProcessWebPageData(page);
// _title = preview.title;
// _description = preview.description;
// _preview.data = page;
// }
// }
//}
} else {
_shownMessageText.clear();
resolveMessageData();
@ -737,34 +502,22 @@ void FieldHeader::resolveMessageData() {
_data->session().api().requestMessageData(peer, itemId, callback);
}
void FieldHeader::previewRequested(
rpl::producer<QString> title,
rpl::producer<QString> description,
rpl::producer<WebPageData*> page) {
void FieldHeader::previewReady(
rpl::producer<Controls::WebpageParsed> parsed) {
_previewLifetime.destroy();
std::move(
title
) | rpl::filter([=] {
return !_preview.cancelled;
}) | rpl::start_with_next([=](const QString &t) {
_title = t;
}, _previewLifetime);
std::move(
description
) | rpl::filter([=] {
return !_preview.cancelled;
}) | rpl::start_with_next([=](const QString &d) {
_description = d;
}, _previewLifetime);
std::move(
page
) | rpl::filter([=] {
return !_preview.cancelled;
}) | rpl::start_with_next([=](WebPageData *p) {
_preview.data = p;
parsed
) | rpl::start_with_next([=](Controls::WebpageParsed parsed) {
_preview.parsed = std::move(parsed);
_preview.title.setText(
st::msgNameStyle,
_preview.parsed.title,
Ui::NameTextOptions());
_preview.description.setText(
st::messageTextStyle,
_preview.parsed.description,
Ui::DialogTextOptions());
updateVisible();
}, _previewLifetime);
}
@ -774,7 +527,7 @@ void FieldHeader::previewUnregister() {
}
void FieldHeader::paintWebPage(Painter &p, not_null<PeerData*> context) {
Expects(ShowWebPagePreview(_preview.data));
Expects(!!_preview.parsed);
const auto textTop = st::msgReplyPadding.top();
auto previewLeft = st::historyReplySkip + st::msgReplyBarSkip;
@ -784,7 +537,7 @@ void FieldHeader::paintWebPage(Painter &p, not_null<PeerData*> context) {
(st::historyReplyHeight - st::historyReplyPreview) / 2,
st::historyReplyPreview,
st::historyReplyPreview);
if (HistoryView::DrawWebPageDataPreview(p, _preview.data, context, to)) {
if (_preview.parsed.drawPreview(p, to)) {
previewLeft += st::historyReplyPreview + st::msgReplyBarSkip;
}
const auto elidedWidth = width()
@ -965,13 +718,7 @@ FullReplyTo FieldHeader::replyingToMessage() const {
}
bool FieldHeader::hasPreview() const {
return ShowWebPagePreview(_preview.data);
}
Data::WebPageDraft FieldHeader::webPageDraft() const {
return hasPreview()
? Data::WebPageDraft{ .id = _preview.data->id }
: Data::WebPageDraft{ .removed = true };
return !!_preview.parsed;
}
FullReplyTo FieldHeader::getDraftReply() const {
@ -1476,8 +1223,7 @@ void ComposeControls::setFieldText(
| TextUpdateEvent::SendTyping;
if (_preview) {
_preview->cancel();
_preview->setAllowed(true);
_preview->checkNow(false);
}
}
@ -1626,7 +1372,7 @@ void ComposeControls::init() {
_header->previewCancelled(
) | rpl::start_with_next([=] {
if (_preview) {
_preview->setAllowed(false);
_preview->apply({ .removed = true });
}
_saveDraftText = true;
_saveDraftStart = crl::now();
@ -2010,7 +1756,7 @@ void ComposeControls::fieldChanged() {
updateSendButtonType();
_hasSendText = HasSendText(_field);
if (!_hasSendText.current() && _preview) {
_preview->setAllowed(true);
_preview->apply({});
}
if (updateBotCommandShown() || updateLikeShown()) {
updateControlsVisibility();
@ -2180,7 +1926,9 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
}
_header->editMessage({});
_header->replyToMessage({});
_preview->refreshDraft({}, false);
if (_preview) {
_preview->apply({});
}
_canReplaceMedia = false;
_photoEditMedia = nullptr;
return;
@ -2194,15 +1942,13 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
draft->cursor.applyTo(_field);
_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
if (_preview) {
const auto disablePreview = (editDraft != nullptr);
_preview->refreshDraft(draft->webpage, disablePreview);
_preview->apply(draft->webpage, draft != editDraft);
}
if (draft == editDraft) {
const auto resolve = [=] {
if (const auto item = _history->owner().message(editingId)) {
const auto media = item->media();
const auto disablePreview = media && !media->webpage();
_canReplaceMedia = media && media->allowsEditMedia();
_photoEditMedia = (_canReplaceMedia
&& _regularWindow
@ -2216,7 +1962,11 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
item->fullId());
}
_header->editMessage(editingId, _photoEditMedia != nullptr);
_preview->refreshDraft(_preview->draft(), disablePreview);
if (_preview) {
_preview->apply(
Data::WebPageDraft::FromItem(item),
false);
}
return true;
}
_canReplaceMedia = false;
@ -3070,9 +2820,11 @@ void ComposeControls::initWebpageProcess() {
return;
}
_preview = std::make_unique<WebpageProcessor>(_history, _field);
_preview = std::make_unique<Controls::WebpageProcessor>(
_history,
_field);
_preview->paintRequests(
_preview->repaintRequests(
) | rpl::start_with_next(crl::guard(_header.get(), [=] {
_header->update();
}), _historyLifetime);
@ -3088,7 +2840,7 @@ void ComposeControls::initWebpageProcess() {
return update.flags;
}) | rpl::start_with_next([=](Data::PeerUpdate::Flags flags) {
if (flags & Data::PeerUpdate::Flag::Rights) {
_preview->checkPreview();
_preview->checkNow(false);
updateStickersByEmoji();
updateFieldPlaceholder();
}
@ -3106,10 +2858,7 @@ void ComposeControls::initWebpageProcess() {
}
}, _historyLifetime);
_header->previewRequested(
_preview->titleChanges(),
_preview->descriptionChanges(),
_preview->pageDataChanges());
_header->previewReady(_preview->parsedValue());
}
void ComposeControls::initForwardProcess() {
@ -3129,7 +2878,7 @@ void ComposeControls::initForwardProcess() {
}
Data::WebPageDraft ComposeControls::webPageDraft() const {
return _header->webPageDraft();
return _preview ? _preview->draft() : Data::WebPageDraft();
}
rpl::producer<Data::MessagePosition> ComposeControls::scrollRequests() const {

View File

@ -79,15 +79,15 @@ namespace Api {
enum class SendProgressType;
} // namespace Api
namespace HistoryView {
namespace Controls {
namespace HistoryView::Controls {
class VoiceRecordBar;
class TTLButton;
} // namespace Controls
class WebpageProcessor;
} // namespace HistoryView::Controls
namespace HistoryView {
class FieldHeader;
class WebpageProcessor;
enum class ComposeControlsMode {
Normal,
@ -422,7 +422,7 @@ private:
std::shared_ptr<Data::PhotoMedia> _photoEditMedia;
bool _canReplaceMedia = false;
std::unique_ptr<WebpageProcessor> _preview;
std::unique_ptr<Controls::WebpageProcessor> _preview;
Fn<void()> _raiseEmojiSuggestions;

View File

@ -0,0 +1,846 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/controls/history_view_draft_options.h"
#include "base/unixtime.h"
#include "boxes/peer_list_box.h"
#include "boxes/peer_list_controllers.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_changes.h"
#include "data/data_drafts.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_thread.h"
#include "data/data_user.h"
#include "data/data_web_page.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"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/discrete_sliders.h"
#include "ui/painter.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include <QtWidgets/QApplication>
#include <QtWidgets/QWidget>
namespace HistoryView::Controls {
namespace {
enum class Section {
Reply,
Link,
};
class PreviewDelegate final : public DefaultElementDelegate {
public:
PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
};
[[nodiscard]] std::unique_ptr<Ui::ChatTheme> DefaultThemeOn(
rpl::lifetime &lifetime) {
auto result = std::make_unique<Ui::ChatTheme>();
using namespace Window::Theme;
const auto push = [=, raw = result.get()] {
const auto background = Background();
const auto &paper = background->paper();
raw->setBackground({
.prepared = background->prepared(),
.preparedForTiled = background->preparedForTiled(),
.gradientForFill = background->gradientForFill(),
.colorForFill = background->colorForFill(),
.colors = paper.backgroundColors(),
.patternOpacity = paper.patternOpacity(),
.gradientRotation = paper.gradientRotation(),
.isPattern = paper.isPattern(),
.tile = background->tile(),
});
};
push();
Background()->updates(
) | rpl::start_with_next([=](const BackgroundUpdate &update) {
if (update.type == BackgroundUpdate::Type::New
|| update.type == BackgroundUpdate::Type::Changed) {
push();
}
}, lifetime);
return result;
}
class PreviewWrap final : public Ui::RpWidget {
public:
PreviewWrap(
not_null<Ui::GenericBox*> box,
not_null<History*> history);
~PreviewWrap();
[[nodiscard]] rpl::producer<TextWithEntities> showQuoteSelector(
not_null<HistoryItem*> item,
const TextWithEntities &quote);
[[nodiscard]] rpl::producer<QString> showLinkSelector(
const TextWithTags &message,
Data::WebPageDraft webpage);
private:
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
void initElement();
void startSelection(TextSelectType type);
[[nodiscard]] TextSelection resolveNewSelection() const;
const not_null<Ui::GenericBox*> _box;
const not_null<History*> _history;
const std::unique_ptr<Ui::ChatTheme> _theme;
const std::unique_ptr<Ui::ChatStyle> _style;
const std::unique_ptr<PreviewDelegate> _delegate;
Section _section = Section::Reply;
HistoryItem *_draftItem = nullptr;
std::unique_ptr<Element> _element;
rpl::variable<TextSelection> _selection;
Ui::PeerUserpicView _userpic;
rpl::lifetime _elementLifetime;
QPoint _position;
base::Timer _trippleClickTimer;
TextSelectType _selectType = TextSelectType::Letters;
uint16 _symbol = 0;
uint16 _selectionStartSymbol = 0;
bool _onlyMessageText = false;
bool _afterSymbol = false;
bool _selectionStartAfterSymbol = false;
bool _over = false;
bool _textCursor = false;
bool _linkCursor = false;
bool _selecting = false;
};
PreviewWrap::PreviewWrap(
not_null<Ui::GenericBox*> box,
not_null<History*> history)
: RpWidget(box)
, _box(box)
, _history(history)
, _theme(DefaultThemeOn(lifetime()))
, _style(std::make_unique<Ui::ChatStyle>())
, _delegate(std::make_unique<PreviewDelegate>(
box,
_style.get(),
[=] { update(); }))
, _position(0, st::msgMargin.bottom()) {
_style->apply(_theme.get());
const auto session = &_history->session();
session->data().viewRepaintRequest(
) | rpl::start_with_next([=](not_null<const Element*> view) {
if (view == _element.get()) {
update();
}
}, lifetime());
_selection.changes() | rpl::start_with_next([=] {
update();
}, lifetime());
_box->setAttribute(Qt::WA_OpaquePaintEvent, false);
_box->paintRequest() | rpl::start_with_next([=](QRect clip) {
const auto geometry = Ui::MapFrom(_box, this, rect());
const auto fill = geometry.intersected(clip);
if (!fill.isEmpty()) {
auto p = QPainter(_box);
p.setClipRect(fill);
Window::SectionWidget::PaintBackground(
p,
_theme.get(),
QSize(_box->width(), _box->window()->height()),
fill);
}
}, lifetime());
setMouseTracking(true);
}
PreviewWrap::~PreviewWrap() {
_selection.reset(TextSelection());
_elementLifetime.destroy();
_element = nullptr;
if (_draftItem) {
_draftItem->destroy();
}
}
rpl::producer<TextWithEntities> PreviewWrap::showQuoteSelector(
not_null<HistoryItem*> item,
const TextWithEntities &quote) {
auto element = item->createView(_delegate.get());
_selection.reset(element->selectionFromQuote(quote));
_element = std::move(element);
if (const auto was = base::take(_draftItem)) {
was->destroy();
}
const auto media = item->media();
_onlyMessageText = media
&& (media->webpage()
|| media->game()
|| (!media->photo() && !media->document()));
_section = Section::Reply;
initElement();
return _selection.value(
) | rpl::map([=](TextSelection selection) {
return _element->selectedQuote(selection);
});
}
rpl::producer<QString> PreviewWrap::showLinkSelector(
const TextWithTags &message,
Data::WebPageDraft webpage) {
_selection.reset(TextSelection());
_element = nullptr;
if (const auto was = base::take(_draftItem)) {
was->destroy();
}
using Flag = MTPDmessageMediaWebPage::Flag;
_draftItem = _history->addNewLocalMessage(
_history->nextNonHistoryEntryId(),
(MessageFlag::FakeHistoryItem
| MessageFlag::Outgoing
| (webpage.invert ? MessageFlag::InvertMedia : MessageFlag())),
UserId(), // via
FullReplyTo(),
base::unixtime::now(), // date
_history->session().userPeerId(),
QString(), // postAuthor
TextWithEntities{
message.text,
TextUtilities::ConvertTextTagsToEntities(message.tags),
},
MTP_messageMediaWebPage(
MTP_flags(Flag()
| (webpage.forceLargeMedia
? Flag::f_force_large_media
: Flag())
| (webpage.forceSmallMedia
? Flag::f_force_small_media
: Flag())),
MTP_webPagePending(
MTP_flags(webpage.url.isEmpty()
? MTPDwebPagePending::Flag()
: MTPDwebPagePending::Flag::f_url),
MTP_long(webpage.id),
MTP_string(webpage.url),
MTP_int(0))),
HistoryMessageMarkupData(),
uint64(0)); // groupedId
_element = _draftItem->createView(_delegate.get());
_selectType = TextSelectType::Letters;
_symbol = _selectionStartSymbol = 0;
_afterSymbol = _selectionStartAfterSymbol = false;
_section = Section::Link;
initElement();
return rpl::never<QString>();
}
void PreviewWrap::paintEvent(QPaintEvent *e) {
if (!_element) {
return;
}
auto p = Painter(this);
auto hq = PainterHighQualityEnabler(p);
auto context = _theme->preparePaintContext(
_style.get(),
rect(),
e->rect(),
!window()->isActiveWindow());
context.outbg = _element->hasOutLayout();
context.selection = _selecting
? resolveNewSelection()
: _selection.current();
p.translate(_position);
_element->draw(p, context);
if (_element->displayFromPhoto()) {
auto userpicMinBottomSkip = st::historyPaddingBottom
+ st::msgMargin.bottom();
auto userpicBottom = height()
- _element->marginBottom()
- _element->marginTop();
const auto item = _element->data();
const auto userpicTop = userpicBottom - st::msgPhotoSize;
if (const auto from = item->displayFrom()) {
from->paintUserpicLeft(
p,
_userpic,
st::historyPhotoLeft,
userpicTop,
width(),
st::msgPhotoSize);
} else if (const auto info = item->hiddenSenderInfo()) {
if (info->customUserpic.empty()) {
info->emptyUserpic.paintCircle(
p,
st::historyPhotoLeft,
userpicTop,
width(),
st::msgPhotoSize);
} else {
const auto valid = info->paintCustomUserpic(
p,
_userpic,
st::historyPhotoLeft,
userpicTop,
width(),
st::msgPhotoSize);
if (!valid) {
info->customUserpic.load(
&item->history()->session(),
item->fullId());
}
}
} else {
Unexpected("Corrupt forwarded information in message.");
}
}
}
void PreviewWrap::leaveEventHook(QEvent *e) {
if (!_element || !_over) {
return;
}
_over = false;
_textCursor = false;
_linkCursor = false;
if (!_selecting) {
setCursor(style::cur_default);
}
}
void PreviewWrap::mouseMoveEvent(QMouseEvent *e) {
if (!_element) {
return;
}
using Flag = Ui::Text::StateRequest::Flag;
auto request = StateRequest{
.flags = (_section == Section::Reply
? Flag::LookupSymbol
: Flag::LookupLink),
.onlyMessageText = (_section == Section::Link || _onlyMessageText),
};
auto resolved = _element->textState(
e->pos() - _position,
request);
_over = true;
const auto text = (_section == Section::Reply)
&& (resolved.cursor == CursorState::Text);
const auto link = (_section == Section::Link) && resolved.link;
if (_textCursor != text || _linkCursor != link) {
_textCursor = text;
_linkCursor = link;
setCursor((text || _selecting)
? style::cur_text
: link
? style::cur_pointer
: style::cur_default);
}
if (_symbol != resolved.symbol
|| _afterSymbol != resolved.afterSymbol) {
_symbol = resolved.symbol;
_afterSymbol = resolved.afterSymbol;
if (_selecting) {
update();
}
}
}
void PreviewWrap::mousePressEvent(QMouseEvent *e) {
if (!_over) {
return;
} else if (_section == Section::Reply) {
startSelection(_trippleClickTimer.isActive()
? TextSelectType::Paragraphs
: TextSelectType::Letters);
}
}
void PreviewWrap::mouseReleaseEvent(QMouseEvent *e) {
if (!_selecting) {
return;
} else if (_section == Section::Reply) {
const auto result = resolveNewSelection();
_selecting = false;
_selectType = TextSelectType::Letters;
if (!_textCursor) {
setCursor(style::cur_default);
}
_selection = result;
}
}
void PreviewWrap::mouseDoubleClickEvent(QMouseEvent *e) {
if (!_over) {
return;
} else if (_section == Section::Reply) {
startSelection(TextSelectType::Words);
_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
}
}
void PreviewWrap::initElement() {
_elementLifetime.destroy();
if (!_element) {
return;
}
_element->initDimensions();
widthValue(
) | rpl::filter([=](int width) {
return width > st::msgMinWidth;
}) | rpl::start_with_next([=](int width) {
const auto height = _position.y()
+ _element->resizeGetHeight(width)
+ st::msgMargin.top();
resize(width, height);
}, _elementLifetime);
}
TextSelection PreviewWrap::resolveNewSelection() const {
if (_section != Section::Reply) {
return TextSelection();
}
const auto make = [](uint16 symbol, bool afterSymbol) {
return uint16(symbol + (afterSymbol ? 1 : 0));
};
const auto first = make(_symbol, _afterSymbol);
const auto second = make(
_selectionStartSymbol,
_selectionStartAfterSymbol);
const auto result = (first <= second)
? TextSelection{ first, second }
: TextSelection{ second, first };
return _element->adjustSelection(result, _selectType);
}
void PreviewWrap::startSelection(TextSelectType type) {
if (_selecting && _selectType >= type) {
return;
}
_selecting = true;
_selectType = type;
_selectionStartSymbol = _symbol;
_selectionStartAfterSymbol = _afterSymbol;
if (!_textCursor) {
setCursor(style::cur_text);
}
update();
}
PreviewDelegate::PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, update)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto PreviewDelegate::elementPathShiftGradient()
-> not_null<Ui::PathShiftGradient*> {
return _pathGradient.get();
}
Context PreviewDelegate::elementContext() {
return Context::History;
}
} // namespace
void ShowReplyToChatBox(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> clearOldDraft) {
class Controller final : public ChooseRecipientBoxController {
public:
using Chosen = not_null<Data::Thread*>;
Controller(not_null<Main::Session*> session)
: ChooseRecipientBoxController(
session,
[=](Chosen thread) mutable { _singleChosen.fire_copy(thread); },
nullptr) {
}
void rowClicked(not_null<PeerListRow*> row) override final {
ChooseRecipientBoxController::rowClicked(row);
}
[[nodiscard]] rpl::producer<Chosen> singleChosen() const{
return _singleChosen.events();
}
bool respectSavedMessagesChat() const override {
return false;
}
private:
void prepareViewHook() override {
delegate()->peerListSetTitle(tr::lng_reply_in_another_title());
}
rpl::event_stream<Chosen> _singleChosen;
};
struct State {
not_null<PeerListBox*> box;
not_null<Controller*> controller;
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto session = &show->session();
const auto state = [&] {
auto controller = std::make_unique<Controller>(session);
const auto controllerRaw = controller.get();
auto box = Box<PeerListBox>(std::move(controller), [=](
not_null<PeerListBox*> box) {
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
});
const auto boxRaw = box.data();
show->show(std::move(box));
auto state = State{ boxRaw, controllerRaw };
return boxRaw->lifetime().make_state<State>(std::move(state));
}();
auto chosen = [=](not_null<Data::Thread*> thread) mutable {
const auto history = thread->owningHistory();
const auto topicRootId = thread->topicRootId();
const auto draft = history->localDraft(topicRootId);
const auto textWithTags = draft
? draft->textWithTags
: TextWithTags();
const auto cursor = draft ? draft->cursor : MessageCursor();
reply.topicRootId = topicRootId;
history->setLocalDraft(std::make_unique<Data::Draft>(
textWithTags,
reply,
cursor,
Data::WebPageDraft()));
history->clearLocalEditDraft(topicRootId);
history->session().changes().entryUpdated(
thread,
Data::EntryUpdate::Flag::LocalDraftSet);
if (clearOldDraft) {
crl::on_main(&history->session(), clearOldDraft);
}
return true;
};
auto callback = [=, chosen = std::move(chosen)](
Controller::Chosen thread) mutable {
const auto weak = Ui::MakeWeak(state->box);
if (!chosen(thread)) {
return;
} else if (const auto strong = weak.data()) {
strong->closeBox();
}
};
state->controller->singleChosen(
) | 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();
const auto replyItem = session->data().message(draft.reply.messageId);
const auto previewDataRaw = draft.webpage.id
? session->data().webpage(draft.webpage.id).get()
: nullptr;
const auto previewData = (previewDataRaw
&& !previewDataRaw->pendingTill
&& !previewDataRaw->failed)
? previewDataRaw
: nullptr;
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());
}
}));
}
} // namespace HistoryView::Controls

View File

@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "data/data_drafts.h"
class History;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
@ -17,10 +21,11 @@ class SessionController;
namespace HistoryView::Controls {
void EditReplyOptions(
void EditDraftOptions(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void(FullReplyTo)> done,
not_null<History*> history,
Data::Draft draft,
Fn<void(FullReplyTo, Data::WebPageDraft)> done,
Fn<void()> highlight,
Fn<void()> clearOldDraft);

View File

@ -1,535 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/controls/history_view_reply_options.h"
#include "boxes/peer_list_box.h"
#include "boxes/peer_list_controllers.h"
#include "chat_helpers/compose/compose_show.h"
#include "data/data_changes.h"
#include "data/data_drafts.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_thread.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"
#include "ui/chat/chat_style.h"
#include "ui/chat/chat_theme.h"
#include "ui/effects/path_shift_gradient.h"
#include "ui/layers/generic_box.h"
#include "ui/widgets/buttons.h"
#include "ui/painter.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include <QtWidgets/QApplication>
#include <QtWidgets/QWidget>
namespace HistoryView::Controls {
namespace {
class PreviewDelegate final : public DefaultElementDelegate {
public:
PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update);
bool elementAnimationsPaused() override;
not_null<Ui::PathShiftGradient*> elementPathShiftGradient() override;
Context elementContext() override;
private:
const not_null<QWidget*> _parent;
const std::unique_ptr<Ui::PathShiftGradient> _pathGradient;
};
[[nodiscard]] std::unique_ptr<Ui::ChatTheme> DefaultThemeOn(
rpl::lifetime &lifetime) {
auto result = std::make_unique<Ui::ChatTheme>();
using namespace Window::Theme;
const auto push = [=, raw = result.get()] {
const auto background = Background();
const auto &paper = background->paper();
raw->setBackground({
.prepared = background->prepared(),
.preparedForTiled = background->preparedForTiled(),
.gradientForFill = background->gradientForFill(),
.colorForFill = background->colorForFill(),
.colors = paper.backgroundColors(),
.patternOpacity = paper.patternOpacity(),
.gradientRotation = paper.gradientRotation(),
.isPattern = paper.isPattern(),
.tile = background->tile(),
});
};
push();
Background()->updates(
) | rpl::start_with_next([=](const BackgroundUpdate &update) {
if (update.type == BackgroundUpdate::Type::New
|| update.type == BackgroundUpdate::Type::Changed) {
push();
}
}, lifetime);
return result;
}
[[nodiscard]] rpl::producer<TextWithEntities> AddQuoteTracker(
not_null<Ui::GenericBox*> box,
std::shared_ptr<ChatHelpers::Show> show,
not_null<HistoryItem*> item,
const TextWithEntities &quote) {
struct State {
std::unique_ptr<Ui::ChatTheme> theme;
std::unique_ptr<Ui::ChatStyle> style;
std::unique_ptr<PreviewDelegate> delegate;
std::unique_ptr<Element> element;
rpl::variable<TextSelection> selection;
Ui::PeerUserpicView userpic;
QPoint position;
base::Timer trippleClickTimer;
TextSelectType selectType = TextSelectType::Letters;
uint16 symbol = 0;
bool afterSymbol = false;
bool textCursor = false;
bool selecting = false;
bool over = false;
uint16 selectionStartSymbol = 0;
bool selectionStartAfterSymbol = false;
};
const auto preview = box->addRow(object_ptr<Ui::RpWidget>(box), {});
const auto state = preview->lifetime().make_state<State>();
state->theme = DefaultThemeOn(preview->lifetime());
state->style = std::make_unique<Ui::ChatStyle>();
state->style->apply(state->theme.get());
state->delegate = std::make_unique<PreviewDelegate>(
box,
state->style.get(),
[=] { preview->update(); });
state->element = item->createView(state->delegate.get());
state->element->initDimensions();
state->position = QPoint(0, st::msgMargin.bottom());
state->selection = state->element->selectionFromQuote(quote);
const auto session = &show->session();
session->data().viewRepaintRequest(
) | rpl::start_with_next([=](not_null<const Element*> view) {
if (view == state->element.get()) {
preview->update();
}
}, preview->lifetime());
state->selection.changes() | rpl::start_with_next([=] {
preview->update();
}, preview->lifetime());
const auto resolveNewSelection = [=] {
const auto make = [](uint16 symbol, bool afterSymbol) {
return uint16(symbol + (afterSymbol ? 1 : 0));
};
const auto first = make(state->symbol, state->afterSymbol);
const auto second = make(
state->selectionStartSymbol,
state->selectionStartAfterSymbol);
const auto result = (first <= second)
? TextSelection{ first, second }
: TextSelection{ second, first };
return state->element->adjustSelection(result, state->selectType);
};
const auto startSelection = [=](TextSelectType type) {
if (state->selecting && state->selectType >= type) {
return;
}
state->selecting = true;
state->selectType = type;
state->selectionStartSymbol = state->symbol;
state->selectionStartAfterSymbol = state->afterSymbol;
if (!state->textCursor) {
preview->setCursor(style::cur_text);
}
preview->update();
};
const auto media = item->media();
const auto onlyMessageText = media
&& (media->webpage()
|| media->game()
|| (!media->photo() && !media->document()));
preview->setMouseTracking(true);
preview->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto type = e->type();
const auto mouse = static_cast<QMouseEvent*>(e.get());
if (type == QEvent::MouseMove) {
auto request = StateRequest{
.flags = Ui::Text::StateRequest::Flag::LookupSymbol,
.onlyMessageText = onlyMessageText,
};
auto resolved = state->element->textState(
mouse->pos() - state->position,
request);
state->over = true;
const auto text = (resolved.cursor == CursorState::Text);
if (state->textCursor != text) {
state->textCursor = text;
preview->setCursor((text || state->selecting)
? style::cur_text
: style::cur_default);
}
if (state->symbol != resolved.symbol
|| state->afterSymbol != resolved.afterSymbol) {
state->symbol = resolved.symbol;
state->afterSymbol = resolved.afterSymbol;
if (state->selecting) {
preview->update();
}
}
} else if (type == QEvent::Leave && state->over) {
state->over = false;
if (state->textCursor) {
state->textCursor = false;
if (!state->selecting) {
preview->setCursor(style::cur_default);
}
}
} else if (type == QEvent::MouseButtonDblClick && state->over) {
startSelection(TextSelectType::Words);
state->trippleClickTimer.callOnce(
QApplication::doubleClickInterval());
} else if (type == QEvent::MouseButtonPress && state->over) {
startSelection(state->trippleClickTimer.isActive()
? TextSelectType::Paragraphs
: TextSelectType::Letters);
} else if (type == QEvent::MouseButtonRelease && state->selecting) {
const auto result = resolveNewSelection();
state->selecting = false;
state->selectType = TextSelectType::Letters;
state->selection = result;
if (!state->textCursor) {
preview->setCursor(style::cur_default);
}
}
}, preview->lifetime());
preview->widthValue(
) | rpl::filter([=](int width) {
return width > st::msgMinWidth;
}) | rpl::start_with_next([=](int width) {
const auto height = state->element->resizeGetHeight(width)
+ state->position.y()
+ st::msgMargin.top();
preview->resize(width, height);
}, preview->lifetime());
box->setAttribute(Qt::WA_OpaquePaintEvent, false);
box->paintRequest() | rpl::start_with_next([=](QRect clip) {
Window::SectionWidget::PaintBackground(
state->theme.get(),
box,
box->window()->height(),
0,
clip);
}, box->lifetime());
preview->paintRequest() | rpl::start_with_next([=](QRect clip) {
auto p = Painter(preview);
auto hq = PainterHighQualityEnabler(p);
p.translate(state->position);
auto context = state->theme->preparePaintContext(
state->style.get(),
preview->rect(),
clip,
!box->window()->isActiveWindow());
context.outbg = state->element->hasOutLayout();
context.selection = state->selecting
? resolveNewSelection()
: state->selection.current();
state->element->draw(p, context);
if (state->element->displayFromPhoto()) {
auto userpicMinBottomSkip = st::historyPaddingBottom
+ st::msgMargin.bottom();
auto userpicBottom = preview->height()
- state->element->marginBottom()
- state->element->marginTop();
const auto userpicTop = userpicBottom - st::msgPhotoSize;
if (const auto from = item->displayFrom()) {
from->paintUserpicLeft(
p,
state->userpic,
st::historyPhotoLeft,
userpicTop,
preview->width(),
st::msgPhotoSize);
} else if (const auto info = item->hiddenSenderInfo()) {
if (info->customUserpic.empty()) {
info->emptyUserpic.paintCircle(
p,
st::historyPhotoLeft,
userpicTop,
preview->width(),
st::msgPhotoSize);
} else {
const auto valid = info->paintCustomUserpic(
p,
state->userpic,
st::historyPhotoLeft,
userpicTop,
preview->width(),
st::msgPhotoSize);
if (!valid) {
info->customUserpic.load(session, item->fullId());
}
}
} else {
Unexpected("Corrupt forwarded information in message.");
}
}
}, preview->lifetime());
return state->selection.value(
) | rpl::map([=](TextSelection selection) {
return state->element->selectedQuote(selection);
});
}
PreviewDelegate::PreviewDelegate(
not_null<QWidget*> parent,
not_null<Ui::ChatStyle*> st,
Fn<void()> update)
: _parent(parent)
, _pathGradient(MakePathShiftGradient(st, update)) {
}
bool PreviewDelegate::elementAnimationsPaused() {
return _parent->window()->isActiveWindow();
}
auto PreviewDelegate::elementPathShiftGradient()
-> not_null<Ui::PathShiftGradient*> {
return _pathGradient.get();
}
Context PreviewDelegate::elementContext() {
return Context::History;
}
} // namespace
void ShowReplyToChatBox(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void()> clearOldDraft) {
class Controller final : public ChooseRecipientBoxController {
public:
using Chosen = not_null<Data::Thread*>;
Controller(not_null<Main::Session*> session)
: ChooseRecipientBoxController(
session,
[=](Chosen thread) mutable { _singleChosen.fire_copy(thread); },
nullptr) {
}
void rowClicked(not_null<PeerListRow*> row) override final {
ChooseRecipientBoxController::rowClicked(row);
}
[[nodiscard]] rpl::producer<Chosen> singleChosen() const{
return _singleChosen.events();
}
bool respectSavedMessagesChat() const override {
return false;
}
private:
void prepareViewHook() override {
delegate()->peerListSetTitle(tr::lng_reply_in_another_title());
}
rpl::event_stream<Chosen> _singleChosen;
};
struct State {
not_null<PeerListBox*> box;
not_null<Controller*> controller;
base::unique_qptr<Ui::PopupMenu> menu;
};
const auto session = &show->session();
const auto state = [&] {
auto controller = std::make_unique<Controller>(session);
const auto controllerRaw = controller.get();
auto box = Box<PeerListBox>(std::move(controller), [=](
not_null<PeerListBox*> box) {
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
});
const auto boxRaw = box.data();
show->show(std::move(box));
auto state = State{ boxRaw, controllerRaw };
return boxRaw->lifetime().make_state<State>(std::move(state));
}();
auto chosen = [=](not_null<Data::Thread*> thread) mutable {
const auto history = thread->owningHistory();
const auto topicRootId = thread->topicRootId();
const auto draft = history->localDraft(topicRootId);
const auto textWithTags = draft
? draft->textWithTags
: TextWithTags();
const auto cursor = draft ? draft->cursor : MessageCursor();
reply.topicRootId = topicRootId;
history->setLocalDraft(std::make_unique<Data::Draft>(
textWithTags,
reply,
cursor,
Data::WebPageDraft()));
history->clearLocalEditDraft(topicRootId);
history->session().changes().entryUpdated(
thread,
Data::EntryUpdate::Flag::LocalDraftSet);
if (clearOldDraft) {
crl::on_main(&history->session(), clearOldDraft);
}
return true;
};
auto callback = [=, chosen = std::move(chosen)](
Controller::Chosen thread) mutable {
const auto weak = Ui::MakeWeak(state->box);
if (!chosen(thread)) {
return;
} else if (const auto strong = weak.data()) {
strong->closeBox();
}
};
state->controller->singleChosen(
) | rpl::start_with_next(std::move(callback), state->box->lifetime());
}
void EditReplyOptions(
std::shared_ptr<ChatHelpers::Show> show,
FullReplyTo reply,
Fn<void(FullReplyTo)> done,
Fn<void()> highlight,
Fn<void()> clearOldDraft) {
const auto session = &show->session();
const auto item = session->data().message(reply.messageId);
if (!item) {
return;
}
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setWidth(st::boxWideWidth);
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());
};
addSkip();
Settings::AddButton(
bottom,
tr::lng_reply_in_another_chat(),
st::settingsButton,
{ &st::menuIconReplace }
)->setClickedCallback([=] {
ShowReplyToChatBox(show, reply, clearOldDraft);
});
Settings::AddButton(
bottom,
tr::lng_reply_show_in_chat(),
st::settingsButton,
{ &st::menuIconShowInChat }
)->setClickedCallback(highlight);
const auto finish = [=](FullReplyTo result) {
const auto weak = Ui::MakeWeak(box);
done(std::move(result));
if (const auto strong = weak.data()) {
strong->closeBox();
}
};
Settings::AddButton(
bottom,
tr::lng_reply_remove(),
st::settingsAttentionButtonWithIcon,
{ &st::menuIconDeleteAttention }
)->setClickedCallback([=] {
finish({});
});
if (!item->originalText().empty()) {
addSkip();
Settings::AddDividerText(
bottom,
tr::lng_reply_about_quote());
}
struct State {
rpl::variable<TextWithEntities> quote;
};
const auto state = box->lifetime().make_state<State>();
state->quote = AddQuoteTracker(box, show, item, reply.quote);
box->setTitle(reply.quote.empty()
? tr::lng_reply_options_header()
: tr::lng_reply_options_quote());
auto save = state->quote.value(
) | rpl::map([=](const TextWithEntities &quote) {
return quote.empty()
? tr::lng_settings_save()
: tr::lng_reply_quote_selected();
}) | rpl::flatten_latest();
box->addButton(std::move(save), [=] {
auto result = reply;
result.quote = state->quote.current();
finish(result);
});
box->addButton(tr::lng_cancel(), [=] {
box->closeBox();
});
session->data().itemRemoved(
) | rpl::filter([=](not_null<const HistoryItem*> removed) {
return removed == item;
}) | rpl::start_with_next([=] {
finish({});
}, box->lifetime());
}));
}
} // namespace HistoryView::Controls

View File

@ -0,0 +1,327 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/controls/history_view_webpage_processor.h"
#include "base/unixtime.h"
#include "data/data_chat_participant_status.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "data/data_web_page.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
namespace HistoryView::Controls {
WebPageText TitleAndDescriptionFromWebPage(not_null<WebPageData*> d) {
QString resultTitle, resultDescription;
const auto document = d->document;
const auto author = d->author;
const auto siteName = d->siteName;
const auto title = d->title;
const auto description = d->description;
const auto filenameOrUrl = [&] {
return ((document && !document->filename().isEmpty())
? document->filename()
: d->url);
};
const auto authorOrFilename = [&] {
return (author.isEmpty()
? filenameOrUrl()
: author);
};
const auto descriptionOrAuthor = [&] {
return (description.text.isEmpty()
? authorOrFilename()
: description.text);
};
if (siteName.isEmpty()) {
if (title.isEmpty()) {
if (description.text.isEmpty()) {
resultTitle = author;
resultDescription = filenameOrUrl();
} else {
resultTitle = description.text;
resultDescription = authorOrFilename();
}
} else {
resultTitle = title;
resultDescription = descriptionOrAuthor();
}
} else {
resultTitle = siteName;
resultDescription = title.isEmpty()
? descriptionOrAuthor()
: title;
}
return { resultTitle, resultDescription };
}
bool DrawWebPageDataPreview(
QPainter &p,
not_null<WebPageData*> webpage,
not_null<PeerData*> context,
QRect to) {
const auto document = webpage->document;
const auto photo = webpage->photo;
if ((!photo || photo->isNull())
&& (!document
|| !document->hasThumbnail()
|| document->isPatternWallPaper())) {
return false;
}
const auto preview = photo
? photo->getReplyPreview(Data::FileOrigin(), context, false)
: document->getReplyPreview(Data::FileOrigin(), context, false);
if (preview) {
const auto w = preview->width();
const auto h = preview->height();
if (w == h) {
p.drawPixmap(to.x(), to.y(), preview->pix());
} else {
const auto from = (w > h)
? QRect((w - h) / 2, 0, h, h)
: QRect(0, (h - w) / 2, w, w);
p.drawPixmap(to, preview->pix(), from);
}
}
return true;
}
[[nodiscard]] bool ShowWebPagePreview(WebPageData *page) {
return page && !page->failed;
}
WebPageText ProcessWebPageData(WebPageData *page) {
auto previewText = TitleAndDescriptionFromWebPage(page);
if (previewText.title.isEmpty()) {
if (page->document) {
previewText.title = tr::lng_attach_file(tr::now);
} else if (page->photo) {
previewText.title = tr::lng_attach_photo(tr::now);
}
}
return previewText;
}
WebpageProcessor::WebpageProcessor(
not_null<History*> history,
not_null<Ui::InputField*> field)
: _history(history)
, _api(&history->session().mtp())
, _parser(field)
, _timer([=] {
if (!ShowWebPagePreview(_data) || _link.isEmpty()) {
return;
}
request();
}) {
_history->session().downloaderTaskFinished(
) | rpl::filter([=] {
return _data && (_data->document || _data->photo);
}) | rpl::start_with_next([=] {
_repaintRequests.fire({});
}, _lifetime);
_history->owner().webPageUpdates(
) | rpl::filter([=](not_null<WebPageData*> page) {
return (_data == page.get());
}) | rpl::start_with_next([=] {
updateFromData();
}, _lifetime);
_parser.list().changes(
) | rpl::start_with_next([=](QStringList &&parsed) {
_parsedLinks = std::move(parsed);
checkPreview();
}, _lifetime);
}
rpl::producer<> WebpageProcessor::repaintRequests() const {
return _repaintRequests.events();
}
Data::WebPageDraft WebpageProcessor::draft() const {
return _draft;
}
void WebpageProcessor::apply(Data::WebPageDraft draft, bool reparse) {
_api.request(base::take(_requestId)).cancel();
if (draft.removed) {
_draft = draft;
_data = nullptr;
_links = QStringList();
_link = QString();
_parsed = WebpageParsed();
updateFromData();
} else if (draft.manual && draft.id && !draft.url.isEmpty()) {
_draft = draft;
_parsedLinks = QStringList();
_links = QStringList();
_link = _draft.url;
const auto page = _history->owner().webpage(draft.id);
if (page->url == draft.url) {
_data = page;
updateFromData();
} else {
request();
}
} else if (!draft.manual && !_draft.manual) {
_draft = draft;
checkNow(reparse);
}
}
void WebpageProcessor::updateFromData() {
_timer.cancel();
auto parsed = WebpageParsed();
if (ShowWebPagePreview(_data)) {
if (const auto till = _data->pendingTill) {
parsed.drawPreview = [](QPainter &p, QRect to) {
return false;
};
parsed.title = tr::lng_preview_loading(tr::now);
parsed.description = _link;
const auto timeout = till - base::unixtime::now();
_timer.callOnce(
std::max(timeout, 0) * crl::time(1000));
} else {
const auto webpage = _data;
const auto context = _history->peer;
const auto preview = ProcessWebPageData(_data);
parsed.title = preview.title;
parsed.description = preview.description;
parsed.drawPreview = [=](QPainter &p, QRect to) {
return DrawWebPageDataPreview(p, webpage, context, to);
};
}
}
_parsed = std::move(parsed);
_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) {
_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::checkNow(bool force) {
_parser.parseNow();
if (force) {
_link = QString();
_links = QStringList();
if (_parsedLinks.isEmpty()) {
_data = nullptr;
updateFromData();
return;
}
}
checkPreview();
}
void WebpageProcessor::checkPreview() {
const auto previewRestricted = _history->peer
&& _history->peer->amRestricted(ChatRestriction::EmbedLinks);
if (_draft.removed) {
return;
} else if (previewRestricted) {
apply({ .removed = true });
_draft.removed = false;
return;
} else if (_draft.manual) {
return;
} else if (_links == _parsedLinks) {
return;
}
_links = _parsedLinks;
auto page = (WebPageData*)nullptr;
auto chosen = QString();
for (const auto &link : _links) {
const auto i = _cache.find(link);
if (i == end(_cache)) {
chosen = link;
break;
} else if (i->second) {
if (i->second->failed) {
i->second = nullptr;
} else {
chosen = link;
page = i->second;
break;
}
}
}
if (_link != chosen) {
_link = chosen;
_api.request(base::take(_requestId)).cancel();
if (!page && !_link.isEmpty()) {
request();
}
}
if (page) {
_data = page;
_draft.id = _data->id;
_draft.url = _data->url;
} else {
_data = nullptr;
_draft = {};
}
updateFromData();
}
rpl::producer<WebpageParsed> WebpageProcessor::parsedValue() const {
return _parsed.value();
}
} // namespace HistoryView::Controls

View File

@ -0,0 +1,100 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/weak_ptr.h"
#include "data/data_drafts.h"
#include "chat_helpers/message_field.h"
#include "mtproto/sender.h"
class History;
namespace Ui {
class InputField;
} // namespace Ui
namespace HistoryView::Controls {
struct WebPageText {
QString title;
QString description;
};
[[nodiscard]] WebPageText TitleAndDescriptionFromWebPage(
not_null<WebPageData*> data);
bool DrawWebPageDataPreview(
QPainter &p,
not_null<WebPageData*> webpage,
not_null<PeerData*> context,
QRect to);
[[nodiscard]] bool ShowWebPagePreview(WebPageData *page);
[[nodiscard]] WebPageText ProcessWebPageData(WebPageData *page);
struct WebpageParsed {
Fn<bool(QPainter &p, QRect to)> drawPreview;
QString title;
QString description;
explicit operator bool() const {
return drawPreview != nullptr;
}
};
class WebpageProcessor final : public base::has_weak_ptr {
public:
WebpageProcessor(
not_null<History*> history,
not_null<Ui::InputField*> field);
void checkNow(bool force);
// If editing a message without a preview we don't want to show
// parsed preview until links set is changed in the message.
//
// If writing a new message we want to parse links immediately,
// unless preview was removed in the draft or manual.
void apply(Data::WebPageDraft draft, bool reparse = true);
[[nodiscard]] Data::WebPageDraft draft() const;
[[nodiscard]] rpl::producer<> repaintRequests() const;
[[nodiscard]] rpl::producer<WebpageParsed> parsedValue() const;
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
void updateFromData();
void checkPreview();
void request();
const not_null<History*> _history;
MTP::Sender _api;
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;
base::Timer _timer;
rpl::lifetime _lifetime;
};
} // namespace HistoryView::Controls

View File

@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/controls/history_view_compose_controls.h"
#include "history/view/controls/history_view_forward_panel.h"
#include "history/view/controls/history_view_reply_options.h"
#include "history/view/controls/history_view_draft_options.h"
#include "history/view/history_view_top_bar_widget.h"
#include "history/view/history_view_list_widget.h"
#include "history/view/history_view_schedule_box.h"

View File

@ -244,31 +244,8 @@ QSize WebPage::countOptimalSize() {
_asArticle = 0;
} else if (_data->photo && (_flags & Flag::ForceSmallMedia)) {
_asArticle = 1;
} else if (!_collage.empty()) {
_asArticle = 0;
} else if (!_data->document
&& _data->photo
&& _data->type != WebPageType::Photo
&& _data->type != WebPageType::Document
&& _data->type != WebPageType::Story
&& _data->type != WebPageType::Video) {
if (_data->type == WebPageType::Profile) {
_asArticle = 1;
} else if (_data->siteName == u"Twitter"_q
|| _data->siteName == u"Facebook"_q
|| _data->type == WebPageType::ArticleWithIV) {
_asArticle = 0;
} else {
_asArticle = 1;
}
if (_asArticle
&& _data->description.text.isEmpty()
&& title.isEmpty()
&& _data->siteName.isEmpty()) {
_asArticle = 0;
}
} else {
_asArticle = 0;
_asArticle = _data->computeDefaultSmallMedia();
}
// init attach

@ -1 +1 @@
Subproject commit c36559a6797f02d8a56a414ac91f9c6fd08b5270
Subproject commit b05f7eb915a86f67249904061d70f293066de618