diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a7dede0c9..38a10d3349c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix date separator decoration view showing in the last message of the current page [#2899](https://github.com/GetStream/stream-chat-swift/pull/2899) - Fix `JumpToUnreadMessagesButton` not localizable [#2917](https://github.com/GetStream/stream-chat-swift/pull/2917) - Fix CocoaPods minimum iOS target not in sync with the Xcode project [#2924](https://github.com/GetStream/stream-chat-swift/pull/2924) +- Fix quoting message without bubble view when text is only emojis [#2925](https://github.com/GetStream/stream-chat-swift/pull/2925) +- Fix user mention not tappable when contains "@" character [#2928](https://github.com/GetStream/stream-chat-swift/pull/2928) +- Fix user mention not tappable if user does not have a name [#2928](https://github.com/GetStream/stream-chat-swift/pull/2928) +- Fix edit action possible in giphy messages [#2926](https://github.com/GetStream/stream-chat-swift/pull/2926) - Fix not adding a space in the message input when mentioning a user [#2927](https://github.com/GetStream/stream-chat-swift/pull/2927) # [4.44.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.44.0) diff --git a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift index 052a15159d9..6164cac110f 100644 --- a/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift +++ b/Sources/StreamChatUI/ChatMessageList/ChatMessage/ChatMessageLayoutOptionsResolver.swift @@ -56,8 +56,9 @@ open class ChatMessageLayoutOptionsResolver { return [.text, .centered] } - // Do not show bubble if the message is to be rendered as large emoji - if !message.shouldRenderAsJumbomoji { + // Do not show bubble if the message is to be rendered as large emoji. + // Unless we are quoting a message, in this case the bubble should still be rendered. + if !message.shouldRenderAsJumbomoji || message.quotedMessage != nil { options.insert(.bubble) } diff --git a/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift b/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift index adf7fb50b1f..3c6baf0d1f7 100644 --- a/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift +++ b/Sources/StreamChatUI/MessageActionsPopup/ChatMessageActionsVC.swift @@ -121,9 +121,9 @@ open class ChatMessageActionsVC: _ViewController, ThemeProvider { actions.append(copyActionItem()) } - if canUpdateAnyMessage { + if canUpdateAnyMessage && message.giphyAttachments.isEmpty { actions.append(editActionItem()) - } else if canUpdateOwnMessage && message.isSentByCurrentUser { + } else if canUpdateOwnMessage && message.isSentByCurrentUser && message.giphyAttachments.isEmpty { actions.append(editActionItem()) } diff --git a/Sources/StreamChatUI/Utils/TextViewMentionedUsersHandler.swift b/Sources/StreamChatUI/Utils/TextViewMentionedUsersHandler.swift index 3f93639a062..901c5894023 100644 --- a/Sources/StreamChatUI/Utils/TextViewMentionedUsersHandler.swift +++ b/Sources/StreamChatUI/Utils/TextViewMentionedUsersHandler.swift @@ -23,7 +23,11 @@ class TextViewMentionedUsersHandler { else { return nil } - let name = String(text[range].replacingOccurrences(of: "@", with: "")) - return mentionedUsers.first(where: { $0.name == name }) + + let mention = String(text[range]) + return mentionedUsers.first(where: { + let name = $0.name ?? $0.id + return mention.contains(name) + }) } } diff --git a/StreamChat.xcodeproj/project.pbxproj b/StreamChat.xcodeproj/project.pbxproj index 90867235fda..a0cd0a24b60 100644 --- a/StreamChat.xcodeproj/project.pbxproj +++ b/StreamChat.xcodeproj/project.pbxproj @@ -1335,6 +1335,7 @@ ADA8EBE928CFD52F00DB9B03 /* TextViewUserMentionsHandler_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA8EBE828CFD52F00DB9B03 /* TextViewUserMentionsHandler_Mock.swift */; }; ADA8EBEB28CFD82C00DB9B03 /* ChatMessageContentViewDelegate_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADA8EBEA28CFD82C00DB9B03 /* ChatMessageContentViewDelegate_Mock.swift */; }; ADAA377125E43C3700C31528 /* ChatSuggestionsVC_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD5A9E725DE8AF6006DC88A /* ChatSuggestionsVC_Tests.swift */; }; + ADAA9F412B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADAA9F402B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift */; }; ADAC47AA275A7C960027B672 /* ChatMessageContentView_Documentation_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADAC47A9275A7C960027B672 /* ChatMessageContentView_Documentation_Tests.swift */; }; ADB22F7C25F1626200853C92 /* OnlineIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB22F7A25F1626200853C92 /* OnlineIndicatorView.swift */; }; ADB22F7D25F1626200853C92 /* ChatPresenceAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB22F7B25F1626200853C92 /* ChatPresenceAvatarView.swift */; }; @@ -3823,6 +3824,7 @@ ADA5A0F7276790C100E1C465 /* ChatMessageListDateSeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageListDateSeparatorView.swift; sourceTree = ""; }; ADA8EBE828CFD52F00DB9B03 /* TextViewUserMentionsHandler_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewUserMentionsHandler_Mock.swift; sourceTree = ""; }; ADA8EBEA28CFD82C00DB9B03 /* ChatMessageContentViewDelegate_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageContentViewDelegate_Mock.swift; sourceTree = ""; }; + ADAA9F402B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewMentionedUsersHandler_Tests.swift; sourceTree = ""; }; ADAC47A9275A7C960027B672 /* ChatMessageContentView_Documentation_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageContentView_Documentation_Tests.swift; sourceTree = ""; }; ADB22F7A25F1626200853C92 /* OnlineIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnlineIndicatorView.swift; sourceTree = ""; }; ADB22F7B25F1626200853C92 /* ChatPresenceAvatarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresenceAvatarView.swift; sourceTree = ""; }; @@ -7193,6 +7195,7 @@ A3960E0427DA5512003AB2B0 /* Utils */ = { isa = PBXGroup; children = ( + ADAA9F402B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift */, AD4C15552A55874700A32955 /* ImageLoading_Tests.swift */, BDDD1EAB2632E32000BA007B /* AppearanceProvider_Tests.swift */, C1320E07276B2E0800A06B35 /* Array+SafeSubscript_Tests.swift */, @@ -10041,6 +10044,7 @@ 401105462A12735900F877C7 /* WaveformView_Tests.swift in Sources */, ADEE651E29BF715600186129 /* ChatMessageListVCDelegate_Mock.swift in Sources */, AD050BA8265D600B006649A5 /* QuotedChatMessageView+SwiftUI_Tests.swift in Sources */, + ADAA9F412B2240300078C3D4 /* TextViewMentionedUsersHandler_Tests.swift in Sources */, 40824D2F2A1271D7003B61FD /* RecordButton_Tests.swift in Sources */, A3D9D69627EDE87900725066 /* Components_Mock.swift in Sources */, AD25070D272C0C8D00BC14C4 /* ChatMessageReactionAuthorsVC_Tests.swift in Sources */, diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift index e1d3e8a6910..d85c22a620a 100644 --- a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/ChatChannelVC_Tests.swift @@ -192,6 +192,31 @@ final class ChatChannelVC_Tests: XCTestCase { ) } + func test_onlyEmojiMessageAppearance_whenQuotingMessage() { + let quotedMessage = ChatMessage.mock(text: "Hello") + channelControllerMock.simulateInitial( + channel: .mock(cid: .unique), + messages: [ + .mock(id: .unique, cid: .unique, text: "👍🏻💯", author: .mock(id: .unique), quotedMessage: quotedMessage), + .mock(id: .unique, cid: .unique, text: "Simple text", author: .mock(id: .unique), isSentByCurrentUser: true), + .mock( + id: .unique, + cid: .unique, + text: "🚀", + author: .mock(id: .unique), + quotedMessage: quotedMessage, + isSentByCurrentUser: true + ), + quotedMessage + ], + state: .localDataFetched + ) + AssertSnapshot( + vc, + isEmbeddedInNavigationController: true + ) + } + func test_whenShouldMessagesStartAtTheTopIsTrue() { var components = Components.mock components.shouldMessagesStartAtTheTop = true diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.default-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.default-light.png new file mode 100644 index 00000000000..8d0a1b56d70 Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.default-light.png differ diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.extraExtraExtraLarge-light.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.extraExtraExtraLarge-light.png new file mode 100644 index 00000000000..55dbba48b5f Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.extraExtraExtraLarge-light.png differ diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.rightToLeftLayout-default.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.rightToLeftLayout-default.png new file mode 100644 index 00000000000..e3b08507b76 Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.rightToLeftLayout-default.png differ diff --git a/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.small-dark.png b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.small-dark.png new file mode 100644 index 00000000000..9b32810efc0 Binary files /dev/null and b/Tests/StreamChatUITests/SnapshotTests/ChatChannel/__Snapshots__/ChatChannelVC_Tests/test_onlyEmojiMessageAppearance_whenQuotingMessage.small-dark.png differ diff --git a/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift b/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift index a0d977d9a03..110cf397c47 100644 --- a/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift +++ b/Tests/StreamChatUITests/SnapshotTests/MessageActionsPopup/ChatMessageActionsVC_Tests.swift @@ -207,6 +207,18 @@ final class ChatMessageActionsVC_Tests: XCTestCase { XCTAssertFalse(vc.messageActions.contains(where: { $0 is EditActionItem })) } + func test_messageActions_whenUpdateOwnMessage_whenGiphy_thenDoesNotContainEditAction() { + chatMessageController.simulateInitial( + message: ChatMessage.mock(attachments: [makeGiphyAttachmentPayload()], isSentByCurrentUser: true), + replies: [], + state: .remoteDataFetched + ) + + vc.channel = .mock(cid: .unique, ownCapabilities: [.updateOwnMessage]) + + XCTAssertFalse(vc.messageActions.contains(where: { $0 is EditActionItem })) + } + func test_messageActions_whenUpdateAnyMessage_messageIsSentByCurrentUser_thenContainsEditAction() { chatMessageController.simulateInitial( message: ChatMessage.mock(isSentByCurrentUser: true), @@ -219,6 +231,18 @@ final class ChatMessageActionsVC_Tests: XCTestCase { XCTAssertTrue(vc.messageActions.contains(where: { $0 is EditActionItem })) } + func test_messageActions_whenUpdateAnyMessage_whenGiphy_thenDoesNotContainEditAction() { + chatMessageController.simulateInitial( + message: ChatMessage.mock(attachments: [makeGiphyAttachmentPayload()], isSentByCurrentUser: true), + replies: [], + state: .remoteDataFetched + ) + + vc.channel = .mock(cid: .unique, ownCapabilities: [.updateAnyMessage]) + + XCTAssertFalse(vc.messageActions.contains(where: { $0 is EditActionItem })) + } + func test_messageActions_whenUpdateAnyMessage_messageIsSentByAnotherUser_thenContainsEditAction() { chatMessageController.simulateInitial( message: ChatMessage.mock(isSentByCurrentUser: false), @@ -385,6 +409,20 @@ final class ChatMessageActionsVC_Tests: XCTestCase { } } +// MARK: - Helpers + +private extension ChatMessageActionsVC_Tests { + func makeGiphyAttachmentPayload() -> AnyChatMessageAttachment { + .dummy( + type: .giphy, + payload: try! JSONEncoder.stream.encode(GiphyAttachmentPayload( + title: nil, + previewURL: URL.localYodaImage + )) + ) + } +} + private extension UIViewController { /// `ChatMessageActionsVC` is not used as a root view controller, so we embed it to snapshot its more realistic size. func embedded() -> UIViewController { diff --git a/Tests/StreamChatUITests/Utils/TextViewMentionedUsersHandler_Tests.swift b/Tests/StreamChatUITests/Utils/TextViewMentionedUsersHandler_Tests.swift new file mode 100644 index 00000000000..bf6a79b12ea --- /dev/null +++ b/Tests/StreamChatUITests/Utils/TextViewMentionedUsersHandler_Tests.swift @@ -0,0 +1,80 @@ +// +// Copyright © 2023 Stream.io Inc. All rights reserved. +// + +@testable import StreamChat +@testable import StreamChatUI +import XCTest + +final class TextViewMentionedUsersHandler_Tests: XCTestCase { + func test_mentionedUserTapped_whenRangeIncludesMention() { + let textView = UITextView() + textView.text = "@Leia Hello!" + + let sut = TextViewMentionedUsersHandler() + let user = sut.mentionedUserTapped( + on: textView, + in: .init(location: 0, length: 5), + with: [.mock(id: "leia", name: "Leia")] + ) + + XCTAssertEqual(user?.name, "Leia") + } + + func test_mentionedUserTapped_whenRangeDoesNotIncludeMention() { + let textView = UITextView() + textView.text = "@Leia Hello!" + + let sut = TextViewMentionedUsersHandler() + let user = sut.mentionedUserTapped( + on: textView, + in: .init(location: 3, length: 7), + with: [.mock(id: "leia", name: "Leia")] + ) + + XCTAssertEqual(user?.name, nil) + } + + func test_mentionedUserTapped_whenIncludesSpecialCharacter() { + let textView = UITextView() + textView.text = "@Lei@ Hello!" + + let sut = TextViewMentionedUsersHandler() + let user = sut.mentionedUserTapped( + on: textView, + in: .init(location: 0, length: 5), + with: [.mock(id: "leia", name: "Lei@")] + ) + + XCTAssertEqual(user?.name, "Lei@") + } + + // Customers can customise how mentions are presented, and so they can chose not to show it. + func test_mentionedUserTapped_whenAtSignIsNotPresent() { + let textView = UITextView() + textView.text = "Lei@ Hello!" + + let sut = TextViewMentionedUsersHandler() + let user = sut.mentionedUserTapped( + on: textView, + in: .init(location: 0, length: 5), + with: [.mock(id: "leia", name: "Lei@")] + ) + + XCTAssertEqual(user?.name, "Lei@") + } + + func test_mentionedUserTapped_whenUserDoesNotHaveName() { + let textView = UITextView() + textView.text = "leia Hello!" + + let sut = TextViewMentionedUsersHandler() + let user = sut.mentionedUserTapped( + on: textView, + in: .init(location: 0, length: 5), + with: [.mock(id: "leia", name: nil)] + ) + + XCTAssertEqual(user?.id, "leia") + } +}