tdesktop/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.cpp
2023-10-26 14:08:40 +04:00

554 lines
14 KiB
C++

/*
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_forward_panel.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/history_item_components.h"
#include "history/view/history_view_item_preview.h"
#include "data/data_session.h"
#include "data/data_media_types.h"
#include "data/data_forum_topic.h"
#include "main/main_session.h"
#include "ui/chat/forward_options_box.h"
#include "ui/effects/spoiler_mess.h"
#include "ui/text/text_options.h"
#include "ui/text/text_utilities.h"
#include "ui/painter.h"
#include "ui/power_saving.h"
#include "core/ui_integration.h"
#include "lang/lang_keys.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
#include "styles/style_chat.h"
#include "styles/style_chat_helpers.h"
#include "apiwrap.h"
#include "boxes/peer_list_controllers.h"
#include "data/data_changes.h"
#include "settings/settings_common.h"
#include "ui/widgets/buttons.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
namespace HistoryView::Controls {
namespace {
constexpr auto kUnknownVersion = -1;
constexpr auto kNameWithCaptionsVersion = -2;
constexpr auto kNameNoCaptionsVersion = -3;
[[nodiscard]] bool HasCaptions(const HistoryItemsList &list) {
for (const auto &item : list) {
if (const auto media = item->media()) {
if (!item->originalText().text.isEmpty()
&& media->allowsEditCaption()) {
return true;
}
}
}
return false;
}
[[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)
: _repaint(std::move(repaint)) {
}
void ForwardPanel::update(
Data::Thread *to,
Data::ResolvedForwardDraft draft) {
if (_to == to
&& _data.items == draft.items
&& _data.options == draft.options) {
return;
}
_dataLifetime.destroy();
_data = std::move(draft);
_to = to;
if (!empty()) {
Assert(to != nullptr);
_data.items.front()->history()->owner().itemRemoved(
) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
itemRemoved(item);
}, _dataLifetime);
if (const auto topic = _to->asTopic()) {
topic->destroyed(
) | rpl::start_with_next([=] {
update(nullptr, {});
}, _dataLifetime);
}
updateTexts();
}
_itemsUpdated.fire({});
}
rpl::producer<> ForwardPanel::itemsUpdated() const {
return _itemsUpdated.events();
}
void ForwardPanel::checkTexts() {
if (empty()) {
return;
}
const auto keepNames = (_data.options
== Data::ForwardOptions::PreserveInfo);
const auto keepCaptions = (_data.options
!= Data::ForwardOptions::NoNamesAndCaptions);
auto version = keepNames
? 0
: keepCaptions
? kNameWithCaptionsVersion
: kNameNoCaptionsVersion;
if (keepNames) {
for (const auto item : _data.items) {
if (const auto from = item->originalSender()) {
version += from->nameVersion();
} else if (const auto info = item->hiddenSenderInfo()) {
++version;
} else {
Unexpected("Corrupt forwarded information in message.");
}
}
}
if (_nameVersion != version) {
_nameVersion = version;
updateTexts();
}
}
void ForwardPanel::updateTexts() {
const auto repainter = gsl::finally([&] {
_repaint();
});
if (empty()) {
_from.clear();
_text.clear();
return;
}
QString from;
TextWithEntities text;
const auto keepNames = (_data.options
== Data::ForwardOptions::PreserveInfo);
const auto keepCaptions = (_data.options
!= Data::ForwardOptions::NoNamesAndCaptions);
if (const auto count = int(_data.items.size())) {
auto insertedPeers = base::flat_set<not_null<PeerData*>>();
auto insertedNames = base::flat_set<QString>();
auto fullname = QString();
auto names = std::vector<QString>();
names.reserve(_data.items.size());
for (const auto item : _data.items) {
if (const auto from = item->originalSender()) {
if (!insertedPeers.contains(from)) {
insertedPeers.emplace(from);
names.push_back(from->shortName());
fullname = from->name();
}
} else if (const auto info = item->hiddenSenderInfo()) {
if (!insertedNames.contains(info->name)) {
insertedNames.emplace(info->name);
names.push_back(info->firstName);
fullname = info->name;
}
} else {
Unexpected("Corrupt forwarded information in message.");
}
}
if (!keepNames) {
from = tr::lng_forward_sender_names_removed(tr::now);
} else if (names.size() > 2) {
from = tr::lng_forwarding_from(
tr::now,
lt_count,
names.size() - 1,
lt_user,
names[0]);
} else if (names.size() < 2) {
from = fullname;
} else {
from = tr::lng_forwarding_from_two(
tr::now,
lt_user,
names[0],
lt_second_user,
names[1]);
}
if (count < 2) {
const auto item = _data.items.front();
text = item->toPreview({
.hideSender = true,
.hideCaption = !keepCaptions,
.generateImages = false,
.ignoreGroup = true,
}).text;
const auto history = item->history();
const auto dropCustomEmoji = !history->session().premium()
&& !_to->peer()->isSelf()
&& (item->computeDropForwardedInfo() || !keepNames);
if (dropCustomEmoji) {
text = DropCustomEmoji(std::move(text));
}
} else {
text = Ui::Text::Colorized(
tr::lng_forward_messages(tr::now, lt_count, count));
}
}
_from.setText(st::msgNameStyle, from, Ui::NameTextOptions());
const auto context = Core::MarkedTextContext{
.session = &_to->session(),
.customEmojiRepaint = _repaint,
};
_text.setMarkedText(
st::defaultTextStyle,
text,
Ui::DialogTextOptions(),
context);
}
void ForwardPanel::refreshTexts() {
_nameVersion = kUnknownVersion;
checkTexts();
}
void ForwardPanel::itemRemoved(not_null<const HistoryItem*> item) {
const auto i = ranges::find(_data.items, item);
if (i != end(_data.items)) {
_data.items.erase(i);
refreshTexts();
_itemsUpdated.fire({});
}
}
const HistoryItemsList &ForwardPanel::items() const {
return _data.items;
}
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 hasCaptions = HasCaptions(_data.items);
const auto hasOnlyForcedForwardedInfo = hasCaptions
? false
: 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();
return;
}
const auto optionsChanged = crl::guard(weak, [=](
Ui::ForwardOptions options) {
if (_data.items.empty()) {
return;
}
const auto newOptions = (options.hasCaptions
&& 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{
.dropNames = dropNames,
.hasCaptions = hasCaptions,
.dropCaptions = dropCaptions,
},
optionsChanged,
changeRecipient));
}
void ForwardPanel::editToNextOption() {
using Options = Data::ForwardOptions;
const auto hasCaptions = HasCaptions(_data.items);
const auto hasOnlyForcedForwardedInfo = hasCaptions
? false
: HasOnlyForcedForwardedInfo(_data.items);
if (hasOnlyForcedForwardedInfo) {
return;
}
const auto now = _data.options;
const auto next = (now == Options::PreserveInfo)
? Options::NoSenderNames
: ((now == Options::NoSenderNames) && hasCaptions)
? Options::NoNamesAndCaptions
: Options::PreserveInfo;
_to->owningHistory()->setForwardDraft(_to->topicRootId(), {
.ids = _to->owner().itemsToIds(_data.items),
.options = next,
});
_repaint();
}
void ForwardPanel::paint(
Painter &p,
int x,
int y,
int available,
int outerWidth) const {
if (empty()) {
return;
}
const_cast<ForwardPanel*>(this)->checkTexts();
const auto now = crl::now();
const auto paused = p.inactive();
const auto pausedSpoiler = paused || On(PowerSaving::kChatSpoiler);
const auto firstItem = _data.items.front();
const auto firstMedia = firstItem->media();
const auto hasPreview = (_data.items.size() < 2)
&& firstMedia
&& firstMedia->hasReplyPreview();
const auto preview = hasPreview ? firstMedia->replyPreview() : nullptr;
const auto spoiler = preview && firstMedia->hasSpoiler();
if (!spoiler) {
_spoiler = nullptr;
} else if (!_spoiler) {
_spoiler = std::make_unique<Ui::SpoilerAnimation>(_repaint);
}
if (preview) {
auto to = QRect(
x,
y + st::msgReplyPadding.top(),
st::msgReplyBarSize.height(),
st::msgReplyBarSize.height());
p.drawPixmap(to.x(), to.y(), preview->pixSingle(
preview->size() / style::DevicePixelRatio(),
{
.options = Images::Option::RoundSmall,
.outer = to.size(),
}));
if (_spoiler) {
Ui::FillSpoilerRect(p, to, Ui::DefaultImageSpoiler().frame(
_spoiler->index(now, pausedSpoiler)));
}
const auto skip = st::msgReplyBarSize.height()
+ st::msgReplyBarSkip
- st::msgReplyBarSize.width()
- st::msgReplyBarPos.x();
x += skip;
available -= skip;
}
p.setPen(st::historyReplyNameFg);
_from.drawElided(
p,
x,
y + st::msgReplyPadding.top(),
available);
p.setPen(st::historyComposeAreaFg);
_text.draw(p, {
.position = QPoint(
x,
y + st::msgReplyPadding.top() + st::msgServiceNameFont->height),
.availableWidth = available,
.palette = &st::historyComposeAreaPalette,
.spoiler = Ui::Text::DefaultSpoilerCache(),
.now = now,
.pausedEmoji = paused || On(PowerSaving::kEmojiChat),
.pausedSpoiler = pausedSpoiler,
.elisionOneLine = true,
});
}
void ClearDraftReplyTo(not_null<Data::Thread*> thread, FullMsgId equalTo) {
ClearDraftReplyTo(
thread->owningHistory(),
thread->topicRootId(),
equalTo);
}
void ClearDraftReplyTo(
not_null<History*> history,
MsgId topicRootId,
FullMsgId equalTo) {
const auto local = history->localDraft(topicRootId);
if (!local || (equalTo && local->reply.messageId != equalTo)) {
return;
}
auto draft = *local;
draft.reply = { .topicRootId = topicRootId };
if (Data::DraftIsNull(&draft)) {
history->clearLocalDraft(topicRootId);
} else {
history->setLocalDraft(
std::make_unique<Data::Draft>(std::move(draft)));
}
if (const auto thread = history->threadFor(topicRootId)) {
history->session().api().saveDraftToCloudDelayed(thread);
}
}
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(rpl::single(u"Reply in..."_q));
}
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), nullptr);
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::PreviewState::Allowed));
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()> highlight,
Fn<void()> clearOldDraft) {
show->show(Box([=](not_null<Ui::GenericBox*> box) {
box->setTitle(rpl::single(u"Reply to Message"_q));
Settings::AddButton(
box->verticalLayout(),
rpl::single(u"Reply in another chat"_q),
st::settingsButton,
{ &st::menuIconReply }
)->setClickedCallback([=] {
ShowReplyToChatBox(show, reply, clearOldDraft);
});
Settings::AddButton(
box->verticalLayout(),
rpl::single(u"Show message"_q),
st::settingsButton,
{ &st::menuIconShowInChat }
)->setClickedCallback(highlight);
box->addButton(tr::lng_box_ok(), [=] {
box->closeBox();
});
}));
}
} // namespace HistoryView::Controls