UI Components

The Layer UI Libraries ship with a number of widgets to simplify application development. This section explores those widgets in more detail.

Widgets in the Layer UI for Web Library can be broken up into two groups: Main Components which will be discussed here, and documented in depth in the API Reference, and Subcomponents which will only be documented in the API Reference.

  • Conversation Panel: Shows messages and lets the user send their own.
  • Conversations List: Manages a Conversation List and allows the user to select a Conversation
  • Identities List: Manages a list of users that can be selected
  • Notifier: Creates desktop and toast notifications for new messages
  • Presence: Shows current user status
  • Send Button: A button for sending the message that can be added to your Message Composer
  • File Upload Button: A button for selecting and sending a file that can be used to the Message Composer

Common Properties

Properties commonly seen among Main Components are described below.

Property 1. The Layer Client

All Main Components require access to your layer.Client (Layer WebSDK), this can be provided in a few ways:

  1. Pass your appId into layerUI.init({appId: '...'})
  2. Pass an app-id attribute or appId property into your Main Component.
  3. Pass a client property with your Layer Client into the widget.

An Application ID is used by the widgets to lookup the correct Client and access its events and data.

Note that all of these require you to have instantiated your layer.Client (Layer WebSDK) before you start creating these widgets. This Client does not need to be authenticated, but it does need to have been created with that App ID.

Examples:

<layer-conversation-panel app-id="layer:///apps/staging/UUID"></layer-conversation-panel>
var panel = document.createElement("layer-conversation-panel");
panel.appId = "layer:///apps/staging/UUID";
var panel = document.createElement("layer-conversation-panel");
panel.client = myClient;

Property 2. The Query

Most of the Main Components list data. This data comes from the Layer WebSDK’s layer.Query class. There are a few options for managing these queries:

  1. Create a layer.Query instance, pass it into the widget, and you can manage the query directly, and use it to influence what is rendered by the widget.
  2. If you don’t provide a Query to the Main Component, it will create its own.
  3. You may set the useGeneratedQuery property to false to prevent a query from being generated and force it to wait for you to provide a query.

When should you provide your own Query?

  1. You are using the Query data elsewhere in your app; having multiple queries loading the same data from the server is not ideal.
  2. Your data flow architecture expects you to control this data
  3. You want to control the predicate of the query so you can adjust what data is shown (better predicate support is coming soon; currently this is very limited)

How do you pass your own Query into the widget?

  1. By setting the widget’s query-id attribute or queryId property with your layer.Query object’s id.
  2. By setting the widget’s query property to your layer.Query.

Note that most apps can ommit this property, and let it be generated for them; Examples below are for apps that need to manage their own query:

Examples:

<layer-conversations-list query-id="layer:///queries/UUID"></layer-conversations-list>
var panel = document.createElement("layer-conversations-list");
panel.queryId = "layer:///queries/UUID";
var panel = document.createElement("layer-conversations-list");
panel.query = myQuery;

The Conversation Panel

The Layer Conversation Panel includes the following subcomponents:

  • Message List: Renders a list of Messages returned by your layer.Query
  • Typing Indicator Panel: Renders a message indicating who is currently typing a Message into this Conversation
  • Composer: A text area for typing a Message, and a panel for adding buttons next to the text area

How to work with this panel depends upon whether you are creating a Query and passing it to the panel or letting the panel create its own query.

Panel Create its Query

If you let the panel create its own query, then typically the only property/event you need for a basic app is the converation-id attribute or conversationId property. This property determines:

  • What Messages the query will request, which in turn controls what messages are rendered in the Messages List
  • What Conversation a Message will be sent to when the user types in a Message into the Composer
  • What Conversation typing indicators will be sent/received for.

Lets take a simple scenario where your app has determined a Conversation ID that should be opened so the user can read and write messages

Note

Conversation IDs will match the layer.Conversation IDs: layer:///conversations/UUID.

Using a templating engine, this might look like:

<layer-conversation-panel conversation-id={mySelectedConversationId}></layer-conversation-panel>

Or using Javascript, this might look like:

var conversationPanel = document.createElement('layer-conversation-panel');
function selectConversation(mySelectedConversationId) {
   conversationPanel.conversationId = mySelectedConversationId;
}

Panel is Provided a Query

If you are managing the Panel’s Query rather than letting it create its own query, then you are responsible for

  • updating the query’s predicate to get new Messages
  • updating the conversationId, conversation-id or conversation so that the Composer and Typing Indicators can still work

Using a templating engine, this might look like:

<script>
var selectedConversationId;
var myQuery = client.createQuery({
  model: layer.Query.Message
});
function selectConversation(newSelectedConversationId) {
  myQuery.update({
    predicate: 'conversation.id = "' + mySelectedConversationId + '"'
  });
  selectedConversationId = newSelectedConversationId;
}
</script>
<layer-conversation-panel query-id={myQuery.id} conversation-id={selectedConversationId}>
</layer-conversation-panel>

Or using Javascript, this might look like:

var conversationPanel = document.createElement('layer-conversation-panel');
var myQuery = client.createQuery({
  model: layer.Query.Message
});
conversationPanel.query = myQuery;
function selectConversation(mySelectedConversationId) {
   conversationPanel.conversationId = mySelectedConversationId;
   myQuery.update({
    predicate: 'conversation.id = "' + mySelectedConversationId + '"'
  });
}

Key Properties

There are various properties worth being aware of; more are documented in the API Reference, but of these, only conversationId is required to use this widget.

Name Type Description
conversationId property Set the Conversation that this panel interacts with. Can also use just conversation to set the layer.Conversation object
appId property Sets the layer.Client that will be used to talk to layer’s services. Can also use the client property or layerUI.init({appId:...})
queryId property Sets the query whose messages will be rendered; will create its own query if not provided. Can also use the query property.
composeButtons property Array of DOM nodes to put next to the Composer; example: A file upload widget. For customizing buttons in your Compose Panel
composeText property Put text into the Composer/get text from the Composer
disable property Disable this panel to prevent it from sending read receipts; use this when the panel is hidden but may not know its hidden

All properties are documented in detail with examples where necessary in the API Reference.

The Lists

Three lists are provided by the Layer UI Framework: Converations, Messages and Identities. Of these, you would typically not directly use the Messages List (but instead would use the Conversation Panel that contains the Messages List). All Lists provide the following properties:

Name Type Description
appId property Sets the layer.Client that will be used to talk to layer’s services. Can also use the client property
queryId property Sets the query whose results will be rendered; will create its own query if not provided. Can also use the query property.
onRenderListItem property Provide custom function for rendering elements between items in your list
pageSize property Number of items to load from the server each time it pages for more data
filter property Function, string or Regular Expression for hiding list items that don’t match

The onRenderListItem property

Of these properties, this one deserves special note. Suppose you want to

  • add separators between yesterday’s messages and today’s messages to designate the start of the day
  • add a separator between messages you have already read and those that remain unread, labeled as “You have read up to here”
  • add separators between Conversations to designate some grouping within the the Conversations
  • add separators between Identities/users to designate some grouping within the user list

The onRenderListItem property of all List widgets, and the customNodeAbove and customNodeBelow nodes of all List Item widgets allows this. Note that the Conversation Panel exposes some List properties such as this which it passes on to the Message List, as shown in this example:

conversationPanel.onRenderListItem = dateSeparatorRenderer;

function dateSeparatorRenderer(widget, messages, index) {
  var html = '';
  var message = widget.item;
  var sentAt = message.sentAt;
  var priorSentAt = index > 0 ? messages[index - 1].sentAt : 0;

  var needsBoundary = index === 0 || sentAt.toDateString() !== priorSentAt.toDateString();
  if (needsBoundary) {
      html += '<div class="sample-date-separator"><span>' + message.sentAt.toDateString() + '</span></div>';
  }

  if (html) {
    widget.customNodeAbove = html;
  } else if (widget.customNodeAbove) {
    widget.customNodeAbove = null;
  }
}

Note that customNodeAbove and customNodeBelow accept either a DOM Node or a string that can be used to generate DOM Nodes.

Also note that you need to be prepared to clear values as state changes. In the above code, paging could cause the first message in the list to no longer be the first message on that given date. Deleting that message could make the next message the new first message in that date.

Note that the Date Separator is such a common use case that this is already provided for those apps that want to use it:

conversationPanel.onRenderListItem = layerUI.utils.dateSeparator;

Or you can use this in combination with other such utilities:

conversationPanel.onRenderListItem = function(widget, messages, index, isTopItem) {
   layerUI.utils.dateSeparator(widget, messages, index);
   handler2(widget, messages, index, isTopItem);
   handler3(widget, messages, index, isTopItem);
}

Finally: A utility is provided to simplify managing multiple widgets trying to all set customNodeAbove or customNodeBelow. Each utility can call LayerUI.addListItemSeparator which will add a parent node if needed, and then append each separator to the parent:

LayerUI.addListItemSeparator(listItem, `<span>${dateStr}</span>`, 'my-css-class', true);

this adds the dateStr to the listItem, with my-css-class as the css class for the container, and true indicates that this goes above rather than below the list item.

The Conversations List

The Conversation List serves the following goals:

  1. Selecting a Conversation or Channel to interact with
  2. Showing the user which Conversation or Channel is selected
  3. Finding Conversations with unread messages
  4. Seeing the state of all of your Conversations

These goals are provided by the following properties which it adds in addition to the List properties shown above:

Name Type Description
selectedId property Set and get the selected Conversation or Channel by ID
layer-conversation-selected event Provide a handler any time the user changes the selected Conversation or Channel. Also accessed by the onConversationSelected property.
canRenderLastMessage property Provide a function to determine if the Conversation’s last message should be fully rendered
layer-conversation-deleted event Provide a handler any time the user deletes a Conversation or Channel. Also accessed by the onConversationDeleted property.

So given the following pseudo-code template:

<layer-conversations-list selected-id={selectedConversation.id}>
  </layer-conversations-list>
<layer-conversation-panel conversation-id={selectedConversation.id}>
  </layer-conversation-panel>
<script>
var selectedConversation = initialConversation;
document.body.addEventListener('layer-conversation-selected', function(evt) {
  selectedConversation = evt.detail.item;
});
</script>

Alternatively in pure Javascript:

var conversationPanel = document.createElement('layer-conversation-panel');
var conversationsList = document.createElement('layer-conversations-list');
var selectedConversation = initialConversation;

conversationsList.selectedId = selectedConversation.id;
conversationPanel.conversationId = selectedConversation.id;

conversationList.addEventListener('layer-conversation-selected', function(evt) {
  selectedConversation = evt.detail.item;
  conversationPanel.conversationId = selectedConversation.id;
});

For more information on the widget and its properties, see the API Reference.

Note that at this time, this widget can have a Query that returns Conversations or Channels. There is not yet a way to have it display both at the same time.

The Identities List

The Identities List serves the following goals:

  1. Present a list of people that your user is connected with and that your user may want to start a conversation with
  2. Present a list of people that your user may want to add to an existing conversation

Its important to understand that the Identities list shows a list of Identity objects the Layer’s servers believe are of interest to your user (i.e. that your user Follows). This means that the list is populated with:

  • People that your user has had Conversations with in the past
  • People that you have explicitly issued a client.followIdentity(userIdentity) operation on from the client
  • People that your server has explicitly issued a follows request on. These requests are sent to Layer’s Server API.

The Identities List goals are provided by the following properties which it adds in addition to the List properties shown above:

Name Type Description
selectedIdentities property Use this property to get and set which users are selected
layer-identity-selected event Use this handler if you need to know each time an Identity has been selected by the user. Also accessed with the onIdentitySelected property.
layer-identity-deselected event Use this handler if you need to know each time an Identity has been deselected by the user. Also accessed with the onIdentityDeselected property.

So given the following pseudo-code template can be used to edit a Conversation’s participants:

<layer-identities-list></layer-identities-list>
<button>OK</button>
<script>
var identitiesList = document.querySelector('layer-identities-list');
identitiesList.selectedIdentities = currentConversation.participants;

var button = document.querySelector('button');
button.addEventListener('click', function() {
  currentConversation.replaceParticipants(identitiesList.selectedIdentities);
});
</script>

Given the following javascript, we can create a new Conversation:

var identities = document.createElement('layer-identities-list');
var button = document.createElement('button');
button.onclick = function() {
  client.createConversation({
    participants: identities.selectedIdentities,
    distinct: false
  });
}

Note

Do not manipulate the selectedIdentities array via push, pop, splice, etc…, you must assign the property a new array for it to receive the updated selection.

The Notifier Widget

The Notifier Widget is a simple widget for managing notifications of new messages. As a Main Component it requries an appId or client property (though it will receive any appId provided via layerUI.init()). It does not use any layer.Query. It provides the following properties:

Name Type Description
notifyInBackground property When a new message is received while the browser tab does not have focus, what kind of notification should it use? desktop, toast, or none?
notifyInForeground property When a new message is received while the browser tab has focus, what kind of notification should it use? desktop, toast, or none?
iconUrl property Provide an icon for your app if you want notifications to contain your app’s icon. Leave it unset to use the message sender’s avatarUrl for an image.
timeoutSeconds property Number of seconds before the desktop or toast notification is automatically closed. Clicking will also close the notification
layer-message-notification event This event is triggered before showing a notification, allowing an app to determine that a notification is/is-not needed. Also accessed with the onMessageNotification property.
layer-notification-click event When the user clicks on a Notification, only your app knows how to show the selected Message or its Conversation. This event lets the app perform that action. Also accessed with the onNotificationClick property.
notifyInTitlebar property Boolean to enable/disable showing an unread indicator in the titlebar? Prefixes the title with ⬤
notifyCharacterForTitlebar property Change the titlebar indicator from ⬤ to some character of your choice

The Notifier Widget currently shows only a single toast notification as the widget itself is rendered to present the notification rather than it generating new notification dialogs with each event. This means that your Notifier widget should go in your document’s body, not its head.

The following example shows a typical scenario.

<body>
  <layer-notifier></layer-notifier>
  <layer-conversations-list></layer-conversations-list>
  <layer-conversation-panel></layer-conversation-panel>
  <script>
  var selectedConversation;
  var conversationsList = document.querySelector('layer-conversations-list');
  var conversationPanel = document.querySelector('layer-conversation-panel');

  // Prevent the notification from triggering if the app is in the foreground and
  // we are already viewing the Conversation.
  //
  // Note that if notifyInForeground
  // is set to "none", this event handling is not needed as the app will only receive
  // notifications when in the background.
  document.body.addEventListener('layer-message-notification', function(evt) {
    var message = evt.detail.item;
    var appIsInBackground = evt.detail.isBackground;
    var conversationId = message.conversationId;
    if (selectedConversation.id === conversationId && !isBackground) {
      evt.preventDefault();
    }
  });

  // On selecting the Notification:
  // 1. update the selected item in the Conversation List so that the list will show
  // you what conversation is selected
  // 2. update the conversationId of the Conversation Panel so it will open that Conversation
  document.body.addEventListener('layer-notification-click', function(evt) {
    var message = evt.detail.item;
    var conversationId = message.conversationId;
    selectedConversation = message.getConversation(true);
    conversationsList.selectedConversationId = conversationId;
    conversationPanel.conversationId = conversationId;
  });
  </script>
</body>

The Presence Widget

The Presence Widget is a simple widget that shows the status of a specific user. It provides the following properties:

Name Type Description
item property Identity object for the user whose status is to be rendered
layer-presence-click event When the user clicks on the Presence widget, provides an event to your app. Also accessed by the onPresenceClick property.

The Presence Widget is built into the <layer-avatar /> widget, where it will show the status of each user as they are rendered. However, this widget is also a Main Component as it can be used on its own to render the authenticated user’s own status, and provide a click event to allow you to provide some UI to change that status.

The following example shows a typical scenario.

<body>
  <layer-presence></layer-presence>
  <script>
  var presenceWidget = document.querySelector('layer-presence');
  presenceWidget.item = client.user;
  presenceWidget.onPresenceClick = function() {
    client.user.setStatus(layer.Client.Identity.STATUS.BUSY);
  }
  </script>
</body>

Utility Components

The Send Button

The default Compose Panel consists of just a text input. Messages are sent by hitting the ENTER key on your keyboard.

This is not suited to all apps, so a Send Button can be easily added using the The Conversation Panel composeButtons property:

myConversationPanel.composeButtons = [
  document.createElement('layer-send-button')
];

The layer-send-button will emit a layer-send-click event when its clicked; this will be intercepted by the Compose Panel widget and will cause it to send the message without any configuration needed on your part. As long as the button is used within the Composer, it works without any extra wiring. If you want a button somewhere else, this widget offers no relevant functionality.

The File Upload Button

The default Compose Panel consists of just a text input. If you want users to be able to open a File Browse Dialog and upload a file, add the layer-file-upload-button button using the The Conversation Panel composeButtons property:

myConversationPanel.composeButtons = [
  document.createElement('layer-file-upload-button')
];

The layer-file-upload-button will emit a layer-file-selected event when a file is selected; this will be intercepted by the Compose Panel widget and will take the file(s) selected, generate Message Parts for them, and send them as a Message. No configuration is necessary.

If you want to customize this behavior; you can intercept the Messages its generating:

var uploadButton = document.createElement('layer-file-upload-button');
myConversationPanel.composeButtons = [
  uploadButton
];

uploadButton.addEventListener('layer-file-selected', function(evt) {
  var messageParts = evt.detail.parts;
  parts.push(new layer.MessagePart({
    mimeType: 'signature',
    body: 'I approve this attachment'
  });
});

The above handler modifies the message parts prior to them being sent. You could also call evt.preventDefault() to prevent the file from being sent. You may also prevent the default handling so you can send it yourself.

The File Utility

While many apps will want a simple file upload button, Layer UI for Web also provides a utility for drag and drop sending of files.

var dropWatcher = new layerUI.files.DragAndDropFileWatcher({
  node: dropNode,
  callback: sendAttachment,
  allowDocumentDrop: false
});
function sendAttachment(messageParts) {
   currentConversation.createMessage({ parts: messageParts }).send();
}
  • If a file is dragged into/out of your dropNode, the node will have a layer-file-drag-and-drop-hover CSS class added/removed
  • If a file is dropped into your dropNode, your sendAttachment callback will be called with the message parts generated from the dropped files.
  • The default allowDocumentDrop value is false. This setting prevents the web page from navigating to the dropped document if it Misses your drop zone.
UI Concepts Best Practices