Skip to content

Commit d15ad21

Browse files
zsw666张诗文
andauthored
v10.9.20 (#457)
Co-authored-by: 张诗文 <zhangshiwen@corp.netease.com>
1 parent 4e00312 commit d15ad21

109 files changed

Lines changed: 6089 additions & 27 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,16 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD
494494

495495
expandMoreAction()
496496

497+
// P2P 会话:异步精确校验对方是否为机器人
498+
// 由于 isRobot 是同步缓存读取,首次进入时缓存可能为空或过期,
499+
// 通过 checkIfRobot 完成后刷新 moreItems,确保音视频按钮显示正确
500+
if V2NIMConversationIdUtil.conversationType(ChatRepo.conversationId) == .CONVERSATION_TYPE_P2P {
501+
NEAIRobotManager.shared.checkIfRobot(ChatRepo.sessionId) { [weak self] _ in
502+
// 无论结果如何,重新计算一次 moreItems(isRobot 缓存已在 checkIfRobot 中更新)
503+
self?.expandMoreAction()
504+
}
505+
}
506+
497507
view.addSubview(jumpDownView)
498508
jumpDownViewRightAncher = jumpDownView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16)
499509
jumpDownViewRightAncher?.isActive = true
@@ -610,6 +620,9 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD
610620

611621
NotificationCenter.default.addObserver(self, selector: #selector(reLoadMoreWithMessage), name: NENotificationName.friendCacheInit, object: nil)
612622

623+
// Markdown 图片下载完成后刷新对应消息 Cell
624+
NotificationCenter.default.addObserver(self, selector: #selector(onMarkdownImageDidLoad(_:)), name: NEMarkdownImageDidLoadNotification, object: nil)
625+
613626
if let pan = navigationController?.interactivePopGestureRecognizer {
614627
tableView.panGestureRecognizer.require(toFail: pan)
615628
}
@@ -650,6 +663,28 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD
650663
isCurrentPage = true
651664
}
652665

666+
/// Markdown 图片下载完成回调
667+
/// 遍历消息列表找到包含该图片 URL 的 AI 流式文本消息,重算高度后刷新对应行
668+
open func onMarkdownImageDidLoad(_ notification: Notification) {
669+
guard let url = notification.userInfo?["url"] as? String, !url.isEmpty else { return }
670+
671+
var reloadIndexPaths = [IndexPath]()
672+
for (index, model) in viewModel.messages.enumerated() {
673+
guard model.type == .aiStreamText,
674+
let textModel = model as? MessageAIStreamModel,
675+
textModel.message?.text?.contains(url) == true else {
676+
continue
677+
}
678+
// 重新解析 Markdown 并重算高度(图片已缓存,此次直接命中)
679+
textModel.resetMessage(textModel.message)
680+
reloadIndexPaths.append(IndexPath(row: index, section: 0))
681+
}
682+
683+
if !reloadIndexPaths.isEmpty {
684+
tableViewReloadIndexs(reloadIndexPaths)
685+
}
686+
}
687+
653688
// MARK: - 子类可重写方法
654689

655690
open func onTeamMemberChange(team: V2NIMTeam) {}
@@ -681,7 +716,21 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD
681716
)
682717
return
683718
}
684-
if let uid = ChatMessageHelper.getSenderId(model?.message) {
719+
guard let uid = ChatMessageHelper.getSenderId(model?.message) else { return }
720+
721+
// P2P 会话中,使用 checkIfRobot 精确判断对方是否为机器人:
722+
// 若是机器人则以"去聊天"模式打开名片页(隐藏"添加好友"按钮)
723+
if V2NIMConversationIdUtil.conversationType(ChatRepo.conversationId) == .CONVERSATION_TYPE_P2P {
724+
NEAIRobotManager.shared.checkIfRobot(uid) { [weak self] isRobot in
725+
guard let self = self else { return }
726+
var params: [String: Any] = ["nav": self.navigationController as Any, "uid": uid]
727+
if isRobot {
728+
// 机器人名片:将"添加好友"替换为"去聊天"
729+
params["isRobot"] = true
730+
}
731+
Router.shared.use(ContactUserInfoPageRouter, parameters: params, closure: nil)
732+
}
733+
} else {
685734
Router.shared.use(
686735
ContactUserInfoPageRouter,
687736
parameters: ["nav": navigationController as Any, "uid": uid],
@@ -3214,7 +3263,8 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD
32143263
}
32153264
}
32163265

3217-
if NEAIUserManager.shared.isAIUser(ChatRepo.sessionId) {
3266+
if NEAIUserManager.shared.isAIUser(ChatRepo.sessionId) ||
3267+
NEAIRobotManager.shared.isRobot(ChatRepo.sessionId) {
32183268
items = items.filter { item in
32193269
if item.type == .rtc {
32203270
return false

NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,14 @@ public class ChatMessageHelper: NSObject {
165165
case .MESSAGE_TYPE_VIDEO:
166166
model = MessageVideoModel(message: message)
167167
case .MESSAGE_TYPE_TEXT:
168+
// 数字人回复的消息使用 Markdown 解析
168169
if message.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_RESPONSE {
169170
return MessageAIStreamModel(message: message)
170171
}
172+
// 机器人回复的消息也使用 Markdown 解析(与数字人一致)
173+
if let senderId = message.senderId, NEAIRobotManager.shared.isRobot(senderId) {
174+
return MessageAIStreamModel(message: message)
175+
}
171176
model = MessageTextModel(message: message)
172177
case .MESSAGE_TYPE_IMAGE:
173178
model = MessageImageModel(message: message)
@@ -221,10 +226,16 @@ public class ChatMessageHelper: NSObject {
221226
model = MessageVideoModel(message: message)
222227
completion(model)
223228
case .MESSAGE_TYPE_TEXT:
229+
// 数字人回复的消息使用 Markdown 解析
224230
if message.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_RESPONSE {
225231
completion(MessageAIStreamModel(message: message))
226232
return
227233
}
234+
// 机器人回复的消息也使用 Markdown 解析(与数字人一致)
235+
if let senderId = message.senderId, NEAIRobotManager.shared.isRobot(senderId) {
236+
completion(MessageAIStreamModel(message: message))
237+
return
238+
}
228239
model = MessageTextModel(message: message)
229240
completion(model)
230241
case .MESSAGE_TYPE_IMAGE:

NEChatUIKit/NEChatUIKit/Classes/Chat/HistorySearch/NEHistorySearchMonthHeaderView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class NEHistorySearchMonthHeaderView: UICollectionReusableView {
1515

1616
lazy var separatorView: UIView = {
1717
let view = UIView()
18-
view.backgroundColor = UIColor(hexString: "#E5E5E5")
18+
view.backgroundColor = .funChatLineBorderColor
1919
return view
2020
}()
2121

NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAIStreamModel.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ open class MessageAIStreamModel: MessageTextModel {
2929

3030
let NEMarkdownParser = NEMarkdownParser(font: .systemFont(ofSize: 16))
3131
NEMarkdownParser.code.textHighlightColor = .black
32+
// 表格宽度 = 消息最大宽度 - 气泡内边距(左右各 chat_cell_margin + 内边距 chat_content_margin)
33+
let contentMaxW = chat_content_maxW - chat_content_margin * 2
34+
NEMarkdownParser.table.maxWidth = contentMaxW
35+
// 图片宽度不超过气泡内容区域
36+
NEMarkdownParser.markdownImage.maxWidth = contentMaxW
37+
NEMarkdownParser.markdownImage.maxHeight = contentMaxW
3238
// NEMarkdownParser.code.font = UIFont(name: "Times New Roman", size: 16)
3339
let mulAttr = NEMarkdownParser.parse(message?.text ?? "")
3440
attributeStr = NSMutableAttributedString(attributedString: mulAttr)

NEChatUIKit/NEChatUIKit/Classes/Markdown/NEMarkdownCodeEscaping.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,34 @@
55
import Foundation
66

77
open class NEMarkdownCodeEscaping: NEMarkdownElement {
8-
fileprivate static let regex = "(\\s+|^)(?<!\\\\)(?:\\\\\\\\)*+(\\`+)(.+?)(\\2)"
8+
// 两个分支合并为一个正则,不使用 .dotMatchesLineSeparators:
9+
// 分支1 — 多反引号(2+):内容可跨行(用 [\s\S]+? 匹配包含换行的任意字符)
10+
// groups: (2)=backticks (3)=content (4)=closing backticks
11+
// 分支2 — 单反引号:内容不含换行和反引号([^\n`]+),防止跨行误吞 Markdown 语法
12+
// groups: (5)=backtick (6)=content (7)=closing backtick
13+
fileprivate static let regex = "(\\s+|^)(?<!\\\\)(?:\\\\\\\\)*+(?:(\\`{2,})([\\s\\S]+?)(\\2)|(\\`)([^\\n`]+)(\\5))"
914

1015
open var regex: String {
1116
NEMarkdownCodeEscaping.regex
1217
}
1318

1419
open func regularExpression() throws -> NSRegularExpression {
15-
try NSRegularExpression(pattern: regex, options: .dotMatchesLineSeparators)
20+
// 不使用 .dotMatchesLineSeparators,跨行逻辑已在正则内部通过 [\s\S] 处理
21+
try NSRegularExpression(pattern: regex, options: [])
1622
}
1723

1824
open func match(_ match: NSTextCheckingResult, attributedString: NSMutableAttributedString) {
19-
let range = match.range(at: 3)
25+
// 根据匹配的分支确定 content 所在的 group 索引
26+
// 分支1(多反引号):group 3
27+
// 分支2(单反引号):group 6
28+
let contentGroupIndex: Int
29+
if match.numberOfRanges > 2, match.range(at: 2).location != NSNotFound {
30+
contentGroupIndex = 3
31+
} else {
32+
contentGroupIndex = 6
33+
}
34+
35+
let range = match.range(at: contentGroupIndex)
2036
// escaping all characters
2137
let matchString = attributedString.attributedSubstring(from: range).string
2238
let escapedString = [UInt16](matchString.utf16)
@@ -26,4 +42,4 @@ open class NEMarkdownCodeEscaping: NEMarkdownElement {
2642
}
2743
attributedString.replaceCharacters(in: range, with: escapedString)
2844
}
29-
}
45+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) 2022 NetEase, Inc. All rights reserved.
2+
// Use of this source code is governed by a MIT license that can be
3+
// found in the LICENSE file.
4+
5+
import UIKit
6+
7+
/// 分隔线渲染 Element。
8+
///
9+
/// 支持标准 GFM 分隔线语法:
10+
/// ```
11+
/// ---
12+
/// ***
13+
/// ___
14+
/// - - -
15+
/// * * *
16+
/// ```
17+
/// (单独一行,由 3 个或以上 `-` `*` `_` 组成,允许中间有空格)
18+
///
19+
/// 渲染效果:将分隔线替换为一行视觉横线。
20+
/// 使用自定义 `NSTextAttachment`,在 `attachmentBounds` 中根据
21+
/// `proposedLineFragment` 自适应气泡宽度,不会超出右边界。
22+
open class NEMarkdownHorizontalRule: NEMarkdownElement {
23+
// 匹配以 - * _ 组成的分隔线(3 个或以上,允许字符间有空格,整行只有这些字符)
24+
public let regex = "^[ \\t]*([\\-\\*\\_][ \\t]*){3,}[ \\t]*$"
25+
26+
public func regularExpression() throws -> NSRegularExpression {
27+
try NSRegularExpression(pattern: regex, options: [.anchorsMatchLines])
28+
}
29+
30+
// MARK: - 样式配置
31+
32+
/// 分隔线颜色
33+
open var lineColor: UIColor = UIColor(red: 0.78, green: 0.78, blue: 0.78, alpha: 1)
34+
/// 分隔线高度(pt)
35+
open var lineHeight: CGFloat = 1.0
36+
/// 分隔线上下的额外间距
37+
open var verticalPadding: CGFloat = 6.0
38+
39+
public init(lineColor: UIColor? = nil) {
40+
if let c = lineColor { self.lineColor = c }
41+
}
42+
43+
// MARK: - NEMarkdownElement
44+
45+
public func match(_ match: NSTextCheckingResult,
46+
attributedString: NSMutableAttributedString) {
47+
let attachment = NEMarkdownHorizontalRuleAttachment(
48+
lineColor: lineColor,
49+
lineHeight: lineHeight,
50+
verticalPadding: verticalPadding
51+
)
52+
let lineAttrStr = NSAttributedString(attachment: attachment)
53+
attributedString.replaceCharacters(in: match.range, with: lineAttrStr)
54+
}
55+
}
56+
57+
// MARK: - 自适应宽度的分隔线 Attachment
58+
59+
/// 在 `attachmentBounds` 阶段根据 `proposedLineFragment` 动态计算宽度,
60+
/// 确保分隔线始终与气泡内容区域等宽,不会超出右边界。
61+
private class NEMarkdownHorizontalRuleAttachment: NSTextAttachment {
62+
let lineColor: UIColor
63+
let lineHeight: CGFloat
64+
let verticalPadding: CGFloat
65+
66+
init(lineColor: UIColor, lineHeight: CGFloat, verticalPadding: CGFloat) {
67+
self.lineColor = lineColor
68+
self.lineHeight = lineHeight
69+
self.verticalPadding = verticalPadding
70+
super.init(data: nil, ofType: nil)
71+
}
72+
73+
required init?(coder: NSCoder) {
74+
fatalError("init(coder:) is not supported")
75+
}
76+
77+
/// 根据所在文本容器的行宽自适应 Attachment 尺寸
78+
override func attachmentBounds(for textContainer: NSTextContainer?,
79+
proposedLineFragment lineFrag: CGRect,
80+
glyphPosition position: CGPoint,
81+
characterIndex charIndex: Int) -> CGRect {
82+
// 宽度 = 当前行的可用宽度(自动匹配气泡宽度)
83+
let width = lineFrag.width
84+
let height = lineHeight + verticalPadding * 2
85+
return CGRect(origin: .zero, size: CGSize(width: width, height: height))
86+
}
87+
88+
/// 按实际 bounds 尺寸动态绘制横线图片
89+
override func image(forBounds imageBounds: CGRect,
90+
textContainer: NSTextContainer?,
91+
characterIndex charIndex: Int) -> UIImage? {
92+
let size = imageBounds.size
93+
guard size.width > 0, size.height > 0 else { return nil }
94+
let renderer = UIGraphicsImageRenderer(size: size)
95+
return renderer.image { ctx in
96+
lineColor.setFill()
97+
let lineRect = CGRect(x: 0,
98+
y: verticalPadding,
99+
width: size.width,
100+
height: lineHeight)
101+
ctx.fill(lineRect)
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)