Repository navigation
feat: scheduled messages for v10 (CHA-5934) - #1990
NikolaGoranNaumovski wants to merge 15 commits into
Conversation
Add the scheduled message API error codes 114 to 124, and the helpers the state and UI layers build on: error reason detection on StreamAPIError, channel limits read from the server config, and the status, failure, error, block, composition end and event constants. The runtime constants for statuses and failure reasons are upper-case objects typed by the generated unions, so they do not clash with the generated ScheduledMessageStatus and ScheduledMessageFailureReason types.
Port the scheduled message state layer to v10 as client.scheduledMessages. The manager keeps the active scheduled messages in a StateStore, applies scheduled_message events, loads and pages through the query endpoint, and wraps update, delete and send-now so the store follows each response. Ordering compares the nanosecond timestamps numerically, newer stored versions win, sent and deleted ids stay tombstoned, and a snapshot keeps messages stored while its request was in flight. Unlike v9, the manager loads itself: once registered it loads whenever the socket turns healthy for a connected, non-anonymous user, which also covers reconnects, and it stops loading automatically once the feature is reported disabled. It is reset on disconnectUser.
Channel responses that carry scheduled_messages, from queryChannelsAndHydrate or from requestChannelState (query, watch and create), replace the stored scheduled messages of that channel. Each request captures the store version first, so a message stored while it was in flight is kept. A sendMessage response with a scheduled_message is stored, while the sent message never reaches the message paginator.
MessageComposer.schedule(at) composes the message and sends it with scheduled_at through channel.sendMessage, so it never enters the message list or the offline queue. editScheduledMessage() and updateScheduledMessage() edit a scheduled message in the composer, and rescheduleFailedMessage() loads a failed one to schedule or send again; the failed message is deleted once the new one goes out, including when it is sent now. Drafts pause while a scheduled message is in the composer, and the earlier composition comes back when it ends. schedule() and updateScheduledMessage() never reject. They return a ScheduledMessageSubmitResult and, unlike send(), keep the composition on failure, since a scheduled message has no list item that could hold it. A composition with a command, poll or location can't be scheduled: scheduleBlockReason exposes why, and a composition middleware placed before poll-only discards it with a warning. An edit ends with a warning when its message is sent, deleted or fails elsewhere, and is refreshed by newer updates. New core notification types cover these cases.
UI SDKs render a stored scheduled message with their message preview components, which take a LocalMessage. Export the helper that builds one, so they do not duplicate its required fields.
queryChannelsAndHydrate iterated the response channels for scheduled message hydration without the guard the rest of the method uses, so a response without channels threw.
Port the v9 composer suite to v10: schedule, edit, reschedule a failed message, the result contract that keeps the composition on failure, blocked compositions, pending and failed uploads, draft pausing, the restore point, scheduled message events ending or refreshing an edit, the update payload built from the stored message, and the preview helper. Also cover sending a failed-message composition now, which deletes the failed scheduled message once the send succeeds.
|
Reviewed at Background. In v10, every list of server data goes through the same few building blocks, so that each one loads, updates, recovers and can be customized in the same way:
Reminders are the closest example of a feature built this way: The hydration in this PR, saving 1. Lists as paginators over a shared store
2. Events routed by the manager, as in
3. Stale copies: newer wins in the shared store, finished records kept
4. Reconnects through
5. Scheduling and editing through
6. Configuration through The PR doesn't add anything to the declared config tree (
|
MartinCupela
left a comment
There was a problem hiding this comment.
I believe the PR needs to be adjusted to existing services provided by the LLC. More details at: #1990 (comment)
Loads the scheduled messages of one channel, or of all channels, page by page over a shared store, sorted as the server returns them: pending before failed, each by scheduled_at.
ScheduledMessageManager holds a list of all scheduled messages and one per channel, all backed by one EntityStore, and routes events, responses and channel hydration to every list. Lists load when the UI needs them and reload after connection.recovered instead of on every connect.
ScheduledMessageManager runs scheduled_message.* and connection.recovered through one EventHandlerPipeline per event type, as ChannelManager does. The defaults are labeled, so an app can add, replace or remove them with addEventHandler, setEventHandlers and removeEventHandlers, or pass its own set to the constructor, without forking the manager.
The manager decides which copy of a scheduled message wins before it reaches the lists or the store: a sent or canceled copy is final, otherwise the newer updated_at wins. Sent and deleted events record the final status instead of removing the message, so each list drops it through its filter and the store keeps the finished record that any older copy loses to. This replaces the set of removed ids.
…connect connection.recovered now runs ScheduledMessageManager.recover(), which reloads the loaded lists except the channel lists that a channel response hydrated since the socket dropped. reload() still reloads every loaded list.
Adds scheduleMessageRequest, updateScheduledMessageRequest, deleteScheduledMessageRequest and sendScheduledMessageNowRequest to the channel request handlers, with the same (params, defaultRequest) signature as sendMessageRequest. Scheduling goes through the new ScheduledMessageManager.scheduleMessage(), so all four requests follow the same path and a replaced request still stores its result.
channel.scheduledMessages.enabled is ANDed with the app having the feature, which the scheduled message limits in the channel server config tell, and with the schedule-message capability in channel.scheduledMessagesEnabled. The composer blocks scheduling when it is off. Missing limits now mean the feature is off instead of falling back to defaults, and client.scheduledMessages.paginator sets the paging of every scheduled message list.
CLA
Description of the changes, What, Why and How?
Ticket: https://linear.app/stream/issue/REACT-1347 (parent), https://linear.app/stream/issue/CHA-5934
Ports scheduled messages ("send later", CHA-5934) to v10. The v9 implementation is split across #1951 (API), #1915 (state) and #1956 (composer). v10's generated client already covers the REST calls, request/response/event types and the
schedule-messagecapability, so this PR adds the state layer and the composer support on top of them. React v15 builds on it: GetStream/stream-chat-react#3383.What is added
Helpers (
src/scheduledMessages/)APIErrorCodes.getScheduledMessageErrorReason,isScheduledMessageErrorandisScheduledMessageNotFoundError, readingStreamAPIError.getScheduledMessageLimits(channel), read fromchannel.serverConfig.SCHEDULED_MESSAGE_STATUS,SCHEDULED_MESSAGE_FAILURE_REASON,ScheduledMessageErrorReason,ScheduledMessageBlockReason,ScheduledMessageCompositionEndReasonandScheduledMessageEventType. The status and failure constants are upper case because the generated models already exportScheduledMessageStatusandScheduledMessageFailureReasonas types.client.scheduledMessages(ScheduledMessageManager)StateStoreand appliesscheduled_message.*events.load()/loadMore(), plusupdateScheduledMessage,deleteScheduledMessageandsendScheduledMessageNowover the generated endpoints.updated_atwins. Timestamps are compared as nanosecond numbers.disconnectUser.Hydration
queryChannelsAndHydrateandrequestChannelState(query, watch, create) replace a channel's scheduled messages when the response carriesscheduled_messages.sendMessageresponse stores itsscheduled_message. The scheduled message never reaches the message paginator or the offline queue.MessageComposer
schedule(at)composes the message and sends it withscheduled_atthroughchannel.sendMessage.editScheduledMessage()/updateScheduledMessage()/cancelEditScheduledMessage()edit a scheduled message in the composer. A newer update from elsewhere refreshes the edit. If the message is sent, deleted or fails elsewhere, the edit ends with ascheduledMessageCompositionEndedwarning.rescheduleFailedMessage()/cancelRescheduleFailedMessage()load a failed message so it can be scheduled again. The failed message is deleted once the new one goes out, andsend()does this too when it is sent now.schedule()andupdateScheduledMessage()never reject, likesend(). They resolve aScheduledMessageSubmitResult(scheduled,sent,nothing-to-send,blockedorfailed). Unlikesend(), they keep the composition on failure, because a scheduled message has no list item that could hold it.scheduleBlockReasontells the UI why.scheduledMessageBlockedwarning.schedule()waits for uploads in flight and refuses (failed) when an upload failed, instead of dropping the attachment.CORE_NOTIFICATION_TYPEentries:scheduledMessageBlocked,scheduledMessageCompositionEnded,scheduledMessageCreateFailed,scheduledMessageUpdateFailed.toScheduledMessagePreview()turns a stored scheduled message into aLocalMessagefor UI previews.How it differs from the v9 PRs
TimestampNScomparisons replaceDate.parse.send()contract.message.custom. A storedpin_expiresis converted back to aDatein the update payload.Commits
ScheduledMessageManagerqueryChannelsresponse without channelsTesting
yarn types,yarn lintand the full unit suite pass: 5071 tests.test/unit/scheduledMessages/*: errors, limits, manager and hydration.test/unit/MessageComposer/scheduledMessages.test.tsChangelog
client.scheduledMessages(ScheduledMessageManager), scheduled message error codes and helpers, andMessageComposer.schedule()with scheduled message editing and rescheduling.