From 06167fe5867e229f166827d7f0146a92192ed29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sat, 8 Feb 2025 16:02:31 +0100 Subject: [PATCH] feat(mentions): Unify mention support local/server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- NextcloudTalk.xcodeproj/project.pbxproj | 12 ++++- NextcloudTalk/BaseChatViewController.swift | 47 +--------------- NextcloudTalk/InputbarViewController.swift | 22 ++++---- NextcloudTalk/Mention.swift | 35 ++++++++++++ NextcloudTalk/MentionSuggestion.swift | 32 ++--------- NextcloudTalk/NCChatMessage.m | 10 +--- NextcloudTalk/NCChatMessage.swift | 40 +++++++++++++- NextcloudTalk/NCMessageParameter.h | 7 +-- NextcloudTalk/NCMessageParameter.m | 37 +++++++------ .../Unit/Chat/UnitNCChatMessageTest.swift | 54 +++++++++++++++++++ .../Chat/UnitNCMessageParameterTest.swift | 7 ++- .../Unit/UnitMentionSuggestionTest.swift | 32 +++++------ 12 files changed, 205 insertions(+), 130 deletions(-) create mode 100644 NextcloudTalk/Mention.swift diff --git a/NextcloudTalk.xcodeproj/project.pbxproj b/NextcloudTalk.xcodeproj/project.pbxproj index 68245f598..5e9fbbf76 100644 --- a/NextcloudTalk.xcodeproj/project.pbxproj +++ b/NextcloudTalk.xcodeproj/project.pbxproj @@ -204,6 +204,10 @@ 1F7AE07A29142E62009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07929142E62009F72AD /* NextcloudKit */; }; 1F7AE07C29142E6A009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07B29142E6A009F72AD /* NextcloudKit */; }; 1F7AE07D29158878009F72AD /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */; }; + 1F7CCC242D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; }; + 1F7CCC252D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; }; + 1F7CCC262D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; }; + 1F7CCC272D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; }; 1F8848122A75B68D00063860 /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */; }; 1F8995B32970644C00CABA33 /* ColorGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8995B22970644C00CABA33 /* ColorGenerator.swift */; }; 1F8995B52973547700CABA33 /* WebRTCCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8995B42973547700CABA33 /* WebRTCCommon.swift */; }; @@ -784,6 +788,7 @@ 1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageTranscribeViewController.m; sourceTree = ""; }; 1F785DDB2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VoiceMessageTranscribeViewController.xib; sourceTree = ""; }; 1F785DDC2707865F00AC4B40 /* VoiceMessageTranscribeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VoiceMessageTranscribeViewController.h; sourceTree = ""; }; + 1F7CCC232D552D2000F3FB77 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = ""; }; 1F8995B22970644C00CABA33 /* ColorGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorGenerator.swift; sourceTree = ""; }; 1F8995B42973547700CABA33 /* WebRTCCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCCommon.swift; sourceTree = ""; }; 1F8AAC312C518759004DA20A /* SignalingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalingSettings.swift; sourceTree = ""; }; @@ -2183,11 +2188,12 @@ 1FAB2E842ACB482B001214EB /* ChatViewController.swift */, 1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */, 2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */, - 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */, F644A2DC2CE287FA00E2ED81 /* ChatFileUploader.swift */, 1F2058292CEA404F00AAA673 /* AiSummaryViewController.swift */, 1F20582B2CEA405700AAA673 /* AiSummaryViewController.xib */, 1F205B9F2CEE1B8800AAA673 /* AiSummaryController.swift */, + 1F7CCC232D552D2000F3FB77 /* Mention.swift */, + 1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */, ); name = Chat; sourceTree = ""; @@ -2856,6 +2862,7 @@ 1F77A5F42AB9A4B2007B6037 /* ABContact.m in Sources */, 1F77A6012AB9A51D007B6037 /* NCNotificationAction.swift in Sources */, 1FB7B9902BF0CDF80093CE98 /* BannedActor.swift in Sources */, + 1F7CCC272D552D2000F3FB77 /* Mention.swift in Sources */, 1F77A5F32AB9A43B007B6037 /* SwiftMarkdownObjCBridge.swift in Sources */, 1FC4B3452CCE671800D28138 /* OcsError.swift in Sources */, 1FF4DA832C025DBF00C1B952 /* NCAPISessionManager.swift in Sources */, @@ -3121,6 +3128,7 @@ 1F35F9042AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */, 2C42ADB420B58E6300296DEA /* NCChatController.m in Sources */, 1F20582A2CEA404F00AAA673 /* AiSummaryViewController.swift in Sources */, + 1F7CCC242D552D2000F3FB77 /* Mention.swift in Sources */, 1FD9182928C55A73009092AB /* BGTaskHelper.swift in Sources */, 1F66B72929FA936E003FB168 /* SLKDefaultReplyView.m in Sources */, 1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */, @@ -3197,6 +3205,7 @@ 1F35F9052AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */, 1F35F90A2AEEE76A00044BDA /* QuotedMessageView.m in Sources */, 2C62B02E24C1BDD7007E460A /* PlaceholderView.m in Sources */, + 1F7CCC262D552D2000F3FB77 /* Mention.swift in Sources */, 2C62B01024C1BDC5007E460A /* NCRoom.m in Sources */, 1FDCC3ED29EC7E6700DEB39B /* AvatarImageView.swift in Sources */, 1F35F8E32AEEBBE000044BDA /* NCChatTitleView.m in Sources */, @@ -3313,6 +3322,7 @@ 2CC0016924A25C3400A20167 /* NCMessageParameter.m in Sources */, 1FB78E292B6AE8CA00B0D69D /* FederationInvitation.swift in Sources */, 2C444704265D641300DF1DBC /* NCUserDefaults.m in Sources */, + 1F7CCC252D552D2000F3FB77 /* Mention.swift in Sources */, 1F205C512CEF91C500AAA673 /* UserAbsence.swift in Sources */, 2CC001B724A37A9A00A20167 /* NCUser.m in Sources */, 2CC0016124A25B5500A20167 /* NCAPIController.m in Sources */, diff --git a/NextcloudTalk/BaseChatViewController.swift b/NextcloudTalk/BaseChatViewController.swift index 584fba0cb..c48bbbb73 100644 --- a/NextcloudTalk/BaseChatViewController.swift +++ b/NextcloudTalk/BaseChatViewController.swift @@ -562,19 +562,6 @@ import SwiftUI return unmanagedTemporaryMessage } - internal func replaceMessageMentionsKeysWithMentionsDisplayNames(message: String, parameters: String) -> String { - var resultMessage = message.trimmingCharacters(in: .whitespacesAndNewlines) - - guard let messageParametersDict = NCMessageParameter.messageParametersDict(fromJSONString: parameters) else { return resultMessage } - - for (parameterKey, parameter) in messageParametersDict { - let parameterKeyString = "{\(parameterKey)}" - resultMessage = resultMessage.replacingOccurrences(of: parameterKeyString, with: parameter.mentionDisplayName) - } - - return resultMessage - } - internal func appendTemporaryMessage(temporaryMessage: NCChatMessage) { DispatchQueue.main.async { let lastSectionBeforeUpdate = self.dateSections.count - 1 @@ -1016,9 +1003,8 @@ import SwiftUI self.removeUnreadMessagesSeparator() self.removePermanentlyTemporaryMessage(temporaryMessage: message) - guard var originalMessage = message.message else { return } + guard var originalMessage = message.sendingMessageWithDisplayNames else { return } if message.messageType != kMessageTypeVoiceMessage { - originalMessage = self.replaceMessageMentionsKeysWithMentionsDisplayNames(message: message.message, parameters: message.messageParametersJSONString ?? "") self.sendChatMessage(message: originalMessage, withParentMessage: message.parent, messageParameters: message.messageParametersJSONString ?? "", silently: message.isSilent) } else { let activeAccount = NCDatabaseManager.sharedInstance().activeAccount() @@ -1108,36 +1094,7 @@ import SwiftUI // Show the message to edit in the reply view self.showReplyView(for: message) self.replyMessageView!.hideCloseButton() - - self.mentionsDict = [:] - - // Try to reconstruct the mentionsDict - for (key, value) in message.messageParameters { - if let key = key as? String, - key.hasPrefix("mention-"), - let value = value as? [String: String] { - - guard let parameter = NCMessageParameter(dictionary: value), - let paramaterDisplayName = parameter.name, - let parameterId = parameter.parameterId - else { continue } - - // For mentions the displayName is in the parameter "name", in our mentionsDict we use - // "mentionsDisplayName" for the displayName with the prefix "@", so we need to construct - // that manually here, so mentions are correctly removed while editing. - // The same needs to happen for "mentionId" -> userId with a prefixed "@" - parameter.mentionDisplayName = "@\(paramaterDisplayName)" - - if parameter.mentionId == nil { - // Fallback for servers that do not return "mention-id" in message parameters. - // This will not work correctly in some cases (e.g. teams) - parameter.mentionId = "@\(parameterId)" - } - - self.mentionsDict[key] = parameter - } - } - + self.mentionsDict = message.mentionMessageParameters self.editingMessage = message // For files without a caption we start with an empty text instead of "{file}" diff --git a/NextcloudTalk/InputbarViewController.swift b/NextcloudTalk/InputbarViewController.swift index 38fa6e030..088087d2a 100644 --- a/NextcloudTalk/InputbarViewController.swift +++ b/NextcloudTalk/InputbarViewController.swift @@ -245,8 +245,10 @@ import UIKit guard let messageParametersDict = NCMessageParameter.messageParametersDict(fromJSONString: parameters) else { return resultMessage } for (parameterKey, parameter) in messageParametersDict { + guard let mention = parameter.mention else { continue } + let parameterKeyString = "{\(parameterKey)}" - resultMessage = resultMessage.replacingOccurrences(of: parameter.mentionDisplayName, with: parameterKeyString) + resultMessage = resultMessage.replacingOccurrences(of: mention.labelForChat, with: parameterKeyString) } return resultMessage @@ -295,23 +297,23 @@ import UIKit if let details = suggestion.details { cell.titleLabel.numberOfLines = 2 - let attributedLabel = (suggestion.label + "\n").withFont(.preferredFont(forTextStyle: .body)) + let attributedLabel = (suggestion.mention.label + "\n").withFont(.preferredFont(forTextStyle: .body)) let attributedDetails = details.withFont(.preferredFont(forTextStyle: .callout)).withTextColor(.secondaryLabel) attributedLabel.append(attributedDetails) cell.titleLabel.attributedText = attributedLabel } else { cell.titleLabel.numberOfLines = 1 - cell.titleLabel.text = suggestion.label + cell.titleLabel.text = suggestion.mention.label } if let suggestionUserStatus = suggestion.userStatus { cell.setUserStatus(suggestionUserStatus) } - if suggestion.id == "all" { + if suggestion.mention.id == "all" { cell.avatarButton.setAvatar(for: self.room) } else { - cell.avatarButton.setActorAvatar(forId: suggestion.id, withType: suggestion.source, withDisplayName: suggestion.label, withRoomToken: self.room.token, using: self.account) + cell.avatarButton.setActorAvatar(forId: suggestion.mention.id, withType: suggestion.source, withDisplayName: suggestion.mention.label, withRoomToken: self.room.token, using: self.account) } cell.accessibilityIdentifier = AutoCompletionCellIdentifier @@ -328,7 +330,7 @@ import UIKit let mentionKey = "mention-\(self.mentionsDict.count)" self.mentionsDict[mentionKey] = suggestion.asMessageParameter() - let mentionWithWhitespace = suggestion.label + " " + let mentionWithWhitespace = suggestion.mention.label + " " self.acceptAutoCompletion(with: mentionWithWhitespace, keepPrefix: true) } @@ -356,13 +358,15 @@ import UIKit let substring = (text as NSString).substring(to: cursorOffset) if var lastPossibleMention = substring.components(separatedBy: "@").last { - lastPossibleMention.insert("@", at: lastPossibleMention.startIndex) - for (mentionKey, mentionParameter) in self.mentionsDict { - if lastPossibleMention != mentionParameter.mentionDisplayName { + guard let mention = mentionParameter.mention else { continue } + + if lastPossibleMention != mention.label { continue } + lastPossibleMention.insert("@", at: lastPossibleMention.startIndex) + // Delete mention let range = NSRange(location: cursorOffset - lastPossibleMention.utf16.count, length: lastPossibleMention.utf16.count) textView.text = (text as NSString).replacingCharacters(in: range, with: "") diff --git a/NextcloudTalk/Mention.swift b/NextcloudTalk/Mention.swift new file mode 100644 index 000000000..840c808f4 --- /dev/null +++ b/NextcloudTalk/Mention.swift @@ -0,0 +1,35 @@ +// +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import Foundation + +@objcMembers public class Mention: NSObject { + + public var id: String + public var label: String + public var mentionId: String? + + init(id: String, label: String) { + self.id = id + self.label = label + } + + init(id: String, label: String, mentionId: String? = nil) { + self.id = id + self.label = label + self.mentionId = mentionId + } + + public var idForChat: String { + // Prefer mentionId if it's supported by the server + let id = self.mentionId ?? self.id + + return "@\"\(id)\"" + } + + public var labelForChat: String { + return "@\(label)" + } +} diff --git a/NextcloudTalk/MentionSuggestion.swift b/NextcloudTalk/MentionSuggestion.swift index 5fa713bdc..3ccf717c3 100644 --- a/NextcloudTalk/MentionSuggestion.swift +++ b/NextcloudTalk/MentionSuggestion.swift @@ -7,48 +7,26 @@ import Foundation @objcMembers public class MentionSuggestion: NSObject { - public var id: String - public var label: String + public var mention: Mention public var source: String - public var mentionId: String? public var userStatus: String? public var details: String? init(dictionary: [String: Any]) { - self.id = dictionary["id"] as? String ?? "" - self.label = dictionary["label"] as? String ?? "" + self.mention = Mention(id: dictionary["id"] as? String ?? "", label: dictionary["label"] as? String ?? "", mentionId: dictionary["mentionId"] as? String) self.source = dictionary["source"] as? String ?? "" - self.mentionId = dictionary["mentionId"] as? String self.userStatus = dictionary["status"] as? String self.details = dictionary["details"] as? String super.init() } - func getIdForChat() -> String { - // When we support a mentionId serverside, we use that - var id = self.mentionId ?? self.id - - if id.contains("/") || id.rangeOfCharacter(from: .whitespaces) != nil { - id = "\"\(id)\"" - } - - return id - } - - func getIdForAvatar() -> String { - // For avatars we always want to use the actorId, so ignore a potential serverside mentionId here - return self.id - } - func asMessageParameter() -> NCMessageParameter { let messageParameter = NCMessageParameter() - messageParameter.parameterId = self.getIdForAvatar() - messageParameter.name = self.label - messageParameter.mentionDisplayName = "@\(self.label)" - // Note: The mentionId on NCMessageParameter is different than the one on MentionSuggestion! - messageParameter.mentionId = "@\(self.getIdForChat())" + messageParameter.parameterId = mention.id + messageParameter.name = mention.label + messageParameter.mention = mention // Set parameter type if self.source == "calls" { diff --git a/NextcloudTalk/NCChatMessage.m b/NextcloudTalk/NCChatMessage.m index 21f5f43cb..c9738258f 100644 --- a/NextcloudTalk/NCChatMessage.m +++ b/NextcloudTalk/NCChatMessage.m @@ -327,10 +327,7 @@ - (NSMutableAttributedString *)parsedMessage // Default replacement string is the parameter name NSString *replaceString = messageParameter.name; // Format user and call mentions - if ([messageParameter.type isEqualToString:@"user"] || [messageParameter.type isEqualToString:@"guest"] || - [messageParameter.type isEqualToString:@"user-group"] || [messageParameter.type isEqualToString:@"call"] || - [messageParameter.type isEqualToString:@"email"] || [messageParameter.type isEqualToString:@"circle"]) { - + if ([messageParameter isMention]) { replaceString = [NSString stringWithFormat:@"@%@", [parameterDict objectForKey:@"name"]]; } parsedMessage = [parsedMessage stringByReplacingOccurrencesOfString:parameter withString:replaceString]; @@ -361,10 +358,7 @@ - (NSMutableAttributedString *)parsedMessage for (NCMessageParameter *param in parameters) { //Set color for mentions - if ([param.type isEqualToString:@"user"] || [param.type isEqualToString:@"guest"] || - [param.type isEqualToString:@"user-group"] || [param.type isEqualToString:@"call"] || - [param.type isEqualToString:@"email"] || [param.type isEqualToString:@"circle"]) { - + if ([param isMention]) { if (param.shouldBeHighlighted) { if (!highlightedColor) { // Only get the elementColor if we really need it to reduce realm queries diff --git a/NextcloudTalk/NCChatMessage.swift b/NextcloudTalk/NCChatMessage.swift index c64bd11ab..736dc705c 100644 --- a/NextcloudTalk/NCChatMessage.swift +++ b/NextcloudTalk/NCChatMessage.swift @@ -131,6 +131,25 @@ import SwiftyAttributes return dict } + public var mentionMessageParameters: [String: NCMessageParameter] { + var result: [String: NCMessageParameter] = [:] + + for case let (key as String, value as [String: String]) in self.messageParameters { + guard key.hasPrefix("mention-"), let parameter = NCMessageParameter(dictionary: value), parameter.isMention() else { continue } + + if parameter.mention == nil, let parameterId = parameter.parameterId, let paramaterDisplayName = parameter.name { + // Try to reconstruct the mention for unsupported servers + parameter.mention = Mention(id: parameterId, label: paramaterDisplayName) + } + + if parameter.mention != nil { + result[key] = parameter + } + } + + return result + } + // TODO: Should probably be an optional? public var systemMessageFormat: NSMutableAttributedString { guard let message = self.parsedMessage() else { return NSMutableAttributedString(string: "") } @@ -139,14 +158,31 @@ import SwiftyAttributes } // TODO: Should probably be an optional? + /// 'Hello {mention-user1}' -> 'Hello @user1' public var sendingMessage: String { guard var resultMessage = self.message else { return "" } resultMessage = resultMessage.trimmingCharacters(in: .whitespacesAndNewlines) for case let (key as String, value as [AnyHashable: Any]) in self.messageParameters { - if let parameter = NCMessageParameter(dictionary: value), let mentionId = parameter.mentionId { - resultMessage = resultMessage.replacingOccurrences(of: "{\(key)}", with: mentionId) + if let parameter = NCMessageParameter(dictionary: value), let mention = parameter.mention { + resultMessage = resultMessage.replacingOccurrences(of: "{\(key)}", with: mention.idForChat) + } + } + + return resultMessage + } + + /// 'Hello {mention-user1}' -> 'Hello @User1 Displayname' + public var sendingMessageWithDisplayNames: String? { + guard var resultMessage = self.message else { return nil } + + resultMessage = message.trimmingCharacters(in: .whitespacesAndNewlines) + + // TODO: Could use mentionMessageParameters directly here? + for case let (key as String, value as [AnyHashable: Any]) in self.messageParameters { + if let parameter = NCMessageParameter(dictionary: value), let mention = parameter.mention { + resultMessage = resultMessage.replacingOccurrences(of: "{\(key)}", with: mention.labelForChat) } } diff --git a/NextcloudTalk/NCMessageParameter.h b/NextcloudTalk/NCMessageParameter.h index e5636cbc5..edcb40429 100644 --- a/NextcloudTalk/NCMessageParameter.h +++ b/NextcloudTalk/NCMessageParameter.h @@ -6,6 +6,8 @@ #import #import +@class Mention; + @interface NCMessageParameter : NSObject @property (nonatomic, strong) NSString *parameterId; @@ -15,13 +17,12 @@ @property (nonatomic, assign) NSRange range; @property (nonatomic, strong) NSString *contactName; @property (nonatomic, strong) NSString *contactPhoto; -// Helper property for mentions created using the app -@property (nonatomic, strong) NSString * _Nullable mentionId; -@property (nonatomic, strong) NSString *mentionDisplayName; +@property (nonatomic, strong) Mention * _Nullable mention; - (instancetype)initWithDictionary:(NSDictionary *)parameterDict; - (BOOL)shouldBeHighlighted; - (UIImage * _Nullable)contactPhotoImage; +- (BOOL)isMention; // parametersDict as [NSString:NCMessageParameter] + (NSString *)messageParametersJSONStringFromDictionary:(NSDictionary *)parametersDict; diff --git a/NextcloudTalk/NCMessageParameter.m b/NextcloudTalk/NCMessageParameter.m index 3a89a6076..753bda6eb 100644 --- a/NextcloudTalk/NCMessageParameter.m +++ b/NextcloudTalk/NCMessageParameter.m @@ -7,6 +7,8 @@ #import "NCDatabaseManager.h" +#import "NextcloudTalk-Swift.h" + @implementation NCMessageParameter - (instancetype)initWithDictionary:(NSDictionary *)parameterDict @@ -32,22 +34,16 @@ - (instancetype)initWithDictionary:(NSDictionary *)parameterDict self.contactName = [parameterDict objectForKey:@"contact-name"]; self.contactPhoto = [parameterDict objectForKey:@"contact-photo"]; - if ([parameterDict objectForKey:@"mention-id"]) { - // "mention-id" (with a dash) is returned by the server and should be preferred if it exists - NSString *mentionId = [parameterDict objectForKey:@"mention-id"]; + if ([self isMention]) { + NSString *mentionDisplayName = [parameterDict objectForKey:@"mention-display-name"]; - // Note: The "mentionId" in NCMessageParameter is different to MentionSuggestion! In NCMessageParameter we require the @-prefix - if ([mentionId containsString:@"/"] || [mentionId rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]].location != NSNotFound) { - self.mentionId = [NSString stringWithFormat:@"@\"%@\"", mentionId]; - } else { - self.mentionId = [NSString stringWithFormat:@"@%@", mentionId]; + if (!mentionDisplayName) { + mentionDisplayName = self.name; } - } else if ([parameterDict objectForKey:@"mentionId"]) { - // "mentionId" (without a dash) is our locally stored mentionId in case a message needs to be resend -> use as fallback - self.mentionId = [parameterDict objectForKey:@"mentionId"]; - } - self.mentionDisplayName = [parameterDict objectForKey:@"mentionDisplayName"]; + NSString *mentionId = [parameterDict objectForKey:@"mention-id"]; + self.mention = [[Mention alloc] initWithId:self.parameterId label:mentionDisplayName mentionId:mentionId]; + } } return self; @@ -61,6 +57,13 @@ - (BOOL)shouldBeHighlighted return ([_type isEqualToString:@"user"] && [activeAccount.userId isEqualToString:_parameterId]) || [_type isEqualToString:@"call"]; } +- (BOOL)isMention +{ + return [_type isEqualToString:@"user"] || [_type isEqualToString:@"guest"] || + [_type isEqualToString:@"user-group"] || [_type isEqualToString:@"call"] || + [_type isEqualToString:@"email"] || [_type isEqualToString:@"circle"]; +} + - (UIImage *)contactPhotoImage { if (self.contactPhoto) { @@ -98,11 +101,11 @@ + (NSDictionary *)dictionaryFromMessageParameter:(NCMessageParameter *)messagePa if (messageParameter.contactPhoto) { [messageParameterDict setObject:messageParameter.contactPhoto forKey:@"contact-photo"]; } - if (messageParameter.mentionId) { - [messageParameterDict setObject:messageParameter.mentionId forKey:@"mentionId"]; + if (messageParameter.mention.mentionId) { + [messageParameterDict setObject:messageParameter.mention.mentionId forKey:@"mention-id"]; } - if (messageParameter.mentionDisplayName) { - [messageParameterDict setObject:messageParameter.mentionDisplayName forKey:@"mentionDisplayName"]; + if (messageParameter.mention.label) { + [messageParameterDict setObject:messageParameter.mention.label forKey:@"mention-display-name"]; } return [[NSDictionary alloc] initWithDictionary:messageParameterDict]; diff --git a/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift b/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift index b456ed7b4..128c754bc 100644 --- a/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift +++ b/NextcloudTalkTests/Unit/Chat/UnitNCChatMessageTest.swift @@ -50,4 +50,58 @@ final class UnitNCChatMessageTest: TestBaseRealm { XCTAssertEqual(mentionMessage.parsedMarkdownForChat().string, "@Username with space{mention-user2}") } + func testMentionParameters() throws { + let messageParameters = """ + { + "actor": { + "type": "user", + "id": "admin", + "name": "admin ABC", + "mention-id": "admin" + }, + "mention-federated-user1": { + "type": "user", + "id": "user1", + "name": "User1 Displayname", + "server": "https://nextcloud.local", + "mention-id": "federated_user/user1@nextcloud.local" + }, + "mention-user1": { + "type": "user", + "id": "alice", + "name": "alice", + "mention-id": "alice" + }, + "mention-call1": { + "type": "call", + "id": "12345", + "name": "Group Conversation", + "call-type": "public", + "icon-url": "https://nextcloud.local/ocs/v2.php/apps/spreed/api/v1/room/12345/avatar?v=1b893bde", + "mention-id": "all" + } + } + """ + + let message = NCChatMessage() + message.messageParametersJSONString = messageParameters + + message.message = "Hello {mention-user1} --- hello {mention-federated-user1} --- hello {mention-call1} 123" + XCTAssertEqual(message.parsedMarkdownForChat().string, "Hello @alice --- hello @User1 Displayname --- hello @Group Conversation 123") + + let mentionsDict = message.mentionMessageParameters + XCTAssertEqual(mentionsDict.count, 3) + + let userMention = mentionsDict.first(where: { $0.value.type == "user" && !$0.key.contains("federated") })!.value + XCTAssertEqual(userMention.mention?.mentionId, "alice") + + let federatedMention = mentionsDict.first(where: { $0.value.type == "user" && $0.key.contains("federated") })!.value + XCTAssertEqual(federatedMention.mention?.mentionId, "federated_user/user1@nextcloud.local") + + let callMention = mentionsDict.first(where: { $0.value.type == "call" })!.value + XCTAssertEqual(callMention.mention?.mentionId, "all") + + XCTAssertEqual(message.sendingMessage, "Hello @\"alice\" --- hello @\"federated_user/user1@nextcloud.local\" --- hello @\"all\" 123") + XCTAssertEqual(message.sendingMessageWithDisplayNames, "Hello @alice --- hello @User1 Displayname --- hello @Group Conversation 123") + } } diff --git a/NextcloudTalkTests/Unit/Chat/UnitNCMessageParameterTest.swift b/NextcloudTalkTests/Unit/Chat/UnitNCMessageParameterTest.swift index 8dee82c0b..03a859c27 100644 --- a/NextcloudTalkTests/Unit/Chat/UnitNCMessageParameterTest.swift +++ b/NextcloudTalkTests/Unit/Chat/UnitNCMessageParameterTest.swift @@ -25,7 +25,10 @@ final class UnitNCMessageParameterTest: XCTestCase { XCTAssertEqual(parameters.parameterId, "admin") XCTAssertEqual(parameters.name, "admin displayname") - XCTAssertEqual(parameters.mentionId, "@admin") + XCTAssertEqual(parameters.mention?.id, "admin") + XCTAssertEqual(parameters.mention?.idForChat, "@\"admin\"") + XCTAssertEqual(parameters.mention?.label, "admin displayname") + XCTAssertEqual(parameters.mention?.labelForChat, "@admin displayname") } func testMentionIdFromServerFederated() throws { @@ -46,7 +49,7 @@ final class UnitNCMessageParameterTest: XCTestCase { XCTAssertEqual(parameters.parameterId, "admin") XCTAssertEqual(parameters.name, "admin displayname") - XCTAssertEqual(parameters.mentionId, "@\"federated_user/admin@nextcloud.local\"") + XCTAssertEqual(parameters.mention?.idForChat, "@\"federated_user/admin@nextcloud.local\"") } } diff --git a/NextcloudTalkTests/Unit/UnitMentionSuggestionTest.swift b/NextcloudTalkTests/Unit/UnitMentionSuggestionTest.swift index 5be0c51bd..1ad8017aa 100644 --- a/NextcloudTalkTests/Unit/UnitMentionSuggestionTest.swift +++ b/NextcloudTalkTests/Unit/UnitMentionSuggestionTest.swift @@ -17,11 +17,12 @@ final class UnitMentionSuggestionTest: XCTestCase { let suggestion = MentionSuggestion(dictionary: data) - XCTAssertEqual(suggestion.id, "my-id") - XCTAssertEqual(suggestion.label, "My Label") XCTAssertEqual(suggestion.source, "users") - XCTAssertEqual(suggestion.getIdForChat(), "my-id") - XCTAssertEqual(suggestion.getIdForAvatar(), "my-id") + + XCTAssertEqual(suggestion.mention.id, "my-id") + XCTAssertEqual(suggestion.mention.label, "My Label") + XCTAssertEqual(suggestion.mention.idForChat, "@\"my-id\"") + XCTAssertEqual(suggestion.mention.labelForChat, "@My Label") } func testLocalGuestMention() throws { @@ -31,9 +32,8 @@ final class UnitMentionSuggestionTest: XCTestCase { let suggestion = MentionSuggestion(dictionary: data) - XCTAssertEqual(suggestion.id, "guest/guest-id") - XCTAssertEqual(suggestion.getIdForChat(), "\"guest/guest-id\"") - XCTAssertEqual(suggestion.getIdForAvatar(), "guest/guest-id") + XCTAssertEqual(suggestion.mention.id, "guest/guest-id") + XCTAssertEqual(suggestion.mention.idForChat, "@\"guest/guest-id\"") } func testLocalWhitespaceMention() throws { @@ -43,9 +43,8 @@ final class UnitMentionSuggestionTest: XCTestCase { let suggestion = MentionSuggestion(dictionary: data) - XCTAssertEqual(suggestion.id, "my id") - XCTAssertEqual(suggestion.getIdForChat(), "\"my id\"") - XCTAssertEqual(suggestion.getIdForAvatar(), "my id") + XCTAssertEqual(suggestion.mention.id, "my id") + XCTAssertEqual(suggestion.mention.idForChat, "@\"my id\"") } func testMentionId() throws { @@ -56,10 +55,9 @@ final class UnitMentionSuggestionTest: XCTestCase { let suggestion = MentionSuggestion(dictionary: data) - XCTAssertEqual(suggestion.id, "my-id") - XCTAssertEqual(suggestion.mentionId, "mention-id") - XCTAssertEqual(suggestion.getIdForChat(), "mention-id") - XCTAssertEqual(suggestion.getIdForAvatar(), "my-id") + XCTAssertEqual(suggestion.mention.id, "my-id") + XCTAssertEqual(suggestion.mention.mentionId, "mention-id") + XCTAssertEqual(suggestion.mention.idForChat, "@\"mention-id\"") } func testMessageParameter() throws { @@ -75,8 +73,10 @@ final class UnitMentionSuggestionTest: XCTestCase { XCTAssertEqual(parameter.parameterId, "my-id") XCTAssertEqual(parameter.name, "My Label") - XCTAssertEqual(parameter.mentionDisplayName, "@My Label") - XCTAssertEqual(parameter.mentionId, "@mention-id") + XCTAssertEqual(parameter.mention?.label, "My Label") + XCTAssertEqual(parameter.mention?.mentionId, "mention-id") + XCTAssertEqual(parameter.mention?.idForChat, "@\"mention-id\"") + XCTAssertEqual(parameter.mention?.labelForChat, "@My Label") XCTAssertEqual(parameter.type, "user") } }