Problem Statement
I'm not sure if the title of this issue is the best, but let me describe the issue first.
In Teams, "actions" are not async. So if you have a modal you want to open, you get a request, and you respond to it with the parameters of the modal, and then it opens. This differs from Slack, where you can cache the parameters for modal opening, and then imparatively trigger the modal to open asynchronously.
With the current Chat interface, processAction returns void. So when the handler gets an open-modal event from Teams, it calls chat.processAction, but it can't do anything with the result of that. The expectation is that chat will call openModal which then imparatively should open the modal.
Proposed Solution
There's a few potential ways to handle this imo:
- Allow
processAction to return the results of actions. So processAction could return the result of openModal, and then it is the responsibility of the adapter to handle that (cache it, send it imparatively, return it immediately. It's on the adapter).
- Make
processAction awaitable. If processAction is awaitable, then openModal can resolve the promise once it's called. This option seems fragile to me, but it's an option.
- Pass
openModal inline as part of processAction. Then in the openModal interceptor, the options.openModal is called first (if provided). The adapter can listen to this function resolving (maybe via a Promise.race).
processAction(event, {
waitUntil,
onOpenModal: async (modal) => {
// adapter-specific handling right here
// Teams: stash it for the invoke response
// Slack: call views.open
return { viewId: '...' };
}
});
Alternatives Considered
No response
Use Case
Here is a more full-example of the 3rd option:
app.on('dialog.open', async (ctx) => {
const activity = ctx.activity;
const threadId = this.encodeThreadId({
conversationId: activity.conversation?.id || "",
serviceUrl: activity.serviceUrl || "",
});
// Promise that resolves when the handler calls openModal
let resolveModal: (modal: ModalElement) => void;
const modalPromise = new Promise<ModalElement>((resolve) => {
resolveModal = resolve;
});
let modalWasOpened = false;
const actionEvent = {
actionId: activity.value?.data?.actionId || "dialog.open",
value: activity.value?.data,
triggerId: activity.id,
user: { /* ... from activity */ },
messageId: activity.replyToId || activity.id || "",
threadId,
adapter: this,
raw: activity,
};
// processAction is still fire-and-forget, but the interceptor
// captures the modal synchronously during handler execution
this.chat.processAction(actionEvent, {
waitUntil: (p) => p, // no-op, we handle timing ourselves
onOpenModal: async (modal, contextId) => {
modalWasOpened = true;
resolveModal(modal);
return { viewId: contextId };
},
});
// Wait for the handler to call openModal (or timeout)
const modal = await Promise.race([
modalPromise,
new Promise<null>((resolve) => setTimeout(() => resolve(null), 5000)),
]);
if (modal && modalWasOpened) {
const card = modalElementToAdaptiveCard(modal);
return {
task: {
type: 'continue',
value: {
title: modal.title || 'Dialog',
card: { contentType: 'application/vnd.microsoft.card.adaptive', content: card },
},
},
};
}
// Handler didn't open a modal — close silently
return { status: 200 };
});
Priority
None
Contribution
Additional Context
No response
Problem Statement
I'm not sure if the title of this issue is the best, but let me describe the issue first.
In Teams, "actions" are not async. So if you have a modal you want to open, you get a request, and you respond to it with the parameters of the modal, and then it opens. This differs from Slack, where you can cache the parameters for modal opening, and then imparatively trigger the modal to open asynchronously.
With the current Chat interface,
processActionreturnsvoid. So when the handler gets an open-modal event from Teams, it callschat.processAction, but it can't do anything with the result of that. The expectation is thatchatwill callopenModalwhich then imparatively should open the modal.Proposed Solution
There's a few potential ways to handle this imo:
processActionto return the results of actions. SoprocessActioncould return the result ofopenModal, and then it is the responsibility of the adapter to handle that (cache it, send it imparatively, return it immediately. It's on the adapter).processActionawaitable. IfprocessActionis awaitable, thenopenModalcan resolve the promise once it's called. This option seems fragile to me, but it's an option.openModalinline as part ofprocessAction. Then in theopenModalinterceptor, theoptions.openModalis called first (if provided). The adapter can listen to this function resolving (maybe via a Promise.race).Alternatives Considered
No response
Use Case
Priority
None
Contribution
Additional Context
No response