Фикс: пуш-аватарки (Communication Notification entitlement) + in-app баннер 1:1 Telegram parity
This commit is contained in:
@@ -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 = "";
|
||||
|
||||
@@ -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 сек.
|
||||
"""
|
||||
)
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.rosetta.dev</string>
|
||||
|
||||
Reference in New Issue
Block a user