My Lists Notifications

SPFx Extension

I recently published in the PnP community a new Application Extension that allows the user to select a list or library and receive a real-time notification with the information that has changed. This article explains the motivation and my experience in implementing this application.

My Lists Notifications

Motivation

Not having to be concerned about when information that is relevant to me changes was the main motivation for creating this extension, don’t matter where it is and if it is a File, Document or a List Item. Having this information centralised in a single place was the second motivation.

Implementation

This extension was developed in TypeScript, React, Fluent UI and uses Microsoft Graph API’s available to subscribe notifications, read information about changed data and save user settings.

How does it work ?

The extension adds an icon (ringer) in the navigation bar of the sites next to the settings option, if the site is a Communication Site or next to the announcements if it is a Team Site, the Icon can be positioned in any position from the right, because the extension has a property where you can specify its position in pixels, This Icon indicates the number of notifications received, (Badge).

The extension on startup subscribes to all lists that user has selected to receive notifications and listens for changes.

Technically the extension will fetch the WebSocket EndPoint defined for the list and make a connection to that server to receive notifications.

Sample:

.
.
.
  const listSubscription = (await msGraphClient
          .api(`/sites/${siteId}/lists/${listId}/subscriptions/socketIo`)
          .get()) as Subscription;
        return listSubscription?.notificationUrl;
.
.
.
 const connectToSocketIOServer = (notificationUrl: string): Socket => {
    const split = notificationUrl.split("/callback?");
    const socket = io(split[0], { query: split[1] as any, transports: ["websocket"] });
    socket.on("connect", () => {
      console.log("Connected!", notificationUrl);
    });
    socket.on("notification", handleNotifications);
    return socket;
  };

Here the link to Microsoft Graph API that explains how to use API

Configure Extension

For the user to receive notifications he has to configure the extension and select the lists for which he wants to be notified.
Clicking on the Icon will open a Panel that allows you to view the notifications received as well as access the configuration settings identified by the gear wheel.

In the configuration settings the user can search the lists that he wants to be notified.

Here I use my React-Graph-control, ListPicker that use MicrosoftGraph Search API.

Where the settings are saved

User settings are saved in the user’s OnDrive for Business in a folder called “Apps” and is known as user’s application’s personal folder. This is provide by Microsoft Graph API, Get Special folder and is automatic created when you read or write data.

Usually in  Apps folder we have this organisation: /Apps/{Application Name}, the Application name is the name of AAD registered Application, in context of SPFx has the name of SharePoint Online Client Extensibility Web Application Principal,.

It is good practice to create a subfolder for each application in order to avoid possible conflicts.

Here sample to Create a folder in OnDrive for Business “Apps”:
const createAppFolder = useCallback(
    async (folderName: string) => {
      try {
        const msGraphClient = await        
        context.serviceScope.consume(MSGraphClientFactory.serviceKey).getClient();
        if (!msGraphClient) return;
        await msGraphClient.api(`/me/drive/special/approot`).header("content- 
        type", "application/json").put({
          name: folderName,
          folder: {},
        });
      } catch (error) {
        console.log("er", error);
        if (error.code !== "nameAlreadyExists") {
          throw error;
        }
      }
    },
    [context.serviceScope]
  );

Here sample to save file into Application folder:
const saveSettings = useCallback(
    async (settings: string) => {
      try {  
        const msGraphClient = await           
        context.serviceScope.consume(MSGraphClientFactory.serviceKey).getClient();
        await msGraphClient           
             .api("/me/drive/special/approot:/MyListsNotifications/appsettings.json:/content")
             .header("content-type", "plain/text")
             .put(JSON.stringify(settings));
      } catch (error) {
        throw error;
      }
    },
    [context.serviceScope]
  );
Here sample to get saved file from Application folder:
  const getSettings = useCallback(async (): Promise<IConfigurationListItem[]> => {
    try {
      const msGraphClient = await  
      context.serviceScope.consume(MSGraphClientFactory.serviceKey).getClient();
      const downLoadUrlResponse = (await msGraphClient
        .api("/me/drive/special/approot:/MyListsNotifications/appsettings.json? 
              select=@microsoft.graph.downloadUrl")
        .get()) as HttpClientResponse;
      const appSettings = await context.httpClient.get(
        downLoadUrlResponse["@microsoft.graph.downloadUrl"],HttpClient.configurations.v1);
      const data: IConfigurationListItem[] = JSON.parse(await appSettings.json());
      return data;
    } catch (error) {
      throw error;
    }
  }, [context.serviceScope, context.httpClient]);

Get Changes

After connecting our lists to the corresponding SockectIO endpoint, we will receive in real time notification about the changes made to the list.

Here a sample format of notification object returned by the SocketIO EndPoint:
{"value":[{"subscriptionId":"9710b5f6-d97f-4794-bb65-939a4b7b0f9a","clientState":"system-managed:8082D436-D8DA-458D-96AD-34C902B73F37","expirationDateTime":"2021-07-10T20:40:15.5870000Z","resource":"b3a4755f-18be-4fbf-a6ae-e0bc14ac857e","tenantId":"ddba30c7-d040-4a20-9739-6a75edeadff2","siteUrl":"/","webId":"b06503c7-8710-4c0a-9b66-f2c36dc94a1b"}]}
here we have an Array of notifications with:
  • subscriptionId
  • clientState
  • expirationDate
  • resource – Id of our List
  • tenantId
  • siteId
  • webId

With the information, I will get the most recent activity from the list with the Microsoft Graph API Recent Activities, to create a notification block with the action that was applied to the Item.

Note: Microsoft Graph API Recent Activities, is in Beta and can be changed in the future.

Here a sample of using API:
 const getListActivities = useCallback(
    async (siteId: string, listId: string):Promise<IActivity[]> => {
      try {
        const msGraphClient = await 
        serviceScope.consume(MSGraphClientFactory.serviceKey).getClient();
        const listActivitiesResults = (await msGraphClient
          .api(`/sites/${siteId}/lists/${listId}/activities`)
          .expand("listItem($expand=fields),driveItem")
          .top(1)
          .version("beta")
          .get()) as IActivities;

        return listActivitiesResults.value;
      } catch (error) {
        throw error;
      }
    },
    [serviceScope]
  );

List notifications and Information of activities

Final considerations

I have to thank the entire Microsoft Graph team for the excellent work they have done with regard to providing API’s that make it possible to access Microsoft 365 data in a simple and versatile way.

Thank you, thank you for making a developer’s life easier.

Thank you very much for read and hope this be helpful in someway. this is available on SharePoint Framework Client-Side Extension Samples

%d bloggers like this: