Signal-iOS/SignalUI/Appearance/ChatColors.swift
2021-10-19 09:39:53 -03:00

755 lines
30 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalCoreKit
public struct ChatColor: Equatable, Codable {
public let id: String
public let setting: ColorOrGradientSetting
public let isBuiltIn: Bool
public let creationTimestamp: UInt64
public let updateTimestamp: UInt64
public init(id: String,
setting: ColorOrGradientSetting,
isBuiltIn: Bool = false,
creationTimestamp: UInt64 = NSDate.ows_millisecondTimeStamp()) {
self.id = id
self.setting = setting
self.isBuiltIn = isBuiltIn
self.creationTimestamp = creationTimestamp
self.updateTimestamp = NSDate.ows_millisecondTimeStamp()
}
public static var randomId: String {
UUID().uuidString
}
public static var placeholderValue: ChatColor {
ChatColors.defaultChatColor
}
// MARK: - Equatable
public static func == (lhs: ChatColor, rhs: ChatColor) -> Bool {
// Ignore timestamps, etc.
(lhs.id == rhs.id) && (lhs.setting == rhs.setting)
}
}
// MARK: -
@objc
public class ChatColors: NSObject, Dependencies {
@objc
public override init() {
super.init()
SwiftSingletons.register(self)
NotificationCenter.default.addObserver(
self,
selector: #selector(warmCaches),
name: .WarmCaches,
object: nil
)
}
// The cache should contain all current values at all times.
@objc
private func warmCaches() {
owsAssertDebug(GRDBSchemaMigrator.areMigrationsComplete)
guard CurrentAppContext().hasUI else { return }
var valueCache = [String: ChatColor]()
// Load built-in colors.
for value in Self.builtInValues {
guard valueCache[value.id] == nil else {
owsFailDebug("Duplicate value: \(value.id).")
continue
}
valueCache[value.id] = value
}
// Load custom colors.
Self.databaseStorage.read { transaction in
let keys = Self.customColorsStore.allKeys(transaction: transaction)
for key in keys {
func loadValue() -> ChatColor? {
do {
return try Self.customColorsStore.getCodableValue(forKey: key, transaction: transaction)
} catch {
owsFailDebug("Error: \(error)")
return nil
}
}
guard let value = loadValue() else {
owsFailDebug("Missing value: \(key)")
continue
}
guard valueCache[value.id] == nil else {
owsFailDebug("Duplicate value: \(value.id).")
continue
}
valueCache[value.id] = value
}
}
Self.unfairLock.withLock {
self.valueCache = valueCache
}
}
// Represents the current "chat color" setting for a given thread
// or the default. "Custom chat colors" have a lifecycle independent
// from the conversations/global defaults which use them.
//
// The keys in this store are thread unique ids _OR_ defaultKey (String).
// The values are ChatColor.id (String).
private static let chatColorSettingStore = SDSKeyValueStore(collection: "chatColorSettingStore")
// The keys in this store are ChatColor.id (String).
// The values are ChatColors.
private static let customColorsStore = SDSKeyValueStore(collection: "customColorsStore.3")
private static let defaultKey = "defaultKey"
private static let unfairLock = UnfairLock()
private var valueCache = [String: ChatColor]()
public func upsertCustomValue(_ value: ChatColor, transaction: SDSAnyWriteTransaction) {
owsAssertDebug(CurrentAppContext().hasUI)
Self.unfairLock.withLock {
self.valueCache[value.id] = value
do {
try Self.customColorsStore.setCodable(value, key: value.id, transaction: transaction)
} catch {
owsFailDebug("Error: \(error)")
}
}
transaction.addAsyncCompletionOffMain {
self.fireCustomChatColorsDidChange()
}
}
public func deleteCustomValue(_ value: ChatColor, transaction: SDSAnyWriteTransaction) {
owsAssertDebug(CurrentAppContext().hasUI)
Self.unfairLock.withLock {
self.valueCache.removeValue(forKey: value.id)
Self.customColorsStore.removeValue(forKey: value.id, transaction: transaction)
}
transaction.addAsyncCompletionOffMain {
self.fireCustomChatColorsDidChange()
}
}
#if DEBUG
@objc
public static func createFakeChatColors(transaction: SDSAnyWriteTransaction) {
func randomUnitCGFloat(precision: UInt32 = 256) -> CGFloat {
(CGFloat(arc4random_uniform(precision)) / CGFloat(precision)).clamp01()
}
func randomOWSColor() -> OWSColor {
OWSColor(red: randomUnitCGFloat(),
green: randomUnitCGFloat(),
blue: randomUnitCGFloat())
}
func randomAngleRadians() -> CGFloat {
CGFloat.pi * 2 * randomUnitCGFloat()
}
let count = 256
for _ in 0..<count {
let setting: ColorOrGradientSetting
if arc4random_uniform(2) == 0 {
setting = .solidColor(color: randomOWSColor())
} else {
setting = .gradient(gradientColor1: randomOWSColor(),
gradientColor2: randomOWSColor(),
angleRadians: randomAngleRadians())
}
let value = ChatColor(id: ChatColor.randomId, setting: setting)
Self.chatColors.upsertCustomValue(value, transaction: transaction)
}
}
#endif
private func fireCustomChatColorsDidChange() {
NotificationCenter.default.postNotificationNameAsync(
Self.customChatColorsDidChange,
object: nil,
userInfo: nil
)
}
private func fireAutoChatColorsDidChange() {
NotificationCenter.default.postNotificationNameAsync(
Self.autoChatColorsDidChange,
object: nil,
userInfo: nil
)
}
private func value(forValueId valueId: String) -> ChatColor? {
owsAssertDebug(CurrentAppContext().hasUI)
return Self.unfairLock.withLock {
self.valueCache[valueId]
}
}
private var allValues: [ChatColor] {
owsAssertDebug(CurrentAppContext().hasUI)
return Self.unfairLock.withLock {
Array(self.valueCache.values)
}
}
public var allValuesSorted: [ChatColor] {
allValues.sorted { (left, right) -> Bool in
left.creationTimestamp < right.creationTimestamp
}
}
public static var allValuesSorted: [ChatColor] { Self.chatColors.allValuesSorted }
public static var defaultChatColor: ChatColor { Values.ultramarine }
// Chat Color Precedence
//
// * If Conversation X has a chat color setting A
// * Use chat color A
// * If Conversation X has no chat color setting...
// * If Conversation X has a wallpaper B
// * Use default chat color for wallpaper B
// * If Conversation X has no wallpaper...
// * If there is a global chat color setting C
// * Use chat color C
// * (even if there is a global wallpaper)
// * If there is no global chat color setting...
// * If there is a global wallpaper D
// * Use default chat color for wallpaper D
// * If there is no global wallpaper...
// * Use default chat color "ultramarine gradient".
public static func autoChatColorForRendering(forThread thread: TSThread?,
ignoreGlobalDefault: Bool = false,
transaction: SDSAnyReadTransaction) -> ChatColor {
if let thread = thread {
let threadWallpaper = Wallpaper.wallpaperSetting(for: thread, transaction: transaction)
if let wallpaper = threadWallpaper,
wallpaper != .photo {
return autoChatColorForRendering(forWallpaper: wallpaper)
} else if !ignoreGlobalDefault,
let value = defaultChatColorSetting(transaction: transaction) {
// In the global settings, we want to ignore the global default
// when rendering the "auto" swatch.
return value
} else if nil == threadWallpaper,
let wallpaper = Wallpaper.wallpaperSetting(for: nil, transaction: transaction),
wallpaper != .photo {
return autoChatColorForRendering(forWallpaper: wallpaper)
} else {
return Self.defaultChatColor
}
} else {
if !ignoreGlobalDefault,
let value = defaultChatColorSetting(transaction: transaction) {
// In the global settings, we want to ignore the global default
// when rendering the "auto" swatch.
return value
} else if let wallpaper = Wallpaper.wallpaperSetting(for: nil, transaction: transaction),
wallpaper != .photo {
return autoChatColorForRendering(forWallpaper: wallpaper)
} else {
return Self.defaultChatColor
}
}
}
public static func autoChatColorForRendering(forWallpaper wallpaper: Wallpaper) -> ChatColor {
wallpaper.defaultChatColor
}
// Returns nil for default/auto.
public static func defaultChatColorSetting(transaction: SDSAnyReadTransaction) -> ChatColor? {
chatColorSetting(key: defaultKey, transaction: transaction)
}
public static func defaultChatColorForRendering(transaction: SDSAnyReadTransaction) -> ChatColor {
autoChatColorForRendering(forThread: nil, transaction: transaction)
}
public static func setDefaultChatColorSetting(_ value: ChatColor?,
transaction: SDSAnyWriteTransaction) {
setChatColorSetting(key: defaultKey, value: value, transaction: transaction)
}
// Returns nil for default/auto.
public static func chatColorSetting(thread: TSThread,
shouldHonorDefaultSetting: Bool,
transaction: SDSAnyReadTransaction) -> ChatColor? {
if let value = chatColorSetting(key: thread.uniqueId, transaction: transaction) {
return value
}
if shouldHonorDefaultSetting {
return ChatColors.defaultChatColorSetting(transaction: transaction)
} else {
return nil
}
}
public static func chatColorForRendering(thread: TSThread,
transaction: SDSAnyReadTransaction) -> ChatColor {
if let value = chatColorSetting(thread: thread,
shouldHonorDefaultSetting: true,
transaction: transaction) {
return value
} else {
return autoChatColorForRendering(forThread: thread, transaction: transaction)
}
}
public static func chatColorForRendering(address: SignalServiceAddress,
transaction: SDSAnyReadTransaction) -> ChatColor {
guard let thread = TSContactThread.getWithContactAddress(address, transaction: transaction) else {
return Self.defaultChatColor
}
return chatColorForRendering(thread: thread, transaction: transaction)
}
public static func setChatColorSetting(_ value: ChatColor?,
thread: TSThread,
transaction: SDSAnyWriteTransaction) {
setChatColorSetting(key: thread.uniqueId, value: value, transaction: transaction)
}
// Returns nil for default/auto.
private static func chatColorSetting(key: String,
transaction: SDSAnyReadTransaction) -> ChatColor? {
guard let valueId = Self.chatColorSettingStore.getString(key, transaction: transaction) else {
return nil
}
guard let value = Self.chatColors.value(forValueId: valueId) else {
// This isn't necessarily an error. A user might apply a custom
// chat color value to a conversation (or the global default),
// then delete the custom chat color value. In that case, all
// references to the value should behave as "auto" (the default).
Logger.warn("Missing value: \(valueId).")
return nil
}
return value
}
// Returns the number of conversations that use a given value.
public static func usageCount(forValue value: ChatColor,
transaction: SDSAnyReadTransaction) -> Int {
let keys = chatColorSettingStore.allKeys(transaction: transaction)
var count: Int = 0
for key in keys {
if value.id == Self.chatColorSettingStore.getString(key, transaction: transaction) {
count += 1
}
}
return count
}
public static let customChatColorsDidChange = NSNotification.Name("customChatColorsDidChange")
public static let autoChatColorsDidChange = NSNotification.Name("autoChatColorsDidChange")
public static let conversationChatColorSettingDidChange = NSNotification.Name("conversationChatColorSettingDidChange")
public static let conversationChatColorSettingDidChangeThreadUniqueIdKey = "conversationChatColorSettingDidChangeThreadUniqueIdKey"
private static func setChatColorSetting(key: String,
value: ChatColor?,
transaction: SDSAnyWriteTransaction) {
if let value = value {
// Ensure the value is already in the cache.
if nil == Self.chatColors.value(forValueId: value.id) {
owsFailDebug("Unknown value: \(value.id).")
}
Self.chatColorSettingStore.setString(value.id, key: key, transaction: transaction)
} else {
Self.chatColorSettingStore.removeValue(forKey: key, transaction: transaction)
}
transaction.addAsyncCompletionOffMain {
if key == defaultKey {
Self.chatColors.fireAutoChatColorsDidChange()
} else {
NotificationCenter.default.postNotificationNameAsync(
Self.conversationChatColorSettingDidChange,
object: nil,
userInfo: [
conversationChatColorSettingDidChangeThreadUniqueIdKey: key
]
)
}
}
}
public static func resetAllSettings(transaction: SDSAnyWriteTransaction) {
Self.chatColorSettingStore.removeAll(transaction: transaction)
}
// MARK: -
public class Values {
@available(*, unavailable, message: "Do not instantiate this class.")
private init() {}
// Default Gradient
static let ultramarine = ChatColor(
id: "Ultramarine",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0x0552F0).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x2C6BED).asOWSColor,
angleRadians: CGFloat.pi * 0),
isBuiltIn: true,
creationTimestamp: 0
)
// Solid Colors
static let crimson = ChatColor(
id: "Crimson",
setting: .solidColor(color: UIColor(rgbHex: 0xCF163E).asOWSColor),
isBuiltIn: true,
creationTimestamp: 1
)
static let vermilion = ChatColor(
id: "Vermilion",
setting: .solidColor(color: UIColor(rgbHex: 0xC73F0A).asOWSColor),
isBuiltIn: true,
creationTimestamp: 2
)
static let burlap = ChatColor(
id: "Burlap",
setting: .solidColor(color: UIColor(rgbHex: 0x6F6A58).asOWSColor),
isBuiltIn: true,
creationTimestamp: 3
)
static let forest = ChatColor(
id: "Forest",
setting: .solidColor(color: UIColor(rgbHex: 0x3B7845).asOWSColor),
isBuiltIn: true,
creationTimestamp: 4
)
static let wintergreen = ChatColor(
id: "Wintergreen",
setting: .solidColor(color: UIColor(rgbHex: 0x1D8663).asOWSColor),
isBuiltIn: true,
creationTimestamp: 5
)
static let teal = ChatColor(
id: "Teal",
setting: .solidColor(color: UIColor(rgbHex: 0x077D92).asOWSColor),
isBuiltIn: true,
creationTimestamp: 6
)
static let blue = ChatColor(
id: "Blue",
setting: .solidColor(color: UIColor(rgbHex: 0x336BA3).asOWSColor),
isBuiltIn: true,
creationTimestamp: 7
)
static let indigo = ChatColor(
id: "Indigo",
setting: .solidColor(color: UIColor(rgbHex: 0x6058CA).asOWSColor),
isBuiltIn: true,
creationTimestamp: 8
)
static let violet = ChatColor(
id: "Violet",
setting: .solidColor(color: UIColor(rgbHex: 0x9932C8).asOWSColor),
isBuiltIn: true,
creationTimestamp: 9
)
static let plum = ChatColor(
id: "Plum",
setting: .solidColor(color: UIColor(rgbHex: 0xAA377A).asOWSColor),
isBuiltIn: true,
creationTimestamp: 10
)
static let taupe = ChatColor(
id: "Taupe",
setting: .solidColor(color: UIColor(rgbHex: 0x8F616A).asOWSColor),
isBuiltIn: true,
creationTimestamp: 11
)
static let steel = ChatColor(
id: "Steel",
setting: .solidColor(color: UIColor(rgbHex: 0x71717F).asOWSColor),
isBuiltIn: true,
creationTimestamp: 12
)
// Gradients
static let ember = ChatColor(
id: "Ember",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0xE57C00).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x5E0000).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(168)),
isBuiltIn: true,
creationTimestamp: 13
)
static let midnight = ChatColor(
id: "Midnight",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0x2C2C3A).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x787891).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(180)),
isBuiltIn: true,
creationTimestamp: 14
)
static let infrared = ChatColor(
id: "Infrared",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0xF65560).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x442CED).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(192)),
isBuiltIn: true,
creationTimestamp: 15
)
static let lagoon = ChatColor(
id: "Lagoon",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0x004066).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x32867D).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(180)),
isBuiltIn: true,
creationTimestamp: 16
)
static let fluorescent = ChatColor(
id: "Fluorescent",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0xEC13DD).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x1B36C6).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(192)),
isBuiltIn: true,
creationTimestamp: 17
)
static let basil = ChatColor(
id: "Basil",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0x2F9373).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x077343).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(180)),
isBuiltIn: true,
creationTimestamp: 18
)
static let sublime = ChatColor(
id: "Sublime",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0x6281D5).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x974460).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(180)),
isBuiltIn: true,
creationTimestamp: 19
)
static let sea = ChatColor(
id: "Sea",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0x498FD4).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x2C66A0).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(180)),
isBuiltIn: true,
creationTimestamp: 20
)
static let tangerine = ChatColor(
id: "Tangerine",
setting: .gradient(gradientColor1: UIColor(rgbHex: 0xDB7133).asOWSColor,
gradientColor2: UIColor(rgbHex: 0x911231).asOWSColor,
angleRadians: parseAngleDegreesFromSpec(192)),
isBuiltIn: true,
creationTimestamp: 21
)
private static func parseAngleDegreesFromSpec(_ angleDegreesFromSpec: CGFloat) -> CGFloat {
// In our models:
// If angleRadians = 0, gradientColor1 is N.
// If angleRadians = PI / 2, gradientColor1 is E.
// etc.
//
// In the spec:
// If angleDegrees = 180, gradientColor1 is N.
// If angleDegrees = 270, gradientColor1 is E.
// etc.
return ((angleDegreesFromSpec - 180) / 180) * CGFloat.pi
}
}
private static var builtInValues: [ChatColor] {
return [
// We use fixed timestamps to ensure that built-in values
// appear before custom values and to control their relative ordering.
// Default Gradient
Values.ultramarine,
// Solid Colors
Values.crimson,
Values.vermilion,
Values.burlap,
Values.forest,
Values.wintergreen,
Values.teal,
Values.blue,
Values.indigo,
Values.violet,
Values.plum,
Values.taupe,
Values.steel,
// Gradients
Values.ember,
Values.midnight,
Values.infrared,
Values.lagoon,
Values.fluorescent,
Values.basil,
Values.sublime,
Values.sea,
Values.tangerine
]
}
}
// MARK: -
public extension ChatColors {
// Represents the "message sender" to "group name color" mapping
// for a given CVC load.
struct GroupNameColors {
fileprivate let colorMap: [SignalServiceAddress: UIColor]
// TODO: Pending design.
fileprivate let defaultColor: UIColor
public func color(for address: SignalServiceAddress) -> UIColor {
colorMap[address] ?? defaultColor
}
fileprivate static var defaultColors: GroupNameColors {
GroupNameColors(colorMap: [:], defaultColor: Theme.primaryTextColor)
}
}
static func groupNameColors(forThread thread: TSThread) -> GroupNameColors {
guard let groupThread = thread as? TSGroupThread else {
return .defaultColors
}
let groupMembership = groupThread.groupMembership
let values = Self.groupNameColorValues
let isDarkThemeEnabled = Theme.isDarkThemeEnabled
var lastIndex: Int = 0
var colorMap = [SignalServiceAddress: UIColor]()
let addresses = Array(groupMembership.fullMembers).stableSort()
for (index, address) in addresses.enumerated() {
let valueIndex = index % values.count
guard let value = values[safe: valueIndex] else {
owsFailDebug("Invalid values.")
return .defaultColors
}
colorMap[address] = value.color(isDarkThemeEnabled: isDarkThemeEnabled)
lastIndex = index
}
let defaultValueIndex = (lastIndex + 1) % values.count
guard let defaultValue = values[safe: defaultValueIndex] else {
owsFailDebug("Invalid values.")
return .defaultColors
}
let defaultColor = defaultValue.color(isDarkThemeEnabled: isDarkThemeEnabled)
return GroupNameColors(colorMap: colorMap, defaultColor: defaultColor)
}
private static var defaultGroupNameColor: UIColor {
let isDarkThemeEnabled = Theme.isDarkThemeEnabled
return Self.groupNameColorValues.first!.color(isDarkThemeEnabled: isDarkThemeEnabled)
}
fileprivate struct GroupNameColorValue {
let lightTheme: UIColor
let darkTheme: UIColor
func color(isDarkThemeEnabled: Bool) -> UIColor {
isDarkThemeEnabled ? darkTheme : lightTheme
}
}
// In descending order of contrast with the other values.
fileprivate static let groupNameColorValues: [GroupNameColorValue] = [
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x006DA3),
darkTheme: UIColor(rgbHex: 0x00A7FA)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x007A3D),
darkTheme: UIColor(rgbHex: 0x00B85C)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xC13215),
darkTheme: UIColor(rgbHex: 0xFF6F52)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xB814B8),
darkTheme: UIColor(rgbHex: 0xF65AF6)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x5B6976),
darkTheme: UIColor(rgbHex: 0x8BA1B6)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x3D7406),
darkTheme: UIColor(rgbHex: 0x5EB309)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xCC0066),
darkTheme: UIColor(rgbHex: 0xF76EB2)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x2E51FF),
darkTheme: UIColor(rgbHex: 0x8599FF)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x9C5711),
darkTheme: UIColor(rgbHex: 0xD5920B)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x007575),
darkTheme: UIColor(rgbHex: 0x00B2B2)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xD00B4D),
darkTheme: UIColor(rgbHex: 0xFF6B9C)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x8F2AF4),
darkTheme: UIColor(rgbHex: 0xBF80FF)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xD00B0B),
darkTheme: UIColor(rgbHex: 0xFF7070)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x067906),
darkTheme: UIColor(rgbHex: 0x0AB80A)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x5151F6),
darkTheme: UIColor(rgbHex: 0x9494FF)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x866118),
darkTheme: UIColor(rgbHex: 0xD68F00)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x067953),
darkTheme: UIColor(rgbHex: 0x00B87A)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xA20CED),
darkTheme: UIColor(rgbHex: 0xCF7CF8)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x4B7000),
darkTheme: UIColor(rgbHex: 0x74AD00)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xC70A88),
darkTheme: UIColor(rgbHex: 0xF76EC9)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xB34209),
darkTheme: UIColor(rgbHex: 0xF57A3D)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x06792D),
darkTheme: UIColor(rgbHex: 0x0AB844)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x7A3DF5),
darkTheme: UIColor(rgbHex: 0xAF8AF9)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x6B6B24),
darkTheme: UIColor(rgbHex: 0xA4A437)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xD00B2C),
darkTheme: UIColor(rgbHex: 0xF77389)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x2D7906),
darkTheme: UIColor(rgbHex: 0x42B309)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xAF0BD0),
darkTheme: UIColor(rgbHex: 0xE06EF7)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x32763E),
darkTheme: UIColor(rgbHex: 0x4BAF5C)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x2662D9),
darkTheme: UIColor(rgbHex: 0x7DA1E8)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x76681E),
darkTheme: UIColor(rgbHex: 0xB89B0A)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x067462),
darkTheme: UIColor(rgbHex: 0x09B397)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x6447F5),
darkTheme: UIColor(rgbHex: 0xA18FF9)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x5E6E0C),
darkTheme: UIColor(rgbHex: 0x8FAA09)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x077288),
darkTheme: UIColor(rgbHex: 0x00AED1)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0xC20AA3),
darkTheme: UIColor(rgbHex: 0xF75FDD)),
GroupNameColorValue(lightTheme: UIColor(rgbHex: 0x2D761E),
darkTheme: UIColor(rgbHex: 0x43B42D))
]
}