feat: store text deleted messages in db

This commit is contained in:
AlexeyZavar 2024-09-23 02:01:41 +03:00
parent 7caf421074
commit 625f9ef816
20 changed files with 14341 additions and 7849 deletions

View file

@ -136,12 +136,12 @@ set(ayugram_files
ayu/ui/context_menu/context_menu.h
ayu/ui/context_menu/menu_item_subtext.cpp
ayu/ui/context_menu/menu_item_subtext.h
ayu/ui/sections/edited/edited_log_inner.cpp
ayu/ui/sections/edited/edited_log_inner.h
ayu/ui/sections/edited/edited_log_item.cpp
ayu/ui/sections/edited/edited_log_item.h
ayu/ui/sections/edited/edited_log_section.cpp
ayu/ui/sections/edited/edited_log_section.h
ayu/ui/message_history/history_inner.cpp
ayu/ui/message_history/history_inner.h
ayu/ui/message_history/history_item.cpp
ayu/ui/message_history/history_item.h
ayu/ui/message_history/history_section.cpp
ayu/ui/message_history/history_section.h
ayu/ui/boxes/edit_deleted_mark.cpp
ayu/ui/boxes/edit_deleted_mark.h
ayu/ui/boxes/edit_edited_mark.cpp

View file

@ -16,7 +16,7 @@
using namespace sqlite_orm;
auto storage = make_storage(
"./tdata/ayudata.db",
make_table(
make_table<DeletedMessage>(
"DeletedMessage",
make_column("fakeId", &DeletedMessage::fakeId, primary_key().autoincrement()),
make_column("userId", &DeletedMessage::userId),
@ -52,7 +52,7 @@ auto storage = make_storage(
make_column("documentAttributesSerialized", &DeletedMessage::documentAttributesSerialized),
make_column("mimeType", &DeletedMessage::mimeType)
),
make_table(
make_table<EditedMessage>(
"EditedMessage",
make_column("fakeId", &EditedMessage::fakeId, primary_key().autoincrement()),
make_column("userId", &EditedMessage::userId),
@ -88,7 +88,7 @@ auto storage = make_storage(
make_column("documentAttributesSerialized", &EditedMessage::documentAttributesSerialized),
make_column("mimeType", &EditedMessage::mimeType)
),
make_table(
make_table<DeletedDialog>(
"DeletedDialog",
make_column("fakeId", &DeletedDialog::fakeId, primary_key().autoincrement()),
make_column("userId", &DeletedDialog::userId),
@ -100,7 +100,7 @@ auto storage = make_storage(
make_column("flags", &DeletedDialog::flags),
make_column("entityCreateDate", &DeletedDialog::entityCreateDate)
),
make_table(
make_table<RegexFilter>(
"RegexFilter",
make_column("id", &RegexFilter::id),
make_column("text", &RegexFilter::text),
@ -108,13 +108,13 @@ auto storage = make_storage(
make_column("caseInsensitive", &RegexFilter::caseInsensitive),
make_column("dialogId", &RegexFilter::dialogId)
),
make_table(
make_table<RegexFilterGlobalExclusion>(
"RegexFilterGlobalExclusion",
make_column("fakeId", &RegexFilterGlobalExclusion::fakeId, primary_key().autoincrement()),
make_column("dialogId", &RegexFilterGlobalExclusion::dialogId),
make_column("filterId", &RegexFilterGlobalExclusion::filterId)
),
make_table(
make_table<SpyMessageRead>(
"SpyMessageRead",
make_column("fakeId", &SpyMessageRead::fakeId, primary_key().autoincrement()),
make_column("userId", &SpyMessageRead::userId),
@ -122,7 +122,7 @@ auto storage = make_storage(
make_column("messageId", &SpyMessageRead::messageId),
make_column("entityCreateDate", &SpyMessageRead::entityCreateDate)
),
make_table(
make_table<SpyMessageContentsRead>(
"SpyMessageContentsRead",
make_column("fakeId", &SpyMessageContentsRead::fakeId, primary_key().autoincrement()),
make_column("userId", &SpyMessageContentsRead::userId),
@ -135,7 +135,7 @@ auto storage = make_storage(
namespace AyuDatabase {
void moveCurrentDatabase() {
auto time = base::unixtime::now();
const auto time = base::unixtime::now();
if (QFile::exists("./tdata/ayudata.db")) {
QFile::rename("./tdata/ayudata.db", QString("./tdata/ayudata_%1.db").arg(time));
@ -190,18 +190,22 @@ void addEditedMessage(const EditedMessage &message) {
storage.begin_transaction();
storage.insert(message);
storage.commit();
} catch (std::exception& ex) {
} catch (std::exception &ex) {
LOG(("Failed to save edited message for some reason: %1").arg(ex.what()));
}
}
std::vector<EditedMessage> getEditedMessages(ID userId, ID dialogId, ID messageId) {
std::vector<EditedMessage> getEditedMessages(ID userId, ID dialogId, ID messageId, ID minId, ID maxId, int totalLimit) {
return storage.get_all<EditedMessage>(
where(
c(&EditedMessage::userId) == userId and
c(&EditedMessage::dialogId) == dialogId and
c(&EditedMessage::messageId) == messageId
)
column<EditedMessage>(&EditedMessage::userId) == userId and
column<EditedMessage>(&EditedMessage::dialogId) == dialogId and
column<EditedMessage>(&EditedMessage::messageId) == messageId and
(column<EditedMessage>(&EditedMessage::fakeId) > minId or minId == 0) and
(column<EditedMessage>(&EditedMessage::fakeId) < maxId or maxId == 0)
),
order_by(column<EditedMessage>(&EditedMessage::fakeId)).desc(),
limit(totalLimit)
);
}
@ -209,15 +213,50 @@ bool hasRevisions(ID userId, ID dialogId, ID messageId) {
try {
return storage.count<EditedMessage>(
where(
c(&EditedMessage::userId) == userId and
c(&EditedMessage::dialogId) == dialogId and
c(&EditedMessage::messageId) == messageId
column<EditedMessage>(&EditedMessage::userId) == userId and
column<EditedMessage>(&EditedMessage::dialogId) == dialogId and
column<EditedMessage>(&EditedMessage::messageId) == messageId
)
) > 0;
} catch (std::exception& ex) {
} catch (std::exception &ex) {
LOG(("Failed to check if message has revisions: %1").arg(ex.what()));
return false;
}
}
void addDeletedMessage(const DeletedMessage &message) {
try {
storage.begin_transaction();
storage.insert(message);
storage.commit();
} catch (std::exception &ex) {
LOG(("Failed to save edited message for some reason: %1").arg(ex.what()));
}
}
std::vector<DeletedMessage> getDeletedMessages(ID dialogId, ID minId, ID maxId, int totalLimit) {
return storage.get_all<DeletedMessage>(
where(
column<DeletedMessage>(&DeletedMessage::dialogId) == dialogId and
(column<DeletedMessage>(&DeletedMessage::messageId) > minId or minId == 0) and
(column<DeletedMessage>(&DeletedMessage::messageId) < maxId or maxId == 0)
),
order_by(column<DeletedMessage>(&DeletedMessage::messageId)).desc(),
limit(totalLimit)
);
}
bool hasDeletedMessages(ID dialogId) {
try {
return storage.count<DeletedMessage>(
where(
column<DeletedMessage>(&DeletedMessage::dialogId) == dialogId
)
) > 0;
} catch (std::exception &ex) {
LOG(("Failed to check if dialog has deleted message: %1").arg(ex.what()));
return false;
}
}
}

View file

@ -13,7 +13,11 @@ namespace AyuDatabase {
void initialize();
void addEditedMessage(const EditedMessage &message);
std::vector<EditedMessage> getEditedMessages(ID userId, ID dialogId, ID messageId);
std::vector<EditedMessage> getEditedMessages(ID userId, ID dialogId, ID messageId, ID minId, ID maxId, int totalLimit);
bool hasRevisions(ID userId, ID dialogId, ID messageId);
void addDeletedMessage(const DeletedMessage &message);
std::vector<DeletedMessage> getDeletedMessages(ID dialogId, ID minId, ID maxId, int totalLimit);
bool hasDeletedMessages(ID dialogId);
}

View file

@ -10,7 +10,6 @@
#define ID long long
template<typename TableName>
class AyuMessageBase
{
public:
@ -49,9 +48,13 @@ public:
std::string mimeType;
};
using DeletedMessage = AyuMessageBase<struct DeletedMessageTag>;
class DeletedMessage : public AyuMessageBase
{
};
using EditedMessage = AyuMessageBase<struct EditedMessageTag>;
class EditedMessage : public AyuMessageBase
{
};
class DeletedDialog
{

View file

@ -24,7 +24,17 @@
namespace AyuMessages {
void map(HistoryMessageEdition &edition, not_null<HistoryItem*> item, EditedMessage &message) {
template<typename DerivedMessage>
std::vector<AyuMessageBase> convertToBase(const std::vector<DerivedMessage> &messages) {
std::vector<AyuMessageBase> based;
based.reserve(messages.size());
for (const auto &msg : messages) {
based.push_back(static_cast<AyuMessageBase>(msg));
}
return based;
}
void map(not_null<HistoryItem*> item, AyuMessageBase &message) {
message.userId = item->history()->owner().session().userId().bare;
message.dialogId = getDialogIdFromPeer(item->history()->peer);
message.groupedId = item->groupId().value;
@ -62,17 +72,21 @@ void map(HistoryMessageEdition &edition, not_null<HistoryItem*> item, EditedMess
void addEditedMessage(HistoryMessageEdition &edition, not_null<HistoryItem*> item) {
EditedMessage message;
map(edition, item, message);
map(item, message);
if (message.text.empty()) {
return;
}
AyuDatabase::addEditedMessage(message);
}
std::vector<EditedMessage> getEditedMessages(not_null<HistoryItem*> item) {
std::vector<AyuMessageBase> getEditedMessages(not_null<HistoryItem*> item, ID minId, ID maxId, int totalLimit) {
auto userId = item->history()->owner().session().userId().bare;
auto dialogId = getDialogIdFromPeer(item->history()->peer);
auto msgId = item->id.bare;
return AyuDatabase::getEditedMessages(userId, dialogId, msgId);
return convertToBase(AyuDatabase::getEditedMessages(userId, dialogId, msgId, minId, maxId, totalLimit));
}
bool hasRevisions(not_null<HistoryItem*> item) {
@ -83,4 +97,23 @@ bool hasRevisions(not_null<HistoryItem*> item) {
return AyuDatabase::hasRevisions(userId, dialogId, msgId);
}
void addDeletedMessage(not_null<HistoryItem*> item) {
DeletedMessage message;
map(item, message);
if (message.text.empty()) {
return;
}
AyuDatabase::addDeletedMessage(message);
}
std::vector<AyuMessageBase> getDeletedMessages(not_null<PeerData*> peer, ID minId, ID maxId, int totalLimit) {
return convertToBase(AyuDatabase::getDeletedMessages(getDialogIdFromPeer(peer), minId, maxId, totalLimit));
}
bool hasDeletedMessages(not_null<PeerData*> peer) {
return AyuDatabase::hasDeletedMessages(getDialogIdFromPeer(peer));
}
}

View file

@ -13,7 +13,11 @@
namespace AyuMessages {
void addEditedMessage(HistoryMessageEdition &edition, not_null<HistoryItem*> item);
std::vector<EditedMessage> getEditedMessages(not_null<HistoryItem*> item);
std::vector<AyuMessageBase> getEditedMessages(not_null<HistoryItem*> item, ID minId, ID maxId, int totalLimit);
bool hasRevisions(not_null<HistoryItem*> item);
void addDeletedMessage(not_null<HistoryItem*> item);
std::vector<AyuMessageBase> getDeletedMessages(not_null<PeerData*> peer, ID minId, ID maxId, int totalLimit);
bool hasDeletedMessages(not_null<PeerData*> peer);
}

File diff suppressed because it is too large Load diff

View file

@ -146,9 +146,9 @@ extern "C" {
** [sqlite3_libversion_number()], [sqlite3_sourceid()],
** [sqlite_version()] and [sqlite_source_id()].
*/
#define SQLITE_VERSION "3.45.3"
#define SQLITE_VERSION_NUMBER 3045003
#define SQLITE_SOURCE_ID "2024-04-15 13:34:05 8653b758870e6ef0c98d46b3ace27849054af85da891eb121e9aaa537f1e8355"
#define SQLITE_VERSION "3.46.1"
#define SQLITE_VERSION_NUMBER 3046001
#define SQLITE_SOURCE_ID "2024-08-13 09:16:08 c9c2ab54ba1f5f46360f1b4f35d849cd3f080e6fc2b6c60e91b16c63f69a1e33"
/*
** CAPI3REF: Run-Time Library Version Numbers
@ -764,11 +764,11 @@ struct sqlite3_file {
** </ul>
** xLock() upgrades the database file lock. In other words, xLock() moves the
** database file lock in the direction NONE toward EXCLUSIVE. The argument to
** xLock() is always on of SHARED, RESERVED, PENDING, or EXCLUSIVE, never
** xLock() is always one of SHARED, RESERVED, PENDING, or EXCLUSIVE, never
** SQLITE_LOCK_NONE. If the database file lock is already at or above the
** requested lock, then the call to xLock() is a no-op.
** xUnlock() downgrades the database file lock to either SHARED or NONE.
* If the lock is already at or below the requested lock state, then the call
** If the lock is already at or below the requested lock state, then the call
** to xUnlock() is a no-op.
** The xCheckReservedLock() method checks whether any database connection,
** either in this process or in some other process, is holding a RESERVED,
@ -3305,8 +3305,8 @@ SQLITE_API int sqlite3_set_authorizer(
#define SQLITE_RECURSIVE 33 /* NULL NULL */
/*
** CAPI3REF: Tracing And Profiling Functions
** METHOD: sqlite3
** CAPI3REF: Deprecated Tracing And Profiling Functions
** DEPRECATED
**
** These routines are deprecated. Use the [sqlite3_trace_v2()] interface
** instead of the routines described here.
@ -6887,6 +6887,12 @@ SQLITE_API int sqlite3_autovacuum_pages(
** The exceptions defined in this paragraph might change in a future
** release of SQLite.
**
** Whether the update hook is invoked before or after the
** corresponding change is currently unspecified and may differ
** depending on the type of change. Do not rely on the order of the
** hook call with regards to the final result of the operation which
** triggers the hook.
**
** The update hook implementation must not do anything that will modify
** the database connection that invoked the update hook. Any actions
** to modify the database connection must be deferred until after the
@ -8357,7 +8363,7 @@ SQLITE_API int sqlite3_test_control(int op, ...);
** The sqlite3_keyword_count() interface returns the number of distinct
** keywords understood by SQLite.
**
** The sqlite3_keyword_name(N,Z,L) interface finds the N-th keyword and
** The sqlite3_keyword_name(N,Z,L) interface finds the 0-based N-th keyword and
** makes *Z point to that keyword expressed as UTF8 and writes the number
** of bytes in the keyword into *L. The string that *Z points to is not
** zero-terminated. The sqlite3_keyword_name(N,Z,L) routine returns
@ -9936,24 +9942,45 @@ SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
** <li value="2"><p>
** ^(If the sqlite3_vtab_distinct() interface returns 2, that means
** that the query planner does not need the rows returned in any particular
** order, as long as rows with the same values in all "aOrderBy" columns
** are adjacent.)^ ^(Furthermore, only a single row for each particular
** combination of values in the columns identified by the "aOrderBy" field
** needs to be returned.)^ ^It is always ok for two or more rows with the same
** values in all "aOrderBy" columns to be returned, as long as all such rows
** are adjacent. ^The virtual table may, if it chooses, omit extra rows
** that have the same value for all columns identified by "aOrderBy".
** ^However omitting the extra rows is optional.
** order, as long as rows with the same values in all columns identified
** by "aOrderBy" are adjacent.)^ ^(Furthermore, when two or more rows
** contain the same values for all columns identified by "colUsed", all but
** one such row may optionally be omitted from the result.)^
** The virtual table is not required to omit rows that are duplicates
** over the "colUsed" columns, but if the virtual table can do that without
** too much extra effort, it could potentially help the query to run faster.
** This mode is used for a DISTINCT query.
** <li value="3"><p>
** ^(If the sqlite3_vtab_distinct() interface returns 3, that means
** that the query planner needs only distinct rows but it does need the
** rows to be sorted.)^ ^The virtual table implementation is free to omit
** rows that are identical in all aOrderBy columns, if it wants to, but
** it is not required to omit any rows. This mode is used for queries
** ^(If the sqlite3_vtab_distinct() interface returns 3, that means the
** virtual table must return rows in the order defined by "aOrderBy" as
** if the sqlite3_vtab_distinct() interface had returned 0. However if
** two or more rows in the result have the same values for all columns
** identified by "colUsed", then all but one such row may optionally be
** omitted.)^ Like when the return value is 2, the virtual table
** is not required to omit rows that are duplicates over the "colUsed"
** columns, but if the virtual table can do that without
** too much extra effort, it could potentially help the query to run faster.
** This mode is used for queries
** that have both DISTINCT and ORDER BY clauses.
** </ol>
**
** <p>The following table summarizes the conditions under which the
** virtual table is allowed to set the "orderByConsumed" flag based on
** the value returned by sqlite3_vtab_distinct(). This table is a
** restatement of the previous four paragraphs:
**
** <table border=1 cellspacing=0 cellpadding=10 width="90%">
** <tr>
** <td valign="top">sqlite3_vtab_distinct() return value
** <td valign="top">Rows are returned in aOrderBy order
** <td valign="top">Rows with the same value in all aOrderBy columns are adjacent
** <td valign="top">Duplicates over all colUsed columns may be omitted
** <tr><td>0<td>yes<td>yes<td>no
** <tr><td>1<td>no<td>yes<td>no
** <tr><td>2<td>no<td>yes<td>yes
** <tr><td>3<td>yes<td>yes<td>yes
** </table>
**
** ^For the purposes of comparing virtual table output values to see if the
** values are same value for sorting purposes, two NULL values are considered
** to be the same. In other words, the comparison operator is "IS"
@ -11998,6 +12025,30 @@ SQLITE_API int sqlite3changegroup_schema(sqlite3_changegroup*, sqlite3*, const c
*/
SQLITE_API int sqlite3changegroup_add(sqlite3_changegroup*, int nData, void *pData);
/*
** CAPI3REF: Add A Single Change To A Changegroup
** METHOD: sqlite3_changegroup
**
** This function adds the single change currently indicated by the iterator
** passed as the second argument to the changegroup object. The rules for
** adding the change are just as described for [sqlite3changegroup_add()].
**
** If the change is successfully added to the changegroup, SQLITE_OK is
** returned. Otherwise, an SQLite error code is returned.
**
** The iterator must point to a valid entry when this function is called.
** If it does not, SQLITE_ERROR is returned and no change is added to the
** changegroup. Additionally, the iterator must not have been opened with
** the SQLITE_CHANGESETAPPLY_INVERT flag. In this case SQLITE_ERROR is also
** returned.
*/
SQLITE_API int sqlite3changegroup_add_change(
sqlite3_changegroup*,
sqlite3_changeset_iter*
);
/*
** CAPI3REF: Obtain A Composite Changeset From A Changegroup
** METHOD: sqlite3_changegroup
@ -12802,8 +12853,8 @@ struct Fts5PhraseIter {
** EXTENSION API FUNCTIONS
**
** xUserData(pFts):
** Return a copy of the context pointer the extension function was
** registered with.
** Return a copy of the pUserData pointer passed to the xCreateFunction()
** API when the extension function was registered.
**
** xColumnTotalSize(pFts, iCol, pnToken):
** If parameter iCol is less than zero, set output variable *pnToken

File diff suppressed because it is too large Load diff

View file

@ -6,25 +6,24 @@
// Copyright @Radolyn, 2024
#include "ayu/ui/context_menu/context_menu.h"
#include <styles/style_menu_icons.h>
#include "lang_auto.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "ayu/ayu_settings.h"
#include "ayu/ayu_state.h"
#include "../../data/messages_storage.h"
#include "ayu/data/messages_storage.h"
#include "ayu/ui/context_menu/menu_item_subtext.h"
#include "ayu/utils/qt_key_modifiers_extended.h"
#include "history/history_item_components.h"
#include "core/mime_type.h"
#include "styles/style_ayu_icons.h"
#include "styles/style_menu_icons.h"
#include "ui/widgets/popup_menu.h"
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
#include "window/window_peer_menu.h"
#include "ayu/ui/sections/edited/edited_log_section.h"
#include "ayu/ui/message_history/history_section.h"
#include "ayu/utils/telegram_helpers.h"
#include "base/unixtime.h"
#include "history/view/history_view_context_menu.h"
@ -37,13 +36,36 @@ bool needToShowItem(int state) {
return state == 1 || (state == 2 && base::IsExtendedContextMenuModifierPressed());
}
void AddDeletedMessagesActions(PeerData *peerData,
not_null<Window::SessionController*> sessionController,
const Dialogs::EntryState &entryState,
const Window::PeerMenuCallback &addCallback) {
if (!peerData) {
return;
}
const auto has = AyuMessages::hasDeletedMessages(peerData);
if (!has) {
return;
}
addCallback(tr::ayu_ViewDeletedMenuText(tr::now),
[=]
{
sessionController->session().tryResolveWindow()
->showSection(std::make_shared<MessageHistory::SectionMemento>(peerData));
},
&st::menuIconArchive);
}
void AddHistoryAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
if (AyuMessages::hasRevisions(item)) {
menu->addAction(tr::ayu_EditsHistoryMenuText(tr::now),
[=]
{
item->history()->session().tryResolveWindow()
->showSection(std::make_shared<EditedLog::SectionMemento>(item->history()->peer, item));
->showSection(
std::make_shared<MessageHistory::SectionMemento>(item->history()->peer, item));
},
&st::ayuEditsHistoryIcon);
}
@ -120,7 +142,7 @@ void AddMessageDetailsAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item) {
if (!containsSingleCustomEmojiPack && emojiPacks.size() > 1) {
const auto author = emojiPacks.front().id >> 32;
auto sameAuthor = true;
for (const auto &pack : emojiPacks) {
for (const auto &pack : emojiPacks) {
if (pack.id >> 32 != author) {
sameAuthor = false;
break;

View file

@ -8,11 +8,18 @@
#include "data/data_document.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_peer_menu.h"
#include "window/window_session_controller.h"
namespace AyuUi {
bool needToShowItem(int state);
void AddDeletedMessagesActions(PeerData *peerData,
not_null<Window::SessionController*> sessionController,
const Dialogs::EntryState &entryState,
const Window::PeerMenuCallback &addCallback);
void AddHistoryAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddHideMessageAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);
void AddUserMessagesAction(not_null<Ui::PopupMenu*> menu, HistoryItem *item);

View file

@ -4,14 +4,14 @@
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2024
#include "ayu/ui/sections/edited/edited_log_inner.h"
#include "ayu/ui/message_history/history_inner.h"
#include "apiwrap.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "api/api_attached_stickers.h"
#include "ayu/data/messages_storage.h"
#include "ayu/ui/sections/edited/edited_log_section.h"
#include "ayu/ui/message_history/history_section.h"
#include "base/call_delayed.h"
#include "base/unixtime.h"
#include "base/platform/base_platform_info.h"
@ -59,17 +59,14 @@
#include "ui/ui_utility.h"
namespace EditedLog {
namespace MessageHistory {
namespace {
// If we require to support more admins we'll have to rewrite this anyway.
constexpr auto kMaxChannelAdmins = 200;
constexpr auto kScrollDateHideTimeout = 1000;
constexpr auto kEventsFirstPage = 20;
constexpr auto kMessagesFirstPage = 20;
constexpr auto kEventsPerPage = 50;
constexpr auto kMessagesPerPage = 30;
constexpr auto kClearUserpicsAfter = 50;
@ -249,7 +246,7 @@ InnerWidget::InnerWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
not_null<HistoryItem*> item)
HistoryItem *item)
: RpWidget(parent),
_controller(controller),
_peer(peer),
@ -380,9 +377,7 @@ void InnerWidget::visibleTopBottomUpdated(
}
updateVisibleTopItem();
if (_items.size() == 0) {
addEvents(Direction::Up);
}
checkPreloadMore();
if (scrolledUp) {
_scrollDateCheck.call();
} else {
@ -465,17 +460,17 @@ void InnerWidget::repaintScrollDateCallback() {
update(0, updateTop, width(), updateHeight);
}
void InnerWidget::checkPreloadMore() {
if (_visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) {
preloadMore(Direction::Down);
}
if (_visibleTop < PreloadHeightsCount * (_visibleBottom - _visibleTop)) {
preloadMore(Direction::Up);
}
}
void InnerWidget::updateEmptyText() {
auto hasSearch = false;
auto hasFilter = false;
auto text = Ui::Text::Semibold((hasSearch || hasFilter)
? tr::lng_admin_log_no_results_title(tr::now)
: tr::lng_admin_log_no_events_title(tr::now));
auto description = _peer->isMegagroup()
? tr::lng_admin_log_no_events_text(tr::now)
: tr::lng_admin_log_no_events_text_channel(tr::now);
text.text.append(u"\n\n"_q + description);
_emptyText.setMarkedText(st::defaultTextStyle, text);
_emptyText.setMarkedText(st::defaultTextStyle, Ui::Text::Semibold(tr::lng_search_messages_none(tr::now)));
}
QString InnerWidget::tooltipText() const {
@ -613,7 +608,7 @@ void InnerWidget::elementStartInteraction(not_null<const Element*> view) {
void InnerWidget::elementStartPremium(
not_null<const HistoryView::Element*> view,
HistoryView::Element *replacing) {
HistoryView::Element *replacing) {
}
void InnerWidget::elementCancelPremium(not_null<const Element*> view) {
@ -633,17 +628,15 @@ bool InnerWidget::elementHideTopicButton(not_null<const Element*> view) {
}
void InnerWidget::saveState(not_null<SectionMemento*> memento) {
if (!_filterChanged) {
for (auto &item : _items) {
item.clearView();
}
memento->setItems(
base::take(_items),
base::take(_eventIds),
_upLoaded,
_downLoaded);
base::take(_itemsByData);
for (auto &item : _items) {
item.clearView();
}
memento->setItems(
base::take(_items),
base::take(_messageIds),
_upLoaded,
_downLoaded);
base::take(_itemsByData);
_upLoaded = _downLoaded = true; // Don't load or handle anything anymore.
}
@ -653,23 +646,62 @@ void InnerWidget::restoreState(not_null<SectionMemento*> memento) {
item.refreshView(this);
_itemsByData.emplace(item->data(), item.get());
}
_eventIds = memento->takeEventIds();
_messageIds = memento->takeMessageIds();
_upLoaded = memento->upLoaded();
_downLoaded = memento->downLoaded();
_filterChanged = false;
updateSize();
}
void InnerWidget::addEvents(Direction direction) {
auto messages = AyuMessages::getEditedMessages(_item);
if (messages.empty()) {
void InnerWidget::preloadMore(Direction direction) {
auto &loadedFlag = (direction == Direction::Up) ? _upLoaded : _downLoaded;
if (loadedFlag) {
return;
}
const auto size = messages.size();
auto &container = _items;
auto maxId = (direction == Direction::Up) ? _minId : 0;
auto minId = (direction == Direction::Up) ? 0 : _maxId;
auto perPage = _items.empty() ? kMessagesFirstPage : kMessagesPerPage;
std::vector<AyuMessageBase> messages;
if (_item) {
// viewing edited history
messages = AyuMessages::getEditedMessages(_item, minId, maxId, perPage);
} else {
// viewing deleted messages
messages = AyuMessages::getDeletedMessages(_peer, minId, maxId, perPage);
}
crl::on_main([=]
{
addMessages(direction, messages);
});
}
void InnerWidget::addMessages(Direction direction, const std::vector<AyuMessageBase> &messages) {
auto up = (direction == Direction::Up);
if (messages.empty()) {
(up ? _upLoaded : _downLoaded) = true;
update();
return;
}
// When loading items up we just add them to the back of the _items vector.
// When loading items down we add them to a new vector and copy _items after them.
auto newItemsForDownDirection = std::vector<OwnedItem>();
auto oldItemsCount = _items.size();
auto &addToItems = (direction == Direction::Up)
? _items
: newItemsForDownDirection;
addToItems.reserve(oldItemsCount + messages.size() * 2);
for (const auto &message : messages) {
const auto id = _item
? message.fakeId // viewing edited history
: message.messageId; // viewing deleted messages
if (_messageIds.find(id) != _messageIds.end()) {
return;
}
auto count = 0;
const auto addOne = [&](
OwnedItem item,
TimeId sentDate,
@ -678,19 +710,50 @@ void InnerWidget::addEvents(Direction direction) {
if (sentDate) {
_itemDates.emplace(item->data(), sentDate);
}
_messageIds.emplace(id);
_itemsByData.emplace(item->data(), item.get());
container.push_back(std::move(item));
addToItems.push_back(std::move(item));
++count;
};
GenerateItems(
this,
_history,
message,
addOne);
if (count > 1) {
// Reverse the inner order of the added messages, because we load messages
// from bottom to top but inside one message they go from top to bottom.
auto full = addToItems.size();
auto from = full - count;
for (auto i = 0, toReverse = count / 2; i != toReverse; ++i) {
std::swap(addToItems[from + i], addToItems[full - i - 1]);
}
}
}
auto newItemsCount = _items.size() + ((direction == Direction::Up) ? 0 : newItemsForDownDirection.size());
if (newItemsCount != oldItemsCount) {
if (direction == Direction::Down) {
for (auto &item : _items) {
newItemsForDownDirection.push_back(std::move(item));
}
_items = std::move(newItemsForDownDirection);
}
updateMinMaxIds();
itemsAdded(direction, newItemsCount - oldItemsCount);
}
itemsAdded(direction, size);
update();
repaint();
}
void InnerWidget::updateMinMaxIds() {
if (_messageIds.empty()) {
_maxId = _minId = 0;
} else {
_maxId = *_messageIds.rbegin();
_minId = *_messageIds.begin();
if (_minId == 1) {
_upLoaded = true;
}
}
}
void InnerWidget::itemsAdded(Direction direction, int addedCount) {
@ -720,6 +783,7 @@ void InnerWidget::updateSize() {
TWidget::resizeToWidth(width());
restoreScrollPosition();
updateVisibleTopItem();
checkPreloadMore();
}
int InnerWidget::resizeGetHeight(int newWidth) {
@ -1636,4 +1700,4 @@ QPoint InnerWidget::mapPointToItem(QPoint point, const Element *view) const {
InnerWidget::~InnerWidget() = default;
} // namespace EditedLog
} // namespace MessageHistory

View file

@ -6,8 +6,8 @@
// Copyright @Radolyn, 2024
#pragma once
#include "ayu/ui/sections/edited/edited_log_item.h"
#include "ayu/ui/sections/edited/edited_log_section.h"
#include "ayu/ui/message_history/history_item.h"
#include "ayu/ui/message_history/history_section.h"
#include "base/timer.h"
#include "history/view/history_view_element.h"
#include "menu/menu_antispam_validator.h"
@ -40,7 +40,7 @@ namespace Window {
class SessionController;
} // namespace Window
namespace EditedLog {
namespace MessageHistory {
class SectionMemento;
@ -52,7 +52,7 @@ public:
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
not_null<HistoryItem*> item);
HistoryItem *item);
[[nodiscard]] Main::Session &session() const;
@ -62,7 +62,7 @@ public:
[[nodiscard]] rpl::producer<int> scrollToSignal() const;
[[nodiscard]] not_null<PeerData*> channel() const {
[[nodiscard]] not_null<PeerData*> peer() const {
return _peer;
}
@ -207,15 +207,18 @@ private:
TextForMimeData getSelectedText() const;
void updateVisibleTopItem();
void preloadMore(Direction direction);
void itemsAdded(Direction direction, int addedCount);
void updateSize();
void updateMinMaxIds();
void updateEmptyText();
void paintEmpty(Painter &p, not_null<const Ui::ChatStyle*> st);
void addEvents(Direction direction);
void addMessages(Direction direction, const std::vector<AyuMessageBase> &messages);
Element *viewForItem(const HistoryItem *item);
void toggleScrollDateShown();
void repaintScrollDateCallback();
void checkPreloadMore();
bool displayScrollDate() const;
void scrollDateHide();
void scrollDateCheck();
@ -247,7 +250,7 @@ private:
const not_null<Window::SessionController*> _controller;
const not_null<PeerData*> _peer;
const not_null<HistoryItem*> _item;
HistoryItem *_item;
const not_null<History*> _history;
MTP::Sender _api;
@ -255,7 +258,7 @@ private:
std::shared_ptr<Ui::ChatTheme> _theme;
std::vector<OwnedItem> _items;
std::set<uint64> _eventIds;
std::set<uint64> _messageIds;
std::map<not_null<const HistoryItem*>, not_null<Element*>> _itemsByData;
base::flat_map<not_null<const HistoryItem*>, TimeId> _itemDates;
base::flat_set<FullMsgId> _animatedStickersPlayed;
@ -279,10 +282,13 @@ private:
Element *_scrollDateLastItem = nullptr;
int _scrollDateLastItemTop = 0;
// Up - max, Down - min.
uint64 _maxId = 0;
uint64 _minId = 0;
// Don't load anything until the memento was read.
bool _upLoaded = true;
bool _downLoaded = true;
bool _filterChanged = false;
Ui::Text::String _emptyText;
MouseAction _mouseAction = MouseAction::None;
@ -308,4 +314,4 @@ private:
};
} // namespace EditedLog
} // namespace MessageHistory

View file

@ -0,0 +1,116 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2024
#include "ayu/ui/message_history/history_item.h"
#include "ayu/data/entities.h"
#include "api/api_chat_participants.h"
#include "api/api_text_entities.h"
#include "ayu/ui/message_history/history_inner.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/data_channel.h"
#include "data/data_file_origin.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/view/history_view_element.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/basic_click_handlers.h"
#include "ui/text/text_utilities.h"
namespace MessageHistory {
OwnedItem::OwnedItem(std::nullptr_t) {
}
OwnedItem::OwnedItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<HistoryItem*> data)
: _data(data), _view(_data->createView(delegate)) {
}
OwnedItem::OwnedItem(OwnedItem &&other)
: _data(base::take(other._data)), _view(base::take(other._view)) {
}
OwnedItem &OwnedItem::operator=(OwnedItem &&other) {
_data = base::take(other._data);
_view = base::take(other._view);
return *this;
}
OwnedItem::~OwnedItem() {
clearView();
if (_data) {
_data->destroy();
}
}
void OwnedItem::refreshView(
not_null<HistoryView::ElementDelegate*> delegate) {
_view = _data->createView(delegate);
}
void OwnedItem::clearView() {
_view = nullptr;
}
void GenerateItems(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
AyuMessageBase message,
Fn<void(OwnedItem item, TimeId sentDate, MsgId)> callback) {
PeerData *from = history->owner().userLoaded(message.fromId);
if (!from) {
from = history->owner().channelLoaded(message.fromId);
}
if (!from) {
from = reinterpret_cast<PeerData*>(history->owner().chatLoaded(message.fromId));
}
if (!from) {
return;
}
const auto date = message.entityCreateDate;
const auto addPart = [&](
not_null<HistoryItem*> item,
TimeId sentDate = 0,
MsgId realId = MsgId())
{
return callback(OwnedItem(delegate, item), sentDate, realId);
};
const auto fromName = from->name();
const auto fromLink = from->createOpenLink();
const auto fromLinkText = Ui::Text::Link(fromName, QString());
const auto makeSimpleTextMessage = [&](TextWithEntities &&text)
{
return history->makeMessage({
.id = history->nextNonHistoryEntryId(),
.flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry,
.from = from->id,
.date = date,
},
std::move(text),
MTP_messageMediaEmpty());
};
const auto addSimpleTextMessage = [&](TextWithEntities &&text)
{
addPart(makeSimpleTextMessage(std::move(text)));
};
const auto text = QString::fromStdString(message.text);
addSimpleTextMessage(Ui::Text::WithEntities(text));
}
} // namespace MessageHistory

View file

@ -15,14 +15,14 @@ class ElementDelegate;
class Element;
} // namespace HistoryView
namespace EditedLog {
namespace MessageHistory {
class OwnedItem;
void GenerateItems(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
EditedMessage message,
AyuMessageBase message,
Fn<void(OwnedItem item, TimeId sentDate, MsgId)> callback);
// Smart pointer wrapper for HistoryItem* that destroys the owned item.
@ -60,4 +60,4 @@ private:
};
} // namespace EditedLog
} // namespace MessageHistory

View file

@ -4,10 +4,10 @@
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2024
#include "ayu/ui/sections/edited/edited_log_section.h"
#include "ayu/ui/message_history/history_section.h"
#include "apiwrap.h"
#include "ayu/ui/sections/edited/edited_log_inner.h"
#include "ayu/ui/message_history/history_inner.h"
#include "base/timer.h"
#include "data/data_channel.h"
#include "data/data_session.h"
@ -25,7 +25,7 @@
#include "window/window_session_controller.h"
#include "window/themes/window_theme.h"
namespace EditedLog {
namespace MessageHistory {
class FixedBar final : public TWidget
{
@ -142,7 +142,7 @@ Widget::Widget(
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
not_null<HistoryItem*> item)
HistoryItem *item)
: Window::SectionWidget(parent, controller, rpl::single<PeerData*>(peer)),
_scroll(this, st::historyScroll, false),
_fixedBar(this, controller, peer),
@ -190,7 +190,7 @@ void Widget::updateAdaptiveLayout() {
}
not_null<PeerData*> Widget::channel() const {
return _inner->channel();
return _inner->peer();
}
Dialogs::RowDescriptor Widget::activeChat() const {
@ -321,4 +321,4 @@ QRect Widget::floatPlayerAvailableRect() {
return mapToGlobal(_scroll->geometry());
}
} // namespace EditedLog
} // namespace MessageHistory

View file

@ -8,7 +8,7 @@
#include "window/section_widget.h"
#include "window/section_memento.h"
#include "ayu/ui/sections/edited/edited_log_item.h"
#include "ayu/ui/message_history/history_item.h"
#include "mtproto/sender.h"
// don't reformat includes above
@ -22,7 +22,7 @@ namespace Profile {
class BackButton;
} // namespace Profile
namespace EditedLog {
namespace MessageHistory {
class FixedBar;
class InnerWidget;
@ -35,7 +35,7 @@ public:
QWidget *parent,
not_null<Window::SessionController*> controller,
not_null<PeerData*> peer,
not_null<HistoryItem*> item);
HistoryItem *item);
not_null<PeerData*> channel() const;
Dialogs::RowDescriptor activeChat() const override;
@ -77,7 +77,7 @@ private:
QPointer<InnerWidget> _inner;
object_ptr<FixedBar> _fixedBar;
object_ptr<Ui::PlainShadow> _fixedBarShadow;
not_null<HistoryItem*> _item;
HistoryItem *_item;
};
@ -91,6 +91,11 @@ public:
_item(item) {
}
SectionMemento(not_null<PeerData*> peer)
: _peer(peer),
_item(nullptr) {
}
object_ptr<Window::SectionWidget> createWidget(
QWidget *parent,
not_null<Window::SessionController*> controller,
@ -124,7 +129,7 @@ public:
return std::move(_items);
}
std::set<uint64> takeEventIds() {
std::set<uint64> takeMessageIds() {
return std::move(_eventIds);
}
@ -138,14 +143,12 @@ public:
private:
not_null<PeerData*> _peer;
not_null<HistoryItem*> _item;
HistoryItem *_item;
int _scrollTop = 0;
std::vector<not_null<UserData*>> _admins;
std::vector<not_null<UserData*>> _adminsCanEdit;
std::vector<OwnedItem> _items;
std::set<uint64> _eventIds;
bool _upLoaded = false;
bool _downLoaded = true;
};
} // namespace EditedLog
} // namespace MessageHistory

View file

@ -1,659 +0,0 @@
// This is the source code of AyuGram for Desktop.
//
// We do not and cannot prevent the use of our code,
// but be respectful and credit the original author.
//
// Copyright @Radolyn, 2024
#include "ayu/ui/sections/edited/edited_log_item.h"
#include "ayu/data/entities.h"
#include "api/api_chat_participants.h"
#include "api/api_text_entities.h"
#include "ayu/ui/sections/edited/edited_log_inner.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/click_handler_types.h"
#include "data/data_channel.h"
#include "data/data_file_origin.h"
#include "data/data_forum_topic.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/view/history_view_element.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/basic_click_handlers.h"
#include "ui/text/text_utilities.h"
namespace EditedLog {
namespace {
const auto CollectChanges = [](
auto &phraseMap,
auto plusFlags,
auto minusFlags)
{
auto withPrefix = [&phraseMap](auto flags, QChar prefix)
{
auto result = QString();
for (auto &phrase : phraseMap) {
if (flags & phrase.first) {
result.append('\n' + (prefix + phrase.second(tr::now)));
}
}
return result;
};
const auto kMinus = QChar(0x2212);
return withPrefix(plusFlags & ~minusFlags, '+')
+ withPrefix(minusFlags & ~plusFlags, kMinus);
};
TextWithEntities GenerateAdminChangeText(
not_null<ChannelData*> channel,
const TextWithEntities &user,
ChatAdminRightsInfo newRights,
ChatAdminRightsInfo prevRights) {
using Flag = ChatAdminRight;
using Flags = ChatAdminRights;
auto result = tr::lng_admin_log_promoted(
tr::now,
lt_user,
user,
Ui::Text::WithEntities);
const auto useInviteLinkPhrase = channel->isMegagroup()
&& channel->anyoneCanAddMembers();
const auto invitePhrase = useInviteLinkPhrase
? tr::lng_admin_log_admin_invite_link
: tr::lng_admin_log_admin_invite_users;
const auto callPhrase = channel->isBroadcast()
? tr::lng_admin_log_admin_manage_calls_channel
: tr::lng_admin_log_admin_manage_calls;
static auto phraseMap = std::map<Flags, tr::phrase<>>{
{Flag::ChangeInfo, tr::lng_admin_log_admin_change_info},
{Flag::PostMessages, tr::lng_admin_log_admin_post_messages},
{Flag::EditMessages, tr::lng_admin_log_admin_edit_messages},
{Flag::DeleteMessages, tr::lng_admin_log_admin_delete_messages},
{Flag::BanUsers, tr::lng_admin_log_admin_ban_users},
{Flag::InviteByLinkOrAdd, invitePhrase},
{Flag::ManageTopics, tr::lng_admin_log_admin_manage_topics},
{Flag::PinMessages, tr::lng_admin_log_admin_pin_messages},
{Flag::ManageCall, tr::lng_admin_log_admin_manage_calls},
{Flag::AddAdmins, tr::lng_admin_log_admin_add_admins},
{Flag::Anonymous, tr::lng_admin_log_admin_remain_anonymous},
};
phraseMap[Flag::InviteByLinkOrAdd] = invitePhrase;
phraseMap[Flag::ManageCall] = callPhrase;
if (!channel->isMegagroup()) {
// Don't display "Ban users" changes in channels.
newRights.flags &= ~Flag::BanUsers;
prevRights.flags &= ~Flag::BanUsers;
}
const auto changes = CollectChanges(
phraseMap,
newRights.flags,
prevRights.flags);
if (!changes.isEmpty()) {
result.text.append('\n' + changes);
}
return result;
};
QString GeneratePermissionsChangeText(
ChatRestrictionsInfo newRights,
ChatRestrictionsInfo prevRights) {
using Flag = ChatRestriction;
using Flags = ChatRestrictions;
static auto phraseMap = std::map<Flags, tr::phrase<>>{
{Flag::ViewMessages, tr::lng_admin_log_banned_view_messages},
{Flag::SendOther, tr::lng_admin_log_banned_send_messages},
{Flag::SendPhotos, tr::lng_admin_log_banned_send_photos},
{Flag::SendVideos, tr::lng_admin_log_banned_send_videos},
{Flag::SendMusic, tr::lng_admin_log_banned_send_music},
{Flag::SendFiles, tr::lng_admin_log_banned_send_files},
{
Flag::SendVoiceMessages,
tr::lng_admin_log_banned_send_voice_messages
},
{
Flag::SendVideoMessages,
tr::lng_admin_log_banned_send_video_messages
},
{
Flag::SendStickers
| Flag::SendGifs
| Flag::SendInline
| Flag::SendGames,
tr::lng_admin_log_banned_send_stickers
},
{Flag::EmbedLinks, tr::lng_admin_log_banned_embed_links},
{Flag::SendPolls, tr::lng_admin_log_banned_send_polls},
{Flag::ChangeInfo, tr::lng_admin_log_admin_change_info},
{Flag::AddParticipants, tr::lng_admin_log_admin_invite_users},
{Flag::CreateTopics, tr::lng_admin_log_admin_create_topics},
{Flag::PinMessages, tr::lng_admin_log_admin_pin_messages},
};
return CollectChanges(phraseMap, prevRights.flags, newRights.flags);
}
TextWithEntities GeneratePermissionsChangeText(
PeerId participantId,
const TextWithEntities &user,
ChatRestrictionsInfo newRights,
ChatRestrictionsInfo prevRights) {
using Flag = ChatRestriction;
const auto newFlags = newRights.flags;
const auto newUntil = newRights.until;
const auto prevFlags = prevRights.flags;
const auto indefinitely = ChannelData::IsRestrictedForever(newUntil);
if (newFlags & Flag::ViewMessages) {
return tr::lng_admin_log_banned(
tr::now,
lt_user,
user,
Ui::Text::WithEntities);
} else if (newFlags == 0
&& (prevFlags & Flag::ViewMessages)
&& !peerIsUser(participantId)) {
return tr::lng_admin_log_unbanned(
tr::now,
lt_user,
user,
Ui::Text::WithEntities);
}
const auto untilText = indefinitely
? tr::lng_admin_log_restricted_forever(tr::now)
: tr::lng_admin_log_restricted_until(
tr::now,
lt_date,
langDateTime(base::unixtime::parse(newUntil)));
auto result = tr::lng_admin_log_restricted(
tr::now,
lt_user,
user,
lt_until,
TextWithEntities{untilText},
Ui::Text::WithEntities);
const auto changes = GeneratePermissionsChangeText(newRights, prevRights);
if (!changes.isEmpty()) {
result.text.append('\n' + changes);
}
return result;
}
QString PublicJoinLink() {
return u"(public_join_link)"_q;
}
QString ExtractInviteLink(const MTPExportedChatInvite &data) {
return data.match([&](const MTPDchatInviteExported &data)
{
return qs(data.vlink());
},
[&](const MTPDchatInvitePublicJoinRequests &data)
{
return PublicJoinLink();
});
}
QString ExtractInviteLinkLabel(const MTPExportedChatInvite &data) {
return data.match([&](const MTPDchatInviteExported &data)
{
return qs(data.vtitle().value_or_empty());
},
[&](const MTPDchatInvitePublicJoinRequests &data)
{
return PublicJoinLink();
});
}
QString InternalInviteLinkUrl(const MTPExportedChatInvite &data) {
const auto base64 = ExtractInviteLink(data).toUtf8().toBase64();
return "internal:show_invite_link/?link=" + QString::fromLatin1(base64);
}
QString GenerateInviteLinkText(const MTPExportedChatInvite &data) {
const auto label = ExtractInviteLinkLabel(data);
return label.isEmpty()
? ExtractInviteLink(data).replace(
u"https://"_q,
QString()
).replace(
u"t.me/joinchat/"_q,
QString()
)
: label;
}
TextWithEntities GenerateInviteLinkLink(const MTPExportedChatInvite &data) {
const auto text = GenerateInviteLinkText(data);
return text.endsWith(Ui::kQEllipsis)
? TextWithEntities{.text = text}
: Ui::Text::Link(text, InternalInviteLinkUrl(data));
}
TextWithEntities GenerateInviteLinkChangeText(
const MTPExportedChatInvite &newLink,
const MTPExportedChatInvite &prevLink) {
auto link = TextWithEntities{GenerateInviteLinkText(newLink)};
if (!link.text.endsWith(Ui::kQEllipsis)) {
link.entities.push_back({
EntityType::CustomUrl,
0,
int(link.text.size()),
InternalInviteLinkUrl(newLink)
});
}
auto result = tr::lng_admin_log_edited_invite_link(
tr::now,
lt_link,
link,
Ui::Text::WithEntities);
result.text.append('\n');
const auto label = [](const MTPExportedChatInvite &link)
{
return link.match([](const MTPDchatInviteExported &data)
{
return qs(data.vtitle().value_or_empty());
},
[&](const MTPDchatInvitePublicJoinRequests &data)
{
return PublicJoinLink();
});
};
const auto expireDate = [](const MTPExportedChatInvite &link)
{
return link.match([](const MTPDchatInviteExported &data)
{
return data.vexpire_date().value_or_empty();
},
[&](const MTPDchatInvitePublicJoinRequests &data)
{
return TimeId();
});
};
const auto usageLimit = [](const MTPExportedChatInvite &link)
{
return link.match([](const MTPDchatInviteExported &data)
{
return data.vusage_limit().value_or_empty();
},
[&](const MTPDchatInvitePublicJoinRequests &data)
{
return 0;
});
};
const auto requestApproval = [](const MTPExportedChatInvite &link)
{
return link.match([](const MTPDchatInviteExported &data)
{
return data.is_request_needed();
},
[&](const MTPDchatInvitePublicJoinRequests &data)
{
return true;
});
};
const auto wrapDate = [](TimeId date)
{
return date
? langDateTime(base::unixtime::parse(date))
: tr::lng_group_invite_expire_never(tr::now);
};
const auto wrapUsage = [](int count)
{
return count
? QString::number(count)
: tr::lng_group_invite_usage_any(tr::now);
};
const auto wasLabel = label(prevLink);
const auto nowLabel = label(newLink);
const auto wasExpireDate = expireDate(prevLink);
const auto nowExpireDate = expireDate(newLink);
const auto wasUsageLimit = usageLimit(prevLink);
const auto nowUsageLimit = usageLimit(newLink);
const auto wasRequestApproval = requestApproval(prevLink);
const auto nowRequestApproval = requestApproval(newLink);
if (wasLabel != nowLabel) {
result.text.append('\n').append(
tr::lng_admin_log_invite_link_label(
tr::now,
lt_previous,
wasLabel,
lt_limit,
nowLabel));
}
if (wasExpireDate != nowExpireDate) {
result.text.append('\n').append(
tr::lng_admin_log_invite_link_expire_date(
tr::now,
lt_previous,
wrapDate(wasExpireDate),
lt_limit,
wrapDate(nowExpireDate)));
}
if (wasUsageLimit != nowUsageLimit) {
result.text.append('\n').append(
tr::lng_admin_log_invite_link_usage_limit(
tr::now,
lt_previous,
wrapUsage(wasUsageLimit),
lt_limit,
wrapUsage(nowUsageLimit)));
}
if (wasRequestApproval != nowRequestApproval) {
result.text.append('\n').append(
nowRequestApproval
? tr::lng_admin_log_invite_link_request_needed(tr::now)
: tr::lng_admin_log_invite_link_request_not_needed(tr::now));
}
result.entities.push_front(
EntityInText(EntityType::Italic, 0, result.text.size()));
return result;
};
auto GenerateParticipantString(
not_null<Main::Session*> session,
PeerId participantId) {
// User name in "User name (@username)" format with entities.
const auto peer = session->data().peer(participantId);
auto name = TextWithEntities{peer->name()};
if (const auto user = peer->asUser()) {
const auto data = TextUtilities::MentionNameDataFromFields({
.selfId = session->userId().bare,
.userId = peerToUser(user->id).bare,
.accessHash = user->accessHash(),
});
name.entities.push_back({
EntityType::MentionName,
0,
int(name.text.size()),
data,
});
}
const auto username = peer->username();
if (username.isEmpty()) {
return name;
}
auto mention = TextWithEntities{'@' + username};
mention.entities.push_back({
EntityType::Mention,
0,
int(mention.text.size())
});
return tr::lng_admin_log_user_with_username(
tr::now,
lt_name,
name,
lt_mention,
mention,
Ui::Text::WithEntities);
}
auto GenerateParticipantChangeText(
not_null<ChannelData*> channel,
const Api::ChatParticipant &participant,
std::optional<Api::ChatParticipant> oldParticipant = std::nullopt) {
using Type = Api::ChatParticipant::Type;
const auto oldRights = oldParticipant
? oldParticipant->rights()
: ChatAdminRightsInfo();
const auto oldRestrictions = oldParticipant
? oldParticipant->restrictions()
: ChatRestrictionsInfo();
const auto generateOther = [&](PeerId participantId)
{
auto user = GenerateParticipantString(
&channel->session(),
participantId);
if (oldParticipant && oldParticipant->type() == Type::Admin) {
return GenerateAdminChangeText(
channel,
user,
ChatAdminRightsInfo(),
oldRights);
} else if (oldParticipant && oldParticipant->type() == Type::Banned) {
return GeneratePermissionsChangeText(
participantId,
user,
ChatRestrictionsInfo(),
oldRestrictions);
} else if (oldParticipant
&& oldParticipant->type() == Type::Restricted
&& (participant.type() == Type::Member
|| participant.type() == Type::Left)) {
return GeneratePermissionsChangeText(
participantId,
user,
ChatRestrictionsInfo(),
oldRestrictions);
}
return tr::lng_admin_log_invited(
tr::now,
lt_user,
user,
Ui::Text::WithEntities);
};
auto result = [&]
{
const auto &peerId = participant.id();
switch (participant.type()) {
case Api::ChatParticipant::Type::Creator: {
// No valid string here :(
const auto user = GenerateParticipantString(
&channel->session(),
peerId);
if (peerId == channel->session().userPeerId()) {
return GenerateAdminChangeText(
channel,
user,
participant.rights(),
oldRights);
}
return tr::lng_admin_log_transferred(
tr::now,
lt_user,
user,
Ui::Text::WithEntities);
}
case Api::ChatParticipant::Type::Admin: {
const auto user = GenerateParticipantString(
&channel->session(),
peerId);
return GenerateAdminChangeText(
channel,
user,
participant.rights(),
oldRights);
}
case Api::ChatParticipant::Type::Restricted:
case Api::ChatParticipant::Type::Banned: {
const auto user = GenerateParticipantString(
&channel->session(),
peerId);
return GeneratePermissionsChangeText(
peerId,
user,
participant.restrictions(),
oldRestrictions);
}
case Api::ChatParticipant::Type::Left:
case Api::ChatParticipant::Type::Member: return generateOther(peerId);
};
Unexpected("Participant type in GenerateParticipantChangeText.");
}();
result.entities.push_front(
EntityInText(EntityType::Italic, 0, result.text.size()));
return result;
}
TextWithEntities GenerateParticipantChangeText(
not_null<ChannelData*> channel,
const MTPChannelParticipant &participant,
std::optional<MTPChannelParticipant> oldParticipant = std::nullopt) {
return GenerateParticipantChangeText(
channel,
Api::ChatParticipant(participant, channel),
oldParticipant
? std::make_optional(Api::ChatParticipant(
*oldParticipant,
channel))
: std::nullopt);
}
TextWithEntities GenerateDefaultBannedRightsChangeText(
not_null<ChannelData*> channel,
ChatRestrictionsInfo rights,
ChatRestrictionsInfo oldRights) {
auto result = TextWithEntities{
tr::lng_admin_log_changed_default_permissions(tr::now)
};
const auto changes = GeneratePermissionsChangeText(rights, oldRights);
if (!changes.isEmpty()) {
result.text.append('\n' + changes);
}
result.entities.push_front(
EntityInText(EntityType::Italic, 0, result.text.size()));
return result;
}
[[nodiscard]] bool IsTopicClosed(const MTPForumTopic &topic) {
return topic.match([](const MTPDforumTopic &data)
{
return data.is_closed();
},
[](const MTPDforumTopicDeleted &)
{
return false;
});
}
[[nodiscard]] bool IsTopicHidden(const MTPForumTopic &topic) {
return topic.match([](const MTPDforumTopic &data)
{
return data.is_hidden();
},
[](const MTPDforumTopicDeleted &)
{
return false;
});
}
[[nodiscard]] TextWithEntities GenerateTopicLink(
not_null<ChannelData*> channel,
const MTPForumTopic &topic) {
return topic.match([&](const MTPDforumTopic &data)
{
return Ui::Text::Link(
Data::ForumTopicIconWithTitle(
data.vid().v,
data.vicon_emoji_id().value_or_empty(),
qs(data.vtitle())),
u"internal:url:https://t.me/c/%1/%2"_q.arg(
peerToChannel(channel->id).bare).arg(
data.vid().v));
},
[](const MTPDforumTopicDeleted &)
{
return TextWithEntities{u"Deleted"_q};
});
}
} // namespace
OwnedItem::OwnedItem(std::nullptr_t) {
}
OwnedItem::OwnedItem(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<HistoryItem*> data)
: _data(data), _view(_data->createView(delegate)) {
}
OwnedItem::OwnedItem(OwnedItem &&other)
: _data(base::take(other._data)), _view(base::take(other._view)) {
}
OwnedItem &OwnedItem::operator=(OwnedItem &&other) {
_data = base::take(other._data);
_view = base::take(other._view);
return *this;
}
OwnedItem::~OwnedItem() {
clearView();
if (_data) {
_data->destroy();
}
}
void OwnedItem::refreshView(
not_null<HistoryView::ElementDelegate*> delegate) {
_view = _data->createView(delegate);
}
void OwnedItem::clearView() {
_view = nullptr;
}
void GenerateItems(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
EditedMessage message,
Fn<void(OwnedItem item, TimeId sentDate, MsgId)> callback) {
PeerData *from = history->owner().userLoaded(message.fromId);
if (!from) {
from = history->owner().channelLoaded(message.fromId);
}
if (!from) {
from = reinterpret_cast<PeerData*>(history->owner().chatLoaded(message.fromId));
}
if (!from) {
return;
}
const auto date = message.entityCreateDate;
const auto addPart = [&](
not_null<HistoryItem*> item,
TimeId sentDate = 0,
MsgId realId = MsgId())
{
return callback(OwnedItem(delegate, item), sentDate, realId);
};
const auto fromName = from->name();
const auto fromLink = from->createOpenLink();
const auto fromLinkText = Ui::Text::Link(fromName, QString());
const auto makeSimpleTextMessage = [&](TextWithEntities &&text)
{
return history->makeMessage({
.id = history->nextNonHistoryEntryId(),
.flags = MessageFlag::HasFromId | MessageFlag::AdminLogEntry,
.from = from->id,
.date = date,
}, std::move(text), MTP_messageMediaEmpty());
};
const auto addSimpleTextMessage = [&](TextWithEntities &&text)
{
addPart(makeSimpleTextMessage(std::move(text)));
};
const auto text = QString::fromStdString(message.text);
addSimpleTextMessage(Ui::Text::WithEntities(text));
}
} // namespace EditedLog

View file

@ -2549,23 +2549,25 @@ void Session::processMessagesDeleted(
return;
}
const auto settings = &AyuSettings::getInstance();
auto historiesToCheck = base::flat_set<not_null<History*>>();
for (const auto &messageId : data) {
const auto i = list ? list->find(messageId.v) : Messages::iterator();
if (list && i != list->end()) {
const auto history = i->second->history();
const auto settings = &AyuSettings::getInstance();
if (!settings->saveDeletedMessages) {
i->second->destroy();
} else {
i->second->setAyuHint(settings->deletedMark);
AyuMessages::addDeletedMessage(i->second);
}
if (!history->chatListMessageKnown()) {
if (!history->chatListMessageKnown() && !settings->saveDeletedMessages) {
historiesToCheck.emplace(history);
}
} else if (affected) {
} else if (affected && !settings->saveDeletedMessages) {
affected->unknownMessageDeleted(messageId.v);
}
}
@ -2575,19 +2577,21 @@ void Session::processMessagesDeleted(
}
void Session::processNonChannelMessagesDeleted(const QVector<MTPint> &data) {
const auto settings = &AyuSettings::getInstance();
auto historiesToCheck = base::flat_set<not_null<History*>>();
for (const auto &messageId : data) {
if (const auto item = nonChannelMessage(messageId.v)) {
const auto history = item->history();
const auto settings = &AyuSettings::getInstance();
if (!settings->saveDeletedMessages) {
item->destroy();
} else {
item->setAyuHint(settings->deletedMark);
AyuMessages::addDeletedMessage(item);
}
if (!history->chatListMessageKnown()) {
if (!history->chatListMessageKnown() && !settings->saveDeletedMessages) {
historiesToCheck.emplace(history);
}
}

View file

@ -104,6 +104,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
// AyuGram includes
#include "styles/style_ayu_icons.h"
#include "ayu/ui/context_menu/context_menu.h"
namespace Window {
@ -1505,6 +1506,7 @@ void Filler::fillHistoryActions() {
addExportChat();
addTranslate();
addReport();
AyuUi::AddDeletedMessagesActions(_peer, _controller, _request, _addAction);
addClearHistory();
addDeleteChat();
addLeaveChat();