Skip to content

Fix draft not deleted when attachments are removed from the composer #791

New issue

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

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

Already on GitHub? # to your account

Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### ✅ Added
- Add `minOriginY` to the initializer of `ReactionsOverlayView` for better UI customization [#793](https://github.com/GetStream/stream-chat-swiftui/pull/793)
### 🔄 Changed
### 🐞 Fixed
- Fix draft not deleted when attachments are removed from the composer [#791](https://github.com/GetStream/stream-chat-swiftui/pull/791)

# [4.75.0](https://github.com/GetStream/stream-chat-swiftui/releases/tag/4.75.0)
_March 26, 2025_
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ open class MessageComposerViewModel: ObservableObject {
@Published public private(set) var addedAssets = [AddedAsset]() {
didSet {
checkPickerSelectionState()

if shouldDeleteDraftMessage(oldValue: oldValue) {
deleteDraftMessage()
}
}
}

Expand All @@ -55,7 +59,7 @@ open class MessageComposerViewModel: ObservableObject {
suggestions = [String: Any]()
mentionedUsers = Set<ChatUser>()

if oldValue != "" && !sendButtonEnabled {
if shouldDeleteDraftMessage(oldValue: oldValue) {
deleteDraftMessage()
}
}
Expand All @@ -71,18 +75,30 @@ open class MessageComposerViewModel: ObservableObject {
addedFileURLs.removeLast()
}
checkPickerSelectionState()

if shouldDeleteDraftMessage(oldValue: oldValue) {
deleteDraftMessage()
}
}
}

@Published public var addedVoiceRecordings = [AddedVoiceRecording]() {
didSet {
checkPickerSelectionState()

if shouldDeleteDraftMessage(oldValue: oldValue) {
deleteDraftMessage()
}
}
}

@Published public var addedCustomAttachments = [CustomAttachment]() {
didSet {
checkPickerSelectionState()

if shouldDeleteDraftMessage(oldValue: oldValue) {
deleteDraftMessage()
}
}
}

Expand Down Expand Up @@ -251,10 +267,10 @@ open class MessageComposerViewModel: ObservableObject {
quotedMessage?.wrappedValue = message.quotedMessage
showReplyInChannel = message.showReplyInChannel

addedAssets.removeAll()
addedFileURLs.removeAll()
addedVoiceRecordings.removeAll()
addedCustomAttachments.removeAll()
var addedAssets: [AddedAsset] = []
var addedFileURLs: [URL] = []
var addedVoiceRecordings: [AddedVoiceRecording] = []
var addedCustomAttachments: [CustomAttachment] = []

message.attachments.forEach { attachment in
switch attachment.type {
Expand All @@ -277,6 +293,11 @@ open class MessageComposerViewModel: ObservableObject {
addedCustomAttachments.append(customAttachment)
}
}

self.addedAssets = addedAssets
self.addedFileURLs = addedFileURLs
self.addedVoiceRecordings = addedVoiceRecordings
self.addedCustomAttachments = addedCustomAttachments
}

/// Updates the draft message locally and on the server.
Expand Down Expand Up @@ -318,7 +339,8 @@ open class MessageComposerViewModel: ObservableObject {
)
}

private func deleteDraftMessage() {
/// Deletes the draft message locally and on the server if it exists.
public func deleteDraftMessage() {
guard draftMessage != nil else {
return
}
Expand All @@ -330,6 +352,11 @@ open class MessageComposerViewModel: ObservableObject {
}
}

/// Checks if the previous value of the content in the composer was not empty and the current value is empty.
private func shouldDeleteDraftMessage(oldValue: any Collection) -> Bool {
!oldValue.isEmpty && !sendButtonEnabled
}

open func sendMessage(
quotedMessage: ChatMessage?,
editedMessage: ChatMessage?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,101 @@ class MessageComposerViewModel_Tests: StreamChatTestCase {
XCTAssertEqual(viewModel.text, "Draft")
}

func test_messageComposerVM_whenLastAssetRemoved_shouldDeleteDraft() {
// Given
let channelController = makeChannelController()
let draftMessage = DraftMessage.mock(text: "")
channelController.channel_mock = .mock(cid: .unique, draftMessage: draftMessage)
let viewModel = makeComposerDraftsViewModel(
channelController: channelController,
messageController: nil
)
let asset = defaultAsset
viewModel.imageTapped(asset)

// When
viewModel.imageTapped(asset) // Remove the asset by tapping again

// Then
XCTAssertEqual(channelController.deleteDraftMessage_callCount, 1)
}

func test_messageComposerVM_whenLastFileRemoved_shouldDeleteDraft() {
// Given
let channelController = makeChannelController()
let draftMessage = DraftMessage.mock(text: "")
channelController.channel_mock = .mock(cid: .unique, draftMessage: draftMessage)
let viewModel = makeComposerDraftsViewModel(
channelController: channelController,
messageController: nil
)
viewModel.addedFileURLs = [mockURL]

// When
viewModel.removeAttachment(with: mockURL.absoluteString)

// Then
XCTAssertEqual(channelController.deleteDraftMessage_callCount, 1)
}

func test_messageComposerVM_whenLastVoiceRecordingRemoved_shouldDeleteDraft() {
// Given
let channelController = makeChannelController()
let draftMessage = DraftMessage.mock(text: "")
channelController.channel_mock = .mock(cid: .unique, draftMessage: draftMessage)
let viewModel = makeComposerDraftsViewModel(
channelController: channelController,
messageController: nil
)
let recording = AddedVoiceRecording(url: mockURL, duration: 1.0, waveform: [0.5])
viewModel.addedVoiceRecordings = [recording]

// When
viewModel.removeAttachment(with: mockURL.absoluteString)

// Then
XCTAssertEqual(channelController.deleteDraftMessage_callCount, 1)
}

func test_messageComposerVM_whenLastCustomAttachmentRemoved_shouldDeleteDraft() {
// Given
let channelController = makeChannelController()
let draftMessage = DraftMessage.mock(text: "")
channelController.channel_mock = .mock(cid: .unique, draftMessage: draftMessage)
let viewModel = makeComposerDraftsViewModel(
channelController: channelController,
messageController: nil
)
let attachment = CustomAttachment(id: .unique, content: .mockFile)
viewModel.customAttachmentTapped(attachment)

// When
viewModel.customAttachmentTapped(attachment) // Remove by tapping again

// Then
XCTAssertEqual(channelController.deleteDraftMessage_callCount, 1)
}

func test_messageComposerVM_whenRemovingAttachment_withTextPresent_shouldNotDeleteDraft() {
// Given
let channelController = makeChannelController()
let draftMessage = DraftMessage.mock(text: "Hello")
channelController.channel_mock = .mock(cid: .unique, draftMessage: draftMessage)
let viewModel = makeComposerDraftsViewModel(
channelController: channelController,
messageController: nil
)
viewModel.text = "Hello"
let asset = defaultAsset
viewModel.imageTapped(asset)

// When
viewModel.imageTapped(asset) // Remove the asset by tapping again

// Then
XCTAssertEqual(channelController.deleteDraftMessage_callCount, 0)
}

// MARK: - private

private func makeComposerDraftsViewModel(
Expand Down
Loading