diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 95173afe4..f10fe677d 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -2615,17 +2615,17 @@ B8439518228510E9000563FE /* Loki */ = { isa = PBXGroup; children = ( + B821F2F72272CED3002C88C0 /* AccountDetailsViewController.swift */, B8162F0222891AD600D46544 /* FriendRequestView.swift */, B8162F0422892C5F00D46544 /* FriendRequestViewDelegate.swift */, - B89841E222B7579F00B1BDC6 /* NewConversationViewController.swift */, - B821F2F72272CED3002C88C0 /* AccountDetailsViewController.swift */, - B821F2F92272CEEE002C88C0 /* SeedViewController.swift */, - 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */, B845B4D3230CD09000D759F0 /* LokiGroupChatPoller.swift */, + 24A830A12293CD0100F4CAC0 /* LokiP2PServer.swift */, B825849F2315024B001B41CB /* LokiRSSFeedPoller.swift */, + B89841E222B7579F00B1BDC6 /* NewConversationViewController.swift */, B825848A230F94FE001B41CB /* QRCodeViewController.swift */, B8258491230FA5DA001B41CB /* ScanQRCodeViewController.h */, B8258492230FA5E9001B41CB /* ScanQRCodeViewController.m */, + B821F2F92272CEEE002C88C0 /* SeedViewController.swift */, ); path = Loki; sourceTree = ""; diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 49c6150a7..1d0c684cb 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -1504,7 +1504,7 @@ static NSTimeInterval launchStartedAt; - (LKGroupChat *)lokiPublicChat { - return [[LKGroupChat alloc] initWithServerID:@(LKGroupChatAPI.publicChatServerID).unsignedIntegerValue server:LKGroupChatAPI.publicChatServer displayName:NSLocalizedString(@"Loki Public Chat", @"") isDeletable:true]; + return [[LKGroupChat alloc] initWithServerID:LKGroupChatAPI.publicChatServerID server:LKGroupChatAPI.publicChatServer displayName:NSLocalizedString(@"Loki Public Chat", @"") isDeletable:true]; } - (LKRSSFeed *)lokiNewsFeed diff --git a/Signal/src/Loki/LokiGroupChatPoller.swift b/Signal/src/Loki/LokiGroupChatPoller.swift index 5ff7add30..ace8f9122 100644 --- a/Signal/src/Loki/LokiGroupChatPoller.swift +++ b/Signal/src/Loki/LokiGroupChatPoller.swift @@ -4,11 +4,16 @@ public final class LokiGroupChatPoller : NSObject { private let group: LokiGroupChat private var pollForNewMessagesTimer: Timer? = nil private var pollForDeletedMessagesTimer: Timer? = nil + private var pollForModerationPermissionTimer: Timer? = nil private var hasStarted = false + private let userHexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + // MARK: Settings private let pollForNewMessagesInterval: TimeInterval = 4 private let pollForDeletedMessagesInterval: TimeInterval = 20 + private let pollForModerationPermissionInterval: TimeInterval = 10 * 60 + // MARK: Lifecycle @objc(initForGroup:) public init(for group: LokiGroupChat) { self.group = group @@ -20,42 +25,80 @@ public final class LokiGroupChatPoller : NSObject { pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForNewMessagesInterval, repeats: true) { [weak self] _ in self?.pollForNewMessages() } pollForNewMessages() // Perform initial update pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: pollForDeletedMessagesInterval, repeats: true) { [weak self] _ in self?.pollForDeletedMessages() } + pollForModerationPermissionTimer = Timer.scheduledTimer(withTimeInterval: pollForModerationPermissionInterval, repeats: true) { [weak self] _ in self?.pollForModerationPermission() } hasStarted = true } @objc public func stop() { pollForNewMessagesTimer?.invalidate() pollForDeletedMessagesTimer?.invalidate() + pollForModerationPermissionTimer?.invalidate() hasStarted = false } + // MARK: Polling private func pollForNewMessages() { + // Prepare let group = self.group - let _ = LokiGroupChatAPI.getMessages(for: group.serverID, on: group.server).done { messages in - messages.reversed().forEach { message in - let senderHexEncodedPublicKey = message.hexEncodedPublicKey - let endIndex = senderHexEncodedPublicKey.endIndex - let cutoffIndex = senderHexEncodedPublicKey.index(endIndex, offsetBy: -8) - let senderDisplayName = "\(message.displayName) (...\(senderHexEncodedPublicKey[cutoffIndex.. #import #import #import #import +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -198,6 +201,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } +- (OWSPrimaryStorage *)primaryStorage +{ + return SSKEnvironment.shared.primaryStorage; +} + - (NSString *)itemId { return self.interaction.uniqueId; @@ -1162,6 +1170,27 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) - (void)deleteAction { [self.interaction remove]; + + if (self.isGroupThread) { + // Skip if the thread is an RSS feed + TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread; + if (groupThread.isRSSFeed) return; + + // Only allow deletion on incoming and outgoing messages + OWSInteractionType interationType = self.interaction.interactionType; + if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return; + + // Make sure it's a public chat message + TSMessage *message = (TSMessage *)self.interaction; + if (!message.isGroupChatMessage) return; + + // Delete the message + BOOL isSentByUser = (interationType == OWSInteractionType_OutgoingMessage); + [[LKGroupChatAPI deleteMessageWithID:message.isGroupChatMessage forGroup:LKGroupChatAPI.publicChatServerID onServer:LKGroupChatAPI.publicChatServer isSentByUser:isSentByUser].catch(^(NSError *error) { + // Roll back + [self.interaction save]; + }) retainUntilComplete]; + } } - (BOOL)hasBodyTextActionContent @@ -1204,6 +1233,34 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return NO; } +- (BOOL)userCanDeleteGroupMessage +{ + if (!self.isGroupThread) return false; + + // Ensure the thread is a public chat and not an RSS feed + TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread; + if (groupThread.isRSSFeed) return false; + + // Only allow deletion on incoming and outgoing messages + OWSInteractionType interationType = self.interaction.interactionType; + if (interationType != OWSInteractionType_OutgoingMessage && interationType != OWSInteractionType_IncomingMessage) return false; + + // Make sure it's a public chat message + TSMessage *message = (TSMessage *)self.interaction; + if (!message.isGroupChatMessage) return false; + + // Only allow deletion on incoming messages if the user has moderation permission + if (interationType == OWSInteractionType_IncomingMessage) { + __block BOOL isModerator; + [[self primaryStorage].dbReadWriteConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + isModerator = [[self primaryStorage] isModeratorForGroup:LKGroupChatAPI.publicChatServerID onServer:LKGroupChatAPI.publicChatServer in:transaction]; + }]; + if (!isModerator) return false; + } + + return true; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift b/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift index 9b24ea586..059d8c91d 100644 --- a/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift @@ -11,7 +11,7 @@ public final class LokiGroupChatAPI : NSObject { // MARK: Public Chat @objc public static let publicChatServer = "https://chat.lokinet.org" @objc public static let publicChatMessageType = "network.loki.messenger.publicChat" - @objc public static let publicChatServerID = 1 + @objc public static let publicChatServerID: UInt64 = 1 // MARK: Convenience private static var userDisplayName: String { @@ -28,7 +28,7 @@ public final class LokiGroupChatAPI : NSObject { // MARK: Error public enum Error : Swift.Error { - case tokenParsingFailed, tokenDecryptionFailed, messageParsingFailed, deletionParsingFailed + case parsingFailed, decryptionFailed } // MARK: Database @@ -87,7 +87,7 @@ public final class LokiGroupChatAPI : NSObject { return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String, let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else { - throw Error.tokenParsingFailed + throw Error.parsingFailed } // Discard the "05" prefix if needed if (serverPublicKey.count == 33) { @@ -97,7 +97,7 @@ public final class LokiGroupChatAPI : NSObject { // The challenge is prefixed by the 16 bit IV guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey), let token = String(bytes: tokenAsData, encoding: .utf8) else { - throw Error.tokenDecryptionFailed + throw Error.decryptionFailed } return token } @@ -136,7 +136,7 @@ public final class LokiGroupChatAPI : NSObject { return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else { print("[Loki] Couldn't parse messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).") - throw Error.messageParsingFailed + throw Error.parsingFailed } return rawMessages.flatMap { message in guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first, let value = annotation["value"] as? JSON, @@ -145,7 +145,6 @@ public final class LokiGroupChatAPI : NSObject { print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(message).") return nil } - guard hexEncodedPublicKey != userHexEncodedPublicKey else { return nil } let lastMessageServerID = getLastMessageServerID(for: group, on: server) if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: group, on: server, to: serverID) } return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp) @@ -168,7 +167,7 @@ public final class LokiGroupChatAPI : NSObject { guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String, let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else { print("[Loki] Couldn't parse message for group chat with ID: \(group) on server: \(server) from: \(rawResponse).") - throw Error.messageParsingFailed + throw Error.parsingFailed } let timestamp = UInt64(date.timeIntervalSince1970) * 1000 return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp) @@ -195,7 +194,7 @@ public final class LokiGroupChatAPI : NSObject { return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else { print("[Loki] Couldn't parse deleted messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).") - throw Error.deletionParsingFailed + throw Error.parsingFailed } return deletions.flatMap { deletion in guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else { @@ -209,6 +208,35 @@ public final class LokiGroupChatAPI : NSObject { } } + public static func deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> Promise { + return getAuthToken(for: server).then { token -> Promise in + let isModerationRequest = !isSentByUser + print("[Loki] Deleting message with ID: \(messageID) for group chat with ID: \(group) on server: \(server) (isModerationRequest = \(isModerationRequest)).") + let urlAsString = isSentByUser ? "\(server)/channels/\(group)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)" + let url = URL(string: urlAsString)! + let request = TSRequest(url: url, method: "DELETE", parameters: [:]) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return TSNetworkManager.shared().makePromise(request: request).done { result -> Void in + print("[Loki] Deleted message with ID: \(messageID) on server: \(server).") + } + } + } + + public static func userHasModerationPermission(for group: UInt64, on server: String) -> Promise { + return getAuthToken(for: server).then { token -> Promise in + let url = URL(string: "\(server)/loki/v1/user_info")! + let request = TSRequest(url: url) + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in + guard let json = rawResponse as? JSON, let data = json["data"] as? JSON else { + print("[Loki] Couldn't parse moderation permission for group chat with ID: \(group) on server: \(server) from: \(rawResponse).") + throw Error.parsingFailed + } + return data["moderator_status"] as? Bool ?? false + } + } + } + // MARK: Public API (Obj-C) @objc(getMessagesForGroup:onServer:) public static func objc_getMessages(for group: UInt64, on server: String) -> AnyPromise { @@ -219,4 +247,9 @@ public final class LokiGroupChatAPI : NSObject { public static func objc_sendMessage(_ message: LokiGroupMessage, to group: UInt64, on server: String) -> AnyPromise { return AnyPromise.from(sendMessage(message, to: group, on: server)) } + + @objc (deleteMessageWithID:forGroup:onServer:isSentByUser:) + public static func objc_deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> AnyPromise { + return AnyPromise.from(deleteMessage(with: messageID, for: group, on: server, isSentByUser: isSentByUser)) + } } diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index 53a9a1ba4..528b70213 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -72,7 +72,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction; -# pragma mark - Last Hash Handling +# pragma mark - Last Hash /** Get the last message hash for the given service node. @@ -95,8 +95,12 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setLastMessageHash(forServiceNode:hash:expiresAt:transaction:)); +# pragma mark - Group Chat + - (void)setIDForMessageWithServerID:(NSUInteger)serverID to:(NSString *)messageID in:(YapDatabaseReadWriteTransaction *)transaction; -- (NSString *)getIDForMessageWithServerID:(NSUInteger)serverID in:(YapDatabaseReadTransaction *)transaction; +- (NSString *_Nullable)getIDForMessageWithServerID:(NSUInteger)serverID in:(YapDatabaseReadTransaction *)transaction; +- (void)setIsModerator:(BOOL)isModerator forGroup:(NSUInteger)group onServer:(NSString *)server in:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setIsModerator(_:for:on:in:)); +- (BOOL)isModeratorForGroup:(NSUInteger)group onServer:(NSString *)server in:(YapDatabaseReadTransaction *)transaction NS_SWIFT_NAME(isModerator(for:on:in:)); @end diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m index 17f778a6c..fa89c1386 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m @@ -19,6 +19,7 @@ #define LKReceivedMessageHashesKey @"LKReceivedMessageHashesKey" #define LKReceivedMessageHashesCollection @"LKReceivedMessageHashesCollection" #define LKMessageIDCollection @"LKMessageIDCollection" +#define LKModerationPermissionCollection @"LKModerationPermissionCollection" @implementation OWSPrimaryStorage (Loki) @@ -163,14 +164,26 @@ [transaction removeObjectForKey:serviceNode inCollection:LKLastMessageHashCollection]; } +# pragma mark - Group Chat + - (void)setIDForMessageWithServerID:(NSUInteger)serverID to:(NSString *)messageID in:(YapDatabaseReadWriteTransaction *)transaction { NSString *key = [NSString stringWithFormat:@"%@", @(serverID)]; [transaction setObject:messageID forKey:key inCollection:LKMessageIDCollection]; } -- (NSString *)getIDForMessageWithServerID:(NSUInteger)serverID in:(YapDatabaseReadTransaction *)transaction { +- (NSString *_Nullable)getIDForMessageWithServerID:(NSUInteger)serverID in:(YapDatabaseReadTransaction *)transaction { NSString *key = [NSString stringWithFormat:@"%@", @(serverID)]; return [transaction objectForKey:key inCollection:LKMessageIDCollection]; } +- (void)setIsModerator:(BOOL)isModerator forGroup:(NSUInteger)group onServer:(NSString *)server in:(YapDatabaseReadWriteTransaction *)transaction { + NSString *key = [NSString stringWithFormat:@"%@.%@", server, @(group)]; + [transaction setBool:isModerator forKey:key inCollection:LKModerationPermissionCollection]; +} + +- (BOOL)isModeratorForGroup:(NSUInteger)group onServer:(NSString *)server in:(YapDatabaseReadTransaction *)transaction NS_SWIFT_NAME(isModerator(for:on:in:)) { + NSString *key = [NSString stringWithFormat:@"%@.%@", server, @(group)]; + return [transaction boolForKey:key inCollection:LKModerationPermissionCollection defaultValue:false]; +} + @end diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.h b/SignalServiceKit/src/Messages/Interactions/TSMessage.h index 14c38e934..a148ea65d 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.h @@ -39,14 +39,15 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) { @property (nonatomic, readonly, nullable) OWSContact *contactShare; @property (nonatomic, readonly, nullable) OWSLinkPreview *linkPreview; // Loki friend request handling -// ======== @property (nonatomic) LKMessageFriendRequestStatus friendRequestStatus; @property (nonatomic, readonly) NSString *friendRequestStatusDescription; @property (nonatomic) uint64_t friendRequestExpiresAt; @property (nonatomic, readonly) BOOL isFriendRequest; @property (nonatomic, readonly) BOOL hasFriendRequestStatusMessage; @property (nonatomic) BOOL isP2P; -// ======== +// Group chat +@property (nonatomic) uint64_t groupChatMessageID; +@property (nonatomic, readonly) BOOL isGroupChatMessage; - (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; @@ -92,6 +93,10 @@ typedef NS_ENUM(NSInteger, LKMessageFriendRequestStatus) { - (void)saveFriendRequestStatus:(LKMessageFriendRequestStatus)friendRequestStatus withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction; - (void)saveFriendRequestExpiresAt:(u_int64_t)expiresAt withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction; +#pragma mark - Group Chat + +- (void)saveGroupChatMessageID:(uint64_t)serverMessageID in:(YapDatabaseReadWriteTransaction *_Nullable)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Interactions/TSMessage.m b/SignalServiceKit/src/Messages/Interactions/TSMessage.m index 440e64b58..2b8c5a50e 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/TSMessage.m @@ -84,6 +84,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4; _linkPreview = linkPreview; _friendRequestStatus = LKMessageFriendRequestStatusNone; _friendRequestExpiresAt = 0; + _groupChatMessageID = -1; return self; } @@ -490,6 +491,23 @@ static const NSUInteger OWSMessageSchemaVersion = 4; return self.isFriendRequest && self.friendRequestStatus != LKMessageFriendRequestStatusSendingOrFailed; } +#pragma mark - Group Chat + +- (BOOL) isGroupChatMessage { + return self.groupChatMessageID > 0; +} + +- (void)saveGroupChatMessageID:(uint64_t)serverMessageID in:(YapDatabaseReadWriteTransaction *_Nullable)transaction { + self.groupChatMessageID = serverMessageID; + if (transaction == nil) { + [self save]; + [self.dbReadWriteConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{}]; + } else { + [self saveWithTransaction:transaction]; + [transaction.connection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() completionBlock:^{}]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index 1350a1af5..fe0c5af2b 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -1360,7 +1360,7 @@ NS_ASSUME_NONNULL_BEGIN envelopeAddress(envelope), groupId, (unsigned long)timestamp); - + // Legit usage of senderTimestamp when creating an incoming group message record TSIncomingMessage *incomingMessage = [[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp @@ -1377,6 +1377,7 @@ NS_ASSUME_NONNULL_BEGIN wasReceivedByUD:wasReceivedByUD]; if (envelope.isPtpMessage) { incomingMessage.isP2P = YES; } + if (dataMessage.publicChatInfo && dataMessage.publicChatInfo.hasServerID) { incomingMessage.groupChatMessageID = dataMessage.publicChatInfo.serverID; } NSArray *attachmentPointers = [TSAttachmentPointer attachmentPointersFromProtos:dataMessage.attachments diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 369b089ad..875e0b743 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -1117,6 +1117,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; [[LKGroupChatAPI sendMessage:groupMessage toGroup:LKGroupChatAPI.publicChatServerID onServer:LKGroupChatAPI.publicChatServer] .thenOn(OWSDispatch.sendingQueue, ^(LKGroupMessage *groupMessage) { [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [message saveGroupChatMessageID:groupMessage.serverID in:transaction]; [OWSPrimaryStorage.sharedManager setIDForMessageWithServerID:groupMessage.serverID to:message.uniqueId in:transaction]; }]; [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages wasSentByUD:false wasSentByWebsocket:false];