mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			130 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Swift
		
	
		
		
			
		
	
	
			130 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Swift
		
	
| 
											3 years ago
										 | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | ||
|  | 
 | ||
|  | import UIKit | ||
| 
											3 years ago
										 | import CryptoKit | ||
|  | import SessionUtilitiesKit | ||
| 
											5 years ago
										 | 
 | ||
|  | public class PlaceholderIcon { | ||
| 
											2 years ago
										 |     private static let placeholderCache: Atomic<NSCache<NSString, UIImage>> = { | ||
|  |         let result = NSCache<NSString, UIImage>() | ||
|  |         result.countLimit = 50 | ||
|  |          | ||
|  |         return Atomic(result) | ||
|  |     }() | ||
|  |      | ||
| 
											5 years ago
										 |     private let seed: Int | ||
| 
											3 years ago
										 |      | ||
| 
											5 years ago
										 |     // Colour palette | ||
| 
											3 years ago
										 |     private var colors: [UIColor] = Theme.PrimaryColor.allCases.map { $0.color } | ||
| 
											5 years ago
										 |      | ||
| 
											2 years ago
										 |     // MARK: - Initialization | ||
|  |      | ||
| 
											3 years ago
										 |     init(seed: Int, colors: [UIColor]? = nil) { | ||
| 
											5 years ago
										 |         self.seed = seed | ||
| 
											3 years ago
										 |         if let colors = colors { self.colors = colors } | ||
| 
											5 years ago
										 |     } | ||
|  |      | ||
| 
											3 years ago
										 |     convenience init(seed: String, colors: [UIColor]? = nil) { | ||
| 
											5 years ago
										 |         // Ensure we have a correct hash | ||
|  |         var hash = seed | ||
| 
											3 years ago
										 |          | ||
|  |         if (hash.matches("^[0-9A-Fa-f]+$") && hash.count >= 12) { | ||
|  |             hash = SHA512.hash(data: Data(seed.bytes)).hexString | ||
|  |         } | ||
| 
											5 years ago
										 |          | ||
|  |         guard let number = Int(hash.substring(to: 12), radix: 16) else { | ||
| 
											2 years ago
										 |             SNLog("Failed to generate number from seed string: \(seed).") | ||
| 
											3 years ago
										 |             self.init(seed: 0, colors: colors) | ||
| 
											5 years ago
										 |             return | ||
|  |         } | ||
|  |          | ||
| 
											3 years ago
										 |         self.init(seed: number, colors: colors) | ||
| 
											5 years ago
										 |     } | ||
|  |      | ||
| 
											2 years ago
										 |     // MARK: - Convenience | ||
|  |      | ||
|  |     public static func generate(seed: String, text: String, size: CGFloat) -> UIImage { | ||
|  |         let icon = PlaceholderIcon(seed: seed) | ||
|  |          | ||
|  |         var content: String = (text.hasSuffix("\(String(seed.suffix(4))))") ? | ||
|  |             (text.split(separator: "(") | ||
|  |                 .first | ||
|  |                 .map { String($0) }) | ||
|  |                 .defaulting(to: text) : | ||
|  |                 text | ||
|  |         ) | ||
|  | 
 | ||
|  |         if content.count > 2 && SessionId.Prefix(from: content) != nil { | ||
|  |             content.removeFirst(2) | ||
|  |         } | ||
|  |          | ||
|  |         let initials: String = content | ||
|  |             .split(separator: " ") | ||
|  |             .compactMap { word in word.first.map { String($0) } } | ||
|  |             .joined() | ||
|  |         let cacheKey: String = "\(content)-\(Int(floor(size)))" | ||
|  |          | ||
|  |         if let cachedIcon: UIImage = placeholderCache.wrappedValue.object(forKey: cacheKey as NSString) { | ||
|  |             return cachedIcon | ||
|  |         } | ||
|  |          | ||
|  |         let layer = icon.generateLayer( | ||
|  |             with: size, | ||
|  |             text: (initials.count >= 2 ? | ||
|  |                 initials.substring(to: 2).uppercased() : | ||
|  |                 content.substring(to: 2).uppercased() | ||
|  |             ) | ||
|  |         ) | ||
|  |          | ||
|  |         let rect = CGRect(origin: CGPoint.zero, size: layer.frame.size) | ||
|  |         let renderer = UIGraphicsImageRenderer(size: rect.size) | ||
|  |         let result = renderer.image { layer.render(in: $0.cgContext) } | ||
|  |          | ||
|  |         placeholderCache.mutate { $0.setObject(result, forKey: cacheKey as NSString) } | ||
|  |          | ||
|  |         return result | ||
|  |     } | ||
|  |      | ||
|  |     // MARK: - Internal | ||
|  |      | ||
|  |     private func generateLayer(with diameter: CGFloat, text: String) -> CALayer { | ||
| 
											3 years ago
										 |         let color: UIColor = self.colors[seed % self.colors.count] | ||
|  |         let base: CALayer = getTextLayer(with: diameter, color: color, text: text) | ||
| 
											5 years ago
										 |         base.masksToBounds = true | ||
| 
											3 years ago
										 |          | ||
| 
											5 years ago
										 |         return base | ||
|  |     } | ||
|  |      | ||
| 
											3 years ago
										 |     private func getTextLayer(with diameter: CGFloat, color: UIColor, text: String) -> CALayer { | ||
| 
											5 years ago
										 |         let font = UIFont.boldSystemFont(ofSize: diameter / 2) | ||
|  |         let height = NSString(string: text).boundingRect(with: CGSize(width: diameter, height: CGFloat.greatestFiniteMagnitude), | ||
|  |             options: .usesLineFragmentOrigin, attributes: [ NSAttributedString.Key.font : font ], context: nil).height | ||
|  |         let frame = CGRect(x: 0, y: (diameter - height) / 2, width: diameter, height: height) | ||
| 
											5 years ago
										 |          | ||
|  |         let layer = CATextLayer() | ||
|  |         layer.frame = frame | ||
| 
											3 years ago
										 |         layer.themeForegroundColorForced = .color(.white) | ||
| 
											5 years ago
										 |         layer.contentsScale = UIScreen.main.scale | ||
|  |          | ||
| 
											5 years ago
										 |         let fontName = font.fontName | ||
|  |         let fontRef = CGFont(fontName as CFString) | ||
|  |         layer.font = fontRef | ||
|  |         layer.fontSize = font.pointSize | ||
| 
											5 years ago
										 |         layer.alignmentMode = .center | ||
|  |         layer.string = text | ||
|  |          | ||
|  |         let base = CALayer() | ||
|  |         base.frame = CGRect(x: 0, y: 0, width: diameter, height: diameter) | ||
| 
											3 years ago
										 |         base.themeBackgroundColorForced = .color(color) | ||
| 
											5 years ago
										 |         base.addSublayer(layer) | ||
|  |          | ||
|  |         return base | ||
|  |     } | ||
|  | } | ||
| 
											5 years ago
										 | 
 | ||
|  | private extension String { | ||
|  |     func matches(_ regex: String) -> Bool { | ||
|  |         return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil | ||
|  |     } | ||
|  | } |