Compare commits

...

6 commits

Author SHA1 Message Date
8ca9f92193
feat(streamer): seeded per-peer color + hide stars rating
- ForcedColorIndex(PeerId) seeded the same way as GeneratedName,
  mod 7 for the standard non-premium palette so it stays uniformly
  available; stable within a session, reshuffled each launch
- streamer-mode-rating: gate StarsRatingValue rpl producer with
  Streamer::Changes() so profile top-bar stars-rating widget shows
  empty Data::StarsRating{} while active
2026-05-01 17:24:15 +06:00
7e12a05466
fix(patches): streamer mode hide admin titles + seen-by avatars
- streamer-mode-rank: hook Message::refreshRightBadge lambda to
  emit empty text when sender is anonymized; covers per-group
  custom admin titles plus generic Owner/Admin labels
- streamer-mode-seen-avatars: hook PeerData::GenerateUserpicImage
  static helper that api_who_reacted uses to render seen-by /
  who-reacted avatar strip + popup; falls through to empty userpic
2026-05-01 17:15:45 +06:00
37d80588b7
fix(patches): streamer mode dialog list anonymization
- drop streamer-mode-online: keep real last-seen text per user pref
- add streamer-mode-forum-topic: anonymize ForumTopic::chatListName
  (per-topic seed = channel id ^ rootId so distinct pseudonyms)
- extend streamer-mode-name: public PeerData::noteNameUpdated bumps
  _nameVersion so Entry::_chatListNameText cache rebuilds on toggle
- ag_refresh: walk histories + topics, invalidate chat-list-entry
  message-view cache (HistoryItem::invalidateChatListEntry resets
  MessageView _textCachedFor so sender prefix re-renders)
- streamer-mode-avatar/style: hunk-header line drift only from
  noteNameUpdated insertion above
2026-05-01 17:07:27 +06:00
a3c58ae970
feat(patches): streamer mode hooks across name/avatar/style/identity
13 stgit patches anonymizing non-self users when toggle is on:
- name/typing/reply-shorten/forward: pseudonym in name accessors and
  firstName bypass paths (forward-header bake also covered via name())
- avatar/call-avatar/short-info-box: force empty userpic at chokepoint
  + fullscreen-call + profile-photo carousel bypasses
- style/badge: strip color, background emoji, premium emoji status,
  verified/scam/fake/bot-verify badges
- profile-identity/profile-actions/online: hide phone/username/bio/
  common-chats/birthday/peer-id/joined-channel/last-seen + presence dot
- autocomplete/search: hide @username in mention popup, dialogs search
  rows, and drag-mime to suppress clipboard leak

Plus ag_refresh::ForEachLoadedPeer helper and toggle handler that
invalidates empty-userpic cache and emits peer-update flags so live
toggle propagates to open profile pages and dialog rows.
2026-05-01 16:43:24 +06:00
9bc9b279d4
fix(arcanegram): match settings divider-label style for streamer preview 2026-05-01 15:46:39 +06:00
dd4fa8bd5a
feat(arcanegram): streamer mode module + settings 2026-05-01 15:42:03 +06:00
28 changed files with 1340 additions and 2 deletions

View file

@ -0,0 +1,34 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 16:16:09 +0600
Subject: [PATCH] streamer mode: hide username in mention autocomplete
---
Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
+++ b/Telegram/SourceFiles/chat_helpers/field_autocomplete.cpp
@@ -52,12 +52,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "styles/style_chat_helpers.h"
#include "styles/style_menu_icons.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
+
#include <QtWidgets/QApplication>
namespace ChatHelpers {
namespace {
[[nodiscard]] QString PrimaryUsername(not_null<UserData*> user) {
+ if (Arcanegram::Streamer::ShouldAnonymize(user)) {
+ return QString();
+ }
const auto &usernames = user->usernames();
return usernames.empty() ? user->username() : usernames.front();
}
--
2.52.0.windows.1

View file

@ -0,0 +1,48 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 15:57:28 +0600
Subject: [PATCH] streamer mode: force empty userpic in PeerData::paintUserpic
---
Telegram/SourceFiles/data/data_peer.cpp | 4 +++-
Telegram/SourceFiles/data/data_peer.h | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -462,7 +462,9 @@ void PeerData::paintUserpic(
return;
}
const auto size = context.size;
- const auto cloud = userpicCloudImage(view);
+ const auto cloud = Arcanegram::Streamer::ShouldAnonymize(this)
+ ? nullptr
+ : userpicCloudImage(view);
const auto ratio = style::DevicePixelRatio();
if (context.shape == Ui::PeerUserpicShape::Auto) {
context.shape = (isForum() && !isBot())
diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer.h
+++ b/Telegram/SourceFiles/data/data_peer.h
@@ -459,6 +459,7 @@ public:
}
[[nodiscard]] QImage *userpicCloudImage(Ui::PeerUserpicView &view) const;
+ void invalidateEmptyUserpic();
[[nodiscard]] bool canPinMessages() const;
[[nodiscard]] bool canEditMessagesIndefinitely() const;
@@ -597,7 +598,6 @@ protected:
const QString &newUsername);
void updateUserpic(PhotoId photoId, MTP::DcId dcId, bool hasVideo);
void clearUserpic();
- void invalidateEmptyUserpic();
void checkTrustedPayForMessage();
private:
--
2.52.0.windows.1

View file

@ -0,0 +1,59 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 16:03:37 +0600
Subject: [PATCH] streamer mode: hide verified/scam/premium badges
---
Telegram/SourceFiles/info/profile/info_profile_badge.cpp | 5 +++++
Telegram/SourceFiles/info/profile/info_profile_values.cpp | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_badge.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_badge.cpp
@@ -22,6 +22,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "styles/style_info.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
+
namespace Info::Profile {
namespace {
@@ -306,6 +308,9 @@ rpl::producer<Badge::Content> VerifiedContentForPeer(
rpl::producer<Badge::Content> BotVerifyBadgeForPeer(
not_null<PeerData*> peer) {
+ if (Arcanegram::Streamer::ShouldAnonymize(peer)) {
+ return rpl::single(Badge::Content{ BadgeType::None });
+ }
return peer->session().changes().peerFlagsValue(
peer,
Data::PeerUpdate::Flag::VerifyInfo
diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp
@@ -36,6 +36,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/edit_peer_permissions_box.h"
#include "base/unixtime.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
+
namespace Info {
namespace Profile {
namespace {
@@ -706,6 +708,9 @@ rpl::producer<BadgeType> BadgeValueFromFlags(Peer peer) {
}
rpl::producer<BadgeType> BadgeValue(not_null<PeerData*> peer) {
+ if (Arcanegram::Streamer::ShouldAnonymize(peer)) {
+ return rpl::single(BadgeType::None);
+ }
if (const auto user = peer->asUser()) {
return BadgeValueFromFlags<UserDataFlag>(user);
} else if (const auto channel = peer->asChannel()) {
--
2.52.0.windows.1

View file

@ -0,0 +1,50 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 15:57:40 +0600
Subject: [PATCH] streamer mode: empty userpic in fullscreen call
---
Telegram/SourceFiles/calls/calls_userpic.cpp | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/Telegram/SourceFiles/calls/calls_userpic.cpp b/Telegram/SourceFiles/calls/calls_userpic.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/calls/calls_userpic.cpp
+++ b/Telegram/SourceFiles/calls/calls_userpic.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "calls/calls_userpic.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "data/data_peer.h"
#include "main/main_session.h"
#include "data/data_changes.h"
@@ -130,9 +131,10 @@ int Userpic::size() const {
void Userpic::processPhoto() {
_userpic = _peer->createUserpicView();
_peer->loadUserpic();
- const auto photo = _peer->userpicPhotoId()
- ? _peer->owner().photo(_peer->userpicPhotoId()).get()
- : nullptr;
+ const auto photo = (Arcanegram::Streamer::ShouldAnonymize(_peer)
+ || !_peer->userpicPhotoId())
+ ? nullptr
+ : _peer->owner().photo(_peer->userpicPhotoId()).get();
if (isGoodPhoto(photo)) {
_photo = photo->createMediaView();
_photo->wanted(Data::PhotoSize::Thumbnail, _peer->userpicPhotoOrigin());
@@ -159,7 +161,10 @@ void Userpic::refreshPhoto() {
_userPhotoFull = true;
createCache(_photo->image(Data::PhotoSize::Thumbnail));
} else if (_userPhoto.isNull()) {
- if (const auto cloud = _peer->userpicCloudImage(_userpic)) {
+ const auto cloud = Arcanegram::Streamer::ShouldAnonymize(_peer)
+ ? nullptr
+ : _peer->userpicCloudImage(_userpic);
+ if (cloud) {
auto image = Image(base::duplicate(*cloud));
createCache(&image);
} else {
--
2.52.0.windows.1

View file

@ -0,0 +1,58 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 16:53:21 +0600
Subject: [PATCH] streamer mode: anonymize forum topic title
---
Telegram/SourceFiles/data/data_forum_topic.cpp | 7 +++++++
Telegram/SourceFiles/data/data_forum_topic.h | 2 +-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.cpp
+++ b/Telegram/SourceFiles/data/data_forum_topic.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_forum_topic.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "data/data_channel.h"
#include "data/data_changes.h"
#include "data/data_forum.h"
@@ -958,6 +959,12 @@ bool ForumTopic::chatListMessageKnown() const {
}
const QString &ForumTopic::chatListName() const {
+ if (channel() && Arcanegram::Streamer::ShouldAnonymize(channel())) {
+ static thread_local QString cached;
+ const auto seed = channel()->id.value ^ quint64(rootId().bare);
+ cached = Arcanegram::Streamer::GeneratedShortName(PeerId(seed));
+ return cached;
+ }
return _title;
}
diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_forum_topic.h
+++ b/Telegram/SourceFiles/data/data_forum_topic.h
@@ -153,6 +153,7 @@ public:
[[nodiscard]] TextWithEntities titleWithIcon() const;
[[nodiscard]] TextWithEntities titleWithIconOrLogo() const;
[[nodiscard]] int titleVersion() const;
+ void invalidateTitleWithIcon();
void applyTitle(const QString &title);
[[nodiscard]] DocumentId iconId() const;
void applyIconId(DocumentId iconId);
@@ -206,7 +207,6 @@ private:
void validateGeneralIcon(const Dialogs::Ui::PaintContext &context) const;
void applyTopicTopMessage(MsgId topMessageId);
void growLastKnownServerMessageId(MsgId id);
- void invalidateTitleWithIcon();
void setLastMessage(HistoryItem *item);
void setLastServerMessage(HistoryItem *item);
--
2.52.0.windows.1

View file

@ -0,0 +1,78 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 15:49:36 +0600
Subject: [PATCH] streamer mode: pseudonym in PeerData::name accessors
---
Telegram/SourceFiles/data/data_peer.cpp | 20 ++++++++++++++++++++
Telegram/SourceFiles/data/data_peer.h | 1 +
2 files changed, 21 insertions(+)
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_peer.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "api/api_sensitive_content.h"
#include "data/data_user.h"
#include "data/data_chat.h"
@@ -1313,6 +1314,11 @@ ChannelData *PeerData::broadcastMonoforum() const {
}
const QString &PeerData::topBarNameText() const {
+ if (Arcanegram::Streamer::ShouldAnonymize(this)) {
+ static thread_local QString cached;
+ cached = Arcanegram::Streamer::GeneratedName(id);
+ return cached;
+ }
if (const auto to = migrateTo()) {
return to->topBarNameText();
} else if (const auto user = asUser()) {
@@ -1327,7 +1333,16 @@ int PeerData::nameVersion() const {
return _nameVersion;
}
+void PeerData::noteNameUpdated() {
+ ++_nameVersion;
+}
+
const QString &PeerData::name() const {
+ if (Arcanegram::Streamer::ShouldAnonymize(this)) {
+ static thread_local QString cached;
+ cached = Arcanegram::Streamer::GeneratedName(id);
+ return cached;
+ }
if (const auto to = migrateTo()) {
return to->name();
} else if (const auto broadcast = monoforumBroadcast()) {
@@ -1337,6 +1352,11 @@ const QString &PeerData::name() const {
}
const QString &PeerData::shortName() const {
+ if (Arcanegram::Streamer::ShouldAnonymize(this)) {
+ static thread_local QString cached;
+ cached = Arcanegram::Streamer::GeneratedShortName(id);
+ return cached;
+ }
if (const auto user = asUser()) {
return user->firstName.isEmpty() ? user->lastName : user->firstName;
} else if (const auto to = migrateTo()) {
diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer.h
+++ b/Telegram/SourceFiles/data/data_peer.h
@@ -365,6 +365,7 @@ public:
}
[[nodiscard]] int nameVersion() const;
+ void noteNameUpdated();
[[nodiscard]] const QString &name() const;
[[nodiscard]] const QString &shortName() const;
[[nodiscard]] const QString &topBarNameText() const;
--
2.52.0.windows.1

View file

@ -0,0 +1,75 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 16:10:29 +0600
Subject: [PATCH] streamer mode: hide peer id, birthday, username link
---
.../info/profile/info_profile_actions.cpp | 21 +++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/profile/info_profile_actions.h"
#include "arcanegram/features/ag_peer_ids.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "api/api_blocked_peers.h"
#include "api/api_chat_participants.h"
#include "api/api_credits.h"
@@ -173,6 +174,9 @@ base::options::toggle ShowChannelJoinedBelowAbout({
const QString &addToLink) {
const auto weak = base::make_weak(controller);
return [=](QString link) {
+ if (Arcanegram::Streamer::ShouldAnonymize(peer)) {
+ return;
+ }
if (link.startsWith(u"internal:"_q)) {
Core::App().openInternalUrl(link,
QVariant::fromValue(ClickHandlerContext{
@@ -213,7 +217,8 @@ base::options::toggle ShowChannelJoinedBelowAbout({
return AboutValue(
peer
) | rpl::map([=](TextWithEntities &&value) {
- if (ShowPeerIdBelowAbout.value()) {
+ const auto anonymize = Arcanegram::Streamer::ShouldAnonymize(peer);
+ if (ShowPeerIdBelowAbout.value() && !anonymize) {
using namespace Ui::Text;
if (!value.empty()) {
value.append("\n\n");
@@ -226,11 +231,11 @@ base::options::toggle ShowChannelJoinedBelowAbout({
"internal:~peer_id~:copy:" + botApi
+ ":mtproto:" + mtproto));
}
- if (ShowChannelJoinedBelowAbout.value()) {
+ if (ShowChannelJoinedBelowAbout.value() && !anonymize) {
if (const auto channel = peer->asChannel()) {
if (!channel->amCreator() && channel->inviteDate) {
if (!value.empty()) {
- if (ShowPeerIdBelowAbout.value()) {
+ if (ShowPeerIdBelowAbout.value() && !anonymize) {
value.append("\n");
} else {
value.append("\n\n");
@@ -898,9 +903,13 @@ void DeleteContactNote(
const auto layout = outer->entity();
layout->setAttribute(Qt::WA_TransparentForMouseEvents);
- auto birthday = BirthdayValue(
- user
- ) | rpl::start_spawning(result->lifetime());
+ auto birthday = rpl::combine(
+ BirthdayValue(user),
+ rpl::single(Arcanegram::Streamer::IsActive())
+ | rpl::then(Arcanegram::Streamer::Changes())
+ ) | rpl::map([](Data::Birthday value, bool streamer) {
+ return streamer ? Data::Birthday() : value;
+ }) | rpl::start_spawning(result->lifetime());
auto label = BirthdayLabelText(rpl::duplicate(birthday));
auto text = BirthdayValueText(
--
2.52.0.windows.1

View file

@ -0,0 +1,100 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 16:08:54 +0600
Subject: [PATCH] streamer mode: hide profile phone/username/bio/common-chats
---
.../info/profile/info_profile_values.cpp | 56 +++++++++++--------
1 file changed, 34 insertions(+), 22 deletions(-)
diff --git a/Telegram/SourceFiles/info/profile/info_profile_values.cpp b/Telegram/SourceFiles/info/profile/info_profile_values.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/info/profile/info_profile_values.cpp
+++ b/Telegram/SourceFiles/info/profile/info_profile_values.cpp
@@ -45,11 +45,14 @@ namespace {
using UpdateFlag = Data::PeerUpdate::Flag;
auto PlainAboutValue(not_null<PeerData*> peer) {
- return peer->session().changes().peerFlagsValue(
- peer,
- UpdateFlag::About
- ) | rpl::map([=] {
- return peer->about();
+ return rpl::combine(
+ peer->session().changes().peerFlagsValue(
+ peer,
+ UpdateFlag::About),
+ rpl::single(Arcanegram::Streamer::IsActive())
+ | rpl::then(Arcanegram::Streamer::Changes())
+ ) | rpl::map([=](const auto &, bool streamer) {
+ return streamer ? QString() : peer->about();
});
}
@@ -126,13 +129,16 @@ rpl::producer<int32> ColorIdValue(not_null<Data::ForumTopic*> topic) {
}
rpl::producer<TextWithEntities> PhoneValue(not_null<UserData*> user) {
- return rpl::merge(
- Countries::Instance().updated(),
- user->session().changes().peerFlagsValue(
- user,
- UpdateFlag::PhoneNumber) | rpl::to_empty
- ) | rpl::map([=] {
- return tr::marked(Ui::FormatPhone(user->phone()));
+ return rpl::combine(
+ rpl::merge(
+ Countries::Instance().updated(),
+ user->session().changes().peerFlagsValue(
+ user,
+ UpdateFlag::PhoneNumber) | rpl::to_empty),
+ rpl::single(Arcanegram::Streamer::IsActive())
+ | rpl::then(Arcanegram::Streamer::Changes())
+ ) | rpl::map([=](const auto &, bool streamer) {
+ return tr::marked(Ui::FormatPhone(streamer ? QString() : user->phone()));
});
}
@@ -161,11 +167,14 @@ rpl::producer<TextWithEntities> PhoneOrHiddenValue(not_null<UserData*> user) {
rpl::producer<TextWithEntities> UsernameValue(
not_null<PeerData*> peer,
bool primary) {
- return (primary
- ? PlainPrimaryUsernameValue(peer)
- : (PlainUsernameValue(peer) | rpl::type_erased)
- ) | rpl::map([](QString &&username) {
- return username.isEmpty()
+ return rpl::combine(
+ (primary
+ ? PlainPrimaryUsernameValue(peer)
+ : (PlainUsernameValue(peer) | rpl::type_erased)),
+ rpl::single(Arcanegram::Streamer::IsActive())
+ | rpl::then(Arcanegram::Streamer::Changes())
+ ) | rpl::map([](const QString &username, bool streamer) {
+ return (streamer || username.isEmpty())
? tr::marked()
: tr::marked('@' + username);
});
@@ -600,11 +609,14 @@ rpl::producer<int> SharedMediaCountValue(
}
rpl::producer<int> CommonGroupsCountValue(not_null<UserData*> user) {
- return user->session().changes().peerFlagsValue(
- user,
- UpdateFlag::CommonChats
- ) | rpl::map([=] {
- return user->commonChatsCount();
+ return rpl::combine(
+ user->session().changes().peerFlagsValue(
+ user,
+ UpdateFlag::CommonChats),
+ rpl::single(Arcanegram::Streamer::IsActive())
+ | rpl::then(Arcanegram::Streamer::Changes())
+ ) | rpl::map([=](const auto &, bool streamer) {
+ return streamer ? 0 : user->commonChatsCount();
});
}
--
2.52.0.windows.1

View file

@ -0,0 +1,35 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 17:12:52 +0600
Subject: [PATCH] streamer mode: drop admin custom title and rank badge for
anonymized senders
---
Telegram/SourceFiles/history/view/history_view_message.cpp | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Telegram/SourceFiles/history/view/history_view_message.cpp b/Telegram/SourceFiles/history/view/history_view_message.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/history/view/history_view_message.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_message.cpp
@@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_suggest_post.h"
#include "api/api_transcribes.h"
#include "arcanegram/features/ag_hidden_users.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "base/qt/qt_key_modifiers.h"
#include "base/unixtime.h"
#include "core/click_handler_types.h" // ClickHandlerContext
@@ -393,6 +394,9 @@ void Message::refreshRightBadge() {
}
const auto item = data();
const auto [text, role, special] = [&]() -> std::tuple<QString, BadgeRole, bool> {
+ if (Arcanegram::Streamer::ShouldAnonymize(item->author())) {
+ return { QString(), BadgeRole::User, false };
+ }
if (item->isDiscussionPost()) {
return {
(delegate()->elementContext() == Context::Replies)
--
2.52.0.windows.1

View file

@ -0,0 +1,46 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 17:22:26 +0600
Subject: [PATCH] streamer mode: hide profile rating
---
Telegram/SourceFiles/data/data_peer_values.cpp | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/Telegram/SourceFiles/data/data_peer_values.cpp b/Telegram/SourceFiles/data/data_peer_values.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer_values.cpp
+++ b/Telegram/SourceFiles/data/data_peer_values.cpp
@@ -20,6 +20,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/image/image_prepare.h"
#include "base/unixtime.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
+
namespace Data {
namespace {
@@ -524,10 +526,16 @@ bool ChannelHasSubscriptionUntilDate(ChannelData *channel) {
rpl::producer<Data::StarsRating> StarsRatingValue(
not_null<PeerData*> peer) {
if (const auto user = peer->asUser()) {
- return user->session().changes().peerFlagsValue(
- user,
- Data::PeerUpdate::Flag::StarsRating
- ) | rpl::map([=] {
+ return rpl::combine(
+ user->session().changes().peerFlagsValue(
+ user,
+ Data::PeerUpdate::Flag::StarsRating),
+ rpl::single(Arcanegram::Streamer::IsActive())
+ | rpl::then(Arcanegram::Streamer::Changes())
+ ) | rpl::map([=](const auto &, bool streamer) {
+ if (streamer) {
+ return Data::StarsRating();
+ }
auto result = user->starsRating();
if (!user->isSelf() && result.level < 0) {
result.stars = 0;
--
2.52.0.windows.1

View file

@ -0,0 +1,39 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 15:50:34 +0600
Subject: [PATCH] streamer mode: pseudonym in shortened reply sender name
---
Telegram/SourceFiles/history/view/history_view_reply.cpp | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/Telegram/SourceFiles/history/view/history_view_reply.cpp b/Telegram/SourceFiles/history/view/history_view_reply.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/history/view/history_view_reply.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_reply.cpp
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_reply.h"
#include "arcanegram/features/ag_hidden_users.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "core/click_handler_types.h"
#include "core/ui_integration.h"
#include "data/stickers/data_custom_emoji.h"
@@ -602,7 +603,13 @@ QString Reply::senderName(
not_null<PeerData*> peer,
bool shorten) const {
const auto user = shorten ? peer->asUser() : nullptr;
- return user ? user->firstName : peer->name();
+ if (user) {
+ const auto firstName = Arcanegram::Streamer::ShouldAnonymize(peer)
+ ? Arcanegram::Streamer::GeneratedShortName(peer->id)
+ : user->firstName;
+ return firstName;
+ }
+ return peer->name();
}
bool Reply::isNameUpdated(
--
2.52.0.windows.1

View file

@ -0,0 +1,55 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 16:16:33 +0600
Subject: [PATCH] streamer mode: hide username in dialogs search rows
---
.../dialogs/dialogs_inner_widget.cpp | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
+++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp
@@ -68,6 +68,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/notifications_manager.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
+
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "window/window_peer_menu.h"
#include "ui/chat/chats_filter_tag.h"
#include "ui/effects/ripple_animation.h"
@@ -1610,7 +1612,9 @@ void InnerWidget::paintPeerSearchResult(
QRect tr(context.st->textLeft, context.st->textTop, namewidth, st::dialogsTextFont->height);
p.setFont(st::dialogsTextFont);
- QString username = peer->username();
+ QString username = Arcanegram::Streamer::ShouldAnonymize(peer)
+ ? QString()
+ : peer->username();
if (!context.active && username.startsWith(_peerSearchQuery, Qt::CaseInsensitive)) {
auto first = '@' + username.mid(0, _peerSearchQuery.size());
auto second = username.mid(_peerSearchQuery.size());
@@ -1755,11 +1759,13 @@ void InnerWidget::performDrag() {
u"application/x-telegram-dialog"_q,
std::move(byteArray));
- if (const auto u = history->peer->username(); !u.isEmpty()) {
- mimeData->setText(history->peer->session().createInternalLinkFull(u));
- mimeData->setData(
- u"application/x-telegram-input-field"_q,
- ('@' + u).toUtf8());
+ if (!Arcanegram::Streamer::ShouldAnonymize(history->peer)) {
+ if (const auto u = history->peer->username(); !u.isEmpty()) {
+ mimeData->setText(history->peer->session().createInternalLinkFull(u));
+ mimeData->setData(
+ u"application/x-telegram-input-field"_q,
+ ('@' + u).toUtf8());
+ }
}
const auto &st = st::defaultDialogRow;
--
2.52.0.windows.1

View file

@ -0,0 +1,29 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 17:13:09 +0600
Subject: [PATCH] streamer mode: force empty userpic in
PeerData::GenerateUserpicImage
---
Telegram/SourceFiles/data/data_peer.cpp | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -518,7 +518,10 @@ QImage PeerData::GenerateUserpicImage(
Ui::PeerUserpicView &view,
int size,
std::optional<int> radius) {
- if (const auto userpic = peer->userpicCloudImage(view)) {
+ const auto userpic = Arcanegram::Streamer::ShouldAnonymize(peer)
+ ? nullptr
+ : peer->userpicCloudImage(view);
+ if (userpic) {
auto image = userpic->scaled(
{ size, size },
Qt::IgnoreAspectRatio,
--
2.52.0.windows.1

View file

@ -0,0 +1,35 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 15:58:05 +0600
Subject: [PATCH] streamer mode: skip photo carousel in profile preview
---
Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
+++ b/Telegram/SourceFiles/boxes/peers/prepare_short_info_box.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peers/prepare_short_info_box.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "base/unixtime.h"
#include "boxes/peers/peer_short_info_box.h"
#include "core/application.h"
@@ -323,7 +324,9 @@ void ValidatePhotoId(
bool ProcessCurrent(
not_null<PeerData*> peer,
not_null<UserpicState*> state) {
- const auto userpicPhotoId = peer->userpicPhotoId();
+ const auto userpicPhotoId = Arcanegram::Streamer::ShouldAnonymize(peer)
+ ? PhotoId{}
+ : peer->userpicPhotoId();
const auto userpicPhoto = (userpicPhotoId
&& (userpicPhotoId != PeerData::kUnknownPhotoId)
&& (state->userpicPhotoId != userpicPhotoId))
--
2.52.0.windows.1

View file

@ -0,0 +1,70 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 16:03:10 +0600
Subject: [PATCH] streamer mode: strip name color and decoration
---
Telegram/SourceFiles/data/data_peer.cpp | 16 ++++++++++++++++
Telegram/SourceFiles/data/data_peer.h | 4 +---
2 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/Telegram/SourceFiles/data/data_peer.cpp b/Telegram/SourceFiles/data/data_peer.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer.cpp
+++ b/Telegram/SourceFiles/data/data_peer.cpp
@@ -1444,7 +1444,17 @@ bool PeerData::clearColorIndex() {
return true;
}
+uint8 PeerData::colorIndex() const {
+ if (Arcanegram::Streamer::ShouldAnonymize(this)) {
+ return Arcanegram::Streamer::ForcedColorIndex(id);
+ }
+ return _colorIndex;
+}
+
DocumentId PeerData::backgroundEmojiId() const {
+ if (Arcanegram::Streamer::ShouldAnonymize(this)) {
+ return DocumentId(0);
+ }
return _backgroundEmojiId;
}
@@ -1505,6 +1515,9 @@ bool PeerData::clearColorProfileIndex() {
}
DocumentId PeerData::profileBackgroundEmojiId() const {
+ if (Arcanegram::Streamer::ShouldAnonymize(this)) {
+ return DocumentId(0);
+ }
return _profileBackgroundEmojiId;
}
@@ -1530,6 +1543,9 @@ void PeerData::setEmojiStatus(EmojiStatusId emojiStatusId, TimeId until) {
}
EmojiStatusId PeerData::emojiStatusId() const {
+ if (Arcanegram::Streamer::ShouldAnonymize(this)) {
+ return EmojiStatusId();
+ }
return _emojiStatusId;
}
diff --git a/Telegram/SourceFiles/data/data_peer.h b/Telegram/SourceFiles/data/data_peer.h
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/data/data_peer.h
+++ b/Telegram/SourceFiles/data/data_peer.h
@@ -215,9 +215,7 @@ public:
[[nodiscard]] Main::Session &session() const;
[[nodiscard]] Main::Account &account() const;
- [[nodiscard]] uint8 colorIndex() const {
- return _colorIndex;
- }
+ [[nodiscard]] uint8 colorIndex() const;
[[nodiscard]] auto colorCollectible() const
-> const std::shared_ptr<Ui::ColorCollectible> & {
return _colorCollectible;
--
2.52.0.windows.1

View file

@ -0,0 +1,87 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: devilreef <devilreef@femboy.page>
Date: Fri, 1 May 2026 15:50:04 +0600
Subject: [PATCH] streamer mode: pseudonym in typing indicator
---
.../history/view/history_view_send_action.cpp | 22 +++++++++++++------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/Telegram/SourceFiles/history/view/history_view_send_action.cpp b/Telegram/SourceFiles/history/view/history_view_send_action.cpp
index 0000000..0000000 100644
--- a/Telegram/SourceFiles/history/view/history_view_send_action.cpp
+++ b/Telegram/SourceFiles/history/view/history_view_send_action.cpp
@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "history/view/history_view_send_action.h"
+#include "arcanegram/features/streamer/ag_streamer.h"
#include "data/data_user.h"
#include "data/data_send_action.h"
#include "data/data_forum_topic.h"
@@ -38,6 +39,13 @@ constexpr auto kStatusShowClientsideChooseSticker = 6 * crl::time(1000);
constexpr auto kStatusShowClientsidePlayGame = 10 * crl::time(1000);
constexpr auto kStatusShowClientsideSpeaking = 6 * crl::time(1000);
+[[nodiscard]] QString DisplayFirstName(not_null<UserData*> user) {
+ if (Arcanegram::Streamer::ShouldAnonymize(user)) {
+ return Arcanegram::Streamer::GeneratedShortName(user->id);
+ }
+ return user->firstName;
+}
+
} // namespace
SendActionPainter::SendActionPainter(
@@ -243,16 +251,16 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
newTypingString = tr::lng_users_typing(
tr::now,
lt_user,
- begin(_typing)->first->firstName,
+ DisplayFirstName(begin(_typing)->first),
lt_second_user,
- (end(_typing) - 1)->first->firstName);
+ DisplayFirstName((end(_typing) - 1)->first));
} else if (typingCount) {
newTypingString = _history->peer->isUser()
? tr::lng_typing(tr::now)
: tr::lng_user_typing(
tr::now,
lt_user,
- begin(_typing)->first->firstName);
+ DisplayFirstName(begin(_typing)->first));
} else if (!_sendActions.empty()) {
// Handles all actions except game playing.
using Type = Api::SendProgressType;
@@ -299,7 +307,7 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
const auto isNamed = !_history->peer->isUser();
newTypingString = sendActionString(
action.type,
- isNamed ? user->firstName : QString());
+ isNamed ? DisplayFirstName(user) : QString());
if (!newTypingString.isEmpty()) {
_sendActionAnimation.start(action.type);
@@ -345,16 +353,16 @@ bool SendActionPainter::updateNeedsAnimating(crl::time now, bool force) {
newTypingString = tr::lng_users_playing_game(
tr::now,
lt_user,
- begin(_sendActions)->first->firstName,
+ DisplayFirstName(begin(_sendActions)->first),
lt_second_user,
- (end(_sendActions) - 1)->first->firstName);
+ DisplayFirstName((end(_sendActions) - 1)->first));
} else {
newTypingString = _history->peer->isUser()
? tr::lng_playing_game(tr::now)
: tr::lng_user_playing_game(
tr::now,
lt_user,
- begin(_sendActions)->first->firstName);
+ DisplayFirstName(begin(_sendActions)->first));
}
_sendActionAnimation.start(Type::PlayGame);
}
--
2.52.0.windows.1

View file

@ -32,3 +32,10 @@
"ag_sync_status_offline" = "Offline";
"ag_sync_status_offline_pending" = "Offline · {n} pending";
"ag_sync_status_conflict" = "Resolving conflict…";
"ag_streamer_title" = "Streamer Mode";
"ag_streamer_subtitle" = "Hide names, avatars and identifiers when screen-sharing.";
"ag_streamer_enabled" = "Anonymize other users";
"ag_streamer_enabled_about" = "Replaces every other user's name with a generated pseudonym, removes their avatar, color and badges, and hides phone, username, ID, bio, birthday and last-seen.";
"ag_streamer_anonymize_bots" = "Anonymize bots";
"ag_streamer_anonymize_bots_about" = "Bots are exposed by default since they are usually intentional. Enable to also pseudonymize them.";
"ag_streamer_preview" = "Sample: {name}";

16
series
View file

@ -23,4 +23,20 @@ feature/peer-id-bot-api-format.patch
feature/fast-messages-shortcuts.patch
feature/fast-messages-hook.patch
misc/app-icon.patch
feature/streamer-mode-name.patch
feature/streamer-mode-typing.patch
feature/streamer-mode-reply-shorten.patch
feature/streamer-mode-avatar.patch
feature/streamer-mode-call-avatar.patch
feature/streamer-mode-short-info-box.patch
feature/streamer-mode-style.patch
feature/streamer-mode-badge.patch
feature/streamer-mode-profile-identity.patch
feature/streamer-mode-profile-actions.patch
feature/streamer-mode-autocomplete.patch
feature/streamer-mode-search.patch
feature/streamer-mode-forum-topic.patch
feature/streamer-mode-rank.patch
feature/streamer-mode-seen-avatars.patch
feature/streamer-mode-rating.patch
misc/branding.patch

View file

@ -56,6 +56,9 @@ PRIVATE
sync/ag_sync_keys.cpp
sync/ag_sync_engine.h
sync/ag_sync_engine.cpp
features/streamer/ag_streamer.cpp
features/streamer/ag_streamer.h
features/streamer/streamer_names.h
)
target_include_directories(arcanegram PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..)

View file

@ -57,6 +57,13 @@ inline auto Disabled = BoolItem("ag_chat_wallpaper_disabled", false);
} // namespace ChatWallpaper
namespace StreamerMode {
inline auto Enabled = BoolItem("ag_streamer_enabled", false);
inline auto AnonymizeBots = BoolItem("ag_streamer_anonymize_bots", false);
} // namespace StreamerMode
namespace Sync {
inline auto Enabled = BoolItem("ag_sync_enabled", false);

View file

@ -5,6 +5,7 @@
#include "arcanegram/features/ag_hidden_users.h"
#include "arcanegram/features/ag_chat_wallpaper.h"
#include "arcanegram/features/ag_fast_messages.h"
#include "arcanegram/features/streamer/ag_streamer.h"
#include "arcanegram/sync/ag_sync_engine.h"
namespace Arcanegram::Hooks {
@ -16,6 +17,7 @@ void init() {
Arcanegram::ChatWallpaper::Init();
Arcanegram::Sync::Init();
Arcanegram::FastMessages::Init();
Arcanegram::Streamer::Init();
}
} // namespace Arcanegram::Hooks

View file

@ -3,6 +3,8 @@
#include "core/application.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_forum.h"
#include "data/data_forum_topic.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "data/data_user.h"
@ -14,7 +16,6 @@
#include "main/main_session.h"
namespace Arcanegram {
namespace {
void ForEachLoadedHistory(Fn<void(not_null<History*>)> action) {
for (const auto &entry : Core::App().domain().accounts()) {
@ -40,7 +41,32 @@ void ForEachLoadedHistory(Fn<void(not_null<History*>)> action) {
}
}
} // namespace
void ForEachLoadedTopic(Fn<void(not_null<Data::ForumTopic*>)> action) {
for (const auto &entry : Core::App().domain().accounts()) {
const auto session = entry.account->maybeSession();
if (!session) {
continue;
}
auto &owner = session->data();
owner.enumerateBroadcasts([&](not_null<ChannelData*> p) {
if (const auto forum = p->forum()) {
forum->enumerateTopics([&](not_null<Data::ForumTopic*> t) {
action(t);
});
}
});
owner.enumerateGroups([&](not_null<PeerData*> p) {
if (const auto channel = p->asChannel()) {
if (const auto forum = channel->forum()) {
forum->enumerateTopics(
[&](not_null<Data::ForumTopic*> t) {
action(t);
});
}
}
});
}
}
void ForEachLoadedItem(Fn<void(not_null<HistoryItem*>)> action) {
ForEachLoadedHistory([&](not_null<History*> h) {
@ -66,6 +92,25 @@ void ForEachLoadedHistoryFor(
}
}
void ForEachLoadedPeer(Fn<void(not_null<PeerData*>)> action) {
for (const auto &entry : Core::App().domain().accounts()) {
const auto session = entry.account->maybeSession();
if (!session) {
continue;
}
auto &owner = session->data();
owner.enumerateUsers([&](not_null<UserData*> p) {
action(p);
});
owner.enumerateGroups([&](not_null<PeerData*> p) {
action(p);
});
owner.enumerateBroadcasts([&](not_null<ChannelData*> p) {
action(p);
});
}
}
void RefreshAllItems() {
ForEachLoadedHistory([](not_null<History*> h) {
auto &owner = h->owner();

View file

@ -4,6 +4,11 @@
class History;
class HistoryItem;
class PeerData;
namespace Data {
class ForumTopic;
} // namespace Data
namespace Arcanegram {
@ -11,6 +16,12 @@ void ForEachLoadedItem(Fn<void(not_null<HistoryItem*>)> action);
void ForEachLoadedHistoryFor(PeerId id, Fn<void(not_null<History*>)> action);
void ForEachLoadedPeer(Fn<void(not_null<PeerData*>)> action);
void ForEachLoadedTopic(Fn<void(not_null<Data::ForumTopic*>)> action);
void ForEachLoadedHistory(Fn<void(not_null<History*>)> action);
void RefreshAllItems();
} // namespace Arcanegram

View file

@ -0,0 +1,6 @@
Adjective and surname lists vendored from the Moby project
(github.com/moby/moby), file pkg/namesgenerator/names-generator.go.
Copyright 2013-2025 Docker, Inc. and contributors.
Licensed under the Apache License, Version 2.0.
http://www.apache.org/licenses/LICENSE-2.0

View file

@ -0,0 +1,238 @@
#include "arcanegram/features/streamer/ag_streamer.h"
#include "arcanegram/features/streamer/streamer_names.h"
#include "arcanegram/ag_config.h"
#include "arcanegram/ag_refresh.h"
#include "arcanegram/ui/ag_settings_main.h"
#include "arcanegram/ui/ag_settings_widgets.h"
#include "data/data_changes.h"
#include "data/data_forum_topic.h"
#include "data/data_peer.h"
#include "data/data_user.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "settings/settings_builder.h"
#include "settings/settings_common_session.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
#include "ui/vertical_list.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/vertical_layout.h"
#include "window/window_session_controller.h"
#include <QtCore/QRandomGenerator>
namespace Arcanegram::Streamer {
namespace {
quint64 &Seed() {
static quint64 value = 0;
return value;
}
rpl::event_stream<bool> &Stream() {
static rpl::event_stream<bool> value;
return value;
}
rpl::lifetime &Lifetime() {
static rpl::lifetime value;
return value;
}
QString Capitalize(std::string_view sv) {
auto s = QString::fromUtf8(sv.data(), int(sv.size()));
if (!s.isEmpty()) {
s[0] = s[0].toUpper();
}
return s;
}
void RefreshAll() {
using Flag = Data::PeerUpdate::Flag;
const auto flags = Flag::Name
| Flag::Username
| Flag::Usernames
| Flag::Photo
| Flag::About
| Flag::Color
| Flag::EmojiStatus;
ForEachLoadedPeer([&](not_null<PeerData*> peer) {
peer->invalidateEmptyUserpic();
// bump _nameVersion so dialog row name cache rebuilds.
peer->noteNameUpdated();
peer->session().changes().peerUpdated(peer, flags);
});
ForEachLoadedTopic([&](not_null<Data::ForumTopic*> topic) {
topic->invalidateTitleWithIcon();
topic->session().changes().topicUpdated(
topic,
Data::TopicUpdate::Flag::Title);
if (const auto last = topic->chatListMessage()) {
last->invalidateChatListEntry();
}
topic->updateChatListEntry();
});
ForEachLoadedHistory([&](not_null<History*> h) {
// drop sender prefix cache in dialog list message preview.
if (const auto last = h->chatListMessage()) {
last->invalidateChatListEntry();
}
h->updateChatListEntry();
});
RefreshAllItems();
}
} // namespace
void Init() {
Seed() = QRandomGenerator::global()->generate64();
Config::StreamerMode::Enabled.changes(
) | rpl::on_next([](bool active) {
Stream().fire_copy(active);
RefreshAll();
}, Lifetime());
Config::StreamerMode::AnonymizeBots.changes(
) | rpl::on_next([](bool) {
if (IsActive()) {
RefreshAll();
}
}, Lifetime());
}
bool IsActive() {
return Config::StreamerMode::Enabled.value();
}
bool ShouldAnonymize(not_null<const PeerData*> peer) {
if (!IsActive()) {
return false;
}
if (peer->isSelf()) {
return false;
}
if (const auto user = peer->asUser()) {
if (user->isBot() && !Config::StreamerMode::AnonymizeBots.value()) {
return false;
}
return true;
}
return peer->isChannel() || peer->isChat();
}
QString GeneratedName(PeerId id) {
const auto mix = Seed() ^ quint64(id.value);
const auto a = mix * 0x9E3779B97F4A7C15ull;
const auto b = (mix >> 17) * 0xBF58476D1CE4E5B9ull;
const auto adj = kAdjectives[a % kAdjectives.size()];
const auto sur = kSurnames[b % kSurnames.size()];
return Capitalize(adj) + QChar(' ') + Capitalize(sur);
}
QString GeneratedShortName(PeerId id) {
const auto mix = Seed() ^ quint64(id.value);
const auto a = mix * 0x9E3779B97F4A7C15ull;
const auto b = (mix >> 17) * 0xBF58476D1CE4E5B9ull;
return Capitalize(kAdjectives[(a ^ b) % kAdjectives.size()]);
}
uint8 ForcedColorIndex(PeerId id) {
const auto mix = Seed() ^ quint64(id.value);
const auto a = mix * 0x9E3779B97F4A7C15ull;
const auto b = (mix >> 17) * 0xBF58476D1CE4E5B9ull;
// mod 7: standard non-premium palette has 7 hues; extended indices fall
// back to standard for free accounts, so 0..6 is uniformly available.
return uint8((a ^ b) % 7);
}
rpl::producer<bool> Changes() {
return Stream().events();
}
namespace {
void BuildPage(::Settings::Builder::SectionBuilder &builder) {
const auto container = builder.container();
Ui::AddSubsectionTitle(container, tr::ag_streamer_title());
Arcanegram::Settings::AddBoolRow(
builder,
u"arcanegram/streamer/enabled"_q,
tr::ag_streamer_enabled(),
tr::ag_streamer_enabled_about(),
Config::StreamerMode::Enabled);
Arcanegram::Settings::AddBoolRow(
builder,
u"arcanegram/streamer/anonymize_bots"_q,
tr::ag_streamer_anonymize_bots(),
tr::ag_streamer_anonymize_bots_about(),
Config::StreamerMode::AnonymizeBots);
Ui::AddSkip(container);
Ui::AddDivider(container);
Ui::AddSkip(container);
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::ag_streamer_preview(
lt_name,
rpl::single(GeneratedName(PeerId(1234567890ULL)))),
st::boxDividerLabel),
st::defaultBoxDividerLabelPadding);
Ui::AddDividerText(container, tr::ag_streamer_subtitle());
}
class Page : public ::Settings::Section<Page> {
public:
Page(QWidget *parent, not_null<Window::SessionController*> controller);
[[nodiscard]] rpl::producer<QString> title() override {
return tr::ag_streamer_title();
}
private:
void setupContent();
};
const auto kPageMeta = ::Settings::Builder::BuildHelper({
.id = Page::Id(),
.parentId = Arcanegram::Settings::Id(),
.title = &tr::ag_streamer_title,
.icon = &st::menuIconStealthLocked,
}, [](::Settings::Builder::SectionBuilder &builder) {
BuildPage(builder);
});
const ::Settings::SectionBuildMethod kPageSection = kPageMeta.build;
Page::Page(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent, controller) {
setupContent();
}
void Page::setupContent() {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
build(content, kPageSection);
Ui::ResizeFitChild(this, content);
}
} // namespace
void Setup(::Settings::Builder::SectionBuilder &builder) {
builder.addSectionButton({
.title = tr::ag_streamer_title(),
.targetSection = Page::Id(),
.icon = { &st::menuIconStealthLocked },
});
}
} // namespace Arcanegram::Streamer

View file

@ -0,0 +1,27 @@
#pragma once
#include "base/basic_types.h"
#include <rpl/producer.h>
class PeerData;
namespace Settings::Builder { class SectionBuilder; }
namespace Arcanegram::Streamer {
void Init();
[[nodiscard]] bool IsActive();
[[nodiscard]] bool ShouldAnonymize(not_null<const PeerData*> peer);
[[nodiscard]] QString GeneratedName(PeerId id);
[[nodiscard]] QString GeneratedShortName(PeerId id);
[[nodiscard]] uint8 ForcedColorIndex(PeerId id);
[[nodiscard]] rpl::producer<bool> Changes();
void Setup(::Settings::Builder::SectionBuilder &builder);
} // namespace Arcanegram::Streamer

View file

@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// adjective + surname lists vendored from docker/moby pkg/namesgenerator.
// original copyright: docker inc. and contributors.
// see NOTICE.txt for license text.
#pragma once
#include <array>
#include <string_view>
namespace Arcanegram::Streamer {
constexpr std::array<std::string_view, 108> kAdjectives = {{
"admiring", "adoring", "affectionate", "agitated", "amazing", "angry",
"awesome", "beautiful", "blissful", "bold", "boring", "brave", "busy",
"charming", "clever", "compassionate", "competent", "condescending",
"confident", "cool", "cranky", "crazy", "dazzling", "determined",
"distracted", "dreamy", "eager", "ecstatic", "elastic", "elated",
"elegant", "eloquent", "epic", "exciting", "fervent", "festive",
"flamboyant", "focused", "friendly", "frosty", "funny", "gallant",
"gifted", "goofy", "gracious", "great", "happy", "hardcore", "heuristic",
"hopeful", "hungry", "infallible", "inspiring", "intelligent",
"interesting", "jolly", "jovial", "keen", "kind", "laughing", "loving",
"lucid", "magical", "modest", "musing", "mystifying", "naughty",
"nervous", "nice", "nifty", "nostalgic", "objective", "optimistic",
"peaceful", "pedantic", "pensive", "practical", "priceless", "quirky",
"quizzical", "recursing", "relaxed", "reverent", "romantic", "sad",
"serene", "sharp", "silly", "sleepy", "stoic", "strange", "stupefied",
"suspicious", "sweet", "tender", "thirsty", "trusting", "unruffled",
"upbeat", "vibrant", "vigilant", "vigorous", "wizardly", "wonderful",
"xenodochial", "youthful", "zealous", "zen",
}};
constexpr std::array<std::string_view, 238> kSurnames = {{
"agnesi", "albattani", "allen", "almeida", "antonelli", "archimedes",
"ardinghelli", "aryabhata", "austin", "babbage", "banach", "banzai",
"bardeen", "bartik", "bassi", "beaver", "bell", "benz", "bhabha",
"bhaskara", "black", "blackburn", "blackwell", "bohr", "booth", "borg",
"bose", "bouman", "boyd", "brahmagupta", "brattain", "brown", "buck",
"burnell", "cannon", "carson", "cartwright", "carver", "cerf",
"chandrasekhar", "chaplygin", "chatelet", "chatterjee", "chaum",
"chebyshev", "clarke", "cohen", "colden", "cori", "cray", "curie",
"curran", "darwin", "davinci", "dewdney", "dhawan", "diffie",
"dijkstra", "dirac", "driscoll", "dubinsky", "easley", "edison",
"einstein", "elbakyan", "elgamal", "elion", "ellis", "engelbart",
"euclid", "euler", "faraday", "feistel", "fermat", "fermi",
"feynman", "franklin", "gagarin", "galileo", "galois", "ganguly",
"gates", "gauss", "germain", "goldberg", "goldstine", "goldwasser",
"golick", "goodall", "gould", "greider", "grothendieck", "haibt",
"hamilton", "haslett", "hawking", "hellman", "heisenberg", "hermann",
"herschel", "hertz", "heyrovsky", "hodgkin", "hofstadter", "hoover",
"hopper", "hugle", "hypatia", "ishizaka", "jackson", "jang",
"jemison", "jennings", "jepsen", "johnson", "joliot", "jones",
"kalam", "kapitsa", "kare", "keldysh", "keller", "kepler", "khayyam",
"khorana", "kilby", "kirch", "knuth", "kowalevski", "lalande",
"lamarr", "lamport", "leakey", "leavitt", "lederberg", "lehmann",
"lewin", "lichterman", "liskov", "lovelace", "lumiere", "mahavira",
"margulis", "matsumoto", "maxwell", "mayer", "mccarthy", "mcclintock",
"mclaren", "mclean", "mcnulty", "mendel", "mendeleev", "meitner",
"meninsky", "merkle", "mestorf", "mirzakhani", "moncada", "montalcini",
"moore", "morse", "moser", "murdock", "napier", "nash", "neumann",
"newton", "nightingale", "nobel", "noether", "northcutt", "noyce",
"panini", "pare", "pascal", "pasteur", "payne", "perlman", "pike",
"poincare", "poitras", "proskuriakova", "ptolemy", "raman",
"ramanujan", "rhodes", "ride", "ritchie", "robinson", "roentgen",
"rosalind", "rubin", "saha", "sammet", "sanderson", "satoshi",
"shamir", "shannon", "shaw", "shirley", "shockley", "shtern",
"sinoussi", "snyder", "solomon", "spence", "stonebraker", "sutherland",
"swanson", "swartz", "swirles", "taussig", "tereshkova", "tesla",
"tharp", "thompson", "torvalds", "tu", "turing", "varahamihira",
"vaughan", "villani", "visvesvaraya", "volhard", "wescoff", "wilbur",
"wiles", "williams", "williamson", "wilson", "wing", "wozniak",
"wright", "wu", "yalow", "yonath", "zhukovsky",
}};
} // namespace Arcanegram::Streamer

View file

@ -5,6 +5,7 @@
#include "arcanegram/features/ag_forwarded_header.h"
#include "arcanegram/features/ag_show_seconds.h"
#include "arcanegram/features/ag_sync_feature.h"
#include "arcanegram/features/streamer/ag_streamer.h"
#include "arcanegram/ui/ag_hidden_users_settings.h"
#include "lang/lang_keys.h"
#include "settings/sections/settings_main.h"
@ -82,6 +83,7 @@ void BuildContent(SectionBuilder &builder) {
.icon = { &st::menuIconPalette },
});
Arcanegram::HiddenUsers::Setup(builder);
Arcanegram::Streamer::Setup(builder);
Arcanegram::FastMessages::Setup(builder);
Arcanegram::CloudSync::Setup(builder);
}