@消息实现方案

更新时间: 2025/02/24 15:55:16

若您需要实现简单的 @ 功能,可以参考网易云信在 IM UIKit 上使用的艾特(@)逻辑,以实现简单的类微信的 @ 功能。

效果展示

@消息是一种即时通讯功能,用户可以在群聊或频道中通过“@”符号提及特定用户或角色,被提及的用户会收到提醒通知,从而快速注意到相关消息。效果如下图所示:

image.png

方案介绍

@ 消息通过扩展参数,保留 @ 消息的相关内容。在消息体 NIMMessage 中可以通过 remoteExt 来获取和设置消息体中的远程传输的扩展参数,@ 消息的内容就以 Object 形式保存在该消息体中,数据格式如下:

JSON//@消息的 key 值
"yxAitMsg": {
    //被@的账号,如果是 @All 则为 ait_all
    "332917623668992": {
        "text": "@昵称 01 ",//在消息中@的展示内容
        "segments": [{//在消息中@的展示位置
            "start": 0,//在消息中@的展示起始位置
            "end": 5,//在消息中@的展示终止位置
        }]
    }
}

在发送一条 @ 消息时,会将上述 @ 消息的内容,设置到该消息的 remoteExt 中。

实现流程

  1. 创建一条文本消息。

    Swift// text 代表要发送的内容
    let message = MessageUtils.textMessage(text: text)
    
  2. 设置 @ 信息和推送配置。

    Swiftopen 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 方法。

  3. 发送消息。

    Swiftlet params = ChatRepo.shared.getSendMessageParams(aiUserAccid, message)
    ChatRepo.shared.sendMessage(message: message,
                                conversationId: conversationId,
                                params: params,
                                completion)
    
  4. 接受方对接收到的消息进行解析。

    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。
此文档是否对你有帮助?
有帮助
去反馈
  • 效果展示
  • 方案介绍
  • 实现流程
  • 参考信息