Фикс: сделал subtitle в списке чатов и текст in-app баннера в одну строку с truncate

This commit is contained in:
2026-04-14 18:36:13 +05:00
parent 69ac9cd270
commit 400538bf2a
40 changed files with 2482 additions and 1409 deletions

View File

@@ -446,8 +446,17 @@ final class NotificationService: UNNotificationServiceExtension {
return Int(index)
}
/// Single-letter initial for group avatars.
/// Copy of RosettaColors.groupInitial(name:publicKey:) from Colors.swift.
private static func groupInitial(name: String, publicKey: String) -> String {
let trimmed = name.trimmingCharacters(in: .whitespaces)
if let first = trimmed.first { return String(first).uppercased() }
if !publicKey.isEmpty { return String(publicKey.prefix(1)).uppercased() }
return "?"
}
/// Desktop parity: 2-letter initials from display name.
/// Exact copy of RosettaColors.initials(name:publicKey:) from Colors.swift:209-223.
/// Exact copy of RosettaColors.initials(name:publicKey:) from Colors.swift.
private static func initials(name: String, publicKey: String) -> String {
let words = name.trimmingCharacters(in: .whitespaces)
.split(whereSeparator: { $0.isWhitespace })
@@ -525,16 +534,15 @@ final class NotificationService: UNNotificationServiceExtension {
return image
}
/// Generates a 50x50 group avatar with person.2.fill icon on solid tint circle.
/// Matches ChatRowView.swift:99-106 (group avatar without photo).
/// Generates a 50x50 group avatar with single-letter initial on Mantine light circle.
private static func generateGroupAvatar(name: String, key: String) -> INImage? {
let size: CGFloat = 50
let colorIndex = avatarColorIndex(for: name, publicKey: key)
let colors = avatarColors[colorIndex]
let text = groupInitial(name: name, publicKey: key)
// Try 2.0 scale first; fallback to 1.0 if NSE memory is constrained.
let image = renderGroupAvatar(size: size, tintHex: colors.tint, scale: 2.0)
?? renderGroupAvatar(size: size, tintHex: colors.tint, scale: 1.0)
let image = renderLetterAvatar(size: size, colors: colors, text: text, scale: 2.0)
?? renderLetterAvatar(size: size, colors: colors, text: text, scale: 1.0)
guard let pngData = image?.pngData() else { return nil }
if let tempURL = storeTemporaryImage(data: pngData, key: "group-\(key)", fileExtension: "png") {
@@ -543,33 +551,6 @@ final class NotificationService: UNNotificationServiceExtension {
return INImage(imageData: pngData)
}
/// Renders group avatar at given scale. Returns nil if UIGraphics context can't be allocated.
private static func renderGroupAvatar(size: CGFloat, tintHex: UInt32, scale: CGFloat) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(CGSize(width: size, height: size), false, scale)
guard UIGraphicsGetCurrentContext() != nil else { return nil }
let rect = CGRect(x: 0, y: 0, width: size, height: size)
uiColor(hex: tintHex).setFill()
UIBezierPath(ovalIn: rect).fill()
let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .medium)
if let symbol = UIImage(systemName: "person.2.fill", withConfiguration: config)?
.withTintColor(.white.withAlphaComponent(0.9), renderingMode: .alwaysOriginal) {
let symbolSize = symbol.size
let symbolRect = CGRect(
x: (size - symbolSize.width) / 2,
y: (size - symbolSize.height) / 2,
width: symbolSize.width,
height: symbolSize.height
)
symbol.draw(in: symbolRect)
}
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
/// Loads sender avatar from shared App Group cache written by the main app.
/// Falls back to letter avatar when no real image is available.
private static func loadNotificationAvatar(for senderKey: String) -> INImage? {