How the Telegram Desktop Admin Log Feature Works: Inside history_admin_log

The admin log feature converts administrative audit events from Telegram Layer (TL) objects into standard chat messages using the history_admin_log subsystem, which orchestrates data fetching, filtering, and rendering through three specialized layers.

The admin log provides supergroup administrators with a searchable, filterable history of management actions. In the telegramdesktop/tdesktop repository, this functionality resides entirely within Telegram/SourceFiles/history/admin_log/ and centers on the history_admin_log implementation. The architecture separates concerns into distinct UI, logic, and item-generation layers while leveraging the existing message rendering pipeline to display audit entries.

Architecture of the Admin Log System

The implementation follows a three-layer architecture that isolates presentation from data handling.

  • UI Container (AdminLog::Widget): Manages the window section, top navigation bar, and search interface. Defined in history_admin_log_section.cpp.
  • Logic Layer (AdminLog::InnerWidget): Handles TL request construction, pagination, filtering, and scroll state. Implemented in history_admin_log_inner.cpp.
  • Item Generation (GenerateItems): Transforms MTPChannelAdminLogEvent objects into standard HistoryItem instances. Located in history_admin_log_item.cpp.

Initializing the Admin Log View

When a user navigates to Settings ▸ Recent actions in a supergroup, the application constructs an AdminLog::Widget instance. The constructor initializes a FixedBar (the blue navigation header) and an InnerWidget that contains the scrollable log list.

_fixedBar = object_ptr<FixedBar>(this, controller, channel);
_inner   = _scroll->setOwnedWidget(
            object_ptr<InnerWidget>(this, controller, channel));

Source: history_admin_log_section.cpp (lines 87–102)

The FixedBar exposes two reactive streams: searchRequests() emits user queries, while searchCancelRequests() signals search dismissal. The Widget forwards these to the InnerWidget:

_fixedBar->searchCancelRequests() | rpl::on_next([=] { setInnerFocus(); });
_fixedBar->searchRequests()    | rpl::on_next([=](const QString &q) {
    _inner->applySearch(q);
});

Source: history_admin_log_section.cpp (lines 101–108)

Loading the Administrator List

Before displaying filter options, the system must identify eligible administrators. The InnerWidget::requestAdmins() method dispatches a channels.getParticipants request with the channelParticipantsAdmins filter.

_api.request(MTPchannels_GetParticipants(
    _channel->inputChannel(),
    MTP_channelParticipantsAdmins(),
    MTP_int(offset),
    MTP_int(kMaxChannelAdmins),
    MTP_long(participantsHash)
))
.done([=](const MTPchannels_ChannelParticipants &result) { … })
.send();

Source: history_admin_log_inner.cpp (lines 70–80)

Upon receiving the response, the method extracts UserData objects into _admins and _adminsCanEdit vectors, then calls showFilter() to populate the filter UI if the user had requested it.

Fetching Admin Log Events

Trigger Points

The system initiates log loading in three scenarios:

  1. Initial load: Widget::showEvent triggers preloadMore(Direction::Up).
  2. Scroll proximity: InnerWidget::checkPreloadMore() monitors _visibleTop and _visibleBottom, calling preloadMore() when the viewport nears content boundaries.
  3. Filter or search changes: applyFilter() and applySearch() set _filterChanged = true and invoke clearAndRequestLog(), resetting pagination and triggering upward preloading.

Building the TL Request

The InnerWidget::preloadMore(Direction direction) method constructs the channels.getAdminLog request by assembling bitmask flags from FilterValue::flags and optional administrator vectors.

auto flags = MTPchannels_GetAdminLog::Flags(0);
auto filter = [&] {
    using Flag = MTPDchannelAdminLogEventsFilter::Flag;
    using LocalFlag = FilterValue::Flag;
    const auto f = _filter.flags.value_or(LocalFlag());
    return empty
        | ((f & LocalFlag::Join)    ? Flag::f_join    : empty)
        | ((f & LocalFlag::Leave)   ? Flag::f_leave   : empty)
        … // additional flags omitted for brevity
}();

QVector<MTPInputUser> admins;
if (_filter.admins && !_filter.admins->empty()) {
    for (const auto &admin : *_filter.admins) {
        admins.push_back(admin->inputUser());
    }
    flags |= MTPchannels_GetAdminLog::Flag::f_admins;
}

auto maxId = (direction == Direction::Up) ? _minId : 0;
auto minId = (direction == Direction::Up) ? 0 : _maxId;
auto perPage = _items.empty() ? kEventsFirstPage : kEventsPerPage;

Source: history_admin_log_inner.cpp (lines 45–88)

The final request includes the channel identifier, search query string, event filter, administrator whitelist, and pagination cursors:

requestId = _api.request(MTPchannels_GetAdminLog(
    MTP_flags(flags),
    _channel->inputChannel(),
    MTP_string(_searchQuery),
    MTP_channelAdminLogEventsFilter(MTP_flags(filter)),
    MTP_vector<MTPInputUser>(admins),
    MTP_long(maxId),
    MTP_long(minId),
    MTP_int(perPage)
))
.done([=, &requestId, &loadedFlag](const MTPchannels_AdminLogResults &result) { … })
.send();

Source: history_admin_log_inner.cpp (lines 88–98)

Handling the Response

The response callback processes MTPchannels_AdminLogResults by updating local user and chat caches, then appending events to the view:

auto &results = result.c_channels_adminLogResults();
_channel->owner().processUsers(results.vusers());
_channel->owner().processChats(results.vchats());
if (!loadedFlag) {
    addEvents(direction, results.vevents().v);
}

Source: history_admin_log_inner.cpp (lines 98–104)

Converting TL Events to Renderable Items

The InnerWidget::addEvents() method iterates over the MTPChannelAdminLogEvent vector and delegates item construction to GenerateItems in history_admin_log_item.cpp.

GenerateItems(this, _history, data, addOne);

Source: history_admin_log_inner.cpp (lines 70–77)

The generator function examines the event's action type (such as channelAdminLogEventActionChangeTitle or channelAdminLogEventActionKick) and constructs a standard HistoryItem through these steps:

  1. Extract metadata: Retrieve original sent dates and real message IDs using ExtractSentDate and ExtractRealMsgId.
  2. Prepare message: Create an MTPMessage surrogate via PrepareLogMessage that mimics regular messages while stripping unnecessary flags.
  3. Instantiate item: Build a HistoryItem through history->addNewMessage and wrap it in an OwnedItem.
  4. Store auxiliary data: Populate _itemDates for floating date headers, _eventIds for deduplication, and _realIdsForReport for report actions.

Rendering the Log

InnerWidget inherits from Ui::RpWidget and implements the HistoryView delegate interface, allowing reuse of the standard chat rendering pipeline:

void InnerWidget::visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;
void InnerWidget::paintEvent(QPaintEvent *e) override;

Source: history_admin_log_inner.h (lines 61–70)

The scroll area (Ui::ScrollArea) provides viewport coordinates. When visibleTopBottomUpdated fires, the method iterates through _items, updates visibility ranges via view->item()->setVisibleTopBottom(...), and triggers additional preloading when necessary. Because items are standard HistoryView::Element subclasses (such as HistoryMessage or HistoryServiceMsg), they automatically support link handling, selection, and context menus without custom rendering code.

Filtering and Searching

The admin log supports both event-type filtering and text search.

  • Filtering: The showFilter() method presents a dialog populated from the _admins vector and action-type checkboxes. Upon confirmation, applyFilter(FilterValue&&) updates _filter.flags and _filter.admins, then calls clearAndRequestLog().
  • Searching: The top bar emits queries through FixedBar::searchRequests(). InnerWidget::applySearch() stores the string in _searchQuery and triggers clearAndRequestLog(), which reissues the channels.getAdminLog request with the updated parameters.

Both operations reset pagination and reload events from the server using the new criteria.

Pagination and State Preservation

The system maintains separate request identifiers for bidirectional loading: _preloadUpRequestId for older events and _preloadDownRequestId for newer ones. Boolean flags _upLoaded and _downLoaded track when pagination reaches log boundaries.

When users navigate away from the admin log, Widget::createMemento() captures the current state via InnerWidget::saveState(), preserving scroll position, loaded items, filter configuration, and search query. InnerWidget::restoreState() reconstructs this state upon return, enabling instant back-navigation without refetching data.

Summary

  • The admin log feature consists of three layers: the section widget (Widget), the logic handler (InnerWidget), and the item generator (GenerateItems).
  • Administrator lists are fetched via channels.getParticipants before filter UI presentation.
  • Log events are retrieved through channels.getAdminLog with bitmask filters, optional admin whitelists, and pagination cursors managed in preloadMore().
  • TL events are converted to standard HistoryItem objects in history_admin_log_item.cpp, enabling reuse of the generic chat view.
  • Filtering and search trigger full reloads with updated parameters, while mementos preserve state for seamless navigation.

Frequently Asked Questions

How does the admin log handle pagination for large audit histories?

The InnerWidget maintains separate request IDs for upward and downward loading (_preloadUpRequestId and _preloadDownRequestId). It calculates maxId and minId cursors based on the loading direction, requesting kEventsPerPage entries per batch. When scrolling near content boundaries, checkPreloadMore() automatically triggers additional fetches until _upLoaded or _downLoaded flags indicate boundary conditions.

Can the admin log display events from specific administrators only?

Yes. When the filter UI includes specific administrators, preloadMore() constructs a QVector<MTPInputUser> from the selected admins and sets the MTPchannels_GetAdminLog::Flag::f_admins flag. The server returns only events performed by the specified users.

Why does the admin log use standard HistoryItem objects instead of custom views?

The implementation reuses HistoryView::Element subclasses to ensure visual consistency with regular chat messages and to inherit existing functionality like text selection, link handling, and context menus. The GenerateItems function creates these standard items by transforming MTPChannelAdminLogEvent data into compatible MTPMessage structures, avoiding duplicate rendering logic.

Where is the search query processed in the admin log pipeline?

The search query originates in FixedBar and propagates to InnerWidget::applySearch(), which stores the string in _searchQuery. This value is passed as MTP_string(_searchQuery) to the channels.getAdminLog request. The Telegram servers perform the text search, returning only matching events to the client.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →