feat(dispatcher): scene transition hooks + ToScene when exiting
This commit is contained in:
parent
8e04c13b60
commit
3968a35654
3 changed files with 135 additions and 13 deletions
59
packages/dispatcher/src/context/scene-transition.ts
Normal file
59
packages/dispatcher/src/context/scene-transition.ts
Normal file
|
@ -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)
|
|
@ -39,6 +39,7 @@ import {
|
||||||
PreCheckoutQueryContext,
|
PreCheckoutQueryContext,
|
||||||
} from './context/index.js'
|
} from './context/index.js'
|
||||||
import { _parsedUpdateToContext, UpdateContextType } from './context/parse.js'
|
import { _parsedUpdateToContext, UpdateContextType } from './context/parse.js'
|
||||||
|
import { SceneTransitionContext } from './context/scene-transition.js'
|
||||||
import { filters, UpdateFilter } from './filters/index.js'
|
import { filters, UpdateFilter } from './filters/index.js'
|
||||||
// begin-codegen-imports
|
// begin-codegen-imports
|
||||||
import {
|
import {
|
||||||
|
@ -143,6 +144,11 @@ export class Dispatcher<State extends object = never> {
|
||||||
state?: UpdateState<State>,
|
state?: UpdateState<State>,
|
||||||
) => MaybePromise<void>
|
) => MaybePromise<void>
|
||||||
|
|
||||||
|
private _sceneTransitionHandler?: (
|
||||||
|
update: SceneTransitionContext,
|
||||||
|
state: UpdateState<State>,
|
||||||
|
) => MaybePromise<PropagationAction | void>
|
||||||
|
|
||||||
protected constructor(client?: TelegramClient, params?: DispatcherParams) {
|
protected constructor(client?: TelegramClient, params?: DispatcherParams) {
|
||||||
this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this)
|
this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this)
|
||||||
this.dispatchUpdate = this.dispatchUpdate.bind(this)
|
this.dispatchUpdate = this.dispatchUpdate.bind(this)
|
||||||
|
@ -462,7 +468,10 @@ export class Dispatcher<State extends object = never> {
|
||||||
(update.name === 'new_message' ||
|
(update.name === 'new_message' ||
|
||||||
update.name === 'edit_message' ||
|
update.name === 'edit_message' ||
|
||||||
update.name === 'callback_query' ||
|
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)
|
if (!parsedContext) parsedContext = _parsedUpdateToContext(this._client, update)
|
||||||
const key = await this._stateKeyDelegate!(parsedContext as any)
|
const key = await this._stateKeyDelegate!(parsedContext as any)
|
||||||
|
@ -521,6 +530,40 @@ export class Dispatcher<State extends object = never> {
|
||||||
handled = true
|
handled = true
|
||||||
} else continue
|
} 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) {
|
switch (result) {
|
||||||
case 'continue':
|
case 'continue':
|
||||||
continue
|
continue
|
||||||
|
@ -535,18 +578,10 @@ export class Dispatcher<State extends object = never> {
|
||||||
throw new MtArgumentError('Cannot use ToScene without state')
|
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) {
|
return dp._dispatchUpdateNowImpl(update, undefined, scene, true)
|
||||||
throw new MtArgumentError('Cannot use ToScene without entering a scene')
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._scenes!.get(scene)!._dispatchUpdateNowImpl(
|
|
||||||
update,
|
|
||||||
undefined,
|
|
||||||
scene,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -739,6 +774,7 @@ export class Dispatcher<State extends object = never> {
|
||||||
child._client = this._client
|
child._client = this._client
|
||||||
child._storage = this._storage
|
child._storage = this._storage
|
||||||
child._deps = this._deps
|
child._deps = this._deps
|
||||||
|
child._scenes = this._scenes
|
||||||
child._stateKeyDelegate = this._stateKeyDelegate
|
child._stateKeyDelegate = this._stateKeyDelegate
|
||||||
child._customStorage ??= this._customStorage
|
child._customStorage ??= this._customStorage
|
||||||
child._customStateKeyDelegate ??= this._customStateKeyDelegate
|
child._customStateKeyDelegate ??= this._customStateKeyDelegate
|
||||||
|
@ -1071,6 +1107,32 @@ export class Dispatcher<State extends object = never> {
|
||||||
this._addKnownHandler('raw', filter, handler, group)
|
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<State>) => MaybePromise<PropagationAction | void>)
|
||||||
|
| null,
|
||||||
|
): void {
|
||||||
|
if (handler) this._sceneTransitionHandler = handler
|
||||||
|
else this._sceneTransitionHandler = undefined
|
||||||
|
}
|
||||||
|
|
||||||
// begin-codegen
|
// begin-codegen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
*
|
*
|
||||||
* `Continue`: Continue propagating the event inside the same handler group.
|
* `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 {
|
export enum PropagationAction {
|
||||||
Stop = 'stop',
|
Stop = 'stop',
|
||||||
|
|
Loading…
Reference in a new issue