mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-19 07:37:11 +02:00
Edit reply / webpage options together.
This commit is contained in:
parent
1409d38ac3
commit
17578be4b9
17 changed files with 1525 additions and 1182 deletions
|
@ -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
|
||||
|
|
|
@ -37,6 +37,7 @@ WebPageDraft WebPageDraft::FromItem(not_null<HistoryItem*> item) {
|
|||
.forceSmallMedia = !!(previewFlags & PageFlag::ForceSmallMedia),
|
||||
.invert = item->invertMedia(),
|
||||
.manual = !!(previewFlags & PageFlag::Manual),
|
||||
.removed = !previewPage,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 "e);
|
||||
[[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 "e) {
|
||||
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 "e, Section shown) {
|
||||
return (quote.empty() || shown != Section::Reply)
|
||||
? tr::lng_settings_save()
|
||||
: tr::lng_reply_quote_selected();
|
||||
}) | rpl::flatten_latest();
|
||||
box->addButton(std::move(save), [=] {
|
||||
finish(resolveReply(), state->webpage);
|
||||
});
|
||||
|
||||
box->addButton(tr::lng_cancel(), [=] {
|
||||
box->closeBox();
|
||||
});
|
||||
|
||||
if (replyItem) {
|
||||
session->data().itemRemoved(
|
||||
) | rpl::filter([=](not_null<const HistoryItem*> removed) {
|
||||
return removed == replyItem;
|
||||
}) | rpl::start_with_next([=] {
|
||||
if (previewData) {
|
||||
state->tabs = nullptr;
|
||||
box->setPinnedToTopContent(
|
||||
object_ptr<Ui::RpWidget>(nullptr));
|
||||
box->setNoContentMargin(false);
|
||||
box->setTitle(state->quote.current().empty()
|
||||
? tr::lng_reply_options_header()
|
||||
: tr::lng_reply_options_quote());
|
||||
state->shown = Section::Link;
|
||||
} else {
|
||||
box->closeBox();
|
||||
}
|
||||
}, box->lifetime());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
} // namespace HistoryView::Controls
|
|
@ -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);
|
||||
|
|
@ -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 "e) {
|
||||
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 "e) {
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue