Push-уведомления: Telegram-parity in-app баннер, threadIdentifier группировка и letter-avatar в NSE

This commit is contained in:
2026-04-01 18:33:59 +05:00
parent 79c5635715
commit 4be6761492
20 changed files with 1347 additions and 240 deletions

View File

@@ -1,3 +1,4 @@
import UIKit
import UserNotifications
import Intents
@@ -161,7 +162,13 @@ final class NotificationService: UNNotificationServiceExtension {
}
}
// 5. Normalize sender_public_key in userInfo for tap navigation.
// 5. Group notifications by conversation (Telegram parity).
// iOS stacks notifications from the same chat together.
if !senderKey.isEmpty {
content.threadIdentifier = senderKey
}
// 6. Normalize sender_public_key in userInfo for tap navigation.
var updatedInfo = content.userInfo
if !senderKey.isEmpty {
updatedInfo["sender_public_key"] = senderKey
@@ -217,11 +224,13 @@ final class NotificationService: UNNotificationServiceExtension {
senderKey: String
) -> UNNotificationContent {
let handle = INPersonHandle(value: senderKey, type: .unknown)
let displayName = senderName.isEmpty ? "Rosetta" : senderName
let avatarImage = generateLetterAvatar(name: displayName, key: senderKey)
let sender = INPerson(
personHandle: handle,
nameComponents: nil,
displayName: senderName.isEmpty ? "Rosetta" : senderName,
image: nil,
displayName: displayName,
image: avatarImage,
contactIdentifier: nil,
customIdentifier: senderKey
)
@@ -237,6 +246,11 @@ final class NotificationService: UNNotificationServiceExtension {
attachments: nil
)
// Set avatar on sender parameter (Telegram parity: 50x50 letter avatar).
if let avatarImage {
intent.setImage(avatarImage, forParameterNamed: \.sender)
}
// Donate the intent so Siri can learn communication patterns.
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
@@ -254,6 +268,68 @@ final class NotificationService: UNNotificationServiceExtension {
}
}
// MARK: - Letter Avatar (Telegram parity: colored circle with initials)
/// Mantine avatar color palette matches AvatarView in main app.
private static let avatarColors: [(bg: UInt32, text: UInt32)] = [
(0x4C6EF5, 0xDBE4FF), // indigo
(0x7950F2, 0xE5DBFF), // violet
(0xF06595, 0xFFDEEB), // pink
(0xFF6B6B, 0xFFE3E3), // red
(0xFD7E14, 0xFFE8CC), // orange
(0xFAB005, 0xFFF3BF), // yellow
(0x40C057, 0xD3F9D8), // green
(0x12B886, 0xC3FAE8), // teal
(0x15AABF, 0xC5F6FA), // cyan
(0x228BE6, 0xD0EBFF), // blue
(0xBE4BDB, 0xF3D9FA), // grape
]
/// Generates a 50x50 circular letter avatar as INImage for notification display.
private static func generateLetterAvatar(name: String, key: String) -> INImage? {
let size: CGFloat = 50
let colorIndex = abs(key.hashValue) % avatarColors.count
let colors = avatarColors[colorIndex]
let initial = String(name.prefix(1)).uppercased()
UIGraphicsBeginImageContextWithOptions(CGSize(width: size, height: size), false, 2.0)
guard let ctx = UIGraphicsGetCurrentContext() else { return nil }
// Background circle.
let bgColor = UIColor(
red: CGFloat((colors.bg >> 16) & 0xFF) / 255,
green: CGFloat((colors.bg >> 8) & 0xFF) / 255,
blue: CGFloat(colors.bg & 0xFF) / 255,
alpha: 1
)
ctx.setFillColor(bgColor.cgColor)
ctx.fillEllipse(in: CGRect(x: 0, y: 0, width: size, height: size))
// Initial letter.
let textColor = UIColor(
red: CGFloat((colors.text >> 16) & 0xFF) / 255,
green: CGFloat((colors.text >> 8) & 0xFF) / 255,
blue: CGFloat(colors.text & 0xFF) / 255,
alpha: 1
)
let font = UIFont.systemFont(ofSize: size * 0.38, weight: .bold)
let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor]
let textSize = (initial as NSString).size(withAttributes: attrs)
let textRect = CGRect(
x: (size - textSize.width) / 2,
y: (size - textSize.height) / 2,
width: textSize.width,
height: textSize.height
)
(initial as NSString).draw(in: textRect, withAttributes: attrs)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let pngData = image?.pngData() else { return nil }
return INImage(imageData: pngData)
}
// MARK: - Helpers
/// Android parity: extract sender key from multiple possible key names.