-
Notifications
You must be signed in to change notification settings - Fork 137
Description
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
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). - Make
processActionawaitable. IfprocessActionis awaitable, thenopenModalcan resolve the promise once it's called. This option seems fragile to me, but it's an option. - Pass
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).
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
- I am willing to help implement this feature
Additional Context
No response