Skip to content

Commit

Permalink
feat(mentions): Unify mention support local/server
Browse files Browse the repository at this point in the history
Signed-off-by: Marcel Müller <marcel-mueller@gmx.de>
  • Loading branch information
SystemKeeper committed Feb 8, 2025
1 parent bb9c7a4 commit 06167fe
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 130 deletions.
12 changes: 11 additions & 1 deletion NextcloudTalk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -784,6 +788,7 @@
1F785DDA2707865F00AC4B40 /* VoiceMessageTranscribeViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VoiceMessageTranscribeViewController.m; sourceTree = "<group>"; };
1F785DDB2707865F00AC4B40 /* VoiceMessageTranscribeViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = VoiceMessageTranscribeViewController.xib; sourceTree = "<group>"; };
1F785DDC2707865F00AC4B40 /* VoiceMessageTranscribeViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VoiceMessageTranscribeViewController.h; sourceTree = "<group>"; };
1F7CCC232D552D2000F3FB77 /* Mention.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mention.swift; sourceTree = "<group>"; };
1F8995B22970644C00CABA33 /* ColorGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorGenerator.swift; sourceTree = "<group>"; };
1F8995B42973547700CABA33 /* WebRTCCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCCommon.swift; sourceTree = "<group>"; };
1F8AAC312C518759004DA20A /* SignalingSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalingSettings.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
47 changes: 2 additions & 45 deletions NextcloudTalk/BaseChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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}"
Expand Down
22 changes: 13 additions & 9 deletions NextcloudTalk/InputbarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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: "")
Expand Down
35 changes: 35 additions & 0 deletions NextcloudTalk/Mention.swift
Original file line number Diff line number Diff line change
@@ -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)"
}
}
32 changes: 5 additions & 27 deletions NextcloudTalk/MentionSuggestion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
10 changes: 2 additions & 8 deletions NextcloudTalk/NCChatMessage.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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
Expand Down
40 changes: 38 additions & 2 deletions NextcloudTalk/NCChatMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "") }
Expand All @@ -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)
}
}

Expand Down
Loading

0 comments on commit 06167fe

Please sign in to comment.