diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000000..c3fa986ad29 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(rg:*)", + "Bash(grep:*)", + "WebFetch(domain:stackoverflow.com)", + "Bash(../gradlew compileDebugJavaWithJavac:*)", + "Bash(./gradlew compileDebugJavaWithJavac:*)", + "Bash(find:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/TMessagesProj/build.gradle b/TMessagesProj/build.gradle index f25200c22ac..a72cad11b55 100644 --- a/TMessagesProj/build.gradle +++ b/TMessagesProj/build.gradle @@ -26,6 +26,10 @@ dependencies { implementation "androidx.sharetarget:sharetarget:1.2.0" implementation 'androidx.interpolator:interpolator:1.0.0' implementation 'androidx.biometric:biometric:1.1.0' + + // Material Design Components for CollapsingToolbarLayout + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'com.google.android.gms:play-services-cast-framework:21.4.0' implementation "androidx.mediarouter:mediarouter:1.7.0" diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java index 0ea5f6092d0..e65802f7796 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/Theme.java @@ -4001,6 +4001,7 @@ public void run() { public static final int key_profile_creatorIcon = colorsCount++; public static final int key_profile_title = colorsCount++; public static final int key_profile_actionIcon = colorsCount++; + public static final int key_profile_actionText = colorsCount++; public static final int key_profile_actionBackground = colorsCount++; public static final int key_profile_actionPressedBackground = colorsCount++; public static final int key_profile_verifiedBackground = colorsCount++; @@ -4012,6 +4013,13 @@ public void run() { public static final int key_profile_tabSelectedLine = colorsCount++; public static final int key_profile_tabSelector = colorsCount++; + // Profile header animation colors + public static final int key_profile_headerAnimationBackground = colorsCount++; + public static final int key_profile_headerAnimationAccent = colorsCount++; + public static final int key_profile_headerAnimationOverlay = colorsCount++; + public static final int key_profile_headerAnimationGradientStart = colorsCount++; + public static final int key_profile_headerAnimationGradientEnd = colorsCount++; + public static final int key_sharedMedia_startStopLoadIcon = colorsCount++; public static final int key_sharedMedia_linkPlaceholder = colorsCount++; public static final int key_sharedMedia_linkPlaceholderText = colorsCount++; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java index ad6b764e9a9..73030edaee8 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ActionBar/ThemeColors.java @@ -535,6 +535,7 @@ public static int[] createDefaultColors() { defaultColors[key_profile_creatorIcon] = 0xff3a95d5; defaultColors[key_profile_actionIcon] = 0xff81868a; + defaultColors[key_profile_actionText] = 0xff000000; defaultColors[key_profile_actionBackground] = 0xffffffff; defaultColors[key_profile_actionPressedBackground] = 0xfff2f2f2; defaultColors[key_profile_verifiedBackground] = 0xffb2d6f8; @@ -547,6 +548,13 @@ public static int[] createDefaultColors() { defaultColors[key_profile_tabSelectedLine] = 0xff4fa6e9; defaultColors[key_profile_tabSelector] = 0x0f000000; + // Profile header animation colors + defaultColors[key_profile_headerAnimationBackground] = 0x80000000; // Semi-transparent black + defaultColors[key_profile_headerAnimationAccent] = 0x60ffffff; // Semi-transparent white + defaultColors[key_profile_headerAnimationOverlay] = 0x20000000; // Light overlay + defaultColors[key_profile_headerAnimationGradientStart] = 0xff3a95d5; // Blue gradient start + defaultColors[key_profile_headerAnimationGradientEnd] = 0xff6ab7ff; // Light blue gradient end + defaultColors[key_player_actionBarSelector] = 0x0f000000; defaultColors[key_player_actionBarTitle] = 0xff2f3438; defaultColors[key_player_actionBarSubtitle] = 0xff8a8a8a; @@ -1420,6 +1428,7 @@ public static SparseArray createColorKeysMap() { colorKeysMap.put(key_profile_creatorIcon, "profile_creatorIcon"); colorKeysMap.put(key_profile_title, "profile_title"); colorKeysMap.put(key_profile_actionIcon, "profile_actionIcon"); + colorKeysMap.put(key_profile_actionText, "profile_actionText"); colorKeysMap.put(key_profile_actionBackground, "profile_actionBackground"); colorKeysMap.put(key_profile_actionPressedBackground, "profile_actionPressedBackground"); colorKeysMap.put(key_profile_verifiedBackground, "profile_verifiedBackground"); @@ -1429,6 +1438,12 @@ public static SparseArray createColorKeysMap() { colorKeysMap.put(key_profile_tabSelectedText, "profile_tabSelectedText"); colorKeysMap.put(key_profile_tabSelectedLine, "profile_tabSelectedLine"); colorKeysMap.put(key_profile_tabSelector, "profile_tabSelector"); + // Profile header animation color mappings + colorKeysMap.put(key_profile_headerAnimationBackground, "profile_headerAnimationBackground"); + colorKeysMap.put(key_profile_headerAnimationAccent, "profile_headerAnimationAccent"); + colorKeysMap.put(key_profile_headerAnimationOverlay, "profile_headerAnimationOverlay"); + colorKeysMap.put(key_profile_headerAnimationGradientStart, "profile_headerAnimationGradientStart"); + colorKeysMap.put(key_profile_headerAnimationGradientEnd, "profile_headerAnimationGradientEnd"); colorKeysMap.put(key_sharedMedia_startStopLoadIcon, "sharedMedia_startStopLoadIcon"); colorKeysMap.put(key_sharedMedia_linkPlaceholder, "sharedMedia_linkPlaceholder"); colorKeysMap.put(key_sharedMedia_linkPlaceholderText, "sharedMedia_linkPlaceholderText"); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 91b0ae9d7fa..8153812c18b 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -11160,7 +11160,7 @@ private void updateInfoTopView(boolean animated) { onClickListener = (v) -> { Bundle args = new Bundle(); args.putLong("user_id", chatInviterId); - presentFragment(new ProfileActivity(args)); + presentFragment(new ProfileNewActivity(args)); }; } } else { @@ -17391,7 +17391,7 @@ public boolean dispatchTouchEvent(MotionEvent ev) { if (pressActionBar) { final INavigationLayout layout = parentLayout; removeSelfFromStack(false); - layout.presentFragment(ProfileActivity.of(dialog_id)); + layout.presentFragment(ProfileNewActivity.of(dialog_id)); } else { parentLayout.expandPreviewFragment(); } @@ -26145,7 +26145,7 @@ private void migrateToNewChat(MessageObject obj) { INavigationLayout actionBarLayout = parentLayout; - if (index > 0 && !(lastFragment instanceof ChatActivity) && !(lastFragment instanceof ProfileActivity) && currentChat.creator) { + if (index > 0 && !(lastFragment instanceof ChatActivity) && !(lastFragment instanceof ProfileNewActivity) && currentChat.creator) { for (int a = index, N = actionBarLayout.getFragmentStack().size() - 1; a < N; a++) { BaseFragment fragment = actionBarLayout.getFragmentStack().get(a); if (fragment instanceof ChatActivity) { @@ -26153,10 +26153,10 @@ private void migrateToNewChat(MessageObject obj) { bundle.putLong("chat_id", channelId); actionBarLayout.addFragmentToStack(new ChatActivity(bundle), a); fragment.removeSelfFromStack(); - } else if (fragment instanceof ProfileActivity) { + } else if (fragment instanceof ProfileNewActivity) { Bundle args = new Bundle(); args.putLong("chat_id", channelId); - actionBarLayout.addFragmentToStack(new ProfileActivity(args), a); + actionBarLayout.addFragmentToStack(new ProfileNewActivity(args), a); fragment.removeSelfFromStack(); } else if (fragment instanceof ChatEditActivity) { Bundle args = new Bundle(); @@ -30876,7 +30876,7 @@ public void dismiss() { } args.putInt("report_reaction_message_id", primaryMessage.getId()); args.putLong("report_reaction_from_dialog_id", dialog_id); - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); presentFragment(fragment); closeMenu(); }).setOnHeightChangedListener((view, newHeight) -> { @@ -30965,7 +30965,7 @@ public void dismiss() { } args.putInt("report_reaction_message_id", primaryMessage.getId()); args.putLong("report_reaction_from_dialog_id", dialog_id); - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); presentFragment(fragment); closeMenu(); }).setOnHeightChangedListener((view, newHeight) -> popupLayout.getSwipeBack().setNewForegroundHeight(foregroundIndex[0], AndroidUtilities.dp(44 + 8) + newHeight, true)); @@ -31031,7 +31031,7 @@ public void onClick(View view) { } else if (object instanceof TLRPC.Chat) { args.putLong("chat_id", ((TLRPC.Chat) object).id); } - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); presentFragment(fragment); closeMenu(); return; @@ -31064,7 +31064,7 @@ public void onClick(View view) { } else if (object instanceof TLRPC.Chat) { args.putLong("chat_id", ((TLRPC.Chat) object).id); } - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); presentFragment(fragment); }); @@ -31137,7 +31137,7 @@ protected void openUser(long userId) { if (userId == getUserConfig().getClientUserId()) { args.putBoolean("my_profile", true); } - presentFragment(new ProfileActivity(args)); + presentFragment(new ProfileNewActivity(args)); } }; final FrameLayout messageSeenLayout = new FrameLayout(contentView.getContext()); @@ -32253,7 +32253,7 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { if (toIndex > 0) { string = string.substring(0, toIndex) + string.substring(toIndex + 2); ssb = new SpannableStringBuilder(string); - ProfileActivity.ShowDrawable drawable = new ProfileActivity.ShowDrawable(string.substring(fromIndex, toIndex)); + ProfileNewActivity.ShowDrawable drawable = new ProfileNewActivity.ShowDrawable(string.substring(fromIndex, toIndex)); drawable.setTextColor(Color.WHITE); drawable.setBackgroundColor(0x1e000000); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); @@ -35337,7 +35337,7 @@ public void openVCard(TLRPC.User user, String phone, String vcard, String first_ args.putString("vcard_phone", phone); args.putString("vcard_first_name", first_name); args.putString("vcard_last_name", last_name); - presentFragment(new ProfileActivity(args)); + presentFragment(new ProfileNewActivity(args)); return; } @@ -36355,7 +36355,7 @@ public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo, boolea CharSequence subtitle = AndroidUtilities.replaceSingleTag(LocaleController.getString(R.string.ApplyAvatarHint), () -> { Bundle args = new Bundle(); args.putLong("user_id", UserConfig.getInstance(currentAccount).clientUserId); - presentFragment(new ProfileActivity(args)); + presentFragment(new ProfileNewActivity(args)); }); BulletinFactory.of(ChatActivity.this).createUsersBulletin(Collections.singletonList(user), title, subtitle, null).show(); } @@ -38686,7 +38686,7 @@ private void openProfile(TLRPC.User user, boolean expandPhoto) { Bundle args = new Bundle(); args.putLong("user_id", user.id); args.putBoolean("expandPhoto", expandPhoto); - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); fragment.setPlayProfileAnimation(currentUser != null && currentUser.id == user.id ? 1 : 0); AndroidUtilities.setAdjustResizeToNothing(getParentActivity(), classGuid); presentFragment(fragment); @@ -38702,7 +38702,7 @@ private void openProfile(TLRPC.Chat chat, boolean expandPhoto) { Bundle args = new Bundle(); args.putLong("chat_id", chat.id); args.putBoolean("expandPhoto", expandPhoto); - presentFragment(new ProfileActivity(args)); + presentFragment(new ProfileNewActivity(args)); } } @@ -39100,7 +39100,7 @@ protected void dispatchDraw(Canvas canvas) { if (scrimPopupWindow != null) { scrimPopupWindow.dismiss(); } - presentFragment(ProfileActivity.of(dialogId)); + presentFragment(ProfileNewActivity.of(dialogId)); }); popupLayout.addView(userButton, LayoutHelper.createLinear(LayoutHelper.MATCH_PARENT, 52)); } @@ -40374,7 +40374,7 @@ public void didPressGiveawayChatButton(ChatMessageCell cell, int pressedPos) { if (cell.getMessageObject().messageOwner.media instanceof TLRPC.TL_messageMediaGiveawayResults) { TLRPC.TL_messageMediaGiveawayResults giveaway = (TLRPC.TL_messageMediaGiveawayResults) cell.getMessageObject().messageOwner.media; long id = giveaway.winners.get(pressedPos); - presentFragment(ProfileActivity.of(id)); + presentFragment(ProfileNewActivity.of(id)); } } @@ -40585,14 +40585,14 @@ private void openUserProfile(long uid) { if (currentEncryptedChat != null && uid == currentUser.id) { args.putLong("dialog_id", dialog_id); } - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); fragment.setPlayProfileAnimation(currentUser != null && currentUser.id == uid ? 1 : 0); presentFragment(fragment); } else { Bundle args = new Bundle(); args.putLong("user_id", uid); args.putBoolean("my_profile", true); - presentFragment(new ProfileActivity(args, null)); + presentFragment(new ProfileNewActivity(args, null)); } } @@ -43052,7 +43052,7 @@ public void didLongPressUsername(ChatMessageCell cell, CharacterStyle link, Stri options.addGap(); if (did != 0) { options.addProfile(obj, getString(isUser ? R.string.ViewProfile : (isChannel ? R.string.ViewChannelProfile : R.string.ViewGroupProfile)), () -> { - presentFragment(ProfileActivity.of(did)); + presentFragment(ProfileNewActivity.of(did)); }); } else { options.addText(getString(R.string.NoUsernameFound2), 13, dp(200)); @@ -43272,7 +43272,7 @@ public void didPressPhoneNumber(ChatMessageCell cell, CharacterStyle link, Strin options.addGap(); options.addProfile(user, getString(R.string.ViewProfile), () -> { dialog.dismiss(); - presentFragment(ProfileActivity.of(user.id)); + presentFragment(ProfileNewActivity.of(user.id)); }); } @@ -43592,7 +43592,7 @@ public void dismiss() { } args.putInt("report_reaction_message_id", messageObject.getId()); args.putLong("report_reaction_from_dialog_id", dialog_id); - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); presentFragment(fragment); closeMenu(); }), LayoutHelper.createFrame(240, LayoutHelper.WRAP_CONTENT)); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java index 20899ca785c..03921130610 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatUsersActivity.java @@ -1309,7 +1309,7 @@ protected void onCancel() { } else { args.putLong("chat_id", -peerId); } - presentFragment(new ProfileActivity(args)); + presentFragment(new ProfileNewActivity(args)); } else { if (bannedRights == null) { bannedRights = new TLRPC.TL_chatBannedRights(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedAvatarView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedAvatarView.java new file mode 100644 index 00000000000..ff0e13d79e3 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AnimatedAvatarView.java @@ -0,0 +1,51 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.View; + +/** + * Custom avatar view that integrates with AvatarAnimationHelper to provide + * black circle transformation effect during collapsing header animations. + */ +public class AnimatedAvatarView extends BackupImageView { + + private AvatarAnimationHelper animationHelper; + + public AnimatedAvatarView(Context context) { + super(context); + } + + /** + * Set the animation helper for black circle effect + */ + public void setAnimationHelper(AvatarAnimationHelper helper) { + this.animationHelper = helper; + } + + @Override + protected void onDraw(Canvas canvas) { + // First draw the normal avatar image + super.onDraw(canvas); + + // Then draw the black circle overlay if animation helper indicates it should + if (animationHelper != null && animationHelper.isInBlackCircleState()) { + animationHelper.drawBlackCircleOverlay(canvas, this); + } + } + + /** + * Get the animation helper for external access + */ + public AvatarAnimationHelper getAnimationHelper() { + return animationHelper; + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/AvatarAnimationHelper.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/AvatarAnimationHelper.java new file mode 100644 index 00000000000..eae8d4c7594 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/AvatarAnimationHelper.java @@ -0,0 +1,252 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.DecelerateInterpolator; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.Components.CubicBezierInterpolator; + +/** + * Helper class for complex avatar animations during collapsing header + * transitions. + * Handles avatar scaling, translation, and transformation to black circle. + */ +public class AvatarAnimationHelper { + + // Animation configuration + private static final float SCALE_THRESHOLD = 0.7f; // When avatar starts scaling + private static final float DISAPPEAR_THRESHOLD = 0.95f; // When avatar disappears + private static final float MIN_SCALE = 0.5f; // Minimum avatar scale - only half as small + private static final float BLACK_CIRCLE_THRESHOLD = 0.85f; // When avatar becomes black circle + + // Material Design animation interpolators for smooth transitions + // Use Telegram's premium CubicBezierInterpolator for ultra-smooth animations + private static final CubicBezierInterpolator EASE_OUT_QUINT = CubicBezierInterpolator.EASE_OUT_QUINT; + private static final CubicBezierInterpolator EASE_OUT = CubicBezierInterpolator.EASE_OUT; + + // Animation state + private float currentProgress = 0f; + private float currentScale = 1f; + private float currentTranslationX = 0f; + private float currentTranslationY = 0f; + private float currentAlpha = 1f; + private boolean isBlackCircle = false; + + // Layout parameters + private float initialX, initialY; + private float targetX, targetY; // Position next to back button + private float avatarSize; + private float toolbarHeight; + + // Paint for black circle effect + private Paint blackCirclePaint; + private Path circlePath; + private RectF circleRect; + + public AvatarAnimationHelper() { + initializePaint(); + } + + private void initializePaint() { + blackCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + blackCirclePaint.setColor(0xFF000000); + blackCirclePaint.setStyle(Paint.Style.FILL); + + circlePath = new Path(); + circleRect = new RectF(); + } + + /** + * Initialize layout parameters for animation calculations + */ + public void initializeLayout(float initialX, float initialY, float targetX, float targetY, + float avatarSize, float toolbarHeight) { + this.initialX = initialX; + this.initialY = initialY; + this.targetX = targetX; + this.targetY = targetY; + this.avatarSize = avatarSize; + this.toolbarHeight = toolbarHeight; + } + + /** + * Update animation state based on scroll progress (0.0 = expanded, 1.0 = + * collapsed) + */ + public void updateAnimation(float progress) { + this.currentProgress = Math.max(0f, Math.min(1f, progress)); + calculateAnimationValues(); + } + + private void calculateAnimationValues() { + // Use smooth interpolation for avatar transitions + // Make avatar move only upward when collapsing header is near + + if (currentProgress <= 0.7f) { + // Start state - avatar stays in original position until header is very near + currentScale = 1f; + currentTranslationX = 0f; + currentTranslationY = 0f; + currentAlpha = 1f; + isBlackCircle = false; + } else if (currentProgress >= 0.98f) { + // End state - avatar moved to toolbar, small black circle, then disappear + currentScale = MIN_SCALE; + currentTranslationX = targetX; + currentTranslationY = targetY; + currentAlpha = 0.3f; // Partially visible black circle + isBlackCircle = true; + } else { + // Ultra-slow interpolation zone (70% to 98% - synchronized with text) + float transitionProgress = (currentProgress - 0.7f) / 0.28f; // 28% range for ultra-slow movement + + // Apply very slow EASE_OUT for ultra-slow avatar motion + float smoothProgress = EASE_OUT.getInterpolation(transitionProgress); + + // Smooth scale transition + currentScale = 1f - (smoothProgress * (1f - MIN_SCALE)); + + // Smooth translation with gentle easing + currentTranslationX = smoothProgress * targetX; + currentTranslationY = smoothProgress * targetY; + + // Start black circle transformation earlier for better visual effect + if (currentProgress >= 0.85f) { + float fadeProgress = (currentProgress - 0.85f) / 0.13f; // 13% fade range (85% to 98%) + currentAlpha = 1f - (EASE_OUT.getInterpolation(fadeProgress) * 0.7f); // Fade to 30% + // isBlackCircle = true; + } else { + currentAlpha = 1f; + isBlackCircle = false; + } + } + } + + /** + * Apply calculated animations to the avatar view + */ + public void applyAnimationToView(View avatarView) { + if (avatarView == null) + return; + + avatarView.setScaleX(currentScale); + avatarView.setScaleY(currentScale); + avatarView.setTranslationX(currentTranslationX); + avatarView.setTranslationY(currentTranslationY); + avatarView.setAlpha(currentAlpha); + + // Trigger redraw for black circle effect + if (isBlackCircle) { + avatarView.invalidate(); + } + } + + /** + * Draw black circle overlay when avatar is transforming + */ + public void drawBlackCircleOverlay(Canvas canvas, View avatarView) { + if (!isBlackCircle || avatarView == null) + return; + + float centerX = avatarView.getX() + avatarView.getWidth() / 2f; + float centerY = avatarView.getY() + avatarView.getHeight() / 2f; + float radius = (avatarView.getWidth() / 2f) * currentScale; + + // Draw black circle with smooth edges + circleRect.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius); + circlePath.reset(); + circlePath.addOval(circleRect, Path.Direction.CW); + + blackCirclePaint.setAlpha((int) (255 * currentAlpha)); + canvas.drawPath(circlePath, blackCirclePaint); + } + + /** + * Get current animation progress for synchronizing with other animations + */ + public float getAnimationProgress() { + return currentProgress; + } + + /** + * Get current scale value + */ + public float getCurrentScale() { + return currentScale; + } + + /** + * Get current translation X + */ + public float getCurrentTranslationX() { + return currentTranslationX; + } + + /** + * Get current translation Y + */ + public float getCurrentTranslationY() { + return currentTranslationY; + } + + /** + * Get current alpha value + */ + public float getCurrentAlpha() { + return currentAlpha; + } + + /** + * Check if avatar is currently in black circle state + */ + public boolean isInBlackCircleState() { + return isBlackCircle; + } + + /** + * Check if avatar should be visible + */ + public boolean shouldDrawAvatar() { + return currentAlpha > 0f && currentProgress < DISAPPEAR_THRESHOLD; + } + + /** + * Reset animation to initial state + */ + public void reset() { + currentProgress = 0f; + currentScale = 1f; + currentTranslationX = 0f; + currentTranslationY = 0f; + currentAlpha = 1f; + isBlackCircle = false; + } + + /** + * Create smooth transition animator for specific progress change + */ + public ValueAnimator createProgressAnimator(float fromProgress, float toProgress, long duration) { + ValueAnimator animator = ValueAnimator.ofFloat(fromProgress, toProgress); + animator.setDuration(duration); + animator.setInterpolator(new AccelerateDecelerateInterpolator()); + animator.addUpdateListener(animation -> { + float progress = (Float) animation.getAnimatedValue(); + updateAnimation(progress); + }); + return animator; + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/BackButtonMenu.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/BackButtonMenu.java index 37e3d94d0e5..06cae44a929 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/BackButtonMenu.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/BackButtonMenu.java @@ -31,7 +31,7 @@ import org.telegram.ui.ChatActivity; import org.telegram.ui.Components.Forum.ForumUtilities; import org.telegram.ui.DialogsActivity; -import org.telegram.ui.ProfileActivity; +import org.telegram.ui.ProfileNewActivity; import org.telegram.ui.TopicsFragment; import java.util.ArrayList; @@ -178,9 +178,9 @@ public static ActionBarPopupWindow show(BaseFragment thisFragment, View backButt if (nextFragment instanceof ChatActivity) { nextFragmentDialogId = ((ChatActivity) nextFragment).getDialogId(); nextFragmentTopicId = ((ChatActivity) nextFragment).getTopicId(); - } else if (nextFragment instanceof ProfileActivity) { - nextFragmentDialogId = ((ProfileActivity) nextFragment).getDialogId(); - nextFragmentTopicId = ((ProfileActivity) nextFragment).getTopicId(); + } else if (nextFragment instanceof ProfileNewActivity) { + nextFragmentDialogId = ((ProfileNewActivity) nextFragment).getDialogId(); + nextFragmentTopicId = ((ProfileNewActivity) nextFragment).getTopicId(); } } if (nextFragmentDialogId != null && nextFragmentDialogId != pDialog.dialogId || topic != null && nextFragmentTopicId != null && topic.id != nextFragmentTopicId) { @@ -308,10 +308,10 @@ public static void goToPulledDialog(BaseFragment fragment, PulledDialog dialog) } else { fragment.presentFragment(new ChatActivity(bundle), true); } - } else if (dialog.activity == ProfileActivity.class) { + } else if (dialog.activity == ProfileNewActivity.class) { Bundle bundle = new Bundle(); bundle.putLong("dialog_id", dialog.dialogId); - fragment.presentFragment(new ProfileActivity(bundle), true); + fragment.presentFragment(new ProfileNewActivity(bundle), true); } if (dialog.activity == TopicsFragment.class) { Bundle bundle = new Bundle(); bundle.putLong("chat_id", dialog.chat.id); @@ -351,9 +351,9 @@ public static ArrayList getStackedHistoryDialogs(BaseFragment this dialogId = chatActivity.getDialogId(); folderId = chatActivity.getDialogFolderId(); filterId = chatActivity.getDialogFilterId(); - } else if (fragment instanceof ProfileActivity) { - activity = ProfileActivity.class; - ProfileActivity profileActivity = (ProfileActivity) fragment; + } else if (fragment instanceof ProfileNewActivity) { + activity = ProfileNewActivity.class; + ProfileNewActivity profileActivity = (ProfileNewActivity) fragment; chat = profileActivity.getCurrentChat(); try { user = profileActivity.getUserInfo().user; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java index 99427cda4c3..f200c4efcbb 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAvatarContainer.java @@ -59,7 +59,7 @@ import org.telegram.ui.Business.BusinessLinksController; import org.telegram.ui.ChatActivity; import org.telegram.ui.Components.Forum.ForumUtilities; -import org.telegram.ui.ProfileActivity; +import org.telegram.ui.ProfileNewActivity; import org.telegram.ui.Stories.StoriesUtilities; import org.telegram.ui.TopicsFragment; @@ -536,7 +536,7 @@ public void openProfile(boolean byAvatar, boolean fromChatAnimation, boolean rem if (parentFragment.isComments) { if (chat == null) return; - parentFragment.presentFragment(ProfileActivity.of(-chat.id), removeLast); + parentFragment.presentFragment(ProfileNewActivity.of(-chat.id), removeLast); return; } @@ -573,7 +573,7 @@ public void openProfile(boolean byAvatar, boolean fromChatAnimation, boolean rem } args.putBoolean("reportSpam", parentFragment.hasReportSpam()); args.putInt("actionBarColor", getThemedColor(Theme.key_actionBarDefault)); - ProfileActivity fragment = new ProfileActivity(args, sharedMediaPreloader); + ProfileNewActivity fragment = new ProfileNewActivity(args, sharedMediaPreloader); if (!monoforum) { fragment.setUserInfo(parentFragment.getCurrentUserInfo(), parentFragment.profileChannelMessageFetcher, parentFragment.birthdayAssetsFetcher); } @@ -590,7 +590,7 @@ public void openProfile(boolean byAvatar, boolean fromChatAnimation, boolean rem } else if (parentFragment.isTopic) { args.putLong("topic_id", parentFragment.getThreadMessage().getId()); } - ProfileActivity fragment = new ProfileActivity(args, sharedMediaPreloader); + ProfileNewActivity fragment = new ProfileNewActivity(args, sharedMediaPreloader); if (!monoforum) { fragment.setChatInfo(parentFragment.getCurrentChatInfo()); } diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/CollapsingHeaderOffsetListener.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/CollapsingHeaderOffsetListener.java new file mode 100644 index 00000000000..805f7c4cbed --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/CollapsingHeaderOffsetListener.java @@ -0,0 +1,433 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.AccelerateDecelerateInterpolator; + +import com.google.android.material.appbar.AppBarLayout; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.Components.CubicBezierInterpolator; + +/** + * Custom AppBarLayout.OnOffsetChangedListener that coordinates sophisticated + * collapsing header animations. + * Handles avatar scaling/translation and dual text view transitions based on + * scroll progress. + */ +public class CollapsingHeaderOffsetListener implements AppBarLayout.OnOffsetChangedListener { + + // Animation helpers + private AvatarAnimationHelper avatarAnimationHelper; + private TextTransitionHelper textTransitionHelper; + private HeaderButtonsAnimationHelper buttonAnimationHelper; + private HeaderScrollResponder scrollResponder; + + // Status bar color callback for collapse animation + private StatusBarColorCallback statusBarColorCallback; + + // Views to animate + private View avatarView; + private View expandedHeaderContainer; + private View collapsedTitleContainer; + private View nameView; + private View statusView; + private View headerButtonsContainer; + + // Layout parameters + private int totalScrollRange = 0; + private float previousProgress = -1f; + private boolean isInitialized = false; + + // Material Design animation thresholds for conditional smooth transitions + private static final float AVATAR_TRANSITION_THRESHOLD = 0.75f; // Avatar snaps to collapsed state + private static final float TEXT_TRANSITION_THRESHOLD = 0.65f; // Text snaps to toolbar position + private static final float BUTTON_COLLAPSE_START = 0.3f; // Buttons start collapsing + private static final float BUTTON_COLLAPSE_COMPLETE = 0.7f; // Buttons completely hidden + + // Legacy thresholds for reference + private static final float AVATAR_ANIMATION_START = 0.1f; // Start avatar animation early + private static final float TEXT_FADE_START = 0.0f; // Start text animation immediately + private static final float COLLAPSED_TEXT_SHOW = 0.8f; // Show collapsed text late (but we don't use this) + private static final float HEADER_BUTTONS_FADE = 0.4f; // Fade buttons mid-way + + // 60fps performance optimization - Material Design standard + private long lastUpdateTime = 0; + private static final long UPDATE_INTERVAL = 16; // 16ms = ~60fps + + // Material Design interpolators for smooth animations - using Telegram's proven + // patterns + private final DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(2.0f); + private final AccelerateDecelerateInterpolator smoothInterpolator = new AccelerateDecelerateInterpolator(); + // Telegram's premium interpolators for ultra-smooth animations + private static final CubicBezierInterpolator EASE_OUT_QUINT = CubicBezierInterpolator.EASE_OUT_QUINT; + private static final CubicBezierInterpolator EASE_OUT = CubicBezierInterpolator.EASE_OUT; + + /** + * Interface for status bar color callbacks during collapse animation + */ + public interface StatusBarColorCallback { + void onCollapseProgressChanged(float progress); + } + + public CollapsingHeaderOffsetListener() { + // Constructor + } + + /** + * Set animation helpers for coordinated animations + */ + public void setAnimationHelpers(AvatarAnimationHelper avatarHelper, TextTransitionHelper textHelper, + HeaderButtonsAnimationHelper buttonHelper) { + this.avatarAnimationHelper = avatarHelper; + this.textTransitionHelper = textHelper; + this.buttonAnimationHelper = buttonHelper; + } + + /** + * Set scroll responder for smooth animation coordination + */ + public void setScrollResponder(HeaderScrollResponder scrollResponder) { + this.scrollResponder = scrollResponder; + } + + /** + * Set status bar color callback for dynamic color changes during collapse + */ + public void setStatusBarColorCallback(StatusBarColorCallback callback) { + this.statusBarColorCallback = callback; + } + + /** + * Set views that will be animated during collapse/expand + */ + public void setAnimatedViews(View avatarView, View expandedContainer, View collapsedContainer, + View buttonsContainer) { + this.avatarView = avatarView; + this.expandedHeaderContainer = expandedContainer; + this.collapsedTitleContainer = collapsedContainer; + this.headerButtonsContainer = buttonsContainer; + } + + /** + * Set individual text views for accurate positioning + */ + public void setTextViews(View nameView, View statusView) { + this.nameView = nameView; + this.statusView = statusView; + } + + @Override + public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) { + // Performance optimization - limit update frequency for 60fps + long currentTime = System.currentTimeMillis(); + if (currentTime - lastUpdateTime < UPDATE_INTERVAL) { + return; + } + lastUpdateTime = currentTime; + + // Initialize if needed + if (!isInitialized) { + initializeScrollRange(appBarLayout); + } + + // Calculate scroll progress using Material Design standard calculation + float scrollProgress = calculateMaterialScrollProgress(verticalOffset); + + // Only update if progress has changed significantly to avoid unnecessary + // redraws + if (Math.abs(scrollProgress - previousProgress) < 0.001f) { + return; + } + previousProgress = scrollProgress; + + // Update all animations with smooth Material Design timing + updateAnimationsWithMaterialTiming(scrollProgress, verticalOffset, appBarLayout); + } + + private void initializeScrollRange(AppBarLayout appBarLayout) { + totalScrollRange = appBarLayout.getTotalScrollRange(); + + // Wait for layout to be ready before calculating positions + appBarLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + appBarLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this); + + // Initialize animation helpers with layout parameters after layout is ready + if (avatarAnimationHelper != null && avatarView != null) { + initializeAvatarAnimation(); + } + + if (textTransitionHelper != null) { + initializeTextAnimation(); + } + } + }); + + isInitialized = true; + } + + private void initializeAvatarAnimation() { + // Avatar should move only UPWARD and very slowly + // Remove leftward movement and reduce upward movement for slower animation + + float leftMovement = 0f; // No leftward movement + float upwardMovement = AndroidUtilities.dp(-50); // Even smaller upward movement for very slow animation + + float avatarSize = AndroidUtilities.dp(84); // Standard avatar size + float toolbarHeight = AndroidUtilities.dp(56); // Standard toolbar height + + // Initialize with simple relative movements - no complex calculations + avatarAnimationHelper.initializeLayout( + 0f, 0f, // Initial position (relative) + leftMovement, upwardMovement, // Target movement (only UP, no left) + avatarSize, toolbarHeight); + } + + private void initializeTextAnimation() { + // Text should move to the exact position next to the back button in the action + // bar + // Calculate proper position for text to end up next to back button + + // Move text to the left to position next to back button (back button is + // typically at left edge) + float leftMovement = AndroidUtilities.dp(-280); // Move further left to reach back button area + float upwardMovement = AndroidUtilities.dp(-200); // Move up to action bar level + + // Initialize with proper positioning for action bar placement + textTransitionHelper.initializeLayout( + 0f, 0f, 0f, AndroidUtilities.dp(20), // Initial positions (relative) + leftMovement, upwardMovement, // Name movement (left and up to action bar) + leftMovement, upwardMovement - AndroidUtilities.dp(15) // Status movement (left and up, slightly higher) + ); + } + + private float calculateMaterialScrollProgress(int verticalOffset) { + if (totalScrollRange == 0) { + return 0f; + } + + // Material Design standard: Convert negative offset to positive progress + // (0.0-1.0) + // verticalOffset is negative when scrolling up, so we use Math.abs + float progress = Math.abs((float) verticalOffset) / (float) totalScrollRange; + + // Ensure progress stays within bounds and apply smooth curve + progress = Math.max(0f, Math.min(1f, progress)); + + // Apply Material Design easing for smoother visual progression + // This creates a more natural feeling scroll animation + return applyMaterialEasing(progress); + } + + private float applyMaterialEasing(float progress) { + // Use Telegram's premium CubicBezierInterpolator for ultra-smooth animations + // EASE_OUT_QUINT provides the smoothest deceleration used throughout Telegram + return EASE_OUT_QUINT.getInterpolation(progress); + } + + private void updateAnimationsWithMaterialTiming(float scrollProgress, int verticalOffset, + AppBarLayout appBarLayout) { + // Update scroll responder for coordinated animations with smooth timing + if (scrollResponder != null) { + scrollResponder.onScrollChanged(scrollProgress); + } + + // Update avatar animation with conditional transitions + if (avatarAnimationHelper != null && avatarView != null) { + updateAvatarAnimationWithConditionalTransitions(scrollProgress); + } + + // Update text transition with conditional state switching + if (textTransitionHelper != null) { + updateTextAnimationWithConditionalTransitions(scrollProgress); + } + + // Update header buttons animation with smooth Y-direction collapse + if (buttonAnimationHelper != null && headerButtonsContainer != null) { + updateButtonAnimationsWithTiming(scrollProgress); + } + + // Update header container animations with proper timing + updateHeaderContainerAnimationsWithTiming(scrollProgress); + + // Update additional view animations + updateAdditionalAnimationsWithTiming(scrollProgress); + + // Update status bar color for collapse animation + if (statusBarColorCallback != null) { + statusBarColorCallback.onCollapseProgressChanged(scrollProgress); + } + } + + private void updateAvatarAnimationWithConditionalTransitions(float scrollProgress) { + // Make avatar animate only when collapsing header is near it + // Synchronize with text animation timing + + if (scrollProgress <= 0.7f) { + // Avatar stays in original position until collapsing header is very near + avatarAnimationHelper.updateAnimation(0f); + } else if (scrollProgress >= 0.98f) { + // Collapsed state - avatar is in toolbar position + avatarAnimationHelper.updateAnimation(1f); + } else { + // Ultra-slow animation zone (70% to 98% - synchronized with text) + float transitionProgress = (scrollProgress - 0.7f) / 0.28f; + // Apply very slow EASE_OUT for ultra-slow avatar motion + float smoothProgress = CubicBezierInterpolator.EASE_OUT.getInterpolation(transitionProgress); + avatarAnimationHelper.updateAnimation(smoothProgress); + } + + avatarAnimationHelper.applyAnimationToView(avatarView); + } + + private void updateTextAnimationWithConditionalTransitions(float scrollProgress) { + // Use gradual transition for text translation movement + // This allows users to see the "left upward motion" as requested + + if (textTransitionHelper != null) { + // Pass the actual scroll progress for gradual translation animation + textTransitionHelper.updateAnimation(scrollProgress); + } + } + + private void updateButtonAnimationsWithTiming(float scrollProgress) { + // Update header buttons with Y-direction collapse animation synchronized with + // header + if (buttonAnimationHelper != null) { + buttonAnimationHelper.updateAnimation(scrollProgress); + } + } + + private void updateHeaderContainerAnimationsWithTiming(float scrollProgress) { + // Fade out expanded header container with smooth timing + if (expandedHeaderContainer != null) { + float expandedAlpha = 1f - applyMaterialEasing(scrollProgress); + expandedHeaderContainer.setAlpha(Math.max(0f, expandedAlpha)); + + // Apply subtle scale animation with smooth easing + float scaleProgress = applyMaterialEasing(scrollProgress); + float scale = 1f - (scaleProgress * 0.05f); // Slight scale down + expandedHeaderContainer.setScaleX(scale); + expandedHeaderContainer.setScaleY(scale); + } + + // DO NOT fade in collapsed title container - let text translate smoothly + // instead + // The text will be handled by the TextTransitionHelper for smooth translation + if (collapsedTitleContainer != null) { + // Keep collapsed container fully transparent since we're using smooth text + // translation + collapsedTitleContainer.setAlpha(0f); + } + } + + private void updateAdditionalAnimationsWithTiming(float scrollProgress) { + // Handle header buttons fade out with smooth Material Design timing + if (scrollProgress >= HEADER_BUTTONS_FADE) { + // Apply smooth easing to button fade animation + float buttonFadeProgress = (scrollProgress - HEADER_BUTTONS_FADE) / (1f - HEADER_BUTTONS_FADE); + float easedProgress = applyMaterialEasing(buttonFadeProgress); + // Button fade implementation would go here based on specific layout + // requirements + } + + // Add any additional animations with Material Design timing + // For example: background color transitions, elevation changes, etc. + } + + /** + * Get current scroll progress for external use + */ + public float getCurrentScrollProgress() { + return previousProgress; + } + + /** + * Check if header is in collapsed state + */ + public boolean isCollapsed() { + return previousProgress >= 0.9f; + } + + /** + * Check if header is in expanded state + */ + public boolean isExpanded() { + return previousProgress <= 0.1f; + } + + /** + * Reset all animations to initial state + */ + public void resetAnimations() { + previousProgress = 0f; + + if (avatarAnimationHelper != null) { + avatarAnimationHelper.reset(); + } + + if (textTransitionHelper != null) { + textTransitionHelper.reset(); + } + + if (buttonAnimationHelper != null) { + buttonAnimationHelper.reset(); + } + + if (scrollResponder != null) { + scrollResponder.resetAnimation(); + } + + // Reset view states + if (avatarView != null) { + avatarView.setScaleX(1f); + avatarView.setScaleY(1f); + avatarView.setTranslationX(0f); + avatarView.setTranslationY(0f); + avatarView.setAlpha(1f); + } + + if (expandedHeaderContainer != null) { + expandedHeaderContainer.setAlpha(1f); + expandedHeaderContainer.setScaleX(1f); + expandedHeaderContainer.setScaleY(1f); + } + + if (collapsedTitleContainer != null) { + collapsedTitleContainer.setAlpha(0f); + } + } + + /** + * Enable or disable animations for performance + */ + public void setAnimationsEnabled(boolean enabled) { + if (!enabled) { + // Disable animations and reset to neutral state + resetAnimations(); + } + } + + /** + * Set animation quality for performance optimization + */ + public void setAnimationQuality(float quality) { + // quality: 0.0 = lowest, 1.0 = highest + // Could be used to reduce animation complexity on low-end devices + quality = Math.max(0f, Math.min(1f, quality)); + + // Adjust update interval based on quality + // Lower quality = less frequent updates + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderButtonsAnimationHelper.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderButtonsAnimationHelper.java new file mode 100644 index 00000000000..bf142aecd73 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderButtonsAnimationHelper.java @@ -0,0 +1,232 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.Components.CubicBezierInterpolator; + +/** + * Helper class for animating header bubble buttons during collapsing header transitions. + * Handles Y-direction collapse animations synchronized with header movement. + */ +public class HeaderButtonsAnimationHelper { + + // Animation configuration - transition thresholds for smooth state changes + // Even wider range for smoother, slower animations (both collapse and uncollapse) + private static final float COLLAPSE_START_THRESHOLD = 0.1f; // When buttons start collapsing (even earlier) + private static final float COLLAPSE_COMPLETE_THRESHOLD = 0.9f; // When buttons completely disappear (even later) + private static final float UNCOLLAPSE_START_THRESHOLD = 0.1f; // When buttons start appearing (even earlier) + + // Material Design interpolators for smooth animations + private static final CubicBezierInterpolator EASE_OUT_QUINT = CubicBezierInterpolator.EASE_OUT_QUINT; + private static final CubicBezierInterpolator EASE_OUT = CubicBezierInterpolator.EASE_OUT; + + // Animation state + private float currentProgress = 0f; + private float currentAlpha = 1f; + private float currentScaleY = 1f; + private float currentTranslationY = 0f; + + // Views to animate + private ViewGroup headerButtonsContainer; + private View[] buttonViews; + + // Layout parameters + private float initialHeight; + private float headerHeight; + private boolean isInitialized = false; + + public HeaderButtonsAnimationHelper() { + // Constructor + } + + /** + * Set the header buttons container and individual button views + */ + public void setButtonViews(ViewGroup buttonsContainer, View... buttons) { + this.headerButtonsContainer = buttonsContainer; + this.buttonViews = buttons; + } + + /** + * Initialize layout parameters for smooth transitions + */ + public void initializeLayout(float headerHeight) { + this.headerHeight = headerHeight; + if (headerButtonsContainer != null) { + this.initialHeight = headerButtonsContainer.getHeight(); + } + this.isInitialized = true; + } + + /** + * Update button animations based on scroll progress with conditional transitions + */ + public void updateAnimation(float progress) { + this.currentProgress = Math.max(0f, Math.min(1f, progress)); + calculateButtonAnimationValues(); + applyAnimationsToButtons(); + } + + /** + * Calculate animation values with ultra-smooth gradual transitions + */ + private void calculateButtonAnimationValues() { + if (!isInitialized) return; + + // Use gradual animation throughout the range for smoother feel + if (currentProgress <= UNCOLLAPSE_START_THRESHOLD) { + // Fully expanded state - buttons are completely visible + currentAlpha = 1f; + currentScaleY = 1f; + currentTranslationY = 0f; + } else if (currentProgress >= COLLAPSE_COMPLETE_THRESHOLD) { + // Fully collapsed state - buttons are completely hidden + currentAlpha = 0f; + currentScaleY = 0.25f; // Match the calculation: 1f - (1f * 0.75f) = 0.25f for consistency + currentTranslationY = AndroidUtilities.dp(-12); // Match the calculation for smooth transition + } else { + // Extended transition zone - ultra-smooth animation between states + float transitionProgress = (currentProgress - UNCOLLAPSE_START_THRESHOLD) / + (COLLAPSE_COMPLETE_THRESHOLD - UNCOLLAPSE_START_THRESHOLD); + + // Use symmetric easing that works smoothly in both directions (collapse and uncollapse) + // EASE_OUT works great for uncollapsing, so use EASE_IN for collapsing to mirror the smoothness + float symmetricProgress = EASE_OUT.getInterpolation(transitionProgress); + + // Apply the same smooth curve to all properties for consistent feel + currentAlpha = 1f - symmetricProgress; + currentScaleY = 1f - (symmetricProgress * 0.75f); // Slightly gentler scale for smoother collapse + currentTranslationY = symmetricProgress * AndroidUtilities.dp(-12); // Even more subtle upward movement + } + } + + /** + * Apply calculated animations to button views + */ + private void applyAnimationsToButtons() { + if (headerButtonsContainer != null) { + // Apply container-level animations + headerButtonsContainer.setAlpha(currentAlpha); + headerButtonsContainer.setScaleY(currentScaleY); + headerButtonsContainer.setTranslationY(currentTranslationY); + + // Remove rotation effect to eliminate chunkiness - keep animation simple and smooth + headerButtonsContainer.setRotation(0f); + } + + // Apply individual button animations with ultra-smooth staggered effect + if (buttonViews != null) { + for (int i = 0; i < buttonViews.length; i++) { + View button = buttonViews[i]; + if (button != null) { + // Reduced stagger delay for smoother collective animation + float staggerDelay = i * 0.02f; // Reduced to 20ms delay for smoother feel + float adjustedProgress = Math.max(0f, currentProgress - staggerDelay); + + if (adjustedProgress <= UNCOLLAPSE_START_THRESHOLD) { + button.setAlpha(1f); + button.setScaleX(1f); + button.setScaleY(1f); + } else if (adjustedProgress >= COLLAPSE_COMPLETE_THRESHOLD) { + button.setAlpha(0f); + button.setScaleX(0.92f); // Match calculation: 1f - (1f * 0.08f) = 0.92f for consistency + button.setScaleY(0.92f); + } else { + float buttonTransitionProgress = (adjustedProgress - UNCOLLAPSE_START_THRESHOLD) / + (COLLAPSE_COMPLETE_THRESHOLD - UNCOLLAPSE_START_THRESHOLD); + + // Use the same symmetric easing as container for consistent smooth feel + float buttonSymmetricProgress = EASE_OUT.getInterpolation(buttonTransitionProgress); + + button.setAlpha(1f - buttonSymmetricProgress); + float scale = 1f - (buttonSymmetricProgress * 0.08f); // Even gentler scaling for ultra-smooth feel + button.setScaleX(scale); + button.setScaleY(scale); + } + } + } + } + } + + /** + * Reset animations to initial state + */ + public void reset() { + currentProgress = 0f; + currentAlpha = 1f; + currentScaleY = 1f; + currentTranslationY = 0f; + applyAnimationsToButtons(); + } + + /** + * Check if buttons are in collapsed state + */ + public boolean isCollapsed() { + return currentProgress >= COLLAPSE_COMPLETE_THRESHOLD; + } + + /** + * Check if buttons are in expanded state + */ + public boolean isExpanded() { + return currentProgress <= UNCOLLAPSE_START_THRESHOLD; + } + + /** + * Get current animation progress + */ + public float getAnimationProgress() { + return currentProgress; + } + + /** + * Get current alpha value + */ + public float getCurrentAlpha() { + return currentAlpha; + } + + /** + * Check if buttons are in transition state + */ + public boolean isInTransition() { + return currentProgress > UNCOLLAPSE_START_THRESHOLD && currentProgress < COLLAPSE_COMPLETE_THRESHOLD; + } + + /** + * Trigger manual collapse animation + */ + public void triggerCollapseAnimation() { + if (headerButtonsContainer != null) { + // Could implement a manual animation trigger here if needed + } + } + + /** + * Set animation enabled/disabled + */ + public void setAnimationEnabled(boolean enabled) { + if (!enabled) { + reset(); + } + } + + /** + * Set custom collapse thresholds for fine-tuning + */ + public void setCollapseThresholds(float startThreshold, float completeThreshold) { + // Could be used to customize animation timing based on header design + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderGiftParticleSystem.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderGiftParticleSystem.java new file mode 100644 index 00000000000..84c28e372fd --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderGiftParticleSystem.java @@ -0,0 +1,354 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.SharedConfig; +import org.telegram.ui.ActionBar.Theme; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class HeaderGiftParticleSystem { + + private static final int MAX_PARTICLES = 20; + private static final int MAX_PARTICLES_LOW_PERFORMANCE = 10; + private static final float PARTICLE_SPAWN_RATE = 0.5f; // particles per second + private static final float PARTICLE_LIFETIME = 8f; // seconds + private static final float PARTICLE_SPEED = 30f; // dp per second + private static final float PARTICLE_SIZE = 24f; // dp + + // Gift emoji types based on the mockup + private static final String[] GIFT_EMOJIS = { + "🎁", "🎀", "🎊", "🎉", "💝", "🌟", "✨", "🎈", "🎂", "🍰", "🎆", "🎇" + }; + + private final Context context; + private final Theme.ResourcesProvider resourcesProvider; + private final List particles; + private final Random random; + private final Paint emojiPaint; + private final DecelerateInterpolator decelerateInterpolator; + private final LinearInterpolator linearInterpolator; + + // Animation state + private float spawnTimer = 0f; + private float intensity = 0.2f; + private boolean isAnimating = false; + private boolean giftsVisible = true; + + // Bounds + private int boundsWidth = 0; + private int boundsHeight = 0; + + // Performance optimization + private int performanceClass = SharedConfig.PERFORMANCE_CLASS_AVERAGE; + private int maxParticles = MAX_PARTICLES; + private boolean performanceOptimization = false; + private float averageFrameTime = 0f; + private long frameTimeSum = 0; + private int frameCount = 0; + + public HeaderGiftParticleSystem(Context context, Theme.ResourcesProvider resourcesProvider) { + this.context = context; + this.resourcesProvider = resourcesProvider; + this.particles = new ArrayList<>(); + this.random = new Random(); + this.decelerateInterpolator = new DecelerateInterpolator(2.0f); + this.linearInterpolator = new LinearInterpolator(); + this.emojiPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + initializePaint(); + setPerformanceClass(SharedConfig.getDevicePerformanceClass()); + } + + private void initializePaint() { + emojiPaint.setTextAlign(Paint.Align.CENTER); + emojiPaint.setTextSize(AndroidUtilities.dp(PARTICLE_SIZE)); + emojiPaint.setAlpha(255); + } + + public void setPerformanceClass(int performanceClass) { + this.performanceClass = performanceClass; + + switch (performanceClass) { + case SharedConfig.PERFORMANCE_CLASS_LOW: + maxParticles = MAX_PARTICLES_LOW_PERFORMANCE; + break; + case SharedConfig.PERFORMANCE_CLASS_AVERAGE: + maxParticles = (int) (MAX_PARTICLES * 0.8f); + break; + case SharedConfig.PERFORMANCE_CLASS_HIGH: + default: + maxParticles = MAX_PARTICLES; + break; + } + + // Remove excess particles if needed + while (particles.size() > maxParticles) { + particles.remove(particles.size() - 1); + } + } + + public void setBounds(int left, int top, int right, int bottom) { + this.boundsWidth = right - left; + this.boundsHeight = bottom - top; + } + + public void setIntensity(float intensity) { + this.intensity = Math.max(0f, Math.min(1f, intensity)); + } + + public void setScrollProgress(float scrollProgress) { + // Reduce particle visibility and move them closer to avatar as user scrolls + for (GiftParticle particle : particles) { + // Move particles closer to avatar center + float targetRadius = AndroidUtilities.dp(40) * (1.0f - scrollProgress * 0.7f); + particle.orbitRadius = targetRadius + AndroidUtilities.dp(20); + + // Fade out particles as user scrolls + float baseAlpha = 1.0f - scrollProgress; + particle.alpha = Math.max(0f, baseAlpha); + } + } + + public void setGiftsVisible(boolean visible) { + this.giftsVisible = visible; + } + + public void startAnimation() { + isAnimating = true; + spawnTimer = 0f; + } + + public void stopAnimation() { + isAnimating = false; + } + + public void update(float deltaTime) { + if (!isAnimating || !giftsVisible) return; + + long frameStartTime = System.currentTimeMillis(); + + // Update spawn timer + spawnTimer += deltaTime; + + // Spawn new particles + float spawnRate = PARTICLE_SPAWN_RATE * intensity; + if (spawnRate > 0 && spawnTimer >= 1f / spawnRate) { + spawnParticle(); + spawnTimer = 0f; + } + + // Update existing particles + for (int i = particles.size() - 1; i >= 0; i--) { + GiftParticle particle = particles.get(i); + particle.update(deltaTime); + + // Remove dead particles + if (particle.isDead()) { + particles.remove(i); + } + } + + // Performance monitoring + updatePerformanceMetrics(System.currentTimeMillis() - frameStartTime); + } + + private void spawnParticle() { + if (particles.size() >= maxParticles || boundsWidth <= 0 || boundsHeight <= 0) { + return; + } + + GiftParticle particle = new GiftParticle(); + + // Calculate avatar center position (centered in header) + float avatarCenterX = boundsWidth / 2f; // Center horizontally + float avatarCenterY = AndroidUtilities.dp(21); // Half of avatar height + + // Random angle around the avatar + float angle = random.nextFloat() * 360f; + float radius = AndroidUtilities.dp(60 + random.nextFloat() * 40); // Distance from avatar + + // Position particle in circle around avatar + particle.x = avatarCenterX + (float) (Math.cos(Math.toRadians(angle)) * radius); + particle.y = avatarCenterY + (float) (Math.sin(Math.toRadians(angle)) * radius); + + // Circular motion parameters + particle.orbitCenterX = avatarCenterX; + particle.orbitCenterY = avatarCenterY; + particle.orbitRadius = radius; + particle.orbitAngle = angle; + particle.orbitSpeed = 10f + random.nextFloat() * 20f; // degrees per second + + // Gentle floating motion + particle.velocityX = (random.nextFloat() - 0.5f) * AndroidUtilities.dp(5); + particle.velocityY = (random.nextFloat() - 0.5f) * AndroidUtilities.dp(5); + + // Random rotation + particle.rotation = random.nextFloat() * 360f; + particle.rotationSpeed = (random.nextFloat() - 0.5f) * 30f; // Slower rotation + + // Random emoji + particle.emoji = GIFT_EMOJIS[random.nextInt(GIFT_EMOJIS.length)]; + + // Random scale variation + particle.scale = 0.6f + random.nextFloat() * 0.4f; + + // Lifetime + particle.maxLifetime = PARTICLE_LIFETIME + random.nextFloat() * 4f; + + particles.add(particle); + } + + public void triggerGiftBurst() { + if (!isAnimating || !giftsVisible) return; + + // Spawn multiple particles at once for burst effect + int burstCount = Math.min(5, maxParticles - particles.size()); + for (int i = 0; i < burstCount; i++) { + spawnParticle(); + } + } + + public void draw(Canvas canvas) { + if (!isAnimating || !giftsVisible || particles.isEmpty()) return; + + for (GiftParticle particle : particles) { + particle.draw(canvas, emojiPaint); + } + } + + public void updateTheme() { + // Update paint colors based on theme if needed + // The emoji paint doesn't need theme updates, but we keep this for consistency + } + + public void setPerformanceOptimization(boolean enabled) { + this.performanceOptimization = enabled; + + if (enabled) { + // Reduce particle count for better performance + maxParticles = Math.max(5, maxParticles / 2); + while (particles.size() > maxParticles) { + particles.remove(particles.size() - 1); + } + } else { + // Restore normal particle count + setPerformanceClass(performanceClass); + } + } + + private void updatePerformanceMetrics(long frameTime) { + frameTimeSum += frameTime; + frameCount++; + + if (frameCount >= 60) { // Update every 60 frames + averageFrameTime = frameTimeSum / (float) frameCount; + frameTimeSum = 0; + frameCount = 0; + + // Auto-optimize performance if needed + if (performanceOptimization && averageFrameTime > 16.7f) { // 60 FPS threshold + setPerformanceOptimization(true); + } + } + } + + public float getAverageFrameTime() { + return averageFrameTime; + } + + private static class GiftParticle { + float x, y; + float velocityX, velocityY; + float rotation, rotationSpeed; + float scale = 1f; + float alpha = 1f; + float lifetime = 0f; + float maxLifetime = PARTICLE_LIFETIME; + String emoji; + + // Orbital motion properties + float orbitCenterX, orbitCenterY; + float orbitRadius; + float orbitAngle; + float orbitSpeed; + + private final RectF tempRect = new RectF(); + private final Path tempPath = new Path(); + + void update(float deltaTime) { + lifetime += deltaTime; + + // Update orbital motion + orbitAngle += orbitSpeed * deltaTime; + if (orbitAngle > 360f) orbitAngle -= 360f; + + // Calculate new position based on orbit + x = orbitCenterX + (float) (Math.cos(Math.toRadians(orbitAngle)) * orbitRadius); + y = orbitCenterY + (float) (Math.sin(Math.toRadians(orbitAngle)) * orbitRadius); + + // Add gentle floating motion + x += velocityX * deltaTime; + y += velocityY * deltaTime; + + // Update rotation + rotation += rotationSpeed * deltaTime; + + // Update alpha based on lifetime + float lifeProgress = lifetime / maxLifetime; + if (lifeProgress > 0.8f) { + // Fade out in the last 20% of lifetime + alpha = 1f - ((lifeProgress - 0.8f) / 0.2f); + } else { + alpha = 1f; + } + + // Add subtle breathing effect to floating motion + float breatheEffect = (float) (Math.sin(lifetime * 3f) * AndroidUtilities.dp(2)); + velocityX += breatheEffect * deltaTime * 0.1f; + velocityY += breatheEffect * deltaTime * 0.1f; + } + + void draw(Canvas canvas, Paint paint) { + if (alpha <= 0f) return; + + paint.setAlpha((int) (255 * alpha)); + + canvas.save(); + canvas.translate(x, y); + canvas.rotate(rotation); + canvas.scale(scale, scale); + + // Draw the emoji + float textSize = paint.getTextSize(); + Paint.FontMetrics fontMetrics = paint.getFontMetrics(); + float textHeight = fontMetrics.descent - fontMetrics.ascent; + float textOffset = (textHeight / 2) - fontMetrics.descent; + + canvas.drawText(emoji, 0, textOffset, paint); + + canvas.restore(); + } + + boolean isDead() { + return lifetime >= maxLifetime || alpha <= 0f; + } + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderGradientController.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderGradientController.java new file mode 100644 index 00000000000..7cb41e2b341 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderGradientController.java @@ -0,0 +1,290 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Shader; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.ActionBar.Theme; + +public class HeaderGradientController { + + private final Context context; + private final Theme.ResourcesProvider resourcesProvider; + private final Paint gradientPaint; + private final Paint overlayPaint; + private final Matrix gradientMatrix; + + // Gradient properties + private LinearGradient baseGradient; + private RadialGradient accentGradient; + private int[] gradientColors; + private int[] accentColors; + private float[] gradientPositions; + + // Animation state + private float animationTime = 0f; + private float scrollProgress = 0f; + private boolean isDarkTheme = false; + + // Bounds + private int boundsWidth = 0; + private int boundsHeight = 0; + + // Theme colors + private int baseColor; + private int accentColor; + private int overlayColor; + + public HeaderGradientController(Context context, Theme.ResourcesProvider resourcesProvider) { + this.context = context; + this.resourcesProvider = resourcesProvider; + + // Initialize paint objects + gradientPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + gradientPaint.setDither(true); + + overlayPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + overlayPaint.setDither(true); + + gradientMatrix = new Matrix(); + + // Initialize gradient positions + gradientPositions = new float[]{0f, 0.3f, 0.7f, 1f}; + + updateTheme(); + } + + public void updateTheme() { + isDarkTheme = Theme.isCurrentThemeDark(); + + // Get theme colors + baseColor = getThemedColor(Theme.key_profile_headerAnimationGradientStart); + accentColor = getThemedColor(Theme.key_profile_headerAnimationGradientEnd); + overlayColor = getThemedColor(Theme.key_profile_headerAnimationOverlay); + + // Create color arrays for gradients + if (isDarkTheme) { + gradientColors = new int[]{ + Color.argb(180, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor)), + Color.argb(120, Color.red(accentColor), Color.green(accentColor), Color.blue(accentColor)), + Color.argb(80, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor)), + Color.argb(40, Color.red(accentColor), Color.green(accentColor), Color.blue(accentColor)) + }; + + int accentThemeColor = getThemedColor(Theme.key_profile_headerAnimationAccent); + accentColors = new int[]{ + Color.argb(60, Color.red(accentThemeColor), Color.green(accentThemeColor), Color.blue(accentThemeColor)), + Color.argb(20, Color.red(accentThemeColor), Color.green(accentThemeColor), Color.blue(accentThemeColor)), + Color.argb(0, Color.red(accentThemeColor), Color.green(accentThemeColor), Color.blue(accentThemeColor)) + }; + } else { + gradientColors = new int[]{ + Color.argb(220, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor)), + Color.argb(160, Color.red(accentColor), Color.green(accentColor), Color.blue(accentColor)), + Color.argb(100, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor)), + Color.argb(60, Color.red(accentColor), Color.green(accentColor), Color.blue(accentColor)) + }; + + int accentThemeColor = getThemedColor(Theme.key_profile_headerAnimationAccent); + accentColors = new int[]{ + Color.argb(80, Color.red(accentThemeColor), Color.green(accentThemeColor), Color.blue(accentThemeColor)), + Color.argb(40, Color.red(accentThemeColor), Color.green(accentThemeColor), Color.blue(accentThemeColor)), + Color.argb(0, Color.red(accentThemeColor), Color.green(accentThemeColor), Color.blue(accentThemeColor)) + }; + } + + // Recreate gradients if bounds are set + if (boundsWidth > 0 && boundsHeight > 0) { + createGradients(); + } + } + + private int getThemedColor(int key) { + return Theme.getColor(key, resourcesProvider); + } + + public void setBounds(int left, int top, int right, int bottom) { + this.boundsWidth = right - left; + this.boundsHeight = bottom - top; + + if (boundsWidth > 0 && boundsHeight > 0) { + createGradients(); + } + } + + private void createGradients() { + // Validate arrays before creating gradients + if (gradientColors == null || gradientPositions == null || + gradientColors.length != gradientPositions.length) { + return; + } + + // Create base gradient (vertical) + baseGradient = new LinearGradient( + 0, 0, 0, boundsHeight, + gradientColors, + gradientPositions, + Shader.TileMode.CLAMP + ); + + // Create accent gradient (radial, centered at top) + if (accentColors != null && accentColors.length > 0) { + float centerX = boundsWidth / 2f; + float centerY = boundsHeight * 0.3f; + float radius = Math.max(boundsWidth, boundsHeight) * 0.8f; + + float[] accentPositions; + if (accentColors.length == 2) { + // Low performance mode - use 2 colors + accentPositions = new float[]{0f, 1f}; + } else { + // Normal mode - use 3 colors + accentPositions = new float[]{0f, 0.5f, 1f}; + } + + accentGradient = new RadialGradient( + centerX, centerY, radius, + accentColors, + accentPositions, + Shader.TileMode.CLAMP + ); + } + } + + public void update(float deltaTime, float scrollProgress) { + this.animationTime += deltaTime; + this.scrollProgress = scrollProgress; + + // Update gradient transformations based on scroll and animation + updateGradientTransformation(); + } + + private void updateGradientTransformation() { + if (baseGradient == null || accentGradient == null) return; + + // Reset matrix + gradientMatrix.reset(); + + // Apply scroll-based transformation + float scrollOffset = scrollProgress * boundsHeight * 0.1f; + + // Apply subtle animation movement + float animationOffset = (float) (Math.sin(animationTime * 0.5f) * AndroidUtilities.dp(10)); + + // Apply transformations + gradientMatrix.postTranslate(animationOffset, scrollOffset); + + // Apply matrix to gradients + baseGradient.setLocalMatrix(gradientMatrix); + accentGradient.setLocalMatrix(gradientMatrix); + } + + public void draw(Canvas canvas, float scrollProgress) { + if (boundsWidth <= 0 || boundsHeight <= 0) return; + + // Update scroll progress + this.scrollProgress = scrollProgress; + + // Calculate alpha based on scroll progress + float baseAlpha = 0.8f + (scrollProgress * 0.2f); + float accentAlpha = 0.3f + (scrollProgress * 0.4f); + + // Draw base gradient + if (baseGradient != null) { + gradientPaint.setShader(baseGradient); + gradientPaint.setAlpha((int) (255 * baseAlpha)); + canvas.drawRect(0, 0, boundsWidth, boundsHeight, gradientPaint); + } + + // Draw accent gradient + if (accentGradient != null) { + gradientPaint.setShader(accentGradient); + gradientPaint.setAlpha((int) (255 * accentAlpha)); + canvas.drawRect(0, 0, boundsWidth, boundsHeight, gradientPaint); + } + + // Draw subtle overlay for better text readability + drawOverlay(canvas, scrollProgress); + } + + private void drawOverlay(Canvas canvas, float scrollProgress) { + // Create a subtle overlay that becomes more visible when scrolling + float overlayAlpha = scrollProgress * 0.1f; + + if (overlayAlpha > 0) { + overlayPaint.setColor(overlayColor); + overlayPaint.setAlpha((int) (255 * overlayAlpha)); + canvas.drawRect(0, 0, boundsWidth, boundsHeight, overlayPaint); + } + } + + // Animation effects for special events + public void pulseEffect() { + // Create a pulse effect by temporarily modifying the gradient + // This could be triggered when gifts are received or profile is viewed + // Implementation depends on specific requirements + } + + public void setIntensity(float intensity) { + // Adjust gradient intensity based on external factors + // This could be used to make the gradient more or less prominent + intensity = Math.max(0f, Math.min(1f, intensity)); + + // Update gradient colors based on intensity + if (gradientColors != null) { + for (int i = 0; i < gradientColors.length; i++) { + int color = gradientColors[i]; + int alpha = (int) (Color.alpha(color) * intensity); + gradientColors[i] = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + // Recreate gradients with new colors + if (boundsWidth > 0 && boundsHeight > 0) { + createGradients(); + } + } + } + + public void setCustomColors(int primaryColor, int secondaryColor) { + // Allow custom colors for special profile types (Premium, Business, etc.) + this.baseColor = primaryColor; + this.accentColor = secondaryColor; + updateTheme(); + } + + // Performance optimization methods + public void setLowPerformanceMode(boolean enabled) { + if (enabled) { + // Simplify gradients for better performance + gradientPositions = new float[]{0f, 1f}; + if (gradientColors != null && gradientColors.length > 1) { + gradientColors = new int[]{gradientColors[0], gradientColors[gradientColors.length - 1]}; + } + if (accentColors != null && accentColors.length > 1) { + accentColors = new int[]{accentColors[0], accentColors[accentColors.length - 1]}; + } + } else { + // Restore full gradient quality + gradientPositions = new float[]{0f, 0.3f, 0.7f, 1f}; + updateTheme(); + } + + if (boundsWidth > 0 && boundsHeight > 0) { + createGradients(); + } + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderScrollResponder.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderScrollResponder.java new file mode 100644 index 00000000000..125242cf536 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/HeaderScrollResponder.java @@ -0,0 +1,294 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.animation.ValueAnimator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.LinearInterpolator; + +import org.telegram.messenger.AndroidUtilities; + +public class HeaderScrollResponder { + + private ProfileHeaderAnimationView animationView; + private ValueAnimator smoothAnimator; + private DecelerateInterpolator decelerateInterpolator; + private LinearInterpolator linearInterpolator; + + // Animation helpers for collapsing header + private AvatarAnimationHelper avatarAnimationHelper; + private TextTransitionHelper textTransitionHelper; + private HeaderButtonsAnimationHelper buttonAnimationHelper; + + // Scroll state + private float currentScrollProgress = 0f; + private float targetScrollProgress = 0f; + private float scrollVelocity = 0f; + private long lastScrollTime = 0; + private boolean isScrolling = false; + + // Animation parameters + private static final float SMOOTH_FACTOR = 0.1f; + private static final float VELOCITY_THRESHOLD = 0.01f; + private static final long SCROLL_TIMEOUT = 150; // ms + private static final float MAX_SCROLL_PROGRESS = 1f; + + // Intensity control + private float baseIntensity = 0.2f; + private float maxIntensity = 0.8f; + private float intensityMultiplier = 1f; + + public HeaderScrollResponder() { + decelerateInterpolator = new DecelerateInterpolator(2.0f); + linearInterpolator = new LinearInterpolator(); + lastScrollTime = System.currentTimeMillis(); + } + + public void setAnimationView(ProfileHeaderAnimationView animationView) { + this.animationView = animationView; + } + + /** + * Set animation helpers for collapsing header animations + */ + public void setAnimationHelpers(AvatarAnimationHelper avatarHelper, TextTransitionHelper textHelper) { + this.avatarAnimationHelper = avatarHelper; + this.textTransitionHelper = textHelper; + } + + /** + * Set button animation helper + */ + public void setButtonAnimationHelper(HeaderButtonsAnimationHelper buttonHelper) { + this.buttonAnimationHelper = buttonHelper; + } + + public void onScrollChanged(float scrollProgress) { + long currentTime = System.currentTimeMillis(); + + // Clamp scroll progress + scrollProgress = Math.max(0f, Math.min(MAX_SCROLL_PROGRESS, scrollProgress)); + + // Calculate velocity + float deltaProgress = scrollProgress - targetScrollProgress; + float deltaTime = (currentTime - lastScrollTime) / 1000f; + + if (deltaTime > 0) { + scrollVelocity = deltaProgress / deltaTime; + } + + targetScrollProgress = scrollProgress; + lastScrollTime = currentTime; + isScrolling = true; + + // Update animation immediately for responsive feel + updateAnimation(); + + // Start smooth animation to target + startSmoothAnimation(); + } + + private void startSmoothAnimation() { + if (smoothAnimator != null) { + smoothAnimator.cancel(); + } + + smoothAnimator = ValueAnimator.ofFloat(currentScrollProgress, targetScrollProgress); + smoothAnimator.setDuration(200); + smoothAnimator.setInterpolator(decelerateInterpolator); + smoothAnimator.addUpdateListener(animation -> { + currentScrollProgress = (Float) animation.getAnimatedValue(); + updateAnimation(); + }); + smoothAnimator.start(); + } + + private void updateAnimation() { + // Update collapsing header animations + if (avatarAnimationHelper != null) { + avatarAnimationHelper.updateAnimation(currentScrollProgress); + } + + if (textTransitionHelper != null) { + textTransitionHelper.updateAnimation(currentScrollProgress); + } + + if (buttonAnimationHelper != null) { + buttonAnimationHelper.updateAnimation(currentScrollProgress); + } + + // Update particle animation view if present + if (animationView != null) { + // Calculate animation intensity based on scroll progress and velocity + float intensity = calculateIntensity(); + animationView.setScrollProgress(currentScrollProgress); + } + + // Check if we should trigger special effects + checkForSpecialEffects(); + + // Update scrolling state + checkScrollingState(); + } + + private float calculateIntensity() { + // Base intensity from scroll progress + float scrollIntensity = baseIntensity + (currentScrollProgress * (maxIntensity - baseIntensity)); + + // Add velocity-based intensity boost + float velocityBoost = Math.min(Math.abs(scrollVelocity) * 0.5f, 0.3f); + + // Combine intensities + float totalIntensity = scrollIntensity + velocityBoost; + + // Apply multiplier and clamp + return Math.max(0f, Math.min(1f, totalIntensity * intensityMultiplier)); + } + + private void checkForSpecialEffects() { + // Trigger gift burst on rapid scroll up + if (scrollVelocity > 2f && currentScrollProgress > 0.5f) { + if (animationView != null) { + animationView.triggerGiftAnimation(); + } + } + + // Trigger pulse effect on scroll direction change + if (Math.abs(scrollVelocity) > VELOCITY_THRESHOLD) { + float previousVelocity = scrollVelocity; + if (previousVelocity > 0 && scrollVelocity < 0 || previousVelocity < 0 && scrollVelocity > 0) { + // Direction changed - could trigger special effect + triggerDirectionChangeEffect(); + } + } + } + + private void triggerDirectionChangeEffect() { + // This could trigger a subtle animation effect when scroll direction changes + // Implementation depends on specific design requirements + } + + private void checkScrollingState() { + long currentTime = System.currentTimeMillis(); + + if (currentTime - lastScrollTime > SCROLL_TIMEOUT) { + if (isScrolling) { + isScrolling = false; + onScrollingStopped(); + } + } + } + + private void onScrollingStopped() { + // Reset velocity + scrollVelocity = 0f; + + // Smooth animation to settle state + if (animationView != null) { + // Could implement a settle animation here + } + } + + // Public API for external control + public void setIntensityMultiplier(float multiplier) { + this.intensityMultiplier = Math.max(0f, Math.min(2f, multiplier)); + } + + public void setBaseIntensity(float intensity) { + this.baseIntensity = Math.max(0f, Math.min(1f, intensity)); + } + + public void setMaxIntensity(float intensity) { + this.maxIntensity = Math.max(baseIntensity, Math.min(1f, intensity)); + } + + public void resetAnimation() { + if (smoothAnimator != null) { + smoothAnimator.cancel(); + smoothAnimator = null; + } + + currentScrollProgress = 0f; + targetScrollProgress = 0f; + scrollVelocity = 0f; + isScrolling = false; + + // Reset collapsing header animations + if (avatarAnimationHelper != null) { + avatarAnimationHelper.reset(); + } + + if (textTransitionHelper != null) { + textTransitionHelper.reset(); + } + + if (buttonAnimationHelper != null) { + buttonAnimationHelper.reset(); + } + + if (animationView != null) { + animationView.setScrollProgress(0f); + } + } + + public void pauseAnimation() { + if (smoothAnimator != null) { + smoothAnimator.pause(); + } + } + + public void resumeAnimation() { + if (smoothAnimator != null) { + smoothAnimator.resume(); + } + } + + // Getters for debugging and monitoring + public float getCurrentScrollProgress() { + return currentScrollProgress; + } + + public float getScrollVelocity() { + return scrollVelocity; + } + + public boolean isScrolling() { + return isScrolling; + } + + public float getCurrentIntensity() { + return calculateIntensity(); + } + + // Special animation triggers + public void triggerManualGiftBurst() { + if (animationView != null) { + animationView.triggerGiftAnimation(); + } + } + + public void setAnimationEnabled(boolean enabled) { + if (animationView != null) { + animationView.setAnimationEnabled(enabled); + } + } + + // Performance optimization + public void setLowPerformanceMode(boolean enabled) { + if (enabled) { + // Reduce animation smoothness for better performance + maxIntensity = 0.5f; + baseIntensity = 0.1f; + } else { + // Restore full animation quality + maxIntensity = 0.8f; + baseIntensity = 0.2f; + } + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ProfileHeaderAnimationView.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ProfileHeaderAnimationView.java new file mode 100644 index 00000000000..01bafacdfd4 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ProfileHeaderAnimationView.java @@ -0,0 +1,313 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Shader; +import android.view.View; +import android.view.animation.DecelerateInterpolator; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.SharedConfig; +import org.telegram.ui.ActionBar.Theme; + +import java.util.ArrayList; +import java.util.List; + +public class ProfileHeaderAnimationView extends View { + + private HeaderGiftParticleSystem particleSystem; + private HeaderGradientController gradientController; + private HeaderScrollResponder scrollResponder; + + private Paint backgroundPaint; + private boolean isAnimating = false; + private float animationProgress = 0f; + private float scrollProgress = 0f; + + // Animation state + private ValueAnimator mainAnimator; + private long lastFrameTime; + private boolean isInitialized = false; + + // Theme support + private final Theme.ResourcesProvider resourcesProvider; + private boolean isDarkTheme = false; + + // Performance optimization + private final int performanceClass; + private boolean isLowPerformance = false; + + public ProfileHeaderAnimationView(Context context, Theme.ResourcesProvider resourcesProvider) { + super(context); + this.resourcesProvider = resourcesProvider; + this.performanceClass = SharedConfig.getDevicePerformanceClass(); + this.isLowPerformance = performanceClass == SharedConfig.PERFORMANCE_CLASS_LOW; + + initializeComponents(); + initializePaint(); + } + + private void initializeComponents() { + // Initialize particle system with performance considerations + particleSystem = new HeaderGiftParticleSystem(getContext(), resourcesProvider); + particleSystem.setPerformanceClass(performanceClass); + + // Initialize gradient controller + gradientController = new HeaderGradientController(getContext(), resourcesProvider); + + // Initialize scroll responder + scrollResponder = new HeaderScrollResponder(); + scrollResponder.setAnimationView(this); + + isInitialized = true; + } + + private void initializePaint() { + backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + backgroundPaint.setDither(true); + updateTheme(); + } + + public void updateTheme() { + if (gradientController != null) { + gradientController.updateTheme(); + } + if (particleSystem != null) { + particleSystem.updateTheme(); + } + invalidate(); + } + + public void setScrollProgress(float progress) { + this.scrollProgress = Math.max(0f, Math.min(1f, progress)); + + if (scrollResponder != null) { + scrollResponder.onScrollChanged(scrollProgress); + } + + // Update particle system based on scroll + if (particleSystem != null) { + particleSystem.setScrollProgress(scrollProgress); + } + + // Update animation intensity based on scroll + updateAnimationIntensity(); + + if (isInitialized) { + invalidate(); + } + } + + private void updateAnimationIntensity() { + if (particleSystem != null) { + // Increase particle generation when scrolling up (revealing header) + float intensity = scrollProgress * 0.3f + 0.1f; // Base intensity + scroll-based boost + particleSystem.setIntensity(intensity); + } + } + + public void startAnimation() { + if (isAnimating || !isInitialized) return; + + isAnimating = true; + lastFrameTime = System.currentTimeMillis(); + + // Start main animation loop + if (mainAnimator != null) { + mainAnimator.cancel(); + } + + mainAnimator = ValueAnimator.ofFloat(0f, 1f); + mainAnimator.setDuration(Long.MAX_VALUE); // Infinite duration + mainAnimator.setInterpolator(new DecelerateInterpolator(1.5f)); + mainAnimator.addUpdateListener(animation -> { + long currentTime = System.currentTimeMillis(); + float deltaTime = (currentTime - lastFrameTime) / 1000f; + lastFrameTime = currentTime; + + updateAnimation(deltaTime); + invalidate(); + }); + mainAnimator.start(); + + // Start particle system + if (particleSystem != null) { + particleSystem.startAnimation(); + } + } + + public void stopAnimation() { + if (!isAnimating) return; + + isAnimating = false; + + if (mainAnimator != null) { + mainAnimator.cancel(); + mainAnimator = null; + } + + if (particleSystem != null) { + particleSystem.stopAnimation(); + } + } + + private void updateAnimation(float deltaTime) { + if (!isAnimating) return; + + // Update animation progress + animationProgress += deltaTime * 0.5f; // Adjust speed as needed + if (animationProgress > 1f) { + animationProgress = 0f; // Loop + } + + // Update particle system + if (particleSystem != null) { + particleSystem.update(deltaTime); + } + + // Update gradient controller + if (gradientController != null) { + gradientController.update(deltaTime, scrollProgress); + } + + // Check performance periodically (every 2 seconds of animation time) + if (animationProgress % 2f < deltaTime) { + checkAndAdjustPerformance(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + + // Update component dimensions + if (particleSystem != null) { + particleSystem.setBounds(0, 0, width, height); + } + + if (gradientController != null) { + gradientController.setBounds(0, 0, width, height); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (!isInitialized) return; + + int width = getWidth(); + int height = getHeight(); + + if (width <= 0 || height <= 0) return; + + // Draw gradient background + if (gradientController != null) { + gradientController.draw(canvas, scrollProgress); + } + + // Draw particle system + if (particleSystem != null && isAnimating) { + particleSystem.draw(canvas); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + // Set performance mode based on device class + setPerformanceOptimization(performanceClass == SharedConfig.PERFORMANCE_CLASS_LOW); + startAnimation(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + stopAnimation(); + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (visibility == VISIBLE) { + startAnimation(); + } else { + stopAnimation(); + } + } + + // Public API for integration with ProfileActivity + public void onScrollChanged(float scrollOffset, float maxScrollOffset) { + float progress = maxScrollOffset > 0 ? scrollOffset / maxScrollOffset : 0f; + setScrollProgress(progress); + } + + public void setGiftsVisible(boolean visible) { + if (particleSystem != null) { + particleSystem.setGiftsVisible(visible); + } + } + + public void triggerGiftAnimation() { + if (particleSystem != null) { + particleSystem.triggerGiftBurst(); + } + } + + public void setAnimationEnabled(boolean enabled) { + if (enabled) { + startAnimation(); + } else { + stopAnimation(); + } + } + + // Performance monitoring and optimization + public float getAverageFrameTime() { + if (particleSystem != null) { + return particleSystem.getAverageFrameTime(); + } + return 0f; + } + + public void setPerformanceOptimization(boolean enabled) { + if (particleSystem != null) { + particleSystem.setPerformanceOptimization(enabled); + } + if (gradientController != null) { + gradientController.setLowPerformanceMode(enabled); + } + if (scrollResponder != null) { + scrollResponder.setLowPerformanceMode(enabled); + } + } + + // Auto performance adjustment based on frame rate + private void checkAndAdjustPerformance() { + float avgFrameTime = getAverageFrameTime(); + if (avgFrameTime > 0) { + // If frame time exceeds 20ms (50 FPS), enable performance optimization + boolean shouldOptimize = avgFrameTime > 20f; + + if (shouldOptimize != isLowPerformance) { + setPerformanceOptimization(shouldOptimize); + isLowPerformance = shouldOptimize; + } + } + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java index 9caded8e2f3..94b2d918e39 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/SharedMediaLayout.java @@ -132,7 +132,7 @@ import org.telegram.ui.LaunchActivity; import org.telegram.ui.PhotoViewer; import org.telegram.ui.PremiumPreviewFragment; -import org.telegram.ui.ProfileActivity; +import org.telegram.ui.ProfileNewActivity; import org.telegram.ui.Stars.StarsController; import org.telegram.ui.Stories.StoriesController; import org.telegram.ui.Stories.StoriesListPlaceProvider; @@ -438,7 +438,7 @@ public void openStory(DialogCell dialogCell, Runnable onDone) { profileActivity.getContext(), dialogCell.getDialogId(), StoriesListPlaceProvider.of((RecyclerListView) dialogCell.getParent()) - .addBottomClip(profileActivity instanceof ProfileActivity && ((ProfileActivity) profileActivity).myProfile ? dp(68) : 0) + .addBottomClip(profileActivity instanceof ProfileNewActivity && ((ProfileNewActivity) profileActivity).myProfile ? dp(68) : 0) ); } } @@ -761,8 +761,8 @@ public SharedMediaPreloader(BaseFragment fragment) { } }); } - } else if (fragment instanceof ProfileActivity) { - ProfileActivity profileActivity = (ProfileActivity) fragment; + } else if (fragment instanceof ProfileNewActivity) { + ProfileNewActivity profileActivity = (ProfileNewActivity) fragment; if (profileActivity.saved) { dialogId = profileActivity.getUserConfig().getClientUserId(); topicId = profileActivity.getDialogId(); @@ -2439,7 +2439,7 @@ public int getStartedTrackingX() { return startedTrackingX; } }; - } else if (profileActivity instanceof ProfileActivity) { + } else if (profileActivity instanceof ProfileNewActivity) { saveItem = new TextView(context); saveItem.setText(getString(R.string.Save).toUpperCase()); saveItem.setTypeface(AndroidUtilities.bold()); @@ -2458,7 +2458,7 @@ public int getStartedTrackingX() { saveItem.setScaleX(0.4f); saveItem.setScaleY(0.4f); - giftsContainer = new ProfileGiftsContainer(profileActivity, context, profileActivity.getCurrentAccount(), ((ProfileActivity) profileActivity).getDialogId(), resourcesProvider) { + giftsContainer = new ProfileGiftsContainer(profileActivity, context, profileActivity.getCurrentAccount(), ((ProfileNewActivity) profileActivity).getDialogId(), resourcesProvider) { @Override protected int processColor(int color) { return SharedMediaLayout.this.processColor(color); @@ -2959,7 +2959,7 @@ public void getItemOffsets(android.graphics.Rect outRect, View view, RecyclerVie } Bundle args = new Bundle(); args.putLong("user_id", user_id); - profileActivity.presentFragment(new ProfileActivity(args)); + profileActivity.presentFragment(new ProfileNewActivity(args)); } } else if (mediaPage.selectedType == TAB_COMMON_GROUPS && view instanceof ProfileSearchCell) { TLRPC.Chat chat = ((ProfileSearchCell) view).getChat(); @@ -4671,8 +4671,8 @@ public void onActionBarItemClick(View v, int id) { } fragment1.finishFragment(); UndoView undoView = null; - if (profileActivity instanceof ProfileActivity) { - undoView = ((ProfileActivity) profileActivity).getUndoView(); + if (profileActivity instanceof ProfileNewActivity) { + undoView = ((ProfileNewActivity) profileActivity).getUndoView(); } if (undoView != null) { if (dids.size() == 1) { @@ -6851,7 +6851,7 @@ private void onItemClick(int index, View view, MessageObject message, int a, int if (forward) { storiesList.load(false, 30); } - }).addBottomClip(profileActivity instanceof ProfileActivity && ((ProfileActivity) profileActivity).myProfile ? dp(68) : 0)); + }).addBottomClip(profileActivity instanceof ProfileNewActivity && ((ProfileNewActivity) profileActivity).myProfile ? dp(68) : 0)); } } updateForwardItem(); @@ -8696,8 +8696,8 @@ public void openPreview(int position) { return; } final BaseFragment fragment = new ChatActivity(args); - if (profileActivity instanceof ProfileActivity) { - ((ProfileActivity) profileActivity).prepareBlurBitmap(); + if (profileActivity instanceof ProfileNewActivity) { + ((ProfileNewActivity) profileActivity).prepareBlurBitmap(); } ActionBarPopupWindow.ActionBarPopupWindowLayout previewMenu = new ActionBarPopupWindow.ActionBarPopupWindowLayout(getContext(), R.drawable.popup_fixed_alert, resourcesProvider, ActionBarPopupWindow.ActionBarPopupWindowLayout.FLAG_SHOWN_FROM_BOTTOM); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/TextTransitionHelper.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/TextTransitionHelper.java new file mode 100644 index 00000000000..e0a712437d0 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/TextTransitionHelper.java @@ -0,0 +1,313 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui.Components; + +import android.animation.ValueAnimator; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.widget.TextView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.ui.Components.CubicBezierInterpolator; + +/** + * Helper class for smooth text transitions during collapsing header animations. + * Manages dual text views (expanded and collapsed) with synchronized + * translation + * transitions. Texts are always fully visible - no fading effects. + */ +public class TextTransitionHelper { + + // Animation configuration + private static final float TRANSLATION_THRESHOLD = 0.8f; // When text translation completes + + // Material Design animation interpolators for smooth text transitions + private final AccelerateInterpolator fadeOutInterpolator = new AccelerateInterpolator(2f); + private final DecelerateInterpolator fadeInInterpolator = new DecelerateInterpolator(2f); + private final DecelerateInterpolator translationInterpolator = new DecelerateInterpolator(2.0f); // Smoother + // translation + + // Text views + private TextView expandedNameView; + private TextView expandedStatusView; + private TextView collapsedNameView; + private TextView collapsedStatusView; + + // Animation state + private float currentProgress = 0f; + private float nameTranslationX = 0f; + private float nameTranslationY = 0f; + private float statusTranslationX = 0f; + private float statusTranslationY = 0f; + private float nameScale = 1f; + private float statusScale = 1f; + + // Layout parameters + private float initialNameX, initialNameY; + private float initialStatusX, initialStatusY; + private float targetNameX, targetNameY; + private float targetStatusX, targetStatusY; + + public TextTransitionHelper() { + // Constructor + } + + /** + * Set the text views for animation + */ + public void setTextViews(TextView expandedName, TextView expandedStatus, + TextView collapsedName, TextView collapsedStatus) { + this.expandedNameView = expandedName; + this.expandedStatusView = expandedStatus; + this.collapsedNameView = collapsedName; + this.collapsedStatusView = collapsedStatus; + + // Initialize collapsed views as hidden since we use translated expanded text + if (collapsedNameView != null) { + collapsedNameView.setAlpha(0f); + } + if (collapsedStatusView != null) { + collapsedStatusView.setAlpha(0f); + } + } + + /** + * Initialize layout parameters for smooth transitions + */ + public void initializeLayout(float initialNameX, float initialNameY, float initialStatusX, float initialStatusY, + float targetNameX, float targetNameY, float targetStatusX, float targetStatusY) { + this.initialNameX = initialNameX; + this.initialNameY = initialNameY; + this.initialStatusX = initialStatusX; + this.initialStatusY = initialStatusY; + this.targetNameX = targetNameX; + this.targetNameY = targetNameY; + this.targetStatusX = targetStatusX; + this.targetStatusY = targetStatusY; + } + + /** + * Update text animations based on scroll progress (0.0 = expanded, 1.0 = + * collapsed) + */ + public void updateAnimation(float progress) { + this.currentProgress = Math.max(0f, Math.min(1f, progress)); + calculateTextAnimationValues(); + applyAnimationsToViews(); + } + + /** + * Apply Material Design easing curve for smooth animations + */ + private float applyMaterialEasing(float progress) { + // Use Telegram's ultra-smooth CubicBezierInterpolator for perfect text + // transitions + // EASE_OUT provides smooth deceleration without stuttering + return CubicBezierInterpolator.EASE_OUT.getInterpolation(progress); + } + + private void calculateTextAnimationValues() { + // Implement actual translation movement instead of fade effects + // Text should visually move from center to toolbar position with "left upward + // motion" + // Text should only start moving when the collapsing header is near the text + + // Text should only start moving when the collapsing header is near the text + // Use much later threshold for more natural animation + if (currentProgress <= 0.7f) { + // Text stays in center position until collapsing header is very near + // No translation until 70% of scroll progress + nameTranslationX = 0f; + nameTranslationY = 0f; + statusTranslationX = 0f; + statusTranslationY = 0f; + nameScale = 1f; + statusScale = 1f; + } else if (currentProgress >= 0.98f) { + // Fully translated to toolbar position - text should be visible next to back + // button + nameTranslationX = targetNameX; + nameTranslationY = targetNameY; + statusTranslationX = targetStatusX; + statusTranslationY = targetStatusY; + nameScale = 0.92f; // Match button scaling for consistency + statusScale = 0.92f; + } else { + // Ultra-slow translation zone (70% to 98% - much longer range for ultra-slow + // movement) + float transitionProgress = (currentProgress - 0.7f) / 0.28f; // 28% range for ultra-slow movement + + // Use very slow EASE_OUT for ultra-slow text movement + float smoothProgress = CubicBezierInterpolator.EASE_OUT.getInterpolation(transitionProgress); + + // Apply ultra-slow left upward motion for BOTH username and status + nameTranslationX = smoothProgress * targetNameX; + nameTranslationY = smoothProgress * targetNameY; + + // Apply same technique to status - synchronized movement + statusTranslationX = smoothProgress * targetStatusX; + statusTranslationY = smoothProgress * targetStatusY; + + // Apply gentle scaling to both + nameScale = 1f - (smoothProgress * 0.08f); // Same gentle scaling as individual buttons + statusScale = 1f - (smoothProgress * 0.08f); // Identical to name scaling + } + } + + private void applyAnimationsToViews() { + // Apply EXACT same animations to expanded text views - ensure both behave + // identically + if (expandedNameView != null) { + // ALWAYS fully visible - no alpha transitions ever + expandedNameView.setAlpha(1f); + expandedNameView.setTranslationX(nameTranslationX); + expandedNameView.setTranslationY(nameTranslationY); + expandedNameView.setScaleX(nameScale); + expandedNameView.setScaleY(nameScale); + + // COMPLETELY disable ellipsize to prevent any text clipping/vanishing + expandedNameView.setEllipsize(null); + expandedNameView.setMaxLines(1); + expandedNameView.setHorizontallyScrolling(true); + // Ensure text stays visible by preventing any layout constraints + expandedNameView.setSingleLine(false); + } + + if (expandedStatusView != null) { + // ALWAYS fully visible - no alpha transitions ever (identical to username) + expandedStatusView.setAlpha(1f); + expandedStatusView.setTranslationY(statusTranslationY); + expandedStatusView.setTranslationX(statusTranslationX); + expandedStatusView.setScaleX(statusScale); + expandedStatusView.setScaleY(statusScale); + + // COMPLETELY disable ellipsize to prevent any text clipping/vanishing + // (identical to username) + expandedStatusView.setEllipsize(null); + expandedStatusView.setMaxLines(1); + expandedStatusView.setHorizontallyScrolling(true); + // Ensure text stays visible by preventing any layout constraints + expandedStatusView.setSingleLine(false); + } + + // Keep collapsed text views hidden - we use translated expanded text instead + if (collapsedNameView != null) { + collapsedNameView.setAlpha(0f); // Always 0f - keep hidden + } + + if (collapsedStatusView != null) { + collapsedStatusView.setAlpha(0f); // Always 0f - keep hidden + } + } + + /** + * Set text content for both expanded and collapsed views + */ + public void setText(CharSequence name, CharSequence status) { + if (expandedNameView != null) { + expandedNameView.setText(name); + } + if (expandedStatusView != null) { + expandedStatusView.setText(status); + } + if (collapsedNameView != null) { + collapsedNameView.setText(name); + } + if (collapsedStatusView != null) { + collapsedStatusView.setText(status); + } + } + + /** + * Get current animation progress + */ + public float getAnimationProgress() { + return currentProgress; + } + + /** + * Check if expanded text is visible + */ + public boolean isExpandedTextVisible() { + return true; // Always visible + } + + /** + * Check if collapsed text is visible + */ + public boolean isCollapsedTextVisible() { + return false; // Always hidden since we use translated expanded text + } + + /** + * Reset animation to initial state + */ + public void reset() { + currentProgress = 0f; + nameTranslationX = 0f; + nameTranslationY = 0f; + statusTranslationX = 0f; + statusTranslationY = 0f; + nameScale = 1f; + statusScale = 1f; + applyAnimationsToViews(); + } + + /** + * Create smooth transition animator for text animations + */ + public ValueAnimator createTextAnimator(float fromProgress, float toProgress, long duration) { + ValueAnimator animator = ValueAnimator.ofFloat(fromProgress, toProgress); + animator.setDuration(duration); + animator.setInterpolator(new DecelerateInterpolator()); + animator.addUpdateListener(animation -> { + float progress = (Float) animation.getAnimatedValue(); + updateAnimation(progress); + }); + return animator; + } + + /** + * Apply custom text colors during animation + */ + public void setTextColors(int expandedColor, int collapsedColor) { + if (expandedNameView != null) { + expandedNameView.setTextColor(expandedColor); + } + if (expandedStatusView != null) { + expandedStatusView.setTextColor(expandedColor); + } + if (collapsedNameView != null) { + collapsedNameView.setTextColor(collapsedColor); + } + if (collapsedStatusView != null) { + collapsedStatusView.setTextColor(collapsedColor); + } + } + + /** + * Handle text size changes during animation + */ + public void setTextSizes(float expandedNameSize, float expandedStatusSize, + float collapsedNameSize, float collapsedStatusSize) { + if (expandedNameView != null) { + expandedNameView.setTextSize(expandedNameSize); + } + if (expandedStatusView != null) { + expandedStatusView.setTextSize(expandedStatusSize); + } + if (collapsedNameView != null) { + collapsedNameView.setTextSize(collapsedNameSize); + } + if (collapsedStatusView != null) { + collapsedStatusView.setTextSize(collapsedStatusSize); + } + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java index 3daac26e9b7..c06ba3ff232 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ContactsActivity.java @@ -692,7 +692,7 @@ public boolean onItemClick(View view, int position) { presentFragment(ChatActivity.of(dialogId)); }) .add(R.drawable.msg_openprofile, LocaleController.getString(R.string.OpenProfile), () -> { - presentFragment(ProfileActivity.of(dialogId)); + presentFragment(ProfileNewActivity.of(dialogId)); }) .addIf(!muted, R.drawable.msg_mute, LocaleController.getString(R.string.NotificationsStoryMute), () -> { MessagesController.getNotificationsSettings(currentAccount).edit().putBoolean("stories_" + key, false).apply(); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java index 5ae5ba84115..1f5bfe08121 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/DialogsActivity.java @@ -5003,7 +5003,7 @@ public void onUserLongPressed(View view, long dialogId) { presentFragment(ChatActivity.of(dialogId)); }) .addIf(dialogId > 0, R.drawable.msg_openprofile, LocaleController.getString(R.string.OpenProfile), () -> { - presentFragment(ProfileActivity.of(dialogId)); + presentFragment(ProfileNewActivity.of(dialogId)); }) .addIf(dialogId < 0, R.drawable.msg_channel, LocaleController.getString(ChatObject.isChannelAndNotMegaGroup(chat) ? R.string.OpenChannel2 : R.string.OpenGroup2), () -> { presentFragment(ChatActivity.of(dialogId)); @@ -13096,7 +13096,7 @@ public long getSearchForumDialogId() { searchViewPager.botsSearchListView.setOnItemClickListener((view, position, x, y) -> { Object obj = searchViewPager.botsSearchAdapter.getObject(position); if (obj instanceof TLRPC.User) { - presentFragment(ProfileActivity.of(((TLRPC.User) obj).id)); + presentFragment(ProfileNewActivity.of(((TLRPC.User) obj).id)); } else if (obj instanceof MessageObject) { MessageObject msg = (MessageObject) obj; Bundle args = new Bundle(); @@ -13241,7 +13241,7 @@ private void openAvatarInProfile() { Bundle args = new Bundle(); args.putLong("user_id", UserConfig.getInstance(currentAccount).getClientUserId()); args.putBoolean("my_profile", true); - presentFragment(new ProfileActivity(args, null)); + presentFragment(new ProfileNewActivity(args, null)); } @Override diff --git a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java index dca65d2c326..caec431b70a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/LaunchActivity.java @@ -688,7 +688,7 @@ public boolean drawChild(Canvas canvas, View child, long drawingTime) { Bundle args = new Bundle(); args.putLong("user_id", UserConfig.getInstance(currentAccount).getClientUserId()); args.putBoolean("my_profile", true); - presentFragment(new ProfileActivity(args, null)); + presentFragment(new ProfileNewActivity(args, null)); } else if (id == 17) { drawerLayoutContainer.closeDrawer(true); Bundle args = new Bundle(); @@ -879,7 +879,7 @@ public void onPreviewOpenAnimationEnd() { break; case "settings": { args.putLong("user_id", UserConfig.getInstance(currentAccount).clientUserId); - ProfileActivity settings = new ProfileActivity(args); + ProfileNewActivity settings = new ProfileNewActivity(args); actionBarLayout.addFragmentToStack(settings); settings.restoreSelfArgs(savedInstanceState); break; @@ -902,7 +902,7 @@ public void onPreviewOpenAnimationEnd() { break; case "chat_profile": if (args != null) { - ProfileActivity profile = new ProfileActivity(args); + ProfileNewActivity profile = new ProfileNewActivity(args); if (actionBarLayout.addFragmentToStack(profile)) { profile.restoreSelfArgs(savedInstanceState); } @@ -1393,7 +1393,7 @@ private void openSettings(boolean expanded) { if (expanded) { args.putBoolean("expandPhoto", true); } - ProfileActivity fragment = new ProfileActivity(args); + ProfileNewActivity fragment = new ProfileNewActivity(args); presentFragment(fragment); drawerLayoutContainer.closeDrawer(false); } @@ -3250,7 +3250,7 @@ private boolean handleIntent(Intent intent, boolean isNew, boolean restore, bool bulletinText = "Logs enabled."; ApplicationLoader.applicationContext.getSharedPreferences("systemConfig", Context.MODE_PRIVATE).edit().putBoolean("logsEnabled", BuildVars.LOGS_ENABLED = true).commit(); } else if (open_settings == 8) { - ProfileActivity.sendLogs(LaunchActivity.this, false); + ProfileNewActivity.sendLogs(LaunchActivity.this, false); } else if (open_settings == 9) { bulletinText = "Logs disabled."; ApplicationLoader.applicationContext.getSharedPreferences("systemConfig", Context.MODE_PRIVATE).edit().putBoolean("logsEnabled", BuildVars.LOGS_ENABLED = false).commit(); @@ -3268,7 +3268,7 @@ private boolean handleIntent(Intent intent, boolean isNew, boolean restore, bool if (open_settings == 1) { Bundle args = new Bundle(); args.putLong("user_id", UserConfig.getInstance(currentAccount).clientUserId); - fragment = new ProfileActivity(args); + fragment = new ProfileNewActivity(args); } else if (open_settings == 2) { fragment = new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC); } else if (open_settings == 3) { @@ -4672,7 +4672,7 @@ public void didChangeOwner(TLRPC.User user) { } else { profile_args.putLong("user_id", peerId); } - getActionBarLayout().presentFragment(new ProfileActivity(profile_args)); + getActionBarLayout().presentFragment(new ProfileNewActivity(profile_args)); } else if (chat != null && chat.forum) { Long topicId = threadId; if (topicId == null && messageId != null) { @@ -7032,9 +7032,9 @@ public void updateDrawState(@NonNull TextPaint ds) { } else if (id == NotificationCenter.didSetPasscode) { flagSecureReason.invalidate(); } else if (id == NotificationCenter.reloadInterface) { - boolean last = mainFragmentsStack.size() > 1 && mainFragmentsStack.get(mainFragmentsStack.size() - 1) instanceof ProfileActivity; + boolean last = mainFragmentsStack.size() > 1 && mainFragmentsStack.get(mainFragmentsStack.size() - 1) instanceof ProfileNewActivity; if (last) { - ProfileActivity profileActivity = (ProfileActivity) mainFragmentsStack.get(mainFragmentsStack.size() - 1); + ProfileNewActivity profileActivity = (ProfileNewActivity) mainFragmentsStack.get(mainFragmentsStack.size() - 1); if (!profileActivity.isSettings()) { last = false; } @@ -7530,8 +7530,8 @@ private void showVoiceChatTooltip(int action) { } else if (fragment instanceof DialogsActivity) { DialogsActivity dialogsActivity = (DialogsActivity) fragment; undoView = dialogsActivity.getUndoView(); - } else if (fragment instanceof ProfileActivity) { - ProfileActivity profileActivity = (ProfileActivity) fragment; + } else if (fragment instanceof ProfileNewActivity) { + ProfileNewActivity profileActivity = (ProfileNewActivity) fragment; undoView = profileActivity.getUndoView(); } if (undoView != null) { @@ -7976,8 +7976,8 @@ protected void onSaveInstanceState(Bundle outState) { outState.putString("fragment", "group"); } else if (lastFragment instanceof WallpapersListActivity) { outState.putString("fragment", "wallpapers"); - } else if (lastFragment instanceof ProfileActivity) { - ProfileActivity profileActivity = (ProfileActivity) lastFragment; + } else if (lastFragment instanceof ProfileNewActivity) { + ProfileNewActivity profileActivity = (ProfileNewActivity) lastFragment; if (profileActivity.isSettings()) { outState.putString("fragment", "settings"); } else if (profileActivity.isChat() && args != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ProfileNewActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNewActivity.java new file mode 100644 index 00000000000..c5657edd1f5 --- /dev/null +++ b/TMessagesProj/src/main/java/org/telegram/ui/ProfileNewActivity.java @@ -0,0 +1,2831 @@ +/* + * This is the source code of Telegram for Android v. 5.x.x. + * It is licensed under GNU GPL v. 2 or later. + * You should have received a copy of the license in this archive (see LICENSE). + * + * Copyright Nikolai Kudashov, 2013-2018. + */ + +package org.telegram.ui; + +import static org.telegram.messenger.AndroidUtilities.dp; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.io.File; + +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.widget.NestedScrollView; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.CollapsingToolbarLayout; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.ImageLocation; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.MessagesController; +import org.telegram.messenger.BuildVars; +import org.telegram.messenger.NotificationCenter; +import org.telegram.messenger.R; +import org.telegram.messenger.UserObject; +import org.telegram.tgnet.TLRPC; +import org.telegram.ui.ActionBar.ActionBar; +import org.telegram.ui.ActionBar.ActionBarMenu; +import org.telegram.ui.ActionBar.ActionBarMenuItem; +import org.telegram.ui.ActionBar.BaseFragment; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.AvatarDrawable; +import org.telegram.ui.Components.BackupImageView; +import org.telegram.ui.Components.AnimatedAvatarView; +import org.telegram.ui.Components.HeaderGiftParticleSystem; +import org.telegram.ui.Components.LayoutHelper; +import org.telegram.ui.Components.RecyclerListView; +import org.telegram.ui.Components.UndoView; +import org.telegram.ui.Components.BulletinFactory; +import org.telegram.ui.Components.SharedMediaLayout; +import org.telegram.ui.Components.voip.VoIPHelper; +import org.telegram.ui.Components.AvatarAnimationHelper; +import org.telegram.ui.Components.TextTransitionHelper; +import org.telegram.ui.Components.HeaderButtonsAnimationHelper; +import org.telegram.ui.Components.HeaderScrollResponder; +import org.telegram.ui.Components.CollapsingHeaderOffsetListener; +import org.telegram.ui.Cells.TextDetailCell; +import org.telegram.ui.Cells.TextCell; +import org.telegram.ui.Cells.ShadowSectionCell; +import org.telegram.ui.Cells.HeaderCell; +import org.telegram.ui.Cells.AboutLinkCell; +import org.telegram.ui.Cells.DividerCell; +import org.telegram.ui.Cells.UserCell; +import org.telegram.ui.Business.ProfileHoursCell; +import org.telegram.ui.Business.ProfileLocationCell; +import org.telegram.messenger.MessageObject; +import org.telegram.tgnet.tl.TL_account; +import java.util.Calendar; + +import org.telegram.ui.Stars.StarsIntroActivity; +import org.telegram.ui.Business.BusinessLinksActivity; +import org.telegram.ui.Components.AlertsCreator; +import org.telegram.ui.ActionBar.AlertDialog; +import org.telegram.messenger.ChatObject; +import org.telegram.messenger.MessagesStorage; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.browser.Browser; +import org.telegram.messenger.Utilities; +import org.telegram.tgnet.ConnectionsManager; + +import androidx.core.content.ContextCompat; + +public class ProfileNewActivity extends BaseFragment implements NotificationCenter.NotificationCenterDelegate, + SharedMediaLayout.Delegate, SharedMediaLayout.SharedMediaPreloaderDelegate { + + private CoordinatorLayout coordinatorLayout; + private AppBarLayout appBarLayout; + private CollapsingToolbarLayout collapsingToolbarLayout; + private NestedScrollView nestedScrollView; + + // Header content containers + private FrameLayout avatarContainer; + private FrameLayout nameContainer; + private FrameLayout onlineContainer; + private LinearLayout headerButtonsContainer; + + // Collapsed title containers + private LinearLayout collapsedTitleContainer; + private FrameLayout collapsedNameContainer; + private FrameLayout collapsedOnlineContainer; + + // Header elements + // private AnimatedAvatarView avatarImageView; + private BackupImageView avatarImageView; + private AvatarDrawable avatarDrawable; + private TextView nameTextView; + private TextView onlineTextView; + + // Collapsed title elements + private TextView collapsedNameTextView; + private TextView collapsedOnlineTextView; + private BubbleButton messageButton; + private BubbleButton muteButton; + private BubbleButton callButton; + private BubbleButton videoButton; + + // Gift particle system + private HeaderGiftParticleSystem giftParticleSystem; + + // Animation helpers for collapsing header + private AvatarAnimationHelper avatarAnimationHelper; + private TextTransitionHelper textTransitionHelper; + private HeaderButtonsAnimationHelper buttonAnimationHelper; + private HeaderScrollResponder scrollResponder; + private CollapsingHeaderOffsetListener offsetListener; + + // Profile data + private long dialogId; + private long chatId; + private long userId; + private TLRPC.User currentUser; + private TLRPC.Chat currentChat; + private TLRPC.UserFull userInfo; + private TLRPC.ChatFull chatInfo; + private boolean isMuted; + private boolean canSearchMembers; + + // Menu items + private ActionBarMenuItem otherItem; + private ActionBarMenuItem qrItem; + + // Menu item constants (from ProfileActivity) + private final static int add_contact = 1; + private final static int block_contact = 2; + private final static int share_contact = 3; + private final static int edit_contact = 4; + private final static int delete_contact = 5; + private final static int leave_group = 7; + private final static int invite_to_group = 9; + private final static int share = 10; + private final static int edit_channel = 12; + private final static int add_shortcut = 14; + private final static int call_item = 15; + private final static int video_call_item = 16; + private final static int search_members = 17; + private final static int statistics = 19; + private final static int start_secret_chat = 20; + private final static int gallery_menu_save = 21; + private final static int view_discussion = 22; + private final static int delete_topic = 23; + private final static int report = 24; + private final static int edit_info = 30; + private final static int logout = 31; + private final static int set_as_main = 33; + private final static int edit_avatar = 34; + private final static int delete_avatar = 35; + private final static int add_photo = 36; + private final static int qr_button = 37; + private final static int gift_premium = 38; + private final static int channel_stories = 39; + private final static int edit_color = 40; + private final static int edit_profile = 41; + private final static int copy_link_profile = 42; + private final static int set_username = 43; + private final static int bot_privacy = 44; + + // SharedMediaLayout for proper content integration + private SharedMediaLayout sharedMediaLayout; + private SharedMediaLayout.SharedMediaPreloader sharedMediaPreloader; + private FullProfileContentAdapter listAdapter; + + // Row tracking variables (complete set from ProfileActivity, excluding + // notification rows) + private int rowCount; + + // Profile Header & Avatar + private int setAvatarRow = -1; + private int setAvatarSectionRow = -1; + private int channelRow = -1; + private int channelDividerRow = -1; + + // Account Section + private int numberSectionRow = -1; + private int numberRow = -1; + private int birthdayRow = -1; + private int setUsernameRow = -1; + private int bioRow = -1; + + // Suggestions + private int phoneSuggestionSectionRow = -1; + private int graceSuggestionRow = -1; + private int graceSuggestionSectionRow = -1; + private int phoneSuggestionRow = -1; + private int passwordSuggestionSectionRow = -1; + private int passwordSuggestionRow = -1; + + // Settings Sections + private int settingsSectionRow = -1; + private int settingsSectionRow2 = -1; + // notificationRow excluded + private int languageRow = -1; + private int privacyRow = -1; + private int dataRow = -1; + private int chatRow = -1; + private int filtersRow = -1; + private int liteModeRow = -1; + private int stickersRow = -1; + private int devicesRow = -1; + private int devicesSectionRow = -1; + + // Help Section + private int helpHeaderRow = -1; + private int questionRow = -1; + private int faqRow = -1; + private int policyRow = -1; + private int helpSectionCell = -1; + + // Debug Section + private int debugHeaderRow = -1; + private int sendLogsRow = -1; + private int sendLastLogsRow = -1; + private int clearLogsRow = -1; + private int switchBackendRow = -1; + private int versionRow = -1; + + // Layout Elements + private int emptyRow = -1; + private int bottomPaddingRow = -1; + + // User Info Section + private int infoHeaderRow = -1; + private int phoneRow = -1; + private int locationRow = -1; + private int userInfoRow = -1; + private int channelInfoRow = -1; + private int usernameRow = -1; + // notificationsDividerRow excluded + // notificationsRow excluded + private int bizHoursRow = -1; + private int bizLocationRow = -1; + // notificationsSimpleRow excluded + private int infoStartRow = -1; + private int infoEndRow = -1; + private int infoSectionRow = -1; + + // Affiliate Program + private int affiliateRow = -1; + private int infoAffiliateRow = -1; + + // Actions + private int sendMessageRow = -1; + private int reportRow = -1; + private int reportReactionRow = -1; + private int reportDividerRow = -1; + private int addToContactsRow = -1; + private int addToGroupButtonRow = -1; + private int addToGroupInfoRow = -1; + + // Premium Features + private int premiumRow = -1; + private int starsRow = -1; + private int tonRow = -1; + private int businessRow = -1; + private int premiumGiftingRow = -1; + private int premiumSectionsRow = -1; + + // Bot Features + private int botAppRow = -1; + private int botPermissionsHeader = -1; + private int botPermissionLocation = -1; + private int botPermissionEmojiStatus = -1; + private int botPermissionBiometry = -1; + private int botPermissionsDivider = -1; + + // Secret Chat Settings + private int settingsTimerRow = -1; + private int settingsKeyRow = -1; + private int secretSettingsSectionRow = -1; + + // Members Section + private int membersHeaderRow = -1; + private int membersStartRow = -1; + private int membersEndRow = -1; + private int addMemberRow = -1; + private int subscribersRow = -1; + private int subscribersRequestsRow = -1; + private int administratorsRow = -1; + private int settingsRow = -1; + + // Balances + private int botStarsBalanceRow = -1; + private int botTonBalanceRow = -1; + private int channelBalanceRow = -1; + private int channelBalanceSectionRow = -1; + private int balanceDividerRow = -1; + private int blockedUsersRow = -1; + private int membersSectionRow = -1; + + // Shared Media + private int sharedMediaRow = -1; + + // Final Actions + private int unblockRow = -1; + private int joinRow = -1; + private int lastSectionRow = -1; + + public ProfileNewActivity(Bundle args) { + this(args, null); + } + + public ProfileNewActivity(Bundle args, SharedMediaLayout.SharedMediaPreloader preloader) { + super(args); + sharedMediaPreloader = preloader; + + if (args != null) { + dialogId = args.getLong("dialog_id", 0); + chatId = args.getLong("chat_id", 0); + userId = args.getLong("user_id", 0); + } + + if (dialogId != 0) { + if (dialogId > 0) { + userId = dialogId; + } else { + chatId = -dialogId; + } + } + + if (userId != 0) { + currentUser = MessagesController.getInstance(currentAccount).getUser(userId); + } else if (chatId != 0) { + currentChat = MessagesController.getInstance(currentAccount).getChat(chatId); + } + } + + @Override + public boolean onFragmentCreate() { + // Load user/chat data like ProfileActivity does + if (userId != 0) { + currentUser = getMessagesController().getUser(userId); + userInfo = getMessagesController().getUserFull(userId); + getMessagesController().loadFullUser(getMessagesController().getUser(userId), classGuid, true); + } else if (chatId != 0) { + currentChat = getMessagesController().getChat(chatId); + if (chatInfo == null) { + chatInfo = getMessagesController().getChatFull(chatId); + } + if (ChatObject.isChannel(currentChat)) { + getMessagesController().loadFullChat(chatId, classGuid, true); + } + } + + // Initialize SharedMediaPreloader if null (copied from ProfileActivity) + if (sharedMediaPreloader == null) { + sharedMediaPreloader = new SharedMediaLayout.SharedMediaPreloader(this); + } + sharedMediaPreloader.addDelegate(this); + + // Initialize animation helpers for collapsing header + avatarAnimationHelper = new AvatarAnimationHelper(); + textTransitionHelper = new TextTransitionHelper(); + buttonAnimationHelper = new HeaderButtonsAnimationHelper(); + scrollResponder = new HeaderScrollResponder(); + offsetListener = new CollapsingHeaderOffsetListener(); + + // Connect animation helpers + scrollResponder.setAnimationHelpers(avatarAnimationHelper, textTransitionHelper); + scrollResponder.setButtonAnimationHelper(buttonAnimationHelper); + offsetListener.setAnimationHelpers(avatarAnimationHelper, textTransitionHelper, buttonAnimationHelper); + offsetListener.setScrollResponder(scrollResponder); + + // Set status bar color callback for dynamic color changes + offsetListener.setStatusBarColorCallback(this::setStatusBarColorForCollapse); + + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.updateInterfaces); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.contactsDidLoad); + NotificationCenter.getInstance(currentAccount).addObserver(this, + NotificationCenter.notificationsSettingsUpdated); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.userInfoDidLoad); + NotificationCenter.getInstance(currentAccount).addObserver(this, NotificationCenter.chatInfoDidLoad); + + return super.onFragmentCreate(); + } + + @Override + public void onFragmentDestroy() { + super.onFragmentDestroy(); + + // Cleanup SharedMedia components (copied from ProfileActivity) + if (sharedMediaLayout != null) { + sharedMediaLayout.onDestroy(); + } + if (sharedMediaPreloader != null) { + sharedMediaPreloader.onDestroy(this); + sharedMediaPreloader.removeDelegate(this); + } + + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.updateInterfaces); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.contactsDidLoad); + NotificationCenter.getInstance(currentAccount).removeObserver(this, + NotificationCenter.notificationsSettingsUpdated); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.userInfoDidLoad); + NotificationCenter.getInstance(currentAccount).removeObserver(this, NotificationCenter.chatInfoDidLoad); + } + + @Override + public View createView(Context context) { + // Configure transparent status bar for collapsing header + configureTransparentStatusBar(); + + actionBar.setBackButtonImage(R.drawable.ic_ab_back); + actionBar.setAllowOverlayTitle(false); // Disable overlay title + actionBar.setTitle(""); // Remove title - we'll use translated text from header + actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { + @Override + public void onItemClick(int id) { + if (id == -1) { + finishFragment(); + } else if (id == 20) { + // Handle QR code + showQRCode(); + } else if (id == block_contact) { + handleBlockContact(); + } else if (id == add_contact) { + handleAddContact(); + } else if (id == share_contact) { + handleShareContact(); + } else if (id == edit_contact) { + handleEditContact(); + } else if (id == delete_contact) { + handleDeleteContact(); + } else if (id == leave_group) { + handleLeaveGroup(); + } else if (id == edit_channel) { + handleEditChannel(); + } else if (id == edit_info) { + handleEditInfo(); + } else if (id == edit_profile) { + handleEditProfile(); + } else if (id == gift_premium) { + handleGiftPremium(); + } else if (id == start_secret_chat) { + handleStartSecretChat(); + } else if (id == bot_privacy) { + handleBotPrivacy(); + } else if (id == statistics) { + handleStatistics(); + } else if (id == search_members) { + handleSearchMembers(); + } else if (id == add_shortcut) { + handleAddShortcut(); + } else if (id == call_item || id == video_call_item) { + handleVoiceVideoCall(id == video_call_item); + } else if (id == edit_color) { + handleEditColor(); + } else if (id == copy_link_profile) { + handleCopyProfileLink(); + } else if (id == set_username) { + handleSetUsername(); + } else if (id == logout) { + handleLogout(); + } else if (id == gallery_menu_save) { + handleGalleryMenuSave(); + } else if (id == set_as_main) { + handleSetAsMain(); + } else if (id == edit_avatar) { + handleEditAvatar(); + } else if (id == delete_avatar) { + handleDeleteAvatar(); + } else if (id == add_photo) { + handleAddPhoto(); + } else if (id == share) { + handleShare(); + } else if (id == report) { + handleReport(); + } else if (id == view_discussion) { + handleViewDiscussion(); + } else if (id == channel_stories) { + handleChannelStories(); + } + } + }); + + // Create menu + ActionBarMenu menu = actionBar.createMenu(); + qrItem = menu.addItem(20, R.drawable.msg_qr_mini); + qrItem.setContentDescription(LocaleController.getString("GetQRCode", R.string.GetQRCode)); + otherItem = menu.addItem(10, R.drawable.ic_ab_other); + + // Create the action bar menu + createActionBarMenu(false); + + // Mark SharedMediaLayout as attached for proper integration + sharedMediaLayoutAttached = true; + + // Inflate the collapsing layout + LayoutInflater inflater = LayoutInflater.from(context); + coordinatorLayout = (CoordinatorLayout) inflater.inflate(R.layout.profile_collapsing_layout, null); + + // Get references to views + appBarLayout = coordinatorLayout.findViewById(R.id.app_bar_layout); + collapsingToolbarLayout = coordinatorLayout.findViewById(R.id.collapsing_toolbar_layout); + + // Get header containers + avatarContainer = coordinatorLayout.findViewById(R.id.avatar_container); + nameContainer = coordinatorLayout.findViewById(R.id.name_container); + onlineContainer = coordinatorLayout.findViewById(R.id.online_container); + headerButtonsContainer = coordinatorLayout.findViewById(R.id.header_buttons_container); + + // Get collapsed title containers + collapsedTitleContainer = coordinatorLayout.findViewById(R.id.collapsed_title_container); + collapsedNameContainer = coordinatorLayout.findViewById(R.id.collapsed_name_container); + collapsedOnlineContainer = coordinatorLayout.findViewById(R.id.collapsed_online_container); + + // Setup StatusBarTransparency - TODO not sure if working or not, but leaving in + // for now + setupStatusBarTransparency(); + + // Setup header content + setupHeaderContent(context); + + // Setup content below header + setupContentArea(context); + + // Setup collapsed title text views + setupCollapsedTitles(context); + + // Setup scroll behavior for collapsing + setupScrollBehavior(); + + // Configure layout containers to prevent clipping during animations + setupLayoutClipping(); + + // Setup theme + updateTheme(); + + fragmentView = coordinatorLayout; + return fragmentView; + } + + // IMPORTANT FOR NEW PROFILE LAYOUT TO SET TRANSPARENCY HERE + @Override + public ActionBar createActionBar(Context context) { + ActionBar actionBar = new ActionBar(context, getResourceProvider()); + actionBar.setBackgroundColor(Color.TRANSPARENT); + actionBar.setItemsBackgroundColor(getThemedColor(Theme.key_actionBarDefaultSelector), false); + actionBar.setItemsBackgroundColor(getThemedColor(Theme.key_actionBarActionModeDefaultSelector), true); + actionBar.setItemsColor(getThemedColor(Theme.key_actionBarDefaultIcon), false); + actionBar.setItemsColor(getThemedColor(Theme.key_actionBarActionModeDefaultIcon), true); + actionBar.setCastShadows(false); + + // Completely remove any shadow or bottom line + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + actionBar.setOutlineProvider(null); + } + + // Remove any background drawable that might cause a line + actionBar.setBackgroundDrawable(null); + actionBar.setBackground(null); + + if (inPreviewMode || inBubbleMode) { + actionBar.setOccupyStatusBar(false); + } + return actionBar; + } + + private void setupHeaderContent(Context context) { + // Create avatar using BackupImageView + avatarImageView = new BackupImageView(context); + avatarImageView.getImageReceiver().setAllowDecodeSingleFrame(true); + avatarImageView.setRoundRadius(dp(42)); + avatarContainer.addView(avatarImageView, LayoutHelper.createFrame(84, 84, Gravity.CENTER)); + + // Initialize avatar drawable + avatarDrawable = new AvatarDrawable(); + + // Create name text view - identical settings to onlineTextView for consistent + // animation behavior + nameTextView = new TextView(context); + nameTextView.setTextColor(Color.WHITE); + nameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); + nameTextView.setTypeface(AndroidUtilities.bold()); + nameTextView.setGravity(Gravity.CENTER); + // Use match_parent width to allow for translation animation space + nameContainer.addView(nameTextView, + LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); + + // Create online status text view + onlineTextView = new TextView(context); + onlineTextView.setTextColor(Color.WHITE); + onlineTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14); + onlineTextView.setGravity(Gravity.CENTER); + onlineTextView.setAlpha(1f); + // Use match_parent width to match name container layout + onlineContainer.addView(onlineTextView, + LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.CENTER)); + + // CRITICAL: Ensure all containers allow text to translate beyond their bounds + setupContainerClippingForTranslation(); + + // ADDITIONAL: Configure FrameLayouts to prevent internal text clipping + setupFrameLayoutsForTranslation(); + + // Create header buttons using provided vector drawable icons + messageButton = new BubbleButton(context, R.drawable.profile_header_message, + LocaleController.getString("SendMessage", R.string.SendMessage)); + muteButton = new BubbleButton(context, R.drawable.profile_header_unmute, + LocaleController.getString("Unmute", R.string.Unmute)); + callButton = new BubbleButton(context, R.drawable.profile_header_call, + LocaleController.getString("Call", R.string.Call)); + videoButton = new BubbleButton(context, R.drawable.profile_header_video, + LocaleController.getString("VideoCall", R.string.VideoCall)); + + // Set button click listeners + messageButton.setOnClickListener(v -> onMessageButtonClick()); + muteButton.setOnClickListener(v -> onMuteButtonClick()); + callButton.setOnClickListener(v -> onCallButtonClick()); + videoButton.setOnClickListener(v -> onVideoCallButtonClick()); + + // Add buttons to container + LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(0, dp(56), 1.0f); + headerButtonsContainer.addView(messageButton, buttonParams); + + buttonParams = new LinearLayout.LayoutParams(0, dp(56), 1.0f); + buttonParams.leftMargin = dp(8); + headerButtonsContainer.addView(muteButton, buttonParams); + + buttonParams = new LinearLayout.LayoutParams(0, dp(56), 1.0f); + buttonParams.leftMargin = dp(8); + headerButtonsContainer.addView(callButton, buttonParams); + + buttonParams = new LinearLayout.LayoutParams(0, dp(56), 1.0f); + buttonParams.leftMargin = dp(8); + headerButtonsContainer.addView(videoButton, buttonParams); + + // Setup gift particle system + // giftParticleSystem = new HeaderGiftParticleSystem(context, null); + // avatarContainer.addView(giftParticleSystem, + // LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, + // LayoutHelper.MATCH_PARENT)); + + // Update header content + updateHeaderContent(); + } + + private void setupContentArea(Context context) { + // Create nested scroll view for content + nestedScrollView = new NestedScrollView(context); + nestedScrollView.setFillViewport(true); + + // Create content container with proper margins + FrameLayout contentContainer = new FrameLayout(context); + contentContainer.setBackgroundColor(getThemedColor(Theme.key_windowBackgroundWhite)); + contentContainer.setPadding(0, 0, 0, dp(80)); // Add bottom margin so bubble buttons are visible + + // Add content here (tabs, lists, etc.) + createProfileContent(contentContainer, context); + + nestedScrollView.addView(contentContainer); + + // Find content placeholder and replace with nested scroll view + FrameLayout contentPlaceholder = coordinatorLayout.findViewById(R.id.content_placeholder); + ViewGroup parent = (ViewGroup) contentPlaceholder.getParent(); + int index = parent.indexOfChild(contentPlaceholder); + parent.removeView(contentPlaceholder); + parent.addView(nestedScrollView, index, contentPlaceholder.getLayoutParams()); + } + + private void createProfileContent(FrameLayout container, Context context) { + // Create SharedMediaLayout first + createSharedMediaLayout(context); + + // Create RecyclerListView with existing ProfileActivity content structure + RecyclerListView listView = new RecyclerListView(context); + listView.setVerticalScrollBarEnabled(false); + listView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); + + // Create adapter with profile content + updateRowsIds(); + listAdapter = new FullProfileContentAdapter(context); + listView.setAdapter(listAdapter); + + // Add scroll listener to handle header visibility changes + listView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + checkListViewScroll(); + } + }); + + // Add click handler that uses processOnClickOrPress logic + listView.setOnItemClickListener((view, position, x, y) -> { + processOnClickOrPress(position, view, x, y); + }); + + container.addView(listView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + } + + private void createSharedMediaLayout(Context context) { + long did = dialogId; + if (currentUser != null) { + did = currentUser.id; + } else if (currentChat != null) { + did = -currentChat.id; + } + + // Initialize with proper parameters like ProfileActivity + int commonGroupsCount = userInfo != null ? userInfo.common_chats_count : 0; + int initialTab = 0; // Start with photos/videos tab + + sharedMediaLayout = new SharedMediaLayout(context, did, sharedMediaPreloader, commonGroupsCount, null, chatInfo, + userInfo, initialTab, this, this, SharedMediaLayout.VIEW_TYPE_PROFILE_ACTIVITY, getResourceProvider()) { + @Override + protected void onSelectedTabChanged() { + // Handle tab change if needed + } + + @Override + protected boolean canShowSearchItem() { + return true; + } + + @Override + protected void onSearchStateChanged(boolean expanded) { + // Handle search state change if needed + } + }; + + sharedMediaLayout.setLayoutParams(new RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, + RecyclerView.LayoutParams.WRAP_CONTENT)); + } + + private void setupCollapsedTitles(Context context) { + // Create collapsed name text view + collapsedNameTextView = new TextView(context); + collapsedNameTextView.setTextColor(Color.WHITE); + collapsedNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18); + collapsedNameTextView.setTypeface(AndroidUtilities.bold()); + collapsedNameTextView.setGravity(Gravity.START); + collapsedNameTextView.setAlpha(1f); + collapsedNameContainer.addView(collapsedNameTextView, + LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.START)); + + // Create collapsed online status text view + collapsedOnlineTextView = new TextView(context); + collapsedOnlineTextView.setTextColor(Color.WHITE); + collapsedOnlineTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); + collapsedOnlineTextView.setGravity(Gravity.START); + collapsedOnlineTextView.setAlpha(1f); + collapsedOnlineContainer.addView(collapsedOnlineTextView, + LayoutHelper.createFrame(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, Gravity.START)); + } + + private void setupScrollBehavior() { + // Connect avatar view with animation helper for black circle effect + // avatarImageView.setAnimationHelper(avatarAnimationHelper); + + // Set up views for the animation helpers + offsetListener.setAnimatedViews(avatarImageView, nameContainer, collapsedTitleContainer, + headerButtonsContainer); + offsetListener.setTextViews(nameTextView, onlineTextView); // Pass individual text views + textTransitionHelper.setTextViews(nameTextView, onlineTextView, collapsedNameTextView, collapsedOnlineTextView); + + // Set up button animation helper with bubble button views + buttonAnimationHelper.setButtonViews(headerButtonsContainer, messageButton, muteButton, callButton, + videoButton); + buttonAnimationHelper.initializeLayout(AndroidUtilities.dp(320)); // Header height + + // Add our sophisticated animation listener to the AppBarLayout + appBarLayout.addOnOffsetChangedListener(offsetListener); + } + + private void setupLayoutClipping() { + // Configure layout containers to prevent text and avatar clipping during + // animations + // This ensures smooth translation animations without content vanishing + + if (appBarLayout instanceof ViewGroup) { + ((ViewGroup) appBarLayout).setClipChildren(false); + ((ViewGroup) appBarLayout).setClipToPadding(false); + } + + if (collapsingToolbarLayout instanceof ViewGroup) { + ((ViewGroup) collapsingToolbarLayout).setClipChildren(false); + ((ViewGroup) collapsingToolbarLayout).setClipToPadding(false); + } + + // Find the expanded header container + View expandedHeaderContainer = coordinatorLayout.findViewById(R.id.expanded_header_container); + if (expandedHeaderContainer instanceof ViewGroup) { + ((ViewGroup) expandedHeaderContainer).setClipChildren(false); + ((ViewGroup) expandedHeaderContainer).setClipToPadding(false); + } + + if (collapsedTitleContainer instanceof ViewGroup) { + ((ViewGroup) collapsedTitleContainer).setClipChildren(false); + ((ViewGroup) collapsedTitleContainer).setClipToPadding(false); + } + + // Also configure the main coordinator layout to allow overflow + if (coordinatorLayout instanceof ViewGroup) { + coordinatorLayout.setClipChildren(false); + coordinatorLayout.setClipToPadding(false); + } + + // Configure text containers to prevent clipping during translation animations + if (nameContainer instanceof ViewGroup) { + ((ViewGroup) nameContainer).setClipChildren(false); + ((ViewGroup) nameContainer).setClipToPadding(false); + } + + if (onlineContainer instanceof ViewGroup) { + ((ViewGroup) onlineContainer).setClipChildren(false); + ((ViewGroup) onlineContainer).setClipToPadding(false); + } + + if (avatarContainer instanceof ViewGroup) { + ((ViewGroup) avatarContainer).setClipChildren(false); + ((ViewGroup) avatarContainer).setClipToPadding(false); + } + } + + private void setupStatusBarTransparency() { + // Apply status bar transparency using multiple research-backed techniques from + // Stack Overflow + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Method 1: Set status bar color to transparent + getParentActivity().getWindow().setStatusBarColor(Color.TRANSPARENT); + + // Method 2: Use FLAG_LAYOUT_NO_LIMITS for complete transparency + getParentActivity().getWindow().setFlags( + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + + // Method 3: Set proper system UI visibility flags + getParentActivity().getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // For KitKat, use translucent status bar + getParentActivity().getWindow().setFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + + // Method 4: Configure CoordinatorLayout for status bar transparency + if (coordinatorLayout != null) { + coordinatorLayout.setFitsSystemWindows(true); + coordinatorLayout.setStatusBarBackground(null); + } + } + + private void updateRowsIds() { + // Initialize all rows to -1 + setAvatarRow = setAvatarSectionRow = channelRow = channelDividerRow = numberSectionRow = numberRow = birthdayRow = setUsernameRow = bioRow = phoneSuggestionSectionRow = graceSuggestionRow = graceSuggestionSectionRow = phoneSuggestionRow = passwordSuggestionSectionRow = passwordSuggestionRow = settingsSectionRow = settingsSectionRow2 = languageRow = privacyRow = dataRow = chatRow = filtersRow = liteModeRow = stickersRow = devicesRow = devicesSectionRow = helpHeaderRow = questionRow = faqRow = policyRow = helpSectionCell = debugHeaderRow = sendLogsRow = sendLastLogsRow = clearLogsRow = switchBackendRow = versionRow = emptyRow = bottomPaddingRow = infoHeaderRow = phoneRow = locationRow = userInfoRow = channelInfoRow = usernameRow = bizHoursRow = bizLocationRow = infoStartRow = infoEndRow = infoSectionRow = affiliateRow = infoAffiliateRow = sendMessageRow = reportRow = reportReactionRow = reportDividerRow = addToContactsRow = addToGroupButtonRow = addToGroupInfoRow = premiumRow = starsRow = tonRow = businessRow = premiumGiftingRow = premiumSectionsRow = botAppRow = botPermissionsHeader = botPermissionLocation = botPermissionEmojiStatus = botPermissionBiometry = botPermissionsDivider = settingsTimerRow = settingsKeyRow = secretSettingsSectionRow = membersHeaderRow = membersStartRow = membersEndRow = addMemberRow = subscribersRow = subscribersRequestsRow = administratorsRow = settingsRow = botStarsBalanceRow = botTonBalanceRow = channelBalanceRow = channelBalanceSectionRow = balanceDividerRow = blockedUsersRow = membersSectionRow = sharedMediaRow = unblockRow = joinRow = lastSectionRow = -1; + + rowCount = 0; + + // Check if SharedMediaLayout has content (copied from ProfileActivity) + boolean hasMedia = false; + if (sharedMediaPreloader != null) { + int[] lastMediaCount = sharedMediaPreloader.getLastMediaCount(); + for (int a = 0; a < lastMediaCount.length; a++) { + if (lastMediaCount[a] > 0) { + hasMedia = true; + break; + } + } + if (!hasMedia) { + hasMedia = sharedMediaPreloader.hasSavedMessages; + } + } + + // Handle different profile types + if (currentUser != null) { + boolean isUserSelf = UserObject.isUserSelf(currentUser); + + if (isUserSelf) { + // Self profile layout + setAvatarRow = rowCount++; + setAvatarSectionRow = rowCount++; + + numberSectionRow = rowCount++; + numberRow = rowCount++; + if (userInfo != null && userInfo.birthday != null) { + birthdayRow = rowCount++; + } + setUsernameRow = rowCount++; + bioRow = rowCount++; + + // Settings sections + settingsSectionRow = rowCount++; + chatRow = rowCount++; + privacyRow = rowCount++; + dataRow = rowCount++; + liteModeRow = rowCount++; + filtersRow = rowCount++; + devicesRow = rowCount++; + devicesSectionRow = rowCount++; + + // Premium features + premiumRow = rowCount++; + starsRow = rowCount++; + businessRow = rowCount++; + premiumSectionsRow = rowCount++; + + // Help section + helpHeaderRow = rowCount++; + questionRow = rowCount++; + faqRow = rowCount++; + policyRow = rowCount++; + helpSectionCell = rowCount++; + + if (BuildVars.LOGS_ENABLED) { + debugHeaderRow = rowCount++; + sendLogsRow = rowCount++; + sendLastLogsRow = rowCount++; + clearLogsRow = rowCount++; + if (BuildVars.DEBUG_VERSION) { + switchBackendRow = rowCount++; + } + versionRow = rowCount++; + } + + } else { + // Other user profile layout + infoHeaderRow = rowCount++; + + if (!TextUtils.isEmpty(currentUser.phone)) { + phoneRow = rowCount++; + } + + if (!TextUtils.isEmpty(currentUser.username)) { + usernameRow = rowCount++; + } + + userInfoRow = rowCount++; + + if (userInfo != null && userInfo.birthday != null) { + birthdayRow = rowCount++; + } + + // Business info if available + // TODO: Add business hours and location checks + + infoSectionRow = rowCount++; + + // Action buttons + if (!currentUser.bot) { + sendMessageRow = rowCount++; + } + + // Contact actions + boolean isContact = getContactsController().isContact(currentUser.id); + if (isContact) { + // Contact options + } else { + addToContactsRow = rowCount++; + } + + reportDividerRow = rowCount++; + reportRow = rowCount++; + } + + } else if (currentChat != null) { + // Chat/Channel profile layout + channelInfoRow = rowCount++; + + if (chatInfo != null && chatInfo.location instanceof TLRPC.TL_channelLocation) { + locationRow = rowCount++; + } + + infoSectionRow = rowCount++; + + // Members section + if (ChatObject.isChannel(currentChat) && !currentChat.megagroup) { + subscribersRow = rowCount++; + if (currentChat.admin_rights != null && currentChat.admin_rights.invite_users) { + subscribersRequestsRow = rowCount++; + } + administratorsRow = rowCount++; + } else if (currentChat.megagroup) { + membersHeaderRow = rowCount++; + // TODO: Add member rows + addMemberRow = rowCount++; + } + + membersSectionRow = rowCount++; + } + + // Add SharedMediaLayout if there's content or it's needed + if (hasMedia || currentUser != null || currentChat != null) { + sharedMediaRow = rowCount++; + } + + // Add bottom padding if no shared media + if (sharedMediaRow == -1) { + bottomPaddingRow = rowCount++; + } + } + + private boolean processOnClickOrPress(final int position, final View view, final float x, final float y) { + if (position == phoneRow && currentUser != null) { + // Handle phone number click - show call/copy options + try { + VoIPHelper.startCall(currentUser, false, currentUser.id != 0, getParentActivity(), null, + getAccountInstance()); + } catch (Exception e) { + FileLog.e(e); + } + return true; + } else if (position == usernameRow && currentUser != null) { + // Handle username click - show QR or copy + showQRCode(); + return true; + } else if (position == bioRow) { + // Handle bio click - edit if self, otherwise do nothing + if (currentUser != null && UserObject.isUserSelf(currentUser)) { + presentFragment(new ChangeBioActivity()); + } + return true; + } else if (position == sendMessageRow) { + // Open chat with user + if (currentUser != null) { + Bundle args = new Bundle(); + args.putLong("user_id", currentUser.id); + presentFragment(new ChatActivity(args), true); + } + return true; + } else if (position == addToContactsRow) { + // Add user to contacts + if (currentUser != null) { + Bundle args = new Bundle(); + args.putLong("user_id", currentUser.id); + args.putBoolean("addContact", true); + presentFragment(new ContactAddActivity(args)); + } + return true; + } else if (position == reportRow) { + // Report user/chat + handleReport(); + return true; + } else if (position == subscribersRow) { + // Show subscribers list + if (currentChat != null) { + Bundle args = new Bundle(); + args.putLong("chat_id", currentChat.id); + args.putInt("type", ChatUsersActivity.TYPE_USERS); + presentFragment(new ChatUsersActivity(args)); + } + return true; + } else if (position == administratorsRow) { + // Show administrators list + if (currentChat != null) { + Bundle args = new Bundle(); + args.putLong("chat_id", currentChat.id); + args.putInt("type", ChatUsersActivity.TYPE_ADMIN); + presentFragment(new ChatUsersActivity(args)); + } + return true; + } else if (position == addMemberRow) { + // Add member to group + if (currentChat != null) { + Bundle args = new Bundle(); + args.putBoolean("addToGroup", true); + args.putLong("chatId", currentChat.id); + presentFragment(new GroupCreateActivity(args), false); + } + return true; + } else if (position == settingsRow) { + // Open chat settings + if (currentChat != null) { + Bundle args = new Bundle(); + args.putLong("chat_id", currentChat.id); + presentFragment(new ChatEditActivity(args)); + } + return true; + } else if (position == chatRow) { + // Chat settings + presentFragment(new ThemeActivity(ThemeActivity.THEME_TYPE_BASIC)); + return true; + } else if (position == privacyRow) { + // Privacy settings + presentFragment(new PrivacySettingsActivity()); + return true; + } else if (position == dataRow) { + // Data settings + presentFragment(new DataSettingsActivity()); + return true; + } else if (position == liteModeRow) { + // Lite mode settings + presentFragment(new LiteModeSettingsActivity()); + return true; + } else if (position == languageRow) { + // Language settings + presentFragment(new LanguageSelectActivity()); + return true; + } else if (position == devicesRow) { + // Devices settings + presentFragment(new SessionsActivity(0)); + return true; + } else if (position == questionRow) { + // Ask a question + showDialog(AlertsCreator.createSupportAlert(this, null)); + return true; + } else if (position == faqRow) { + // FAQ + Browser.openUrl(getParentActivity(), LocaleController.getString("TelegramFaqUrl", R.string.TelegramFaqUrl)); + return true; + } else if (position == policyRow) { + // Privacy policy + Browser.openUrl(getParentActivity(), + LocaleController.getString("PrivacyPolicyUrl", R.string.PrivacyPolicyUrl)); + return true; + } else if (position == sendLogsRow) { + // Send logs + sendLogs(false); + return true; + } else if (position == sendLastLogsRow) { + // Send last logs + sendLogs(true); + return true; + } else if (position == clearLogsRow) { + // Clear logs + FileLog.cleanupLogs(); + return true; + } else if (position == switchBackendRow) { + // Switch backend (debug only) + if (BuildVars.DEBUG_VERSION) { + // Toggle backend and restart + SharedPreferences preferences = MessagesController.getGlobalMainSettings(); + preferences.edit().putInt("dc" + currentAccount, ConnectionsManager.DEFAULT_DATACENTER_ID == 1 ? 3 : 1) + .apply(); + // Show restart dialog + } + return true; + } else if (position == premiumRow) { + // Premium settings + presentFragment(new PremiumPreviewFragment("settings")); + return true; + } else if (position == starsRow) { + // Stars + presentFragment(new StarsIntroActivity()); + return true; + } else if (position == businessRow) { + // Business settings + presentFragment(new BusinessLinksActivity()); + return true; + } + return false; + } + + private void showQRCode() { + if (currentUser != null && !TextUtils.isEmpty(currentUser.username)) { + // Show QR code for user + Bundle args = new Bundle(); + args.putLong("chat_id", userId); + args.putString("username", currentUser.username); + // presentFragment(new QrActivity(args)); // TODO: Implement QR activity + } + } + + private void sendLogs(boolean last) { + // Send debug logs functionality + if (getParentActivity() == null) { + return; + } + AlertDialog progressDialog = new AlertDialog(getParentActivity(), AlertDialog.ALERT_TYPE_SPINNER); + progressDialog.setCanCancel(false); + progressDialog.show(); + + Utilities.globalQueue.postRunnable(() -> { + try { + File logDir = AndroidUtilities.getLogsDir(); + if (logDir == null) { + return; + } + File[] files = logDir.listFiles(); + if (files == null || files.length == 0) { + return; + } + + File shareFile = null; + if (last) { + // Send only the last log file + long lastModified = 0; + for (File file : files) { + if (file.lastModified() > lastModified) { + lastModified = file.lastModified(); + shareFile = file; + } + } + } else { + // Send all logs in a ZIP file + // TODO: Implement ZIP creation and sharing + shareFile = files[files.length - 1]; // For now, just send the last file + } + + if (shareFile != null) { + File finalFile = shareFile; + AndroidUtilities.runOnUIThread(() -> { + progressDialog.dismiss(); + try { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_STREAM, + androidx.core.content.FileProvider.getUriForFile(getParentActivity(), + ApplicationLoader.getApplicationId() + ".provider", finalFile)); + getParentActivity().startActivity(Intent.createChooser(intent, "Send logs")); + } catch (Exception e) { + FileLog.e(e); + } + }); + } + } catch (Exception e) { + FileLog.e(e); + AndroidUtilities.runOnUIThread(() -> progressDialog.dismiss()); + } + }); + } + + // Full content adapter that mirrors ProfileActivity structure + private class FullProfileContentAdapter extends RecyclerListView.SelectionAdapter { + private final static int VIEW_TYPE_HEADER = 1, + VIEW_TYPE_TEXT_DETAIL = 2, + VIEW_TYPE_ABOUT_LINK = 3, + VIEW_TYPE_TEXT = 4, + VIEW_TYPE_DIVIDER = 5, + VIEW_TYPE_NOTIFICATIONS_CHECK = 6, + VIEW_TYPE_SHADOW = 7, + VIEW_TYPE_USER = 8, + VIEW_TYPE_EMPTY = 11, + VIEW_TYPE_BOTTOM_PADDING = 12, + VIEW_TYPE_SHARED_MEDIA = 13, + VIEW_TYPE_VERSION = 14, + VIEW_TYPE_SUGGESTION = 15, + VIEW_TYPE_ADDTOGROUP_INFO = 17, + VIEW_TYPE_PREMIUM_TEXT_CELL = 18, + VIEW_TYPE_TEXT_DETAIL_MULTILINE = 19, + VIEW_TYPE_NOTIFICATIONS_CHECK_SIMPLE = 20, + VIEW_TYPE_LOCATION = 21, + VIEW_TYPE_HOURS = 22, + VIEW_TYPE_CHANNEL = 23, + VIEW_TYPE_STARS_TEXT_CELL = 24, + VIEW_TYPE_BOT_APP = 25, + VIEW_TYPE_SHADOW_TEXT = 26, + VIEW_TYPE_COLORFUL_TEXT = 27; + + private Context context; + + public FullProfileContentAdapter(Context context) { + this.context = context; + } + + @Override + public int getItemCount() { + return rowCount; + } + + @Override + public int getItemViewType(int position) { + if (position == setAvatarRow || position == infoHeaderRow || position == helpHeaderRow || + position == debugHeaderRow || position == membersHeaderRow) { + return VIEW_TYPE_HEADER; + } else if (position == phoneRow || position == usernameRow || position == numberRow || + position == birthdayRow || position == setUsernameRow || position == userInfoRow || + position == locationRow || position == channelInfoRow) { + return VIEW_TYPE_TEXT_DETAIL; + } else if (position == bioRow) { + return VIEW_TYPE_ABOUT_LINK; + } else if (position == chatRow || position == privacyRow || position == dataRow || + position == liteModeRow || position == filtersRow || position == devicesRow || + position == languageRow || position == questionRow || position == faqRow || + position == policyRow || position == premiumRow || position == starsRow || + position == businessRow || position == sendMessageRow || position == addToContactsRow || + position == reportRow || position == subscribersRow || position == administratorsRow || + position == addMemberRow || position == sendLogsRow || position == sendLastLogsRow || + position == clearLogsRow) { + return VIEW_TYPE_TEXT; + } else if (position == setAvatarSectionRow || position == numberSectionRow || + position == settingsSectionRow || position == devicesSectionRow || + position == helpSectionCell || position == infoSectionRow || + position == reportDividerRow || position == membersSectionRow) { + return VIEW_TYPE_SHADOW; + } else if (position == bizHoursRow) { + return VIEW_TYPE_HOURS; + } else if (position == bizLocationRow) { + return VIEW_TYPE_LOCATION; + } else if (position == sharedMediaRow) { + return VIEW_TYPE_SHARED_MEDIA; + } else if (position == versionRow) { + return VIEW_TYPE_VERSION; + } else if (position == bottomPaddingRow) { + return VIEW_TYPE_BOTTOM_PADDING; + } else if (position == emptyRow) { + return VIEW_TYPE_EMPTY; + } + return VIEW_TYPE_TEXT; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view; + switch (viewType) { + case VIEW_TYPE_HEADER: + view = new HeaderCell(context, getResourceProvider()); + break; + case VIEW_TYPE_TEXT_DETAIL: + view = new TextDetailCell(context, getResourceProvider()); + break; + case VIEW_TYPE_ABOUT_LINK: + view = new AboutLinkCell(context, ProfileNewActivity.this); + break; + case VIEW_TYPE_TEXT: + view = new TextCell(context, getResourceProvider()); + break; + case VIEW_TYPE_DIVIDER: + view = new DividerCell(context); + break; + case VIEW_TYPE_SHADOW: + view = new ShadowSectionCell(context); + break; + case VIEW_TYPE_USER: + view = new UserCell(context, 61, 0, false); + break; + case VIEW_TYPE_SHARED_MEDIA: + if (sharedMediaLayout != null && sharedMediaLayout.getParent() == null) { + view = sharedMediaLayout; + } else { + view = new FrameLayout(context) { + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, + MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(90), MeasureSpec.EXACTLY)); + } + }; + view.setBackgroundColor(getThemedColor(Theme.key_windowBackgroundWhite)); + } + break; + case VIEW_TYPE_EMPTY: + view = new View(context); + view.setBackgroundColor(getThemedColor(Theme.key_windowBackgroundWhite)); + break; + case VIEW_TYPE_BOTTOM_PADDING: + view = new View(context) { + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, + MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(90), MeasureSpec.EXACTLY)); + } + }; + view.setBackgroundColor(getThemedColor(Theme.key_windowBackgroundWhite)); + break; + case VIEW_TYPE_VERSION: + view = new TextCell(context, getResourceProvider()); + break; + case VIEW_TYPE_HOURS: + view = new ProfileHoursCell(context, getResourceProvider()); + break; + case VIEW_TYPE_LOCATION: + view = new ProfileLocationCell(context, getResourceProvider()); + break; + default: + view = new TextCell(context, getResourceProvider()); + break; + } + return new RecyclerListView.Holder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + if (holder == null || holder.itemView == null) + return; + + View view = holder.itemView; + + // Handle different row types with complete ProfileActivity binding logic + if (position == setAvatarRow) { + HeaderCell cell = (HeaderCell) view; + cell.setText(LocaleController.getString("SetProfilePhoto", R.string.SetProfilePhoto)); + } else if (position == infoHeaderRow) { + HeaderCell cell = (HeaderCell) view; + cell.setText(LocaleController.getString("Info", R.string.Info)); + } else if (position == helpHeaderRow) { + HeaderCell cell = (HeaderCell) view; + cell.setText(LocaleController.getString("SettingsHelp", R.string.SettingsHelp)); + } else if (position == debugHeaderRow) { + HeaderCell cell = (HeaderCell) view; + cell.setText("Debug"); + } else if (position == membersHeaderRow) { + HeaderCell cell = (HeaderCell) view; + cell.setText(LocaleController.getString("ChannelMembers", R.string.ChannelMembers)); + } else if (position == phoneRow && currentUser != null && !TextUtils.isEmpty(currentUser.phone)) { + TextDetailCell cell = (TextDetailCell) view; + cell.setTextAndValue(LocaleController.getString("PhoneMobile", R.string.PhoneMobile), + "+" + currentUser.phone, false); + } else if (position == usernameRow && currentUser != null && !TextUtils.isEmpty(currentUser.username)) { + TextDetailCell cell = (TextDetailCell) view; + cell.setTextAndValue(LocaleController.getString("Username", R.string.Username), + "@" + currentUser.username, false); + + // Add QR code icon like in ProfileActivity + try { + Drawable drawable = ContextCompat.getDrawable(context, R.drawable.msg_qr_mini); + if (drawable != null) { + drawable.setColorFilter(new PorterDuffColorFilter( + getThemedColor(Theme.key_switch2TrackChecked), PorterDuff.Mode.MULTIPLY)); + cell.setImage(drawable, LocaleController.getString("GetQRCode", R.string.GetQRCode)); + cell.setImageClickListener(v -> showQRCode()); + } + } catch (Exception e) { + // Ignore QR icon setup errors + } + } else if (position == numberRow && currentUser != null) { + TextDetailCell cell = (TextDetailCell) view; + cell.setTextAndValue(LocaleController.getString("Phone", R.string.Phone), + !TextUtils.isEmpty(currentUser.phone) ? "+" + currentUser.phone : "Unknown", false); + } else if (position == birthdayRow && userInfo != null && userInfo.birthday != null) { + TextDetailCell cell = (TextDetailCell) view; + String birthdayText = formatBirthday(userInfo.birthday); + cell.setTextAndValue(LocaleController.getString("ContactBirthday", R.string.ContactBirthday), + birthdayText, false); + } else if (position == userInfoRow && currentUser != null) { + TextDetailCell cell = (TextDetailCell) view; + String about = userInfo != null && !TextUtils.isEmpty(userInfo.about) ? userInfo.about : ""; + cell.setTextAndValue(LocaleController.getString("UserBio", R.string.UserBio), about, false); + } else if (position == locationRow && chatInfo != null + && chatInfo.location instanceof TLRPC.TL_channelLocation) { + TextDetailCell cell = (TextDetailCell) view; + TLRPC.TL_channelLocation location = (TLRPC.TL_channelLocation) chatInfo.location; + String locationText = formatLocation(location); + cell.setTextAndValue(LocaleController.getString("AttachLocation", R.string.AttachLocation), + locationText, false); + } else if (position == channelInfoRow && currentChat != null) { + TextDetailCell cell = (TextDetailCell) view; + String about = chatInfo != null && !TextUtils.isEmpty(chatInfo.about) ? chatInfo.about : ""; + cell.setTextAndValue(LocaleController.getString("Info", R.string.Info), about, false); + } else if (position == bioRow) { + AboutLinkCell cell = (AboutLinkCell) view; + String bio = ""; + if (currentUser != null && userInfo != null && !TextUtils.isEmpty(userInfo.about)) { + bio = userInfo.about; + } else if (currentChat != null && chatInfo != null && !TextUtils.isEmpty(chatInfo.about)) { + bio = chatInfo.about; + } + cell.setText(bio, false); + } else if (position == chatRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("ChatSettings", R.string.ChatSettings), + R.drawable.msg_settings, true); + } else if (position == privacyRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("PrivacySettings", R.string.PrivacySettings), + R.drawable.msg_secret, true); + } else if (position == dataRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("DataSettings", R.string.DataSettings), + R.drawable.msg_settings, true); + } else if (position == liteModeRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("LiteMode", R.string.LiteMode), R.drawable.msg_settings, + true); + } else if (position == languageRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("Language", R.string.Language), R.drawable.msg_language, + true); + } else if (position == devicesRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("Devices", R.string.Devices), R.drawable.msg_settings, + true); + } else if (position == questionRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("AskAQuestion", R.string.AskAQuestion), + R.drawable.msg_settings, true); + } else if (position == faqRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("TelegramFAQ", R.string.TelegramFAQ), + R.drawable.msg_settings, true); + } else if (position == policyRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("PrivacyPolicy", R.string.PrivacyPolicy), + R.drawable.msg_policy, true); + } else if (position == premiumRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("TelegramPremium", R.string.TelegramPremium), + R.drawable.msg_settings, true); + } else if (position == starsRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("TelegramStars", R.string.TelegramStars), + R.drawable.menu_premium_main, true); + } else if (position == businessRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("TelegramBusiness", R.string.TelegramBusiness), + R.drawable.menu_premium_main, true); + } else if (position == sendMessageRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("SendMessage", R.string.SendMessage), + R.drawable.msg_message, true); + } else if (position == addToContactsRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("AddContact", R.string.AddContact), + R.drawable.msg_addcontact, true); + } else if (position == reportRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("ReportChat", R.string.ReportChat), + R.drawable.msg_report, true); + } else if (position == subscribersRow) { + TextCell cell = (TextCell) view; + int count = chatInfo != null ? chatInfo.participants_count + : currentChat != null ? currentChat.participants_count : 0; + cell.setTextAndValueAndIcon( + LocaleController.getString("ChannelSubscribers", R.string.ChannelSubscribers), + String.valueOf(count), R.drawable.msg_groups, true); + } else if (position == administratorsRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("ChannelAdministrators", R.string.ChannelAdministrators), + R.drawable.msg_settings, true); + } else if (position == addMemberRow) { + TextCell cell = (TextCell) view; + cell.setTextAndIcon(LocaleController.getString("AddMember", R.string.AddMember), + R.drawable.msg_contact_add, true); + } else if (position == sendLogsRow) { + TextCell cell = (TextCell) view; + cell.setText("Send Logs", false); + } else if (position == sendLastLogsRow) { + TextCell cell = (TextCell) view; + cell.setText("Send Last Logs", false); + } else if (position == clearLogsRow) { + TextCell cell = (TextCell) view; + cell.setText("Clear Logs", false); + } else if (position == versionRow) { + TextCell cell = (TextCell) view; + cell.setTextAndValue("Version", "Telegram " + BuildVars.BUILD_VERSION_STRING, false); + } else if (position == sharedMediaRow) { + // SharedMediaLayout is self-contained and doesn't need binding + // The view is either SharedMediaLayout or a placeholder FrameLayout + } else if (position == bizHoursRow && chatInfo != null) { + ProfileHoursCell cell = (ProfileHoursCell) view; + // TODO: Bind business hours data when available + // cell.setBusinessHours(chatInfo.business_hours); + } else if (position == bizLocationRow && chatInfo != null) { + ProfileLocationCell cell = (ProfileLocationCell) view; + if (chatInfo.location instanceof TLRPC.TL_channelLocation) { + TLRPC.TL_channelLocation location = (TLRPC.TL_channelLocation) chatInfo.location; + // cell.setLocation(location); + } + } + } + + @Override + public boolean isEnabled(RecyclerView.ViewHolder holder) { + int viewType = holder.getItemViewType(); + return viewType == VIEW_TYPE_TEXT_DETAIL || + viewType == VIEW_TYPE_SHARED_MEDIA; + } + } + + private void updateHeaderContent() { + String name = ""; + String onlineStatus = ""; + + if (currentUser != null) { + // Update user profile using helper methods + name = UserObject.getUserName(currentUser); + onlineStatus = formatUserStatus(currentUser); + + nameTextView.setText(name); + onlineTextView.setText(onlineStatus); + + // Update text transition helper with new text + if (textTransitionHelper != null) { + textTransitionHelper.setText(name, onlineStatus); + } + + // Set up avatar drawable and load avatar image using ProfileActivity patterns + avatarDrawable.setInfo(currentAccount, currentUser); + ImageLocation imageLocation = ImageLocation.getForUserOrChat(currentUser, ImageLocation.TYPE_BIG); + ImageLocation thumbLocation = ImageLocation.getForUserOrChat(currentUser, ImageLocation.TYPE_SMALL); + + if (imageLocation != null) { + avatarImageView.setImage(imageLocation, "150_150", thumbLocation, "50_50", avatarDrawable, currentUser); + } else { + avatarImageView.setImageDrawable(avatarDrawable); + } + + // Update avatar click handler for photo viewing + avatarImageView.setOnClickListener(v -> openAvatar()); + + // Update button visibility for user profiles + messageButton.setVisibility(View.VISIBLE); + muteButton.setVisibility(View.VISIBLE); + callButton.setVisibility( + !currentUser.bot && currentUser.phone != null && !currentUser.phone.isEmpty() ? View.VISIBLE + : View.GONE); + videoButton.setVisibility( + !currentUser.bot && currentUser.phone != null && !currentUser.phone.isEmpty() ? View.VISIBLE + : View.GONE); + + } else if (currentChat != null) { + // Update chat profile using helper methods + name = currentChat.title; + onlineStatus = formatChatStatus(currentChat); + + nameTextView.setText(name); + onlineTextView.setText(onlineStatus); + + // Update text transition helper with new text + if (textTransitionHelper != null) { + textTransitionHelper.setText(name, onlineStatus); + } + + // Set up avatar drawable and load chat avatar image + avatarDrawable.setInfo(currentAccount, currentChat); + ImageLocation imageLocation = ImageLocation.getForUserOrChat(currentChat, ImageLocation.TYPE_BIG); + ImageLocation thumbLocation = ImageLocation.getForUserOrChat(currentChat, ImageLocation.TYPE_SMALL); + + if (imageLocation != null) { + avatarImageView.setImage(imageLocation, "150_150", thumbLocation, "50_50", avatarDrawable, currentChat); + } else { + avatarImageView.setImageDrawable(avatarDrawable); + } + + // Update avatar click handler for photo viewing + avatarImageView.setOnClickListener(v -> openAvatar()); + + // Update button visibility for chat profiles + messageButton.setVisibility(View.VISIBLE); + muteButton.setVisibility(View.VISIBLE); + callButton.setVisibility(View.GONE); + videoButton.setVisibility(View.GONE); + } + + // Update collapsed title text views with the same content + if (collapsedNameTextView != null) { + collapsedNameTextView.setText(name); + } + if (collapsedOnlineTextView != null) { + collapsedOnlineTextView.setText(onlineStatus); + } + + // Update mute button state + updateMuteButton(); + + // Update stories display + updateStoriesDisplay(); + + // Update premium status + updatePremiumStatus(); + } + + private void updateMuteButton() { + isMuted = getMessagesController().isDialogMuted(dialogId, 0); + if (isMuted) { + muteButton.updateIcon(R.drawable.profile_header_unmute); + muteButton.updateText(LocaleController.getString("Unmute", R.string.Unmute)); + } else { + muteButton.updateIcon(R.drawable.profile_header_mute); + muteButton.updateText(LocaleController.getString("Mute", R.string.Mute)); + } + } + + private void updateTheme() { + // Update theme colors - use the same color for both header and toolbar + int headerColor = getThemedColor(Theme.key_avatar_backgroundActionBarBlue); + appBarLayout.setBackgroundColor(headerColor); + } + + // Button click handlers + private void onMessageButtonClick() { + Bundle args = new Bundle(); + if (currentUser != null) { + args.putLong("user_id", currentUser.id); + } else if (currentChat != null) { + args.putLong("chat_id", currentChat.id); + } + presentFragment(new ChatActivity(args)); + } + + private void onMuteButtonClick() { + boolean muted = getMessagesController().isDialogMuted(dialogId, 0); + getNotificationsController().muteDialog(dialogId, 0, !muted); + if (fragmentView != null) { + BulletinFactory.createMuteBulletin(ProfileNewActivity.this, !muted, null).show(); + } + updateMuteButton(); + } + + private void onCallButtonClick() { + if (currentUser != null && !currentUser.bot) { + VoIPHelper.startCall(currentUser, false, + currentUser.id != 0, + getParentActivity(), null, getAccountInstance()); + } + } + + private void onVideoCallButtonClick() { + if (currentUser != null && !currentUser.bot) { + VoIPHelper.startCall(currentUser, true, + currentUser.id != 0, + getParentActivity(), null, getAccountInstance()); + } + } + + private void createActionBarMenu(boolean animated) { + if (otherItem == null) { + return; + } + + otherItem.removeAllSubItems(); + + if (currentUser != null) { + if (UserObject.isUserSelf(currentUser)) { + // Self user menu + otherItem.addSubItem(edit_info, R.drawable.msg_edit, + LocaleController.getString("EditInfo", R.string.EditInfo)); + otherItem.addSubItem(add_photo, R.drawable.msg_addphoto, + LocaleController.getString("AddPhoto", R.string.AddPhoto)); + otherItem.addSubItem(edit_color, R.drawable.menu_profile_colors, + LocaleController.getString("ProfileColorEdit", R.string.ProfileColorEdit)); + otherItem.addSubItem(set_username, R.drawable.menu_username_change, + LocaleController.getString("ProfileUsernameEdit", R.string.ProfileUsernameEdit)); + otherItem.addSubItem(copy_link_profile, R.drawable.msg_link2, + LocaleController.getString("ProfileCopyLink", R.string.ProfileCopyLink)); + otherItem.addSubItem(logout, R.drawable.msg_leave, + LocaleController.getString("LogOut", R.string.LogOut)); + } else { + // Other user menu + boolean isBot = currentUser.bot; + boolean userBlocked = getMessagesController().blockePeers.indexOfKey(currentUser.id) >= 0; + + if (isBot) { + otherItem.addSubItem(share, R.drawable.msg_share, + LocaleController.getString("BotShare", R.string.BotShare)); + otherItem.addSubItem(bot_privacy, R.drawable.menu_privacy_policy, + LocaleController.getString("BotPrivacyPolicy", R.string.BotPrivacyPolicy)); + otherItem.addSubItem(report, R.drawable.msg_report, + LocaleController.getString("ReportBot", R.string.ReportBot)); + if (userBlocked) { + otherItem.addSubItem(block_contact, R.drawable.msg_block, + LocaleController.getString("Unblock", R.string.Unblock)); + } else { + otherItem.addSubItem(block_contact, R.drawable.msg_block2, + LocaleController.getString("DeleteAndBlock", R.string.DeleteAndBlock)); + } + } else { + // Regular user menu + boolean isContact = getContactsController().isContact(currentUser.id); + + if (!isContact) { + otherItem.addSubItem(add_contact, R.drawable.msg_addcontact, + LocaleController.getString("AddContact", R.string.AddContact)); + } else { + otherItem.addSubItem(edit_contact, R.drawable.msg_edit, + LocaleController.getString("EditContact", R.string.EditContact)); + otherItem.addSubItem(delete_contact, R.drawable.msg_delete, + LocaleController.getString("DeleteContact", R.string.DeleteContact)); + } + + otherItem.addSubItem(share_contact, R.drawable.msg_share, + LocaleController.getString("ShareContact", R.string.ShareContact)); + + if (userBlocked) { + otherItem.addSubItem(block_contact, R.drawable.msg_block, + LocaleController.getString("Unblock", R.string.Unblock)); + } else { + otherItem.addSubItem(block_contact, R.drawable.msg_block, + LocaleController.getString("BlockContact", R.string.BlockContact)); + } + + if (!UserObject.isDeleted(currentUser)) { + otherItem.addSubItem(gift_premium, R.drawable.msg_gift_premium, + LocaleController.getString("ProfileSendAGift", R.string.ProfileSendAGift)); + otherItem.addSubItem(start_secret_chat, R.drawable.msg_secret, + LocaleController.getString("StartEncryptedChat", R.string.StartEncryptedChat)); + } + + otherItem.addSubItem(report, R.drawable.msg_report, + LocaleController.getString("ReportChat", R.string.ReportChat)); + } + + otherItem.addSubItem(add_shortcut, R.drawable.msg_link, + LocaleController.getString("AddShortcut", R.string.AddShortcut)); + } + } else if (currentChat != null) { + // Chat menu + if (ChatObject.isChannel(currentChat)) { + // Channel menu + if (ChatObject.canManageCalls(currentChat)) { + otherItem.addSubItem(call_item, R.drawable.msg_voicechat, + LocaleController.getString("StartVoipChannel", R.string.StartVoipChannel)); + } + + if (ChatObject.hasAdminRights(currentChat)) { + otherItem.addSubItem(statistics, R.drawable.msg_stats, + LocaleController.getString("Statistics", R.string.Statistics)); + otherItem.addSubItem(edit_channel, R.drawable.msg_edit, + LocaleController.getString("EditAdminRights", R.string.EditAdminRights)); + } + + otherItem.addSubItem(search_members, R.drawable.msg_search, + LocaleController.getString("SearchMembers", R.string.SearchMembers)); + otherItem.addSubItem(leave_group, R.drawable.msg_leave, + LocaleController.getString("LeaveChannelMenu", R.string.LeaveChannelMenu)); + otherItem.addSubItem(share, R.drawable.msg_share, + LocaleController.getString("BotShare", R.string.BotShare)); + + // Note: linked_chat_id might not be available, using a simple check + // if (currentChat.linked_chat_id != 0) { + // otherItem.addSubItem(view_discussion, R.drawable.msg_discussion, + // LocaleController.getString("ViewDiscussion", R.string.ViewDiscussion)); + // } + + otherItem.addSubItem(channel_stories, R.drawable.msg_archive, + LocaleController.getString("OpenChannelArchiveStories", R.string.OpenChannelArchiveStories)); + } else { + // Group menu + if (ChatObject.canManageCalls(currentChat)) { + otherItem.addSubItem(call_item, R.drawable.msg_voicechat, + LocaleController.getString("StartVoipChat", R.string.StartVoipChat)); + } + + otherItem.addSubItem(search_members, R.drawable.msg_search, + LocaleController.getString("SearchMembers", R.string.SearchMembers)); + otherItem.addSubItem(leave_group, R.drawable.msg_leave, + LocaleController.getString("DeleteAndExit", R.string.DeleteAndExit)); + + if (ChatObject.hasAdminRights(currentChat)) { + otherItem.addSubItem(edit_channel, R.drawable.msg_edit, + LocaleController.getString("EditAdminRights", R.string.EditAdminRights)); + } + } + + otherItem.addSubItem(add_shortcut, R.drawable.msg_link, + LocaleController.getString("AddShortcut", R.string.AddShortcut)); + } + } + + // Menu handler methods + private void handleBlockContact() { + if (currentUser == null) + return; + + boolean userBlocked = getMessagesController().blockePeers.indexOfKey(currentUser.id) >= 0; + if (userBlocked) { + getMessagesController().unblockPeer(currentUser.id); + BulletinFactory.createBanBulletin(this, false).show(); + } else { + getMessagesController().blockPeer(currentUser.id); + BulletinFactory.createBanBulletin(this, true).show(); + } + createActionBarMenu(true); + } + + private void handleAddContact() { + if (currentUser == null) + return; + Bundle args = new Bundle(); + args.putLong("user_id", currentUser.id); + args.putBoolean("addContact", true); + presentFragment(new ContactAddActivity(args)); + } + + private void handleShareContact() { + if (currentUser == null) + return; + Bundle args = new Bundle(); + args.putBoolean("onlySelect", true); + args.putInt("dialogsType", DialogsActivity.DIALOGS_TYPE_FORWARD); + DialogsActivity fragment = new DialogsActivity(args); + fragment.setDelegate(new DialogsActivity.DialogsActivityDelegate() { + @Override + public boolean didSelectDialogs(DialogsActivity fragment1, ArrayList dids, + CharSequence message, boolean param, boolean notify, int scheduleDate, + TopicsFragment topicsFragment) { + // Handle sharing contact to selected dialogs + return true; + } + }); + presentFragment(fragment); + } + + private void handleEditContact() { + if (currentUser == null) + return; + Bundle args = new Bundle(); + args.putLong("user_id", currentUser.id); + presentFragment(new ContactAddActivity(args)); + } + + private void handleDeleteContact() { + if (currentUser == null) + return; + + ArrayList users = new ArrayList<>(); + users.add(currentUser); + + getContactsController().deleteContact(users, false); + createActionBarMenu(true); + } + + private void handleLeaveGroup() { + if (currentChat == null) + return; + // TODO: Implement leave group functionality + // This would typically show a confirmation dialog and then leave the chat + } + + private void handleEditChannel() { + if (currentChat == null) + return; + Bundle args = new Bundle(); + args.putLong("chat_id", currentChat.id); + // presentFragment(new ChatEditActivity(args)); // TODO: Add proper import + } + + private void handleEditInfo() { + // presentFragment(new UserInfoActivity()); // TODO: Add proper import + } + + private void handleEditProfile() { + // presentFragment(new UserInfoActivity()); // TODO: Add proper import + } + + private void handleGiftPremium() { + if (currentUser == null) + return; + // showDialog(new GiftSheet(getContext(), currentAccount, currentUser.id, null, + // null)); // TODO: Add proper import + } + + private void handleStartSecretChat() { + if (currentUser == null) + return; + // TODO: Implement secret chat creation + } + + private void handleBotPrivacy() { + if (currentUser == null) + return; + // BotWebViewAttachedSheet.openPrivacy(currentAccount, currentUser.id); // TODO: + // Add proper import + } + + private void handleStatistics() { + if (currentChat == null) + return; + // presentFragment(StatisticActivity.create(currentChat, false)); // TODO: Add + // proper import + } + + private void handleSearchMembers() { + if (currentChat == null) + return; + Bundle args = new Bundle(); + args.putLong("chat_id", currentChat.id); + // presentFragment(new ChatUsersActivity(args)); // Already imported + presentFragment(new ChatUsersActivity(args)); + } + + private void handleAddShortcut() { + // TODO: Implement add shortcut functionality + } + + private void handleVoiceVideoCall(boolean isVideo) { + if (currentUser != null && !currentUser.bot) { + VoIPHelper.startCall(currentUser, isVideo, + currentUser.id != 0, + getParentActivity(), null, getAccountInstance()); + } else if (currentChat != null) { + // Handle group calls + // VoIPHelper.showGroupCallAlert(this, currentChat, null, false, + // getAccountInstance()); // TODO: Add proper import + } + } + + private void handleEditColor() { + // presentFragment(new + // PeerColorActivity(0).startOnProfile().setOnApplied(this)); // TODO: Add + // proper import + } + + private void handleCopyProfileLink() { + if (currentUser != null && !TextUtils.isEmpty(currentUser.username)) { + String link = "https://t.me/" + currentUser.username; + AndroidUtilities.addToClipboard(link); + BulletinFactory.createCopyLinkBulletin(this).show(); + } + } + + private void handleSetUsername() { + // presentFragment(new ChangeUsernameActivity()); // TODO: Add proper import + } + + private void handleLogout() { + // presentFragment(new LogoutActivity()); // TODO: Add proper import + } + + private void handleGalleryMenuSave() { + // TODO: Implement save to gallery functionality + } + + private void handleSetAsMain() { + // TODO: Implement set as main avatar functionality + } + + private void handleEditAvatar() { + // TODO: Implement edit avatar functionality + } + + private void handleDeleteAvatar() { + // TODO: Implement delete avatar functionality + } + + private void handleAddPhoto() { + // TODO: Implement add photo functionality (camera/gallery picker) + } + + private void handleShare() { + if (currentUser != null) { + String shareText = "https://t.me/" + (currentUser.username != null ? currentUser.username : ""); + if (!TextUtils.isEmpty(shareText)) { + Bundle args = new Bundle(); + args.putBoolean("onlySelect", true); + args.putInt("dialogsType", DialogsActivity.DIALOGS_TYPE_FORWARD); + DialogsActivity fragment = new DialogsActivity(args); + presentFragment(fragment); + } + } else if (currentChat != null) { + // Handle chat sharing + String shareText = "https://t.me/" + (currentChat.username != null ? currentChat.username : ""); + if (!TextUtils.isEmpty(shareText)) { + Bundle args = new Bundle(); + args.putBoolean("onlySelect", true); + args.putInt("dialogsType", DialogsActivity.DIALOGS_TYPE_FORWARD); + DialogsActivity fragment = new DialogsActivity(args); + presentFragment(fragment); + } + } + } + + private void handleReport() { + if (currentUser != null) { + // TODO: Implement user reporting + } else if (currentChat != null) { + // TODO: Implement chat reporting + } + } + + private void handleViewDiscussion() { + if (currentChat != null) { + // Note: linked_chat_id field might not be available in current TLRPC version + // Bundle args = new Bundle(); + // args.putLong("chat_id", currentChat.linked_chat_id); + // presentFragment(new ChatActivity(args)); + } + } + + private void handleChannelStories() { + if (currentChat == null) + return; + // TODO: Implement channel stories functionality + } + + @Override + public void didReceivedNotification(int id, int account, Object... args) { + if (id == NotificationCenter.updateInterfaces) { + int mask = (Integer) args[0]; + if ((mask & MessagesController.UPDATE_MASK_STATUS) != 0) { + updateHeaderContent(); + } + } else if (id == NotificationCenter.contactsDidLoad) { + updateHeaderContent(); + createActionBarMenu(false); + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } else if (id == NotificationCenter.notificationsSettingsUpdated) { + updateMuteButton(); + } else if (id == NotificationCenter.userInfoDidLoad) { + Long uid = (Long) args[0]; + if (currentUser != null && uid == currentUser.id) { + userInfo = (TLRPC.UserFull) args[1]; + updateRowsIds(); + updateHeaderContent(); + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } + } else if (id == NotificationCenter.chatInfoDidLoad) { + Long chatId = (Long) args[0]; + if (currentChat != null && chatId == currentChat.id) { + chatInfo = (TLRPC.ChatFull) args[1]; + updateRowsIds(); + updateHeaderContent(); + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } + } + } + + // Static factory method for compatibility + public static ProfileNewActivity of(long dialogId) { + Bundle args = new Bundle(); + args.putLong("dialog_id", dialogId); + return new ProfileNewActivity(args); + } + + // Method for compatibility with ProfileActivity animations + public void setPlayProfileAnimation(int animationType) { + // Handle profile animation states if needed + } + + // Method for compatibility with ProfileActivity photo expansion + public void setExpandPhoto(boolean expand) { + // Handle photo expansion if needed + } + + // Additional compatibility methods + public void setUserInfo(Object userInfo, Object channelMessageFetcher, Object birthdayAssetsFetcher) { + // Handle user info setting for compatibility + } + + public void setChatInfo(Object chatInfo) { + // Handle chat info setting for compatibility + } + + public boolean myProfile = false; // Compatibility field + public boolean saved = false; // Compatibility field + + // Additional compatibility methods + public long getDialogId() { + return dialogId; + } + + public long getTopicId() { + return 0; // Default topic ID + } + + public UndoView getUndoView() { + return null; // Placeholder for undo view + } + + public void prepareBlurBitmap() { + // Placeholder for blur bitmap preparation + } + + public boolean isSettings() { + return false; // Not a settings profile + } + + public boolean isChat() { + return chatId != 0; // True if this is a chat profile + } + + public static void sendLogs(Object activity, boolean debug) { + // Placeholder for log sending functionality + } + + // Additional compatibility methods for BackButtonMenu and other components + public TLRPC.Chat getCurrentChat() { + return currentChat; + } + + public UserInfoWrapper getUserInfo() { + if (currentUser == null) + return null; + return new UserInfoWrapper(currentUser); + } + + // Simple wrapper class to match BackButtonMenu expectations + public static class UserInfoWrapper { + public TLRPC.User user; + + public UserInfoWrapper(TLRPC.User user) { + this.user = user; + } + } + + // Essential helper methods copied from ProfileActivity + private void checkListViewScroll() { + // Get the RecyclerView from the content placeholder container + RecyclerView listView = null; + if (coordinatorLayout != null && coordinatorLayout.getChildCount() > 1) { + FrameLayout contentContainer = (FrameLayout) coordinatorLayout.findViewById(R.id.content_placeholder); + if (contentContainer != null && contentContainer.getChildCount() > 0) { + View child = contentContainer.getChildAt(0); + if (child instanceof RecyclerView) { + listView = (RecyclerView) child; + } + } + } + + if (listView == null || listView.getVisibility() != View.VISIBLE) { + return; + } + if (sharedMediaLayout != null && sharedMediaLayoutAttached) { + sharedMediaLayout.setVisibleHeight(listView.getMeasuredHeight() - sharedMediaLayout.getTop()); + } + + if (listView.getChildCount() <= 0) { + return; + } + + // Check scroll position and update header visibility + RecyclerView.ViewHolder firstHolder = listView.findViewHolderForAdapterPosition(0); + if (firstHolder != null) { + int top = firstHolder.itemView.getTop(); + boolean mediaHeaderVisible = sharedMediaRow != -1 && top <= 0; + setMediaHeaderVisible(mediaHeaderVisible); + } + } + + private void setMediaHeaderVisible(boolean visible) { + if (mediaHeaderVisible == visible) { + return; + } + mediaHeaderVisible = visible; + + // Update toolbar menu items visibility based on media header state + if (otherItem != null) { + otherItem.setVisibility(visible ? View.GONE : View.VISIBLE); + } + } + + private void updateAvatarRoundRadius() { + if (avatarImageView != null) { + // Update avatar corner radius - for Material Design, we keep it circular + avatarImageView.setRoundRadius(AndroidUtilities.dp(21)); + } + } + + private int getSmallAvatarRoundRadius() { + // For Material Design collapsing header, we maintain circular avatars + return AndroidUtilities.dp(21); + } + + private void updateNotificationSettings() { + if (currentUser != null) { + isMuted = getMessagesController().isDialogMuted(currentUser.id, 0); + } else if (currentChat != null) { + isMuted = getMessagesController().isDialogMuted(-currentChat.id, 0); + } + updateMuteButton(); + } + + private String formatUserStatus(TLRPC.User user) { + if (user == null) + return ""; + + if (user.bot) { + return LocaleController.getString("Bot", R.string.Bot); + } else if (UserObject.isUserSelf(user)) { + return LocaleController.getString("Online", R.string.Online); + } else { + return LocaleController.formatUserStatus(currentAccount, user); + } + } + + private String formatChatStatus(TLRPC.Chat chat) { + if (chat == null) + return ""; + + if (ChatObject.isChannel(chat)) { + int count = chat.participants_count; + if (chatInfo != null) { + count = chatInfo.participants_count; + } + return LocaleController.formatPluralString("Subscribers", count); + } else { + int count = chat.participants_count; + if (chatInfo != null) { + count = chatInfo.participants_count; + } + return LocaleController.formatPluralString("Members", count); + } + } + + private void updateStoriesViewBounds(boolean animated) { + // For Material Design header, stories integration would be in the header area + updateStoriesDisplay(); + } + + private boolean isUserOnline(TLRPC.User user) { + if (user == null) + return false; + if (user.bot) + return false; + if (UserObject.isUserSelf(user)) + return true; + + return user.status != null + && user.status.expires > ConnectionsManager.getInstance(currentAccount).getCurrentTime(); + } + + private void updateProfileData(boolean reload) { + if (reload) { + // Reload user/chat info + if (currentUser != null) { + getMessagesController().loadFullUser(currentUser, classGuid, true); + } else if (currentChat != null) { + getMessagesController().loadFullChat(currentChat.id, classGuid, true); + } + } + + updateHeaderContent(); + updateRowsIds(); + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } + + private void updateFloatingButtonColor() { + // Update header button colors to match Material Design theme + // BubbleButton colors are handled internally + } + + private boolean mediaHeaderVisible = false; + private boolean sharedMediaLayoutAttached = false; + + // PhotoViewer provider for avatar viewing - copied from ProfileActivity + private PhotoViewer.PhotoViewerProvider provider = new PhotoViewer.EmptyPhotoViewerProvider() { + + @Override + public PhotoViewer.PlaceProviderObject getPlaceForPhoto(MessageObject messageObject, + TLRPC.FileLocation fileLocation, int index, boolean needPreview, boolean closing) { + if (fileLocation == null) { + return null; + } + + TLRPC.FileLocation photoBig = null; + if (userId != 0) { + TLRPC.User user = getMessagesController().getUser(userId); + if (user != null && user.photo != null && user.photo.photo_big != null) { + photoBig = user.photo.photo_big; + } + } else if (chatId != 0) { + TLRPC.Chat chat = getMessagesController().getChat(chatId); + if (chat != null && chat.photo != null && chat.photo.photo_big != null) { + photoBig = chat.photo.photo_big; + } + } + + if (photoBig != null && photoBig.local_id == fileLocation.local_id + && photoBig.volume_id == fileLocation.volume_id && photoBig.dc_id == fileLocation.dc_id) { + int[] coords = new int[2]; + avatarImageView.getLocationInWindow(coords); + PhotoViewer.PlaceProviderObject object = new PhotoViewer.PlaceProviderObject(); + object.viewX = coords[0]; + object.viewY = coords[1] - (Build.VERSION.SDK_INT >= 21 ? 0 : AndroidUtilities.statusBarHeight); + object.parentView = avatarImageView; + object.imageReceiver = avatarImageView.getImageReceiver(); + if (userId != 0) { + object.dialogId = userId; + } else if (chatId != 0) { + object.dialogId = -chatId; + } + object.thumb = object.imageReceiver.getBitmapSafe(); + object.size = -1; + object.radius = avatarImageView.getImageReceiver().getRoundRadius(true); + object.scale = avatarImageView.getScaleX(); + object.canEdit = userId == getUserConfig().clientUserId; + return object; + } + return null; + } + + @Override + public void willHidePhotoViewer() { + avatarImageView.getImageReceiver().setVisible(true, true); + } + }; + + // Birthday formatting helper + private String formatBirthday(TL_account.TL_birthday birthday) { + if (birthday == null) + return ""; + + try { + Calendar calendar = Calendar.getInstance(); + calendar.set( + birthday.year != 0 ? birthday.year : calendar.get(Calendar.YEAR), + birthday.month - 1, // Calendar months are 0-based + birthday.day); + + java.text.DateFormat dateFormat = java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM); + return dateFormat.format(calendar.getTime()); + } catch (Exception e) { + return birthday.day + "/" + birthday.month + (birthday.year != 0 ? "/" + birthday.year : ""); + } + } + + // Location formatting helper + private String formatLocation(TLRPC.TL_channelLocation location) { + if (location == null || TextUtils.isEmpty(location.address)) + return ""; + return location.address; + } + + // Avatar handling methods - copied from ProfileActivity + private void openAvatar() { + if (userId != 0) { + TLRPC.User user = getMessagesController().getUser(userId); + if (user != null && user.photo != null && user.photo.photo_big != null) { + PhotoViewer.getInstance().setParentActivity(ProfileNewActivity.this); + if (user.photo.dc_id != 0) { + user.photo.photo_big.dc_id = user.photo.dc_id; + } + PhotoViewer.getInstance().openPhoto(user.photo.photo_big, provider); + } + } else if (chatId != 0) { + TLRPC.Chat chat = getMessagesController().getChat(chatId); + if (chat != null && chat.photo != null && chat.photo.photo_big != null) { + PhotoViewer.getInstance().setParentActivity(ProfileNewActivity.this); + if (chat.photo.dc_id != 0) { + chat.photo.photo_big.dc_id = chat.photo.dc_id; + } + ImageLocation videoLocation; + if (chatInfo != null && (chatInfo.chat_photo instanceof TLRPC.TL_photo) + && !chatInfo.chat_photo.video_sizes.isEmpty()) { + videoLocation = ImageLocation.getForPhoto(chatInfo.chat_photo.video_sizes.get(0), + chatInfo.chat_photo); + } else { + videoLocation = null; + } + PhotoViewer.getInstance().openPhotoWithVideo(chat.photo.photo_big, videoLocation, provider); + } + } + } + + // Story integration - copied from ProfileActivity + private boolean needInsetForStories() { + return getMessagesController().getStoriesController().hasStories(getDialogId()); + } + + private void updateStoriesDisplay() { + // Update avatar to show story ring when stories are available + // In Material Design header, story rings would be handled differently + boolean hasStories = needInsetForStories(); + // TODO: Implement story ring display for Material Design avatar + } + + // Premium status handling + private void updatePremiumStatus() { + if (currentUser != null && userInfo != null) { + // Handle premium badge display in name text view + // Premium indicators would be shown alongside the name + } + } + + // ShowDrawable class for compatibility + public static class ShowDrawable extends Drawable { + private String text; + private Paint textPaint; + private Paint backgroundPaint; + + public ShowDrawable(String text) { + this.text = text; + textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setTextSize(dp(14)); + textPaint.setColor(Color.WHITE); + textPaint.setTypeface(AndroidUtilities.bold()); + + backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + backgroundPaint.setColor(0x1e000000); + } + + @Override + public void draw(@NonNull Canvas canvas) { + // Draw background + canvas.drawRect(getBounds(), backgroundPaint); + // Draw text + canvas.drawText(text, getBounds().centerX(), getBounds().centerY(), textPaint); + } + + @Override + public void setAlpha(int alpha) { + textPaint.setAlpha(alpha); + } + + @Override + public void setColorFilter(android.graphics.ColorFilter colorFilter) { + textPaint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return android.graphics.PixelFormat.TRANSLUCENT; + } + + // Additional methods for compatibility + public void setTextColor(int color) { + textPaint.setColor(color); + } + + public void setBackgroundColor(int color) { + backgroundPaint.setColor(color); + } + } + + // Bubble Button Component - reusable component for header action buttons + private static class BubbleButton extends FrameLayout { + private ImageView iconView; + private TextView textView; + private GradientDrawable background; + + public BubbleButton(Context context, int iconRes, String text) { + super(context); + init(context, iconRes, text); + } + + private void init(Context context, int iconRes, String text) { + // Create transparent background for bubble effect + background = new GradientDrawable(); + background.setShape(GradientDrawable.RECTANGLE); + background.setCornerRadius(dp(16)); + background.setColor(Color.argb(51, 255, 255, 255)); // 20% white transparency + setBackground(background); + + // Create container for centered icon and text + LinearLayout container = new LinearLayout(context); + container.setOrientation(LinearLayout.VERTICAL); + container.setGravity(Gravity.CENTER); + addView(container, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); + + // Create icon using provided vector drawable + iconView = new ImageView(context); + iconView.setImageResource(iconRes); + iconView.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)); + container.addView(iconView, LayoutHelper.createLinear(24, 24, Gravity.CENTER_HORIZONTAL, 0, 4, 0, 2)); + + // Create text label + textView = new TextView(context); + textView.setText(text); + textView.setTextColor(Color.WHITE); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11); + textView.setGravity(Gravity.CENTER); + textView.setSingleLine(true); + textView.setEllipsize(TextUtils.TruncateAt.END); + container.addView(textView, LayoutHelper.createLinear(LayoutHelper.WRAP_CONTENT, LayoutHelper.WRAP_CONTENT, + Gravity.CENTER_HORIZONTAL, 0, 0, 0, 4)); + + // Add press animation and touch feedback + setOnTouchListener((v, event) -> { + switch (event.getAction()) { + case android.view.MotionEvent.ACTION_DOWN: + animate().scaleX(0.95f).scaleY(0.95f).setDuration(100).start(); + background.setColor(Color.argb(77, 255, 255, 255)); // 30% white on press + break; + case android.view.MotionEvent.ACTION_UP: + case android.view.MotionEvent.ACTION_CANCEL: + animate().scaleX(1f).scaleY(1f).setDuration(100).start(); + background.setColor(Color.argb(51, 255, 255, 255)); // 20% white normal + break; + } + return false; + }); + } + + public void updateIcon(int iconRes) { + iconView.setImageResource(iconRes); + } + + public void updateText(String text) { + textView.setText(text); + } + } + + // SharedMediaLayout.Delegate implementation + @Override + public void scrollToSharedMedia() { + // Handle scroll to shared media + } + + @Override + public boolean onMemberClick(TLRPC.ChatParticipant participant, boolean b, boolean resultOnly, View view) { + // Handle member click + return false; + } + + @Override + public boolean isFragmentOpened() { + return !isPaused; + } + + @Override + public RecyclerListView getListView() { + // Return the RecyclerListView from the adapter + return null; // TODO: Store reference to listView + } + + @Override + public boolean canSearchMembers() { + return canSearchMembers; + } + + @Override + public void updateSelectedMediaTabText() { + // Handle media tab text updates + } + + // SharedMediaPreloaderDelegate implementation + @Override + public void mediaCountUpdated() { + if (sharedMediaLayout != null && sharedMediaPreloader != null) { + sharedMediaLayout.setNewMediaCounts(sharedMediaPreloader.getLastMediaCount()); + } + updateRowsIds(); + if (listAdapter != null) { + listAdapter.notifyDataSetChanged(); + } + } + + /** + * Configure transparent status bar for collapsing header animation + * Comprehensive solution supporting minSDK 19+ with proper API level handling + */ + private void configureTransparentStatusBar() { + if (getParentActivity() == null) + return; + + android.view.Window window = getParentActivity().getWindow(); + + // Apply API 19+ layout flags for full screen content + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + + // Handle API 19-20 (KitKat) - Use translucent status bar + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && + Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + + setWindowFlag(window, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, true); + + // Adjust layout for translucent status bar on KitKat + if (coordinatorLayout != null) { + coordinatorLayout.setFitsSystemWindows(true); + } + } + + // Handle API 21+ (Lollipop and above) - Use fully transparent status bar + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Remove translucent flag if it was set + setWindowFlag(window, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, false); + + // Set transparent status bar + window.setStatusBarColor(Color.TRANSPARENT); + + // Add system bar background flag + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + + // Ensure layout extends behind status bar + if (coordinatorLayout != null) { + coordinatorLayout.setFitsSystemWindows(false); + } + } + + // Handle API 30+ (Android 11+) - Use modern edge-to-edge approach + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setStatusBarColor(Color.TRANSPARENT); + window.setDecorFitsSystemWindows(false); + } + } + + /** + * Helper method to set window flags safely across API levels + */ + private void setWindowFlag(android.view.Window window, final int bits, boolean on) { + WindowManager.LayoutParams winParams = window.getAttributes(); + if (on) { + winParams.flags |= bits; + } else { + winParams.flags &= ~bits; + } + window.setAttributes(winParams); + } + + /** + * Set status bar color dynamically during collapse animation + * Adapts to the current collapse state for smooth transitions + */ + private void setStatusBarColorForCollapse(float collapseProgress) { + if (getParentActivity() == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return; + } + + android.view.Window window = getParentActivity().getWindow(); + + // Interpolate between transparent (expanded) and theme color (collapsed) + int themeColor = Theme.getColor(Theme.key_actionBarDefault); + int alpha = (int) (collapseProgress * 255); + int statusBarColor = Color.argb(alpha, Color.red(themeColor), Color.green(themeColor), Color.blue(themeColor)); + + window.setStatusBarColor(statusBarColor); + } + + /** + * Setup container clipping to allow smooth text translation beyond bounds + * This is critical for preventing username text from being clipped during + * animation + */ + private void setupContainerClippingForTranslation() { + // Disable clipping on all containers in the hierarchy to allow text translation + + // Primary text containers (already set in XML but ensuring programmatically) + if (nameContainer instanceof ViewGroup) { + ((ViewGroup) nameContainer).setClipChildren(false); + ((ViewGroup) nameContainer).setClipToPadding(false); + } + + if (onlineContainer instanceof ViewGroup) { + ((ViewGroup) onlineContainer).setClipChildren(false); + ((ViewGroup) onlineContainer).setClipToPadding(false); + } + + // Parent containers - critical for allowing text to move beyond normal bounds + if (appBarLayout instanceof ViewGroup) { + ((ViewGroup) appBarLayout).setClipChildren(false); + ((ViewGroup) appBarLayout).setClipToPadding(false); + } + + if (collapsingToolbarLayout instanceof ViewGroup) { + ((ViewGroup) collapsingToolbarLayout).setClipChildren(false); + ((ViewGroup) collapsingToolbarLayout).setClipToPadding(false); + } + + if (coordinatorLayout instanceof ViewGroup) { + ((ViewGroup) coordinatorLayout).setClipChildren(false); + ((ViewGroup) coordinatorLayout).setClipToPadding(false); + } + + // Header content container + View expandedHeaderContainer = coordinatorLayout.findViewById(R.id.expanded_header_container); + if (expandedHeaderContainer instanceof ViewGroup) { + ((ViewGroup) expandedHeaderContainer).setClipChildren(false); + ((ViewGroup) expandedHeaderContainer).setClipToPadding(false); + } + + // Root coordinator layout - ensure no clipping at top level + if (coordinatorLayout.getParent() instanceof ViewGroup) { + ((ViewGroup) coordinatorLayout.getParent()).setClipChildren(false); + ((ViewGroup) coordinatorLayout.getParent()).setClipToPadding(false); + } + } + + /** + * Configure FrameLayouts specifically to prevent text clipping during + * translation + * This addresses the specific issue where TextViews are clipped by their + * FrameLayout containers + */ + private void setupFrameLayoutsForTranslation() { + // Configure name container FrameLayout + if (nameContainer instanceof FrameLayout) { + FrameLayout nameFrame = (FrameLayout) nameContainer; + // Ensure FrameLayout doesn't constrain child TextView during translation + nameFrame.setClipChildren(false); + nameFrame.setClipToPadding(false); + nameFrame.setClipBounds(null); // Remove any clip bounds + + // Ensure the FrameLayout allows overflow + nameFrame.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + } + + // Configure online container FrameLayout (apply same settings for consistency) + if (onlineContainer instanceof FrameLayout) { + FrameLayout onlineFrame = (FrameLayout) onlineContainer; + // Apply identical settings to ensure both text views behave the same + onlineFrame.setClipChildren(false); + onlineFrame.setClipToPadding(false); + onlineFrame.setClipBounds(null); // Remove any clip bounds + + // Ensure the FrameLayout allows overflow + onlineFrame.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + } + + // Additional TextView configuration to prevent clipping + if (nameTextView != null) { + // Ensure TextView itself doesn't have constraints + nameTextView.setIncludeFontPadding(false); + nameTextView.setSingleLine(false); // Allow text to flow freely + nameTextView.setEllipsize(null); // Never ellipsize + nameTextView.setHorizontallyScrolling(true); // Allow horizontal overflow + } + + if (onlineTextView != null) { + // Apply identical settings to online text view + onlineTextView.setIncludeFontPadding(false); + onlineTextView.setSingleLine(false); // Allow text to flow freely + onlineTextView.setEllipsize(null); // Never ellipsize + onlineTextView.setHorizontallyScrolling(true); // Allow horizontal overflow + } + } +} \ No newline at end of file diff --git a/TMessagesProj/src/main/res/drawable/profile_header_call.xml b/TMessagesProj/src/main/res/drawable/profile_header_call.xml new file mode 100644 index 00000000000..11a71df6315 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/profile_header_call.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/TMessagesProj/src/main/res/drawable/profile_header_message.xml b/TMessagesProj/src/main/res/drawable/profile_header_message.xml new file mode 100644 index 00000000000..6b4a6e8d1dd --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/profile_header_message.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/TMessagesProj/src/main/res/drawable/profile_header_mute.xml b/TMessagesProj/src/main/res/drawable/profile_header_mute.xml new file mode 100644 index 00000000000..9ee82f853b2 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/profile_header_mute.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/TMessagesProj/src/main/res/drawable/profile_header_unmute.xml b/TMessagesProj/src/main/res/drawable/profile_header_unmute.xml new file mode 100644 index 00000000000..a8a90d8bce4 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/profile_header_unmute.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/TMessagesProj/src/main/res/drawable/profile_header_video.xml b/TMessagesProj/src/main/res/drawable/profile_header_video.xml new file mode 100644 index 00000000000..235b505ac8d --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/profile_header_video.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/TMessagesProj/src/main/res/drawable/toolbar_bg.xml b/TMessagesProj/src/main/res/drawable/toolbar_bg.xml new file mode 100644 index 00000000000..712176ab669 --- /dev/null +++ b/TMessagesProj/src/main/res/drawable/toolbar_bg.xml @@ -0,0 +1,8 @@ + + + + diff --git a/TMessagesProj/src/main/res/layout/profile_collapsing_layout.xml b/TMessagesProj/src/main/res/layout/profile_collapsing_layout.xml new file mode 100644 index 00000000000..9ff13025a6f --- /dev/null +++ b/TMessagesProj/src/main/res/layout/profile_collapsing_layout.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TMessagesProj/src/main/res/values/styles.xml b/TMessagesProj/src/main/res/values/styles.xml index aa1eb8b1909..57505231453 100644 --- a/TMessagesProj/src/main/res/values/styles.xml +++ b/TMessagesProj/src/main/res/values/styles.xml @@ -246,4 +246,13 @@ @color/widget_badge + + + diff --git a/TMessagesProj/src/main/res/values/values.xml b/TMessagesProj/src/main/res/values/values.xml index 640e5810162..56efb589b28 100644 --- a/TMessagesProj/src/main/res/values/values.xml +++ b/TMessagesProj/src/main/res/values/values.xml @@ -3,4 +3,5 @@ false 2dip 16dp + 76dp \ No newline at end of file