Allow forward+reply, options in single box.

This commit is contained in:
John Preston 2024-12-06 14:58:51 +04:00
parent d157eb0b6e
commit eb29b6bffe
26 changed files with 497 additions and 338 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -4912,6 +4912,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_forward_show_captions" = "Show captions";
"lng_forward_change_recipient" = "Change recipient";
"lng_forward_sender_names_removed" = "Sender names removed";
"lng_forward_header_short" = "Forward";
"lng_forward_action_show_sender" = "Show Sender Name";
"lng_forward_action_show_senders" = "Show Sender Names";
"lng_forward_action_hide_sender" = "Hide Sender Name";
"lng_forward_action_hide_senders" = "Hide Sender Names";
"lng_forward_action_show_caption" = "Show Caption";
"lng_forward_action_show_captions" = "Show Captions";
"lng_forward_action_hide_caption" = "Hide Caption";
"lng_forward_action_hide_captions" = "Hide Captions";
"lng_forward_action_change_recipient" = "Change Recipient";
"lng_forward_action_remove" = "Do Not Forward";
"lng_passport_title" = "Telegram Passport";
"lng_passport_request1" = "{bot} requests access to your personal data";

View file

@ -36,6 +36,8 @@ using Options = base::flags<Option>;
namespace Data {
struct FileOrigin;
struct UploadState {
explicit UploadState(int64 size) : size(size) {
}
@ -58,8 +60,6 @@ constexpr auto kVoiceMessageCacheTag = uint8(0x03);
constexpr auto kVideoMessageCacheTag = uint8(0x04);
constexpr auto kAnimationCacheTag = uint8(0x05);
struct FileOrigin;
} // namespace Data
struct MessageGroupId {
@ -342,3 +342,29 @@ enum class MediaWebPageFlag : uint8 {
};
inline constexpr bool is_flag_type(MediaWebPageFlag) { return true; }
using MediaWebPageFlags = base::flags<MediaWebPageFlag>;
namespace Data {
enum class ForwardOptions {
PreserveInfo,
NoSenderNames,
NoNamesAndCaptions,
};
struct ForwardDraft {
MessageIdsList ids;
ForwardOptions options = ForwardOptions::PreserveInfo;
friend inline auto operator<=>(
const ForwardDraft&,
const ForwardDraft&) = default;
};
using ForwardDrafts = base::flat_map<MsgId, ForwardDraft>;
struct ResolvedForwardDraft {
HistoryItemsList items;
ForwardOptions options = ForwardOptions::PreserveInfo;
};
} // namespace Data

View file

@ -26,7 +26,6 @@ class HistoryMainElementDelegateMixin;
struct LanguageId;
namespace Data {
struct Draft;
class Session;
class Folder;
@ -34,29 +33,6 @@ class ChatFilter;
struct SponsoredFrom;
class SponsoredMessages;
class HistoryMessages;
enum class ForwardOptions {
PreserveInfo,
NoSenderNames,
NoNamesAndCaptions,
};
struct ForwardDraft {
MessageIdsList ids;
ForwardOptions options = ForwardOptions::PreserveInfo;
friend inline auto operator<=>(
const ForwardDraft&,
const ForwardDraft&) = default;
};
using ForwardDrafts = base::flat_map<MsgId, ForwardDraft>;
struct ResolvedForwardDraft {
HistoryItemsList items;
ForwardOptions options = ForwardOptions::PreserveInfo;
};
} // namespace Data
namespace Dialogs {

View file

@ -136,6 +136,9 @@ template <typename T>
HistoryItemCommonFields fields,
not_null<History*> history,
not_null<HistoryItem*> original) {
if (fields.flags & MessageFlag::FakeHistoryItem) {
return fields;
}
fields.flags |= NewForwardedFlags(history->peer, fields.from, original);
return fields;
}
@ -509,7 +512,8 @@ HistoryItem::HistoryItem(
auto config = CreateConfig();
const auto originalMedia = original->media();
const auto dropForwardInfo = original->computeDropForwardedInfo();
const auto dropForwardInfo = fields.ignoreForwardFrom
|| original->computeDropForwardedInfo();
const auto topicRootId = fields.replyTo.topicRootId;
config.reply.messageId = config.reply.topMessageId = topicRootId;
config.reply.topicPost = (topicRootId != 0) ? 1 : 0;
@ -597,9 +601,22 @@ HistoryItem::HistoryItem(
}
}
setText(dropForwardInfo
const auto dropText = fields.ignoreForwardCaptions
&& _media
&& (_media->photo() || _media->document())
&& !_media->webpage();
setText(dropText
? TextWithEntities()
: dropForwardInfo
? DropDisallowedCustomEmoji(history->peer, original->originalText())
: original->originalText());
if (fields.groupedId) {
setGroupId(MessageGroupId::FromRaw(
history->peer->id,
fields.groupedId,
_flags & MessageFlag::IsOrWasScheduled));
}
}
HistoryItem::HistoryItem(

View file

@ -107,6 +107,8 @@ struct HistoryItemCommonFields {
uint64 groupedId = 0;
EffectId effectId = 0;
HistoryMessageMarkupData markup;
bool ignoreForwardFrom = false;
bool ignoreForwardCaptions = false;
};
enum class HistoryReactionSource : char {

View file

@ -2109,7 +2109,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
if (!_replyEditMsg) {
requestMessageData(_editMsgId);
}
} else if (!readyToForward()) {
} else {
const auto draft = _history->localDraft({});
_processingReplyTo = draft ? draft->reply : FullReplyTo();
if (_processingReplyTo) {
@ -4982,7 +4982,7 @@ bool HistoryWidget::showRecordButton() const {
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !HasSendText(_field)
&& !_previewDrawPreview
&& !readyToForward()
&& (_replyTo || !readyToForward())
&& !_editMsgId;
}
@ -6825,18 +6825,15 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) {
_peer,
Window::SectionShow::Way::Forward,
_editMsgId);
} else if (isReadyToForward) {
if (e->button() != Qt::LeftButton) {
_forwardPanel->editToNextOption();
} else {
_forwardPanel->editOptions(controller()->uiShow());
}
} else if (_replyTo
&& ((e->modifiers() & Qt::ControlModifier)
|| (e->button() != Qt::LeftButton))) {
jumpToReply(_replyTo);
} else if (_replyTo) {
} else if (_replyTo
|| (isReadyToForward && e->button() == Qt::LeftButton)) {
editDraftOptions();
} else if (isReadyToForward) {
_forwardPanel->editToNextOption();
} else if (_kbReplyTo) {
controller()->showPeerHistory(
_kbReplyTo->history()->peer->id,
@ -6851,15 +6848,18 @@ void HistoryWidget::editDraftOptions() {
const auto history = _history;
const auto reply = _replyTo;
const auto webpage = _preview->draft();
const auto forward = _forwardPanel->draft();
const auto done = [=](
FullReplyTo replyTo,
Data::WebPageDraft webpage) {
Data::WebPageDraft webpage,
Data::ForwardDraft forward) {
if (replyTo) {
replyToMessage(replyTo);
} else {
cancelReply();
}
history->setForwardDraft({}, std::move(forward));
_preview->apply(webpage);
};
const auto replyToId = reply.messageId;
@ -6873,6 +6873,7 @@ void HistoryWidget::editDraftOptions() {
.history = history,
.draft = Data::Draft(_field, reply, _preview->draft()),
.usedLink = _preview->link(),
.forward = _forwardPanel->draft(),
.links = _preview->links(),
.resolver = _preview->resolver(),
.done = done,
@ -7956,7 +7957,7 @@ void HistoryWidget::processReply() {
{ .ids = { 1, itemId } });
}),
.confirmText = tr::lng_selected_forward(),
}));
}));
}
return processCancel();
#endif
@ -7985,7 +7986,6 @@ void HistoryWidget::setReplyFieldsFromProcessing() {
return;
}
_history->setForwardDraft(MsgId(), {});
if (_composeSearch) {
_composeSearch->hideAnimated();
}
@ -8243,10 +8243,10 @@ void HistoryWidget::cancelFieldAreaState() {
_preview->apply({ .removed = true });
} else if (_editMsgId) {
cancelEdit();
} else if (readyToForward()) {
_history->setForwardDraft(MsgId(), {});
} else if (_replyTo) {
cancelReply();
} else if (readyToForward()) {
_history->setForwardDraft(MsgId(), {});
} else if (_kbReplyTo) {
toggleKeyboard();
}
@ -8595,9 +8595,6 @@ void HistoryWidget::updateForwarding() {
_forwardPanel->update(_history, _history
? _history->resolveForwardDraft(MsgId())
: Data::ResolvedForwardDraft());
if (readyToForward()) {
cancelReply();
}
updateControlsVisibility();
updateControlsGeometry();
}

View file

@ -146,6 +146,7 @@ public:
[[nodiscard]] bool isEditingMessage() const;
[[nodiscard]] bool readyToForward() const;
[[nodiscard]] const HistoryItemsList &forwardItems() const;
[[nodiscard]] const Data::ResolvedForwardDraft &forwardDraft() const;
[[nodiscard]] FullReplyTo replyingToMessage() const;
[[nodiscard]] FullMsgId editMsgId() const;
[[nodiscard]] rpl::producer<FullMsgId> editMsgIdValue() const;
@ -281,23 +282,23 @@ void FieldHeader::init() {
st::historyLinkIcon.paint(p, position, width());
} else if (isEditingMessage()) {
st::historyEditIcon.paint(p, position, width());
} else if (readyToForward()) {
st::historyForwardIcon.paint(p, position, width());
} else if (const auto reply = replyingToMessage()) {
if (!reply.quote.empty()) {
st::historyQuoteIcon.paint(p, position, width());
} else {
st::historyReplyIcon.paint(p, position, width());
}
} else if (readyToForward()) {
st::historyForwardIcon.paint(p, position, width());
}
if (_preview.parsed) {
paintWebPage(
p,
_history ? _history->peer : _data->session().user());
} else if (isEditingMessage() || !readyToForward()) {
} else if (isEditingMessage() || replyingToMessage()) {
paintEditOrReplyToMessage(p);
} else {
} else if (readyToForward()) {
paintForwardInfo(p);
}
}, lifetime());
@ -339,10 +340,10 @@ void FieldHeader::init() {
_previewCancelled.fire({});
} else if (_editMsgId.current()) {
_editCancelled.fire({});
} else if (readyToForward()) {
_forwardCancelled.fire({});
} else if (_replyTo.current()) {
_replyCancelled.fire({});
} else if (readyToForward()) {
_forwardCancelled.fire({});
}
updateVisible();
update();
@ -403,12 +404,9 @@ void FieldHeader::init() {
_jumpToItemRequests.fire(FullReplyTo{
.messageId = _editMsgId.current()
});
} else if (readyToForward()) {
_forwardPanel->editOptions(_show);
} else if (reply
&& (e->modifiers() & Qt::ControlModifier)) {
} else if (reply && (e->modifiers() & Qt::ControlModifier)) {
_jumpToItemRequests.fire_copy(reply);
} else if (reply) {
} else if (reply || readyToForward()) {
_editOptionsRequests.fire({});
}
} else if (!isLeftButton) {
@ -419,6 +417,8 @@ void FieldHeader::init() {
_hasSendText());
} else if (const auto reply = replyingToMessage()) {
_jumpToItemRequests.fire_copy(reply);
} else if (readyToForward()) {
_forwardPanel->editToNextOption();
}
}
}
@ -713,6 +713,10 @@ const HistoryItemsList &FieldHeader::forwardItems() const {
return _forwardPanel->items();
}
const Data::ResolvedForwardDraft &FieldHeader::forwardDraft() const {
return _forwardPanel->draft();
}
FullReplyTo FieldHeader::replyingToMessage() const {
return _replyTo.current();
}
@ -764,9 +768,6 @@ void FieldHeader::updateForwarding(
Data::Thread *thread,
Data::ResolvedForwardDraft items) {
_forwardPanel->update(thread, std::move(items));
if (readyToForward()) {
replyToMessage({});
}
updateControlsGeometry(size());
}
@ -1408,12 +1409,14 @@ void ComposeControls::init() {
const auto done = [=](
FullReplyTo replyTo,
Data::WebPageDraft webpage) {
Data::WebPageDraft webpage,
Data::ForwardDraft forward) {
if (replyTo) {
replyToMessage(replyTo);
} else {
cancelReplyMessage();
}
history->setForwardDraft(topicRootId, std::move(forward));
_preview->apply(webpage);
_field->setFocus();
};
@ -1428,6 +1431,7 @@ void ComposeControls::init() {
.history = history,
.draft = Data::Draft(_field, reply, _preview->draft()),
.usedLink = _preview->link(),
.forward = _header->forwardDraft(),
.links = _preview->links(),
.resolver = _preview->resolver(),
.done = done,
@ -2013,9 +2017,6 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) {
_canReplaceMedia = _canAddMedia = false;
_photoEditMedia = nullptr;
_header->replyToMessage(draft->reply);
if (_header->replyingToMessage()) {
cancelForward();
}
_header->editMessage({});
if (_preview) {
_preview->setDisabled(false);
@ -3018,9 +3019,6 @@ void ComposeControls::replyToMessage(FullReplyTo id) {
}
} else {
_header->replyToMessage(id);
if (_header->replyingToMessage()) {
cancelForward();
}
}
_saveDraftText = true;
@ -3071,12 +3069,12 @@ bool ComposeControls::handleCancelRequest() {
} else if (isEditingMessage()) {
maybeCancelEditMessage();
return true;
} else if (readyToForward()) {
cancelForward();
return true;
} else if (replyingToMessage()) {
cancelReplyMessage();
return true;
} else if (readyToForward()) {
cancelForward();
return true;
}
return false;
}

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/controls/history_view_draft_options.h"
#include "base/random.h"
#include "base/timer_rpl.h"
#include "base/unixtime.h"
#include "boxes/filters/edit_filter_chats_list.h"
@ -20,12 +21,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_thread.h"
#include "data/data_user.h"
#include "data/data_web_page.h"
#include "history/view/controls/history_view_forward_panel.h"
#include "history/view/controls/history_view_webpage_processor.h"
#include "history/view/history_view_element.h"
#include "history/view/history_view_cursor_state.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
@ -41,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/ui_utility.h"
#include "window/themes/window_theme.h"
#include "window/section_widget.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
#include "styles/style_chat.h"
@ -56,6 +60,7 @@ namespace {
enum class Section {
Reply,
Forward,
Link,
};
@ -104,6 +109,10 @@ public:
not_null<History*> history);
~PreviewWrap();
[[nodiscard]] bool hasViewForItem(
not_null<const HistoryItem*> item) const;
void showForwardSelector(Data::ResolvedForwardDraft draft);
[[nodiscard]] rpl::producer<SelectedQuote> showQuoteSelector(
const SelectedQuote &quote);
[[nodiscard]] rpl::producer<QString> showLinkSelector(
@ -117,6 +126,11 @@ public:
}
private:
struct Entry {
HistoryItem *item = nullptr;
std::unique_ptr<Element> view;
};
void paintEvent(QPaintEvent *e) override;
void leaveEventHook(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
@ -129,7 +143,8 @@ private:
_visibleBottom = bottom;
}
void initElement();
void clear(std::vector<Entry> entries);
void initElements();
void highlightUsedLink(
const TextWithTags &message,
const QString &usedLink,
@ -144,8 +159,8 @@ private:
const std::unique_ptr<PreviewDelegate> _delegate;
Section _section = Section::Reply;
HistoryItem *_draftItem = nullptr;
std::unique_ptr<Element> _element;
std::vector<Entry> _entries;
base::flat_set<not_null<const Element*>> _views;
rpl::variable<TextSelection> _selection;
rpl::event_stream<QString> _chosenUrl;
Ui::PeerUserpicView _userpic;
@ -191,7 +206,7 @@ PreviewWrap::PreviewWrap(
const auto session = &_history->session();
session->data().viewRepaintRequest(
) | rpl::start_with_next([=](not_null<const Element*> view) {
if (view == _element.get()) {
if (_views.contains(view)) {
update();
}
}, lifetime());
@ -221,26 +236,92 @@ PreviewWrap::PreviewWrap(
PreviewWrap::~PreviewWrap() {
_selection.reset(TextSelection());
base::take(_views);
clear(base::take(_entries));
}
void PreviewWrap::clear(std::vector<Entry> entries) {
_elementLifetime.destroy();
_element = nullptr;
if (_draftItem) {
_draftItem->destroy();
for (auto &entry : entries) {
entry.view = nullptr;
if (const auto item = entry.item) {
item->destroy();
}
}
}
bool PreviewWrap::hasViewForItem(not_null<const HistoryItem*> item) const {
return (item->history() == _history)
&& ranges::contains(_views, item, &Element::data);
}
void PreviewWrap::showForwardSelector(Data::ResolvedForwardDraft draft) {
Expects(!draft.items.empty());
_selection.reset(TextSelection());
auto was = base::take(_entries);
auto groups = base::flat_map<MessageGroupId, uint64>();
const auto groupByItem = [&](not_null<HistoryItem*> item) {
const auto groupId = item->groupId();
if (!groupId) {
return uint64();
}
auto i = groups.find(groupId);
if (i == end(groups)) {
i = groups.emplace(groupId, base::RandomValue<uint64>()).first;
}
return i->second;
};
const auto wasViews = base::take(_views);
using Options = Data::ForwardOptions;
const auto dropNames = (draft.options != Options::PreserveInfo);
const auto dropCaptions = (draft.options == Options::NoNamesAndCaptions);
for (const auto &source : draft.items) {
const auto groupedId = groupByItem(source);
const auto item = _history->addNewLocalMessage({
.id = _history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeHistoryItem
| MessageFlag::Outgoing
| MessageFlag::HasFromId
| (source->invertMedia()
? MessageFlag::InvertMedia
: MessageFlag())),
.from = _history->session().userPeerId(),
.date = base::unixtime::now(),
.groupedId = groupedId,
.ignoreForwardFrom = dropNames,
.ignoreForwardCaptions = dropCaptions,
}, source);
_entries.push_back({ item });
}
for (auto &entry : _entries) {
entry.view = entry.item->createView(_delegate.get());
_views.emplace(entry.view.get());
}
_link = _pressedLink = nullptr;
clear(std::move(was));
_section = Section::Forward;
initElements();
}
rpl::producer<SelectedQuote> PreviewWrap::showQuoteSelector(
const SelectedQuote &quote) {
_selection.reset(TextSelection());
auto was = base::take(_entries);
const auto wasViews = base::take(_views);
const auto item = quote.item;
const auto group = item->history()->owner().groups().find(item);
const auto leader = group ? group->items.front().get() : item;
_element = leader->createView(_delegate.get());
_entries.push_back({
.view = leader->createView(_delegate.get()),
});
_views.emplace(_entries.back().view.get());
_link = _pressedLink = nullptr;
if (const auto was = base::take(_draftItem)) {
was->destroy();
}
clear(std::move(was));
const auto media = item->media();
_onlyMessageText = media
@ -249,12 +330,13 @@ rpl::producer<SelectedQuote> PreviewWrap::showQuoteSelector(
|| (!media->photo() && !media->document()));
_section = Section::Reply;
initElement();
initElements();
_selection = _element->selectionFromQuote(quote);
const auto view = _entries.back().view.get();
_selection = view->selectionFromQuote(quote);
return _selection.value(
) | rpl::map([=](TextSelection selection) {
if (const auto result = _element->selectedQuote(selection)) {
if (const auto result = view->selectedQuote(selection)) {
return result;
}
return SelectedQuote{ item };
@ -267,13 +349,11 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
const std::vector<MessageLinkRange> &links,
const QString &usedLink) {
_selection.reset(TextSelection());
base::take(_views);
clear(base::take(_entries));
_element = nullptr;
if (const auto was = base::take(_draftItem)) {
was->destroy();
}
using Flag = MTPDmessageMediaWebPage::Flag;
_draftItem = _history->addNewLocalMessage({
const auto item = _history->addNewLocalMessage({
.id = _history->nextNonHistoryEntryId(),
.flags = (MessageFlag::FakeHistoryItem
| MessageFlag::Outgoing
@ -299,13 +379,15 @@ rpl::producer<QString> PreviewWrap::showLinkSelector(
MTP_long(webpage.id),
MTP_string(webpage.url),
MTP_int(0))));
_element = _draftItem->createView(_delegate.get());
_entries.push_back({ item, item->createView(_delegate.get()) });
_views.emplace(_entries.back().view.get());
_selectType = TextSelectType::Letters;
_symbol = _selectionStartSymbol = 0;
_afterSymbol = _selectionStartAfterSymbol = false;
_section = Section::Link;
initElement();
initElements();
highlightUsedLink(message, usedLink, links);
return _chosenUrl.events();
@ -338,7 +420,8 @@ void PreviewWrap::highlightUsedLink(
text = text.mid(0, text.size() - 1);
--selection.to;
}
const auto basic = _element->textState(QPoint(0, 0), {
const auto view = _entries.back().view.get();
const auto basic = view->textState(QPoint(0, 0), {
.flags = Ui::Text::StateRequest::Flag::LookupSymbol,
.onlyMessageText = true,
});
@ -353,30 +436,31 @@ void PreviewWrap::highlightUsedLink(
}
void PreviewWrap::paintEvent(QPaintEvent *e) {
if (!_element) {
return;
}
auto p = Painter(this);
p.translate(_position);
auto context = _theme->preparePaintContext(
_style.get(),
rect(),
e->rect(),
!window()->isActiveWindow());
context.outbg = _element->hasOutLayout();
context.selection = _selecting
? resolveNewSelection()
: _selection.current();
for (const auto &entry : _entries) {
context.outbg = entry.view->hasOutLayout();
context.selection = _selecting
? resolveNewSelection()
: _selection.current();
p.translate(_position);
_element->draw(p, context);
entry.view->draw(p, context);
if (_element->displayFromPhoto()) {
p.translate(0, entry.view->height());
}
const auto top = _entries.empty() ? nullptr : _entries.back().view.get();
if (top && top->displayFromPhoto()) {
auto userpicBottom = height()
- _element->marginBottom()
- _element->marginTop();
const auto item = _element->data();
- top->marginBottom()
- top->marginTop();
const auto item = top->data();
const auto userpicTop = userpicBottom - st::msgPhotoSize;
if (const auto from = item->displayFrom()) {
from->paintUserpicLeft(
@ -415,7 +499,7 @@ void PreviewWrap::paintEvent(QPaintEvent *e) {
}
void PreviewWrap::leaveEventHook(QEvent *e) {
if (!_element || !_over) {
if (!_over) {
return;
}
_over = false;
@ -427,7 +511,7 @@ void PreviewWrap::leaveEventHook(QEvent *e) {
}
void PreviewWrap::mouseMoveEvent(QMouseEvent *e) {
if (!_element) {
if (_entries.empty()) {
return;
}
using Flag = Ui::Text::StateRequest::Flag;
@ -438,7 +522,16 @@ void PreviewWrap::mouseMoveEvent(QMouseEvent *e) {
.onlyMessageText = (_section == Section::Link || _onlyMessageText),
};
const auto position = e->pos();
auto resolved = _element->textState(position - _position, request);
auto local = position - _position;
auto resolved = TextState();
for (auto &entry : _entries) {
const auto height = entry.view->height();
if (local.y() < height) {
resolved = entry.view->textState(local, request);
break;
}
local.setY(local.y() - height);
}
_over = true;
const auto text = (_section == Section::Reply)
&& (resolved.cursor == CursorState::Text);
@ -518,27 +611,25 @@ void PreviewWrap::mouseDoubleClickEvent(QMouseEvent *e) {
}
}
void PreviewWrap::initElement() {
_elementLifetime.destroy();
if (!_element) {
return;
void PreviewWrap::initElements() {
for (auto &entry : _entries) {
entry.view->initDimensions();
}
_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();
auto height = _position.y();
for (const auto &entry : _entries) {
height += entry.view->resizeGetHeight(width);
}
height += st::msgMargin.top();
resize(width, height);
}, _elementLifetime);
}
TextSelection PreviewWrap::resolveNewSelection() const {
if (_section != Section::Reply) {
if (_section != Section::Reply || _entries.empty()) {
return TextSelection();
}
const auto make = [](uint16 symbol, bool afterSymbol) {
@ -551,7 +642,7 @@ TextSelection PreviewWrap::resolveNewSelection() const {
const auto result = (first <= second)
? TextSelection{ first, second }
: TextSelection{ second, first };
return _element->adjustSelection(result, _selectType);
return _entries.back().view->adjustSelection(result, _selectType);
}
void PreviewWrap::startSelection(TextSelectType type) {
@ -607,9 +698,10 @@ void DraftOptionsBox(
const auto &draft = args.draft;
struct State {
rpl::variable<Section> shown;
rpl::variable<Section> shown = Section::Link;
rpl::lifetime shownLifetime;
rpl::variable<SelectedQuote> quote;
Data::ResolvedForwardDraft forward;
Data::WebPageDraft webpage;
WebPageData *preview = nullptr;
QString link;
@ -619,41 +711,90 @@ void DraftOptionsBox(
Fn<void(const QString &link, WebPageData *page)> performSwitch;
Fn<void(const QString &link, bool force)> requestAndSwitch;
rpl::lifetime resolveLifetime;
Fn<void()> rebuild;
};
const auto state = box->lifetime().make_state<State>();
state->link = args.usedLink;
state->quote = SelectedQuote{
replyItem,
draft.reply.quote,
draft.reply.quoteOffset,
};
state->forward = std::move(args.forward);
state->webpage = draft.webpage;
state->preview = previewData;
state->shown = previewData ? Section::Link : Section::Reply;
if (replyItem && previewData) {
box->setNoContentMargin(true);
state->tabs = box->setPinnedToTopContent(
object_ptr<Ui::SettingsSlider>(
box.get(),
st::defaultTabsSlider));
state->tabs->resizeToWidth(st::boxWideWidth);
state->tabs->move(0, 0);
state->tabs->setRippleTopRoundRadius(st::boxRadius);
state->tabs->setSections({
tr::lng_reply_header_short(tr::now),
tr::lng_link_header_short(tr::now),
});
state->tabs->setActiveSectionFast(1);
state->tabs->sectionActivated(
) | rpl::start_with_next([=](int section) {
state->shown = section ? Section::Link : Section::Reply;
}, box->lifetime());
} else {
box->setTitle(previewData
? tr::lng_link_options_header()
: draft.reply.quote.empty()
? tr::lng_reply_options_header()
: tr::lng_reply_options_quote());
}
state->rebuild = [=] {
const auto hasLink = (state->preview != nullptr);
const auto hasReply = (state->quote.current().item != nullptr);
const auto hasForward = !state->forward.items.empty();
if (!hasLink && !hasReply && !hasForward) {
box->closeBox();
return;
}
const auto section = state->shown.current();
const auto changeSection = (section == Section::Link)
? !hasLink
: (section == Section::Reply)
? !hasReply
: !hasForward;
const auto now = !changeSection
? section
: hasLink
? Section::Link
: hasReply
? Section::Reply
: Section::Forward;
auto labels = std::vector<QString>();
auto indices = base::flat_map<Section, int>();
auto sections = std::vector<Section>();
const auto push = [&](Section section, tr::phrase<> phrase) {
indices[section] = labels.size();
labels.push_back(phrase(tr::now));
sections.push_back(section);
};
if (hasLink) {
push(Section::Link, tr::lng_link_header_short);
}
if (hasReply) {
push(Section::Reply, tr::lng_reply_header_short);
}
if (hasForward) {
push(Section::Forward, tr::lng_forward_header_short);
}
if (labels.size() > 1) {
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(labels);
state->tabs->setActiveSectionFast(indices[now]);
state->tabs->sectionActivated(
) | rpl::start_with_next([=](int index) {
state->shown = sections[index];
}, box->lifetime());
} else {
const auto forwardCount = state->forward.items.size();
box->setTitle(hasLink
? tr::lng_link_options_header()
: hasReply
? (state->quote.current().text.empty()
? tr::lng_reply_options_header()
: tr::lng_reply_options_quote())
: (forwardCount == 1)
? tr::lng_forward_title()
: tr::lng_forward_many_title(
lt_count,
rpl::single(forwardCount * 1.0)));
}
state->shown.force_assign(now);
};
state->rebuild();
const auto bottom = box->setPinnedToBottomContent(
object_ptr<Ui::VerticalLayout>(box));
@ -675,9 +816,17 @@ void DraftOptionsBox(
};
const auto finish = [=](
FullReplyTo result,
Data::WebPageDraft webpage) {
Data::WebPageDraft webpage,
std::optional<Data::ForwardOptions> options) {
const auto weak = Ui::MakeWeak(box);
done(std::move(result), std::move(webpage));
auto forward = Data::ForwardDraft();
if (options) {
forward.options = *options;
for (const auto &item : state->forward.items) {
forward.ids.push_back(item->fullId());
}
}
done(std::move(result), std::move(webpage), std::move(forward));
if (const auto strong = weak.data()) {
strong->closeBox();
}
@ -716,7 +865,7 @@ void DraftOptionsBox(
st::settingsAttentionButtonWithIcon,
{ &st::menuIconDeleteAttention }
)->setClickedCallback([=] {
finish({}, state->webpage);
finish({}, state->webpage, state->forward.options);
});
if (!item->originalText().empty()) {
@ -774,7 +923,8 @@ void DraftOptionsBox(
st::settingsAttentionButtonWithIcon,
{ &st::menuIconDeleteAttention }
)->setClickedCallback([=] {
finish(resolveReply(), { .removed = true });
const auto options = state->forward.options;
finish(resolveReply(), { .removed = true }, options);
});
if (args.links.size() > 1) {
@ -783,6 +933,92 @@ void DraftOptionsBox(
}
};
const auto setupForwardActions = [=] {
using Options = Data::ForwardOptions;
const auto now = state->forward.options;
const auto &items = state->forward.items;
const auto count = items.size();
const auto dropNames = (now != Options::PreserveInfo);
const auto sendersCount = ItemsForwardSendersCount(items);
const auto captionsCount = ItemsForwardCaptionsCount(items);
const auto hasOnlyForcedForwardedInfo = !captionsCount
&& HasOnlyForcedForwardedInfo(items);
const auto dropCaptions = (now == Options::NoNamesAndCaptions);
AddFilledSkip(bottom);
if (!hasOnlyForcedForwardedInfo) {
Settings::AddButtonWithIcon(
bottom,
(dropNames
? (sendersCount == 1
? tr::lng_forward_action_show_sender
: tr::lng_forward_action_show_senders)
: (sendersCount == 1
? tr::lng_forward_action_hide_sender
: tr::lng_forward_action_hide_senders))(),
st::settingsButton,
{ dropNames
? &st::menuIconUserShow
: &st::menuIconUserHide }
)->setClickedCallback([=] {
state->forward.options = dropNames
? Options::PreserveInfo
: Options::NoSenderNames;
state->shown.force_assign(Section::Forward);
});
}
if (captionsCount) {
Settings::AddButtonWithIcon(
bottom,
(dropCaptions
? (captionsCount == 1
? tr::lng_forward_action_show_caption
: tr::lng_forward_action_show_captions)
: (captionsCount == 1
? tr::lng_forward_action_hide_caption
: tr::lng_forward_action_hide_captions))(),
st::settingsButton,
{ dropCaptions
? &st::menuIconCaptionShow
: &st::menuIconCaptionHide }
)->setClickedCallback([=] {
state->forward.options = dropCaptions
? Options::NoSenderNames
: Options::NoNamesAndCaptions;
state->shown.force_assign(Section::Forward);
});
}
Settings::AddButtonWithIcon(
bottom,
tr::lng_forward_action_change_recipient(),
st::settingsButton,
{ &st::menuIconReplace }
)->setClickedCallback([=] {
auto draft = base::take(state->forward);
finish(resolveReply(), state->webpage, std::nullopt);
Window::ShowForwardMessagesBox(show, {
.ids = show->session().data().itemsToIds(draft.items),
.options = draft.options,
});
});
Settings::AddButtonWithIcon(
bottom,
tr::lng_forward_action_remove(),
st::settingsAttentionButtonWithIcon,
{ &st::menuIconDeleteAttention }
)->setClickedCallback([=] {
finish(resolveReply(), state->webpage, std::nullopt);
});
AddFilledSkip(bottom);
Ui::AddDividerText(bottom, (count == 1
? tr::lng_forward_about()
: tr::lng_forward_many_about()));
};
const auto &resolver = args.resolver;
state->performSwitch = [=](const QString &link, WebPageData *page) {
const auto now = base::unixtime::now();
@ -847,20 +1083,27 @@ void DraftOptionsBox(
state->shown.value() | rpl::start_with_next([=](Section shown) {
bottom->clear();
state->shownLifetime.destroy();
if (shown == Section::Reply) {
state->quote = state->wrap->showQuoteSelector(
state->quote.current());
setupReplyActions();
} else {
state->wrap->showLinkSelector(
draft.textWithTags,
state->webpage,
linkRanges,
state->link
) | rpl::start_with_next([=](QString link) {
switchTo(link);
}, state->shownLifetime);
setupLinkActions();
switch (shown) {
case Section::Reply: {
state->quote = state->wrap->showQuoteSelector(
state->quote.current());
setupReplyActions();
} break;
case Section::Link: {
state->wrap->showLinkSelector(
draft.textWithTags,
state->webpage,
linkRanges,
state->link
) | rpl::start_with_next([=](QString link) {
switchTo(link);
}, state->shownLifetime);
setupLinkActions();
} break;
case Section::Forward: {
state->wrap->showForwardSelector(state->forward);
setupForwardActions();
} break;
}
}, box->lifetime());
@ -879,7 +1122,8 @@ void DraftOptionsBox(
.text = { tr::lng_reply_quote_long_text(tr::now) },
});
} else {
finish(resolveReply(), state->webpage);
const auto options = state->forward.options;
finish(resolveReply(), state->webpage, options);
}
});
@ -887,31 +1131,29 @@ void DraftOptionsBox(
box->closeBox();
});
if (replyItem) {
args.show->session().data().itemRemoved(
) | rpl::filter([=](not_null<const HistoryItem*> removed) {
const auto current = state->quote.current().item;
if ((removed == replyItem) || (removed == current)) {
return true;
}
const auto group = current->history()->owner().groups().find(
current);
return (group && ranges::contains(group->items, removed));
}) | rpl::start_with_next([=] {
if (previewData) {
state->tabs = nullptr;
box->setPinnedToTopContent(
object_ptr<Ui::RpWidget>(nullptr));
box->setNoContentMargin(false);
box->setTitle(state->quote.current().text.empty()
? tr::lng_reply_options_header()
: tr::lng_reply_options_quote());
state->shown = Section::Link;
} else {
box->closeBox();
}
}, box->lifetime());
}
args.show->session().data().itemRemoved(
) | rpl::start_with_next([=](not_null<const HistoryItem*> removed) {
const auto inReply = (state->quote.current().item == removed);
if (inReply) {
state->quote = SelectedQuote();
}
const auto i = ranges::find(state->forward.items, removed);
const auto inForward = (i != end(state->forward.items));
if (inForward) {
state->forward.items.erase(i);
}
if (inReply || inForward) {
state->rebuild();
}
}, box->lifetime());
args.show->session().data().itemViewRefreshRequest(
) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
if (state->wrap->hasViewForItem(item)) {
state->rebuild();
}
}, box->lifetime());
}
struct AuthorSelector {
@ -1165,7 +1407,7 @@ void EditDraftOptions(EditDraftOptionsArgs &&args) {
&& !previewDataRaw->failed)
? previewDataRaw
: nullptr;
if (!replyItem && !previewData) {
if (!replyItem && !previewData && args.forward.items.empty()) {
return;
}
args.show->show(

View file

@ -29,9 +29,10 @@ struct EditDraftOptionsArgs {
not_null<History*> history;
Data::Draft draft;
QString usedLink;
Data::ResolvedForwardDraft forward;
std::vector<MessageLinkRange> links;
std::shared_ptr<WebpageResolver> resolver;
Fn<void(FullReplyTo, Data::WebPageDraft)> done;
Fn<void(FullReplyTo, Data::WebPageDraft, Data::ForwardDraft)> done;
Fn<void(FullReplyTo)> highlight;
Fn<void()> clearOldDraft;
};

View file

@ -44,19 +44,6 @@ constexpr auto kUnknownVersion = -1;
constexpr auto kNameWithCaptionsVersion = -2;
constexpr auto kNameNoCaptionsVersion = -3;
[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) {
for (const auto &item : list) {
if (const auto media = item->media()) {
if (!media->forceForwardedInfo()) {
return false;
}
} else {
return false;
}
}
return true;
}
} // namespace
ForwardPanel::ForwardPanel(Fn<void()> repaint)
@ -228,6 +215,10 @@ void ForwardPanel::itemRemoved(not_null<const HistoryItem*> item) {
}
}
const Data::ResolvedForwardDraft &ForwardPanel::draft() const {
return _data;
}
const HistoryItemsList &ForwardPanel::items() const {
return _data.items;
}
@ -236,63 +227,17 @@ bool ForwardPanel::empty() const {
return _data.items.empty();
}
void ForwardPanel::editOptions(std::shared_ptr<ChatHelpers::Show> show) {
using Options = Data::ForwardOptions;
const auto now = _data.options;
const auto count = _data.items.size();
const auto dropNames = (now != Options::PreserveInfo);
const auto sendersCount = ItemsForwardSendersCount(_data.items);
const auto captionsCount = ItemsForwardCaptionsCount(_data.items);
const auto hasOnlyForcedForwardedInfo = !captionsCount
&& HasOnlyForcedForwardedInfo(_data.items);
const auto dropCaptions = (now == Options::NoNamesAndCaptions);
const auto weak = base::make_weak(this);
const auto changeRecipient = crl::guard(this, [=] {
if (_data.items.empty()) {
return;
}
auto data = base::take(_data);
_to->owningHistory()->setForwardDraft(_to->topicRootId(), {});
Window::ShowForwardMessagesBox(show, {
.ids = _to->owner().itemsToIds(data.items),
.options = data.options,
});
});
if (hasOnlyForcedForwardedInfo) {
changeRecipient();
void ForwardPanel::applyOptions(Data::ForwardOptions options) {
if (_data.items.empty()) {
return;
} else if (_data.options != options) {
_data.options = options;
_to->owningHistory()->setForwardDraft(_to->topicRootId(), {
.ids = _to->owner().itemsToIds(_data.items),
.options = options,
});
_repaint();
}
const auto optionsChanged = crl::guard(weak, [=](
Ui::ForwardOptions options) {
if (_data.items.empty()) {
return;
}
const auto newOptions = (options.captionsCount
&& options.dropCaptions)
? Options::NoNamesAndCaptions
: options.dropNames
? Options::NoSenderNames
: Options::PreserveInfo;
if (_data.options != newOptions) {
_data.options = newOptions;
_to->owningHistory()->setForwardDraft(_to->topicRootId(), {
.ids = _to->owner().itemsToIds(_data.items),
.options = newOptions,
});
_repaint();
}
});
show->showBox(Box(
Ui::ForwardOptionsBox,
count,
Ui::ForwardOptions{
.sendersCount = sendersCount,
.captionsCount = captionsCount,
.dropNames = dropNames,
.dropCaptions = dropCaptions,
},
optionsChanged,
changeRecipient));
}
void ForwardPanel::editToNextOption() {
@ -485,7 +430,19 @@ void EditWebPageOptions(
box->closeBox();
});
}));
}
bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list) {
for (const auto &item : list) {
if (const auto media = item->media()) {
if (!media->forceForwardedInfo()) {
return false;
}
} else {
return false;
}
}
return true;
}
} // namespace HistoryView::Controls

View file

@ -47,9 +47,10 @@ public:
[[nodiscard]] rpl::producer<> itemsUpdated() const;
void editOptions(std::shared_ptr<ChatHelpers::Show> show);
void applyOptions(Data::ForwardOptions options);
void editToNextOption();
[[nodiscard]] const Data::ResolvedForwardDraft &draft() const;
[[nodiscard]] const HistoryItemsList &items() const;
[[nodiscard]] bool empty() const;
@ -83,4 +84,6 @@ void EditWebPageOptions(
Data::WebPageDraft draft,
Fn<void(Data::WebPageDraft)> done);
[[nodiscard]] bool HasOnlyForcedForwardedInfo(const HistoryItemsList &list);
} // namespace HistoryView::Controls

View file

@ -65,72 +65,4 @@ void FillForwardOptions(
}
}
void ForwardOptionsBox(
not_null<GenericBox*> box,
int count,
ForwardOptions options,
Fn<void(ForwardOptions)> optionsChanged,
Fn<void()> changeRecipient) {
Expects(optionsChanged != nullptr);
Expects(changeRecipient != nullptr);
box->setTitle((count == 1)
? tr::lng_forward_title()
: tr::lng_forward_many_title(
lt_count,
rpl::single(count) | tr::to_count()));
box->addButton(tr::lng_box_done(), [=] {
box->closeBox();
});
box->events(
) | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::KeyPress) {
const auto k = static_cast<QKeyEvent*>(e.get());
if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return) {
box->closeBox();
}
}
}, box->lifetime());
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
(count == 1
? tr::lng_forward_about()
: tr::lng_forward_many_about()),
st::boxLabel),
st::boxRowPadding);
const auto checkboxPadding = style::margins(
st::boxRowPadding.left(),
st::boxRowPadding.left(),
st::boxRowPadding.right(),
st::boxRowPadding.bottom());
auto createView = [&](rpl::producer<QString> &&text, bool checked) {
return box->addRow(
object_ptr<Ui::Checkbox>(
box.get(),
std::move(text),
checked,
st::defaultBoxCheckbox),
checkboxPadding)->checkView();
};
FillForwardOptions(
std::move(createView),
options,
std::move(optionsChanged),
box->lifetime());
box->addRow(
object_ptr<Ui::LinkButton>(
box.get(),
tr::lng_forward_change_recipient(tr::now)),
checkboxPadding
)->setClickedCallback([=] {
box->closeBox();
changeRecipient();
});
}
} // namespace Ui

View file

@ -28,11 +28,4 @@ void FillForwardOptions(
Fn<void(ForwardOptions)> optionsChanged,
rpl::lifetime &lifetime);
void ForwardOptionsBox(
not_null<GenericBox*> box,
int count,
ForwardOptions options,
Fn<void(ForwardOptions)> optionsChanged,
Fn<void()> changeRecipient);
} // namespace Ui

View file

@ -150,6 +150,10 @@ menuIconAbove: icon {{ "menu/link_above", menuIconColor }};
menuIconBelow: icon {{ "menu/link_below", menuIconColor }};
menuIconEnlarge: icon {{ "menu/link_enlarge", menuIconColor }};
menuIconShrink: icon {{ "menu/link_shrink", menuIconColor }};
menuIconUserShow: icon {{ "menu/name_show", menuIconColor }};
menuIconUserHide: icon {{ "menu/name_hide", menuIconColor }};
menuIconCaptionShow: icon {{ "menu/caption_show", menuIconColor }};
menuIconCaptionHide: icon {{ "menu/caption_hide", menuIconColor }};
menuIconAsTopics: icon {{ "menu/mode_topics", menuIconColor }};
menuIconAsMessages: icon {{ "menu/mode_messages", menuIconColor }};
menuIconTagFilter: icon{{ "menu/tag_filter", menuIconColor }};