Meeting apps in Microsoft Teams (1) – Pre-meeting

Meeting apps in Microsoft Teams (1) – Pre-meeting

Announced in Ignite 2020 and made available same year’s autumn apps for Microsoft Teams meetings are a new fantastic capability to enhance daily collaboration life. In this and the following posts I want to introduce several of the possibilities developers have to enhance the meeting experience.

The sample story I received from Microsoft and already includes several cases: Assume you have a meeting, maybe with international participants and are unsure about some of the name’s pronunciation? To avoid any pitfall why not let any participant record their name and make all these recordings available to all participants so each of them can playback any time needed?

In this first part I want to show the pre-meeting experience having an app that is available in the context of a meeting but before the start. Because it makes sense to at least record their names for each participant upfront the meeting but maybe also getting familiar with the pronunciation of some others.

Series

Content

Interestingly you do not need to learn many new things to get this up and running. In fact it’s quite simple. All we need is a Teams tab, including known SSO technology to store and retrieve our recordings (from) somewhere. The only specials we have here are some new settings in the app manifest (to let our tab appear inside a meeting) and some new properties from the context to get to know we are in a meeting right now and in which one (because depending on the meeting audience I might pronunce my own name slightly different 😉 )

Setup solution

The setup of such a Teams tab with the yeoman generator for Teams is well known from lots of my previous posts. I spared out lots of available options, simply to concentrate on the essential things once again:

yo teams setup – A tab with SSO for meeting app

App registration

Next an app registration is needed because the app needs to store recordings somewhere and for staying with Microsoft 365 the choice is storing to a SharePoint library together with some metadata.

  • Give the app registration a name
  • Give it a secret and note that down
  • Expose an API and give the Api URI a name like api://xxxx.ngrok.io/<Your App ID> (xxxx depends on your current ngrok Url)
  • Set scope name to access_as_user and provide Admin & User messages
  • Add Teams Client 1fec8e78-bce4-4aaf-ab1b-5451cc387264 and Teams Web Client 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 IDs under “Add a client application”
  • Grant the following Api permissions
    • Sites.ReadWrite.All

Add following webApplicationInfo to your manifest (while ensuring to have the placeholders in your .env):

"webApplicationInfo": {
    "id": "{{TAB_APP_ID}}",
    "resource": "api://{{PUBLIC_HOSTNAME}}/{{TAB_APP_ID}}"
 }

In more detail this is described here by Wictor Wilen.

The client side

To implement the client side content the PronunceNameTab.tsx needs to be adjusted. Due to the yo teams selections above a client-side SSO is already there but this needs to be enhanced. After the client side token is successfully there still the user name is evaluated followed by storing the token to the state and retrieving any existing recordings for this meeting.

export const PronunceNameTab = () => {
    const [{ inTeams, theme, context }] = useTeams();
    const [meetingId, setMeetingId] = useState<string | undefined>();
    const [name, setName] = useState<string>("");
    const [accesstoken, setAccesstoken] = useState<string>();
    const [error, setError] = useState<string>();
    const [recording, setRecording] = useState<boolean>(false);
    const [recordings, setRecordings] = useState<IRecording[]>([]);

    useEffect(() => {
        if (inTeams === true) {
            microsoftTeams.authentication.getAuthToken({
                successCallback: (token: string) => {
                    const decoded: { [key: string]: any; } = jwtDecode(token) as { [key: string]: any; };
                    setName(decoded!.name);
                    setAccesstoken(token);
                    getRecordings(token);
                    microsoftTeams.appInitialization.notifySuccess();
                },
                failureCallback: (message: string) => {
                    setError(message);
                    microsoftTeams.appInitialization.notifyFailure({
                        reason: microsoftTeams.appInitialization.FailedReason.AuthFailed,
                        message
                    });
                },
                resources: [`api://${process.env.PUBLIC_HOSTNAME}/${process.env.TAB_APP_ID}`]
            });
        }
    }, [inTeams]);

    useEffect(() => {
        if (context) {
            setMeetingId(context.meetingId);
        }
    }, [context]);

    const btnClicked = () => {
        setRecording(true);
    };

    const closeRecording = () => {
        setRecording(false);
    };
    
    const getRecordings = async (token: string) => {
        const response = await Axios.get(`https://${process.env.PUBLIC_HOSTNAME}/api/files/${context?.meetingId}`,
        { headers: { Authorization: `Bearer ${token}` }});

        setRecordings(response.data);
    };

    return (
        <Provider className={context && context.frameContext === microsoftTeams.FrameContexts.sidePanel ? "panelSize" : ""} theme={theme}>
            <Flex fill={true} column styles={{
                padding: ".8rem 0 .8rem .5rem"
            }}>
                <Flex.Item>
                    <Header content="User name recordings" />
                </Flex.Item>
                <Flex.Item>
                    <div>
                        {recordings.length > 0 && recordings.map((recording: any) => {
                            return <UserRecordedName key={recording.id} userName={recording.username} driveItemId={recording.id} accessToken={accesstoken} dataUrl={recording.dataUrl} />;
                        })}

                        {(context && context.frameContext === microsoftTeams.FrameContexts.content) && <div>
                            {!recording ? (
                                <Button onClick={btnClicked}>Record name</Button>
                            ) :
                            (<div className="closeDiv"><CloseIcon className="closeIcon" onClick={closeRecording} />
                                <RecordingArea userID={context?.userObjectId} clientType={context?.hostClientType} callback={blobReceived} />
                            </div>)}
                        </div>}
                    </div>
                </Flex.Item>
            </Flex>
        </Provider>
    );
};

Furthermore interesting here is the context.meetingId in line 48. This is needed to only get recordings for the current meeting and will also be stored later in case the user records his name. Finally in the jsx part another meeting-specific setting is interesting. In line 67 pay attention on microsoftTeams.FrameContexts.content. This (content) is only the case in a pre-meeting details tab (the opposite (sidePanel) is present in line 54, but we will come to that later when talking about in-meeting experience). Nevertheless it’s already clear that with evaluating context.frameContext it is possible to detect in which kind of component our solution is rendered. Here it is used to show an area for record the user name only in pre-meeting experience and hide it otherwise.

The result now looks like this:

User recordings with collapsed recording areaUser recordings with expanded recording area

The manifest

Once the client side is implemented now it’s time to render it in the right context. To make this tab available in a meeting, specifically in a pre-meeting experience the following needs to be added to the manifest:

{
  "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.9/MicrosoftTeams.schema.json",
  "manifestVersion": "1.9",
  ....
  "configurableTabs": [
    {
      "configurationUrl": "https://{{PUBLIC_HOSTNAME}}/pronunceNameTab/config.html?name={loginHint}&tenant={tid}&group={groupId}&theme={theme}",
      "canUpdateConfiguration": true,
      "scopes": [
        "groupchat"
      ],
      "context": [
        "meetingDetailsTab",
        "meetingChatTab",
        "meetingSidePanel"
      ],
      "meetingSurfaces": [
        "sidePanel"
      ]
    }
  ],
  ...
  "devicePermissions": [
    "media"
  ],
  ...
  "webApplicationInfo": {
    "id": "{{TAB_APP_ID}}",
    "resource": "api://{{PUBLIC_HOSTNAME}}/{{TAB_APP_ID}}"
  }
}

The first thing to mention is that version 1.9 or above is used. Not necessarily for this part but for the later in-meeting experience meetingSurfaces is not available in prior to 1.9 versions. For our pre-meeting part a configurableTab with scopes: ["groupchat"] and context: ["meetingDetailsTab","meetingChatTab", ...] is needed. The rest shown here was either already mentioned (webApplicationInfo) or will be pointed out in a later part (devicePermissions).

So far the basics for a pre-meeting Teams application are covered although the concrete sample offers a bit more. So now you are ready to install a first and simple basic app into a meeting of your choice.

App installation

Once done with the implementation the app either needs to run locally with ngrok or installed to a host such as Azure. Having that and created the app package with gulp manifest it can be either uploaded to the app catalog or sideloaded for a quick view.

Next a meeting needs to be created while it’s important to add at least one participant. Now the meeting can be opened from the calendar and expanded for edit. Right beside the tabs above the + can be clicked to add an app. This can be selected from the app catalog or sideloaded via Manage Apps below and upload the app package directly for this meeting.

All in all the app should be available now for the meeting as a details tab. Having that users can simply record their name before the meeting.

The whole app in “pre-meeting experience” as a details tab

Next part would be to make the result available in a live meeting with “in-meeting experience” as well as users do not want to switch between live meeting window and Teams app. So it would be better to have it show up as a “sidePanel” similar to chat or participants list. Also there is a need to talk about the details of the audio recordings as access to the microphone is required. Stay tuned!

I hope it was interesting to get behind the scenes of Microsoft Teams Meeting apps. This post only covered parts of the sample of which you can investigate the full code in my github repository. Finally I would like to thank Bob German and Wajeed Shaikh from Microsoft for providing me the sample idea and their support figuring things out.

Markus is a SharePoint architect and technical consultant with focus on latest technology stack in Microsoft 365 and SharePoint Online development. He loves the new SharePoint Framework as well as some backend stuff around Azure Automation or Azure Functions and also has a passion for Microsoft Graph.
He works for Avanade as an expert for Microsoft 365 Dev and is based in Munich.
Although if partially inspired by his daily work opinions are always personal.

4 thoughts on “Meeting apps in Microsoft Teams (1) – Pre-meeting

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s