Фикс: сделал subtitle в списке чатов и текст in-app баннера в одну строку с truncate
This commit is contained in:
@@ -45,8 +45,8 @@ final class ChatListBottomInsetTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
/// Test 2: Verify automatic safe area adjustment is enabled
|
||||
func testContentInsetAdjustmentBehaviorIsAutomatic() {
|
||||
/// Test 2: Verify manual inset mode is enabled (UIKit auto-adjust disabled).
|
||||
func testContentInsetAdjustmentBehaviorIsNever() {
|
||||
_ = controller.view
|
||||
|
||||
let collectionView = controller.value(forKey: "collectionView") as? UICollectionView
|
||||
@@ -54,8 +54,8 @@ final class ChatListBottomInsetTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(
|
||||
collectionView?.contentInsetAdjustmentBehavior,
|
||||
.automatic,
|
||||
"Should use automatic adjustment (respects tab bar safe area)"
|
||||
.never,
|
||||
"Should use manual inset mode for custom tab-bar safe area handling"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,35 +19,90 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
/// Standalone replica of `ChatDetailView.replyPreviewText(for:)`.
|
||||
/// Must match the real implementation exactly.
|
||||
private func replyPreviewText(for message: ChatMessage) -> String {
|
||||
if message.attachments.contains(where: { $0.type == .image }) {
|
||||
let caption = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return caption.isEmpty ? "Photo" : caption
|
||||
}
|
||||
if let file = message.attachments.first(where: { $0.type == .file }) {
|
||||
let caption = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !caption.isEmpty { return caption }
|
||||
let parsed = AttachmentPreviewCodec.parseFilePreview(file.preview)
|
||||
if !parsed.fileName.isEmpty { return parsed.fileName }
|
||||
return file.id.isEmpty ? "File" : file.id
|
||||
}
|
||||
if message.attachments.contains(where: { $0.type == .avatar }) { return "Avatar" }
|
||||
if message.attachments.contains(where: { $0.type == .messages }) { return "Forwarded message" }
|
||||
if message.attachments.contains(where: { $0.type == .call }) { return "Call" }
|
||||
let trimmed = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty { return message.text }
|
||||
if !message.attachments.isEmpty { return "Attachment" }
|
||||
let attachmentLabel: String? = {
|
||||
for att in message.attachments {
|
||||
switch att.type {
|
||||
case .image:
|
||||
return "Photo"
|
||||
case .file:
|
||||
let parsed = AttachmentPreviewCodec.parseFilePreview(att.preview)
|
||||
if !parsed.fileName.isEmpty { return parsed.fileName }
|
||||
return att.id.isEmpty ? "File" : att.id
|
||||
case .avatar:
|
||||
return "Avatar"
|
||||
case .messages:
|
||||
return "Forwarded message"
|
||||
case .call:
|
||||
return "Call"
|
||||
case .voice:
|
||||
return "Voice message"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
let visibleText: String = {
|
||||
let stripped = message.text
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.filter { !$0.isASCII || $0.asciiValue! >= 0x20 }
|
||||
if MessageCellLayout.isGarbageOrEncrypted(stripped) { return "" }
|
||||
return stripped
|
||||
}()
|
||||
let visibleTextDecoded = EmojiParser.replaceShortcodes(in: visibleText)
|
||||
|
||||
if attachmentLabel != nil, !visibleTextDecoded.isEmpty { return visibleTextDecoded }
|
||||
if let label = attachmentLabel { return label }
|
||||
if !visibleTextDecoded.isEmpty { return visibleTextDecoded }
|
||||
return ""
|
||||
}
|
||||
|
||||
/// Standalone replica of SessionManager in-app banner preview logic.
|
||||
/// Intentionally mirrors current behavior for regression coverage.
|
||||
private func inAppBannerPreviewText(text: String, attachments: [MessageAttachment]) -> String {
|
||||
if !text.isEmpty { return EmojiParser.replaceShortcodes(in: text) }
|
||||
if let firstAtt = attachments.first {
|
||||
switch firstAtt.type {
|
||||
case .image: return "Photo"
|
||||
case .file: return "File"
|
||||
case .voice: return "Voice message"
|
||||
case .avatar: return "Avatar"
|
||||
case .messages: return "Forwarded message"
|
||||
case .call: return "Call"
|
||||
default: return "Attachment"
|
||||
}
|
||||
}
|
||||
return "New message"
|
||||
}
|
||||
|
||||
/// Standalone replica of NativeMessageList reply text cache logic for UIKit cells.
|
||||
/// When outer message is empty, it is treated as forwarded wrapper (no quote text shown).
|
||||
private func nativeReplyCacheText(outerMessageText: String, reply: ReplyMessageData) -> String? {
|
||||
let displayText = MessageCellLayout.isGarbageOrEncrypted(outerMessageText) ? "" : outerMessageText
|
||||
guard !displayText.isEmpty else { return nil }
|
||||
|
||||
let trimmed = reply.message.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !trimmed.isEmpty {
|
||||
return EmojiParser.replaceShortcodes(in: reply.message)
|
||||
}
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.image.rawValue }) { return "Photo" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.messages.rawValue }) { return "Forwarded message" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.file.rawValue }) { return "File" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.avatar.rawValue }) { return "Avatar" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.voice.rawValue }) { return "Voice message" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.call.rawValue }) { return "Call" }
|
||||
return "Attachment"
|
||||
}
|
||||
|
||||
/// Standalone replica of `MessageCellView.replyQuoteView` preview logic.
|
||||
/// Uses `ReplyMessageData` + `ReplyAttachmentData` (Int type), not `MessageAttachment`.
|
||||
private func replyQuotePreviewText(for reply: ReplyMessageData) -> String {
|
||||
let trimmed = reply.message.trimmingCharacters(in: .whitespaces)
|
||||
if !trimmed.isEmpty { return reply.message }
|
||||
if !trimmed.isEmpty { return EmojiParser.replaceShortcodes(in: reply.message) }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.image.rawValue }) { return "Photo" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.messages.rawValue }) { return "Forwarded message" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.file.rawValue }) { return "File" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.avatar.rawValue }) { return "Avatar" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.voice.rawValue }) { return "Voice message" }
|
||||
if reply.attachments.contains(where: { $0.type == AttachmentType.call.rawValue }) { return "Call" }
|
||||
return "Attachment"
|
||||
}
|
||||
@@ -63,6 +118,7 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
case .avatar: return "Avatar"
|
||||
case .messages: return "Forwarded message"
|
||||
case .call: return "Call"
|
||||
case .voice: return "Voice message"
|
||||
}
|
||||
} else if textIsEmpty {
|
||||
return ""
|
||||
@@ -195,6 +251,11 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Avatar")
|
||||
}
|
||||
|
||||
func testReplyBar_AvatarWithCaptionUsesCaption() {
|
||||
let msg = makeMessage(text: "Profile pic", attachments: [makeAttachment(type: .avatar)])
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Profile pic")
|
||||
}
|
||||
|
||||
// MARK: Forward
|
||||
|
||||
func testReplyBar_Forward() {
|
||||
@@ -202,6 +263,11 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Forwarded message")
|
||||
}
|
||||
|
||||
func testReplyBar_ForwardWithCaptionUsesCaption() {
|
||||
let msg = makeMessage(text: "See this", attachments: [makeAttachment(type: .messages)])
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "See this")
|
||||
}
|
||||
|
||||
// MARK: Call (Android/Desktop parity)
|
||||
|
||||
func testReplyBar_Call() {
|
||||
@@ -209,6 +275,21 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Call")
|
||||
}
|
||||
|
||||
func testReplyBar_CallWithCaptionUsesCaption() {
|
||||
let msg = makeMessage(text: "Missed you", attachments: [makeAttachment(type: .call)])
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Missed you")
|
||||
}
|
||||
|
||||
func testReplyBar_VoiceMessage() {
|
||||
let msg = makeMessage(attachments: [makeAttachment(type: .voice)])
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Voice message")
|
||||
}
|
||||
|
||||
func testReplyBar_VoiceWithCaptionUsesCaption() {
|
||||
let msg = makeMessage(text: "Listen", attachments: [makeAttachment(type: .voice)])
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Listen")
|
||||
}
|
||||
|
||||
// MARK: Text
|
||||
|
||||
func testReplyBar_TextOnly() {
|
||||
@@ -216,6 +297,13 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "Hello world")
|
||||
}
|
||||
|
||||
func testReplyBar_TextShortcodesAreDecodedToEmoji() {
|
||||
let msg = makeMessage(text: "ok :emoji_1f44d:")
|
||||
let preview = replyPreviewText(for: msg)
|
||||
XCTAssertFalse(preview.contains(":emoji_"))
|
||||
XCTAssertTrue(preview.contains(EmojiParser.unifiedToEmoji("1f44d")))
|
||||
}
|
||||
|
||||
func testReplyBar_Empty() {
|
||||
let msg = makeMessage()
|
||||
XCTAssertEqual(replyPreviewText(for: msg), "")
|
||||
@@ -281,6 +369,11 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(replyQuotePreviewText(for: reply), "Call")
|
||||
}
|
||||
|
||||
func testQuote_VoiceMessage() {
|
||||
let reply = makeReply(attachments: [makeReplyAttachment(type: AttachmentType.voice.rawValue)])
|
||||
XCTAssertEqual(replyQuotePreviewText(for: reply), "Voice message")
|
||||
}
|
||||
|
||||
func testQuote_UnknownType() {
|
||||
let reply = makeReply(attachments: [makeReplyAttachment(type: 99)])
|
||||
XCTAssertEqual(replyQuotePreviewText(for: reply), "Attachment")
|
||||
@@ -291,6 +384,13 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(replyQuotePreviewText(for: reply), "Some text")
|
||||
}
|
||||
|
||||
func testQuote_TextShortcodesAreDecodedToEmoji() {
|
||||
let reply = makeReply(message: "ok :emoji_1f44d:", attachments: [])
|
||||
let preview = replyQuotePreviewText(for: reply)
|
||||
XCTAssertFalse(preview.contains(":emoji_"))
|
||||
XCTAssertTrue(preview.contains(EmojiParser.unifiedToEmoji("1f44d")))
|
||||
}
|
||||
|
||||
func testQuote_WhitespaceText() {
|
||||
let reply = makeReply(message: " ", attachments: [makeReplyAttachment(type: 0)])
|
||||
XCTAssertEqual(replyQuotePreviewText(for: reply), "Photo")
|
||||
@@ -320,6 +420,10 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .call)]), "Call")
|
||||
}
|
||||
|
||||
func testChatList_Voice() {
|
||||
XCTAssertEqual(chatListPreviewText(text: "", attachments: [makeAttachment(type: .voice)]), "Voice message")
|
||||
}
|
||||
|
||||
func testChatList_TextWins() {
|
||||
XCTAssertEqual(chatListPreviewText(text: "Hello", attachments: [makeAttachment(type: .image)]), "Hello")
|
||||
}
|
||||
@@ -339,6 +443,41 @@ final class ReplyPreviewTextTests: XCTestCase {
|
||||
XCTAssertEqual(chatListPreviewText(text: control, attachments: [makeAttachment(type: .call)]), "Call")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - In-App Banner Preview (SessionManager)
|
||||
// =========================================================================
|
||||
|
||||
func testInAppBanner_TextShortcodesAreDecodedToEmoji() {
|
||||
let input = "ok :emoji_1f44d:"
|
||||
let preview = inAppBannerPreviewText(text: input, attachments: [])
|
||||
XCTAssertFalse(preview.contains(":emoji_"))
|
||||
XCTAssertTrue(preview.contains(EmojiParser.unifiedToEmoji("1f44d")))
|
||||
}
|
||||
|
||||
func testInAppBanner_VoiceAttachmentLabel() {
|
||||
let preview = inAppBannerPreviewText(text: "", attachments: [makeAttachment(type: .voice)])
|
||||
XCTAssertEqual(preview, "Voice message")
|
||||
}
|
||||
|
||||
func testInAppBanner_AvatarAttachmentLabel() {
|
||||
let preview = inAppBannerPreviewText(text: "", attachments: [makeAttachment(type: .avatar)])
|
||||
XCTAssertEqual(preview, "Avatar")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - Native Reply Cache (NativeMessageList UIKit path)
|
||||
// =========================================================================
|
||||
|
||||
func testNativeReplyCache_VoiceAttachmentLabel() {
|
||||
let reply = makeReply(message: "", attachments: [makeReplyAttachment(type: AttachmentType.voice.rawValue)])
|
||||
XCTAssertEqual(nativeReplyCacheText(outerMessageText: "fix", reply: reply), "Voice message")
|
||||
}
|
||||
|
||||
func testNativeReplyCache_DecodesEmojiShortcodes() {
|
||||
let reply = makeReply(message: "ok :emoji_1f44d:", attachments: [])
|
||||
XCTAssertEqual(nativeReplyCacheText(outerMessageText: "text", reply: reply), "ok 👍")
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// MARK: - AttachmentType JSON Decode Fault Tolerance
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user