diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java
index 21daa55e..e3a36353 100644
--- a/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java
+++ b/TMessagesProj/src/main/java/org/telegram/ui/Adapters/DialogsAdapter.java
@@ -56,6 +56,7 @@ import org.telegram.ui.Cells.DialogMeUrlCell;
import org.telegram.ui.Cells.DialogsEmptyCell;
import org.telegram.ui.Cells.DialogsHintCell;
import org.telegram.ui.Cells.DialogsRequestedEmptyCell;
+import org.telegram.ui.Cells.FoxAccountInfoCell;
import org.telegram.ui.Cells.GraySectionCell;
import org.telegram.ui.Cells.HeaderCell;
import org.telegram.ui.Cells.ProfileSearchCell;
@@ -82,6 +83,8 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
+import tw.nekomimi.nekogram.helpers.SponsorHelper;
+
public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements DialogCell.DialogCellDelegate {
public final static int VIEW_TYPE_DIALOG = 0,
VIEW_TYPE_FLICKER = 1,
@@ -104,7 +107,8 @@ public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements
VIEW_TYPE_STORIES = 18,
VIEW_TYPE_ARCHIVE_FULLSCREEN = 19,
VIEW_TYPE_GRAY_SECTION = 20,
- VIEW_TYPE_FORWARD_TO_STORIES_CELL = 21;
+ VIEW_TYPE_FORWARD_TO_STORIES_CELL = 21,
+ VIEW_TYPE_FOX_ACCOUNT_INFO = 22;
private Context mContext;
private ArchiveHintCell archiveHintCell;
@@ -172,6 +176,18 @@ public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements
isReordering = reordering;
}
+ private boolean shouldShowFoxAccountInfo() {
+ return dialogsType == DialogsActivity.DIALOGS_TYPE_DEFAULT
+ && folderId == 0
+ && !isOnlySelect
+ && !isTransitionSupport
+ && !collapsedView
+ && requestPeerType == null
+ && (parentFragment == null || !parentFragment.isReplyTo)
+ && getCurrentFilter() == null
+ && SponsorHelper.isCurrentUserLinked();
+ }
+
public int fixPosition(int position) {
if (hasChatlistHint) {
position--;
@@ -328,6 +344,8 @@ public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements
this.emptyType = viewTypeEmpty;
if (viewTypeEmpty == VIEW_TYPE_LAST_EMPTY) {
stableId = 1;
+ } else if (viewTypeEmpty == VIEW_TYPE_FOX_ACCOUNT_INFO) {
+ stableId = 7;
} else {
if (viewType == VIEW_TYPE_ARCHIVE_FULLSCREEN) {
stableId = 5;
@@ -574,7 +592,7 @@ public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements
return viewType != VIEW_TYPE_FLICKER && viewType != VIEW_TYPE_EMPTY && viewType != VIEW_TYPE_DIVIDER &&
viewType != VIEW_TYPE_SHADOW && viewType != VIEW_TYPE_HEADER &&
viewType != VIEW_TYPE_LAST_EMPTY && viewType != VIEW_TYPE_NEW_CHAT_HINT && viewType != VIEW_TYPE_CONTACTS_FLICKER &&
- viewType != VIEW_TYPE_REQUIREMENTS && viewType != VIEW_TYPE_REQUIRED_EMPTY && viewType != VIEW_TYPE_STORIES && viewType != VIEW_TYPE_ARCHIVE_FULLSCREEN && viewType != VIEW_TYPE_GRAY_SECTION;
+ viewType != VIEW_TYPE_REQUIREMENTS && viewType != VIEW_TYPE_REQUIRED_EMPTY && viewType != VIEW_TYPE_STORIES && viewType != VIEW_TYPE_ARCHIVE_FULLSCREEN && viewType != VIEW_TYPE_GRAY_SECTION && viewType != VIEW_TYPE_FOX_ACCOUNT_INFO;
}
@Override
@@ -765,6 +783,9 @@ public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements
case VIEW_TYPE_FOLDER_UPDATE_HINT:
view = new DialogsHintCell(mContext);
break;
+ case VIEW_TYPE_FOX_ACCOUNT_INFO:
+ view = new FoxAccountInfoCell(mContext);
+ break;
case VIEW_TYPE_STORIES: {
view = new View(mContext) {
@Override
@@ -1083,6 +1104,10 @@ public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements
}
break;
}
+ case VIEW_TYPE_FOX_ACCOUNT_INFO: {
+ ((FoxAccountInfoCell) holder.itemView).update();
+ break;
+ }
}
if (i >= dialogsCount + 1) {
holder.itemView.setAlpha(1f);
@@ -1475,6 +1500,10 @@ public class DialogsAdapter extends RecyclerListView.SelectionAdapter implements
return;
}
+ if (shouldShowFoxAccountInfo()) {
+ itemInternals.add(new ItemInternal(VIEW_TYPE_FOX_ACCOUNT_INFO));
+ }
+
if (!hasHints && dialogsType == 0 && folderId == 0 && messagesController.isDialogsEndReached(folderId) && !forceUpdatingContacts) {
if (messagesController.getAllFoldersDialogsCount() <= 10 && ContactsController.getInstance(currentAccount).doneLoadingContacts && !ContactsController.getInstance(currentAccount).contacts.isEmpty()) {
onlineContacts = new ArrayList<>(ContactsController.getInstance(currentAccount).contacts);
diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Cells/FoxAccountInfoCell.java b/TMessagesProj/src/main/java/org/telegram/ui/Cells/FoxAccountInfoCell.java
new file mode 100644
index 00000000..5ba4925f
--- /dev/null
+++ b/TMessagesProj/src/main/java/org/telegram/ui/Cells/FoxAccountInfoCell.java
@@ -0,0 +1,85 @@
+package org.telegram.ui.Cells;
+
+import android.content.Context;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import org.telegram.messenger.AndroidUtilities;
+import org.telegram.messenger.LocaleController;
+import org.telegram.messenger.R;
+import org.telegram.messenger.UserConfig;
+import org.telegram.ui.ActionBar.Theme;
+import org.telegram.ui.Components.LayoutHelper;
+
+import java.util.Locale;
+
+import tw.nekomimi.nekogram.helpers.SponsorHelper;
+
+/**
+ * A slim, non-clickable info row shown at the very top of the chats list for
+ * users who linked their Telegram account to GhostCloud/FoxCloud. Shows the
+ * account balance on the left and the subscription expiry on the right.
+ */
+public class FoxAccountInfoCell extends FrameLayout {
+
+ private final TextView balanceTextView;
+ private final TextView subTextView;
+
+ public FoxAccountInfoCell(Context context) {
+ super(context);
+
+ balanceTextView = new TextView(context);
+ balanceTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13);
+ balanceTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2));
+ balanceTextView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
+ balanceTextView.setSingleLine();
+ addView(balanceTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT,
+ Gravity.LEFT | Gravity.CENTER_VERTICAL, 16, 0, 8, 0));
+
+ subTextView = new TextView(context);
+ subTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13);
+ subTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2));
+ subTextView.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
+ subTextView.setSingleLine();
+ addView(subTextView, LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.MATCH_PARENT,
+ Gravity.RIGHT | Gravity.CENTER_VERTICAL, 8, 0, 16, 0));
+
+ update();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(30), MeasureSpec.EXACTLY));
+ }
+
+ public void update() {
+ var user = UserConfig.getInstance(UserConfig.selectedAccount).getCurrentUser();
+ long tgId = user != null ? user.id : 0;
+
+ long kopecks = SponsorHelper.getBalanceKopecks(tgId);
+ double rubles = kopecks / 100.0;
+ String balanceStr;
+ if (rubles == Math.floor(rubles)) {
+ balanceStr = String.format(Locale.getDefault(), "%,d \u20bd", (long) rubles);
+ } else {
+ balanceStr = String.format(Locale.getDefault(), "%,.2f \u20bd", rubles);
+ }
+ balanceTextView.setText(LocaleController.formatString(R.string.FoxAccountBalance, balanceStr));
+
+ String expireAt = SponsorHelper.getExpireAt(tgId);
+ long expireMs = SponsorHelper.parseExpireMillis(expireAt);
+ if (expireMs <= 0) {
+ subTextView.setText(LocaleController.getString(R.string.FoxAccountSubNone));
+ } else {
+ long now = System.currentTimeMillis();
+ if (expireMs <= now) {
+ subTextView.setText(LocaleController.getString(R.string.FoxAccountSubExpired));
+ } else {
+ long daysLeft = (long) Math.ceil((expireMs - now) / (24.0 * 60 * 60 * 1000));
+ subTextView.setText(LocaleController.formatString(R.string.FoxAccountDaysLeft, (int) daysLeft));
+ }
+ }
+ }
+}
diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java
index bbf1e4db..8f9031d2 100644
--- a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java
+++ b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java
@@ -2865,6 +2865,7 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
if (!onlySelect) {
globalObserversGroup.add(NotificationCenter.closeSearchByActiveAction);
globalObserversGroup.add(NotificationCenter.proxySettingsChanged);
+ globalObserversGroup.add(NotificationCenter.sponsorStatusUpdated);
observersGroup.add(NotificationCenter.filterSettingsUpdated);
observersGroup.add(NotificationCenter.dialogsUnreadCounterChanged);
}
@@ -6992,6 +6993,9 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
@Override
public void onResume() {
super.onResume();
+ if (!onlySelect && folderId == 0) {
+ tw.nekomimi.nekogram.helpers.SponsorHelper.refresh(false);
+ }
if (dialogStoriesCell != null) {
dialogStoriesCell.onResume();
}
@@ -10380,6 +10384,16 @@ public class DialogsActivity extends BaseFragment implements NotificationCenter.
@SuppressWarnings("unchecked")
@Override
public void didReceivedNotification(int id, int account, Object... args) {
+ if (id == NotificationCenter.sponsorStatusUpdated) {
+ if (viewPages != null) {
+ for (int a = 0; a < viewPages.length; a++) {
+ if (viewPages[a] != null && viewPages[a].dialogsAdapter != null) {
+ viewPages[a].dialogsAdapter.notifyDataSetChanged();
+ }
+ }
+ }
+ return;
+ }
if (id == NotificationCenter.dialogsNeedReload) {
if (viewPages == null || dialogsListFrozen) {
return;
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 7695108f..db03bfe8 100644
--- a/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java
+++ b/TMessagesProj/src/main/java/tw/nekomimi/nekogram/helpers/SponsorHelper.java
@@ -61,6 +61,73 @@ public final class SponsorHelper {
return user != null && isSponsor(user.id);
}
+ /**
+ * Whether the given Telegram id is linked to a FoxCloud/GhostCloud account.
+ * Only meaningful after at least one successful {@link #refresh(boolean)}.
+ */
+ public static boolean isLinked(long telegramId) {
+ if (telegramId == 0) {
+ return false;
+ }
+ return prefs().getBoolean("linked_" + telegramId, false);
+ }
+
+ public static boolean isCurrentUserLinked() {
+ var user = UserConfig.getInstance(UserConfig.selectedAccount).getCurrentUser();
+ return user != null && isLinked(user.id);
+ }
+
+ /** Cached account balance in kopecks (1/100 of a ruble). */
+ public static long getBalanceKopecks(long telegramId) {
+ if (telegramId == 0) {
+ return 0;
+ }
+ return prefs().getLong("balance_" + telegramId, 0);
+ }
+
+ /**
+ * Cached subscription expiry, as the raw string returned by the backend
+ * ({@code "yyyy-MM-dd HH:mm:ss"}), or empty when there is no subscription.
+ */
+ public static String getExpireAt(long telegramId) {
+ if (telegramId == 0) {
+ return "";
+ }
+ return prefs().getString("expire_" + telegramId, "");
+ }
+
+ /** Cached plan name, or empty. */
+ public static String getPlan(long telegramId) {
+ if (telegramId == 0) {
+ return "";
+ }
+ return prefs().getString("plan_" + telegramId, "");
+ }
+
+ /**
+ * Parse the backend expiry string ({@code "yyyy-MM-dd HH:mm:ss"}, treated as
+ * UTC) into epoch millis, or 0 if absent/unparseable.
+ */
+ public static long parseExpireMillis(String expireAt) {
+ if (expireAt == null || expireAt.isEmpty()) {
+ return 0;
+ }
+ String s = expireAt.trim();
+ String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd"};
+ for (String pattern : patterns) {
+ try {
+ java.text.SimpleDateFormat fmt = new java.text.SimpleDateFormat(pattern, java.util.Locale.US);
+ fmt.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
+ java.util.Date d = fmt.parse(s);
+ if (d != null) {
+ return d.getTime();
+ }
+ } catch (Exception ignore) {
+ }
+ }
+ return 0;
+ }
+
/**
* Kick off a background refresh of the sponsor status for the current
* account. Safe to call repeatedly: it self-throttles via {@link #TTL_MS}
@@ -124,7 +191,12 @@ public final class SponsorHelper {
return;
}
boolean sponsor = json.optBoolean("is_sponsor", false);
- store(telegramId, sponsor);
+ boolean linked = json.optBoolean("telegram_linked", false);
+ // balance is sent in kopecks; the client divides by 100 for display.
+ long balance = json.optLong("balance", 0);
+ String expireAt = json.isNull("expire_at") ? "" : json.optString("expire_at", "");
+ String plan = json.isNull("plan") ? "" : json.optString("plan", "");
+ store(telegramId, sponsor, linked, balance, expireAt, plan);
} catch (Exception e) {
FileLog.e(e);
} finally {
@@ -134,13 +206,29 @@ public final class SponsorHelper {
}
}
- private static void store(long telegramId, boolean sponsor) {
- boolean previous = prefs().getBoolean("sponsor_" + telegramId, false);
- prefs().edit()
+ private static void store(long telegramId, boolean sponsor, boolean linked, long balance, String expireAt, String plan) {
+ SharedPreferences p = prefs();
+ boolean previousSponsor = p.getBoolean("sponsor_" + telegramId, false);
+ boolean previousLinked = p.getBoolean("linked_" + telegramId, false);
+ long previousBalance = p.getLong("balance_" + telegramId, 0);
+ String previousExpire = p.getString("expire_" + telegramId, "");
+ String previousPlan = p.getString("plan_" + telegramId, "");
+
+ p.edit()
.putBoolean("sponsor_" + telegramId, sponsor)
+ .putBoolean("linked_" + telegramId, linked)
+ .putLong("balance_" + telegramId, balance)
+ .putString("expire_" + telegramId, expireAt != null ? expireAt : "")
+ .putString("plan_" + telegramId, plan != null ? plan : "")
.putLong("checked_" + telegramId, System.currentTimeMillis())
.apply();
- if (previous != sponsor) {
+
+ boolean changed = previousSponsor != sponsor
+ || previousLinked != linked
+ || previousBalance != balance
+ || !previousExpire.equals(expireAt != null ? expireAt : "")
+ || !previousPlan.equals(plan != null ? plan : "");
+ if (changed) {
AndroidUtilities.runOnUIThread(() ->
NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.sponsorStatusUpdated));
}
@@ -149,7 +237,6 @@ public final class SponsorHelper {
// ─────────────────────────────────────────────────────────────
// Top sponsors leaderboard
// ─────────────────────────────────────────────────────────────
-
public static class Sponsor {
public int rank;
public String name;
diff --git a/TMessagesProj/src/main/res/values-ru/strings_neko.xml b/TMessagesProj/src/main/res/values-ru/strings_neko.xml
index 164d62b1..b0c7be17 100644
--- a/TMessagesProj/src/main/res/values-ru/strings_neko.xml
+++ b/TMessagesProj/src/main/res/values-ru/strings_neko.xml
@@ -331,4 +331,9 @@
Те, кто поддерживает проект
Пока нет спонсоров
Не удалось загрузить список
+ Баланс: %1$s
+ Подписка: %1$s
+ Подписка истекла
+ Нет подписки
+ осталось %1$d дн.
diff --git a/TMessagesProj/src/main/res/values/strings_neko.xml b/TMessagesProj/src/main/res/values/strings_neko.xml
index bd49058f..df91974e 100644
--- a/TMessagesProj/src/main/res/values/strings_neko.xml
+++ b/TMessagesProj/src/main/res/values/strings_neko.xml
@@ -157,6 +157,11 @@
People who support the project
No sponsors yet
Failed to load sponsors
+ Balance: %1$s
+ Subscription: %1$s
+ Subscription expired
+ No subscription
+ %1$d d. left
Downloading update...
A notification will be shown when the update completes.
The app will relaunch when the update completes.