Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dismiss thread when hard deleting root message #3569

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
# Upcoming

## StreamChat
### ✅ Added
- Add `MessageHardDeletedEvent` [#3569](https://github.com/GetStream/stream-chat-swift/pull/3569)
nuno-vieira marked this conversation as resolved.
Show resolved Hide resolved
- Expose `Event.name` to easily check which event it is [#3569](https://github.com/GetStream/stream-chat-swift/pull/3569)
### 🐞 Fixed
- Calling async `connectUser()` methods can sometimes throw `CurrentUserDoesNotExist()` unexpectedly [#3565](https://github.com/GetStream/stream-chat-swift/pull/3565)
- Fix creating controllers from background threads leading to rare crashes [#3566](https://github.com/GetStream/stream-chat-swift/pull/3566)
Expand All @@ -12,6 +15,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `ChatMessage.replacing()`
- Add `ChatChannel.replacing()`
- Add `ChatChannelMember.replacing()`
- Fix hard deleted message events not being reported in `EventsController` [#3569](https://github.com/GetStream/stream-chat-swift/pull/3569)
### 🔄 Changed
- Deprecates `MessageDeletedEvent.isHardDeleted` [#3569](https://github.com/GetStream/stream-chat-swift/pull/3569)
nuno-vieira marked this conversation as resolved.
Show resolved Hide resolved

## StreamChatUI
### ✅ Added
Expand All @@ -26,6 +32,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### 🐞 Fixed
- Fix thread reply action shown when inside a Thread [#3561](https://github.com/GetStream/stream-chat-swift/pull/3561)
- Fix reaction author's view with shrinked reaction images in iOS 18 [#3568](https://github.com/GetStream/stream-chat-swift/pull/3568)
- Fix duplicated `didReceiveEvent` inside `ChatThreadVC` [#3569](https://github.com/GetStream/stream-chat-swift/pull/3569)
### 🔄 Changed
- Deprecates `ChatThreadVC.channelEventsController` [#3569](https://github.com/GetStream/stream-chat-swift/pull/3569)

# [4.70.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.70.0)
_January 14, 2025_
Expand Down
24 changes: 24 additions & 0 deletions DemoApp/StreamChat/Components/DemoChatThreadVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,30 @@ class DemoChatThreadVC: ChatThreadVC, CurrentChatUserControllerDelegate {
])
}

// MARK: - Dismissing the thread if the root message was hard deleted.

override func messageController(
_ controller: ChatMessageController,
didChangeReplies changes: [ListChange<ChatMessage>]
) {
// If the message is hard deleted, do not update the UI
// (Otherwise it would should the root message disappearing)
nuno-vieira marked this conversation as resolved.
Show resolved Hide resolved
if controller.message == nil {
navigationController?.popViewController(animated: true)
return
}
super.messageController(controller, didChangeReplies: changes)
}

override func eventsController(_ controller: EventsController, didReceiveEvent event: any Event) {
super.eventsController(controller, didReceiveEvent: event)

// Dismiss the thread if the root message was hard deleted.
if let event = event as? MessageHardDeletedEvent, event.messageId == messageController.messageId {
navigationController?.popViewController(animated: true)
}
}

// MARK: - Loading previous and next messages state handling.

override func loadPreviousReplies(completion: @escaping (Error?) -> Void) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChat/WebSocketClient/Events/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation
/// An `Event` object representing an event in the chat system.
public protocol Event {}

extension Event {
public extension Event {
var name: String {
String(describing: Self.self).replacingOccurrences(of: "DTO", with: "")
}
Expand Down
44 changes: 38 additions & 6 deletions Sources/StreamChat/WebSocketClient/Events/MessageEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,28 @@ public struct MessageDeletedEvent: ChannelSpecificEvent {
public let createdAt: Date

/// A Boolean value indicating whether it is an hard delete or not.
@available(*, deprecated, message: "The `MessageHardDeletedEvent` should be used instead.")
public let isHardDelete: Bool
}

/// Triggered when a new message is hard deleted.
public struct MessageHardDeletedEvent: ChannelSpecificEvent {
/// The user who deleted the message.
public let user: ChatUser?

/// The channel identifier a message was deleted from.
public var cid: ChannelId { channel.cid }

/// The channel a message was deleted from.
public let channel: ChatChannel

/// The hard deleted message id.
public let messageId: MessageId

/// The event timestamp.
public let createdAt: Date
}

class MessageDeletedEventDTO: EventDTO {
let user: UserPayload?
let cid: ChannelId
Expand All @@ -154,14 +173,27 @@ class MessageDeletedEventDTO: EventDTO {
}

func toDomainEvent(session: DatabaseSession) -> Event? {
guard
let messageDTO = session.message(id: message.id),
let channelDTO = session.channel(cid: cid),
let userDTO = user.flatMap({ session.user(id: $0.id) })
else { return nil }
guard let channelDTO = session.channel(cid: cid) else {
return nil
}

let userDTO = user.flatMap { session.user(id: $0.id) }

if hardDelete {
return try? MessageHardDeletedEvent(
user: userDTO?.asModel(),
channel: channelDTO.asModel(),
messageId: message.id,
createdAt: createdAt
)
}

guard let messageDTO = session.message(id: message.id) else {
return nil
}

return try? MessageDeletedEvent(
user: userDTO.asModel(),
user: userDTO?.asModel(),
channel: channelDTO.asModel(),
message: messageDTO.asModel(),
createdAt: createdAt,
Expand Down
2 changes: 1 addition & 1 deletion Sources/StreamChatUI/ChatThread/ChatThreadVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ open class ChatThreadVC: _ViewController,
public var initialReplyId: MessageId?

/// Controller for observing typing events for this thread.
@available(*, deprecated, message: "Events are now handled by the `eventsController`.")
nuno-vieira marked this conversation as resolved.
Show resolved Hide resolved
open lazy var channelEventsController: ChannelEventsController = client.channelEventsController(for: messageController.cid)

/// A controller for observing web socket events.
Expand Down Expand Up @@ -123,7 +124,6 @@ open class ChatThreadVC: _ViewController,
}

messageController.delegate = self
channelEventsController.delegate = self
eventsController.delegate = self

messageListVC.swipeToReplyGestureHandler.onReply = { [weak self] message in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,9 @@ class DatabaseSession_Mock: DatabaseSession {
underlyingSession.unpin(message: message)
}

var mockedMessageDTO: MessageDTO?
func message(id: MessageId) -> MessageDTO? {
underlyingSession.message(id: id)
mockedMessageDTO ?? underlyingSession.message(id: id)
}

func messageExists(id: MessageId) -> Bool {
Expand Down Expand Up @@ -297,9 +298,10 @@ class DatabaseSession_Mock: DatabaseSession {
try throwErrorIfNeeded()
return try underlyingSession.saveChannel(payload: payload, query: query, cache: cache)
}


var mockedChannelDTO: ChannelDTO?
func channel(cid: ChannelId) -> ChannelDTO? {
underlyingSession.channel(cid: cid)
mockedChannelDTO ?? underlyingSession.channel(cid: cid)
}

func saveMember(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,34 @@ final class MessageEvents_Tests: XCTestCase {
XCTAssertEqual(event?.hardDelete, true)
}

func test_messageDeletedEvent_toDomainEvent_thenIsMessageDeletedEvent() throws {
let json = XCTestCase.mockData(fromJSONFile: "MessageDeleted")
let event = try eventDecoder.decode(from: json) as? MessageDeletedEventDTO

let channelId = try XCTUnwrap(event?.cid)
let message = try XCTUnwrap(event?.message)
let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
_ = try session.saveChannel(payload: .dummy(cid: channelId), query: nil, cache: nil)
_ = try session.saveMessage(payload: message, for: channelId, cache: nil)

let domainEvent = event?.toDomainEvent(session: session)
XCTAssertEqual(domainEvent is MessageDeletedEvent, true)
}

func test_messageDeletedEvent_toDomainEvent_whenIsHardDeleted_thenIsMessageHardDeletedEvent() throws {
let json = XCTestCase.mockData(fromJSONFile: "MessageDeletedHard")
let event = try eventDecoder.decode(from: json) as? MessageDeletedEventDTO

let channelId = try XCTUnwrap(event?.cid)
let message = try XCTUnwrap(event?.message)
let session = DatabaseContainer_Spy(kind: .inMemory).viewContext
_ = try session.saveChannel(payload: .dummy(cid: channelId), query: nil, cache: nil)
_ = try session.saveMessage(payload: message, for: channelId, cache: nil)

let domainEvent = event?.toDomainEvent(session: session)
XCTAssertEqual(domainEvent is MessageHardDeletedEvent, true)
}

func test_read() throws {
let json = XCTestCase.mockData(fromJSONFile: "MessageRead")
let event = try eventDecoder.decode(from: json) as? MessageReadEventDTO
Expand Down
Loading