Notifications

Finsemble's notifications refresh brings with it 3 new pieces of UI. A Notification bell Icon, new and improved Toasts, and a fully featured Notification Center.

Notifications UI

The Notification Bell Icon indicating that there are unread notifications.
Notification Bell Icon indicating that there are unread notifications.

A notification toast sliding out to grab the user's attention.
A Notification Toast sliding out to grab the user's attention.

A fully featured Notification Center.
A fully featured Notification Center.

Notification Search

The Notification Center provides search functionality. The search is case-insensitive and only gets triggered when the search term is at least 3 characters long. Additionally, the search is atomic, meaning that it will try to match the exact characters in the search term as opposed to a best match approach with approximate results.

There are also 2 different search modes. Targeted search and generic search.

  • Targeted search - allows you to specify the field to which the search term will be applied. The targeted search can be performed by prefixing a field: to your search term. The available fields are: title:, details: and source: and the search term will only be applied to the selected field. If an invalid field is inserted, this will be ignored and a Generic search will be performed.

  • Generic search - will search for the search term in all fields (title, details, source).

Notification Modes

The new Finsemble UI distinguishes between two modes of Notification. Informational and Actionable Notifications.

Any notification that does not have Actions is treated as an Informational Notification. Informational notifications differ in behavior from Actionable ones in that Informational notifications will disappear from the Toast UI after a configurable amount of time. Whereas an Actionable Notification will stay on the screen waiting for user interaction.

Sending an Informational Notification

FSBL.Clients.NotificationClient.notify({
	// id: "adf-3484-38729zg", // distinguishes individual notifications - provided by Finsemble if not supplied
	// issuedAt: "2021-12-25T00:00:00.001Z", // The notifications was sent - provided by Finsemble if not supplied
	// type: "configDefinedType", // Types defined in the config will have those values set as default
	source: "Finsemble", // Where the Notification was sent from
	title: "Request for Quote",
	details: "RIO - 9409 @ $9409",
	// headerLogo: "URL to Icon",
	// actions: [], // Note this has no Actions making it Informational
	// meta: {} // Use the meta object to send any extra data needed in the notification payload
})

Note: The previous example defined the Notification inline. You're also able to instantiate a Notification object for better code completion and discoverability of the Notification fields.

const notification = new FSBL.Clients.NotificationClient.Notification();
notification.source = "Finsemble";
notification.title = "Request for Quote";
notification.details = "RIO - 9409 @ $9409";
FSBL.Clients.NotificationClient.notify(notification);

Sending an Actionable Notification

Finsemble allows for 6 different Action types: SNOOZE, SPAWN, DISMISS, TRANSMIT, QUERY and PUBLISH. SNOOZE, SPAWN and DISMISS are handled internally by Finsemble while TRANSMIT, QUERY, PUBLISH allow you to perform custom actions on a notification.

Please Note: With the Finsemble 5.3 Notifications UI refresh, the DISMISS Action is equivalent to marking a notification as read. This is not to be confused with the dismissing of a toast from view

Here are the steps required to send actions along with a notification:

// Anatomy of A spawn action
const spawnAction = {
	buttonText: "Open Component",
	type: FSBL.Clients.NotificationClient.ActionTypes.SPAWN,
	component: "Welcome Component", // The Component Type to spawn when using the spawn action.
	spawnParams: {}, // The spawnParams passed to the spawn function. See the spawn function on the LauncherClient for more info.
	markAsRead: false // If true, the Notification will be marked as read after performing the action.
}

// Anatomy of SNOOZE action
// You're also able to instantiate an equivalent Action object to allow for better code completion.
const snoozeAction = new FSBL.Clients.NotificationClient.Action(); // A single action has been added making this an Actionable Notification
snoozeAction.buttonText = "Snooze";
snoozeAction.type = FSBL.Clients.NotificationClient.ActionTypes.SPAWN;
snoozeAction.milliseconds = "300000"; // Used to override the snooze length when using the SNOOZE action type
// NOTE: the markAsRead value is ignored for SNOOZE and DISMISS actions.


// Anatomy of QUERY, TRANSMIT and PUBLISH actions
const queryAction = {
	buttonText: "My Custom Action",
	type: FSBL.Clients.NotificationClient.ActionTypes.QUERY, // |TRANSMIT|PUBLISH,
	channel: "my.router.channel", // Channel to transmit payload on QUERY, TRANSMIT and PUBLISH actions
	payload: { myData: "value" } // Payload transmitted along channel on QUERY
}


FSBL.Clients.NotificationClient.notify({
	source: "Finsemble", // Where the Notification was sent from
	title: "Request for Quote",
	details: "RIO - 9409 @ $9409",
	actions: [spawnAction, queryAction, snoozeAction]
	// Actions have been added making this an Actionable Notification
})

Action Groups

In Finsemble's 5.3 Notifications UI refresh we've introduced the concept of Action Groups which allows combining related actions in the UI.

A groups of actions combined in Action Group (closed)
A group of actions combined into an Action Group (closed)

A groups of actions combined in Action Group (closed)
A group of actions combined into an Action Group (open)

Sending an Action Group is as easy as sending a notification with two or more notifications combined in an array.

const snoozeAction = {
	buttonText: "Snooze",
	type: FSBL.Clients.NotificationClient.ActionTypes.SNOOZE
}

const snoozeAction2 = {
	buttonText: "5 Min",
	type: FSBL.Clients.NotificationClient.ActionTypes.SNOOZE,
	milliseconds: "300000"
}

const snoozeAction3 = {
	buttonText: "10 Min",
	type: FSBL.Clients.NotificationClient.ActionTypes.SNOOZE,
	milliseconds: "600000"
}

// Combine them in an array
const actionGroup = [snoozeAction, snoozeAction2, snoozeAction3]

// Send
FSBL.Clients.NotificationClient.notify({
	source: "Finsemble", // Where the Notification was sent from
	title: "Request for Quote",
	details: "RIO - 9409 @ $9409",
	actions: [actionGroup] // You are able to mix and match Action Groups and Individual Actions
})

Custom Actions

Snoozing, spawning a component, and marking a notification as read (DISMISS) are very useful ways of interacting with a notification on their own. These actions have some limitation in that they perform only a single operation. If you're looking to augment your user's workflow with more complex Notification to operation behavior, this is where the QUERY, TRANSMIT and PUBLISH actions shine.

Sending a notification with a QUERY, TRANSMIT or PUBLISH action can be seen below. Take a look at the Router tutorial for information on which communication model is best for your use case.

Sending a notification with a TRANSMIT action:

const queryAction = {
	buttonText: "My Custom Action",
	type: FSBL.Clients.NotificationClient.ActionTypes.TRANSMIT, // |QUERY|PUBLISH,
	channel: "my.custom.action", // Channel to transmit payload on QUERY, TRANSMIT and PUBLISH actions
	payload: { myData: "value" }, // Payload transmitted along channel on QUERY
	markAsRead: false,
};

FSBL.Clients.NotificationClient.notify({
	source: "Finsemble", // Where the Notification was sent from
	title: "Request for Quote",
	details: "RIO - 9409 @ $9409",
	actions: [queryAction],
});

We suggest setting up listeners to perform complex Actions inside a Finsemble Service as to separate concerns where possible but like with any Router api call, it can be done in a component too.

Setting up listener to perform complex queries.

const { RouterClient, NotificationClient } = FSBL.Clients; // Component Syntax
// const { RouterClient, NotificationClient } = Finsemble.Clients; // Service Syntax

RouterClient.addListener(
	"my.custom.action", // Matches action.channel
	(error, response) => {
		if (!error) {
			let notification = response.data.notification;
			let payload = response.data.actionPayload;

			console.log(notification);
			console.log(payload);
			// performCustomOperation(notification, payload)

			// Change the notification state and re-notify it
			notification.isRead = true;
			NotificationClient.notify(notification);
		}
	}
);

Notifications from an External Service

When sending notifications, you might want to know the last time Finsemble received a notification from a specific source. This will allow you to send any through any previously queued notifications if Finsemble was shut down at the time of the initial sending.

const sendQueuedNotifications = async () => {
	const notificationSource = "Finsemble";
	const isoFormattedTimeStamp = await FSBL.Clients.NotificationClient.getLastIssuedAt(notificationSource);
	console.log(isoFormattedTimeStamp);

	// Fetch Notifications from External web service since "isoFormattedTimeStamp"
	// Send multiple notifications
	// FSBL.Clients.NotificationClient.notify([newNotification1, newNotification2, ...]);
};

sendQueuedNotifications();

Getting Notifications inside your Components

There are two ways to get notifications. Getting previously sent notifications and subscribing to future notifications.

Getting previously sent Notifications

// Get notifications sent between now and one month ago
const getNotificationsHistory = async () => {
	let lastMonth = new Date();
	lastMonth.setMonth(lastMonth.getMonth() - 1);

	const notifications = await FSBL.Clients.NotificationClient.fetchHistory(lastMonth.toISOString());
	console.log(notifications);
};

getNotificationsHistory();

Subscribe to All Future Notifications

const notificationsSubscribe = async () => {
	const subscription = {};
	const subData = await FSBL.Clients.NotificationClient.subscribe(subscription, (notification) => {
		console.log("Notification Received", notification);
		// DO something
	});
	// use subData to unsubscribe
};

notificationsSubscribe();

Subscribe to a subset of Notifications

It may be that you only want to receive some notifications. This is possible by providing a filter when performing the subscription.

const filterSubscribe = async () => {
	const subscription = {
		filter: {
			// The callback below will be executed if notifications with notification.type == 'chat-notification' AND
			// notification.source == 'Symphony' are received
			include: [
				{
					type: "chat-notification",
					source: "Symphony",
				},
				// It's also possible to do deep matches
				{ "meta.myCustomObject.field": "email-notification" }, // For an OR match add two objects in the include filter
			],
			// The exclude filter works the same as the include filter but the callback will NOT be executed if there
			// is a match. Exclude matches take precedence over include matches.
			exclude: [],
		},
	};
	const subData = await FSBL.Clients.NotificationClient.subscribe(subscription, (notification) => {
		console.log("Filter Matches. Notification Received", notification);
		// DO something
	});
	// use subData to unsubscribe
};

filterSubscribe();

// The notification below will be Received with the filter above
FSBL.Clients.NotificationClient.notify({
	source: "Finsemble", // Where the Notification was sent from
	title: "Request for Quote",
	details: "RIO - 9409 @ $9409",
	type: "chat-notification",
	source: "Symphony",
});

Send Notifications to the OS

Using the config, you can also send your notifications to the OS. Do this by specifying the proxyToWebApiFilter on the servicesConfig.notifications object in the seed ./configs/application/config.json. This is achieved by specifying a filter for the proxyToWebApiFilter field.

Note: Notifications proxied to the OS have the limitation of not being able to perform actions.

For more information on Filters and Filtering see Subscribe to a subset of Notifications and Subscription Filters Explained

For more information on filtering, see the section on Fetching and Receiving Notifications.

Example config:

{
	"servicesConfig": {
		"notifications": {
			"proxyToWebApiFilter": {
				"include": [
					{
						"type": "web"
					}
				],
				"exclude": []
			}
		}
	}
}

Notification Types (Templates)

Defining a Notification type allows you to apply a default set of values to notifications. When a notification arrives with the type field set, the template is retrieved from the configuration and the values specified are applied to any empty/unset fields. Hence, a Notification Type is a template, making it possible to populate a Notification by only setting the type (plus any other field you need to populate or override).

Notification Types also inform the preferences UI, allowing granularity on Muting Notifications and giving users a finer level of control, by allowing them to specify Types of Notifications from each source to mute (as opposed to all notifications from that source).

Specifying a Type using the key default, will apply defaults to all notifications sent, provided that they do not match any other notification type specified.

Configure Types by adding to the servicesConfig.notifications.types object in ./configs/application/config.json in your Finsemble project.

Example type definition:

{
	"servicesConfig": {
		"notifications": {
			"types": {
				"urgent": {
					"defaults": {
						"cssClassName": "my-urgent-class-name",
						"actions": [
							{
								"buttonText": "Mark Read",
								"type": "DISMISS"
							}
						]
					}
				}
			}
		}
	}
}

The Type defined in the config above is urgent, and in the code snippet below, the type is set to urgent. Since the notification sent has no actions and no cssClassName, these fields will be automatically populated with the values specified in the type's config.

FSBL.Clients.NotificationClient.notify([
	{
		source: "Finsemble", // Where the Notification was sent from
		title: `Request for Quote`,
		details: "RIO - 9409 @ $9409",
		type: "urgent",
	},
]);

Customizing the Display of Notifications

Theming Notifications

If you're looking to change the theme of Notifications in general, take a look at Customizing The UI

Styling Individual Notifications and Types

You may need certain notifications or notification types to display differently in order to draw more or less attention to them. This is possible with the use of the cssClassName field on a notification. Setting this field will add the specified class name to the notification HTML making it possible to style the notifications with CSS.

To inspect and target the specific notification HTML, open up a developer console by clicking on the Finsemble icon in the top left corner of the toolbar and select "Central Logger" from the menu. In the bottom left panel, click and open the developer tools by clicking on the Notification Center and/or Notification Toasts components.

Place any targeted css in your theme outlined by the Customizing The UI tutorial.

Additional information: Take a look at Notification Types on how to apply a css class to all notifications belonging to a specific type.

Changing the Entire Notification

If Theming and Style are not enough to cover your needs, it's possible to create an entirely custom Notification Card:

Create your own Notification Card component:

// Create a custom representation of your notifications / display custom meta fields
const NotificationCard = (props: any) => {
	return (
		<div style={{ backgroundColor: "black", height: "200px" }}>
			This is a custom notification card: {props.notification.id}
			Meta field: {props.notification.meta.id}
		</div>
	);
};

Import your custom notification card into the Notification Center and Notification Toasts components:

// finsemble-seed/src/components/notificationsToasts/NotificationsToasts.tsx
import NotificationCard from "NotificationCard.tsx";

ReactDOM.render(
	<FinsembleProvider>
		<NotificationsToasts notificationCard={NotificationCard} />
	</FinsembleProvider>,
	document.getElementById("notifications-toasts")
);
// finsemble-seedp/src/components/notificationsCenter/NotificationsCenter.tsx

import NotificationCard from "NotificationCard.tsx";

ReactDOM.render(
	<FinsembleProvider>
		<NotificationsCenter notificationCard={NotificationCard} />
	</FinsembleProvider>,
	document.getElementById("notifications-center")
);

Note: For the Notification Center it's also possible to customize the List Row and List Header using the method details above.

Muting Notifications

At its core, the purpose of a notification is to draw the user's attention and let them know something has or is about to happen. The most effective way of grabbing attention is with a Notification toast popping up from off-screen. If the information presented in the Toast is not important or relevant, and this unimportant/irrelevant information grabs the user's attention too frequently, it can frequently be distracting and can hinder the user in doing their tasks. There are two ways to control this behaviour. The first, for developers, is via the Notifications Toasts component Filter config, and the second is user-controlled muting.

Notification Toasts Component Config

Toasts are often the main source of distraction/annoyance for a user receiving too many Notifications. It might be the case you wish to send a notification and not have it pop-up and grab the user's attention but still have it available in the Notification Center. In this case, you are able to send a notification with a specific field defined internally within your organisation identifying that this Notification is of low importance and configure the Toasts component not to show this Notification.

See below for an example:

Configure the Notification Toasts component to exclude any notifications that come in with the value meta.userDefinedFieldToSkipToasts set to true. Do this by adding an exclude filter to the NotificationsToasts.window.data.notifications object

// finsemble-seed/configs/application/UIComponents.json

"NotificationsToasts": {
    "window": {
        ...
        "data": {
            "notifications": {
                "applyMuteFilters": true,
                "filter": {
                    "include": [],
                    "exclude": [{
                        "meta.userDefinedFieldToSkipToasts": true
                    }]
                },
                ...
            }
        },
        ...
    },
    ...
}

After Restarting Finsemble, sending a notification such as the one below will not toast while still appearing in the Notification Center.

// Run in a Finsemble Developer Console
FSBL.Clients.NotificationClient.notify({
	title: "Notify world",
	source: "oms",
	type: "general",
	meta: { userDefinedFieldToSkipToasts: true },
});

User Controlled Muting

Users also have some ability to control what notifications are able to grab their attention. Muting is done via the Notification context menu.

Muting from the notification context menu
Muting from the notification context menu

After muting, any notifications process after that point will have the isMuted value set to true. At this point the Notification UI can then be configured to respect or ignore this isMuted value. This is done changing the component's applyMuteFilters value to true. The default configuration is for the mute filters to only be applied to the toasts component, meaning muted notifications will appear in the Center. Setting this to false would mean disabling user muting.

Set the applyMuteFilters value.

// finsemble-seed/configs/application/UIComponents.json

"NotificationsToasts": {
    "window": {
        ...
        "data": {
            "notifications": {
                "applyMuteFilters": true,
                ...
            }
        },
        ...
    },
    ...
}

Note: we do not recommend applying mute filters on the Notification Center

Muting from the Preferences panel

User are also able to mute from the Preferences Panel found in the File Menu > Preferences > Notifications > Notification Sources.

Notification Preferences Panel

If the panel above does not appear in your User Preferences component, add it in by following the step "Add The Notifications Menu to the User Preferences" in Using Notifications with Older Seeds below.

Notification Storage and Persistence

Persistence

By default, Finsemble stores a maximum of 1000 notifications or 7 days which ever comes first. These directives can be changed on the notifications object on the servicesConfig object in ./configs/application/config.json. See the Config Reference for more information.

Note: Notifications are not actively purged. As a new notification comes in, the collection is evaluated for notifications to purge from storage.

Storage

Finsemble notification gets persisted using the finsemble.notifications storage topic. To change the storage adapter Notifications uses, set the appropriate value by adding the finsemble.notifications key to the servicesConfig.storage.topicToDataStoreAdapters object in ./configs/application/config.json in the Finsemble seed:

{
	"servicesConfig": {
		"storage": {
			"topicToDataStoreAdapters": {
				"finsemble.notifications": "IndexedDBAdapter"
			}
		}
	}
}

Remote Desktop and Transparency Support

Please Note: Notifications do work over RDP but will need a config update due to the way that transparency appears when running Finsemble on RDP.

In your configs/application/UIComponents.json in the Finsemble Seed, change to the value "[Component Name].window.options.transparent": true to false for the NotificationsToasts and NotificationsCenter components.

Subscription Filters Explained

Primitives

A primitive is an object with properties that are matched.

  • {source:"Finsemble"} - primitive that checks that the source field is equal to "Finsemble"

Multiple fields in a primitive are, by default, joined by logical AND. See under Modifiers to change this.

  • {source:"Finsemble",type:"RFQ"} - primitive that checks that the source field is equal to "Finsemble" AND that the type field is equal to RFQ

The name of a field in a primitive is always the name of the field to match in the record. The value can be one of:

  • Basic types: string, number, date - will match directly against the value in the record. Case is ignored in string matches.
  • Array: will match against any one of the values in the array. See below.

Primitives will filter against individual values and against one or more matches in an array. So the filter {field:"value1"} will match against any of the following objects:

  • {field:"value1"}
  • {field:["value1","differentValue"]}
  • {field:["differentValue","value1"]}

Deep Filtering

You are not limited to filtering only at the top level. You also can do deep filter on an object of an object using dot-notation. So if you want to match on the object {meta: {customValue: true}} then you can filter:

let filter = { "meta.customValue": true };

The above is a filter primitive that checks that the field "meta" has an object as its value, which in turn has a key "customValue" with a value of true. You can go as deep as you want. The following is a completely valid deep-filter primitive:

let filter = {"meta.customObject.moreData.field":"my value"}

Any modifiers that apply to simple primitives apply to deep fields as well.

Deep Filtering Arrays

Deep Filtering is not limited to objects embedded in objects. You can have arrays of objects embedded in objects. You even can have arrays of objects embedded in arrays of objects embedded in... (you get the idea!).

Array Primitive

If the value of a field in a primitive is an array, then it will accept a match of any one of the array values.

{field:["valueA","valueB"]} // accepts any record where the field matches 'valueA' or 'valueB'

Additionally, if the target record also has an array, it will accept a match if any one of the values in the array of the record matches any one of the values in the array of the filter term.

{field:["valueA","valueB"]}

will match any of these:

{field:"valueA"}
{field:"valueB"}
{field:["valueA","Jim"]}

Using Notifications with Older Seeds

In order to use the new Notifications functionality (5.3 and beyond) with older version 5.x seeds please take the following steps:

  • Update the following Finsemble packages in the seed package.json to the latest and do a yarn install:
    "@finsemble/finsemble-cli": "5.4.*"
    "@finsemble/finsemble-core": "5.4.*",
    "@finsemble/finsemble-electron-adapter": "5.4.*",
    "@finsemble/finsemble-ui": "5.4.*"
  • Set up the two react components
  • Add the new components entries to the webpack build
  • Add the configuration for the new components
  • Add the bell icon to the Toolbar
    • // finsemble-seed/src/components/toolbar/src/Toolbar.tsx
      import { NotificationControl } from "@finsemble/finsemble-ui/react/components/toolbar";
      
      ...
      
      return (
        <ToolbarShell hotkeyShow={["ctrl", "alt", "t"]} hotkeyHide={["ctrl", "alt", "h"]}>
      
          ...
      
          <ToolbarSection className="right">
      
            ...
      
            <RevealAll />
            <NotificationControl />
          </ToolbarSection>
          <div className="resize-area"></div>
        </ToolbarShell>
      );
      
      ...
      
  • Add The Notifications Menu to the User Preferences (added in 5.4.0)
    • // finsemble-seed/src/components/userPreferences/UserPreferences.tsx
      import {
      	UserPreferences,
      	General,
      	Workspaces,
      	DashbarEditor,
          // Import the Notifications component
      	Notifications,
      } from "@finsemble/finsemble-ui/react/components/userPreferences";
      
      ...
      
      const sections = {
        General: General,
        Workspaces: Workspaces,
        Dashbar: DashbarEditor,
        // Add in the Notifications Section
        Notifications: Notifications,
      };
      
      ...
      
  • Start Finsemble
  • Send a notification from the developer console:
    FSBL.Clients.NotificationClient.notify({
    	source: "Finsemble",
    	title: "Request for Quote",
    	details: "RIO - 9409 @ $9409",
    });