diff --git a/Rosetta.xcodeproj/project.pbxproj b/Rosetta.xcodeproj/project.pbxproj index efabb2c..dd42e79 100644 --- a/Rosetta.xcodeproj/project.pbxproj +++ b/Rosetta.xcodeproj/project.pbxproj @@ -641,7 +641,7 @@ CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_TEAM = QN8Z263QGX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -657,7 +657,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.3.4; PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -681,7 +681,7 @@ CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 35; DEVELOPMENT_TEAM = QN8Z263QGX; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -697,7 +697,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.3.4; PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Rosetta/Core/Utils/ReleaseNotes.swift b/Rosetta/Core/Utils/ReleaseNotes.swift index d6a862e..03237de 100644 --- a/Rosetta/Core/Utils/ReleaseNotes.swift +++ b/Rosetta/Core/Utils/ReleaseNotes.swift @@ -13,7 +13,7 @@ enum ReleaseNotes { body: """ **Пуш-уведомления** - In-app баннеры (Telegram parity) — при получении сообщения внутри приложения. Исправлен спам вибраций при входе. Аватарки в пушах совпадают с приложением. Группы без фото — иконка людей. Desktop-suppression 30 сек. + Аватарки отправителей в системных пушах (Communication Notification). In-app баннер переделан 1-в-1 как в Telegram (glass-фон, жесты, анимации). Исправлен спам вибраций при входе. Desktop-suppression 30 сек. """ ) ] diff --git a/Rosetta/DesignSystem/Components/InAppBannerView.swift b/Rosetta/DesignSystem/Components/InAppBannerView.swift index cbfb799..7d80f9b 100644 --- a/Rosetta/DesignSystem/Components/InAppBannerView.swift +++ b/Rosetta/DesignSystem/Components/InAppBannerView.swift @@ -5,13 +5,13 @@ import SwiftUI /// Telegram-style in-app notification banner shown when a message arrives /// while the app is in foreground and the user is NOT in that chat. /// -/// Specs (from Telegram iOS `ChatMessageNotificationItem.swift`): -/// - Panel: 74pt height, 24pt corner radius, 8pt horizontal margin -/// - Avatar: 54pt circle, 12pt left inset +/// Specs (from Telegram iOS `ChatMessageNotificationItem.swift` + `NotificationItemContainerNode.swift`): +/// - Panel: 74pt min height, 24pt corner radius, 8pt horizontal margin +/// - Avatar: 54pt circle, 12pt left inset, 23pt avatar-to-text spacing /// - Title: semibold 16pt, white, 1 line -/// - Message: regular 16pt, white 70% opacity, max 2 lines -/// - Background: ultraThinMaterial (dark) -/// - Slide from top: 0.4s, auto-dismiss: 5s, swipe-up dismiss +/// - Message: regular 16pt, white (full), max 2 lines +/// - Background: TelegramGlass (CABackdropLayer / UIGlassEffect) +/// - Slide from top: 0.4s, auto-dismiss: 5s, swipe dismiss with 0.55 damping struct InAppBannerView: View { let senderName: String let messagePreview: String @@ -28,7 +28,7 @@ struct InAppBannerView: View { private let horizontalMargin: CGFloat = 8 var body: some View { - HStack(spacing: 12) { + HStack(spacing: 23) { avatarView VStack(alignment: .leading, spacing: 1) { Text(senderName) @@ -37,36 +37,44 @@ struct InAppBannerView: View { .lineLimit(1) Text(messagePreview) .font(.system(size: 16)) - .foregroundStyle(.white.opacity(0.7)) + .foregroundStyle(.white) .lineLimit(2) } Spacer(minLength: 0) } - .padding(.horizontal, 12) - .frame(height: panelHeight) + .padding(.leading, 12) + .padding(.trailing, 10) + .frame(minHeight: panelHeight) .frame(maxWidth: .infinity) - .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: cornerRadius)) + .glass(cornerRadius: cornerRadius) .padding(.horizontal, horizontalMargin) .offset(y: dragOffset) .gesture( DragGesture(minimumDistance: 5) .onChanged { value in - // Only allow upward drag (negative Y). - if value.translation.height < 0 { - dragOffset = value.translation.height + let translation = value.translation.height + if translation < 0 { + // Dragging up — full 1:1 tracking. + dragOffset = translation + } else { + // Dragging down — logarithmic rubber-band (Telegram: 0.55/50 cap). + // Formula: -((1 - 1/((delta*0.55/50)+1)) * 50) + let delta = translation + dragOffset = (1.0 - (1.0 / ((delta * 0.55 / 50.0) + 1.0))) * 50.0 } } .onEnded { value in - // Dismiss if dragged up > 20pt or velocity > 300. - if value.translation.height < -20 - || value.predictedEndTranslation.height < -100 { - withAnimation(.easeOut(duration: 0.3)) { + let velocity = value.predictedEndTranslation.height - value.translation.height + // Dismiss: swiped up > 20pt or fast upward velocity (Telegram: -20pt / 300pt/s). + if value.translation.height < -20 || velocity < -300 { + withAnimation(.easeOut(duration: 0.4)) { dragOffset = -200 } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { onDismiss() } } else { + // Spring back (Telegram: 0.3s easeInOut). withAnimation(.easeInOut(duration: 0.3)) { dragOffset = 0 } diff --git a/Rosetta/RosettaApp.swift b/Rosetta/RosettaApp.swift index 0b1e69e..75dcc2c 100644 --- a/Rosetta/RosettaApp.swift +++ b/Rosetta/RosettaApp.swift @@ -850,7 +850,8 @@ struct RosettaApp: App { @ViewBuilder private var inAppBannerOverlay: some View { - VStack { + // Telegram parity: 8pt inset below safe area top (NotificationItemContainerNode.swift:98). + VStack(spacing: 0) { if let banner = bannerManager.currentBanner { InAppBannerView( senderName: banner.senderName, diff --git a/RosettaNotificationService/NotificationService.swift b/RosettaNotificationService/NotificationService.swift index 5fd9864..399500b 100644 --- a/RosettaNotificationService/NotificationService.swift +++ b/RosettaNotificationService/NotificationService.swift @@ -497,6 +497,9 @@ final class NotificationService: UNNotificationServiceExtension { UIGraphicsEndImageContext() guard let pngData = image?.pngData() else { return nil } + if let tempURL = storeTemporaryImage(data: pngData, key: "letter-\(key)", fileExtension: "png") { + return INImage(url: tempURL) + } return INImage(imageData: pngData) } @@ -532,6 +535,9 @@ final class NotificationService: UNNotificationServiceExtension { UIGraphicsEndImageContext() guard let pngData = image?.pngData() else { return nil } + if let tempURL = storeTemporaryImage(data: pngData, key: "group-\(key)", fileExtension: "png") { + return INImage(url: tempURL) + } return INImage(imageData: pngData) } @@ -550,6 +556,9 @@ final class NotificationService: UNNotificationServiceExtension { guard !normalized.isEmpty else { continue } let avatarURL = avatarsDir.appendingPathComponent("\(normalized).jpg") if let data = try? Data(contentsOf: avatarURL), !data.isEmpty { + if let tempURL = storeTemporaryImage(data: data, key: "photo-\(normalized)", fileExtension: "jpg") { + return INImage(url: tempURL) + } return INImage(imageData: data) } } @@ -593,6 +602,28 @@ final class NotificationService: UNNotificationServiceExtension { .lowercased() } + /// Writes image data to NSTemporaryDirectory so INImage can reference it via file URL. + /// Telegram approach: INImage(imageData:) is unreliable in NSE — file URL works. + private static func storeTemporaryImage(data: Data, key: String, fileExtension: String) -> URL? { + let imagesPath = NSTemporaryDirectory() + "aps-data" + try? FileManager.default.createDirectory( + at: URL(fileURLWithPath: imagesPath), + withIntermediateDirectories: true + ) + let fileName = key.replacingOccurrences(of: "/", with: "_") + let tempURL = URL(fileURLWithPath: imagesPath) + .appendingPathComponent("\(fileName).\(fileExtension)") + if FileManager.default.fileExists(atPath: tempURL.path) { + return tempURL + } + do { + try data.write(to: tempURL, options: [.atomic]) + return tempURL + } catch { + return nil + } + } + // MARK: - Helpers /// Android parity: extract sender key from multiple possible key names. diff --git a/RosettaNotificationService/RosettaNotificationService.entitlements b/RosettaNotificationService/RosettaNotificationService.entitlements index e07eedf..fcb2c96 100644 --- a/RosettaNotificationService/RosettaNotificationService.entitlements +++ b/RosettaNotificationService/RosettaNotificationService.entitlements @@ -2,6 +2,8 @@ + com.apple.developer.usernotifications.communication + com.apple.security.application-groups group.com.rosetta.dev