285 lines
7.8 KiB
Swift
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))
|
|
}
|
|
}
|