Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,52 @@ extension ScheduledMessageListQuery {
}
}

// MARK: - Async

extension ScheduledMessagesRepository {
func loadScheduledMessages(query: ScheduledMessageListQuery) async throws -> ScheduledMessageListResponse {
try await withCheckedThrowingContinuation { continuation in
loadScheduledMessages(query: query) { continuation.resume(with: $0) }
}
}

func schedule(id: String, cid: ChannelId, content: ScheduledMessageContent, scheduledAt: Date) async throws -> ScheduledMessage {
try await withCheckedThrowingContinuation { continuation in
schedule(id: id, cid: cid, content: content, scheduledAt: scheduledAt) { continuation.resume(with: $0) }
}
}

func update(id: String, content: ScheduledMessageContent, scheduledAt: Date?) async throws -> ScheduledMessage {
try await withCheckedThrowingContinuation { continuation in
update(id: id, content: content, scheduledAt: scheduledAt) { continuation.resume(with: $0) }
}
}

func reschedule(id: String, scheduledAt: Date) async throws -> ScheduledMessage {
try await withCheckedThrowingContinuation { continuation in
reschedule(id: id, scheduledAt: scheduledAt) { continuation.resume(with: $0) }
}
}

func resend(id: String) async throws -> ScheduledMessage {
try await withCheckedThrowingContinuation { continuation in
resend(id: id) { continuation.resume(with: $0) }
}
}

func delete(id: String) async throws {
try await withCheckedThrowingContinuation { continuation in
delete(id: id) { continuation.resume(with: $0) }
}
}

func sendNow(id: String) async throws {
try await withCheckedThrowingContinuation { continuation in
sendNow(id: id) { continuation.resume(with: $0) }
}
}
}

private extension ScheduledMessageFailure.ServerRejection.Reason {
/// The reason of the server, or one derived from the status code when the server gives none.
init?(apiError: APIError) {
Expand Down
197 changes: 196 additions & 1 deletion Sources/StreamChat/StateLayer/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
private let eventSender: EventSender
private let memberUpdater: ChannelMemberUpdater
private let messageUpdater: MessageUpdater
private let scheduledMessagesRepository: ScheduledMessagesRepository
@MainActor private var stateBuilder: StateBuilder<ChatState>
private let typingEventsSender: TypingEventsSender

Expand Down Expand Up @@ -48,6 +49,10 @@
client.databaseContainer,
client.apiClient
)
scheduledMessagesRepository = environment.scheduledMessagesRepositoryBuilder(
client.databaseContainer,
client.apiClient
)
typingEventsSender = environment.typingEventsSenderBuilder(
client.databaseContainer,
client.apiClient
Expand Down Expand Up @@ -1345,7 +1350,192 @@
try await memberUpdater.pinMemberChannel(false, userId: currentUserId, cid: cid)
}
}


// MARK: - Scheduling Messages

/// Schedules a new message to be sent to the channel at a later date.
///
/// The message is not added to the channel until it is sent. It is saved locally first and sent to the server
/// once its attachments are uploaded, see `ScheduledMessage.status`.
///
/// - SeeAlso: Listing scheduled messages: ``ChatClient/makeScheduledMessageList(with:)``.
///
/// - Parameters:
/// - text: Text of the message.
/// - attachments: An array of the attachments for the message.
/// - quotedMessageId: An id of the quoted message.
/// - mentions: An array of mentioned user ids.
/// - messageId: A custom id for the scheduled message. Scheduling twice with the same id returns the existing scheduled message.
/// - scheduledAt: When to send the message, at most 120 days from now. The server rounds it up to the next whole minute,
/// at least 1 minute from now. A date in the past sends the message right away.
/// - extraData: Additional extra data of the message object.
///
/// - Throws: An error while saving the scheduled message locally, not an error of the server. Unlike `sendMessage`, it
/// does not wait for the server: the server answers later, on the scheduled message, and a rejection sets
/// `ScheduledMessage.status` to `.failed`, with the reason in `ScheduledMessage.failure`.
/// - Returns: The scheduled message.
@discardableResult
public func scheduleMessage(
with text: String,
attachments: [AnyAttachmentPayload] = [],
quote quotedMessageId: MessageId? = nil,
mentions: [UserId] = [],
messageId: MessageId? = nil,
scheduledAt: Date,
extraData: [String: RawJSON] = [:]
) async throws -> ScheduledMessage {
let cid = try await self.cid
let content = ScheduledMessageContent(
text: text,
attachments: attachments,
mentionedUserIds: mentions,
quotedMessageId: quotedMessageId,
parentId: nil,
showReplyInChannel: false,
extraData: extraData
)
return try await scheduledMessagesRepository.schedule(
id: messageId ?? .newUniqueId,
cid: cid,
content: content,
scheduledAt: scheduledAt
)
}

/// Schedules a new reply to the specified message, to be sent at a later date.
///
/// The reply is not added to the thread until it is sent. It is saved locally first and sent to the server
/// once its attachments are uploaded, see `ScheduledMessage.status`.
///
/// - Parameters:
/// - parentMessageId: The id of the message to reply to.
/// - text: Text of the reply.
/// - showReplyInChannel: If `true`, the reply is also visible in the channel, not only in the thread.
/// - attachments: An array of the attachments for the reply.
/// - quotedMessageId: An id of the quoted message.
/// - mentions: An array of mentioned user ids.
/// - messageId: A custom id for the scheduled message. Scheduling twice with the same id returns the existing scheduled message.
/// - scheduledAt: When to send the reply, at most 120 days from now. The server rounds it up to the next whole minute,
/// at least 1 minute from now. A date in the past sends the reply right away.
/// - extraData: Additional extra data of the reply.
///
/// - Throws: An error while saving the scheduled reply locally, not an error of the server. Unlike `sendMessage`, it
/// does not wait for the server: the server answers later, on the scheduled message, and a rejection sets
/// `ScheduledMessage.status` to `.failed`, with the reason in `ScheduledMessage.failure`.
/// - Returns: The scheduled reply.
@discardableResult
public func scheduleReply(

Check warning on line 1427 in Sources/StreamChat/StateLayer/Chat.swift

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Function has 9 parameters, which is greater than the 7 authorized.

See more on https://sonarcloud.io/project/issues?id=GetStream_stream-chat-swift&issues=AaF_mmrMa-RXeLPqKuzP&open=AaF_mmrMa-RXeLPqKuzP&pullRequest=4358
to parentMessageId: MessageId,
text: String,
showReplyInChannel: Bool = false,
attachments: [AnyAttachmentPayload] = [],
quote quotedMessageId: MessageId? = nil,
mentions: [UserId] = [],
messageId: MessageId? = nil,
scheduledAt: Date,
extraData: [String: RawJSON] = [:]
) async throws -> ScheduledMessage {
let cid = try await self.cid
let content = ScheduledMessageContent(
text: text,
attachments: attachments,
mentionedUserIds: mentions,
quotedMessageId: quotedMessageId,
parentId: parentMessageId,
showReplyInChannel: showReplyInChannel,
extraData: extraData
)
return try await scheduledMessagesRepository.schedule(
id: messageId ?? .newUniqueId,
cid: cid,
content: content,
scheduledAt: scheduledAt
)
}

/// Updates the content of a scheduled message, and optionally when it is sent.
///
/// A scheduled reply stays in its thread. A failed scheduled message can not be changed: delete it and schedule a new one.
/// The changes are sent to the server once its attachments are uploaded, see `ScheduledMessage.status`.
///
/// - Parameters:
/// - id: The id of the scheduled message.
/// - text: The new text of the message.
/// - attachments: The new attachments of the message.
/// - quotedMessageId: The new quoted message of the message.
/// - mentions: The new mentioned user ids of the message.
/// - scheduledAt: When to send the message, not in the past. The server rounds it up to the next whole minute.
/// `nil` keeps the current date.
/// - extraData: The new extra data of the message.
///
/// - Throws: An error while updating the scheduled message locally, not an error of the server. Unlike `sendMessage`, it
/// does not wait for the server: the server answers later, on the scheduled message, and a rejection sets
/// `ScheduledMessage.status` to `.failed`, with the reason in `ScheduledMessage.failure`.
/// - Returns: The updated scheduled message.
@discardableResult
public func updateScheduledMessage(
id: String,
text: String,
attachments: [AnyAttachmentPayload] = [],
quote quotedMessageId: MessageId? = nil,
mentions: [UserId] = [],
scheduledAt: Date? = nil,
extraData: [String: RawJSON] = [:]
) async throws -> ScheduledMessage {
let content = ScheduledMessageContent(
text: text,
attachments: attachments,
mentionedUserIds: mentions,
quotedMessageId: quotedMessageId,
parentId: nil,
showReplyInChannel: false,
extraData: extraData
)
return try await scheduledMessagesRepository.update(id: id, content: content, scheduledAt: scheduledAt)
}

/// Changes when a scheduled message is sent. A failed scheduled message can not be changed: delete it and schedule a new one.
///
/// The new date is sent to the server right after it is saved locally, see `ScheduledMessage.status`.
///
/// - Parameters:
/// - id: The id of the scheduled message.
/// - scheduledAt: When to send the message, at most 120 days from now and not in the past. The server rounds it up to the
/// next whole minute, at least 1 minute from now.
///
/// - Throws: An error while rescheduling the message locally, not an error of the server. Unlike `sendMessage`, it does
/// not wait for the server: the server answers later, on the scheduled message, and a rejection sets
/// `ScheduledMessage.status` to `.failed`, with the reason in `ScheduledMessage.failure`.
/// - Returns: The rescheduled message.
@discardableResult
public func rescheduleMessage(id: String, scheduledAt: Date) async throws -> ScheduledMessage {
try await scheduledMessagesRepository.reschedule(id: id, scheduledAt: scheduledAt)
}

/// Deletes a scheduled message, so it is never sent.
///
/// - Parameter id: The id of the scheduled message.
///
/// - Throws: An error while deleting the scheduled message.
public func deleteScheduledMessage(id: String) async throws {
try await scheduledMessagesRepository.delete(id: id)
}

/// Sends a scheduled message right away.
///
/// It is marked locally and sent once its local changes and attachments are on the server, so it is never sent
/// with older content. The message is added to the channel when it is sent, which is reported by the
/// `ScheduledMessageSentEvent`.
///
/// - Parameter id: The id of the scheduled message.
///
/// - Throws: An error when the message can't be sent, for example when its quoted message was deleted. It does not
/// wait for the server: a rejection sets `ScheduledMessage.status` to `.failed`, with the reason in
/// `ScheduledMessage.failure`.
public func sendScheduledMessageNow(id: String) async throws {
try await scheduledMessagesRepository.sendNow(id: id)
}

// MARK: - Sending and Listening to Events

/// Subscribes to web-socket events of a single type which is a channel specific event in this channel.
Expand Down Expand Up @@ -1770,6 +1960,11 @@
)
}

var scheduledMessagesRepositoryBuilder: @Sendable (
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> ScheduledMessagesRepository = { ScheduledMessagesRepository(database: $0, apiClient: $1) }

var typingEventsSenderBuilder: @Sendable (
_ database: DatabaseContainer,
_ apiClient: APIClient
Expand Down
16 changes: 16 additions & 0 deletions Sources/StreamChat/StateLayer/ChatClient+Factory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,22 @@ extension ChatClient {
}
}

// MARK: - Factory Methods for Creating Scheduled Message Lists

extension ChatClient {
/// Creates an instance of ``ScheduledMessageList`` which represents the current user's scheduled messages matching to the specified ``ScheduledMessageListQuery``.
///
/// Loaded scheduled messages are stored in ``ScheduledMessageListState/scheduledMessages`` and kept up to date with local changes and events.
/// Call ``ScheduledMessageList/get()`` for loading the first page.
///
/// - Parameter query: The query which filters the scheduled messages, for example by channel or status.
///
/// - Returns: An instance of ``ScheduledMessageList`` which represents actions and the current state of the list.
public func makeScheduledMessageList(with query: ScheduledMessageListQuery = .init()) -> ScheduledMessageList {
ScheduledMessageList(query: query, client: self)
}
}

// MARK: - Factory Methods for Creating User Lists

extension ChatClient {
Expand Down
Loading
Loading