diff --git a/packages/dispatcher/src/context/scene-transition.ts b/packages/dispatcher/src/context/scene-transition.ts new file mode 100644 index 00000000..bc9249af --- /dev/null +++ b/packages/dispatcher/src/context/scene-transition.ts @@ -0,0 +1,59 @@ +import { MtTypeAssertionError } from '@mtcute/core' +import { makeInspectable } from '@mtcute/core/utils.js' + +import { BusinessMessageContext } from './business-message.js' +import { CallbackQueryContext, InlineCallbackQueryContext } from './callback-query.js' +import { MessageContext } from './message.js' +import { UpdateContextType } from './parse.js' + +/** Update which is dispatched whenever scene is entered or exited */ +export class SceneTransitionContext { + constructor( + /** Name of the previous scene, if any */ + readonly previousScene: string | null, + /** Update, handler for which triggered the transition */ + readonly update: UpdateContextType, + ) {} + + /** Get {@link update}, asserting it is a message-related update */ + get message(): MessageContext { + if (this.update instanceof MessageContext) { + return this.update + } + + throw new MtTypeAssertionError('SceneTransitionContext.message', 'message', this.update._name) + } + + /** Get {@link update}, asserting it is a business message-related update */ + get businessMessage(): BusinessMessageContext { + if (this.update instanceof BusinessMessageContext) { + return this.update + } + + throw new MtTypeAssertionError('SceneTransitionContext.businessMessage', 'business message', this.update._name) + } + + /** Get {@link update}, asserting it is a callback query update */ + get callbackQuery(): CallbackQueryContext { + if (this.update instanceof CallbackQueryContext) { + return this.update + } + + throw new MtTypeAssertionError('SceneTransitionContext.callbackQuery', 'callback query', this.update._name) + } + + /** Get {@link update}, asserting it is an inline callback query update */ + get inlineCallbackQuery(): InlineCallbackQueryContext { + if (this.update instanceof InlineCallbackQueryContext) { + return this.update + } + + throw new MtTypeAssertionError( + 'SceneTransitionContext.inlineCallbackQuery', + 'inline callback query', + this.update._name, + ) + } +} + +makeInspectable(SceneTransitionContext) diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index 68b2ea29..d1b18b45 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -39,6 +39,7 @@ import { PreCheckoutQueryContext, } from './context/index.js' import { _parsedUpdateToContext, UpdateContextType } from './context/parse.js' +import { SceneTransitionContext } from './context/scene-transition.js' import { filters, UpdateFilter } from './filters/index.js' // begin-codegen-imports import { @@ -143,6 +144,11 @@ export class Dispatcher { state?: UpdateState, ) => MaybePromise + private _sceneTransitionHandler?: ( + update: SceneTransitionContext, + state: UpdateState, + ) => MaybePromise + protected constructor(client?: TelegramClient, params?: DispatcherParams) { this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this) this.dispatchUpdate = this.dispatchUpdate.bind(this) @@ -462,7 +468,10 @@ export class Dispatcher { (update.name === 'new_message' || update.name === 'edit_message' || update.name === 'callback_query' || - update.name === 'message_group') + update.name === 'message_group' || + update.name === 'new_business_message' || + update.name === 'edit_business_message' || + update.name === 'business_message_group') ) { if (!parsedContext) parsedContext = _parsedUpdateToContext(this._client, update) const key = await this._stateKeyDelegate!(parsedContext as any) @@ -521,6 +530,40 @@ export class Dispatcher { handled = true } else continue + if (parsedState && this._scenes) { + // check if scene transition was made + const newScene = parsedState.scene + + if (parsedScene !== newScene) { + const nextDp = newScene ? this._scenes.get(newScene) : this._parent + + if (!nextDp) { + throw new MtArgumentError(`Scene ${newScene} not found`) + } + + if (nextDp._sceneTransitionHandler) { + const transition = new SceneTransitionContext(parsedScene, parsedContext) + const transitionResult = await nextDp._sceneTransitionHandler?.( + transition, + parsedState, + ) + + switch (transitionResult) { + case 'stop': + return true + case 'continue': + continue + case 'scene': { + const scene = parsedState.scene + const dp = scene ? nextDp._scenes!.get(scene)! : nextDp._parent! + + return dp._dispatchUpdateNowImpl(update, undefined, scene, true) + } + } + } + } + } + switch (result) { case 'continue': continue @@ -535,18 +578,10 @@ export class Dispatcher { throw new MtArgumentError('Cannot use ToScene without state') } - const scene = parsedState['_scene'] + const scene = parsedState.scene + const dp = scene ? this._scenes!.get(scene)! : this._parent! - if (!scene) { - throw new MtArgumentError('Cannot use ToScene without entering a scene') - } - - return this._scenes!.get(scene)!._dispatchUpdateNowImpl( - update, - undefined, - scene, - true, - ) + return dp._dispatchUpdateNowImpl(update, undefined, scene, true) } } @@ -739,6 +774,7 @@ export class Dispatcher { child._client = this._client child._storage = this._storage child._deps = this._deps + child._scenes = this._scenes child._stateKeyDelegate = this._stateKeyDelegate child._customStorage ??= this._customStorage child._customStateKeyDelegate ??= this._customStateKeyDelegate @@ -1071,6 +1107,32 @@ export class Dispatcher { this._addKnownHandler('raw', filter, handler, group) } + /** + * Register a scene transition handler + * + * This handler is called whenever a scene transition occurs + * in the context of the scene that is being entered, + * and before any of the its own handlers are called, + * and can be used to customize the transition behavior: + * - `Stop` to prevent dispatching the update any further **even if ToScene/ToRoot was used** + * - `Continue` same as Stop, but still dispatch the update to children + * - `ToScene` to prevent the transition and dispatch the update to the scene entered in the transition handler + * + * > **Note**: if multiple `state.enter()` calls were made within the same update, + * > this handler will only be called for the last one. + * + * @param handler Raw update handler + * @param group Handler group index + */ + onSceneTransition( + handler: + | ((ctx: SceneTransitionContext, state: UpdateState) => MaybePromise) + | null, + ): void { + if (handler) this._sceneTransitionHandler = handler + else this._sceneTransitionHandler = undefined + } + // begin-codegen /** diff --git a/packages/dispatcher/src/propagation.ts b/packages/dispatcher/src/propagation.ts index 9679bb42..1e9184e0 100644 --- a/packages/dispatcher/src/propagation.ts +++ b/packages/dispatcher/src/propagation.ts @@ -11,7 +11,8 @@ * * `Continue`: Continue propagating the event inside the same handler group. * - * `ToScene`: Used after using `state.enter()` to dispatch the update to the scene + * `ToScene`: Used after using `state.enter()` to dispatch the update to the scene, + * or after `state.exit()` to dispatch the update to the root dispatcher. */ export enum PropagationAction { Stop = 'stop',