diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index f10fe677d..0499016f8 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 241C6314231F64C000B4198E /* JazzIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C630E231F5AAC00B4198E /* JazzIcon.swift */; }; + 241C6315231F64CE00B4198E /* CGFloat+Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */; }; + 241C6316231F64CE00B4198E /* UIColor+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 241C6310231F5C4400B4198E /* UIColor+Helper.swift */; }; 24A830A22293CD0100F4CAC0 /* LokiP2PServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */; }; 2AE2882E4C2B96BFFF9EE27C /* Pods_SignalShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0F94C85CB0B235DA37F68ED0 /* Pods_SignalShareExtension.framework */; }; 3403B95D20EA9527001A1F44 /* OWSContactShareButtonsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */; }; @@ -660,6 +663,9 @@ 0F94C85CB0B235DA37F68ED0 /* Pods_SignalShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1C93CF3971B64E8B6C1F9AC1 /* Pods-SignalShareExtension.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.test.xcconfig"; sourceTree = ""; }; 1CE3CD5C23334683BDD3D78C /* Pods-Signal.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.test.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.test.xcconfig"; sourceTree = ""; }; + 241C630E231F5AAC00B4198E /* JazzIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JazzIcon.swift; sourceTree = ""; }; + 241C6310231F5C4400B4198E /* UIColor+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Helper.swift"; sourceTree = ""; }; + 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGFloat+Rounding.swift"; sourceTree = ""; }; 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LokiP2PServer.swift; sourceTree = ""; }; 264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalMessaging.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareButtonsView.m; sourceTree = ""; }; @@ -1652,6 +1658,7 @@ 4C948FF62146EB4800349F0D /* BlockListCache.swift */, 343D3D991E9283F100165CA4 /* BlockListUIUtils.h */, 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */, + 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */, 3466087120E550F300AFFE73 /* ConversationStyle.swift */, 34480B4D1FD0A7A300BC14EF /* DebugLogger.h */, 34480B4E1FD0A7A300BC14EF /* DebugLogger.m */, @@ -1684,6 +1691,7 @@ 45360B8C1F9521F800FA666C /* Searcher.swift */, 346129BD1FD2068600532771 /* ThreadUtil.h */, 346129BE1FD2068600532771 /* ThreadUtil.m */, + 241C6310231F5C4400B4198E /* UIColor+Helper.swift */, 340872BE22393CF900CB25B0 /* UIGestureRecognizer+OWS.swift */, 4C858A51212DC5E1001B45D3 /* UIImage+OWS.swift */, B97940251832BD2400BD66CB /* UIUtil.h */, @@ -2633,6 +2641,7 @@ B846365922B7417900AF1514 /* Loki */ = { isa = PBXGroup; children = ( + 241C630E231F5AAC00B4198E /* JazzIcon.swift */, B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */, ); path = Loki; @@ -3493,6 +3502,7 @@ 34AC0A1C211B39EA00997B47 /* OWSFlatButton.swift in Sources */, 340872D822397F4600CB25B0 /* AttachmentCaptionViewController.swift in Sources */, 34C3C7932040B0DD0000134C /* OWSAudioPlayer.m in Sources */, + 241C6316231F64CE00B4198E /* UIColor+Helper.swift in Sources */, 34AC09E5211B39B100997B47 /* ScreenLockViewController.m in Sources */, 34AC09F7211B39B100997B47 /* MediaMessageView.swift in Sources */, 34BBC858220C7ADA00857249 /* ImageEditorContents.swift in Sources */, @@ -3569,6 +3579,7 @@ 346129A61FD1F09100532771 /* OWSContactsManager.m in Sources */, 4541B71D209D3B7A0008608F /* ContactShareViewModel.swift in Sources */, 4C618199219DF03A009BD6B5 /* OWSButton.swift in Sources */, + 241C6314231F64C000B4198E /* JazzIcon.swift in Sources */, 4598198F204E2F28009414F2 /* OWS108CallLoggingPreference.m in Sources */, 34AC09F3211B39B100997B47 /* NewNonContactConversationViewController.m in Sources */, 4C3E245C21F29FCE000AE092 /* Toast.swift in Sources */, @@ -3586,6 +3597,7 @@ 34BBC85A220C7ADA00857249 /* ImageEditorTextItem.swift in Sources */, 34641E182088D7E900E2EDE5 /* OWSScreenLock.swift in Sources */, 346129721FD1D74C00532771 /* SignalKeyingStorage.m in Sources */, + 241C6315231F64CE00B4198E /* CGFloat+Rounding.swift in Sources */, 349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */, 34480B561FD0A7A400BC14EF /* DebugLogger.m in Sources */, 459B775C207BA46C0071D0AB /* OWSQuotedReplyModel.m in Sources */, diff --git a/SignalMessaging/Loki/Identicon+ObjC.swift b/SignalMessaging/Loki/Identicon+ObjC.swift index 33ac195f0..8d8bb909a 100644 --- a/SignalMessaging/Loki/Identicon+ObjC.swift +++ b/SignalMessaging/Loki/Identicon+ObjC.swift @@ -3,17 +3,30 @@ import IGIdenticon @objc(LKIdenticon) final class Identicon : NSObject { - @objc static func generateIcon(string: String, size: CGSize) -> UIImage { - let identicon = IGIdenticon.Identicon().icon(from: string, size: size)! - let rect = CGRect(origin: CGPoint.zero, size: identicon.size) - UIGraphicsBeginImageContextWithOptions(identicon.size, false, UIScreen.main.scale) - let context = UIGraphicsGetCurrentContext()! - context.setFillColor(UIColor.white.cgColor) - context.fill(rect) - context.draw(identicon.cgImage!, in: rect) - context.drawPath(using: CGPathDrawingMode.fill) - let result = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() - return result +// @objc static func generateIcon(string: String, size: CGSize) -> UIImage { +// let identicon = IGIdenticon.Identicon().icon(from: string, size: size)! +// let rect = CGRect(origin: CGPoint.zero, size: identicon.size) +// UIGraphicsBeginImageContextWithOptions(identicon.size, false, UIScreen.main.scale) +// let context = UIGraphicsGetCurrentContext()! +// context.setFillColor(UIColor.white.cgColor) +// context.fill(rect) +// context.draw(identicon.cgImage!, in: rect) +// context.drawPath(using: CGPathDrawingMode.fill) +// let result = UIGraphicsGetImageFromCurrentImageContext()! +// UIGraphicsEndImageContext() +// return result +// } + + @objc static func generateIcon(string: String, size: CGFloat) -> UIImage { + let icon = JazzIcon(seed: string) + let iconLayer = icon.generateLayer(ofSize: size) + let rect = CGRect(origin: CGPoint.zero, size: iconLayer.frame.size) + let renderer = UIGraphicsImageRenderer(size: rect.size) + let image = renderer.image { + context in + + return iconLayer.render(in: context.cgContext) + } + return image } } diff --git a/SignalMessaging/Loki/JazzIcon.swift b/SignalMessaging/Loki/JazzIcon.swift new file mode 100644 index 000000000..423816b67 --- /dev/null +++ b/SignalMessaging/Loki/JazzIcon.swift @@ -0,0 +1,137 @@ + +import CryptoSwift + +private class RNG { + private var seed: UInt + private var initial: UInt + + init(seed: UInt) { + self.seed = seed % 2147483647 + self.initial = self.seed + } + + func next() -> UInt { + let seed = (self.seed * 16807) % 2147483647 + self.seed = seed + return seed + } + + func nextFloat() -> Float { + return Float(next() - 1) / 2147483647.0 + } + + func nextCGFloat() -> CGFloat { + return CGFloat(nextFloat()) + } + + func reset() { + seed = initial + } +} + + +public class JazzIcon { + private let generator: RNG + + // Colour palette + private var colours: [UIColor] = [ + 0x01888c, // Teal + 0xfc7500, // bright orange + 0x034f5d, // dark teal + 0xE784BA, // light pink + 0x81C8B6, // bright green + 0xc7144c, // raspberry + 0xf3c100, // goldenrod + 0x1598f2, // lightning blue + 0x2465e1, // sail blue + 0xf19e02, // gold + ].map { UIColor(rgb: $0) } + + // Defaults + private let shapeCount = 4 + private let wobble = 30 + + init(seed: UInt, colours: [UIColor]? = nil) { + self.generator = RNG(seed: seed) + if let colours = colours { + self.colours = colours + } + } + + convenience init(seed: String, colours: [UIColor]? = nil) { + let hash = seed.md5() + guard let number = UInt(hash.substring(to: min(hash.count, 12)), radix: 16) else { + owsFailDebug("[JazzIcon] Failed to generate number from seed string: \(seed)") + self.init(seed: 1234, colours: colours) + return + } + + self.init(seed: number, colours: colours) + } + + public func generateLayer(ofSize diameter: CGFloat) -> CALayer { + generator.reset() + + let newColours = hueShift(colours: colours) + let shuffled = shuffle(newColours) + + let base = getSquareLayer(with: diameter, colour: shuffled[0].cgColor) + base.masksToBounds = true + + for index in 0.. CAShapeLayer { + let frame = CGRect(x: 0, y: 0, width: diameter, height: diameter) + + let layer = CAShapeLayer() + layer.frame = frame + layer.path = UIBezierPath(roundedRect: frame, cornerRadius: 0).cgPath + layer.fillColor = colour + return layer + } + + private func generateShapeLayer(diameter: CGFloat, colour: CGColor, index: Int, total: Int) -> CALayer { + let center = diameter / 2 + let firstRotation = generator.nextCGFloat() + let angle = CGFloat.pi * 2 * firstRotation + + let a = diameter / CGFloat(total) + let b: CGFloat = generator.nextCGFloat() + let c = CGFloat(index) * a + let velocity = a * b + c + let translation = CGPoint(x: cos(angle) * velocity, y: sin(angle) * velocity) + + // Third random is a shape rotation ontop of all that + let secondRotation = generator.nextCGFloat() + let rotation = (firstRotation * 360.0) + (secondRotation * 180) + let radians = rotation.rounded(toPlaces: 1) * CGFloat.pi / 180.0 + + let layer = getSquareLayer(with: diameter, colour: colour) + layer.position = CGPoint(x: center + translation.x, y: center + translation.y) + layer.transform = CATransform3DMakeRotation(radians, 0, 0, center) + + return layer + } + + private func shuffle(_ array: [T]) -> [T] { + var currentIndex = array.count + var mutated = array + while (currentIndex > 0) { + let randomIndex = Int(generator.next()) % currentIndex + currentIndex -= 1 + mutated.swapAt(currentIndex, randomIndex) + } + return mutated + } + + private func hueShift(colours: [UIColor]) -> [UIColor] { + let amount = generator.nextCGFloat() * 30 - CGFloat(wobble / 2); + return colours.map { $0.adjust(hueBy: amount / 360.0) } + } +} diff --git a/SignalMessaging/utils/CGFloat+Rounding.swift b/SignalMessaging/utils/CGFloat+Rounding.swift new file mode 100644 index 000000000..6cbc113c9 --- /dev/null +++ b/SignalMessaging/utils/CGFloat+Rounding.swift @@ -0,0 +1,7 @@ +extension CGFloat { + /// Rounds the float to decimal places value + func rounded(toPlaces places:Int) -> CGFloat { + let divisor = pow(10.0, CGFloat(places)) + return (self * divisor).rounded() / divisor + } +} diff --git a/SignalMessaging/utils/OWSAvatarBuilder.m b/SignalMessaging/utils/OWSAvatarBuilder.m index 85644ce6a..ae61345f0 100644 --- a/SignalMessaging/utils/OWSAvatarBuilder.m +++ b/SignalMessaging/utils/OWSAvatarBuilder.m @@ -30,7 +30,7 @@ typedef void (^OWSAvatarDrawBlock)(CGContextRef context); OWSAvatarBuilder *avatarBuilder; if ([thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)thread; - return [LKIdenticon generateIconWithString:contactThread.contactIdentifier size:CGSizeMake(diameter, diameter)]; + return [LKIdenticon generateIconWithString:contactThread.contactIdentifier size:((CGFloat)diameter)]; } else if ([thread isKindOfClass:[TSGroupThread class]]) { avatarBuilder = [[OWSGroupAvatarBuilder alloc] initWithThread:(TSGroupThread *)thread diameter:diameter]; } else { diff --git a/SignalMessaging/utils/OWSContactAvatarBuilder.m b/SignalMessaging/utils/OWSContactAvatarBuilder.m index dc6ff3216..1c4e68ff4 100644 --- a/SignalMessaging/utils/OWSContactAvatarBuilder.m +++ b/SignalMessaging/utils/OWSContactAvatarBuilder.m @@ -181,7 +181,7 @@ NS_ASSUME_NONNULL_BEGIN } */ - UIImage *image = [LKIdenticon generateIconWithString:self.signalId size:CGSizeMake(self.diameter, self.diameter)]; + UIImage *image = [LKIdenticon generateIconWithString:self.signalId size:((CGFloat)self.diameter)]; [OWSContactAvatarBuilder.contactsManager.avatarCache setImage:image forKey:self.cacheKey diameter:self.diameter]; return image; } diff --git a/SignalMessaging/utils/UIColor+Helper.swift b/SignalMessaging/utils/UIColor+Helper.swift new file mode 100644 index 000000000..4c229c4f4 --- /dev/null +++ b/SignalMessaging/utils/UIColor+Helper.swift @@ -0,0 +1,37 @@ +extension UIColor { + + public func adjust(hueBy hue: CGFloat = 0, saturationBy saturation: CGFloat = 0, brightnessBy brightness: CGFloat = 0) -> UIColor { + + var currentHue: CGFloat = 0.0 + var currentSaturation: CGFloat = 0.0 + var currentBrigthness: CGFloat = 0.0 + var currentAlpha: CGFloat = 0.0 + + if getHue(¤tHue, saturation: ¤tSaturation, brightness: ¤tBrigthness, alpha: ¤tAlpha) { + return UIColor(hue: currentHue + hue, + saturation: currentSaturation + saturation, + brightness: currentBrigthness + brightness, + alpha: currentAlpha) + } else { + return self + } + } + + convenience init(red: Int, green: Int, blue: Int, a: CGFloat = 1.0) { + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: a + ) + } + + convenience init(rgb: Int, a: CGFloat = 1.0) { + self.init( + red: (rgb >> 16) & 0xFF, + green: (rgb >> 8) & 0xFF, + blue: rgb & 0xFF, + a: a + ) + } +}