Skip to content

Commit 638679e

Browse files
committed
feat(mentions): Unify mention support local/server
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
1 parent 299319b commit 638679e

12 files changed

+205
-130
lines changed

NextcloudTalk.xcodeproj/project.pbxproj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@
204204
1F7AE07A29142E62009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07929142E62009F72AD /* NextcloudKit */; };
205205
1F7AE07C29142E6A009F72AD /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = 1F7AE07B29142E6A009F72AD /* NextcloudKit */; };
206206
1F7AE07D29158878009F72AD /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */; };
207+
1F7CCC242D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; };
208+
1F7CCC252D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; };
209+
1F7CCC262D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; };
210+
1F7CCC272D552D2000F3FB77 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7CCC232D552D2000F3FB77 /* Mention.swift */; };
207211
1F8848122A75B68D00063860 /* IntentsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F90EFC225FE489B00F3FA55 /* IntentsUI.framework */; };
208212
1F8995B32970644C00CABA33 /* ColorGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8995B22970644C00CABA33 /* ColorGenerator.swift */; };
209213
1F8995B52973547700CABA33 /* WebRTCCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F8995B42973547700CABA33 /* WebRTCCommon.swift */; };
@@ -788,6 +792,7 @@
788792
1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageTranscribeViewController.m; sourceTree = "<group>"; };
789793
1F785DDB2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VoiceMessageTranscribeViewController.xib; sourceTree = "<group>"; };
790794
1F785DDC2707865F00AC4B40 /* VoiceMessageTranscribeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VoiceMessageTranscribeViewController.h; sourceTree = "<group>"; };
795+
1F7CCC232D552D2000F3FB77 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
791796
1F8995B22970644C00CABA33 /* ColorGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorGenerator.swift; sourceTree = "<group>"; };
792797
1F8995B42973547700CABA33 /* WebRTCCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCCommon.swift; sourceTree = "<group>"; };
793798
1F8AAC312C518759004DA20A /* SignalingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalingSettings.swift; sourceTree = "<group>"; };
@@ -2188,12 +2193,13 @@
21882193
1FAB2E842ACB482B001214EB /* ChatViewController.swift */,
21892194
1F35F8FA2AEEDBC600044BDA /* ChatViewControllerExtension.swift */,
21902195
2C4230F62B207AB00013E1FA /* ContextChatViewController.swift */,
2191-
1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */,
21922196
F644A2DC2CE287FA00E2ED81 /* ChatFileUploader.swift */,
21932197
1F2058292CEA404F00AAA673 /* AiSummaryViewController.swift */,
21942198
1F20582B2CEA405700AAA673 /* AiSummaryViewController.xib */,
21952199
1F205B9F2CEE1B8800AAA673 /* AiSummaryController.swift */,
21962200
2C1C68062D51229500A7F98A /* CalendarEvent.swift */,
2201+
1F7CCC232D552D2000F3FB77 /* Mention.swift */,
2202+
1F0B0A712BA264540073FF8D /* MentionSuggestion.swift */,
21972203
);
21982204
name = Chat;
21992205
sourceTree = "<group>";
@@ -2862,6 +2868,7 @@
28622868
1F77A5F42AB9A4B2007B6037 /* ABContact.m in Sources */,
28632869
1F77A6012AB9A51D007B6037 /* NCNotificationAction.swift in Sources */,
28642870
1FB7B9902BF0CDF80093CE98 /* BannedActor.swift in Sources */,
2871+
1F7CCC272D552D2000F3FB77 /* Mention.swift in Sources */,
28652872
1F77A5F32AB9A43B007B6037 /* SwiftMarkdownObjCBridge.swift in Sources */,
28662873
1FC4B3452CCE671800D28138 /* OcsError.swift in Sources */,
28672874
1FF4DA832C025DBF00C1B952 /* NCAPISessionManager.swift in Sources */,
@@ -3129,6 +3136,7 @@
31293136
1F35F9042AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */,
31303137
2C42ADB420B58E6300296DEA /* NCChatController.m in Sources */,
31313138
1F20582A2CEA404F00AAA673 /* AiSummaryViewController.swift in Sources */,
3139+
1F7CCC242D552D2000F3FB77 /* Mention.swift in Sources */,
31323140
1FD9182928C55A73009092AB /* BGTaskHelper.swift in Sources */,
31333141
1F66B72929FA936E003FB168 /* SLKDefaultReplyView.m in Sources */,
31343142
1F785DDD2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m in Sources */,
@@ -3206,6 +3214,7 @@
32063214
1F35F9052AEEDF0E00044BDA /* AutoCompletionTableViewCell.m in Sources */,
32073215
1F35F90A2AEEE76A00044BDA /* QuotedMessageView.m in Sources */,
32083216
2C62B02E24C1BDD7007E460A /* PlaceholderView.m in Sources */,
3217+
1F7CCC262D552D2000F3FB77 /* Mention.swift in Sources */,
32093218
2C62B01024C1BDC5007E460A /* NCRoom.m in Sources */,
32103219
1FDCC3ED29EC7E6700DEB39B /* AvatarImageView.swift in Sources */,
32113220
1F35F8E32AEEBBE000044BDA /* NCChatTitleView.m in Sources */,
@@ -3323,6 +3332,7 @@
33233332
2CC0016924A25C3400A20167 /* NCMessageParameter.m in Sources */,
33243333
1FB78E292B6AE8CA00B0D69D /* FederationInvitation.swift in Sources */,
33253334
2C444704265D641300DF1DBC /* NCUserDefaults.m in Sources */,
3335+
1F7CCC252D552D2000F3FB77 /* Mention.swift in Sources */,
33263336
1F205C512CEF91C500AAA673 /* UserAbsence.swift in Sources */,
33273337
2CC001B724A37A9A00A20167 /* NCUser.m in Sources */,
33283338
2CC0016124A25B5500A20167 /* NCAPIController.m in Sources */,

NextcloudTalk/BaseChatViewController.swift

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -562,19 +562,6 @@ import SwiftUI
562562
return unmanagedTemporaryMessage
563563
}
564564

565-
internal func replaceMessageMentionsKeysWithMentionsDisplayNames(message: String, parameters: String) -> String {
566-
var resultMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
567-
568-
guard let messageParametersDict = NCMessageParameter.messageParametersDict(fromJSONString: parameters) else { return resultMessage }
569-
570-
for (parameterKey, parameter) in messageParametersDict {
571-
let parameterKeyString = "{\(parameterKey)}"
572-
resultMessage = resultMessage.replacingOccurrences(of: parameterKeyString, with: parameter.mentionDisplayName)
573-
}
574-
575-
return resultMessage
576-
}
577-
578565
internal func appendTemporaryMessage(temporaryMessage: NCChatMessage) {
579566
DispatchQueue.main.async {
580567
let lastSectionBeforeUpdate = self.dateSections.count - 1
@@ -1016,9 +1003,8 @@ import SwiftUI
10161003
self.removeUnreadMessagesSeparator()
10171004

10181005
self.removePermanentlyTemporaryMessage(temporaryMessage: message)
1019-
guard var originalMessage = message.message else { return }
1006+
guard var originalMessage = message.sendingMessageWithDisplayNames else { return }
10201007
if message.messageType != kMessageTypeVoiceMessage {
1021-
originalMessage = self.replaceMessageMentionsKeysWithMentionsDisplayNames(message: message.message, parameters: message.messageParametersJSONString ?? "")
10221008
self.sendChatMessage(message: originalMessage, withParentMessage: message.parent, messageParameters: message.messageParametersJSONString ?? "", silently: message.isSilent)
10231009
} else {
10241010
let activeAccount = NCDatabaseManager.sharedInstance().activeAccount()
@@ -1108,36 +1094,7 @@ import SwiftUI
11081094
// Show the message to edit in the reply view
11091095
self.showReplyView(for: message)
11101096
self.replyMessageView!.hideCloseButton()
1111-
1112-
self.mentionsDict = [:]
1113-
1114-
// Try to reconstruct the mentionsDict
1115-
for (key, value) in message.messageParameters {
1116-
if let key = key as? String,
1117-
key.hasPrefix("mention-"),
1118-
let value = value as? [String: String] {
1119-
1120-
guard let parameter = NCMessageParameter(dictionary: value),
1121-
let paramaterDisplayName = parameter.name,
1122-
let parameterId = parameter.parameterId
1123-
else { continue }
1124-
1125-
// For mentions the displayName is in the parameter "name", in our mentionsDict we use
1126-
// "mentionsDisplayName" for the displayName with the prefix "@", so we need to construct
1127-
// that manually here, so mentions are correctly removed while editing.
1128-
// The same needs to happen for "mentionId" -> userId with a prefixed "@"
1129-
parameter.mentionDisplayName = "@\(paramaterDisplayName)"
1130-
1131-
if parameter.mentionId == nil {
1132-
// Fallback for servers that do not return "mention-id" in message parameters.
1133-
// This will not work correctly in some cases (e.g. teams)
1134-
parameter.mentionId = "@\(parameterId)"
1135-
}
1136-
1137-
self.mentionsDict[key] = parameter
1138-
}
1139-
}
1140-
1097+
self.mentionsDict = message.mentionMessageParameters
11411098
self.editingMessage = message
11421099

11431100
// For files without a caption we start with an empty text instead of "{file}"

NextcloudTalk/InputbarViewController.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,10 @@ import UIKit
245245
guard let messageParametersDict = NCMessageParameter.messageParametersDict(fromJSONString: parameters) else { return resultMessage }
246246

247247
for (parameterKey, parameter) in messageParametersDict {
248+
guard let mention = parameter.mention else { continue }
249+
248250
let parameterKeyString = "{\(parameterKey)}"
249-
resultMessage = resultMessage.replacingOccurrences(of: parameter.mentionDisplayName, with: parameterKeyString)
251+
resultMessage = resultMessage.replacingOccurrences(of: mention.labelForChat, with: parameterKeyString)
250252
}
251253

252254
return resultMessage
@@ -295,23 +297,23 @@ import UIKit
295297
if let details = suggestion.details {
296298
cell.titleLabel.numberOfLines = 2
297299

298-
let attributedLabel = (suggestion.label + "\n").withFont(.preferredFont(forTextStyle: .body))
300+
let attributedLabel = (suggestion.mention.label + "\n").withFont(.preferredFont(forTextStyle: .body))
299301
let attributedDetails = details.withFont(.preferredFont(forTextStyle: .callout)).withTextColor(.secondaryLabel)
300302
attributedLabel.append(attributedDetails)
301303
cell.titleLabel.attributedText = attributedLabel
302304
} else {
303305
cell.titleLabel.numberOfLines = 1
304-
cell.titleLabel.text = suggestion.label
306+
cell.titleLabel.text = suggestion.mention.label
305307
}
306308

307309
if let suggestionUserStatus = suggestion.userStatus {
308310
cell.setUserStatus(suggestionUserStatus)
309311
}
310312

311-
if suggestion.id == "all" {
313+
if suggestion.mention.id == "all" {
312314
cell.avatarButton.setAvatar(for: self.room)
313315
} else {
314-
cell.avatarButton.setActorAvatar(forId: suggestion.id, withType: suggestion.source, withDisplayName: suggestion.label, withRoomToken: self.room.token, using: self.account)
316+
cell.avatarButton.setActorAvatar(forId: suggestion.mention.id, withType: suggestion.source, withDisplayName: suggestion.mention.label, withRoomToken: self.room.token, using: self.account)
315317
}
316318

317319
cell.accessibilityIdentifier = AutoCompletionCellIdentifier
@@ -328,7 +330,7 @@ import UIKit
328330
let mentionKey = "mention-\(self.mentionsDict.count)"
329331
self.mentionsDict[mentionKey] = suggestion.asMessageParameter()
330332

331-
let mentionWithWhitespace = suggestion.label + " "
333+
let mentionWithWhitespace = suggestion.mention.label + " "
332334
self.acceptAutoCompletion(with: mentionWithWhitespace, keepPrefix: true)
333335
}
334336

@@ -356,13 +358,15 @@ import UIKit
356358
let substring = (text as NSString).substring(to: cursorOffset)
357359

358360
if var lastPossibleMention = substring.components(separatedBy: "@").last {
359-
lastPossibleMention.insert("@", at: lastPossibleMention.startIndex)
360-
361361
for (mentionKey, mentionParameter) in self.mentionsDict {
362-
if lastPossibleMention != mentionParameter.mentionDisplayName {
362+
guard let mention = mentionParameter.mention else { continue }
363+
364+
if lastPossibleMention != mention.label {
363365
continue
364366
}
365367

368+
lastPossibleMention.insert("@", at: lastPossibleMention.startIndex)
369+
366370
// Delete mention
367371
let range = NSRange(location: cursorOffset - lastPossibleMention.utf16.count, length: lastPossibleMention.utf16.count)
368372
textView.text = (text as NSString).replacingCharacters(in: range, with: "")

NextcloudTalk/Mention.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
//
5+
6+
import Foundation
7+
8+
@objcMembers public class Mention: NSObject {
9+
10+
public var id: String
11+
public var label: String
12+
public var mentionId: String?
13+
14+
init(id: String, label: String) {
15+
self.id = id
16+
self.label = label
17+
}
18+
19+
init(id: String, label: String, mentionId: String? = nil) {
20+
self.id = id
21+
self.label = label
22+
self.mentionId = mentionId
23+
}
24+
25+
public var idForChat: String {
26+
// Prefer mentionId if it's supported by the server
27+
let id = self.mentionId ?? self.id
28+
29+
return "@\"\(id)\""
30+
}
31+
32+
public var labelForChat: String {
33+
return "@\(label)"
34+
}
35+
}

NextcloudTalk/MentionSuggestion.swift

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,26 @@ import Foundation
77

88
@objcMembers public class MentionSuggestion: NSObject {
99

10-
public var id: String
11-
public var label: String
10+
public var mention: Mention
1211
public var source: String
13-
public var mentionId: String?
1412
public var userStatus: String?
1513
public var details: String?
1614

1715
init(dictionary: [String: Any]) {
18-
self.id = dictionary["id"] as? String ?? ""
19-
self.label = dictionary["label"] as? String ?? ""
16+
self.mention = Mention(id: dictionary["id"] as? String ?? "", label: dictionary["label"] as? String ?? "", mentionId: dictionary["mentionId"] as? String)
2017
self.source = dictionary["source"] as? String ?? ""
21-
self.mentionId = dictionary["mentionId"] as? String
2218
self.userStatus = dictionary["status"] as? String
2319
self.details = dictionary["details"] as? String
2420

2521
super.init()
2622
}
2723

28-
func getIdForChat() -> String {
29-
// When we support a mentionId serverside, we use that
30-
var id = self.mentionId ?? self.id
31-
32-
if id.contains("/") || id.rangeOfCharacter(from: .whitespaces) != nil {
33-
id = "\"\(id)\""
34-
}
35-
36-
return id
37-
}
38-
39-
func getIdForAvatar() -> String {
40-
// For avatars we always want to use the actorId, so ignore a potential serverside mentionId here
41-
return self.id
42-
}
43-
4424
func asMessageParameter() -> NCMessageParameter {
4525
let messageParameter = NCMessageParameter()
4626

47-
messageParameter.parameterId = self.getIdForAvatar()
48-
messageParameter.name = self.label
49-
messageParameter.mentionDisplayName = "@\(self.label)"
50-
// Note: The mentionId on NCMessageParameter is different than the one on MentionSuggestion!
51-
messageParameter.mentionId = "@\(self.getIdForChat())"
27+
messageParameter.parameterId = mention.id
28+
messageParameter.name = mention.label
29+
messageParameter.mention = mention
5230

5331
// Set parameter type
5432
if self.source == "calls" {

NextcloudTalk/NCChatMessage.m

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,10 +327,7 @@ - (NSMutableAttributedString *)parsedMessage
327327
// Default replacement string is the parameter name
328328
NSString *replaceString = messageParameter.name;
329329
// Format user and call mentions
330-
if ([messageParameter.type isEqualToString:@"user"] || [messageParameter.type isEqualToString:@"guest"] ||
331-
[messageParameter.type isEqualToString:@"user-group"] || [messageParameter.type isEqualToString:@"call"] ||
332-
[messageParameter.type isEqualToString:@"email"] || [messageParameter.type isEqualToString:@"circle"]) {
333-
330+
if ([messageParameter isMention]) {
334331
replaceString = [NSString stringWithFormat:@"@%@", [parameterDict objectForKey:@"name"]];
335332
}
336333
parsedMessage = [parsedMessage stringByReplacingOccurrencesOfString:parameter withString:replaceString];
@@ -361,10 +358,7 @@ - (NSMutableAttributedString *)parsedMessage
361358

362359
for (NCMessageParameter *param in parameters) {
363360
//Set color for mentions
364-
if ([param.type isEqualToString:@"user"] || [param.type isEqualToString:@"guest"] ||
365-
[param.type isEqualToString:@"user-group"] || [param.type isEqualToString:@"call"] ||
366-
[param.type isEqualToString:@"email"] || [param.type isEqualToString:@"circle"]) {
367-
361+
if ([param isMention]) {
368362
if (param.shouldBeHighlighted) {
369363
if (!highlightedColor) {
370364
// Only get the elementColor if we really need it to reduce realm queries

NextcloudTalk/NCChatMessage.swift

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,25 @@ import SwiftyAttributes
131131
return dict
132132
}
133133

134+
public var mentionMessageParameters: [String: NCMessageParameter] {
135+
var result: [String: NCMessageParameter] = [:]
136+
137+
for case let (key as String, value as [String: String]) in self.messageParameters {
138+
guard key.hasPrefix("mention-"), let parameter = NCMessageParameter(dictionary: value), parameter.isMention() else { continue }
139+
140+
if parameter.mention == nil, let parameterId = parameter.parameterId, let paramaterDisplayName = parameter.name {
141+
// Try to reconstruct the mention for unsupported servers
142+
parameter.mention = Mention(id: parameterId, label: paramaterDisplayName)
143+
}
144+
145+
if parameter.mention != nil {
146+
result[key] = parameter
147+
}
148+
}
149+
150+
return result
151+
}
152+
134153
// TODO: Should probably be an optional?
135154
public var systemMessageFormat: NSMutableAttributedString {
136155
guard let message = self.parsedMessage() else { return NSMutableAttributedString(string: "") }
@@ -139,14 +158,31 @@ import SwiftyAttributes
139158
}
140159

141160
// TODO: Should probably be an optional?
161+
/// 'Hello {mention-user1}' -> 'Hello @user1'
142162
public var sendingMessage: String {
143163
guard var resultMessage = self.message else { return "" }
144164

145165
resultMessage = resultMessage.trimmingCharacters(in: .whitespacesAndNewlines)
146166

147167
for case let (key as String, value as [AnyHashable: Any]) in self.messageParameters {
148-
if let parameter = NCMessageParameter(dictionary: value), let mentionId = parameter.mentionId {
149-
resultMessage = resultMessage.replacingOccurrences(of: "{\(key)}", with: mentionId)
168+
if let parameter = NCMessageParameter(dictionary: value), let mention = parameter.mention {
169+
resultMessage = resultMessage.replacingOccurrences(of: "{\(key)}", with: mention.idForChat)
170+
}
171+
}
172+
173+
return resultMessage
174+
}
175+
176+
/// 'Hello {mention-user1}' -> 'Hello @User1 Displayname'
177+
public var sendingMessageWithDisplayNames: String? {
178+
guard var resultMessage = self.message else { return nil }
179+
180+
resultMessage = message.trimmingCharacters(in: .whitespacesAndNewlines)
181+
182+
// TODO: Could use mentionMessageParameters directly here?
183+
for case let (key as String, value as [AnyHashable: Any]) in self.messageParameters {
184+
if let parameter = NCMessageParameter(dictionary: value), let mention = parameter.mention {
185+
resultMessage = resultMessage.replacingOccurrences(of: "{\(key)}", with: mention.labelForChat)
150186
}
151187
}
152188

0 commit comments

Comments
 (0)