Фикс: пуш-аватарки (Communication Notification entitlement) + in-app баннер 1:1 Telegram parity

This commit is contained in:
2026-04-08 01:03:13 +05:00
parent bde2e78f3d
commit f6af59ba11
6 changed files with 67 additions and 25 deletions

View File

@@ -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 сек.
"""
)
]

View File

@@ -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
}

View File

@@ -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,