diff --git a/Pods b/Pods index 1d4d7b782..8f1b31fca 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 1d4d7b782a10e903551407c77351bf11a37d342d +Subproject commit 8f1b31fca49c76533b4d56808a30524b073c32e2 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 585f6b3a9..c35ea15de 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 */; }; @@ -666,6 +669,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 = ""; }; @@ -2641,7 +2647,10 @@ B846365922B7417900AF1514 /* Loki */ = { isa = PBXGroup; children = ( + 241C630E231F5AAC00B4198E /* JazzIcon.swift */, B846365A22B7418B00AF1514 /* Identicon+ObjC.swift */, + 241C6312231F5F1D00B4198E /* CGFloat+Rounding.swift */, + 241C6310231F5C4400B4198E /* UIColor+Helper.swift */, ); path = Loki; sourceTree = ""; @@ -3533,6 +3542,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 */, @@ -3609,6 +3619,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 */, @@ -3626,6 +3637,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/Signal/src/Loki/LokiGroupChatPoller.swift b/Signal/src/Loki/LokiGroupChatPoller.swift index 9f75b2e4a..029fe95cc 100644 --- a/Signal/src/Loki/LokiGroupChatPoller.swift +++ b/Signal/src/Loki/LokiGroupChatPoller.swift @@ -65,11 +65,13 @@ public final class LokiGroupChatPoller : NSObject { let x3 = SSKProtoContent.builder() x3.setDataMessage(try! x2.build()) let x4 = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: message.timestamp) - x4.setSource(senderDisplayName) + x4.setSource(senderHexEncodedPublicKey) x4.setSourceDevice(OWSDevicePrimaryDeviceId) x4.setContent(try! x3.build().serializedData()) + let storage = OWSPrimaryStorage.shared() storage.dbReadWriteConnection.readWrite { transaction in + transaction.setObject(senderDisplayName, forKey: senderHexEncodedPublicKey, inCollection: group.id) SSKEnvironment.shared.messageManager.throws_processEnvelope(try! x4.build(), plaintextData: try! x3.build().serializedData(), wasReceivedByUD: false, transaction: transaction) } } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m index b5527d2d0..e9162562e 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewModel.m @@ -21,6 +21,8 @@ #import #import #import +#import +#import #import #import #import @@ -1507,13 +1509,26 @@ static const int kYapDatabaseRangeMaxLength = 25000; = (![NSObject isNullableObject:previousIncomingSenderId equalTo:incomingSenderId] || viewItem.hasCellHeader); } + if (shouldShowSenderName) { - senderName = [self.contactsManager - attributedContactOrProfileNameForPhoneIdentifier:incomingSenderId - primaryAttributes:[OWSMessageBubbleView - senderNamePrimaryAttributes] - secondaryAttributes:[OWSMessageBubbleView - senderNameSecondaryAttributes]]; + senderName = [self.contactsManager attributedContactOrProfileNameForPhoneIdentifier:incomingSenderId primaryAttributes:[OWSMessageBubbleView senderNamePrimaryAttributes] + secondaryAttributes:[OWSMessageBubbleView senderNameSecondaryAttributes]]; + + if ([self.thread isKindOfClass:[TSGroupThread class]]) { + TSGroupThread *groupThread = (TSGroupThread *)self.thread; + NSData *groupId = groupThread.groupModel.groupId; + NSString *stringGroupId = [[NSString alloc] initWithData:groupId encoding:NSUTF8StringEncoding]; + + if (stringGroupId != nil) { + NSString __block *displayName; + [self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + displayName = [transaction objectForKey:incomingSenderId inCollection:stringGroupId]; + }]; + if (displayName != nil) { + senderName = [[NSAttributedString alloc] initWithString:displayName attributes:[OWSMessageBubbleView senderNamePrimaryAttributes]]; + } + } + } } // Show the sender avatar for incoming group messages unless diff --git a/SignalMessaging/Loki/CGFloat+Rounding.swift b/SignalMessaging/Loki/CGFloat+Rounding.swift new file mode 100644 index 000000000..8ee83badd --- /dev/null +++ b/SignalMessaging/Loki/CGFloat+Rounding.swift @@ -0,0 +1,9 @@ + +extension CGFloat { + + /// Round the number to the given amount of decimal places. + func rounded(toPlaces places:Int) -> CGFloat { + let divisor = pow(10.0, CGFloat(places)) + return (self * divisor).rounded() / divisor + } +} diff --git a/SignalMessaging/Loki/Identicon+ObjC.swift b/SignalMessaging/Loki/Identicon+ObjC.swift index 33ac195f0..9b8c81bfd 100644 --- a/SignalMessaging/Loki/Identicon+ObjC.swift +++ b/SignalMessaging/Loki/Identicon+ObjC.swift @@ -1,19 +1,13 @@ -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: 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 { iconLayer.render(in: $0.cgContext) } + return image } } diff --git a/SignalMessaging/Loki/JazzIcon.swift b/SignalMessaging/Loki/JazzIcon.swift new file mode 100644 index 000000000..cde5fef06 --- /dev/null +++ b/SignalMessaging/Loki/JazzIcon.swift @@ -0,0 +1,148 @@ + +import CryptoSwift + +extension String { + func matches(_ regex: String) -> Bool { + return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil + } +} + +private class RNG { + private var seed: Int + private var initial: Int + + init(seed: Int) { + self.seed = seed % 2147483647 + if (self.seed <= 0) { self.seed += 2147483646 } + self.initial = self.seed + } + + func next() -> Int { + // Casting to Int64 incase number goes above Int32 + let seed = (Int64(self.seed) * 16807) % 2147483647 + self.seed = Int(seed) + return self.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: Int, colours: [UIColor]? = nil) { + self.generator = RNG(seed: seed) + if let colours = colours { + self.colours = colours + } + } + + convenience init(seed: String, colours: [UIColor]? = nil) { + // Ensure we have a correct hash + var hash = seed + if !hash.matches("^[0-9A-Fa-f]+$") || hash.count < 12 { hash = seed.sha512() } + + guard let number = Int(hash.substring(to: 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 = 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) } + } +} diff --git a/SignalMessaging/Loki/UIColor+Helper.swift b/SignalMessaging/Loki/UIColor+Helper.swift new file mode 100644 index 000000000..47cec7330 --- /dev/null +++ b/SignalMessaging/Loki/UIColor+Helper.swift @@ -0,0 +1,48 @@ + +extension UIColor { + + public func adjust(hueBy degrees: CGFloat) -> 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) { + // Round values so we get closer values to Desktop + let currentHueDegrees = (currentHue * 360.0).rounded() + let normalizedDegrees = fmod(degrees, 360.0).rounded() + + // Make sure we're in the range 0 to 360 + var newHue = fmod(currentHueDegrees + normalizedDegrees, 360.0) + if (newHue < 0) { newHue = 360 + newHue } + + let decimalHue = (currentHueDegrees + normalizedDegrees) / 360.0 + + return UIColor(hue: decimalHue, + saturation: currentSaturation, + brightness: currentBrigthness, + alpha: 1.0) + } 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 + ) + } +} 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/SignalServiceKit/src/Loki/Messaging/SSKProtoEnvelope+Loki.swift b/SignalServiceKit/src/Loki/Messaging/SSKProtoEnvelope+Loki.swift new file mode 100644 index 000000000..97e8a9f44 --- /dev/null +++ b/SignalServiceKit/src/Loki/Messaging/SSKProtoEnvelope+Loki.swift @@ -0,0 +1,12 @@ + +@objc public extension SSKProtoEnvelope { + + @objc public var isGroupChatMessage: Bool { + do { + let contentProto = try SSKProtoContent.parseData(self.content!) + return contentProto.dataMessage!.group != nil + } catch { + return false + } + } +} diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 0ea8de380..e7f5a667d 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -1386,9 +1386,8 @@ NS_ASSUME_NONNULL_BEGIN [pointer saveWithTransaction:transaction]; [incomingMessage.attachmentIds addObject:pointer.uniqueId]; } - - // Loki: Do this before the check below - [self handleFriendRequestMessageIfNeededWithEnvelope:envelope message:incomingMessage thread:oldGroupThread transaction:transaction]; + + // Loki: Don't process friend requests in group chats if (body.length == 0 && attachmentPointers.count < 1 && !contact) { OWSLogWarn(@"ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu", @@ -1532,8 +1531,8 @@ NS_ASSUME_NONNULL_BEGIN // The difference between this function and `handleFriendRequestAcceptIfNeededWithEnvelope:` is that this will setup the incoming message for display to the user // While `handleFriendRequestAcceptIfNeededWithEnvelope:` handles friend request accepting logic and doesn't need a message - (void)handleFriendRequestMessageIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope message:(TSIncomingMessage *)message thread:(TSThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction { - // Check if it's a friend request message - if (envelope.type != SSKProtoEnvelopeTypeFriendRequest) return; + // Check if it's a friend request message and make sure it's not a group message + if (envelope.isGroupChatMessage || envelope.type != SSKProtoEnvelopeTypeFriendRequest) return; if (thread.hasCurrentUserSentFriendRequest) { // This can happen if Alice sent Bob a friend request, Bob declined, but then Bob changed his @@ -1568,7 +1567,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)handleFriendRequestAcceptIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope transaction:(YapDatabaseReadWriteTransaction *)transaction { // If we get any other envelope type then we can assume that we had to use signal cipher decryption // and that means we must have a session with the other person. - if (envelope.type == SSKProtoEnvelopeTypeFriendRequest) return; + // We also need to ensure that we're contacting the person directly and not through a public chat + if (envelope.isGroupChatMessage || envelope.type == SSKProtoEnvelopeTypeFriendRequest) return; // If we're already friends then there's no point in continuing // TODO: We'll need to fix this up if we ever start using Sync messages