Фикс: имя файла в пересланных сообщениях, потеря фоток/файлов при пересылке forwarded-сообщений, Фоллбэк при unwrap forwarded-сообщения, защита БД от перезаписи синком

This commit is contained in:
2026-03-21 20:28:11 +05:00
parent 224b8a2b54
commit 65e5991f97
24 changed files with 2715 additions and 1037 deletions

View File

@@ -568,6 +568,11 @@ private struct ChatListDialogContent: View {
var body: some View {
let _ = PerformanceLogger.shared.track("chatList.bodyEval")
// CRITICAL: Read DialogRepository.dialogs directly to establish @Observable tracking.
// Without this, ChatListDialogContent only observes viewModel (ObservableObject)
// which never publishes objectWillChange for dialog mutations.
// The read forces SwiftUI to re-evaluate body when dialogs dict changes.
let _ = DialogRepository.shared.dialogs.count
// Use pre-partitioned arrays from ViewModel (single-pass O(n) instead of 3× filter).
let pinned = viewModel.allModePinned
let unpinned = viewModel.allModeUnpinned
@@ -636,6 +641,7 @@ private struct ChatListDialogContent: View {
.scrollContentBackground(.hidden)
.scrollDismissesKeyboard(.immediately)
.scrollIndicators(.hidden)
.modifier(ClassicSwipeActionsModifier())
// Scroll-to-top: tap "Chats" in toolbar
.onReceive(NotificationCenter.default.publisher(for: .chatListScrollToTop)) { _ in
// Scroll to first dialog ID (pinned or unpinned)
@@ -712,7 +718,7 @@ struct SyncAwareChatRow: View {
if !dialog.isSavedMessages {
Button {
viewModel.toggleMute(dialog)
withAnimation { viewModel.toggleMute(dialog) }
} label: {
Label(
dialog.isMuted ? "Unmute" : "Mute",
@@ -724,7 +730,7 @@ struct SyncAwareChatRow: View {
}
.swipeActions(edge: .leading, allowsFullSwipe: true) {
Button {
viewModel.togglePin(dialog)
withAnimation { viewModel.togglePin(dialog) }
} label: {
Label(dialog.isPinned ? "Unpin" : "Pin", systemImage: dialog.isPinned ? "pin.slash" : "pin")
}
@@ -777,6 +783,22 @@ private struct DeviceApprovalBanner: View {
}
}
// MARK: - iOS 26+ Classic Swipe Actions
/// iOS 26: disable Liquid Glass on the List so swipe action buttons use
/// solid colors (same as iOS < 26). Uses UIAppearance override.
private struct ClassicSwipeActionsModifier: ViewModifier {
func body(content: Content) -> some View {
content.onAppear {
if #available(iOS 26, *) {
// Disable glass on UITableView-backed List swipe actions.
let appearance = UITableView.appearance()
appearance.backgroundColor = .clear
}
}
}
}
#Preview("Chat List") {
ChatListView(isSearchActive: .constant(false), isDetailPresented: .constant(false))
.preferredColorScheme(.dark)

View File

@@ -141,7 +141,7 @@ private extension ChatRowView {
if isTyping && !dialog.isSavedMessages {
return "typing..."
}
if dialog.lastMessage.isEmpty {
if dialog.lastMessage.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return "No messages yet"
}
if let cached = Self.messageTextCache[dialog.lastMessage] {
@@ -195,11 +195,11 @@ private extension ChatRowView {
.rotationEffect(.degrees(45))
}
// Desktop parity: delivery icon and unread badge are
// mutually exclusive badge hidden when lastMessageFromMe.
// Also hidden during sync (desktop hides badges while
// protocolState == SYNCHRONIZATION).
if dialog.unreadCount > 0 && !dialog.lastMessageFromMe && !isSyncing {
// Show unread badge whenever there are unread messages.
// Previously hidden when lastMessageFromMe (desktop parity),
// but this caused invisible unreads when user sent a reply
// without reading prior incoming messages first.
if dialog.unreadCount > 0 && !isSyncing {
unreadBadge
}
}

View File

@@ -71,13 +71,9 @@ struct RequestChatsView: View {
}
}
@ViewBuilder
// Use TelegramGlass* for ALL iOS versions SwiftUI .glassEffect() blocks touches.
private func glassCapsule(strokeOpacity: Double = 0.18, strokeColor: Color = .white) -> some View {
if #available(iOS 26.0, *) {
Capsule().fill(.clear).glassEffect(.regular, in: .capsule)
} else {
TelegramGlassCapsule()
}
TelegramGlassCapsule()
}
private func requestRow(_ dialog: Dialog, isFirst: Bool) -> some View {