Add sponsor badge next to own name on profile

- SponsorHelper queries FoxCloud /mobile/me by Telegram ID (X-Tg-Id) and caches is_sponsor per account
- Self profile shows a sponsor badge next to the name when is_sponsor is true
- Reactive refresh via new NotificationCenter.sponsorStatusUpdated global event

Verified: TMessagesProj compileReleaseJavaWithJavac succeeds.
This commit is contained in:
instant992 2026-06-09 04:34:19 +04:00
parent d200a290f8
commit 51bde550b9
5 changed files with 185 additions and 0 deletions

View file

@ -279,6 +279,7 @@ public class NotificationCenter {
public static final int updatedChatRanks = totalEvents++;
public static final int joinedGroup = totalEvents++;
public static final int loadedAiComposeTones = totalEvents++;
public static final int sponsorStatusUpdated = totalEvents++;
//global
public static final int pushMessagesUpdated = totalEvents++;

View file

@ -341,6 +341,7 @@ import tw.nekomimi.nekogram.BackButtonMenuRecent;
import tw.nekomimi.nekogram.NekoConfig;
import tw.nekomimi.nekogram.SimpleTextViewSwitcher;
import tw.nekomimi.nekogram.helpers.PopupHelper;
import tw.nekomimi.nekogram.helpers.SponsorHelper;
import tw.nekomimi.nekogram.helpers.remote.ConfigHelper;
import tw.nekomimi.nekogram.settings.NekoSettingsActivity;
import tw.nekomimi.nekogram.translator.Translator;
@ -375,6 +376,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
private final Drawable[] verifiedCheckDrawable = new Drawable[2];
private final CrossfadeDrawable[] verifiedCrossfadeDrawable = new CrossfadeDrawable[2];
private final CrossfadeDrawable[] premiumCrossfadeDrawable = new CrossfadeDrawable[2];
private final Drawable[] foxSponsorDrawable = new Drawable[2];
private ScamDrawable scamDrawable;
private UndoView undoView;
private OverlaysView overlaysView;
@ -2261,6 +2263,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
getNotificationCenter().addObserver(this, NotificationCenter.profileMusicUpdated);
getNotificationCenter().addObserver(this, NotificationCenter.updatedChatRanks);
NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.emojiLoaded);
NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.sponsorStatusUpdated);
if (userId == getUserConfig().getClientUserId() || myProfile) {
SponsorHelper.refresh(false);
}
updateRowsIds();
if (listAdapter != null) {
listAdapter.notifyDataSetChanged();
@ -2402,6 +2408,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
getNotificationCenter().removeObserver(this, NotificationCenter.profileMusicUpdated);
getNotificationCenter().removeObserver(this, NotificationCenter.updatedChatRanks);
NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.emojiLoaded);
NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.sponsorStatusUpdated);
if (avatarsViewPager != null) {
avatarsViewPager.onDestroy();
}
@ -9185,6 +9192,10 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
public void didReceivedNotification(int id, int account, final Object... args) {
if (id == NotificationCenter.uploadStoryEnd || id == NotificationCenter.chatWasBoostedByUser) {
checkCanSendStoryForPosting();
} else if (id == NotificationCenter.sponsorStatusUpdated) {
if (userId != 0) {
updateProfileData(false);
}
} else if (id == NotificationCenter.updateInterfaces) {
int mask = (Integer) args[0];
boolean infoChanged = (mask & MessagesController.UPDATE_MASK_AVATAR) != 0 || (mask & MessagesController.UPDATE_MASK_NAME) != 0 || (mask & MessagesController.UPDATE_MASK_STATUS) != 0 || (mask & MessagesController.UPDATE_MASK_EMOJI_STATUS) != 0;
@ -11167,6 +11178,19 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
return verifiedCrossfadeDrawable[a];
}
private Drawable getFoxSponsorDrawable(int a) {
if (foxSponsorDrawable[a] == null) {
Drawable d = ContextCompat.getDrawable(getParentActivity(), R.drawable.foxsponsor_badge).mutate();
int color = getThemedColor(Theme.key_profile_verifiedBackground);
if (a == 1) {
color = dontApplyPeerColor(color);
}
d.setColorFilter(color, PorterDuff.Mode.SRC_IN);
foxSponsorDrawable[a] = d;
}
return foxSponsorDrawable[a];
}
private Drawable getPremiumCrossfadeDrawable(int a) {
if (premiumCrossfadeDrawable[a] == null) {
premiumStarDrawable[a] = ContextCompat.getDrawable(getParentActivity(), R.drawable.msg_premium_liststar).mutate();
@ -11538,6 +11562,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
} else if (getMessagesController().isDialogMuted(dialogId != 0 ? dialogId : userId, topicId)) {
nameTextView[a].setRightDrawable2(getThemedDrawable(Theme.key_drawable_muteIconDrawable));
nameTextViewRightDrawable2ContentDescription = LocaleController.getString(R.string.NotificationsMuted);
} else if (user.self && SponsorHelper.isSponsor(user.id)) {
nameTextView[a].setRightDrawable2(getFoxSponsorDrawable(a));
nameTextViewRightDrawable2ContentDescription = LocaleController.getString(R.string.FoxSponsorBadge);
} else {
nameTextView[a].setRightDrawable2(null);
nameTextViewRightDrawable2ContentDescription = null;
@ -11561,6 +11588,8 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
nameTextView[a].setRightDrawable2(getScamDrawable(user.scam ? 0 : 1));
} else if (user.verified) {
nameTextView[a].setRightDrawable2(getVerifiedCrossfadeDrawable(a));
} else if (user.self && SponsorHelper.isSponsor(user.id)) {
nameTextView[a].setRightDrawable2(getFoxSponsorDrawable(a));
} else {
nameTextView[a].setRightDrawable2(null);
}

View file

@ -0,0 +1,145 @@
package tw.nekomimi.nekogram.helpers;
import android.content.SharedPreferences;
import org.json.JSONObject;
import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.ApplicationLoader;
import org.telegram.messenger.FileLog;
import org.telegram.messenger.NotificationCenter;
import org.telegram.messenger.UserConfig;
import org.telegram.messenger.Utilities;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Resolves the "sponsor" status of the current Telegram user against the
* FoxCloud backend and caches it locally.
*
* Identification is done purely by Telegram ID: the backend endpoint
* {@code GET /mobile/me} accepts an {@code X-Tg-Id} header and looks the user
* up by the Telegram account that was linked on the website / bot. No
* subscription URL or manual input is required.
*
* The result is cached per-account in shared preferences so the badge can be
* drawn synchronously while a fresh check runs in the background. When the
* cached value changes, {@link NotificationCenter#sponsorStatusUpdated} is
* posted on the global instance.
*/
public final class SponsorHelper {
private SponsorHelper() {}
private static final String TAG = "SponsorHelper";
private static final String BASE_URL = "https://vpnghost.space";
private static final String PREFS = "foxsponsor";
// Re-check at most once per hour while the value is fresh.
private static final long TTL_MS = 60 * 60 * 1000L;
private static volatile boolean checking = false;
private static SharedPreferences prefs() {
return ApplicationLoader.applicationContext.getSharedPreferences(PREFS, android.content.Context.MODE_PRIVATE);
}
/** Cached sponsor flag for the given Telegram user id. Defaults to false. */
public static boolean isSponsor(long telegramId) {
if (telegramId == 0) {
return false;
}
return prefs().getBoolean("sponsor_" + telegramId, false);
}
/** Convenience: sponsor flag for the currently selected account. */
public static boolean isCurrentUserSponsor() {
var user = UserConfig.getInstance(UserConfig.selectedAccount).getCurrentUser();
return user != null && isSponsor(user.id);
}
/**
* Kick off a background refresh of the sponsor status for the current
* account. Safe to call repeatedly: it self-throttles via {@link #TTL_MS}
* unless {@code force} is set.
*/
public static void refresh(boolean force) {
var user = UserConfig.getInstance(UserConfig.selectedAccount).getCurrentUser();
if (user == null || user.id == 0) {
return;
}
final long telegramId = user.id;
if (checking) {
return;
}
if (!force) {
long last = prefs().getLong("checked_" + telegramId, 0);
if (Math.abs(System.currentTimeMillis() - last) < TTL_MS) {
return;
}
}
checking = true;
Utilities.globalQueue.postRunnable(() -> {
try {
doCheck(telegramId);
} finally {
checking = false;
}
});
}
// @WorkerThread
private static void doCheck(long telegramId) {
HttpURLConnection connection = null;
try {
URL url = new URL(BASE_URL + "/mobile/me");
connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(true);
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
connection.setRequestProperty("X-Tg-Id", String.valueOf(telegramId));
connection.setRequestProperty("Accept", "application/json");
int code = connection.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
FileLog.d(TAG + ": /mobile/me returned HTTP " + code);
// Don't clobber a previously known value on transient errors.
return;
}
StringBuilder sb = new StringBuilder();
try (InputStream in = connection.getInputStream()) {
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) >= 0) {
sb.append(new String(buffer, 0, read, "UTF-8"));
}
}
JSONObject json = new JSONObject(sb.toString());
if (!json.optBoolean("success", false)) {
return;
}
boolean sponsor = json.optBoolean("is_sponsor", false);
store(telegramId, sponsor);
} catch (Exception e) {
FileLog.e(e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private static void store(long telegramId, boolean sponsor) {
boolean previous = prefs().getBoolean("sponsor_" + telegramId, false);
prefs().edit()
.putBoolean("sponsor_" + telegramId, sponsor)
.putLong("checked_" + telegramId, System.currentTimeMillis())
.apply();
if (previous != sponsor) {
AndroidUtilities.runOnUIThread(() ->
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.sponsorStatusUpdated));
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2L14.6,7.27L20.42,8.12L16.21,12.22L17.2,18.02L12,15.28L6.8,18.02L7.79,12.22L3.58,8.12L9.4,7.27L12,2Z" />
</vector>

View file

@ -149,6 +149,7 @@
<string name="ShareNekogram">Share FoxiGram...</string>
<string name="NekogramVersion">FoxiGram %1$s\nBased on Telegram %2$s\nDesigned by %3$s</string>
<string name="UpdateInstalling">Installing update...</string>
<string name="FoxSponsorBadge">Sponsor</string>
<string name="UpdateDownloading">Downloading update...</string>
<string name="UpdateInstallingNotification">A notification will be shown when the update completes.</string>
<string name="UpdateInstallingRelaunch">The app will relaunch when the update completes.</string>