From 3c258b75805c1bab8d3bbdba2b2fa017ed62475f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 07:02:50 +0000 Subject: [PATCH 1/7] feat(chat): force gallery refresh on attach open and add quick-send popup - force MediaStore reload when opening the attach menu so newly taken photos and screenshots appear without waiting for the observer's 2s debounce - add a bottom-right floating popup in ChatActivity that surfaces the latest image (added while the chat was away) and quick-sends it on tap; detection runs only on onResume / first entry - new setting under Chat settings: "Quick send popup for new photos" --- .../java/org/telegram/ui/ChatActivity.java | 88 +++++ .../ChatAttachAlertPhotoLayout.java | 14 +- .../nnngram/activity/ChatSettingActivity.java | 12 + .../nnngram/ui/QuickSendMediaPopup.java | 325 ++++++++++++++++++ .../xyz/nextalone/nnngram/utils/Defines.kt | 1 + .../res/values-zh-rTW/strings_nullgram.xml | 4 + .../main/res/values-zh/strings_nullgram.xml | 4 + .../src/main/res/values/strings_nullgram.xml | 4 + 8 files changed, 443 insertions(+), 9 deletions(-) create mode 100644 TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 4c85d6116..6c5f909cb 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -360,6 +360,7 @@ import xyz.nextalone.nnngram.helpers.TranslateHelper.Status; import xyz.nextalone.nnngram.translate.LanguageDetectorTimeout; import xyz.nextalone.nnngram.ui.BackButtonRecentMenu; +import xyz.nextalone.nnngram.ui.QuickSendMediaPopup; import xyz.nextalone.nnngram.ui.TranslatorSettingsPopupWrapper; import xyz.nextalone.nnngram.ui.sortList.items.TextStyleItems; import xyz.nextalone.nnngram.utils.Defines; @@ -507,6 +508,9 @@ public class ChatActivity extends BaseFragment implements private ChatNotificationsPopupWrapper chatNotificationsPopupWrapper; // private ChatActivitySideControlsButtonsLayout topButtonsLayout; private ChatActivitySideControlsButtonsLayout sideControlsButtonsLayout; + private QuickSendMediaPopup quickSendMediaPopup; + private long quickSendPauseTimeSec; + private final java.util.HashSet quickSendDismissedIds = new java.util.HashSet<>(); private ActionBarMenuItem.Item hideTitleItem; /* private float pagedownButtonEnterProgress; private float searchUpDownEnterProgress; @@ -7353,6 +7357,9 @@ public void setAlpha(float alpha) { sideControlsButtonsLayout.setOnLongClickListener(this::onSideControlButtonOnLongClick); contentView.addView(sideControlsButtonsLayout, LayoutHelper.createFrame(57, 300, Gravity.RIGHT | Gravity.BOTTOM)); + quickSendMediaPopup = new QuickSendMediaPopup(context); + contentView.addView(quickSendMediaPopup, LayoutHelper.createFrame(72, 72, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 6, 110)); + contentView.addView(topPanelLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP)); updateMessageListAccessibilityVisibility(); @@ -11109,6 +11116,9 @@ private void updatePagedownButtonsPosition() { - getTopicTabsSideSize(TopicsTabsView.Position.BOTTOM) - dp(ChatInputViewsContainer.INPUT_BUBBLE_BOTTOM + 4); sideControlsButtonsLayout.setTranslationY(baseTranslationY2); + if (quickSendMediaPopup != null) { + quickSendMediaPopup.setTranslationY(baseTranslationY2); + } } if (suggestEmojiPanel != null) { @@ -29757,6 +29767,8 @@ public void onShow(Bulletin bulletin) { if (starReactionsOverlay != null) { starReactionsOverlay.bringToFront(); } + + checkQuickSendMediaPopup(); } public float getPullingDownOffset() { @@ -29772,6 +29784,78 @@ public void checkAdjustResize() { } } + private void checkQuickSendMediaPopup() { + if (!Config.quickSendMediaPopup || quickSendMediaPopup == null) { + return; + } + if (chatMode != 0 || inPreviewMode || inBubbleMode || isReport() || isInsideContainer) { + return; + } + if (getParentActivity() == null || currentEncryptedChat != null) { + // Skip secret chats to avoid surprising users with a separate send path. + return; + } + if (currentChat != null && !ChatObject.canSendPhoto(currentChat)) { + return; + } + if (!xyz.nextalone.nnngram.utils.PermissionUtils.isImagesPermissionGranted()) { + return; + } + final long sinceSec; + if (quickSendPauseTimeSec > 0) { + // Only show photos added while we were away from this chat. + sinceSec = quickSendPauseTimeSec; + } else { + // First time entering: look back 3 minutes for a freshly taken shot. + sinceSec = System.currentTimeMillis() / 1000L - 180L; + } + QuickSendMediaPopup.queryLatestImage(sinceSec, entry -> { + if (entry == null || quickSendMediaPopup == null || paused) { + return; + } + if (quickSendDismissedIds.contains(entry.id)) { + return; + } + if (quickSendMediaPopup.isShowingFor(entry.id)) { + return; + } + quickSendMediaPopup.show(entry, e -> { + quickSendDismissedIds.add(e.id); + sendQuickMediaEntry(e); + }); + }); + } + + private void sendQuickMediaEntry(QuickSendMediaPopup.QuickSendMediaEntry e) { + if (e == null || (e.path == null && e.uri == null)) { + return; + } + if (!checkSlowModeAlert()) { + return; + } + SendMessagesHelper.prepareSendingPhoto( + getAccountInstance(), + e.path, + e.uri, + dialog_id, + replyingMessageObject, + getThreadMessage(), + replyingQuote, + null, + null, + null, + null, + 0, + null, + true, + 0, + chatMode, + quickReplyShortcut, + getQuickReplyId() + ); + afterMessageSend(); + } + @Override public void finishFragment() { super.finishFragment(); @@ -29840,6 +29924,10 @@ public void onPause() { if (contentView != null) { contentView.onPause(); } + if (quickSendMediaPopup != null) { + quickSendMediaPopup.dismiss(false); + } + quickSendPauseTimeSec = System.currentTimeMillis() / 1000L; if (chatMode == 0 || chatMode == MODE_SAVED && getUserConfig().getClientUserId() == getSavedDialogId() || chatMode == MODE_SUGGESTIONS && ChatObject.isMonoForum(currentChat)) { saveDraft(); getMessagesController().cancelTyping(0, dialog_id, threadMessageId); diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java index c2d0b3a7f..d638e436d 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java @@ -2560,15 +2560,11 @@ public void onAnimationEnd(Animator animator) { } public void loadGalleryPhotos() { - MediaController.AlbumEntry albumEntry; - if (shouldLoadAllMedia()) { - albumEntry = MediaController.allMediaAlbumEntry; - } else { - albumEntry = MediaController.allPhotosAlbumEntry; - } - if (albumEntry == null) { - MediaController.loadGalleryPhotosAlbums(0); - } + // Always request a MediaStore refresh so newly added photos/screenshots + // appear without waiting for the ContentObserver's 2s debounce. The load + // runs on a background thread and updates the UI via NotificationCenter + // (albumsDidLoad) while the cached album (if any) keeps rendering. + MediaController.loadGalleryPhotosAlbums(0); } private boolean shouldLoadAllMedia() { diff --git a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/activity/ChatSettingActivity.java b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/activity/ChatSettingActivity.java index 975ef2753..4e3305c3c 100644 --- a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/activity/ChatSettingActivity.java +++ b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/activity/ChatSettingActivity.java @@ -123,6 +123,7 @@ public class ChatSettingActivity extends BaseActivity { private int hideTimeForStickerRow; private int showMessageIDRow; private int hideQuickSendMediaBottomRow; + private int quickSendMediaPopupRow; private int customQuickMessageRow; private int scrollableChatPreviewRow; private int showTabsOnForwardRow; @@ -343,6 +344,11 @@ protected void onItemClick(View view, int position, float x, float y) { if (view instanceof TextCheckCell) { ((TextCheckCell) view).setChecked(Config.hideQuickSendMediaBottom); } + } else if (position == quickSendMediaPopupRow) { + Config.toggleQuickSendMediaPopup(); + if (view instanceof TextCheckCell) { + ((TextCheckCell) view).setChecked(Config.quickSendMediaPopup); + } } else if (position == customQuickMessageRow) { setCustomQuickMessage(); listAdapter.notifyItemChanged(position, PARTIAL); @@ -582,6 +588,7 @@ protected void updateRows() { quickToggleAnonymous = addRow("quickToggleAnonymous"); hideSendAsButtonRow = addRow("hideSendAsButton"); hideQuickSendMediaBottomRow = addRow("hideQuickSendMediaBottom"); + quickSendMediaPopupRow = addRow("quickSendMediaPopup"); customQuickMessageRow = addRow("customQuickMessage"); scrollableChatPreviewRow = addRow("scrollableChatPreview"); showTabsOnForwardRow = addRow("showTabsOnForward"); @@ -747,6 +754,11 @@ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, boole } else if (position == hideQuickSendMediaBottomRow) { textCell.setTextAndCheck(LocaleController.getString("DisableQuickSendMediaBottom", R.string.DisableQuickSendMediaBottom), Config.hideQuickSendMediaBottom, true); + } else if (position == quickSendMediaPopupRow) { + textCell.setTextAndValueAndCheck( + LocaleController.getString("quickSendMediaPopup", R.string.quickSendMediaPopup), + LocaleController.getString("quickSendMediaPopupInfo", R.string.quickSendMediaPopupInfo), + Config.quickSendMediaPopup, true, true); } else if (position == scrollableChatPreviewRow) { textCell.setTextAndCheck(LocaleController.getString("scrollableChatPreview", R.string.scrollableChatPreview), Config.scrollableChatPreview, true); } else if (position == showTabsOnForwardRow) { diff --git a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java new file mode 100644 index 000000000..8df992ff9 --- /dev/null +++ b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2019-2026 qwq233 + * https://github.com/qwq233/Nullgram + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this software. + * If not, see + * + */ +package xyz.nextalone.nnngram.ui; + +import static org.telegram.messenger.AndroidUtilities.dp; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Outline; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.ColorDrawable; +import android.media.ExifInterface; +import android.net.Uri; +import android.provider.MediaStore; +import android.view.Gravity; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.view.animation.OvershootInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import org.telegram.messenger.AndroidUtilities; +import org.telegram.messenger.ApplicationLoader; +import org.telegram.messenger.FileLog; +import org.telegram.messenger.LocaleController; +import org.telegram.messenger.R; +import org.telegram.messenger.Utilities; +import org.telegram.ui.ActionBar.Theme; +import org.telegram.ui.Components.LayoutHelper; + +public class QuickSendMediaPopup extends FrameLayout { + + public interface Delegate { + void onSend(QuickSendMediaEntry entry); + } + + public static class QuickSendMediaEntry { + public long id; + public String path; + public Uri uri; + public long dateAdded; + public boolean isVideo; + public int orientation; + } + + private static final long AUTO_DISMISS_MS = 10_000L; + + private final ImageView thumbView; + private final ImageView closeButton; + private final ImageView sendIcon; + private QuickSendMediaEntry entry; + private Delegate delegate; + private boolean dismissed; + private boolean sending; + + private final Runnable autoDismissRunnable = () -> dismiss(true); + + public QuickSendMediaPopup(Context context) { + super(context); + + setWillNotDraw(false); + setClipChildren(false); + + FrameLayout card = new FrameLayout(context); + card.setBackground(Theme.createRoundRectDrawable(dp(14), Theme.getColor(Theme.key_dialogBackground))); + card.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), dp(14)); + } + }); + card.setElevation(dp(6)); + addView(card, LayoutHelper.createFrame(60, 60, Gravity.CENTER, 8, 8, 8, 8)); + + thumbView = new ImageView(context); + thumbView.setScaleType(ImageView.ScaleType.CENTER_CROP); + thumbView.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), dp(10)); + } + }); + thumbView.setClipToOutline(true); + thumbView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundGray)); + card.addView(thumbView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT, Gravity.CENTER, 4, 4, 4, 4)); + + sendIcon = new ImageView(context); + sendIcon.setImageResource(R.drawable.attach_send); + sendIcon.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)); + sendIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + sendIcon.setBackground(Theme.createCircleDrawable(dp(22), Theme.getColor(Theme.key_chats_actionBackground))); + sendIcon.setContentDescription(LocaleController.getString("Send", R.string.Send)); + card.addView(sendIcon, LayoutHelper.createFrame(22, 22, Gravity.BOTTOM | Gravity.RIGHT, 0, 0, -4, -4)); + + closeButton = new ImageView(context); + closeButton.setImageResource(R.drawable.msg_close); + closeButton.setColorFilter(new PorterDuffColorFilter(Theme.getColor(Theme.key_dialogTextBlack), PorterDuff.Mode.SRC_IN)); + closeButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE); + closeButton.setBackground(Theme.createCircleDrawable(dp(20), Theme.getColor(Theme.key_dialogBackground))); + closeButton.setElevation(dp(4)); + closeButton.setContentDescription(LocaleController.getString("Close", R.string.Close)); + addView(closeButton, LayoutHelper.createFrame(20, 20, Gravity.TOP | Gravity.RIGHT, 0, 0, 0, 0)); + + card.setOnClickListener(v -> { + if (sending || entry == null || delegate == null) { + return; + } + sending = true; + AndroidUtilities.cancelRunOnUIThread(autoDismissRunnable); + delegate.onSend(entry); + dismiss(true); + }); + closeButton.setOnClickListener(v -> dismiss(true)); + + setAlpha(0f); + setScaleX(0.6f); + setScaleY(0.6f); + setVisibility(GONE); + } + + public void show(QuickSendMediaEntry entry, Delegate delegate) { + this.entry = entry; + this.delegate = delegate; + dismissed = false; + sending = false; + setVisibility(VISIBLE); + loadThumb(entry); + animate().cancel(); + setAlpha(0f); + setScaleX(0.6f); + setScaleY(0.6f); + animate() + .alpha(1f) + .scaleX(1f) + .scaleY(1f) + .setDuration(260) + .setInterpolator(new OvershootInterpolator(1.6f)) + .start(); + + AndroidUtilities.cancelRunOnUIThread(autoDismissRunnable); + AndroidUtilities.runOnUIThread(autoDismissRunnable, AUTO_DISMISS_MS); + } + + public void dismiss(boolean animated) { + if (dismissed) { + return; + } + dismissed = true; + AndroidUtilities.cancelRunOnUIThread(autoDismissRunnable); + animate().cancel(); + if (animated) { + animate() + .alpha(0f) + .scaleX(0.6f) + .scaleY(0.6f) + .setDuration(180) + .withEndAction(() -> setVisibility(GONE)) + .start(); + } else { + setAlpha(0f); + setVisibility(GONE); + } + } + + public QuickSendMediaEntry getEntry() { + return entry; + } + + public boolean isShowingFor(long id) { + return !dismissed && getVisibility() == VISIBLE && entry != null && entry.id == id; + } + + private void loadThumb(QuickSendMediaEntry e) { + thumbView.setImageDrawable(new ColorDrawable(Theme.getColor(Theme.key_windowBackgroundGray))); + Utilities.searchQueue.postRunnable(() -> { + Bitmap bitmap = decodeThumb(e); + AndroidUtilities.runOnUIThread(() -> { + if (dismissed || entry == null || entry.id != e.id) { + if (bitmap != null) { + bitmap.recycle(); + } + return; + } + if (bitmap != null) { + thumbView.setImageBitmap(bitmap); + } + }); + }); + } + + private Bitmap decodeThumb(QuickSendMediaEntry e) { + try { + int target = dp(120); + Bitmap bitmap = null; + if (e.path != null) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + BitmapFactory.decodeFile(e.path, opts); + int w = opts.outWidth; + int h = opts.outHeight; + int sample = 1; + while (w / sample > target * 2 && h / sample > target * 2) { + sample *= 2; + } + BitmapFactory.Options decodeOpts = new BitmapFactory.Options(); + decodeOpts.inSampleSize = sample; + bitmap = BitmapFactory.decodeFile(e.path, decodeOpts); + } + if (bitmap == null && e.uri != null) { + try { + bitmap = android.provider.MediaStore.Images.Media.getBitmap( + ApplicationLoader.applicationContext.getContentResolver(), e.uri); + } catch (Throwable ignored) { + } + } + if (bitmap != null && e.orientation != 0) { + Matrix matrix = new Matrix(); + matrix.postRotate(e.orientation); + Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + if (rotated != bitmap) { + bitmap.recycle(); + } + bitmap = rotated; + } + return bitmap; + } catch (Throwable t) { + FileLog.e(t); + return null; + } + } + + /** + * Queries MediaStore for the newest image added after {@code sinceDateSec} (unix seconds). + * Runs on a background thread and calls {@code callback} on the UI thread. + */ + public static void queryLatestImage(long sinceDateSec, java.util.function.Consumer callback) { + Utilities.searchQueue.postRunnable(() -> { + QuickSendMediaEntry result = null; + Cursor cursor = null; + try { + String[] projection = new String[]{ + MediaStore.Images.Media._ID, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.DATE_ADDED, + MediaStore.Images.Media.ORIENTATION + }; + String selection = MediaStore.Images.Media.DATE_ADDED + " > ?"; + String[] args = new String[]{String.valueOf(sinceDateSec)}; + // LIMIT in sortOrder is unsupported on Android 11+; take first row from ordered cursor instead. + cursor = ApplicationLoader.applicationContext.getContentResolver().query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, + selection, + args, + MediaStore.Images.Media.DATE_ADDED + " DESC" + ); + if (cursor != null && cursor.moveToFirst()) { + int idCol = cursor.getColumnIndex(MediaStore.Images.Media._ID); + int dataCol = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + int dateCol = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED); + int orientCol = cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION); + QuickSendMediaEntry e = new QuickSendMediaEntry(); + e.id = idCol >= 0 ? cursor.getLong(idCol) : 0L; + e.path = dataCol >= 0 ? cursor.getString(dataCol) : null; + e.dateAdded = dateCol >= 0 ? cursor.getLong(dateCol) : 0L; + e.orientation = orientCol >= 0 ? cursor.getInt(orientCol) : 0; + e.uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, e.id); + e.isVideo = false; + // DATA column may be null on Android 10+ in some cases; fall back to EXIF via URI later. + if (e.orientation == 0 && e.path != null) { + try { + ExifInterface exif = new ExifInterface(e.path); + int o = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + switch (o) { + case ExifInterface.ORIENTATION_ROTATE_90: e.orientation = 90; break; + case ExifInterface.ORIENTATION_ROTATE_180: e.orientation = 180; break; + case ExifInterface.ORIENTATION_ROTATE_270: e.orientation = 270; break; + } + } catch (Throwable ignored) { + } + } + if (e.path != null || e.uri != null) { + result = e; + } + } + } catch (Throwable t) { + FileLog.e(t); + } finally { + if (cursor != null) { + try { cursor.close(); } catch (Throwable ignored) {} + } + } + final QuickSendMediaEntry finalResult = result; + AndroidUtilities.runOnUIThread(() -> callback.accept(finalResult)); + }); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + AndroidUtilities.cancelRunOnUIThread(autoDismissRunnable); + } +} diff --git a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt index f81a728da..ecee88927 100644 --- a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt +++ b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt @@ -72,6 +72,7 @@ object Defines { @BooleanConfig const val hideTimeForSticker = "hideTimeForSticker" @BooleanConfig const val showMessageID = "showMessageID" @BooleanConfig const val hideQuickSendMediaBottom = "hideQuickSendMediaButtom" + @BooleanConfig(true) const val quickSendMediaPopup = "quickSendMediaPopup" @BooleanConfig const val largeAvatarAsBackground = "largeAvatarAsBackground" @BooleanConfig const val useSystemEmoji = "useSystemEmoji" const val customQuickMessage = "customQuickCommand" diff --git a/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml b/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml index a42d26ada..79acb0c95 100644 --- a/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml @@ -294,6 +294,10 @@ 覆蓋 TELEOFFICIAL 的裝置效能偵測器。\n低模式將盡量減少動畫效果和其他內容,有助於節省前景中的電力。\n高模式可能會大幅增加能源消耗和效能負擔,並導致卡頓現象。 跳至開始 停用快速傳送媒體底部 + 新圖片快捷傳送懸浮視窗 + 在聊天介面右下角顯示一個懸浮按鈕,用來快速傳送剛剛拍攝的照片或螢幕截圖。僅在開啟聊天或從背景恢復時偵測。 + 傳送新照片? + 傳送中… 百度翻譯 TranSmart 翻譯器 總是無聲傳送 diff --git a/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml b/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml index 2f0468cbd..088c85dae 100644 --- a/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml @@ -297,6 +297,10 @@ 覆盖 TELEOFFICIAL 的设备性能检测。\n低模式将尽可能减少动画效果和其他东西,这可能有助于节省前台运行时的耗电。\n高模式可能会大幅增加功耗和性能负担并导致卡顿。 回到顶部 禁用快速发送媒体按钮 + 新增图片快捷发送悬浮窗 + 在聊天界面右下角显示一个悬浮按钮,用于快速发送刚刚拍摄的照片或截图。仅在打开聊天或从后台恢复时检测。 + 发送新照片? + 发送中… 百度翻译 腾讯交互翻译 总是静音发送 diff --git a/TMessagesProj/src/main/res/values/strings_nullgram.xml b/TMessagesProj/src/main/res/values/strings_nullgram.xml index ebc0d2581..05b604873 100644 --- a/TMessagesProj/src/main/res/values/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values/strings_nullgram.xml @@ -301,6 +301,10 @@ Override TELEOFFICIAL\'s device performance detector.\nLow mode will reduce animation effects and other stuff as much as possible, which may help to save power in the foreground.\nHigh mode may greatly increase power consumption and performance burden and cause stuttering. Jump To Beginning Disable Quick Send Media Bottom + Quick send popup for new photos + Show a floating button in the bottom right corner of the chat to quickly send a photo or screenshot taken just now. Detection runs when you open or resume the chat. + Send new photo? + Sending… Baidu Translator TranSmart Translator Always Send Without Sound From 89f468a24446ba3b696a6a1a9d3d61d39ef287e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 08:05:08 +0000 Subject: [PATCH 2/7] feat(chat): confirm quick-send and fast-path prepend recent photos - show an Alert confirmation before the quick-send popup actually sends, so an accidental tap no longer fires the message - MediaController.loadGalleryPhotosAlbums now takes an urgent flag: attach-menu opens get NORM_PRIORITY (was MIN_PRIORITY) and parallel scans are coalesced via an atomic flag + queued rerun - add MediaController.quickRefreshLatestPhotos: a lightweight MediaStore query for the top N rows that prepends just-taken photos into the cached All Media / All Photos albums and broadcasts albumsDidLoad right away, so the attach grid surfaces new shots in ~100ms while the full rescan keeps running in the background --- .../telegram/messenger/MediaController.java | 127 +++++++++++++++++- .../java/org/telegram/ui/ChatActivity.java | 14 +- .../ChatAttachAlertPhotoLayout.java | 11 +- .../res/values-zh-rTW/strings_nullgram.xml | 1 + .../main/res/values-zh/strings_nullgram.xml | 1 + .../src/main/res/values/strings_nullgram.xml | 1 + 6 files changed, 148 insertions(+), 7 deletions(-) diff --git a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java index 1d4bd12bf..96f2719ae 100644 --- a/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java +++ b/TMessagesProj/src/main/java/org/telegram/messenger/MediaController.java @@ -5825,8 +5825,25 @@ public static String copyFileToCache(Uri uri, String ext, long sizeLimit) { return null; } + private static final java.util.concurrent.atomic.AtomicBoolean galleryScanInFlight = new java.util.concurrent.atomic.AtomicBoolean(false); + private static volatile boolean galleryScanQueued = false; + private static volatile int galleryScanQueuedGuid = 0; + private static volatile boolean galleryScanQueuedUrgent = false; + public static void loadGalleryPhotosAlbums(final int guid) { + loadGalleryPhotosAlbums(guid, false); + } + + public static void loadGalleryPhotosAlbums(final int guid, final boolean urgent) { + if (!galleryScanInFlight.compareAndSet(false, true)) { + // Coalesce: a scan is running; remember that another refresh was requested. + galleryScanQueued = true; + galleryScanQueuedGuid = guid; + galleryScanQueuedUrgent = galleryScanQueuedUrgent || urgent; + return; + } Thread thread = new Thread(() -> { + try { final ArrayList mediaAlbumsSorted = new ArrayList<>(); final ArrayList photoAlbumsSorted = new ArrayList<>(); SparseArray mediaAlbums = new SparseArray<>(); @@ -6070,11 +6087,119 @@ public static void loadGalleryPhotosAlbums(final int guid) { }); } broadcastNewPhotos(guid, mediaAlbumsSorted, photoAlbumsSorted, mediaCameraAlbumId, allMediaAlbum, allPhotosAlbum, allVideosAlbum, 0); + } finally { + galleryScanInFlight.set(false); + if (galleryScanQueued) { + galleryScanQueued = false; + final int queuedGuid = galleryScanQueuedGuid; + final boolean queuedUrgent = galleryScanQueuedUrgent; + galleryScanQueuedUrgent = false; + AndroidUtilities.runOnUIThread(() -> loadGalleryPhotosAlbums(queuedGuid, queuedUrgent)); + } + } + }); + // Urgent scans (user opened the attach menu) run at normal priority so newly + // added photos/screenshots appear quickly; background observer refreshes stay low. + thread.setPriority(urgent ? Thread.NORM_PRIORITY : Thread.MIN_PRIORITY); + thread.start(); + } + + /** + * Lightweight gallery refresh: queries only the top {@code limit} most-recent images + * from {@link MediaStore} and merges any entries that are new (by imageId) into the + * head of {@link #allMediaAlbumEntry} / {@link #allPhotosAlbumEntry}, then broadcasts + * {@link NotificationCenter#albumsDidLoad}. This is complementary to the full scan + * in {@link #loadGalleryPhotosAlbums(int, boolean)} - it lets newly taken photos + * surface in the attach grid in ~100ms instead of waiting for the full rescan. + */ + public static void quickRefreshLatestPhotos(final int guid, final int limit) { + if (allMediaAlbumEntry == null && allPhotosAlbumEntry == null) { + // No cache to merge into - nothing useful to do; full scan will populate it. + return; + } + Thread thread = new Thread(() -> { + final Context context = ApplicationLoader.applicationContext; + if (Build.VERSION.SDK_INT >= 33 + ? context.checkSelfPermission(Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED + : context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + return; + } + final ArrayList headNewEntries = new ArrayList<>(); + Cursor cursor = null; + try { + cursor = MediaStore.Images.Media.query(context.getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projectionPhotos, null, null, (Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.DATE_MODIFIED : MediaStore.Images.Media.DATE_TAKEN) + " DESC"); + if (cursor != null) { + int imageIdColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID); + int bucketIdColumn = cursor.getColumnIndex(MediaStore.Images.Media.BUCKET_ID); + int dataColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATA); + int dateColumn = cursor.getColumnIndex(Build.VERSION.SDK_INT > 28 ? MediaStore.Images.Media.DATE_MODIFIED : MediaStore.Images.Media.DATE_TAKEN); + int orientationColumn = cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION); + int widthColumn = cursor.getColumnIndex(MediaStore.Images.Media.WIDTH); + int heightColumn = cursor.getColumnIndex(MediaStore.Images.Media.HEIGHT); + int sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE); + int count = 0; + while (cursor.moveToNext() && count < limit) { + count++; + String path = cursor.getString(dataColumn); + if (TextUtils.isEmpty(path)) continue; + int imageId = cursor.getInt(imageIdColumn); + int bucketId = cursor.getInt(bucketIdColumn); + long dateTaken = cursor.getLong(dateColumn); + int orientation = cursor.getInt(orientationColumn); + int width = cursor.getInt(widthColumn); + int height = cursor.getInt(heightColumn); + long size = cursor.getLong(sizeColumn); + headNewEntries.add(new PhotoEntry(bucketId, imageId, dateTaken, path, orientation, 0, false, width, height, size)); + } + } + } catch (Throwable e) { + FileLog.e(e); + } finally { + if (cursor != null) { + try { cursor.close(); } catch (Exception e) { FileLog.e(e); } + } + } + if (headNewEntries.isEmpty()) { + return; + } + AndroidUtilities.runOnUIThread(() -> mergeHeadEntriesIntoCache(guid, headNewEntries)); }); - thread.setPriority(Thread.MIN_PRIORITY); + thread.setPriority(Thread.NORM_PRIORITY); thread.start(); } + private static void mergeHeadEntriesIntoCache(int guid, ArrayList headEntries) { + AlbumEntry mediaAlbum = allMediaAlbumEntry; + AlbumEntry photosAlbum = allPhotosAlbumEntry; + if (mediaAlbum == null && photosAlbum == null) { + return; + } + AlbumEntry referenceAlbum = mediaAlbum != null ? mediaAlbum : photosAlbum; + ArrayList newOnes = new ArrayList<>(); + for (PhotoEntry e : headEntries) { + if (referenceAlbum.photosByIds.get(e.imageId) == null) { + newOnes.add(e); + } + } + if (newOnes.isEmpty()) { + return; + } + // Prepend to existing albums (on UI thread, so safe w.r.t. adapter iteration + // as long as we notify right after). + for (int i = newOnes.size() - 1; i >= 0; i--) { + PhotoEntry e = newOnes.get(i); + if (mediaAlbum != null && mediaAlbum.photosByIds.get(e.imageId) == null) { + mediaAlbum.photos.add(0, e); + mediaAlbum.photosByIds.put(e.imageId, e); + } + if (photosAlbum != null && photosAlbum.photosByIds.get(e.imageId) == null) { + photosAlbum.photos.add(0, e); + photosAlbum.photosByIds.put(e.imageId, e); + } + } + NotificationCenter.getGlobalInstance().postNotificationName(NotificationCenter.albumsDidLoad, guid, allMediaAlbums, allPhotoAlbums, null); + } + public static boolean forceBroadcastNewPhotos; private static void broadcastNewPhotos(final int guid, final ArrayList mediaAlbumsSorted, final ArrayList photoAlbumsSorted, final Integer cameraAlbumIdFinal, final AlbumEntry allMediaAlbumFinal, final AlbumEntry allPhotosAlbumFinal, final AlbumEntry allVideosAlbumFinal, int delay) { if (broadcastPhotosRunnable != null) { diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 6c5f909cb..8a832df75 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -29821,11 +29821,23 @@ private void checkQuickSendMediaPopup() { } quickSendMediaPopup.show(entry, e -> { quickSendDismissedIds.add(e.id); - sendQuickMediaEntry(e); + showQuickSendConfirmation(e); }); }); } + private void showQuickSendConfirmation(QuickSendMediaPopup.QuickSendMediaEntry e) { + if (e == null || getParentActivity() == null) { + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity(), themeDelegate); + builder.setTitle(LocaleController.getString("quickSendMediaPopupTitle", R.string.quickSendMediaPopupTitle)); + builder.setMessage(LocaleController.getString("quickSendMediaPopupConfirm", R.string.quickSendMediaPopupConfirm)); + builder.setPositiveButton(LocaleController.getString("Send", R.string.Send), (dialog, which) -> sendQuickMediaEntry(e)); + builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); + showDialog(builder.create()); + } + private void sendQuickMediaEntry(QuickSendMediaPopup.QuickSendMediaEntry e) { if (e == null || (e.path == null && e.uri == null)) { return; diff --git a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java index d638e436d..e364810bd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/Components/ChatAttachAlertPhotoLayout.java @@ -2560,11 +2560,12 @@ public void onAnimationEnd(Animator animator) { } public void loadGalleryPhotos() { - // Always request a MediaStore refresh so newly added photos/screenshots - // appear without waiting for the ContentObserver's 2s debounce. The load - // runs on a background thread and updates the UI via NotificationCenter - // (albumsDidLoad) while the cached album (if any) keeps rendering. - MediaController.loadGalleryPhotosAlbums(0); + // Fast path: if we already have a cached album, prepend just-taken photos + // into it (~100ms) so the grid shows them immediately instead of waiting + // for the full rescan. The full rescan still runs right after at normal + // priority so bucket counts and album lists stay accurate. + MediaController.quickRefreshLatestPhotos(0, 30); + MediaController.loadGalleryPhotosAlbums(0, true); } private boolean shouldLoadAllMedia() { diff --git a/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml b/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml index 79acb0c95..792612420 100644 --- a/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml @@ -297,6 +297,7 @@ 新圖片快捷傳送懸浮視窗 在聊天介面右下角顯示一個懸浮按鈕,用來快速傳送剛剛拍攝的照片或螢幕截圖。僅在開啟聊天或從背景恢復時偵測。 傳送新照片? + 這張照片將被傳送到目前對話。 傳送中… 百度翻譯 TranSmart 翻譯器 diff --git a/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml b/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml index 088c85dae..df820a47e 100644 --- a/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml @@ -300,6 +300,7 @@ 新增图片快捷发送悬浮窗 在聊天界面右下角显示一个悬浮按钮,用于快速发送刚刚拍摄的照片或截图。仅在打开聊天或从后台恢复时检测。 发送新照片? + 这张照片将被发送到当前对话。 发送中… 百度翻译 腾讯交互翻译 diff --git a/TMessagesProj/src/main/res/values/strings_nullgram.xml b/TMessagesProj/src/main/res/values/strings_nullgram.xml index 05b604873..81f65f794 100644 --- a/TMessagesProj/src/main/res/values/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values/strings_nullgram.xml @@ -304,6 +304,7 @@ Quick send popup for new photos Show a floating button in the bottom right corner of the chat to quickly send a photo or screenshot taken just now. Detection runs when you open or resume the chat. Send new photo? + This photo will be sent to the current chat. Sending… Baidu Translator TranSmart Translator From be9c8352958a935976e7ceb619cd6025bd12edb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 08:46:28 +0000 Subject: [PATCH 3/7] fix(chat): keep dismissed quick-send popup from reappearing Closing the popup with x now records the image in the per-instance dismissed set and nudges the detection watermark past its DATE_ADDED, so the same photo won't resurface on the next onResume. --- .../java/org/telegram/ui/ChatActivity.java | 20 ++++++++++++++++--- .../nnngram/ui/QuickSendMediaPopup.java | 10 +++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 8a832df75..2440655dd 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -29819,9 +29819,23 @@ private void checkQuickSendMediaPopup() { if (quickSendMediaPopup.isShowingFor(entry.id)) { return; } - quickSendMediaPopup.show(entry, e -> { - quickSendDismissedIds.add(e.id); - showQuickSendConfirmation(e); + quickSendMediaPopup.show(entry, new QuickSendMediaPopup.Delegate() { + @Override + public void onSend(QuickSendMediaPopup.QuickSendMediaEntry e) { + quickSendDismissedIds.add(e.id); + showQuickSendConfirmation(e); + } + + @Override + public void onUserDismiss(QuickSendMediaPopup.QuickSendMediaEntry e) { + // User explicitly closed the popup with ×: never re-surface this image + // for this ChatActivity instance (and nudge the "since" watermark past it + // so it is excluded on subsequent resume queries too). + quickSendDismissedIds.add(e.id); + if (e.dateAdded > 0) { + quickSendPauseTimeSec = Math.max(quickSendPauseTimeSec, e.dateAdded); + } + } }); }); } diff --git a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java index 8df992ff9..7efa63785 100644 --- a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java +++ b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/ui/QuickSendMediaPopup.java @@ -54,6 +54,9 @@ public class QuickSendMediaPopup extends FrameLayout { public interface Delegate { void onSend(QuickSendMediaEntry entry); + + /** Called when the user explicitly taps the × to dismiss. Not called on auto-dismiss. */ + default void onUserDismiss(QuickSendMediaEntry entry) {} } public static class QuickSendMediaEntry { @@ -132,7 +135,12 @@ public void getOutline(View view, Outline outline) { delegate.onSend(entry); dismiss(true); }); - closeButton.setOnClickListener(v -> dismiss(true)); + closeButton.setOnClickListener(v -> { + if (entry != null && delegate != null) { + delegate.onUserDismiss(entry); + } + dismiss(true); + }); setAlpha(0f); setScaleX(0.6f); From 867f45d81df62aaa5d1e44a78fd252bfe799b619 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 10:55:09 +0000 Subject: [PATCH 4/7] ci: strip backticks from commit message before Telegram upload uploadCI.py wraps $COMMIT_MESSAGE in a triple-backtick fence and sends it with parse_mode=Markdown. When the commit body carries single backticks (e.g. identifier spans like the recent retention / filter commits) the inner ` clashes with the outer ```, Telegram returns "can't parse entities" and the upload step fails. Sanitize the env var in the workflow so the uploader still formats normally while the body stays legal. --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed7396184..fa86b9f78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,12 @@ jobs: chmod +x telegram-bot-api-binary ./telegram-bot-api-binary --api-id=21724 --api-hash=3e0cb5efcd52300aec5994fdfc5bdc16 --local 2>&1 > /dev/null & curl https://raw.githubusercontent.com/PreviousAlone/ActionScript/main/uploadCI.py -o uploadCI.py - python uploadCI.py + # Legacy Telegram Markdown (used by uploadCI.py) fails with + # "can't parse entities" when the commit body contains backticks — + # they clash with the ``` fence the template wraps the message in. + # Strip them (and any CR) before passing to the uploader. + sanitized=$(printf '%s' "$COMMIT_MESSAGE" | tr -d '`\r') + COMMIT_MESSAGE="$sanitized" python uploadCI.py env: CHAT_ID: ${{ secrets.TELEGRAM_CHATID }} TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} From 00b3ab32133a3f2b768e7b211e45b96a6d5d14dd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 11:48:19 +0000 Subject: [PATCH 5/7] feat(chat): open PhotoViewer preview for quick-send and persist dismissals - tapping the quick-send popup now opens the standard PhotoViewer preview/editor with Send, so users can review (and caption/crop) the photo before it goes out, matching the attach-menu selection flow - persist the last dismissed image id in ConfigManager under quickSendMediaLastDismissedId; any MediaStore photo with a smaller or equal id is never resurfaced, so closing the popup with x stays dismissed across fragment re-entries and process restarts --- .../java/org/telegram/ui/ChatActivity.java | 80 +++++++++++++++---- .../xyz/nextalone/nnngram/utils/Defines.kt | 1 + .../res/values-zh-rTW/strings_nullgram.xml | 3 - .../main/res/values-zh/strings_nullgram.xml | 3 - .../src/main/res/values/strings_nullgram.xml | 3 - 5 files changed, 66 insertions(+), 24 deletions(-) diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 2440655dd..fdf32f43a 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -29809,10 +29809,17 @@ private void checkQuickSendMediaPopup() { // First time entering: look back 3 minutes for a freshly taken shot. sinceSec = System.currentTimeMillis() / 1000L - 180L; } + final long persistedDismissedId = xyz.nextalone.nnngram.config.ConfigManager + .getLongOrDefault(xyz.nextalone.nnngram.utils.Defines.quickSendMediaLastDismissedId, 0L); QuickSendMediaPopup.queryLatestImage(sinceSec, entry -> { if (entry == null || quickSendMediaPopup == null || paused) { return; } + if (entry.id <= persistedDismissedId) { + // Previously dismissed across sessions - MediaStore imageIds increase + // monotonically, so anything at or below the watermark is old news. + return; + } if (quickSendDismissedIds.contains(entry.id)) { return; } @@ -29823,33 +29830,75 @@ private void checkQuickSendMediaPopup() { @Override public void onSend(QuickSendMediaPopup.QuickSendMediaEntry e) { quickSendDismissedIds.add(e.id); - showQuickSendConfirmation(e); + openQuickSendPreview(e); } @Override public void onUserDismiss(QuickSendMediaPopup.QuickSendMediaEntry e) { - // User explicitly closed the popup with ×: never re-surface this image - // for this ChatActivity instance (and nudge the "since" watermark past it - // so it is excluded on subsequent resume queries too). - quickSendDismissedIds.add(e.id); - if (e.dateAdded > 0) { - quickSendPauseTimeSec = Math.max(quickSendPauseTimeSec, e.dateAdded); - } + // User explicitly closed the popup with ×: never re-surface this image, + // even after finishing and re-entering the fragment. + rememberQuickSendDismissed(e); } }); }); } - private void showQuickSendConfirmation(QuickSendMediaPopup.QuickSendMediaEntry e) { + private void rememberQuickSendDismissed(QuickSendMediaPopup.QuickSendMediaEntry e) { + if (e == null) return; + quickSendDismissedIds.add(e.id); + if (e.dateAdded > 0) { + quickSendPauseTimeSec = Math.max(quickSendPauseTimeSec, e.dateAdded); + } + long prev = xyz.nextalone.nnngram.config.ConfigManager + .getLongOrDefault(xyz.nextalone.nnngram.utils.Defines.quickSendMediaLastDismissedId, 0L); + if (e.id > prev) { + xyz.nextalone.nnngram.config.ConfigManager + .putLong(xyz.nextalone.nnngram.utils.Defines.quickSendMediaLastDismissedId, e.id); + } + } + + private void openQuickSendPreview(QuickSendMediaPopup.QuickSendMediaEntry e) { if (e == null || getParentActivity() == null) { return; } - AlertDialog.Builder builder = new AlertDialog.Builder(getParentActivity(), themeDelegate); - builder.setTitle(LocaleController.getString("quickSendMediaPopupTitle", R.string.quickSendMediaPopupTitle)); - builder.setMessage(LocaleController.getString("quickSendMediaPopupConfirm", R.string.quickSendMediaPopupConfirm)); - builder.setPositiveButton(LocaleController.getString("Send", R.string.Send), (dialog, which) -> sendQuickMediaEntry(e)); - builder.setNegativeButton(LocaleController.getString("Cancel", R.string.Cancel), null); - showDialog(builder.create()); + if (!checkSlowModeAlert()) { + return; + } + final String path = e.path; + if (path == null) { + // No file path (rare, scoped-storage edge case) - fall back to direct send via URI. + sendQuickMediaEntry(e); + return; + } + Pair orientation = AndroidUtilities.getImageOrientation(path); + final MediaController.PhotoEntry photoEntry = + new MediaController.PhotoEntry(0, 0, 0, path, orientation.first, false, 0, 0, 0) + .setOrientation(orientation); + final ArrayList list = new ArrayList<>(); + list.add(photoEntry); + + PhotoViewer.getInstance().setParentActivity(this, themeDelegate); + if (PhotoViewer.getInstance().isVisible()) { + PhotoViewer.getInstance().closePhoto(false, false); + } + PhotoViewer.getInstance().openPhotoForSelect(list, 0, 0, false, new PhotoViewer.EmptyPhotoViewerProvider() { + @Override + public void sendButtonPressed(int index, VideoEditedInfo videoEditedInfo, boolean notify, int scheduleDate, int scheduleRepeatPeriod, boolean forceDocument) { + // User confirmed from the preview: persist dismissal and send. + rememberQuickSendDismissed(e); + sendMedia(photoEntry, videoEditedInfo, notify, scheduleDate, scheduleRepeatPeriod, forceDocument, 0); + } + + @Override + public boolean canScrollAway() { + return false; + } + + @Override + public boolean canCaptureMorePhotos() { + return false; + } + }, this); } private void sendQuickMediaEntry(QuickSendMediaPopup.QuickSendMediaEntry e) { @@ -29859,6 +29908,7 @@ private void sendQuickMediaEntry(QuickSendMediaPopup.QuickSendMediaEntry e) { if (!checkSlowModeAlert()) { return; } + rememberQuickSendDismissed(e); SendMessagesHelper.prepareSendingPhoto( getAccountInstance(), e.path, diff --git a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt index ecee88927..4bc4eb5ab 100644 --- a/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt +++ b/TMessagesProj/src/main/java/xyz/nextalone/nnngram/utils/Defines.kt @@ -73,6 +73,7 @@ object Defines { @BooleanConfig const val showMessageID = "showMessageID" @BooleanConfig const val hideQuickSendMediaBottom = "hideQuickSendMediaButtom" @BooleanConfig(true) const val quickSendMediaPopup = "quickSendMediaPopup" + const val quickSendMediaLastDismissedId = "quickSendMediaLastDismissedId" @BooleanConfig const val largeAvatarAsBackground = "largeAvatarAsBackground" @BooleanConfig const val useSystemEmoji = "useSystemEmoji" const val customQuickMessage = "customQuickCommand" diff --git a/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml b/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml index 792612420..b50e0b7ca 100644 --- a/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values-zh-rTW/strings_nullgram.xml @@ -296,9 +296,6 @@ 停用快速傳送媒體底部 新圖片快捷傳送懸浮視窗 在聊天介面右下角顯示一個懸浮按鈕,用來快速傳送剛剛拍攝的照片或螢幕截圖。僅在開啟聊天或從背景恢復時偵測。 - 傳送新照片? - 這張照片將被傳送到目前對話。 - 傳送中… 百度翻譯 TranSmart 翻譯器 總是無聲傳送 diff --git a/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml b/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml index df820a47e..6089b6bd9 100644 --- a/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values-zh/strings_nullgram.xml @@ -299,9 +299,6 @@ 禁用快速发送媒体按钮 新增图片快捷发送悬浮窗 在聊天界面右下角显示一个悬浮按钮,用于快速发送刚刚拍摄的照片或截图。仅在打开聊天或从后台恢复时检测。 - 发送新照片? - 这张照片将被发送到当前对话。 - 发送中… 百度翻译 腾讯交互翻译 总是静音发送 diff --git a/TMessagesProj/src/main/res/values/strings_nullgram.xml b/TMessagesProj/src/main/res/values/strings_nullgram.xml index 81f65f794..806d5e085 100644 --- a/TMessagesProj/src/main/res/values/strings_nullgram.xml +++ b/TMessagesProj/src/main/res/values/strings_nullgram.xml @@ -303,9 +303,6 @@ Disable Quick Send Media Bottom Quick send popup for new photos Show a floating button in the bottom right corner of the chat to quickly send a photo or screenshot taken just now. Detection runs when you open or resume the chat. - Send new photo? - This photo will be sent to the current chat. - Sending… Baidu Translator TranSmart Translator Always Send Without Sound From 0e1a0c169c15f118bedb3667e4a3cb8491bcb353 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 11:52:13 +0000 Subject: [PATCH 6/7] fix(chat): lower quick-send popup above input bar Move the popup from bottomMargin=110 / rightMargin=6 (stacked above sideControlsButtonsLayout) to bottomMargin=6 / rightMargin=64, so it sits just above the input bar and to the left of the scroll-to-bottom button (clear of the 57dp-wide side controls column). Feels like a proper bottom-right affordance instead of floating mid-chat. --- .../src/main/java/org/telegram/ui/ChatActivity.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index fdf32f43a..826c857b9 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -7358,7 +7358,10 @@ public void setAlpha(float alpha) { contentView.addView(sideControlsButtonsLayout, LayoutHelper.createFrame(57, 300, Gravity.RIGHT | Gravity.BOTTOM)); quickSendMediaPopup = new QuickSendMediaPopup(context); - contentView.addView(quickSendMediaPopup, LayoutHelper.createFrame(72, 72, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 6, 110)); + // Placed just above the input bar (small bottomMargin so it reads as a + // bottom-right affordance) and shifted left of sideControlsButtonsLayout + // (57dp wide) so the popup and scroll-to-bottom button don't overlap. + contentView.addView(quickSendMediaPopup, LayoutHelper.createFrame(72, 72, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 64, 6)); contentView.addView(topPanelLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP)); From 6c84cb7705cb27b36be203dc4b4af8e24453b267 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 12:41:17 +0000 Subject: [PATCH 7/7] fix(chat): dismiss quick-send popup on first show and anchor bottom-right - persist the dismissal watermark the moment the popup surfaces a photo, so send / cancel-from-preview / 10s timeout / leaving the chat all count as "already seen" and the same image can never pop again - move popup to rightMargin=6 / bottomMargin=60 so it sits right-aligned with the scroll-to-bottom button and just above it, instead of floating further left with oversized right margin --- .../java/org/telegram/ui/ChatActivity.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java index 826c857b9..daada8262 100644 --- a/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java +++ b/TMessagesProj/src/main/java/org/telegram/ui/ChatActivity.java @@ -7358,10 +7358,10 @@ public void setAlpha(float alpha) { contentView.addView(sideControlsButtonsLayout, LayoutHelper.createFrame(57, 300, Gravity.RIGHT | Gravity.BOTTOM)); quickSendMediaPopup = new QuickSendMediaPopup(context); - // Placed just above the input bar (small bottomMargin so it reads as a - // bottom-right affordance) and shifted left of sideControlsButtonsLayout - // (57dp wide) so the popup and scroll-to-bottom button don't overlap. - contentView.addView(quickSendMediaPopup, LayoutHelper.createFrame(72, 72, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 64, 6)); + // Sits at the bottom-right of the chat, right-aligned with the + // scroll-to-bottom button and just above it, so it reads as a + // proper bottom-right affordance above the input bar. + contentView.addView(quickSendMediaPopup, LayoutHelper.createFrame(72, 72, Gravity.RIGHT | Gravity.BOTTOM, 0, 0, 6, 60)); contentView.addView(topPanelLayout, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.WRAP_CONTENT, Gravity.TOP)); @@ -29829,18 +29829,21 @@ private void checkQuickSendMediaPopup() { if (quickSendMediaPopup.isShowingFor(entry.id)) { return; } + // The photo is about to be surfaced once; persist the dismissal + // watermark immediately so that *any* exit path (send, cancel + // from the preview, 10s timeout, leaving the chat, process + // restart) counts as "already seen" and the same image is not + // surfaced a second time. + rememberQuickSendDismissed(entry); quickSendMediaPopup.show(entry, new QuickSendMediaPopup.Delegate() { @Override public void onSend(QuickSendMediaPopup.QuickSendMediaEntry e) { - quickSendDismissedIds.add(e.id); openQuickSendPreview(e); } @Override public void onUserDismiss(QuickSendMediaPopup.QuickSendMediaEntry e) { - // User explicitly closed the popup with ×: never re-surface this image, - // even after finishing and re-entering the fragment. - rememberQuickSendDismissed(e); + // Already persisted on show(); nothing extra to do. } }); });