Show sponsor badge next to names everywhere + update app icon

- Sponsor heart now appears in the chats list (DialogCell), chat header
  (ChatAvatarContainer), message author names (ChatMessageCell) and user
  lists (UserCell), not just on the profile screen
- Added ShimmerHeartDrawable.drawStatic() for cheap static rendering in
  frequently-repainted list cells; header keeps the animated shimmer
- Cells request sponsor status via SponsorHelper batch lookup and redraw
  on sponsorStatusUpdated
- Regenerate launcher icon from updated foxigram4.png
This commit is contained in:
instant992 2026-06-10 01:00:16 +04:00
parent 695de54a0a
commit 00479f6f98
16 changed files with 132 additions and 1 deletions

View file

@ -1663,6 +1663,8 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate
private boolean drawName; private boolean drawName;
private boolean drawNameLayout; private boolean drawNameLayout;
private boolean drawNameAvatar; private boolean drawNameAvatar;
private boolean drawNameSponsor;
private final android.graphics.RectF nameSponsorRect = new android.graphics.RectF();
private boolean drawTopic; private boolean drawTopic;
private TopicButton topicButton; private TopicButton topicButton;
@ -18762,6 +18764,13 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate
} else { } else {
currentNameString = ""; currentNameString = "";
} }
drawNameSponsor = false;
if (!messageObject.isOutOwner() && currentNameStatus == null && currentNameBotVerificationId == 0
&& currentUser != null && currentUser.id != 0 && messageObject.isFromUser()
&& !UserObject.isUserSelf(currentUser) && !currentUser.verified && !currentUser.bot) {
tw.nekomimi.nekogram.helpers.SponsorHelper.requestSponsorStatus(currentUser.id);
drawNameSponsor = tw.nekomimi.nekogram.helpers.SponsorHelper.isKnownSponsor(currentUser.id);
}
int additionalWidth = dp(currentMessageObject.isSponsored() ? -24 : 0); int additionalWidth = dp(currentMessageObject.isSponsored() ? -24 : 0);
CharSequence nameStringFinal = AndroidUtilities.removeDiacritics(currentNameString.replace('\n', ' ').replace('\u200F', ' ')); CharSequence nameStringFinal = AndroidUtilities.removeDiacritics(currentNameString.replace('\n', ' ').replace('\u200F', ' '));
try { try {
@ -21910,6 +21919,14 @@ public class ChatMessageCell extends BaseCell implements SeekBar.SeekBarDelegate
Theme.chat_namePaint.setAlpha(oldAlpha); Theme.chat_namePaint.setAlpha(oldAlpha);
canvas.restore(); canvas.restore();
if (drawNameSponsor && viaNameWidth <= 0) {
int sz = dp(15);
float hx = nx + nameOffsetX + nameLayoutWidth + dp(4);
float hy = ny + (nameLayout.getHeight() - sz) / 2f;
nameSponsorRect.set(hx, hy, hx + sz, hy + sz);
tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable.drawStatic(canvas, nameSponsorRect, (int) (0xFF * nameAlpha));
}
float end; float end;
if (currentMessagesGroup != null && !currentMessagesGroup.isDocuments) { if (currentMessagesGroup != null && !currentMessagesGroup.isDocuments) {
int dWidth = getGroupPhotosWidth(); int dWidth = getGroupPhotosWidth();

View file

@ -596,6 +596,8 @@ public class DialogCell extends BaseCell implements StoriesListPlaceProvider.Ava
private boolean drawVerified; private boolean drawVerified;
private boolean drawBotVerified; private boolean drawBotVerified;
private boolean drawPremium; private boolean drawPremium;
private boolean drawSponsor;
private final android.graphics.RectF sponsorHeartRect = new android.graphics.RectF();
private final View emojiStatusView; private final View emojiStatusView;
private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable emojiStatus; private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable emojiStatus;
private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable botVerification; private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable botVerification;
@ -1190,6 +1192,7 @@ public class DialogCell extends BaseCell implements StoriesListPlaceProvider.Ava
drawVerified = false; drawVerified = false;
drawBotVerified = false; drawBotVerified = false;
drawPremium = false; drawPremium = false;
drawSponsor = false;
drawForwardIcon = false; drawForwardIcon = false;
drawGiftIcon = false; drawGiftIcon = false;
drawScam = 0; drawScam = 0;
@ -1418,6 +1421,10 @@ public class DialogCell extends BaseCell implements StoriesListPlaceProvider.Ava
emojiStatus.setParticles(false, false); emojiStatus.setParticles(false, false);
} }
} }
if (!drawVerified && !drawPremium && drawScam == 0 && user.id != 0 && !UserObject.isUserSelf(user)) {
tw.nekomimi.nekogram.helpers.SponsorHelper.requestSponsorStatus(user.id);
drawSponsor = tw.nekomimi.nekogram.helpers.SponsorHelper.isKnownSponsor(user.id);
}
} }
if (dialogBotVerificationIcon != 0 && drawBotVerified) { if (dialogBotVerificationIcon != 0 && drawBotVerified) {
botVerification.set(dialogBotVerificationIcon, false); botVerification.set(dialogBotVerificationIcon, false);
@ -2260,6 +2267,13 @@ public class DialogCell extends BaseCell implements StoriesListPlaceProvider.Ava
if (LocaleController.isRTL) { if (LocaleController.isRTL) {
nameLeft += w; nameLeft += w;
} }
} else if (drawSponsor) {
int w = dp(6) + dp(18);
nameWidth -= w;
nameAdditionalsForChannelSubscriber += w;
if (LocaleController.isRTL) {
nameLeft += w;
}
} }
if (drawBotVerified) { if (drawBotVerified) {
nameWidth -= dp(21); nameWidth -= dp(21);
@ -2691,6 +2705,8 @@ public class DialogCell extends BaseCell implements StoriesListPlaceProvider.Ava
nameMutedIconLeft = nameMuteLeft - dp(6) - Theme.dialogs_muteDrawable.getIntrinsicWidth(); nameMutedIconLeft = nameMuteLeft - dp(6) - Theme.dialogs_muteDrawable.getIntrinsicWidth();
} else if (drawScam != 0) { } else if (drawScam != 0) {
nameMuteLeft = (int) (nameLeft + (nameWidth - widthpx) - dp(6) - (drawScam == 1 ? Theme.dialogs_scamDrawable : Theme.dialogs_fakeDrawable).getIntrinsicWidth()); nameMuteLeft = (int) (nameLeft + (nameWidth - widthpx) - dp(6) - (drawScam == 1 ? Theme.dialogs_scamDrawable : Theme.dialogs_fakeDrawable).getIntrinsicWidth());
} else if (drawSponsor) {
nameMuteLeft = (int) (nameLeft + (nameWidth - widthpx) - dp(6) - dp(18));
} else { } else {
nameMuteLeft = (int) (nameLeft + (nameWidth - widthpx) - dp(6) - Theme.dialogs_muteDrawable.getIntrinsicWidth()); nameMuteLeft = (int) (nameLeft + (nameWidth - widthpx) - dp(6) - Theme.dialogs_muteDrawable.getIntrinsicWidth());
} }
@ -4343,6 +4359,15 @@ public class DialogCell extends BaseCell implements StoriesListPlaceProvider.Ava
} }
setDrawableBounds((drawScam == 1 ? Theme.dialogs_scamDrawable : Theme.dialogs_fakeDrawable), nameMuteLeft, y); setDrawableBounds((drawScam == 1 ? Theme.dialogs_scamDrawable : Theme.dialogs_fakeDrawable), nameMuteLeft, y);
(drawScam == 1 ? Theme.dialogs_scamDrawable : Theme.dialogs_fakeDrawable).draw(canvas); (drawScam == 1 ? Theme.dialogs_scamDrawable : Theme.dialogs_fakeDrawable).draw(canvas);
} else if (drawSponsor) {
int y = dp(useForceThreeLines || SharedConfig.useThreeLinesLayout ? 13.5f : 16.5f);
if ((!(useForceThreeLines || SharedConfig.useThreeLinesLayout) || isForumCell()) && hasTags()) {
y -= dp(9);
}
int sz = dp(18);
int sx = nameMuteLeft - dp(1);
sponsorHeartRect.set(sx, y, sx + sz, y + sz);
tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable.drawStatic(canvas, sponsorHeartRect, 255);
} }
if (drawReorder || reorderIconProgress != 0) { if (drawReorder || reorderIconProgress != 0) {

View file

@ -74,7 +74,7 @@ public class UserCell extends FrameLayout implements NotificationCenter.Notifica
private TextView addButton; private TextView addButton;
private ImageView mutualView; private ImageView mutualView;
private Drawable premiumDrawable; private Drawable premiumDrawable;
private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable botVerification; private Drawable sponsorDrawable; private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable botVerification;
private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable emojiStatus; private final AnimatedEmojiDrawable.SwapAnimatedEmojiDrawable emojiStatus;
private ImageView closeView; private ImageView closeView;
protected Theme.ResourcesProvider resourcesProvider; protected Theme.ResourcesProvider resourcesProvider;
@ -712,10 +712,21 @@ public class UserCell extends FrameLayout implements NotificationCenter.Notifica
nameTextView.setRightDrawable(premiumDrawable); nameTextView.setRightDrawable(premiumDrawable);
} }
nameTextView.setRightDrawableTopPadding(-dp(0.5f)); nameTextView.setRightDrawableTopPadding(-dp(0.5f));
} else if (currentUser != null && currentUser.id != 0 && !UserObject.isUserSelf(currentUser)
&& !currentUser.verified && !currentUser.bot
&& tw.nekomimi.nekogram.helpers.SponsorHelper.isKnownSponsor(currentUser.id)) {
if (sponsorDrawable == null) {
sponsorDrawable = new tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable(dp(16));
}
nameTextView.setRightDrawable(sponsorDrawable);
nameTextView.setRightDrawableTopPadding(-dp(0.5f));
} else { } else {
nameTextView.setRightDrawable(null); nameTextView.setRightDrawable(null);
nameTextView.setRightDrawableTopPadding(0); nameTextView.setRightDrawableTopPadding(0);
} }
if (currentUser != null && currentUser.id != 0 && !UserObject.isUserSelf(currentUser) && !currentUser.bot) {
tw.nekomimi.nekogram.helpers.SponsorHelper.requestSponsorStatus(currentUser.id);
}
if (currentStatus != null) { if (currentStatus != null) {
statusTextView.setTextColor(statusColor); statusTextView.setTextColor(statusColor);
CharSequence status = currentStatus; CharSequence status = currentStatus;
@ -829,6 +840,8 @@ public class UserCell extends FrameLayout implements NotificationCenter.Notifica
public void didReceivedNotification(int id, int account, Object... args) { public void didReceivedNotification(int id, int account, Object... args) {
if (id == NotificationCenter.emojiLoaded) { if (id == NotificationCenter.emojiLoaded) {
nameTextView.invalidate(); nameTextView.invalidate();
} else if (id == NotificationCenter.sponsorStatusUpdated) {
update(0);
} }
} }
@ -836,6 +849,7 @@ public class UserCell extends FrameLayout implements NotificationCenter.Notifica
protected void onAttachedToWindow() { protected void onAttachedToWindow() {
super.onAttachedToWindow(); super.onAttachedToWindow();
NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.emojiLoaded); NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.emojiLoaded);
NotificationCenter.getGlobalInstance().addObserver(this, NotificationCenter.sponsorStatusUpdated);
emojiStatus.attach(); emojiStatus.attach();
botVerification.attach(); botVerification.attach();
} }
@ -844,6 +858,7 @@ public class UserCell extends FrameLayout implements NotificationCenter.Notifica
protected void onDetachedFromWindow() { protected void onDetachedFromWindow() {
super.onDetachedFromWindow(); super.onDetachedFromWindow();
NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.emojiLoaded); NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.emojiLoaded);
NotificationCenter.getGlobalInstance().removeObserver(this, NotificationCenter.sponsorStatusUpdated);
emojiStatus.detach(); emojiStatus.detach();
botVerification.detach(); botVerification.detach();
storyParams.onDetachFromWindow(); storyParams.onDetachFromWindow();

View file

@ -3204,6 +3204,7 @@ public class ChatActivity extends BaseFragment implements
if (themeDelegate.isThemeChangeAvailable(false)) { if (themeDelegate.isThemeChangeAvailable(false)) {
globalObserversGroup.add(NotificationCenter.needSetDayNightTheme); globalObserversGroup.add(NotificationCenter.needSetDayNightTheme);
} }
globalObserversGroup.add(NotificationCenter.sponsorStatusUpdated);
if (chatInvite != null) { if (chatInvite != null) {
int timeout = chatInvite.expires - getConnectionsManager().getCurrentTime(); int timeout = chatInvite.expires - getConnectionsManager().getCurrentTime();
@ -19775,6 +19776,16 @@ public class ChatActivity extends BaseFragment implements
avatarContainer.setTitle(AndroidUtilities.removeRTL(AndroidUtilities.removeDiacritics(UserObject.getUserName(currentUser))), currentUser.scam, currentUser.fake, currentUser.verified, getMessagesController().isPremiumUser(currentUser), !MessagesController.isSupportUser(currentUser) ? currentUser.emoji_status : null, animated); avatarContainer.setTitle(AndroidUtilities.removeRTL(AndroidUtilities.removeDiacritics(UserObject.getUserName(currentUser))), currentUser.scam, currentUser.fake, currentUser.verified, getMessagesController().isPremiumUser(currentUser), !MessagesController.isSupportUser(currentUser) ? currentUser.emoji_status : null, animated);
} }
} }
if (avatarContainer != null) {
boolean sponsor = false;
if (currentChat == null && currentUser != null && !currentUser.self
&& !currentUser.verified && !currentUser.scam && !currentUser.fake
&& !getMessagesController().isPremiumUser(currentUser)) {
tw.nekomimi.nekogram.helpers.SponsorHelper.requestSponsorStatus(currentUser.id);
sponsor = tw.nekomimi.nekogram.helpers.SponsorHelper.isKnownSponsor(currentUser.id);
}
avatarContainer.setSponsorBadge(sponsor);
}
setParentActivityTitle(avatarContainer.getTitleTextView().getText()); setParentActivityTitle(avatarContainer.getTitleTextView().getText());
updateTitleIcons(); updateTitleIcons();
} }
@ -20496,6 +20507,15 @@ public class ChatActivity extends BaseFragment implements
@Override @Override
public void didReceivedNotification(int id, int account, final Object... args) { public void didReceivedNotification(int id, int account, final Object... args) {
if (id == NotificationCenter.sponsorStatusUpdated) {
if (avatarContainer != null) {
updateTitle(false);
}
if (chatListView != null) {
chatListView.invalidateViews();
}
return;
}
if (id == NotificationCenter.messagesDidLoad) { if (id == NotificationCenter.messagesDidLoad) {
int guid = (Integer) args[10]; int guid = (Integer) args[10];
if (guid != classGuid) { if (guid != classGuid) {

View file

@ -950,6 +950,36 @@ public class ChatAvatarContainer extends FrameLayout implements NotificationCent
private Drawable verifiedBackground; private Drawable verifiedBackground;
private Drawable verifiedCheck; private Drawable verifiedCheck;
private boolean sponsorBadgeShown;
/**
* Show/hide the FoxiGram sponsor heart next to the title. Only applied when
* the title has no scam/verified badge in {@code rightDrawable2}. Uses the
* animated {@link tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable} since a
* single header is cheap to animate.
*/
public void setSponsorBadge(boolean show) {
if (show == sponsorBadgeShown) {
return;
}
if (show) {
if (rightDrawableIsScamOrVerified) {
return; // scam/verified takes precedence
}
tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable heart =
new tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable(dp(18));
titleTextView.setRightDrawable2(heart);
rightDrawable2ContentDescription = getString(R.string.FoxSponsorBadge);
sponsorBadgeShown = true;
} else {
if (sponsorBadgeShown) {
titleTextView.setRightDrawable2(null);
rightDrawable2ContentDescription = null;
}
sponsorBadgeShown = false;
}
}
public void setSubtitle(CharSequence value) { public void setSubtitle(CharSequence value) {
if (lastSubtitle == null) { if (lastSubtitle == null) {

View file

@ -72,6 +72,30 @@ public class ShimmerHeartDrawable extends Drawable {
return sharedBitmap; return sharedBitmap;
} }
/** Shared heart artwork, lazily decoded. May be null if decoding failed. */
public static Bitmap getSharedBitmap() {
return loadBitmap();
}
private static final Paint STATIC_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
/**
* Draw the sponsor heart statically (no shimmer) into {@code dst}. Cheap
* enough for list cells and headers that repaint frequently.
*/
public static void drawStatic(@NonNull Canvas canvas, @NonNull RectF dst, int alpha) {
Bitmap bmp = loadBitmap();
if (bmp == null) {
return;
}
if (alpha < 255) {
STATIC_PAINT.setAlpha(alpha);
} else {
STATIC_PAINT.setAlpha(255);
}
canvas.drawBitmap(bmp, null, dst, STATIC_PAINT);
}
private void buildShine(int w) { private void buildShine(int w) {
// A bright diagonal band, narrow relative to the badge width. // A bright diagonal band, narrow relative to the badge width.
shineWidth = Math.max(1, (int) (w * 0.55f)); shineWidth = Math.max(1, (int) (w * 0.55f));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 177 KiB