diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java index c8266fb4..e4e66868 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileActivity.java @@ -11359,6 +11359,9 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter. if (avatarContainer == null || nameTextView == null || getParentActivity() == null) { return; } + if (userId != 0) { + SponsorHelper.requestSponsorStatus(userId); + } String onlineTextOverride; int currentConnectionState = getConnectionsManager().getConnectionState(); if (currentConnectionState == ConnectionsManager.ConnectionStateWaitingForNetwork) { @@ -11568,7 +11571,7 @@ 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)) { + } else if (SponsorHelper.isKnownSponsor(user.id)) { rightIconIsSponsor = true; nameTextView[a].setRightDrawable2(getFoxSponsorDrawable(a)); nameTextViewRightDrawable2ContentDescription = LocaleController.getString(R.string.FoxSponsorBadge); @@ -11595,7 +11598,7 @@ 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)) { + } else if (SponsorHelper.isKnownSponsor(user.id)) { rightIconIsSponsor = true; nameTextView[a].setRightDrawable2(getFoxSponsorDrawable(a)); } else { diff --git a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java index db03bfe8..0f53a190 100644 --- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java +++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java @@ -15,7 +15,11 @@ import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; /** * Resolves the "sponsor" status of the current Telegram user against the @@ -43,6 +47,16 @@ public final class SponsorHelper { private static volatile boolean checking = false; + // ─── Per-user sponsor cache (for showing badges of OTHER users) ─── + // Cached sponsor flag per Telegram id, with a freshness timestamp. + private static final ConcurrentHashMap otherSponsor = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap otherChecked = new ConcurrentHashMap<>(); + // Ids waiting to be resolved in the next batch request. + private static final Set pendingLookup = new LinkedHashSet<>(); + private static volatile boolean lookupScheduled = false; + // Re-check another user's status at most once every 6 hours. + private static final long OTHER_TTL_MS = 6 * 60 * 60 * 1000L; + private static SharedPreferences prefs() { return ApplicationLoader.applicationContext.getSharedPreferences(PREFS, android.content.Context.MODE_PRIVATE); } @@ -61,6 +75,144 @@ public final class SponsorHelper { return user != null && isSponsor(user.id); } + /** + * Sponsor flag for ANY Telegram user id (not just the current account). + * Combines the locally-stored value for the current account with the + * per-user cache populated by {@link #requestSponsorStatus(long)}. Returns + * the best known answer synchronously; callers should also invoke + * {@link #requestSponsorStatus(long)} to keep the cache warm and get a + * {@link NotificationCenter#sponsorStatusUpdated} when it changes. + */ + public static boolean isKnownSponsor(long telegramId) { + if (telegramId == 0) { + return false; + } + // The current account's own status is authoritative from /mobile/me. + var self = UserConfig.getInstance(UserConfig.selectedAccount).getCurrentUser(); + if (self != null && self.id == telegramId) { + return isSponsor(telegramId); + } + Boolean cached = otherSponsor.get(telegramId); + return cached != null && cached; + } + + /** + * Ask the backend whether {@code telegramId} is a sponsor. Self-throttles + * per id and batches concurrent requests. Posts + * {@link NotificationCenter#sponsorStatusUpdated} when a value changes so + * the UI can redraw the badge. + */ + public static void requestSponsorStatus(long telegramId) { + if (telegramId == 0) { + return; + } + var self = UserConfig.getInstance(UserConfig.selectedAccount).getCurrentUser(); + if (self != null && self.id == telegramId) { + return; // covered by refresh()/mobile-me + } + Long last = otherChecked.get(telegramId); + if (last != null && Math.abs(System.currentTimeMillis() - last) < OTHER_TTL_MS) { + return; + } + synchronized (pendingLookup) { + pendingLookup.add(telegramId); + if (lookupScheduled) { + return; + } + lookupScheduled = true; + } + // Small delay so several visible rows coalesce into one request. + AndroidUtilities.runOnUIThread(() -> Utilities.globalQueue.postRunnable(SponsorHelper::flushLookup), 250); + } + + // @WorkerThread + private static void flushLookup() { + List batch; + synchronized (pendingLookup) { + lookupScheduled = false; + if (pendingLookup.isEmpty()) { + return; + } + batch = new ArrayList<>(pendingLookup); + pendingLookup.clear(); + } + if (batch.size() > 200) { + batch = batch.subList(0, 200); + } + StringBuilder ids = new StringBuilder(); + for (int i = 0; i < batch.size(); i++) { + if (i > 0) { + ids.append(','); + } + ids.append(batch.get(i)); + } + + HttpURLConnection connection = null; + try { + URL url = new URL(BASE_URL + "/api/auth/mobile/sponsors/by-telegram?ids=" + ids); + connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(true); + connection.setConnectTimeout(15000); + connection.setReadTimeout(15000); + connection.setRequestProperty("Accept", "application/json"); + + int code = connection.getResponseCode(); + if (code != HttpURLConnection.HTTP_OK) { + FileLog.d(TAG + ": sponsors/by-telegram returned HTTP " + code); + 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; + } + Set sponsors = new HashSet<>(); + JSONArray arr = json.optJSONArray("sponsors"); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + sponsors.add(arr.optLong(i)); + } + } + + boolean changed = false; + long now = System.currentTimeMillis(); + for (Long id : batch) { + boolean isSponsor = sponsors.contains(id); + Boolean prev = otherSponsor.put(id, isSponsor); + otherChecked.put(id, now); + if (prev == null || prev != isSponsor) { + changed = true; + } + } + if (changed) { + AndroidUtilities.runOnUIThread(() -> + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.sponsorStatusUpdated)); + } + } catch (Exception e) { + FileLog.e(e); + } finally { + if (connection != null) { + connection.disconnect(); + } + // If more ids queued up while we were busy, schedule another pass. + synchronized (pendingLookup) { + if (!pendingLookup.isEmpty() && !lookupScheduled) { + lookupScheduled = true; + Utilities.globalQueue.postRunnable(SponsorHelper::flushLookup, 250); + } + } + } + } + /** * Whether the given Telegram id is linked to a FoxCloud/GhostCloud account. * Only meaningful after at least one successful {@link #refresh(boolean)}. diff --git a/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher.png b/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher.png index b499d04e..9837feb9 100644 Binary files a/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher.png and b/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index c85afb44..3daff98c 100644 Binary files a/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/TMessagesProj/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher.png b/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher.png index a46ef992..3993e997 100644 Binary files a/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher.png and b/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index 1b56a735..56f06a53 100644 Binary files a/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/TMessagesProj/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher.png b/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher.png index 42e3a6d5..1479e06c 100644 Binary files a/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher.png and b/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 998ad28b..4ad5f5b6 100644 Binary files a/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/TMessagesProj/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher.png b/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher.png index c838c49f..fc490135 100644 Binary files a/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index aa814640..d961d1a9 100644 Binary files a/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/TMessagesProj/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 9ba4b7a7..f1a59b12 100644 Binary files a/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index b1547372..273cd87b 100644 Binary files a/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/TMessagesProj/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ