Sponsor badge: shimmering heart + tap-to-explain bulletin

- Replace static star with self-animating iridescent ShimmerHeartDrawable
- Move badge to clickable rightDrawable slot; tap shows a Nekogram-style
  bulletin explaining why the badge was granted
- Add en/ru strings; drop unused vector resource
This commit is contained in:
instant992 2026-06-09 08:35:21 +04:00
parent e926e7afc6
commit 323c1ff10e
5 changed files with 159 additions and 27 deletions

View file

@ -11180,17 +11180,23 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
private Drawable getFoxSponsorDrawable(int a) { private Drawable getFoxSponsorDrawable(int a) {
if (foxSponsorDrawable[a] == null) { if (foxSponsorDrawable[a] == null) {
Drawable d = ContextCompat.getDrawable(getParentActivity(), R.drawable.foxsponsor_badge).mutate(); foxSponsorDrawable[a] = new tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable(AndroidUtilities.dp(a == 1 ? 22 : 18));
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]; return foxSponsorDrawable[a];
} }
private void showSponsorInfo() {
if (getParentActivity() == null) {
return;
}
BulletinFactory.of(ProfileActivity.this)
.createSimpleBulletin(
new tw.nekomimi.nekogram.helpers.ShimmerHeartDrawable(AndroidUtilities.dp(28)),
LocaleController.getString(R.string.FoxSponsorBadge),
LocaleController.getString(R.string.FoxSponsorBadgeInfo))
.show();
}
private Drawable getPremiumCrossfadeDrawable(int a) { private Drawable getPremiumCrossfadeDrawable(int a) {
if (premiumCrossfadeDrawable[a] == null) { if (premiumCrossfadeDrawable[a] == null) {
premiumStarDrawable[a] = ContextCompat.getDrawable(getParentActivity(), R.drawable.msg_premium_liststar).mutate(); premiumStarDrawable[a] = ContextCompat.getDrawable(getParentActivity(), R.drawable.msg_premium_liststar).mutate();
@ -11550,7 +11556,7 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
}, resourcesProvider); }, resourcesProvider);
} : null); } : null);
Drawable leftIcon = currentEncryptedChat != null ? getLockIconDrawable() : null; Drawable leftIcon = currentEncryptedChat != null ? getLockIconDrawable() : null;
boolean rightIconIsPremium = false, rightIconIsStatus = false; boolean rightIconIsPremium = false, rightIconIsStatus = false, rightIconIsSponsor = false;
nameTextView[a].setRightDrawableOutside(a == 0); nameTextView[a].setRightDrawableOutside(a == 0);
if (a == 0 && !copyFromChatActivity) { if (a == 0 && !copyFromChatActivity) {
if (user.scam || user.fake) { if (user.scam || user.fake) {
@ -11562,14 +11568,15 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
} else if (getMessagesController().isDialogMuted(dialogId != 0 ? dialogId : userId, topicId)) { } else if (getMessagesController().isDialogMuted(dialogId != 0 ? dialogId : userId, topicId)) {
nameTextView[a].setRightDrawable2(getThemedDrawable(Theme.key_drawable_muteIconDrawable)); nameTextView[a].setRightDrawable2(getThemedDrawable(Theme.key_drawable_muteIconDrawable));
nameTextViewRightDrawable2ContentDescription = LocaleController.getString(R.string.NotificationsMuted); 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 { } else {
nameTextView[a].setRightDrawable2(null); nameTextView[a].setRightDrawable2(null);
nameTextViewRightDrawable2ContentDescription = null; nameTextViewRightDrawable2ContentDescription = null;
} }
if (user != null/* && !getMessagesController().premiumFeaturesBlocked()*/ && !MessagesController.isSupportUser(user) && DialogObject.getEmojiStatusDocumentId(user.emoji_status) != 0) { if (user.self && SponsorHelper.isSponsor(user.id)) {
rightIconIsSponsor = true;
nameTextView[a].setRightDrawable(getFoxSponsorDrawable(a));
nameTextViewRightDrawableContentDescription = LocaleController.getString(R.string.FoxSponsorBadge);
} else if (user != null/* && !getMessagesController().premiumFeaturesBlocked()*/ && !MessagesController.isSupportUser(user) && DialogObject.getEmojiStatusDocumentId(user.emoji_status) != 0) {
rightIconIsStatus = true; rightIconIsStatus = true;
rightIconIsPremium = false; rightIconIsPremium = false;
nameTextView[a].setRightDrawable(getEmojiStatusDrawable(user.emoji_status, false, false, a)); nameTextView[a].setRightDrawable(getEmojiStatusDrawable(user.emoji_status, false, false, a));
@ -11588,12 +11595,13 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
nameTextView[a].setRightDrawable2(getScamDrawable(user.scam ? 0 : 1)); nameTextView[a].setRightDrawable2(getScamDrawable(user.scam ? 0 : 1));
} else if (user.verified) { } else if (user.verified) {
nameTextView[a].setRightDrawable2(getVerifiedCrossfadeDrawable(a)); nameTextView[a].setRightDrawable2(getVerifiedCrossfadeDrawable(a));
} else if (user.self && SponsorHelper.isSponsor(user.id)) {
nameTextView[a].setRightDrawable2(getFoxSponsorDrawable(a));
} else { } else {
nameTextView[a].setRightDrawable2(null); nameTextView[a].setRightDrawable2(null);
} }
if (/*!getMessagesController().premiumFeaturesBlocked() && */user != null && !MessagesController.isSupportUser(user) && DialogObject.getEmojiStatusDocumentId(user.emoji_status) != 0) { if (user.self && SponsorHelper.isSponsor(user.id)) {
rightIconIsSponsor = true;
nameTextView[a].setRightDrawable(getFoxSponsorDrawable(a));
} else if (/*!getMessagesController().premiumFeaturesBlocked() && */user != null && !MessagesController.isSupportUser(user) && DialogObject.getEmojiStatusDocumentId(user.emoji_status) != 0) {
rightIconIsStatus = true; rightIconIsStatus = true;
rightIconIsPremium = false; rightIconIsPremium = false;
nameTextView[a].setRightDrawable(getEmojiStatusDrawable(user.emoji_status, true, true, a)); nameTextView[a].setRightDrawable(getEmojiStatusDrawable(user.emoji_status, true, true, a));
@ -11612,10 +11620,12 @@ public class ProfileActivity extends BaseFragment implements NotificationCenter.
nameTextView[a].setLeftDrawableOutside(false); nameTextView[a].setLeftDrawableOutside(false);
} }
nameTextView[a].setLeftDrawable(leftIcon); nameTextView[a].setLeftDrawable(leftIcon);
if (a == 1 && (rightIconIsStatus || rightIconIsPremium)) { if (a == 1 && (rightIconIsStatus || rightIconIsPremium || rightIconIsSponsor)) {
nameTextView[a].setRightDrawableOutside(true); nameTextView[a].setRightDrawableOutside(true);
} }
if (user.self && getMessagesController().isPremiumUser(user)) { if (rightIconIsSponsor) {
nameTextView[a].setRightDrawableOnClick(v -> showSponsorInfo());
} else if (user.self && getMessagesController().isPremiumUser(user)) {
nameTextView[a].setRightDrawableOnClick(v -> { nameTextView[a].setRightDrawableOnClick(v -> {
showStatusSelect(); showStatusSelect();
}); });

View file

@ -0,0 +1,128 @@
package tw.nekomimi.nekogram.helpers;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import org.telegram.messenger.AndroidUtilities;
/**
* A heart-shaped badge filled with a continuously moving iridescent gradient
* ("shimmer"). It self-invalidates each frame, so when attached to a view via
* {@code setRightDrawable(...)} the host keeps repainting automatically (the
* SimpleTextView drawable callback chain handles invalidation).
*/
public class ShimmerHeartDrawable extends Drawable {
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path heart = new Path();
private final Matrix gradientMatrix = new Matrix();
private LinearGradient gradient;
private int lastWidth = -1;
private int gradientWidth;
// Iridescent palette swept across the heart.
private static final int[] COLORS = new int[] {
0xFFFF4D6D, // pink-red
0xFFFF8FA3, // light pink
0xFFFFC2D1, // pale
0xFFFF5C8A, // magenta
0xFFFF1E56, // deep red
0xFFFF4D6D, // back to start (seamless loop)
};
private static final long CYCLE_MS = 2200L;
private final int size;
public ShimmerHeartDrawable() {
this(AndroidUtilities.dp(18));
}
public ShimmerHeartDrawable(int sizePx) {
this.size = sizePx;
paint.setStyle(Paint.Style.FILL);
}
private void buildGradient(int width) {
gradientWidth = width * 2;
gradient = new LinearGradient(0, 0, gradientWidth, 0, COLORS, null, Shader.TileMode.MIRROR);
paint.setShader(gradient);
lastWidth = width;
}
private void buildHeart(Rect b) {
heart.reset();
float w = b.width();
float h = b.height();
float l = b.left;
float t = b.top;
// Heart path normalized to the bounds.
float cx = l + w / 2f;
heart.moveTo(cx, t + h * 0.30f);
heart.cubicTo(l + w * 0.40f, t + h * 0.05f, l + w * 0.02f, t + h * 0.18f, l + w * 0.10f, t + h * 0.45f);
heart.cubicTo(l + w * 0.17f, t + h * 0.66f, l + w * 0.40f, t + h * 0.80f, cx, t + h * 0.97f);
heart.cubicTo(l + w * 0.60f, t + h * 0.80f, l + w * 0.83f, t + h * 0.66f, l + w * 0.90f, t + h * 0.45f);
heart.cubicTo(l + w * 0.98f, t + h * 0.18f, l + w * 0.60f, t + h * 0.05f, cx, t + h * 0.30f);
heart.close();
}
@Override
protected void onBoundsChange(@NonNull Rect bounds) {
super.onBoundsChange(bounds);
buildHeart(bounds);
buildGradient(bounds.width());
}
@Override
public void draw(@NonNull Canvas canvas) {
Rect b = getBounds();
if (b.width() == 0 || b.height() == 0) {
return;
}
if (gradient == null || lastWidth != b.width()) {
buildGradient(b.width());
buildHeart(b);
}
float phase = (System.currentTimeMillis() % CYCLE_MS) / (float) CYCLE_MS;
gradientMatrix.reset();
gradientMatrix.setTranslate(b.left - phase * gradientWidth, 0);
gradient.setLocalMatrix(gradientMatrix);
canvas.drawPath(heart, paint);
invalidateSelf();
}
@Override
public int getIntrinsicWidth() {
return size;
}
@Override
public int getIntrinsicHeight() {
return size;
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}

View file

@ -1,9 +0,0 @@
<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

@ -322,5 +322,7 @@
<string name="TabsPositionBottom">Снизу</string> <string name="TabsPositionBottom">Снизу</string>
<string name="StrokeOnViews">Эффекты бликов</string> <string name="StrokeOnViews">Эффекты бликов</string>
<!-- Accessibility --> <!-- Accessibility -->
<string name="AccessibilitySettings">Настройки доступности</string> <string name="AccessibilitySettings">Настройки специальных возможностей</string>
<string name="FoxSponsorBadge">Спонсор</string>
<string name="FoxSponsorBadgeInfo">Эта отметка выдана за поддержку проекта. Спасибо!</string>
</resources> </resources>

View file

@ -150,6 +150,7 @@
<string name="NekogramVersion">FoxiGram %1$s\nBased on Telegram %2$s\nDesigned by %3$s</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="UpdateInstalling">Installing update...</string>
<string name="FoxSponsorBadge">Sponsor</string> <string name="FoxSponsorBadge">Sponsor</string>
<string name="FoxSponsorBadgeInfo">You received this badge for supporting the project. Thank you!</string>
<string name="UpdateDownloading">Downloading update...</string> <string name="UpdateDownloading">Downloading update...</string>
<string name="UpdateInstallingNotification">A notification will be shown when the update completes.</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> <string name="UpdateInstallingRelaunch">The app will relaunch when the update completes.</string>