TM-SGNL-iOS/SignalServiceKit/Util/Avatars/AvatarModel.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

285 lines
7.8 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public struct AvatarModel: Equatable {
public let identifier: String
public var type: AvatarType
public var theme: AvatarTheme
public init(identifier: String? = nil, type: AvatarType, theme: AvatarTheme) {
if let identifier = identifier {
if case .icon(let icon) = type { owsAssertDebug(identifier == icon.rawValue) }
self.identifier = identifier
} else {
switch type {
case .icon(let icon): self.identifier = icon.rawValue
default: self.identifier = UUID().uuidString
}
}
self.type = type
self.theme = theme
}
}
public enum AvatarType: Equatable {
case image(URL)
case icon(AvatarIcon)
case text(String)
public var isEditable: Bool {
switch self {
case .image: return false
case .icon: return true
case .text: return true
}
}
public var isDeletable: Bool {
switch self {
case .image: return true
case .icon: return false
case .text: return true
}
}
public static func == (lhs: AvatarType, rhs: AvatarType) -> Bool {
switch (lhs, rhs) {
case (.image(let lhs), .image(let rhs)):
// We implement a custom "Equatable", since two URLs
// are equatated with object equality, and even with
// the same URL treated as different objects with the
// compiler synthesized function.
return lhs.path == rhs.path
case (.icon(let lhs), .icon(let rhs)):
return lhs == rhs
case (.text(let lhs), .text(let rhs)):
return lhs == rhs
default: return false
}
}
}
public enum AvatarIcon: String, CaseIterable {
case abstract01
case abstract02
case abstract03
case cat
case dinosaur
case dog
case fox
case ghost
case incognito
case pig
case sloth
case tucan
case heart
case house
case melon
case drink
case celebration
case balloon
case book
case briefcase
case sunset
case surfboard
case soccerball
case football
public var image: UIImage { UIImage(named: imageName)! }
public var imageName: String { "avatar_\(rawValue)"}
// todo: real names / final icons
public static var defaultGroupIcons: [AvatarIcon] = [
.heart,
.house,
.melon,
.drink,
.celebration,
.balloon,
.book,
.briefcase,
.sunset,
.surfboard,
.soccerball,
.football
]
public static var defaultProfileIcons: [AvatarIcon] = [
.abstract01,
.abstract02,
.abstract03,
.cat,
.dog,
.fox,
.tucan,
.sloth,
.dinosaur,
.pig,
.incognito,
.ghost
]
}
public enum AvatarTheme: String, CaseIterable {
case A100
case A110
case A120
case A130
case A140
case A150
case A160
case A170
case A180
case A190
case A200
case A210
public var foregroundColor: UIColor {
switch self {
case .A100: return UIColor(rgbHex: 0x3838F5)
case .A110: return UIColor(rgbHex: 0x1251D3)
case .A120: return UIColor(rgbHex: 0x086DA0)
case .A130: return UIColor(rgbHex: 0x067906)
case .A140: return UIColor(rgbHex: 0x661AFF)
case .A150: return UIColor(rgbHex: 0x9F00F0)
case .A160: return UIColor(rgbHex: 0xB8057C)
case .A170: return UIColor(rgbHex: 0xBE0404)
case .A180: return UIColor(rgbHex: 0x836B01)
case .A190: return UIColor(rgbHex: 0x7D6F40)
case .A200: return UIColor(rgbHex: 0x4F4F6D)
case .A210: return UIColor(rgbHex: 0x5C5C5C)
}
}
public var backgroundColor: UIColor {
switch self {
case .A100: return UIColor(rgbHex: 0xE3E3FE)
case .A110: return UIColor(rgbHex: 0xDDE7FC)
case .A120: return UIColor(rgbHex: 0xD8E8F0)
case .A130: return UIColor(rgbHex: 0xCDE4CD)
case .A140: return UIColor(rgbHex: 0xEAE0FD)
case .A150: return UIColor(rgbHex: 0xF5E3FE)
case .A160: return UIColor(rgbHex: 0xF6D8EC)
case .A170: return UIColor(rgbHex: 0xF5D7D7)
case .A180: return UIColor(rgbHex: 0xFEF5D0)
case .A190: return UIColor(rgbHex: 0xEAE6D5)
case .A200: return UIColor(rgbHex: 0xD2D2DC)
case .A210: return UIColor(rgbHex: 0xD7D7D9)
}
}
}
// MARK: - Avatar Colors
public extension AvatarTheme {
static var `default`: AvatarTheme { .A100 }
static func forThread(_ thread: TSThread) -> AvatarTheme {
if let contactThread = thread as? TSContactThread {
return forAddress(contactThread.contactAddress)
} else if let groupThread = thread as? TSGroupThread {
return forGroupId(groupThread.groupId)
} else {
owsFailDebug("Invalid thread.")
return Self.default
}
}
static func forGroupModel(_ groupModel: TSGroupModel) -> AvatarTheme {
forGroupId(groupModel.groupId)
}
static func forGroupId(_ groupId: Data) -> AvatarTheme {
forData(groupId)
}
static func forAddress(_ address: SignalServiceAddress) -> AvatarTheme {
guard let seed = address.serviceIdentifier else {
owsFailDebug("Missing serviceIdentifier.")
return Self.default
}
return forSeed(seed)
}
static func forSeed(_ seed: String) -> AvatarTheme {
guard let data = seed.data(using: .utf8) else {
owsFailDebug("Invalid seed.")
return Self.default
}
return forData(data)
}
static func forIcon(_ icon: AvatarIcon) -> AvatarTheme {
switch icon {
case .abstract01: return .A130
case .abstract02: return .A120
case .abstract03: return .A170
case .cat: return .A190
case .dog: return .A140
case .fox: return .A190
case .tucan: return .A120
case .sloth: return .A160
case .dinosaur: return .A130
case .pig: return .A180
case .incognito: return .A210
case .ghost: return .A100
case .heart: return .A180
case .house: return .A120
case .melon: return .A110
case .drink: return .A170
case .celebration: return .A100
case .balloon: return .A210
case .book: return .A100
case .briefcase: return .A180
case .sunset: return .A120
case .surfboard: return .A110
case .soccerball: return .A130
case .football: return .A210
}
}
static func forData(_ data: Data) -> AvatarTheme {
var hash: UInt = 0
for value in data {
// A primitive hashing function.
// We only require it to be stable and fast.
hash = hash.rotateLeft(3) ^ UInt(value)
}
let values = AvatarTheme.allCases
guard let value = values[safe: Int(hash % UInt(values.count))] else {
owsFailDebug("Could not determine avatar color.")
return Self.default
}
return value
}
}
// MARK: -
extension UInt {
public static let is64bit = { UInt.bitWidth == UInt64.bitWidth }()
public static let is32bit = { UInt.bitWidth == UInt32.bitWidth }()
public static let highestBit: UInt = {
if is32bit {
return UInt(1).rotateLeft(31)
} else if is64bit {
return UInt(1).rotateLeft(63)
} else {
owsFail("Unexpected UInt width.")
}
}()
// <<<
public func rotateLeft(_ count: Int) -> UInt {
let count = count % UInt.bitWidth
return (self << count) | (self >> (UInt.bitWidth - count))
}
}