| 1 | import Foundation |
| 2 | #if canImport(UIKit) |
| 3 | import UIKit |
| 4 | #elseif canImport(AppKit) |
| 5 | import AppKit |
| 6 | #endif |
| 7 | |
| 8 | // The initials fallback for a notification's sender image: without one, a |
| 9 | // communication notification shows the system's gray silhouette — so senders |
| 10 | // with no profile photo get the SAME monogram the app draws for them (Avatar |
| 11 | // on iOS, the Mac's circles): first initial, white, over a colour hashed from |
| 12 | // the name. Keep initial(for:)/paletteIndex(for:) in lockstep with |
| 13 | // ios/LChat/Views/Avatar.swift so a person's banner face matches their in-app |
| 14 | // face. Rendered square — the notification pipeline circle-crops it. |
| 15 | public enum MonogramImage { |
| 16 | /// The uppercased first character, "?" for an all-whitespace name — |
| 17 | /// Avatar.initial verbatim. |
| 18 | static func initial(for name: String) -> String { |
| 19 | let s = String(name.trimmingCharacters(in: .whitespaces).prefix(1)).uppercased() |
| 20 | return s.isEmpty ? "?" : s |
| 21 | } |
| 22 | |
| 23 | /// djb2 over the name's UTF-8, mod the 8-colour palette — Avatar.color verbatim. |
| 24 | static func paletteIndex(for name: String) -> Int { |
| 25 | var h = 5381 |
| 26 | for b in name.utf8 { h = (h &* 33) &+ Int(b) } |
| 27 | return abs(h) % 8 |
| 28 | } |
| 29 | |
| 30 | #if canImport(UIKit) || canImport(AppKit) |
| 31 | // The concrete twins of Avatar's SwiftUI palette (.blue, .purple, …). |
| 32 | #if canImport(UIKit) |
| 33 | private static let palette: [UIColor] = [ |
| 34 | .systemBlue, .systemPurple, .systemPink, .systemOrange, |
| 35 | .systemGreen, .systemTeal, .systemIndigo, .systemRed, |
| 36 | ] |
| 37 | #else |
| 38 | private static let palette: [NSColor] = [ |
| 39 | .systemBlue, .systemPurple, .systemPink, .systemOrange, |
| 40 | .systemGreen, .systemTeal, .systemIndigo, .systemRed, |
| 41 | ] |
| 42 | #endif |
| 43 | |
| 44 | /// PNG bytes of the monogram tile, or nil if rendering fails. |
| 45 | public static func pngData(name: String, size: CGFloat = 256) -> Data? { |
| 46 | let color = palette[paletteIndex(for: name)] |
| 47 | let text = initial(for: name) as NSString |
| 48 | let rect = CGRect(x: 0, y: 0, width: size, height: size) |
| 49 | #if canImport(UIKit) |
| 50 | let attrs: [NSAttributedString.Key: Any] = [ |
| 51 | .font: UIFont.systemFont(ofSize: size * 0.42, weight: .semibold), |
| 52 | .foregroundColor: UIColor.white, |
| 53 | ] |
| 54 | let img = UIGraphicsImageRenderer(size: rect.size).image { ctx in |
| 55 | color.setFill() |
| 56 | ctx.fill(rect) |
| 57 | let s = text.size(withAttributes: attrs) |
| 58 | text.draw(at: CGPoint(x: (size - s.width) / 2, y: (size - s.height) / 2), |
| 59 | withAttributes: attrs) |
| 60 | } |
| 61 | return img.pngData() |
| 62 | #else |
| 63 | let attrs: [NSAttributedString.Key: Any] = [ |
| 64 | .font: NSFont.systemFont(ofSize: size * 0.42, weight: .semibold), |
| 65 | .foregroundColor: NSColor.white, |
| 66 | ] |
| 67 | let img = NSImage(size: rect.size) |
| 68 | img.lockFocus() |
| 69 | color.setFill() |
| 70 | rect.fill() |
| 71 | let s = text.size(withAttributes: attrs) |
| 72 | text.draw(at: CGPoint(x: (size - s.width) / 2, y: (size - s.height) / 2), |
| 73 | withAttributes: attrs) |
| 74 | img.unlockFocus() |
| 75 | guard let tiff = img.tiffRepresentation, |
| 76 | let rep = NSBitmapImageRep(data: tiff) else { return nil } |
| 77 | return rep.representation(using: .png, properties: [:]) |
| 78 | #endif |
| 79 | } |
| 80 | #endif |
| 81 | } |
| 82 | |