From fee8690ca6e592918705dac688f2e2829c3c628b Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 18 Jun 2017 16:08:49 +0300 Subject: [PATCH] Channel action log items display and layout. --- Telegram/Resources/langs/lang.strings | 29 ++ Telegram/SourceFiles/core/utils.h | 22 ++ Telegram/SourceFiles/history.cpp | 13 +- Telegram/SourceFiles/history.h | 4 +- .../history/history_admin_log_inner.cpp | 109 ++++++- .../history/history_admin_log_inner.h | 48 ++- .../history/history_admin_log_item.cpp | 285 ++++++++++++++++++ .../history/history_admin_log_item.h | 68 +++++ .../history/history_admin_log_section.cpp | 2 +- Telegram/SourceFiles/history/history_item.cpp | 52 ++-- Telegram/SourceFiles/history/history_item.h | 34 ++- .../SourceFiles/history/history_message.cpp | 41 +-- .../SourceFiles/history/history_message.h | 30 +- .../profile/profile_block_info.cpp | 9 +- Telegram/gyp/telegram_sources.txt | 2 + 15 files changed, 659 insertions(+), 89 deletions(-) create mode 100644 Telegram/SourceFiles/history/history_admin_log_item.cpp create mode 100644 Telegram/SourceFiles/history/history_admin_log_item.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 94e3398fe..7b2180523 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1323,6 +1323,35 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org "lng_admin_log_no_events_title" = "No events yet"; "lng_admin_log_no_events_text" = "There were no service actions taken by the group's members and admins in the last 48 hours."; +"lng_admin_log_empty_text" = "Empty"; +"lng_admin_log_changed_title_group" = "{from} changed group name to «{title}»"; +"lng_admin_log_changed_title_channel" = "{from} changed channel name to «{title}»"; +"lng_admin_log_changed_description_group" = "{from} edited group description:"; +"lng_admin_log_removed_description_group" = "{from} removed group description"; +"lng_admin_log_changed_description_channel" = "{from} edited channel description:"; +"lng_admin_log_removed_description_channel" = "{from} removed channel description"; +"lng_admin_log_previous_description" = "Previous description"; +"lng_admin_log_changed_link_group" = "{from} changed group link:"; +"lng_admin_log_removed_link_group" = "{from} removed group link"; +"lng_admin_log_changed_link_channel" = "{from} changed channel link:"; +"lng_admin_log_removed_link_channel" = "{from} removed channel link"; +"lng_admin_log_previous_link" = "Previous link"; +"lng_admin_log_changed_photo_group" = "{from} changed group photo"; +"lng_admin_log_changed_photo_channel" = "{from} changed channel photo"; +"lng_admin_log_invites_enabled" = "{from} enabled group invites"; +"lng_admin_log_invites_disabled" = "{from} disabled group invites"; +"lng_admin_log_signatures_enabled" = "{from} enabled signatures"; +"lng_admin_log_signatures_disabled" = "{from} disabled signatures"; +"lng_admin_log_pinned_message" = "{from} pinned message:"; +"lng_admin_log_edited_caption" = "{from} edited caption:"; +"lng_admin_log_removed_caption" = "{from} removed caption:"; +"lng_admin_log_previous_caption" = "Original caption"; +"lng_admin_log_edited_message" = "{from} edited message:"; +"lng_admin_log_previous_message" = "Original message"; +"lng_admin_log_deleted_message" = "{from} deleted message:"; +"lng_admin_log_participant_joined" = "{from} joined the group"; +"lng_admin_log_participant_left" = "{from} left the group"; + // Not used "lng_topbar_info" = "Info"; diff --git a/Telegram/SourceFiles/core/utils.h b/Telegram/SourceFiles/core/utils.h index d3e14b568..92888fe49 100644 --- a/Telegram/SourceFiles/core/utils.h +++ b/Telegram/SourceFiles/core/utils.h @@ -228,6 +228,28 @@ inline int compare_bytes(const_byte_span a, const_byte_span b) { return (aSize > bSize) ? 1 : (aSize < bSize) ? -1 : memcmp(a.data(), b.data(), aSize); } +// Thanks https://stackoverflow.com/a/28139075 + +template +struct reversion_wrapper { + Container &container; +}; + +template +auto begin(reversion_wrapper wrapper) { + return std::rbegin(wrapper.container); +} + +template +auto end(reversion_wrapper wrapper) { + return std::rend(wrapper.container); +} + +template +reversion_wrapper reversed(Container &&container) { + return { container }; +} + } // namespace base // using for_const instead of plain range-based for loop to ensure usage of const_iterator diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 03e8af6f0..4091204c7 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -785,9 +785,10 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, } switch (msg.type()) { - case mtpc_messageEmpty: - result = HistoryService::create(this, msg.c_messageEmpty().vid.v, date(), lang(lng_message_empty)); - break; + case mtpc_messageEmpty: { + auto message = HistoryService::PreparedText { lang(lng_message_empty) }; + result = HistoryService::create(this, msg.c_messageEmpty().vid.v, date(), message); + } break; case mtpc_message: { auto &m = msg.c_message(); @@ -851,7 +852,8 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, if (badMedia == MediaCheckResult::Unsupported) { result = createUnsupportedMessage(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v); } else if (badMedia == MediaCheckResult::Empty) { - result = HistoryService::create(this, m.vid.v, date(m.vdate), lang(lng_message_empty), m.vflags.v, m.has_from_id() ? m.vfrom_id.v : 0); + auto message = HistoryService::PreparedText { lang(lng_message_empty) }; + result = HistoryService::create(this, m.vid.v, date(m.vdate), message, m.vflags.v, m.has_from_id() ? m.vfrom_id.v : 0); } else { result = HistoryMessage::create(this, m); } @@ -1034,7 +1036,8 @@ HistoryItem *History::createItemGame(MsgId id, MTPDmessage::Flags flags, int32 v } HistoryItem *History::addNewService(MsgId msgId, QDateTime date, const QString &text, MTPDmessage::Flags flags, bool newMsg) { - return addNewItem(HistoryService::create(this, msgId, date, text, flags), newMsg); + auto message = HistoryService::PreparedText { text }; + return addNewItem(HistoryService::create(this, msgId, date, message, flags), newMsg); } HistoryItem *History::addNewMessage(const MTPMessage &msg, NewMessageType type) { diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index 253726e8e..e5d42a6b7 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -217,6 +217,9 @@ public: HistoryItem *addNewPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); HistoryItem *addNewGame(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, GameData *game, const MTPReplyMarkup &markup); + // Used only internally and for channel admin log. + HistoryItem *createItem(const MTPMessage &msg, bool applyServiceAction, bool detachExistingItem); + void addOlderSlice(const QVector &slice); void addNewerSlice(const QVector &slice); bool addToOverview(MediaOverviewType type, MsgId msgId, AddToOverviewMethod method); @@ -480,7 +483,6 @@ protected: void clearBlocks(bool leaveItems); - HistoryItem *createItem(const MTPMessage &msg, bool applyServiceAction, bool detachExistingItem); HistoryItem *createItemForwarded(MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *msg); HistoryItem *createItemDocument(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); HistoryItem *createItemPhoto(MsgId id, MTPDmessage::Flags flags, int32 viaBotId, MsgId replyTo, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); diff --git a/Telegram/SourceFiles/history/history_admin_log_inner.cpp b/Telegram/SourceFiles/history/history_admin_log_inner.cpp index dcada8839..427f15649 100644 --- a/Telegram/SourceFiles/history/history_admin_log_inner.cpp +++ b/Telegram/SourceFiles/history/history_admin_log_inner.cpp @@ -20,17 +20,20 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org */ #include "history/history_admin_log_inner.h" +#include "history/history_admin_log_item.h" #include "history/history_admin_log_section.h" namespace AdminLog { namespace { -constexpr int kEventsPerPage = 50; +constexpr int kEventsPerPage = 3; } // namespace -InnerWidget::InnerWidget(QWidget *parent, gsl::not_null channel) : TWidget(parent) -, _channel(channel) { +InnerWidget::InnerWidget(QWidget *parent, gsl::not_null channel, base::lambda scrollTo) : TWidget(parent) +, _channel(channel) +, _history(App::history(channel)) +, _scrollTo(std::move(scrollTo)) { setMouseTracking(true); } @@ -38,12 +41,30 @@ void InnerWidget::setVisibleTopBottom(int visibleTop, int visibleBottom) { _visibleTop = visibleTop; _visibleBottom = visibleBottom; + updateVisibleTopItem(); checkPreloadMore(); } +void InnerWidget::updateVisibleTopItem() { + auto start = std::rbegin(_items), end = std::rend(_items); + auto from = std::upper_bound(start, end, _visibleTop, [](int top, auto &elem) { + return top <= elem->top() + elem->height(); + }); + if (from != end) { + _visibleTopItem = from->get(); + _visibleTopFromItem = _visibleTop - _visibleTopItem->top(); + } else { + _visibleTopItem = nullptr; + _visibleTopFromItem = _visibleTop; + } +} + void InnerWidget::checkPreloadMore() { if (_visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) { - preloadMore(); + preloadMore(Direction::Down); + } + if (_visibleTop < PreloadHeightsCount * (_visibleBottom - _visibleTop)) { + preloadMore(Direction::Up); } } @@ -65,7 +86,7 @@ void InnerWidget::saveState(gsl::not_null memento) const { void InnerWidget::restoreState(gsl::not_null memento) { //auto list = memento->getCommonGroups(); - _allLoaded = false; + //_allLoaded = false; //if (!list.empty()) { // showInitial(list); //} @@ -81,10 +102,13 @@ void InnerWidget::restoreState(gsl::not_null memento) { // updateSize(); //} -void InnerWidget::preloadMore() { - if (_preloadRequestId || _allLoaded) { +void InnerWidget::preloadMore(Direction direction) { + auto &requestId = (direction == Direction::Up) ? _preloadUpRequestId : _preloadDownRequestId; + auto &loadedFlag = (direction == Direction::Up) ? _upLoaded : _downLoaded; + if (requestId != 0 || loadedFlag) { return; } + auto flags = MTPchannels_GetAdminLog::Flags(0); auto filter = MTP_channelAdminLogEventsFilter(MTP_flags(_filterFlags)); if (_filterFlags != 0) { @@ -99,17 +123,48 @@ void InnerWidget::preloadMore() { flags |= MTPchannels_GetAdminLog::Flag::f_admins; } auto query = QString(); - auto _maxId = 0ULL; - auto _minId = 0ULL; - _preloadRequestId = request(MTPchannels_GetAdminLog(MTP_flags(flags), _channel->inputChannel, MTP_string(query), filter, MTP_vector(admins), MTP_long(_maxId), MTP_long(_minId), MTP_int(kEventsPerPage))).done([this](const MTPchannels_AdminLogResults &result) { - _preloadRequestId = 0; - _allLoaded = true; - }).fail([this](const RPCError &error) { + auto maxId = (direction == Direction::Up) ? _minId : 0; + auto minId = (direction == Direction::Up) ? 0 : _maxId; + requestId = request(MTPchannels_GetAdminLog(MTP_flags(flags), _channel->inputChannel, MTP_string(query), filter, MTP_vector(admins), MTP_long(maxId), MTP_long(minId), MTP_int(kEventsPerPage))).done([this, &requestId, &loadedFlag, direction](const MTPchannels_AdminLogResults &result) { + Expects(result.type() == mtpc_channels_adminLogResults); + requestId = 0; + + auto &results = result.c_channels_adminLogResults(); + App::feedUsers(results.vusers); + App::feedChats(results.vchats); + auto &events = results.vevents.v; + if (!events.empty()) { + _items.reserve(_items.size() + events.size()); + for_const (auto &event, events) { + t_assert(event.type() == mtpc_channelAdminLogEvent); + _items.push_back(std::make_unique(_history, _idManager, event.c_channelAdminLogEvent())); + } + if (!_items.empty()) { + _maxId = _items.front()->id(); + _minId = _items.back()->id(); + if (_minId == 1) { + _upLoaded = true; + } + } + itemsAdded(direction); + } else { + loadedFlag = true; + } + }).fail([this, &requestId, &loadedFlag](const RPCError &error) { + requestId = 0; + loadedFlag = true; }).send(); } +void InnerWidget::itemsAdded(Direction direction) { + updateSize(); +} + void InnerWidget::updateSize() { TWidget::resizeToWidth(width()); + auto newVisibleTop = (_visibleTopItem ? _visibleTopItem->top() : 0) + _visibleTopFromItem; + _scrollTo(newVisibleTop); + updateVisibleTopItem(); checkPreloadMore(); } @@ -117,6 +172,10 @@ int InnerWidget::resizeGetHeight(int newWidth) { update(); auto newHeight = 0; + for (auto &item : base::reversed(_items)) { + item->setTop(newHeight); + newHeight += item->resizeGetHeight(newWidth); + } return qMax(newHeight, _minHeight); } @@ -126,6 +185,30 @@ void InnerWidget::paintEvent(QPaintEvent *e) { auto ms = getms(); auto clip = e->rect(); + if (_items.empty() && _upLoaded && _downLoaded) { + paintEmpty(p); + } else { + auto start = std::rbegin(_items), end = std::rend(_items); + auto from = std::upper_bound(start, end, clip.top(), [](int top, auto &elem) { + return top <= elem->top() + elem->height(); + }); + auto to = std::lower_bound(start, end, clip.top() + clip.height(), [](auto &elem, int bottom) { + return elem->top() < bottom; + }); + if (from != end) { + auto top = (*from)->top(); + p.translate(0, top); + for (auto i = from; i != to; ++i) { + (*i)->draw(p, clip.translated(0, -top), TextSelection(), ms); + auto height = (*i)->height(); + top += height; + p.translate(0, height); + } + } + } +} + +void InnerWidget::paintEmpty(Painter &p) { //style::font font(st::msgServiceFont); //int32 w = font->width(lang(lng_willbe_history)) + st::msgPadding.left() + st::msgPadding.right(), h = font->height + st::msgServicePadding.top() + st::msgServicePadding.bottom() + 2; //QRect tr((width() - w) / 2, (height() - _field->height() - 2 * st::historySendPadding - h) / 2, w, h); diff --git a/Telegram/SourceFiles/history/history_admin_log_inner.h b/Telegram/SourceFiles/history/history_admin_log_inner.h index 9afb8c47e..8e7df385d 100644 --- a/Telegram/SourceFiles/history/history_admin_log_inner.h +++ b/Telegram/SourceFiles/history/history_admin_log_inner.h @@ -25,10 +25,31 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org namespace AdminLog { class SectionMemento; +class Item; + +class LocalIdManager { +public: + LocalIdManager() = default; + LocalIdManager(const LocalIdManager &other) = delete; + LocalIdManager &operator=(const LocalIdManager &other) = delete; + LocalIdManager(LocalIdManager &&other) : _counter(std::exchange(other._counter, ServerMaxMsgId)) { + } + LocalIdManager &operator=(LocalIdManager &&other) { + _counter = std::exchange(other._counter, ServerMaxMsgId); + return *this; + } + MsgId next() { + return ++_counter; + } + +private: + MsgId _counter = ServerMaxMsgId; + +}; class InnerWidget final : public TWidget, private MTP::Sender { public: - InnerWidget(QWidget *parent, gsl::not_null channel); + InnerWidget(QWidget *parent, gsl::not_null channel, base::lambda scrollTo); gsl::not_null channel() const { return _channel; @@ -64,20 +85,37 @@ protected: int resizeGetHeight(int newWidth) override; private: + enum class Direction { + Up, + Down, + }; void checkPreloadMore(); - void preloadMore(); + void updateVisibleTopItem(); + void preloadMore(Direction direction); + void itemsAdded(Direction direction); void updateSize(); + void paintEmpty(Painter &p); gsl::not_null _channel; + gsl::not_null _history; base::lambda _cancelledCallback; + base::lambda _scrollTo; + std::vector> _items; + LocalIdManager _idManager; int _minHeight = 0; int _visibleTop = 0; int _visibleBottom = 0; + Item *_visibleTopItem = nullptr; + int _visibleTopFromItem = 0; - int32 _preloadGroupId = 0; - mtpRequestId _preloadRequestId = 0; - bool _allLoaded = true; + // Up - max, Down - min. + uint64 _maxId = 0; + uint64 _minId = 0; + mtpRequestId _preloadUpRequestId = 0; + mtpRequestId _preloadDownRequestId = 0; + bool _upLoaded = false; + bool _downLoaded = true; MTPDchannelAdminLogEventsFilter::Flags _filterFlags = 0; std::vector> _filterAdmins; diff --git a/Telegram/SourceFiles/history/history_admin_log_item.cpp b/Telegram/SourceFiles/history/history_admin_log_item.cpp new file mode 100644 index 000000000..e80e19532 --- /dev/null +++ b/Telegram/SourceFiles/history/history_admin_log_item.cpp @@ -0,0 +1,285 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#include "history/history_admin_log_item.h" + +#include "lang/lang_keys.h" +#include "messenger.h" + +namespace AdminLog { +namespace { + +MTPMessage PrepareLogMessage(const MTPMessage &message, MsgId newId, int32 newDate) { + switch (message.type()) { + case mtpc_messageEmpty: return MTP_messageEmpty(MTP_int(newId)); + case mtpc_messageService: { + auto &data = message.c_messageService(); + auto flags = data.vflags.v & ~(MTPDmessageService::Flag::f_out | MTPDmessageService::Flag::f_post/* | MTPDmessageService::Flag::f_reply_to_msg_id*/); + return MTP_messageService(MTP_flags(flags), MTP_int(newId), data.vfrom_id, data.vto_id, data.vreply_to_msg_id, data.vdate, data.vaction); + } break; + case mtpc_message: { + auto &data = message.c_message(); + auto flags = data.vflags.v & ~(MTPDmessage::Flag::f_out | MTPDmessage::Flag::f_post | MTPDmessage::Flag::f_reply_to_msg_id); + return MTP_message(MTP_flags(flags), MTP_int(newId), data.vfrom_id, data.vto_id, data.vfwd_from, data.vvia_bot_id, data.vreply_to_msg_id, MTP_int(newDate), data.vmessage, data.vmedia, data.vreply_markup, data.ventities, data.vviews, data.vedit_date); + } break; + } + Unexpected("Type in PrepareLogMessage()"); +} + +bool MessageHasCaption(const MTPMessage &message) { + if (message.type() != mtpc_message) { + return false; + } + auto &data = message.c_message(); + auto mediaType = data.has_media() ? data.vmedia.type() : mtpc_messageMediaEmpty; + return (mediaType == mtpc_messageMediaDocument || mediaType == mtpc_messageMediaPhoto); +} + +} // namespace + +Item::Item(gsl::not_null history, LocalIdManager &idManager, const MTPDchannelAdminLogEvent &event) +: _id(event.vid.v) +, _history(history) +, _from(App::user(event.vuser_id.v)) { + auto &action = event.vaction; + auto date = event.vdate; + + using ServiceFlag = MTPDmessageService::Flag; + using Flag = MTPDmessage::Flag; + auto fromName = App::peerName(_from); + auto fromLink = _from->openLink(); + auto fromLinkText = textcmdLink(1, fromName); + + auto addSimpleServiceMessage = [this, &idManager, date, fromLink](const QString &text) { + auto message = HistoryService::PreparedText { text }; + message.links.push_back(fromLink); + addPart(HistoryService::create(_history, idManager.next(), ::date(date), message, 0, peerToUser(_from->id))); + }; + + auto createChangeTitle = [this, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionChangeTitle &action) { + auto text = (channel()->isMegagroup() ? lng_action_changed_title : lng_admin_log_changed_title_channel)(lt_from, fromLinkText, lt_title, qs(action.vnew_value)); + addSimpleServiceMessage(text); + }; + + auto createChangeAbout = [this, &idManager, date, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionChangeAbout &action) { + auto newValue = qs(action.vnew_value); + auto oldValue = qs(action.vprev_value); + auto text = (channel()->isMegagroup() + ? (newValue.isEmpty() ? lng_admin_log_removed_description_group : lng_admin_log_changed_description_group) + : (newValue.isEmpty() ? lng_admin_log_removed_description_channel : lng_admin_log_changed_description_channel) + )(lt_from, fromLinkText); + addSimpleServiceMessage(text); + + auto bodyFlags = Flag::f_entities | Flag::f_from_id; + auto bodyReplyTo = 0; + auto bodyViaBotId = 0; + auto newDescription = PrepareText(newValue, QString()); + auto body = HistoryMessage::create(_history, idManager.next(), bodyFlags, bodyReplyTo, bodyViaBotId, ::date(date), peerToUser(_from->id), newDescription); + if (!oldValue.isEmpty()) { + auto oldDescription = PrepareText(oldValue, QString()); + body->addLogEntryOriginal(lang(lng_admin_log_previous_description), oldDescription); + } + addPart(body); + }; + + auto createChangeUsername = [this, &idManager, date, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionChangeUsername &action) { + auto newValue = qs(action.vnew_value); + auto oldValue = qs(action.vprev_value); + auto text = (channel()->isMegagroup() + ? (newValue.isEmpty() ? lng_admin_log_removed_link_group : lng_admin_log_changed_link_group) + : (newValue.isEmpty() ? lng_admin_log_removed_link_channel : lng_admin_log_changed_link_channel) + )(lt_from, fromLinkText); + addSimpleServiceMessage(text); + + auto bodyFlags = Flag::f_entities | Flag::f_from_id; + auto bodyReplyTo = 0; + auto bodyViaBotId = 0; + auto newLink = newValue.isEmpty() ? TextWithEntities() : PrepareText(Messenger::Instance().createInternalLinkFull(newValue), QString()); + auto body = HistoryMessage::create(_history, idManager.next(), bodyFlags, bodyReplyTo, bodyViaBotId, ::date(date), peerToUser(_from->id), newLink); + if (!oldValue.isEmpty()) { + auto oldLink = PrepareText(Messenger::Instance().createInternalLinkFull(oldValue), QString()); + body->addLogEntryOriginal(lang(lng_admin_log_previous_link), oldLink); + } + addPart(body); + }; + + auto createChangePhoto = [this, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionChangePhoto &action) { + auto text = (channel()->isMegagroup() ? lng_admin_log_changed_photo_group : lng_admin_log_changed_photo_channel)(lt_from, fromLinkText); + addSimpleServiceMessage(text); + }; + + auto createToggleInvites = [this, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionToggleInvites &action) { + auto enabled = (action.vnew_value.type() == mtpc_boolTrue); + auto text = (enabled ? lng_admin_log_invites_enabled : lng_admin_log_invites_disabled)(lt_from, fromLinkText); + addSimpleServiceMessage(text); + }; + + auto createToggleSignatures = [this, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionToggleSignatures &action) { + auto enabled = (action.vnew_value.type() == mtpc_boolTrue); + auto text = (enabled ? lng_admin_log_signatures_enabled : lng_admin_log_signatures_disabled)(lt_from, fromLinkText); + addSimpleServiceMessage(text); + }; + + auto createUpdatePinned = [this, &idManager, date, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionUpdatePinned &action) { + auto text = lng_admin_log_pinned_message(lt_from, fromLinkText); + addSimpleServiceMessage(text); + + auto applyServiceAction = false; + auto detachExistingItem = false; + addPart(_history->createItem(PrepareLogMessage(action.vmessage, idManager.next(), date.v), applyServiceAction, detachExistingItem)); + }; + + auto createEditMessage = [this, &idManager, date, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionEditMessage &action) { + auto text = (MessageHasCaption(action.vnew_message) ? lng_admin_log_edited_caption : lng_admin_log_edited_message)(lt_from, fromLinkText); + addSimpleServiceMessage(text); + + auto applyServiceAction = false; + auto detachExistingItem = false; + addPart(_history->createItem(PrepareLogMessage(action.vnew_message, idManager.next(), date.v), applyServiceAction, detachExistingItem)); + }; + + auto createDeleteMessage = [this, &idManager, date, addSimpleServiceMessage, fromLinkText](const MTPDchannelAdminLogEventActionDeleteMessage &action) { + auto text = lng_admin_log_deleted_message(lt_from, fromLinkText); + addSimpleServiceMessage(text); + + auto applyServiceAction = false; + auto detachExistingItem = false; + addPart(_history->createItem(PrepareLogMessage(action.vmessage, idManager.next(), date.v), applyServiceAction, detachExistingItem)); + }; + + auto createParticipantJoin = [this, &idManager, addSimpleServiceMessage, fromLinkText]() { + auto text = lng_admin_log_participant_joined(lt_from, fromLinkText); + addSimpleServiceMessage(text); + }; + + auto createParticipantLeave = [this, &idManager, addSimpleServiceMessage, fromLinkText]() { + auto text = lng_admin_log_participant_left(lt_from, fromLinkText); + addSimpleServiceMessage(text); + }; + + auto createParticipantInvite = [this, &idManager, date](const MTPDchannelAdminLogEventActionParticipantInvite &action) { + auto bodyFlags = Flag::f_entities | Flag::f_from_id; + auto bodyReplyTo = 0; + auto bodyViaBotId = 0; + addPart(HistoryMessage::create(_history, idManager.next(), bodyFlags, bodyReplyTo, bodyViaBotId, ::date(date), peerToUser(_from->id), { "participant invite text", EntitiesInText() })); + }; + + auto createParticipantToggleBan = [this, &idManager, date](const MTPDchannelAdminLogEventActionParticipantToggleBan &action) { + auto bodyFlags = Flag::f_entities | Flag::f_from_id; + auto bodyReplyTo = 0; + auto bodyViaBotId = 0; + addPart(HistoryMessage::create(_history, idManager.next(), bodyFlags, bodyReplyTo, bodyViaBotId, ::date(date), peerToUser(_from->id), { "participant toggle ban text", EntitiesInText() })); + }; + + auto createParticipantToggleAdmin = [this, &idManager, date](const MTPDchannelAdminLogEventActionParticipantToggleAdmin &action) { + auto bodyFlags = Flag::f_entities | Flag::f_from_id; + auto bodyReplyTo = 0; + auto bodyViaBotId = 0; + addPart(HistoryMessage::create(_history, idManager.next(), bodyFlags, bodyReplyTo, bodyViaBotId, ::date(date), peerToUser(_from->id), { "participant toggle admin text", EntitiesInText() })); + }; + + switch (action.type()) { + case mtpc_channelAdminLogEventActionChangeTitle: createChangeTitle(action.c_channelAdminLogEventActionChangeTitle()); break; + case mtpc_channelAdminLogEventActionChangeAbout: createChangeAbout(action.c_channelAdminLogEventActionChangeAbout()); break; + case mtpc_channelAdminLogEventActionChangeUsername: createChangeUsername(action.c_channelAdminLogEventActionChangeUsername()); break; + case mtpc_channelAdminLogEventActionChangePhoto: createChangePhoto(action.c_channelAdminLogEventActionChangePhoto()); break; + case mtpc_channelAdminLogEventActionToggleInvites: createToggleInvites(action.c_channelAdminLogEventActionToggleInvites()); break; + case mtpc_channelAdminLogEventActionToggleSignatures: createToggleSignatures(action.c_channelAdminLogEventActionToggleSignatures()); break; + case mtpc_channelAdminLogEventActionUpdatePinned: createUpdatePinned(action.c_channelAdminLogEventActionUpdatePinned()); break; + case mtpc_channelAdminLogEventActionEditMessage: createEditMessage(action.c_channelAdminLogEventActionEditMessage()); break; + case mtpc_channelAdminLogEventActionDeleteMessage: createDeleteMessage(action.c_channelAdminLogEventActionDeleteMessage()); break; + case mtpc_channelAdminLogEventActionParticipantJoin: createParticipantJoin(); break; + case mtpc_channelAdminLogEventActionParticipantLeave: createParticipantLeave(); break; + case mtpc_channelAdminLogEventActionParticipantInvite: createParticipantInvite(action.c_channelAdminLogEventActionParticipantInvite()); break; + case mtpc_channelAdminLogEventActionParticipantToggleBan: createParticipantToggleBan(action.c_channelAdminLogEventActionParticipantToggleBan()); break; + case mtpc_channelAdminLogEventActionParticipantToggleAdmin: createParticipantToggleAdmin(action.c_channelAdminLogEventActionParticipantToggleAdmin()); break; + default: Unexpected("channelAdminLogEventAction type in AdminLog::Item::Item()"); + } +} + +void Item::addPart(HistoryItem *item) { + _parts.push_back(item); +} + +int Item::resizeGetHeight(int newWidth) { + _height = 0; + for (auto part : _parts) { + _height += part->resizeGetHeight(newWidth); + } + return _height; +} + +void Item::draw(Painter &p, QRect clip, TextSelection selection, TimeMs ms) { + auto top = 0; + for (auto part : _parts) { + auto height = part->height(); + if (clip.top() < top + height && clip.top() + clip.height() > top) { + p.translate(0, top); + part->draw(p, clip.translated(0, -top), selection, ms); + p.translate(0, -top); + } + top += height; + } +} + +bool Item::hasPoint(QPoint point) const { + auto top = 0; + for (auto part : _parts) { + auto height = part->height(); + if (point.y() >= top && point.y() < top + height) { + return part->hasPoint(point.x(), point.y() - top); + } + top += height; + } + return false; +} + +HistoryTextState Item::getState(QPoint point, HistoryStateRequest request) const { + auto top = 0; + for (auto part : _parts) { + auto height = part->height(); + if (point.y() >= top && point.y() < top + height) { + return part->getState(point.x(), point.y() - top, request); + } + top += height; + } + return HistoryTextState(); +} + +TextWithEntities Item::PrepareText(const QString &value, const QString &emptyValue) { + auto result = TextWithEntities { textClean(value) }; + if (result.text.isEmpty()) { + result.text = emptyValue; + if (!emptyValue.isEmpty()) { + result.entities.push_back(EntityInText(EntityInTextItalic, 0, emptyValue.size())); + } + } else { + textParseEntities(result.text, TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands, &result.entities); + } + return result; +} + +Item::~Item() { + for (auto part : _parts) { + part->destroy(); + } +} + +} // namespace AdminLog diff --git a/Telegram/SourceFiles/history/history_admin_log_item.h b/Telegram/SourceFiles/history/history_admin_log_item.h new file mode 100644 index 000000000..d29666ec1 --- /dev/null +++ b/Telegram/SourceFiles/history/history_admin_log_item.h @@ -0,0 +1,68 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include "history/history_admin_log_inner.h" + +namespace AdminLog { + +class Item { +public: + Item(gsl::not_null history, LocalIdManager &idManager, const MTPDchannelAdminLogEvent &event); + + uint64 id() const { + return _id; + } + int top() const { + return _top; + } + void setTop(int top) { + _top = top; + } + int height() const { + return _height; + } + + int resizeGetHeight(int newWidth); + void draw(Painter &p, QRect clip, TextSelection selection, TimeMs ms); + bool hasPoint(QPoint point) const; + HistoryTextState getState(QPoint point, HistoryStateRequest request) const; + + ~Item(); + +private: + gsl::not_null channel() { + return _history->peer->asChannel(); + } + void addPart(HistoryItem *item); + + static TextWithEntities PrepareText(const QString &value, const QString &emptyValue); + + uint64 _id = 0; + gsl::not_null _history; + gsl::not_null _from; + std::vector _parts; + int _top = 0; + int _height = 0; + +}; + +} // namespace AdminLog diff --git a/Telegram/SourceFiles/history/history_admin_log_section.cpp b/Telegram/SourceFiles/history/history_admin_log_section.cpp index a1dbdb59f..dc27e4c52 100644 --- a/Telegram/SourceFiles/history/history_admin_log_section.cpp +++ b/Telegram/SourceFiles/history/history_admin_log_section.cpp @@ -137,7 +137,7 @@ Widget::Widget(QWidget *parent, gsl::not_null controller, g updateAdaptiveLayout(); subscribe(Adaptive::Changed(), [this]() { updateAdaptiveLayout(); }); - _inner = _scroll->setOwnedWidget(object_ptr(this, channel)); + _inner = _scroll->setOwnedWidget(object_ptr(this, channel, [this](int top) { _scroll->scrollToY(top); })); _scroll->move(0, _fixedBar->height()); _scroll->show(); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 387ff0f7e..9a6f25f62 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -543,6 +543,9 @@ void HistoryMessageDate::paint(Painter &p, int y, int w) const { HistoryLayout::ServiceMessagePainter::paintDate(p, _text, _width, y, w); } +HistoryMessageLogEntryOriginal::HistoryMessageLogEntryOriginal() : _text(st::msgMinWidth - st::webPageLeft) { +} + HistoryMediaPtr::HistoryMediaPtr(std::unique_ptr pointer) : _pointer(std::move(pointer)) { if (_pointer) { _pointer->attachToParent(); @@ -656,26 +659,39 @@ void HistoryItem::clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pres Ui::repaintHistoryItem(this); } -void HistoryItem::destroy() { - // All this must be done for all items manually in History::clear(false)! - eraseFromOverview(); +void HistoryItem::addLogEntryOriginal(const QString &label, const TextWithEntities &content) { + Expects(isLogEntry()); + AddComponents(HistoryMessageLogEntryOriginal::Bit()); + auto original = Get(); + original->_label = label; + original->_labelWidth = st::webPageTitleFont->width(label); + original->_text.setMarkedText(st::webPageDescriptionStyle, content); +} - auto wasAtBottom = history()->loadedAtBottom(); - _history->removeNotification(this); - detach(); - if (history()->isChannel()) { - if (history()->peer->isMegagroup() && history()->peer->asChannel()->mgInfo->pinnedMsgId == id) { - history()->peer->asChannel()->mgInfo->pinnedMsgId = 0; +void HistoryItem::destroy() { + if (isLogEntry()) { + t_assert(detached()); + } else { + // All this must be done for all items manually in History::clear(false)! + eraseFromOverview(); + + auto wasAtBottom = history()->loadedAtBottom(); + _history->removeNotification(this); + detach(); + if (history()->isChannel()) { + if (history()->peer->isMegagroup() && history()->peer->asChannel()->mgInfo->pinnedMsgId == id) { + history()->peer->asChannel()->mgInfo->pinnedMsgId = 0; + } + } + if (history()->lastMsg == this) { + history()->fixLastMessage(wasAtBottom); + } + if (history()->lastKeyboardId == id) { + history()->clearLastKeyboard(); + } + if ((!out() || isPost()) && unread() && history()->unreadCount() > 0) { + history()->setUnreadCount(history()->unreadCount() - 1); } - } - if (history()->lastMsg == this) { - history()->fixLastMessage(wasAtBottom); - } - if (history()->lastKeyboardId == id) { - history()->clearLastKeyboard(); - } - if ((!out() || isPost()) && unread() && history()->unreadCount() > 0) { - history()->setUnreadCount(history()->unreadCount() - 1); } Global::RefPendingRepaintItems().remove(this); delete this; diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 85fd055b9..5d7ec47df 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -379,8 +379,8 @@ private: }; -// any HistoryItem can have this Interface for -// displaying the day mark above the message +// Any HistoryItem can have this Component for +// displaying the day mark above the message. struct HistoryMessageDate : public RuntimeComponent { void init(const QDateTime &date); @@ -391,8 +391,8 @@ struct HistoryMessageDate : public RuntimeComponent { int _width = 0; }; -// any HistoryItem can have this Interface for -// displaying the unread messages bar above the message +// Any HistoryItem can have this Component for +// displaying the unread messages bar above the message. struct HistoryMessageUnreadBar : public RuntimeComponent { void init(int count); @@ -404,16 +404,29 @@ struct HistoryMessageUnreadBar : public RuntimeComponent { + HistoryMessageLogEntryOriginal(); + + void paint(Painter &p, int y, int w) const; + HistoryTextState getState(int x, int y, HistoryStateRequest request) const; + + QString _label; + int _labelWidth = 0; + Text _text; + +}; + // HistoryMedia has a special owning smart pointer // which regs/unregs this media to the holding HistoryItem class HistoryMedia; @@ -504,6 +517,11 @@ public: return (bot && bot->botInfo) ? bot : nullptr; }; + bool isLogEntry() const { + return (id > ServerMaxMsgId); + } + void addLogEntryOriginal(const QString &label, const TextWithEntities &content); + History *history() const { return _history; } diff --git a/Telegram/SourceFiles/history/history_message.cpp b/Telegram/SourceFiles/history/history_message.cpp index 8503cb8a5..302477b32 100644 --- a/Telegram/SourceFiles/history/history_message.cpp +++ b/Telegram/SourceFiles/history/history_message.cpp @@ -2208,15 +2208,15 @@ HistoryService::PreparedText HistoryService::preparePaymentSentText() { return result; } -HistoryService::HistoryService(History *history, const MTPDmessageService &msg) : - HistoryItem(history, msg.vid.v, mtpCastFlags(msg.vflags.v), ::date(msg.vdate), msg.has_from_id() ? msg.vfrom_id.v : 0) { - createFromMtp(msg); - setMessageByAction(msg.vaction); +HistoryService::HistoryService(History *history, const MTPDmessageService &message) : + HistoryItem(history, message.vid.v, mtpCastFlags(message.vflags.v), ::date(message.vdate), message.has_from_id() ? message.vfrom_id.v : 0) { + createFromMtp(message); + setMessageByAction(message.vaction); } -HistoryService::HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags, int32 from) : +HistoryService::HistoryService(History *history, MsgId msgId, QDateTime date, const PreparedText &message, MTPDmessage::Flags flags, int32 from) : HistoryItem(history, msgId, flags, date, from) { - setServiceText({ msg }); + setServiceText(message); } void HistoryService::initDimensions() { @@ -2497,18 +2497,19 @@ HistoryService::~HistoryService() { _media.reset(); } -HistoryJoined::HistoryJoined(History *history, const QDateTime &inviteDate, UserData *inviter, MTPDmessage::Flags flags) - : HistoryService(history, clientMsgId(), inviteDate, QString(), flags) { - auto prepared = PreparedText {}; - prepared.text = ([history, inviter, &prepared]() { - if (inviter->id == AuthSession::CurrentUserPeerId()) { - return lang(history->isMegagroup() ? lng_action_you_joined_group : lng_action_you_joined); - } - prepared.links.push_back(peerOpenClickHandler(inviter)); - if (history->isMegagroup()) { - return lng_action_add_you_group(lt_from, textcmdLink(1, inviter->name)); - } - return lng_action_add_you(lt_from, textcmdLink(1, inviter->name)); - })(); - setServiceText(prepared); +HistoryJoined::HistoryJoined(gsl::not_null history, const QDateTime &inviteDate, gsl::not_null inviter, MTPDmessage::Flags flags) + : HistoryService(history, clientMsgId(), inviteDate, GenerateText(history, inviter), flags) { +} + +HistoryJoined::PreparedText HistoryJoined::GenerateText(gsl::not_null history, gsl::not_null inviter) { + if (inviter->id == AuthSession::CurrentUserPeerId()) { + return { lang(history->isMegagroup() ? lng_action_you_joined_group : lng_action_you_joined) }; + } + auto result = PreparedText {}; + result.links.push_back(peerOpenClickHandler(inviter)); + if (history->isMegagroup()) { + result.text = lng_action_add_you_group(lt_from, textcmdLink(1, inviter->name)); + } + result.text = lng_action_add_you(lt_from, textcmdLink(1, inviter->name)); + return result; } diff --git a/Telegram/SourceFiles/history/history_message.h b/Telegram/SourceFiles/history/history_message.h index 76a26936c..e69879418 100644 --- a/Telegram/SourceFiles/history/history_message.h +++ b/Telegram/SourceFiles/history/history_message.h @@ -273,11 +273,16 @@ class ServiceMessagePainter; class HistoryService : public HistoryItem, private HistoryItemInstantiated { public: - static HistoryService *create(History *history, const MTPDmessageService &msg) { - return _create(history, msg); + struct PreparedText { + QString text; + QList links; + }; + + static HistoryService *create(History *history, const MTPDmessageService &message) { + return _create(history, message); } - static HistoryService *create(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags = 0, int32 from = 0) { - return _create(history, msgId, date, msg, flags, from); + static HistoryService *create(History *history, MsgId msgId, QDateTime date, const PreparedText &message, MTPDmessage::Flags flags = 0, int32 from = 0) { + return _create(history, msgId, date, message, flags, from); } bool updateDependencyItem() override; @@ -333,17 +338,13 @@ public: protected: friend class HistoryLayout::ServiceMessagePainter; - HistoryService(History *history, const MTPDmessageService &msg); - HistoryService(History *history, MsgId msgId, QDateTime date, const QString &msg, MTPDmessage::Flags flags = 0, int32 from = 0); + HistoryService(History *history, const MTPDmessageService &message); + HistoryService(History *history, MsgId msgId, QDateTime date, const PreparedText &message, MTPDmessage::Flags flags = 0, int32 from = 0); friend class HistoryItemInstantiated; void initDimensions() override; int resizeGetHeight_(int width) override; - struct PreparedText { - QString text; - QList links; - }; void setServiceText(const PreparedText &prepared); QString fromLinkText() const { @@ -384,13 +385,16 @@ private: class HistoryJoined : public HistoryService, private HistoryItemInstantiated { public: - static HistoryJoined *create(History *history, const QDateTime &date, UserData *from, MTPDmessage::Flags flags) { - return _create(history, date, from, flags); + static HistoryJoined *create(gsl::not_null history, const QDateTime &inviteDate, gsl::not_null inviter, MTPDmessage::Flags flags) { + return _create(history, inviteDate, inviter, flags); } protected: - HistoryJoined(History *history, const QDateTime &date, UserData *from, MTPDmessage::Flags flags); + HistoryJoined(gsl::not_null history, const QDateTime &inviteDate, gsl::not_null inviter, MTPDmessage::Flags flags); using HistoryItemInstantiated::_create; friend class HistoryItemInstantiated; +private: + static PreparedText GenerateText(gsl::not_null history, gsl::not_null inviter); + }; diff --git a/Telegram/SourceFiles/profile/profile_block_info.cpp b/Telegram/SourceFiles/profile/profile_block_info.cpp index 65272169a..1994c68ec 100644 --- a/Telegram/SourceFiles/profile/profile_block_info.cpp +++ b/Telegram/SourceFiles/profile/profile_block_info.cpp @@ -149,14 +149,13 @@ void InfoWidget::refreshAbout() { }; _about.destroy(); - auto aboutText = textClean(getAboutText()); - if (!aboutText.isEmpty()) { + auto aboutText = TextWithEntities { textClean(getAboutText()) }; + if (!aboutText.text.isEmpty()) { _about.create(this, st::profileBlockTextPart); _about->show(); - EntitiesInText aboutEntities; - textParseEntities(aboutText, TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands, &aboutEntities); - _about->setMarkedText({ aboutText, aboutEntities }); + textParseEntities(aboutText.text, TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands, &aboutText.entities); + _about->setMarkedText(aboutText); _about->setSelectable(true); _about->setClickHandlerHook([this](const ClickHandlerPtr &handler, Qt::MouseButton button) { BotCommandClickHandler::setPeerForCommand(peer()); diff --git a/Telegram/gyp/telegram_sources.txt b/Telegram/gyp/telegram_sources.txt index f042062ff..d73156209 100644 --- a/Telegram/gyp/telegram_sources.txt +++ b/Telegram/gyp/telegram_sources.txt @@ -143,6 +143,8 @@ <(src_loc)/dialogs/dialogs_row.h <(src_loc)/history/history_admin_log_inner.cpp <(src_loc)/history/history_admin_log_inner.h +<(src_loc)/history/history_admin_log_item.cpp +<(src_loc)/history/history_admin_log_item.h <(src_loc)/history/history_admin_log_section.cpp <(src_loc)/history/history_admin_log_section.h <(src_loc)/history/history_common.h