// 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, 2026 #include "ayu/features/message_shot/message_shot.h" #include "qguiapplication.h" #include "ayu/ayu_settings.h" #include "ayu/ui/boxes/message_shot_box.h" #include "ayu/utils/telegram_helpers.h" #include "boxes/abstract_box.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_file_origin.h" #include "data/data_forum.h" #include "data/data_peer.h" #include "data/data_photo.h" #include "data/data_photo_media.h" #include "data/data_session.h" #include "dialogs/ui/dialogs_video_userpic.h" #include "history/history.h" #include "history/history_inner_widget.h" #include "history/history_item.h" #include "history/history_item_components.h" #include "history/view/history_view_element.h" #include "history/view/media/history_view_media.h" #include "main/main_session.h" #include "styles/style_ayu_styles.h" #include "styles/style_chat.h" #include "styles/style_layers.h" #include "ui/painter.h" #include "ui/chat/chat_theme.h" #include "ui/effects/path_shift_gradient.h" #include "ui/layers/box_content.h" #include "window/themes/window_theme.h" namespace AyuFeatures::MessageShot { ShotConfig *config = nullptr; bool takingShot = false; bool choosingTheme = false; void setShotConfig(ShotConfig &config) { MessageShot::config = &config; } void resetShotConfig() { config = nullptr; } ShotConfig getShotConfig() { return *config; } bool ignoreRender(RenderPart part) { if (!config) { return false; } const auto &s = AyuSettings::getInstance().messageShotSettings(); return isTakingShot() && ((part == RenderPart::Date && !s.showDate()) || (part == RenderPart::Reactions && !s.showReactions()) || (part == RenderPart::HeaderDecorations && !s.showHeaderDecorations())); } bool isTakingShot() { return takingShot; } bool setChoosingTheme(bool val) { choosingTheme = val; return choosingTheme; } bool isChoosingTheme() { return choosingTheme; } class MessageShotDelegate final : public HistoryView::DefaultElementDelegate { public: MessageShotDelegate( not_null parent, not_null st, Fn update, not_null history); bool elementAnimationsPaused() override; not_null elementPathShiftGradient() override; HistoryView::Context elementContext() override; bool elementHideReply(not_null view) override; HistoryView::ElementChatMode elementChatMode() override; private: const not_null _parent; const std::unique_ptr _pathGradient; not_null _history; }; MessageShotDelegate::MessageShotDelegate( not_null parent, not_null st, Fn update, not_null history) : _parent(parent) , _pathGradient(HistoryView::MakePathShiftGradient(st, update)) , _history(history) { } bool MessageShotDelegate::elementAnimationsPaused() { return _parent->window()->isActiveWindow(); } auto MessageShotDelegate::elementPathShiftGradient() -> not_null { return _pathGradient.get(); } HistoryView::Context MessageShotDelegate::elementContext() { return HistoryView::Context::AdminLog; } bool MessageShotDelegate::elementHideReply(not_null view) { if (const auto reply = view->data()->Get()) { const auto replyToPeerId = reply->externalPeerId() ? reply->externalPeerId() : _history->peer->id; if (reply->fields().manualQuote) { return false; } else if (replyToPeerId == _history->peer->id) { return _history->asForum() && _history->asForum()->topicFor(reply->messageId()); } } return false; } HistoryView::ElementChatMode MessageShotDelegate::elementChatMode() { using Mode = HistoryView::ElementChatMode; return Mode::Wide; } QImage removeEmptySpaceAround(const QImage &original) { if (original.isNull()) { return {}; } int minX = original.width(); int minY = original.height(); int maxX = 0; int maxY = 0; for (int x = 0; x < original.width(); ++x) { for (int y = 0; y < original.height(); ++y) { if (qAlpha(original.pixel(x, y)) != 0) { minX = std::min(minX, x); minY = std::min(minY, y); maxX = std::max(maxX, x); maxY = std::max(maxY, y); } } } if (minX > maxX || minY > maxY) { LOG(("Image is fully transparent ?")); return {}; } const QRect bounds(minX, minY, maxX - minX + 1, maxY - minY + 1); return original.copy(bounds); } QImage addPadding(const QImage &original) { if (original.isNull()) { return {}; } QImage paddedImage( original.width() + 2 * st::messageShotPadding * style::DevicePixelRatio(), original.height() + 2 * st::messageShotPadding * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied ); paddedImage.setDevicePixelRatio(style::DevicePixelRatio()); paddedImage.fill(Qt::transparent); Painter painter(&paddedImage); painter.drawImage(st::messageShotPadding, st::messageShotPadding, original); painter.end(); return paddedImage; } QColor makeDefaultBackgroundColor() { if (Window::Theme::IsNightMode()) { return st::boxBg->c.lighter(175); } return st::boxBg->c.darker(110); } void Make(not_null box, const ShotConfig &config, const Fn& callback) { const auto controller = config.controller; const auto st = config.st; auto messages = config.messages; if (messages.empty()) { return; } auto delegate = std::make_shared( box, st.get(), [=] { box->update(); }, messages.front()->history()); // remove deleted messages messages.erase( std::ranges::remove_if( messages, [=](const auto &message) { return !message || !controller->session().data().message(message->fullId()); }).begin(), messages.end() ); if (messages.empty()) { return; } auto createdViews = std::make_shared, std::shared_ptr>>(); createdViews->reserve(messages.size()); for (const auto &message : messages) { createdViews->emplace(message, message->createView(delegate.get())); } auto getView = [createdViews](not_null msg) { return createdViews->at(msg).get(); }; // recalculate blocks if (messages.size() > 1) { auto currentMsg = messages[0].get(); for (auto i = 1; i != messages.size(); ++i) { const auto nextMsg = messages[i].get(); if (getView(nextMsg)->isHidden()) { getView(nextMsg)->setDisplayDate(false); } else { const auto viewDate = getView(currentMsg)->dateTime(); const auto nextDate = getView(nextMsg)->dateTime(); getView(nextMsg)->setDisplayDate(nextDate.date() != viewDate.date()); auto attached = getView(nextMsg)->computeIsAttachToPrevious(getView(currentMsg)); getView(nextMsg)->setAttachToPrevious(attached, getView(currentMsg)); getView(currentMsg)->setAttachToNext(attached, getView(nextMsg)); currentMsg = nextMsg; } } getView(messages[messages.size() - 1])->setAttachToNext(false); } else { getView(messages[0])->setAttachToPrevious(false); getView(messages[0])->setAttachToNext(false); } struct MediaPreload { std::vector> photos; std::vector> documents; }; auto preload = std::make_shared(); for (const auto &message : messages) { if (!message->media()) continue; const auto origin = Data::FileOrigin(message->fullId()); if (const auto photo = message->media()->photo()) { auto media = photo->activeMediaView() ? photo->activeMediaView() : photo->createMediaView(); if (!media->loaded()) { photo->load(origin, LoadFromCloudOrLocal, false); } preload->photos.push_back(std::move(media)); } else if (const auto document = message->media()->document()) { if (document->hasThumbnail()) { auto media = document->activeMediaView() ? document->activeMediaView() : document->createMediaView(); if (!media->thumbnail()) { document->loadThumbnail(origin); } preload->documents.push_back(std::move(media)); } } } const auto showBackground = AyuSettings::getInstance().messageShotSettings().showBackground(); auto render = [=, messages = std::move(messages), delegate = std::move(delegate)](bool final) { takingShot = true; // calculate the size of the image int width = st::msgMaxWidth + (st::boxPadding.left() + st::boxPadding.right()); int height = 0; for (int i = 0; i < messages.size(); i++) { const auto &message = messages[i]; const auto view = getView(message); view->itemDataChanged(); // refresh reactions height += view->resizeGetHeight(width); if (AyuSettings::getInstance().messageShotSettings().revealSpoilers()) { view->revealSpoilers(); } } width *= style::DevicePixelRatio(); height *= style::DevicePixelRatio(); // create the image QImage image(width, height, QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(style::DevicePixelRatio()); image.fill(Qt::transparent); const auto viewport = QRect(0, 0, width, height); base::flat_map, Ui::PeerUserpicView> userpics; base::flat_map hiddenSenderUserpics; Painter p(&image); // draw the messages int y = 0; for (int i = 0; i < messages.size(); i++) { const auto &message = messages[i]; const auto view = getView(message); const auto displayUserpic = view->displayFromPhoto() || message->isPost(); const auto rect = QRect(0, 0, width, view->height()); auto context = controller->defaultChatTheme()->preparePaintContext( st.get(), viewport, rect, rect, true); p.translate(0, y); view->draw(p, context); p.translate(0, -y); if (displayUserpic) { const auto picX = st::msgMargin.left(); const auto picY = y + view->height() - st::msgPhotoSize; if (const auto from = message->displayFrom()) { Dialogs::Ui::PaintUserpic( p, from, nullptr, userpics[from], picX, picY, width, st::msgPhotoSize, context.paused); } else if (const auto info = message->displayHiddenSenderInfo()) { if (info->customUserpic.empty()) { info->emptyUserpic.paintCircle( p, picX, picY, width, st::msgPhotoSize); } } } y += view->height(); } takingShot = false; auto result = addPadding(removeEmptySpaceAround(image)); if (!showBackground) { callback(result, final); return; } auto newResult = QImage(result.size(), QImage::Format_ARGB32_Premultiplied); newResult.setDevicePixelRatio(style::DevicePixelRatio()); newResult.fill(makeDefaultBackgroundColor()); Painter painter(&newResult); painter.drawImage(0, 0, result); callback(newResult, final); }; if (!preload->documents.empty() || !preload->photos.empty()) { render(false); // render immediately to give box width auto lifetime = std::make_shared(); auto latch = std::make_shared(1); rpl::single() | rpl::then( config.controller->session().downloaderTaskFinished() ) | rpl::filter([=] { for (const auto &media : preload->photos) { if (media->owner()->loading()) return false; } for (const auto &media : preload->documents) { if (media->owner()->thumbnailLoading()) return false; } return true; } ) | rpl::take(1) | rpl::on_next([=] { latch->countDown(); }, *lifetime); crl::async([=, render = std::move(render)] { latch->await(std::chrono::seconds(3)); crl::on_main([=] { lifetime->destroy(); render(true); }); }); } else { render(true); } } namespace { // 🥀🥀🥀 std::shared_ptr BuildShotChatStyle( not_null controller) { const auto &shot = AyuSettings::getInstance().messageShotSettings(); const auto hasSavedTheme = shot.embeddedThemeType() != -1 || shot.cloudThemeId() != 0; const auto persistedPalette = getPersistedPalette(); if (hasSavedTheme && persistedPalette) { return std::make_shared(persistedPalette.get()); } return std::make_shared(controller->chatStyle()); } template void ShowMessageShotBox( ResolveMessage resolveMessage, not_null controller, const MessageIdsList &ids, Fn clearSelected) { auto messages = std::vector>(); messages.reserve(ids.size()); for (const auto item : ids) { if (const auto message = resolveMessage(item)) { messages.push_back(message); } } if (messages.empty()) { return; } const AyuFeatures::MessageShot::ShotConfig config = { controller, BuildShotChatStyle(controller), messages, }; auto box = Box(config); const auto raw = box.data(); raw->boxClosing() | rpl::on_next([=] { if (raw->tookShot()) clearSelected(); }, raw->lifetime()); Ui::show(std::move(box)); } template void WrapperImpl( not_null widget, GetIds getIds, Fn clearSelected) { const auto items = getIds(widget); if (items.empty()) { return; } const auto session = &widget->session(); const auto controller = widget->session().tryResolveWindow(); if (!controller) { return; } ShowMessageShotBox( [=](const auto item) { return session->data().message(item); }, controller, items, std::move(clearSelected)); } } void Wrapper(not_null widget, Fn clearSelected) { WrapperImpl( widget, [](const auto widget) { return widget->getSelectedIds(); }, std::move(clearSelected)); } void Wrapper(not_null widget, Fn clearSelected) { WrapperImpl( widget, [](const auto widget) { return widget->getSelectedItems(); }, std::move(clearSelected)); } }