Sponsor badges for everyone + new app icon

- Sponsor checkmark now shows on ANY user's profile, not just your own
- SponsorHelper batches sponsor-status lookups by Telegram id (cached 6h)
  and posts sponsorStatusUpdated so the badge redraws when resolved
- ProfileActivity requests the lookup when opening a profile
- Replace the main launcher icon with foxigram4.png (all densities,
  legacy + adaptive foreground)
This commit is contained in:
instant992 2026-06-10 00:13:37 +04:00
parent b4c3d9fbd6
commit 695de54a0a
12 changed files with 157 additions and 2 deletions

View file

@ -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 {

View file

@ -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<Long, Boolean> otherSponsor = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<Long, Long> otherChecked = new ConcurrentHashMap<>();
// Ids waiting to be resolved in the next batch request.
private static final Set<Long> 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<Long> 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<Long> 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)}.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 211 KiB