@消息实现方案
更新时间: 2025/02/24 15:55:16
若您需要实现简单的 @ 功能,可以参考网易云信在 IM UIKit 上使用的艾特(@)逻辑,以实现简单的类微信的 @ 功能。
效果展示
@消息是一种即时通讯功能,用户可以在群聊或频道中通过“@”符号提及特定用户或角色,被提及的用户会收到提醒通知,从而快速注意到相关消息。效果如下图所示:

方案介绍
@ 消息通过扩展参数,保留 @ 消息的相关内容。在消息体 NIMMessage
中可以通过 remoteExt
来获取和设置消息体中的远程传输的扩展参数,@ 消息的内容就以 Object 形式保存在该消息体中,数据格式如下:
JSON//@消息的 key 值
"yxAitMsg": {
//被@的账号,如果是 @All 则为 ait_all
"332917623668992": {
"text": "@昵称 01 ",//在消息中@的展示内容
"segments": [{//在消息中@的展示位置
"start": 0,//在消息中@的展示起始位置
"end": 5,//在消息中@的展示终止位置
}]
}
}
在发送一条 @ 消息时,会将上述 @ 消息的内容,设置到该消息的 remoteExt
中。
实现流程
-
创建一条文本消息。
Swift
// text 代表要发送的内容 let message = MessageUtils.textMessage(text: text)
-
设置 @ 信息和推送配置。
Swift
open func getAtRemoteExtension(_ attri: NSAttributedString?) -> [String: Any]? { guard let attribute = attri else { return nil } var atDic = [String: [String: Any]]() let string = attribute.string attribute.enumerateAttribute( NSAttributedString.Key.foregroundColor, in: NSMakeRange(0, attribute.length) ) { value, findRange, stop in guard let findColor = value as? UIColor else { return } if isEqualToColor(findColor, UIColor.ne_normalTheme) == false { return } if let range = Range(findRange, in: string) { let text = string[range] let model = MessageAtInfoModel() print("range text : ", String(text)) // 计算 at 前有表情导致索引新增的数量 let expandIndex = getConvertedExtraIndex(attribute.attributedSubstring(from: NSRange(location: 0, length: findRange.location))) print("expand index value ", expandIndex) model.start = findRange.location + expandIndex let nameExpandCount = getConvertedExtraIndex(attribute.attributedSubstring(from: findRange)) print("name expand index value ", nameExpandCount) model.end = model.start + findRange.length + nameExpandCount print("model start : ", model.start, " model end : ", model.end) var dic: [String: Any]? var array: [Any]? if let accid = nickAccidDic[String(text)] { if let atCacheDic = atDic[accid] { dic = atCacheDic } else { dic = [String: Any]() } if let atCacheArray = dic?[atSegmentsKey] as? [Any] { array = atCacheArray } else { array = [Any]() } if let object = model.yx_modelToJSONObject() { array?.append(object) } dic?[atSegmentsKey] = array dic?[atTextKey] = String(text) + " " dic?[#keyPath(MessageAtCacheModel.accid)] = accid atDic[accid] = dic } } } if atDic.count > 0 { return [yxAtMsg: atDic] } return nil }
@ 信息需要按照上述的 JSON 格式配置,如果使用 UIKit,则可以通过 NEBaseChatInputView 提供的方法。示例代码可参考 ChatViewController 中的 sendContentText 方法。
-
发送消息。
Swift
let params = ChatRepo.shared.getSendMessageParams(aiUserAccid, message) ChatRepo.shared.sendMessage(message: message, conversationId: conversationId, params: params, completion)
-
接受方对接收到的消息进行解析。
Swift
/// 解析消息中的 @ /// - Parameters: /// - message: 消息 /// - attributeStr: 消息富文本 /// - Returns: 高亮 @ 后的消息富文本 public static func loadAtInMessage(_ message: V2NIMMessage?, _ attributeStr: NSMutableAttributedString?) -> NSMutableAttributedString? { // 数字人回复的消息不展示高亮(serverExtension 会被带回) if message?.aiConfig != nil, message?.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_RESPONSE { return nil } let text = message?.text ?? "" let messageTextFont = UIFont.systemFont(ofSize: ChatUIConfig.shared.messageProperties.messageTextSize) // 兼容老的表情消息,如果前面有表情而位置计算异常则回退回老的解析 var notFound = false // 计算表情(根据转码后的 index) if let remoteExt = getDictionaryFromJSONString(message?.serverExtension ?? ""), let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { for (_, value) in dic { if let contentDic = value as? [String: AnyObject] { if let array = contentDic[atSegmentsKey] as? [AnyObject] { if let models = NSArray.yx_modelArray(with: MessageAtInfoModel.self, json: array) as? [MessageAtInfoModel] { for model in models { // 前面因为表情增加的索引数量 var count = 0 if text.count > model.start { let frontAttributeStr = NEEmotionTool.getAttWithStr( str: String(text.prefix(model.start)), font: messageTextFont ) count = getReduceIndexCount(frontAttributeStr) } let start = model.start - count if start < 0 { notFound = true break } var end = model.end - count if model.end + atRangeOffset > text.count { notFound = true break } // 获取起始索引 let startIndex = text.index(text.startIndex, offsetBy: model.start) // 获取结束索引 let endIndex = text.index(text.startIndex, offsetBy: model.end + atRangeOffset) let frontAttributeStr = NEEmotionTool.getAttWithStr( str: String(text[startIndex ..< endIndex]), font: messageTextFont ) let innerCount = getReduceIndexCount(frontAttributeStr) end = end - innerCount if end <= start { notFound = true break } if attributeStr?.length ?? 0 > end { attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(start, end - start + atRangeOffset)) } } } } } } } if notFound == true, let remoteExt = getDictionaryFromJSONString(message?.serverExtension ?? ""), let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { for (_, value) in dic { if let contentDic = value as? [String: AnyObject] { if let array = contentDic[atSegmentsKey] as? [AnyObject] { if let models = NSArray.yx_modelArray(with: MessageAtInfoModel.self, json: array) as? [MessageAtInfoModel] { for model in models { if attributeStr?.length ?? 0 > model.end { attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(model.start, model.end - model.start + atRangeOffset)) } } } } } } } return attributeStr }
参考信息
消息构建和发送消息的示例代码可以参考:
- ChatViewController 中的 sendContentText 方法。
- @相关的数据组装和解析参考 NEBaseChatInputView。
- @消息接受和解析参考 NEBaseChatInputView 和 NEAtMessageManager。
此文档是否对你有帮助?