Skip to content

Commit cb8ceca

Browse files
committed
Merge branch 'develop' into v5
# Conflicts: # Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift # Sources/StreamChat/Workers/MessageUpdater.swift
2 parents 6a7b905 + 51e5ada commit cb8ceca

File tree

35 files changed

+529
-25
lines changed

35 files changed

+529
-25
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
33

44
# Upcoming
55

6-
### 🔄 Changed
6+
## StreamChat
7+
### ✅ Added
8+
- Add support for deleting messages only for the current user [#3836](https://github.com/GetStream/stream-chat-swift/pull/3836)
9+
- Add `ChatMessageController.deleteMessageForMe()`
10+
- Add `ChatMessage.deletedForMe`
11+
### 🐞 Fixed
12+
- Fix logout not clearing token when current user had no device registered [#3838](https://github.com/GetStream/stream-chat-swift/pull/3838)
713

814
# [4.90.0](https://github.com/GetStream/stream-chat-swift/releases/tag/4.90.0)
915
_October 07, 2025_

DemoApp/StreamChat/Components/DemoChatMessageActionsVC.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
1414
if message?.isSentByCurrentUser == true {
1515
if AppConfig.shared.demoAppConfig.isHardDeleteEnabled {
1616
actions.append(hardDeleteActionItem())
17+
actions.append(deleteForMeActionItem())
1718
}
1819
}
1920

@@ -108,6 +109,22 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
108109
)
109110
}
110111

112+
func deleteForMeActionItem() -> ChatMessageActionItem {
113+
DeleteForMeActionItem(
114+
action: { [weak self] _ in
115+
guard let self = self else { return }
116+
self.alertsRouter.showMessageDeletionConfirmationAlert { confirmed in
117+
guard confirmed else { return }
118+
119+
self.messageController.deleteMessageForMe { _ in
120+
self.delegate?.chatMessageActionsVCDidFinish(self)
121+
}
122+
}
123+
},
124+
appearance: appearance
125+
)
126+
}
127+
111128
func translateActionItem() -> ChatMessageActionItem {
112129
TranslateActionitem(
113130
action: { [weak self] _ in
@@ -224,6 +241,21 @@ final class DemoChatMessageActionsVC: ChatMessageActionsVC {
224241
}
225242
}
226243

244+
struct DeleteForMeActionItem: ChatMessageActionItem {
245+
var title: String { "Delete only for me" }
246+
var isDestructive: Bool { true }
247+
let icon: UIImage
248+
let action: (ChatMessageActionItem) -> Void
249+
250+
init(
251+
action: @escaping (ChatMessageActionItem) -> Void,
252+
appearance: Appearance = .default
253+
) {
254+
self.action = action
255+
icon = appearance.images.messageActionDelete
256+
}
257+
}
258+
227259
struct TranslateActionitem: ChatMessageActionItem {
228260
var title: String { "Translate to Turkish" }
229261
var isDestructive: Bool { false }

DemoApp/StreamChat/Components/DemoChatMessageContentView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ final class DemoChatMessageContentView: ChatMessageContentView {
7575
timestampLabel?.text?.append(" - Translated to Turkish")
7676
}
7777

78+
if content?.deletedForMe == true {
79+
timestampLabel?.text?.append(" - Deleted only for me")
80+
}
81+
7882
if content?.isPinned == true, let pinInfoLabel = pinInfoLabel {
7983
pinInfoLabel.text = "📌 Pinned"
8084
if let pinDetails = content?.pinDetails {

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ GEM
274274
puma (6.6.1)
275275
nio4r (~> 2.0)
276276
racc (1.8.1)
277-
rack (3.2.0)
277+
rack (3.2.2)
278278
rack-protection (4.1.1)
279279
base64 (>= 0.1.0)
280280
logger (>= 1.6.0)

Sources/StreamChat/APIClient/Endpoints/MessageEndpoints.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ extension Endpoint {
1515
)
1616
}
1717

18-
static func deleteMessage(messageId: MessageId, hard: Bool) -> Endpoint<MessagePayload.Boxed> {
19-
.init(
18+
static func deleteMessage(messageId: MessageId, hard: Bool, deleteForMe: Bool? = nil) -> Endpoint<MessagePayload.Boxed> {
19+
var body: [String: AnyEncodable] = ["hard": AnyEncodable(hard)]
20+
if let deleteForMe = deleteForMe {
21+
body["delete_for_me"] = AnyEncodable(deleteForMe)
22+
}
23+
return .init(
2024
path: .deleteMessage(messageId),
2125
method: .delete,
2226
queryItems: nil,
2327
requiresConnectionId: false,
24-
body: ["hard": hard]
28+
body: body
2529
)
2630
}
2731

Sources/StreamChat/APIClient/Endpoints/Payloads/MessagePayloads.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ enum MessagePayloadsCodingKeys: String, CodingKey, CaseIterable {
5757
case location = "shared_location"
5858
case reminder
5959
case member
60+
case deletedForMe = "deleted_for_me"
6061
}
6162

6263
extension MessagePayload {
@@ -119,6 +120,7 @@ final class MessagePayload: Decodable, Sendable {
119120
let location: SharedLocationPayload?
120121
let reminder: ReminderPayload?
121122
let member: MemberInfoPayload?
123+
let deletedForMe: Bool?
122124

123125
/// Only message payload from `getMessage` endpoint contains channel data. It's a convenience workaround for having to
124126
/// make an extra call do get channel details.
@@ -189,6 +191,7 @@ final class MessagePayload: Decodable, Sendable {
189191
location = try container.decodeIfPresent(SharedLocationPayload.self, forKey: .location)
190192
reminder = try container.decodeIfPresent(ReminderPayload.self, forKey: .reminder)
191193
member = try container.decodeIfPresent(MemberInfoPayload.self, forKey: .member)
194+
deletedForMe = try container.decodeIfPresent(Bool.self, forKey: .deletedForMe)
192195
}
193196

194197
init(
@@ -233,7 +236,8 @@ final class MessagePayload: Decodable, Sendable {
233236
draft: DraftPayload? = nil,
234237
reminder: ReminderPayload? = nil,
235238
location: SharedLocationPayload? = nil,
236-
member: MemberInfoPayload? = nil
239+
member: MemberInfoPayload? = nil,
240+
deletedForMe: Bool? = nil
237241
) {
238242
self.id = id
239243
self.cid = cid
@@ -277,6 +281,7 @@ final class MessagePayload: Decodable, Sendable {
277281
self.location = location
278282
self.reminder = reminder
279283
self.member = member
284+
self.deletedForMe = deletedForMe
280285
}
281286
}
282287

Sources/StreamChat/ChatClient.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -516,14 +516,13 @@ public class ChatClient: @unchecked Sendable {
516516
}
517517
self?.authenticationRepository.logOutUser()
518518
}
519+
} else {
520+
authenticationRepository.logOutUser()
519521
}
520522

523+
// Clear current user id instantly even if pending removing device.
521524
authenticationRepository.clearCurrentUserId()
522525

523-
if removeDevice == false {
524-
authenticationRepository.logOutUser()
525-
}
526-
527526
// Stop tracking active components
528527
syncRepository.removeAllTracked()
529528

Sources/StreamChat/Controllers/MessageController/MessageController.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,26 @@ public class ChatMessageController: DataController, DelegateCallable, DataStoreP
355355
}
356356
}
357357

358+
/// Deletes the message this controller manages only for the current user.
359+
///
360+
/// This method deletes the message only for the current user, making it invisible to them while keeping it visible to other users.
361+
/// This is different from the regular delete which affects all users in the channel.
362+
///
363+
/// - Parameter completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
364+
/// If request fails, the completion will be called with an error.
365+
///
366+
public func deleteMessageForMe(completion: ((Error?) -> Void)? = nil) {
367+
messageUpdater.deleteMessage(
368+
messageId: messageId,
369+
hard: false,
370+
deleteForMe: true
371+
) { error in
372+
self.callback {
373+
completion?(error)
374+
}
375+
}
376+
}
377+
358378
/// Creates a new reply message locally and schedules it for send.
359379
///
360380
/// - Parameters:

Sources/StreamChat/Database/DTOs/MessageDTO.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class MessageDTO: NSManagedObject {
118118
@NSManaged var defaultSortingKey: DBDate!
119119

120120
@NSManaged var channelRole: String?
121+
@NSManaged var deletedForMe: Bool
121122

122123
override func willSave() {
123124
super.willSave()
@@ -990,6 +991,10 @@ extension NSManagedObjectContext: MessageDatabaseSession {
990991

991992
dto.isSilent = payload.isSilent
992993
dto.isShadowed = payload.isShadowed
994+
if let deletedForMe = payload.deletedForMe {
995+
dto.deletedForMe = deletedForMe
996+
}
997+
993998
// Due to backend not working as advertised
994999
// (sending `shadowed: true` flag to the shadow banned user)
9951000
// we have to implement this workaround to get the advertised behavior
@@ -1776,6 +1781,7 @@ private extension ChatMessage {
17761781
let isBounced = dto.isBounced
17771782
let isSilent = dto.isSilent
17781783
let isShadowed = dto.isShadowed
1784+
let deletedForMe = dto.deletedForMe
17791785
let reactionScores = dto.reactionScores.mapKeys { MessageReactionType(rawValue: $0) }
17801786
let reactionCounts = dto.reactionCounts.mapKeys { MessageReactionType(rawValue: $0) }
17811787
let reactionGroups = dto.reactionGroups.asModel()
@@ -1890,6 +1896,7 @@ private extension ChatMessage {
18901896
isBounced: isBounced,
18911897
isSilent: isSilent,
18921898
isShadowed: isShadowed,
1899+
deletedForMe: deletedForMe,
18931900
reactionScores: reactionScores,
18941901
reactionCounts: reactionCounts,
18951902
reactionGroups: reactionGroups,

Sources/StreamChat/Database/DatabaseSession.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,11 @@ extension DatabaseSession {
842842
return
843843
}
844844

845+
// Update the message if deleted only for the current user.
846+
if payload.eventType == .messageDeleted && payload.deletedForMe == true {
847+
savedMessage.deletedForMe = true
848+
}
849+
845850
// When a message is updated, make sure to update
846851
// the messages quoting the edited message by triggering a DB Update.
847852
if payload.eventType == .messageUpdated {

0 commit comments

Comments
 (0)