Creating Custom Messages

Developing a Custom Message Type Model and its Message Type View is a key step in building a Custom Messaging Experiences.

Many projects will need to implement Custom Message Type Model components. Some of these Message Type Model components will be simple informational messages, and others will provide rich interactive messages. Message Type Model components that are interactive and allow participants in the conversation to interact using UIs provided within these Messages help form what Layer refers to as Messaging Experiences.

A Messaging Experience is:

  • a Message that is sent within a Conversation
  • is visible to participants of that Conversation
  • Enables UI interactions within that Message
  • Shares state changes from that UI across all participants

Message Model Lifecycles

On receiving a new Message and creating a MessageTypeModel for it, the following lifecycle is followed:

On creating a new Message to represent some Message Type Model:

var model = new TextModel({
  title: "I am an important title",
  text: "I am the text of the message"
});
model.send({
  conversation,
  callback: message => message.send()
});

The following lifecycle is followed:

Note

parseMessage and parseModelPart are not called in this flow; its assumed that the simple JSON-based properties are already setup correctly after calling the constructor.

Here are a list of methods used to create custom MessageTypeModel subclasses; their use is illustrated below.

Name Parameters Description
parseMessage() () Calls all methods needed to initialize the MessageTypeModel from a Message. Custom subclasses should not provide a custom version of this
parseModelPart() ({ payload, isEdit }) Subclass this method if your class does anything other than copying in simple string/number/booleans from the model.part.body JSON properties
parseModelChildParts() ({ changes, isEdit }) Subclass this method if your class has MessagePartcChild nodes to process or MessageTypeModel child nodes to process during intialization
parseModelResponses() () Subclass this method if your class uses Message Response data to set any of its properties.
getModelsByRole() (roleName) Returns array of MessageTypeModel objects with the specified role; typically used from MessageTypeModelparseModelChildParts().
initBodyWithMetadata() (fieldList) Returns a plain object that can be serialized and set as a MessagePartbody; used from MessageTypeModelgenerateParts() to specify which fields we want in our MessagePartbody.
addChildModel() (model, role, callback) Adds a model as a child model of the current model, and generates a MessagePart and adds it to the Message Part Tree. Used from MessageTypeModelgenerateParts() to generate MessagePart child nodes
addChildPart() (part, role) Adds a MessagePart as a child Message Part of the message being generated within MessageTypeModelgenerateParts().

Opinion Message Example

Goals: Introduce the basics of creating a custom Message Type that uses a Standard Message View Container.

An Opinion Message is a sample message used to allow a user to rate and comment on a previous message. Key data that this message will contain:

Name Type Description
comment String A comment that the user has about some item of content
rating Number A number from 1-5 indicating the level of enthusiasm for their comment (1: Comment is simply brain storming throwing ideas out there; 5: This is It! This is the Idea! We are Geniuses!)
description String The Message that an opinion is being expressed about
author String The author of the message described by description
A rating of 4, and a comment of "I love this stuff" for Lamb Stew

The Opinion Message Type Model

This Opinion Message Type Model is a subclass of Message Type Model, which is defined using the root class Root.

Step 1: Setup a Basic Message Type Model Class

At a minimum, your class should setup

import { Core } from '@layerhq/web-xdk'
const { Root, MessageTypeModel, Client } = Core;

class OpinionModel extends MessageTypeModel { }

OpinionModel.MIMEType = 'application/vnd.customco.opinion+json';
OpinionModel.messageRenderer = 'vnd-customco-opinion-message-type-view';

OpinionModel.prototype.comment = '';
OpinionModel.prototype.rating = 0;
OpinionModel.prototype.description = '';
OpinionModel.prototype.author = '';

Core.Root.initClass.apply(OpinionModel, [OpinionModel, 'OpinionModel']);
Client.registerMessageTypeModelClass(OpinionModel, 'OpinionModel');

export default OpinionModel;
Step 2: Generating a Message

Each MessageTypeModel needs to know how to generate a Message that represents its data. This is done using the MessageTypeModelgenerateParts() method. This method knows what properties need to be written to the MessagePartbody, and creates the MessagePart that will transmit this Model’s data to all participants. This implementation uses the MessageTypeModelinitBodyWithMetadata() which generates the MessagePartbody from the specified property values:

import { Core } from '@layerhq/web-xdk';
const { MessagePart, MessageTypeModel } = Core;

class OpinionModel extends MessageTypeModel {
  generateParts(callback) {
    const body = this.initBodyWithMetadata(['comment', 'rating', 'description', 'author']);

    // Create the MessagePart using the static MIMEType property defined below;
    // Store the part in `this.part`
    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    // Provide the new part via a callback
    callback([this.part]);
  }
}

OpinionModel.prototype.comment = '';
OpinionModel.prototype.rating = 0;
OpinionModel.prototype.description = '';
OpinionModel.prototype.author = '';

The MessageTypeModelgenerateParts() method should

  1. Provide an array of MessagePart objects to the callback
  2. Set this.part to the Root MessagePart for its Message Part Subtree.
  3. Generate a MessagePart that contains all properties that need to be conveyed to other users. (comment, rating, description and author)

The MessageTypeModelgenerateParts() method will be called when creating a message to represent a locally created model:

// Locally created model
const model = new OpinionModel({
  comment: `I think thats a great idea, but we should consider passenger pidgeon
    as an alternative to make sure we've done our due diligence`,
  rating: 2,
  description: `Perhaps we should use Layer's XDK and build custom messages
     to create an optimized experience for our users`,
  author: "CTO"
});

 // Calling send or generateMessage() or send() will call generateParts()
model.send({ conversation });
Step 3: Working with the Standard Message View Container

Not all Models are designed to work with the Standard Message View Container; the Container shows metadata (title, description, footer, etc…) under the Message Type View. A Receipt Message would not be rendered with a title, description and footer below it. Nor would a Status Message. The Opinion Message however is going to support the Standard Message View Container which is done by supporting the getTitle, getDescription and getFooter methods:

class OpinionModel extends MessageTypeModel {
  getTitle() {return 'Opinion about:'; }
  getDescription() { return this.description; }
  getFooter() { return this.author; }
}
Step 4: Working with the Conversation Lists

The MessageTypeModelgetOneLineSummary() method controls how this Message is summarized, primarily for use within the Conversation List. Each Converation in the Converation List shows the Last Message summary; which for a Text Model/Message is usually just the text of the message. For an image though it might be “Image Received”. For this Opinion Model:

class OpinionModel extends MessageTypeModel {
  getOneLineSummary() {
    return `Opinion on ${this.author}'s Message`;
  }
}

This method may also be used to generate notifications to be sent with messages.

Step 5: Wrap up

Here is the full definition:

import { Core } from '@layerhq/web-xdk'
const { Root, MessageTypeModel, Client } = Core;

class OpinionModel extends MessageTypeModel {
  generateParts(callback) {
    const body = this.initBodyWithMetadata(['comment', 'rating', 'description', 'author']);

    // Create the MessagePart using the static MIMEType property defined below
    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    // Provide the new part via a callback
    callback([this.part]);
  }

  getTitle() {return 'Opinion about:'; }
  getDescription() { return this.description; }
  getFooter() { return this.author; }

  getOneLineSummary() {
    return `Opinion on ${this.author}'s Message`;
  }
}

OpinionModel.prototype.comment = '';
OpinionModel.prototype.rating = 0;
OpinionModel.prototype.description = '';
OpinionModel.prototype.author = '';

// Static property defines the MIME Type for this model
OpinionModel.MIMEType = 'application/vnd.customco.opinion+json';

// Static property specifies the preferred Message Type View for representing this Model
OpinionModel.messageRenderer = 'vnd-customco-opinion-message-type-view';

Root.initClass.apply(OpinionModel, [OpinionModel, 'OpinionModel']);
Client.registerMessageTypeModelClass(OpinionModel, 'OpinionModel');

export default OpinionModel;

The above MessageTypeModel subclass can be initialized in two ways: from a Message or from properties fed into a constructor.

Typically, a MessageTypeModel is initialized from a Message as an automatic part of processing the Messages being added to the Message List:

const model = message.createModel();

MessagecreateModel() checks to see if the Message has an existing Model, and if not, instantiates a new one. This will call MessageTypeModelparseMessage(); most of the handling of MessageTypeModelparseMessage() is handled by the by MessageTypeModel; the Opinion Model being a simple model does not need to provide its own version of this method.

The Opinion Message Type View

A Message Type View is a type of Webcomponent we will create using the Layer XDK Framework; it uses the MessageViewMixin Mixin to properly setup the View and its properties. Among other things, the Mixin defines the model property, and insures that any time the model is modified, the Model’s change events are wired up to call the View’s onRerender() method.

Step 1: Setup a Basic Message Type View Class
import Layer from '@layerhq/web-xdk';
const registerComponent = Layer.UI.registerComponent;
const MessageViewMixin = Layer.UI.mixins.MessageViewMixin;

registerComponent('vnd-customco-opinion-message-type-view', {
  mixins: [MessageViewMixin],
  style: `vnd-customco-opinion-message-type-view {
    display: block;
  }
  `,

  methods: {
    onRerender() {
      this.innerHTML = `${this.comment} : ${this.model.rating}`;
    }
  }
});

The above Message Type View is oversimplified but also functional and runnable.

  • The MessageViewMixin sets up the model property and wires its change events to the onRerender() method
  • The style insures that the UI Component has a suitable default display styling (required)
  • The onRerender method handles both initial rendering and any rerendering due to change events from the OpinionModel.

The onRerender method is called immediately after the onRender method, and handles any rendering that is likely to change during the lifespan of the component. The above innerHTML assignment could be done in onRender() if we can rule out allowing users to change their comment or rating.

Step 2: Using Templates, Styles and TextFormatters

An HTML template will have much nicer rendering than our initial version:

registerComponent('vnd-customco-opinion-message-type-view', {
  mixins: [MessageViewMixin],
  template: `
    <div class="user-rating" layer-id="rating"></div>
    <div class="user-comment" layer-id="comment"></div>
  `,
  style: `
    vnd-customco-opinion-message-type-view {
      display: flex;
      flex-direction: row;
      width: 100%;
    }
    vnd-customco-opinion-message-type-view .user-comment {
      min-height: 40px;
      text-align: center;
      flex-grow: 1;
      width: 100px /* Flexbox bug workaround */
    }
    vnd-customco-opinion-message-type-view .user-comment p {
      line-height: 40px;
    }

    vnd-customco-opinion-message-type-view .user-rating {
      text-align: center;
      line-height: 40px;
      width: 30px;
      border-right: solid 1px #ccc;
    }
    vnd-customco-opinion-message-type-view.rating1 .user-rating {
      background-color: light-green;
    }
    vnd-customco-opinion-message-type-view.rating2 .user-rating {
      background-color: #baefba;
      color: white;
    }
    vnd-customco-opinion-message-type-view.rating3 .user-rating {
      background-color: yellow;
    }
    vnd-customco-opinion-message-type-view.rating4 .user-rating {
      background-color: orange;
    }
    vnd-customco-opinion-message-type-view.rating5 .user-rating {
      background-color: red;
      color: white;
    }
  `,

  methods: {
    onRerender() {
      this.nodes.comment.innerHTML = this.comment;
      this.nodes.rating.innerHTML = this.model.rating;
      this.classList.add('rating' + this.model.rating);
    }
  }
});

The above View definition provides an HTML template, which defines this.nodes.comment and this.nodes.rating so that they can be directly manipulated and set.

It also provides CSS classes to adjust the styling based on the rating. Typically these class changes go at the top level of the View (i.e. vnd-customco-opinion-message-type-view.rating5 rather than .user-rating.rating5) so that any child nodes can be restyled based on those classes.

One additional improvement to the above UI Component would process the text, insure that newline characters are rendered with line breaks, emojis render correctly, URIs are hyperlinked, etc…

import Layer from '@layerhq/web-xdk';
const processText = Layer.UI.handlers.text.processText;
registerComponent('vnd-customco-opinion-message-type-view', {
  mixins: [MessageViewMixin],
  methods: {
    onRerender() {
      this.nodes.comment.innerHTML = processText(this.model.comment);
      this.nodes.rating.innerHTML = this.model.rating;
      this.classList.add('rating' + this.model.rating);
    }
  }
});
Step 3: Managing widths and heights of Messages
Message Heights

For best results, a Message should have either a known Height or have all content be renderable synchronously; failure to do this will result in messages that change height as they render causing the Messages after your Message to shift down. Messages shifting down may also cause the user looking at the bottom message to have that bottom message scroll out of view.

A Text Message does not have a known height, but all of its text is ready to render, and once its been put into the Message List, it will lay itself out synchronously.

An Image Message which does not know the height of its image will not know how much space it needs until the Image has been fetched from a remote server.

There are two ways of handling Message Height under these conditions:

  1. Fix the height of your message; once the height is fixed, all of your content must scale to fit within it.
  2. Use the isHeightAllocated property of your Message Type View to notify the Message List when one of its Messages has changed height

Fixing the message height can be done via CSS, but its recommended that you use the Message View Mixin’s height property:

registerComponent('my-message-view', {
  properties: {
    height: {
      value: 250,
    },
  }
});

Or:

registerComponent('my-message-view', {
  methods: {
    onAfterCreate() {
      this.height = this.model.needsLotsOfHeight ? 350 : 200;
    }
  }
});

The height property, if used in the Property Definition or in onAfterCreate makes using isHeightAllocated unnecessary. However if height may be set later, the following pattern should be used:

registerComponent('my-message-view', {
  properties: {
    isHeightAllocated: {
      value: false,
    },
    myData: {}
  },
  methods: {
    onAfterCreate() {
      myAsyncFetchContent(this.model.myCustomContentUrl, this._contentIsReady);
    },
    _contentIsReady(data) {
      this.myData = data;
      this.onRerender();
      this.isHeightAllocated = true;
    },
  },
});

The setter for isHeightAllocated will trigger an event telling the Message List that a height has changed. It will then adjust its scroll position to compensate if it can.

Height will not need to be managed for this Sample Message

Message Widths

For small screens, a Message’s maximum width is 85% of the Message List. For large screens, the maximym width is 60% of the Message List. Your Message may wish to further constrain these behaviors using the maxWidth and minWidth properties; these properties will be used in conjunction with the available width to set your Message’s actual width.

Why use properties instead of style sheets?

The number one reason is that when a Carousel of these items is rendered, the Carousel knows how best to resize these items to fit within itself. Other reasons:

  1. It makes it easier to customize it on the fly
  2. It makes it easier for your view to examine its available space, compare that with the assigned values of maxWidth and minWidth and determine the best behavior
  3. Style Sheets would need to account for the different conditions under which a Message might render (In a Message List, as an Item within a Carousel – or other Parent message --, Outside of the Message List, etc…).
import Layer from '@layerhq/web-xdk';

const Widths = Layer.UI.Constants.WIDTH;
registerComponent('vnd-customco-opinion-message-type-view', {
  mixins: [MessageViewMixin],
  style: `vnd-customco-opinion-message-type-view {
    display: block;
  }
  `,
  properties: {
    maxWidth: {
      value: 500
    },
    minWidth: {
      value: 350
    },
  }
});
Step 4: Working with the Standard Message View Container

In order to work with the Standard Message View Container, this View will a messageViewContainerTagName property.

Any Message Type View that uses Message View Container must specify the HTML Tag Name for the container that will contain that Message Type View using the View’s messageViewContainerTagName property.

import Layer from '@layerhq/web-xdk';

registerComponent('vnd-customco-opinion-message-type-view', {
  mixins: [MessageViewMixin],
  style: `vnd-customco-opinion-message-type-view {
    display: block;
  }
  `,
  properties: {
    messageViewContainerTagName: {
      noGetterFromSetter: true,
      value: 'layer-standard-message-view-container'
    }
  }
});

Using React Rendering for The Opinion Message Type View

Some developers may prefer to write their Views in react. This section shows how to use a React Component to handle rendering while using the webcomponent as a framework for it, providing the expected properties and methods provided by the MessageViewMixin.

Step 1: Setup a Basic Message Type View Class

This basically copies the template from above. All styling other than insuring that the webcomponent gets a display can be put into your project’s Style Sheets.

import { Layer } from '../../get-layer'
import './opinion-message-type-model';

const registerComponent = Layer.UI.registerComponent;
const MessageViewMixin = Layer.UI.mixins.MessageViewMixin;
const Widths = Layer.UI.Constants.WIDTH;

registerComponent('vnd-customco-opinion-message-type-view', {

  // Make sure that this View contains all the necessary Message Type View properties and methods
  mixins: [MessageViewMixin],

  // Make sure that the webcomponent has a display style
  style: `
    vnd-customco-opinion-message-type-view {
      display: block;
      width: 100%;
    }
  `,

  properties: {
    maxWidth: {
      value: 400
    },

    // Setup the Message Container
    messageViewContainerTagName: {
      noGetterFromSetter: true,
      value: 'layer-standard-message-view-container'
    }
  }
});
Step 2: Adding a React Renderer

The React component will take as its main property, the model:

import { Component } from "react";

class Opinion extends Component {
  render() {
    return (
      <div className={'rating' + this.props.model.rating}>
        <div className="user-rating">{this.props.model.rating}</div>
        <div className="user-comment">{this.props.model.comment}</div>
      </div>
    );
  }
}

Your react component can manage rendering, as well as internal state changes. It may also be useful within the constructor to subscribe to model changes:

import { Component } from "react";

class Opinion extends Component {
  constructor(props) {
    super(props);
    props.model.on('message-type-model:change', evt => this.setState({ lastEvent: evt }), this);
  }
  render() {
    return (
      <div className={'rating' + this.props.model.rating}>
        <div className="user-rating">{this.props.model.rating}</div>
        <div className="user-comment">{this.props.model.comment}</div>
      </div>
    );
  }
}

Finally, the webcomponent itself will need on onRender method:

import Layer from '@layerhq/web-xdk';
const registerComponent = Layer.UI.registerComponent;

registerComponent('vnd-customco-opinion-message-type-view', {
  methods: {
    onRender() {
      ReactDOM.render(<Opinion model={this.model} />, this);
    }
  },
});

PDF Message Example

Goals: Introduce the use of Message Part child nodes for carrying additional data

Recall that Message Parts are stored as an array, but the mimeAttributes are used to help understand which Message Parts are child nodes of other Message Parts; this is discussed in more detail in discussion of the Message Part Tree.

This PDF Message will render a PDF document with a title and author below the document. The PDF Message Type Model starts with what we saw in the Opinion Message Type Model and adds the following:

  • It has a MessagePart child node in its MessageTypeModelchildParts array
  • It has an action that it supports (performed when the user clicks/taps on the Message)
  • It renders richer content rather than simple textual information

A utility is provided by the Message Type Model for handling the common use case of a Message that has either a Message Part with data or a URL referencing data:

MessageTypeModel.DefineFileBehaviors({
  classDef: PDFModel,
  propertyName: 'source',
  sizeProperty: 'size',
  nameProperty: 'title',
  roleName: 'source',
});

The above call modifies your model class to do the following:

  • Provide a source and sourceUrl property (only one of which should be used at a time)
  • Provides a size and title property
  • Defines that when a calling new PDFModel({ source: PDFFileBlob }) that the FileBlob will turned into a Message Part whose role is source, and that Message Part will be written to the model.source property
  • Tells the model that when receiving PDFFileBlob that its file size can be written to a size property
  • Tells the model that when receiving PDFFileBlob that its file name can be written to its title property
  • Provides a model.getSourceUrl(callback) for retrieving content without needing to know if that content is in model.source or model.sourceUrl
  • Provides a model.getSourceBody(callback) for retrieving content without needing to know if that content is in model.source.body has already been fetched or not

A model could have multiple of these properties defined for it; for example, the Image Message not only uses the above definition, but additionally has defined preview, previewUrl, getPreviewUrl(callback) and getPreviewBody(callback) with:

MessageTypeModel.DefineFileBehaviors({
  classDef: ImageModel,
  propertyName: 'preview',
  roleName: 'preview',
});

The PDF Message will have the following properties:

Name Type Description
source MessagePart A MessagePart containing the PDF data
sourceUrl String Alternate to source for referencing a PDF file
title String The title of the document
author String The author of the document
size Number The size of the document in Bytes
A PDF Message

The PDF Message Type Model

Step 1: Setup a Basic Message Type Model Class

Following the example for Step 1 of the Opinion Message Type Model, we start with:

import { Core } from '@layerhq/web-xdk';
const { Client, MessagePart, Root, MessageTypeModel } = Core;

class PDFModel extends MessageTypeModel {}

PDFModel.prototype.author = '';
PDFModel.prototype.title = '';

MessageTypeModel.DefineFileBehaviors({
  classDef: PDFModel,
  propertyName: 'source',
  sizeProperty: 'size',
  nameProperty: 'title',
  roleName: 'source',
});

// Static property specifies the preferred Message Type View for representing this Model
PDFModel.messageRenderer = 'vnd-customco-pdf-message-type-view';

// Static property defines the MIME Type that will be used when creating new Messages from this Model
PDFModel.MIMEType = 'application/vnd.customco.pdf+json';

Core.Root.initClass.apply(PDFModel, [PDFModel, 'PDFModel']);
Client.registerMessageTypeModelClass(PDFModel, 'PDFModel');

export default PDFModel;
Step 2: Generating a Message

As shown with the Opinion Message Type Model Example, we provide a generateParts() method to transform the Model into a Message. The key difference from the Opinion Message that was generated is the need to store the PDF data in a separate MessagePart. generateParts() will be called automatically when a Model is turned into a Message to be sent.

Lets suppose that this Model is created after a Javascript File Object has been generated via an <input type="file" /> input:

var model = new PDFModel({
  source: fileInput.files[0],
  title: "Death star plans",
  author: "Varth Dater"
});

The above model is transformed into a Message using MessageTypeModelgenerateParts() as follows:

import Layer from '@layerhq/web-xdk';
class PDFModel extends MessageTypeModel {
  generateParts(callback) {
    const body = this.initBodyWithMetadata(['title', 'author', 'size']);

    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    // Replace the File/Blob source property with a proper MessagePart property.
    this.source = new MessagePart(this.source);

    // Setup this Message Part to be a child node within the Message Part Tree
    this.addChildPart(this.source, 'source');

    // Provide the Parts Array for this PDFModel
    callback([this.part, this.source]);
  }
}

The above example not only creates a Message with an extra MessagePart, but also establishes its place within the Message Part Tree by calling MessageTypeModeladdChildPart().

Note

The source property is not written to this.part.body; but rather is sent as a separate MessagPart; only title and author are sent in this.part.body.

The DefineFileBehaviors() call however sets up the parent class method such that the above generateParts() call can be simplified down to just:

import Layer from '@layerhq/web-xdk';
class PDFModel extends MessageTypeModel {
  generateParts(callback) {
    // Handles converting the PDF File Blob into the Source Message Part
    super.generateParts();

    // Generate our root Message Part:
    const body = this.initBodyWithMetadata(['title', 'author', 'size', 'sourceUrl']);
    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    // Provide the Parts Array for this PDFModel
    callback([this.part].concat(this.childParts));
  }
}
Step 3: Parsing a Message

In the previous example, the Opinion Message Type Model was simple enough that there were no Child Message Parts/Child Models. MessageTypeModelparseModelChildParts method is a lifecycle method used during initialization to look at all Child Models and Child Message Parts and initialize this Models’ state from those parts/models.

class PDFModel extends MessageTypeModel {
  parseModelChildParts({ changes, isEdit }) {
    super.parseModelChildParts({ changes, isEdit });

    // Setup this.source to refer to the MessagePart whose role=source
    this.source = this.childParts.filter(part => part.role === 'source')[0];
  }
}

The MessageTypeModelchildParts property:

  • Stores an array of MessagePart objects that are direct descendant child nodes of the PDF Message Type Model’s main Message Part within the Message Part Tree
  • Has 0 or more items in the array

Using the MessagePartrole property we can filter for the source part.

The DefineFileBehaviors() call however sets up the parent class method such that the above parseModelChildParts() call sets the this.source property for you; for this example, parseModelChildParts() can in fact be left out entirely.

Step 4: Working with the Standard Message View Container

In the previous example, the Opinion Message Type Model provided getTitle(), getDescription() and getFooter() methods in order to work with a Standard Message View Container. The PDF Message will need the same.

class PDFModel extends MessageTypeModel {
  getTitle() {
    return this.title || ''
  }
  getDescription() {
    return this.author || '';
  }
  getFooter() {
    // Returns a string with human readable file sizes
    return Layer.UI.UIUtils.humanFileSize(this.size);
  }
}
Step 5: Working with the Conversation Lists

As was shown in the Opinion Message Type Model Example, a getOneLineSummary() method controls how the Message is rendered in the Conversation List:

class OpinionModel extends MessageTypeModel {
  getOneLineSummary() {
    return this.title || 'PDF File';
  }
}

This method may also be used to generate notifications to be sent with messages.

Step 6: Providing an Action

A defaultAction property is added which specifies what happens when the user taps/clicks on a PDF Message View representing this Model:

class PDFModel extends MessageTypeModel {}

PDFModel.defaultAction = 'vnd-open-pdf';

The defaultAction of vnd-open-pdf tells the XDK that whenever the user selects a View representing this Model, the vnd-open-pdf action should be run. Note that each Message can be created with a custom action when the Message is sent; this overrides the default action.

A handler for vnd-open-pdf is registered with the XDK as follows:

import Layer from '@layerhq/web-xdk';
const register = Layer.UI.MessageActions.register;

register('vnd-open-pdf', function openPDFHandler({ data, model }) {
  // URLs to content will expire.  Use fetchStream and let the Message Part either provide
  // the current URL or request a new url from the server so we can open it.
  this.model.source.fetchStream(url => window.open(url));
});

Note

Providing an Action for this Message Type is done to explain how this is done. If the user should be able to interact directly with the message, providing an action is a bad idea. The next example gets rid of this action handler.

Step 7: Wrap up
import { Core } from '@layerhq/web-xdk';
const { Client, MessagePart, Root, MessageTypeModel } = Core;

class PDFModel extends MessageTypeModel {
  generateParts(callback) {
    // Handles converting the PDF File Blob into the Source Message Part
    super.generateParts();

    // Generate our root Message Part:
    const body = this.initBodyWithMetadata(['title', 'author', 'size', 'sourceUrl']);
    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    // Provide the Parts Array for this PDFModel
    callback([this.part].concat(this.childParts));
  }


  getTitle() {
    return this.title || ''
  }
  getDescription() {
    return this.author || '';
  }
  getFooter() {
    // Returns a string with human readable file sizes
    return Layer.UI.UIUtils.humanFileSize(this.size);
  }

  getOneLineSummary() {
    return this.title || 'PDF File';
  }
}

PDFModel.prototype.author = '';
PDFModel.prototype.title = '';

MessageTypeModel.DefineFileBehaviors({
  classDef: PDFModel,
  propertyName: 'source',
  sizeProperty: 'size',
  nameProperty: 'title',
  roleName: 'source',
});

// Static property specifies the preferred Message Type View for representing this Model
PDFModel.messageRenderer = 'vnd-customco-pdf-message-type-view';

// Static property defines the MIME Type that will be used when creating new Messages from this Model
PDFModel.MIMEType = 'application/vnd.customco.pdf+json';

PDFModel.defaultAction = 'vnd-open-pdf';

Core.Root.initClass.apply(PDFModel, [PDFModel, 'PDFModel']);
Client.registerMessageTypeModelClass(PDFModel, 'PDFModel');

export default PDFModel;

The PDF Message Type View

A Message Type View is a type of Webcomponent we will create using the Layer XDK Framework; it uses the MessageViewMixin Mixin to properly setup the View and its properties. Among other things, the Mixin defines the model property, and insures that any time the model is modified, the Model’s change events are wired up to call the View’s onRerender() method.

This viewer looks a lot like the Opinion Message Type View, but has a very different template property and onRerender() method.

Step 1: Quick Start

Lets get things setup using what we learned in the Opinion Message Type View, and setup a basic starting point that works with the Standard Message View Container

import Layer from '@layerhq/web-xdk';
const registerComponent = Layer.UI.registerComponent;
const MessageViewMixin = Layer.UI.mixins.MessageViewMixin;

registerComponent('vnd-customco-pdf-message-type-view', {
  mixins: [MessageViewMixin],

  // Every UI Component must define an initial display style
  style: `
    vnd-customco-pdf-message-type-view {
      display: block;
      width: 100%;
    }
  `,

  properties: {
    height: {
      value: 350
    },
    minWidth: {
      value: 400
    },
    messageViewContainerTagName: {
      noGetterFromSetter: true,
      value: 'layer-standard-message-view-container'
    },
  },
  methods: {
    onRender() {
    },
    onRerender() {
    },
  }
});
Step 2: Render the PDF

For rendering this Message we will need to add a template and an onRender method. We use onRender here instead of onRerender as we do not envision updates being made to the PDF document after its been sent.

registerComponent('vnd-customco-pdf-message-type-view', {
  template: `
    <object layer-id="pdf" type="application/pdf" width="100%" height="100%">
      <a layer-id="fallback">Download PDF</a>
    </object>
  `,

  methods: {
    onRender() {
      this.model.getSourceUrl((url) => {
        this.nodes.pdf.data = url;
        this.nodes.fallback.href = url;
      });
    },
    onRerender() {
    }
  }
});

Recall that getSourceUrl() was generated for your class by the DefineFileBehaviors() method, and fetches a URL for you, handling various circumstances including allowing that URL to come from model.source or model.sourceUrl.

PDF Message with Large Message Variant

Goals: Introduces providing a Large Message view of a Message

The PDF as shown in the prior sample is pretty small and hard to read. Ideally, we’d wait for the user to click on it, and popup a PDF Viewer.

The Message View shown above is the Standard Sized Message; we will add a Large Sized Message that shows the PDF message in more detail.

Updating the Model

The Model needs two changes:

  1. It needs to have an action of layer-show-large-message which tells the XDK to pop up the Large Message version of this Message
  2. It needs to have a largeMessageRenderer static property indicating which UI Component renders the Large Message view.
import { Core } from '@layerhq/web-xdk';
const { Client, MessagePart, Root, MessageTypeModel } = Core;

class PDFModel extends MessageTypeModel {
  // same as before
}

// Static property specifies the preferred Message Type View for representing this Model
PDFModel.messageRenderer = 'vnd-customco-pdf-message-type-view';
PDFModel.largeMessageRenderer = 'vnd-customco-pdf-message-type-large-view';

PDFModel.defaultAction = 'layer-show-large-message';

Updating the View

One minor change to make to the view is to cover it so that users can’t accidentally click on it and interact with the tiny PDF viewer (which exposes scrollbars, hyperlinks, buttons, etc…). Rather, we cover it with an node so that clicking on the PDF Message will trigger the Message Action of layer-show-large-message (otherwise, clicks go into an inner HTML document and do not propagate to this HTML document’s nodes).

registerComponent('vnd-customco-pdf-message-type-view', {
  mixins: [MessageViewMixin],
  template: `
    <div class='pdf-message-cover'></div>
    <object layer-id="pdf" type="application/pdf" width="100%" height="100%">
      <a layer-id="fallback">Download PDF</a>
    </object>
  `,
  // Every UI Component must define an initial display style
  style: `
    vnd-customco-pdf-message-type-view {
      display: block;
      width: 100%;
      position: relative;
    }
    /* Prevent clicks from interacting with the PDF document in the Standard Message View */
    vnd-customco-pdf-message-type-view .pdf-message-cover {
      position: absolute;
      width: 100%;
      height: 100%;
      opacity: 0.02;
      background-color: black;
    }
  `
});

Creating the Large View

The Message Model indicates that the Large Message Renderer is vnd-customco-pdf-message-type-large-view so we must define a UI Component registered with that name.

The following should be noted when defining the Large View:

  1. While you may use the messageViewContainerTagName property to wrap your UI Component, your UI Component will already be wrapped in a Dialog, and will automaticaly get a Title Bar, so typically this property is left out
  2. Because it automatically gets a Title Bar, it must provide a getTitle() method
  3. Because we want to maximize the size of our PDF viewer, the default sizing of the Dialog is overridden using max-width: inherit
import { Layer } from '../../get-layer'
const registerComponent = Layer.UI.registerComponent;
const MessageViewMixin = Layer.UI.mixins.MessageViewMixin;

registerComponent('vnd-customco-pdf-message-type-large-view', {
  mixins: [MessageViewMixin],
  template: `
    <object layer-id="pdf" type="application/pdf" width="100%" height="100%">
      <a layer-id="fallback">Download PDF</a>
    </object>
  `,
  // Every UI Component must define an initial display style
  style: `
    vnd-customco-pdf-message-type-large-view {
      display: block;
      width: 100%;
      height: 100%;
    }
    vnd-customco-pdf-message-type-large-view object {
      height: 100%;
    }
    /* Override the maximum width/height of the Dialog */
    layer-dialog.vnd-customco-pdf-message-type-large-view .layer-dialog-inner {
      max-width: inherit;
      max-height: inherit;
    }
  `,
  properties: {
  },
  methods: {
    // Provide text for the title bar
    getTitle() {
      return this.model.title;
    },

    // Identical to the Normal sized PDF View
    onRender() {
      this.model.getSourceUrl((url) => {
        this.nodes.pdf.data = url;
        this.nodes.fallback.href = url;
      });
    },
  }
});

Stateful PDF Message Example

Goals: Introduces tools for sharing the state of a message between participants

The Layer XDKs share state using the following process:

  1. A Message Type Model registers the states that it tracks; these are registered during its initialization
  2. The Model updates one or more states based on local events
  3. The Model adds a textual description for the state change
  4. A Response Message is sent that renders that textual description as a Status Message, and instructs the server on what states have changed
  5. The server updates the Message that the Response Message is describing changes for
  6. Each Client gets notified that they have a new/updated Response Summary Message Part
    • The Response Summary is a Message Part added to the Message containing the latest state for the message
  7. Each Client’s Message Type Model applies those change to its state and then triggers change events
  8. Each Client’s Message Type View can render those state changes on receiving those change events

For this example, we will have users sign the PDF document, and once signed, all participants will see it rendered as having been signed.

The PDF Message will require the following additional properties:

  1. signatureEnabledFor: Indicates the Identity ID of the user whose signature is required. Other than for testing purposes, we wouldn’t want a user to send a message and then sign it themselves.
  2. signature: The value representing the signature. For this example, it will simply be the name that the user types in.
A PDF Message
Large PDF View

Stateful PDF Message Type Model

A PDF Message could be created with:

var model = new PDFModel({
  source: pdfFile,
  title: "Death star plans",
  author: "Varth Dater",
  signatureEnabledFor: "layer:///identities/FrodoTheDodo"
});

The Message Model will need the following changes in order to work:

  1. Add the signatureEnabledFor and signature properties
  2. Add the signatureEnabledFor to the Message Part body so that all recipients know which user is allowed to sign.
    • note that signature is set only via manipulating state, and is not sent with the original Message
  3. Register signature as a state to be set and tracked
    • The registerAllStates method is a part of the lifecycle of every Message Type Model and is where all of your state registry code goes
  4. Provide a method for setting the signature state (i.e. signDocument())
  5. Provide a method for tracking the signature state
    • The parseModelResponses is part of the Message Type Model lifecycle that is called whenever any state changes, allowing each Message Type Model to incorporate those state changes into its local properties and trigger events for the Message Type View.
class PDFModel extends MessageTypeModel {
  generateParts(callback) {
    // NEW: When sending the message, insure that signatureEnabledFor is a part of the message
    const body = this.initBodyWithMetadata(['title', 'size', 'sourceUrl', 'author', 'signatureEnabledFor']);

    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    callback([this.part].concat(this.childParts));
  }

  // NEW: Register signature as a state to be tracked.
  // Signature is registered as a First Writer Wins state such that once its set,
  // it cannot be changed.
  registerAllStates() {
    this.responses.registerState('signature', Layer.Constants.CRDT_TYPES.FIRST_WRITER_WINS);
  }

  // NEW: Provide a method that the Message Type View can call when the user has
  // signed the document
  signDocument(name) {
    // Verify that the user who performed this action is the user permitted to sign
    if (this.signatureEnabledFor !== Layer.client.user.id) return;

    // As this is a First Writer Wins, there should never be a prior value;
    // however most other types of states can have a prior value that we are changing.
    const initialValue = this.signature;

    // Set the signature to be the name passed into this method
    this.responses.addState('signature', name);

    // Set some text to be shown to users when the Response Message carries the
    // signature to the server
    this.responses.setResponseMessageText(
      this.title + " Signed by " + Layer.client.user.displayName + " as " + name);

    // Force the Response Message to send now instead of waiting 100ms:
    this.responses.sendResponseMessage();

    // Update our local property
    this.signature = name;

    // Trigger a change event that the Message Type View can listen for and rerender on receiving
    this.trigger('message-type-model:change', {
      property: 'signature',
      newValue: name,
      oldValue: initialValue,
    });
  }

  // NEW: Parse the Response Summary whenever it changes to check for changes to the signature
  // This is called whenever any registered state is changed for this Message
  parseModelResponses() {
    // As this is a First Writer Wins, there should never be a prior value;
    // however most other types of states can have a prior value that we are changing.
    const oldSignature = this.signature;
    const signature = this.responses.getState('signature', this.signatureEnabledFor);

    // If the signature has changed, update our local property and trigger
    // a change event for the Message Type View to rerender
    if (signature !== oldSignature) {
      this.signature = signature;
      this.trigger('message-type-model:change', {
        property: 'terms',
        newValue: signature,
        oldValue: oldSignature,
      });
    }
  }
}

// NEW: Add the two new properties to the class definition
PDFModel.prototype.signature = '';
PDFModel.prototype.signatureEnabledFor = '';

Note that calling addState('signature', name) schedules a Response Message to be sent… but also waits for any additional state values to be added. Calling setResponseMessageText adds a human readable Status Message to that state change. We could then directly call this.responses.sendResponseMessage() or simply wait 100ms for it to send.

Stateful PDF Message (Standard Size)

The standard PDF Message view needs only one change: the ability to indicate that its been signed. Actual signing of the PDF is only done using the Large Message View.

This View will add a Checkmark icon to the Standard Message Container’s controls, and add CSS to render that checkmark only if the document is signed.

registerComponent('vnd-customco-pdf-message-type-view', {
  mixins: [MessageViewMixin],
  template: `
    <div class='pdf-message-cover'></div>
    <object layer-id="pdf" type="application/pdf" width="100%" height="100%">
      <a layer-id="fallback">Download PDF</a>
    </object>
  `,
  // Every UI Component must define an initial display style
  style: `
    ....
    // Add CSS for rendering the checkmark based on the "pdf-signed" css class
    layer-message-viewer.vnd-customco-pdf-message-type-view .pdf-checkmark {
      opacity: 0.1;
      margin-right: 20px;
    }
    layer-message-viewer.vnd-customco-pdf-message-type-view .pdf-checkmark.pdf-signed {
      opacity: 1.0;
    }
  `,
  methods: {
    // Create a checkmark node and pass it to the Standard Message Container that
    // wraps this View
    onAfterCreate() {
      this.nodes.checkmark = document.createElement('div');
      this.nodes.checkmark.innerHTML = '✅';
      this.nodes.checkmark.classList.add('pdf-checkmark');

      // Pass the dom node to the Standard Message Container's customControls property
      this.parentComponent.customControls = this.nodes.checkmark;
    },

    // Any time the signature changes, the Model's "message-type-model:change" event is triggered,
    // this calls the View's onRerender so we can rerender on every change
    onRerender() {
      this.nodes.checkmark.classList.toggle('pdf-signed', Boolean(this.model.signature));
    },
  }
});

Stateful PDF Message (Large Size)

The Large View will add a UI for signing the document, as well as hiding the signature UI if its already been signed.

registerComponent('vnd-customco-pdf-message-type-large-view', {
  mixins: [MessageViewMixin],
  template: `
    <object layer-id="pdf" type="application/pdf" width="100%" height="100%">
      <a layer-id="fallback">Download PDF</a>
    </object>
    <div class="pdf-review-panel">
      <div class="pdf-review-query">
        I have read this document and am signing this by typing in my name here:
        <input layer-id="name" type="text"/>
        <layer-action-button style="display: inline-block" layer-id="button" text="send">
      </layer-action-button></div>
      <div class="pdf-review-summary" layer-id="summary"></div>
    </div>
  `,
  style: `
    vnd-customco-pdf-message-type-large-view {
      display: flex;
      flex-direction: column;
      width: 100%;
      height: 100%;
    }
    vnd-customco-pdf-message-type-large-view object {
      flex-grow: 1;
    }
    layer-dialog.vnd-customco-pdf-message-type-large-view .layer-dialog-inner {
      max-width: inherit;
      max-height: inherit;
    }
    vnd-customco-pdf-message-type-large-view .pdf-review-panel {
      margin: 10px 20px;
      text-align: center;
    }
    vnd-customco-pdf-message-type-large-view.is-signed .pdf-review-query {
      display: none;
    }
    vnd-customco-pdf-message-type-large-view:not(.is-signed) .pdf-review-summary {
      display: none;
    }
    vnd-customco-pdf-message-type-large-view .pdf-review-summary {
      font-weight: bold;
    }
  `,
  methods: {
    // Wire up the signature button to our event handler
    onCreate() {
      this.nodes.button.addEventListener('click', this.handleSendEvent.bind(this));
    },

    // On first rendering, and any time there is a state change (the signature property change),
    // update our rendering based on whether its signed or not
    onRerender() {
      this.classList.toggle('is-signed', Boolean(this.model.signature));
      this.nodes.name.value = this.model.signature;
      if (this.model.signature) {
        this.nodes.summary.innerHTML = 'Signed by ' +
          Layer.client.getIdentity(this.model.signatureEnabledFor).displayName +
          ' as ' + this.model.signature;
      }
    },

    // When the Send button is clicked call the model's signDocument method
    handleSendEvent(evt) {
      if (this.nodes.name.value) {
        this.model.signDocument(this.nodes.name.value);

        // Tell the dialog that contains the Large Message View that we are done
        this.trigger('layer-container-done');
      }
    }
  }
});

Pie Chart Example

Goals: Introduces Model child nodes, using Message Viewer to render Model child nodes and using a Titled Message View Container (instead of the Standard Message View Container)

While the PDF Message Example above contains simple rendering and leaves the rest of its rendering to the Standard Message View Container, more sophisticated messages will need to do all of their own rendering (though for this example we leave rendering the Title Bar of the message to the Titled Message View Container). This example will illustrate:

The Pie Chart Message will have the following properties:

  • title: The title of the document

The Pie Chart Message will have a Sub Model that is a File Message that contains an application/csv file.

Note

The PDF Example above could also have used a File Message for its PDF file; instead it used a raw application/pdf MessagePart solely for purposes of illustrating how to use a raw Message Part without an accompanying Message Type Model.

A sample Pie Cart Message with a File Message message child node in the bottom right corner

Pie Message Type Model

This Message Type Model introduces one new concept: a Sub Model. Note that while the PDF Message Type had a Sub Message Part, it used a MessagePart for raw data. This Custom Message Model uses a MessagePart that will be represented by a FileMessageModel, and which can be rendered AS a File Message.

This model differs from the PDF Model:

  • Adding Submodels requires those models be handled as part of of generating a Message in generateParts
  • Adding Submodels requires those models to be imported from parseModelChildParts
  • Not using a Standard Message View Container means we do not need getTitle(), getDescription() and getFooter()
  • The model is responsible for loading and parsing its data (in this case CSV data)

Note that a Pie Model can be created as follows:

var model = new PieChartModel({
  title: "Eat Pie?",
  fileModel: new FileModel({
    source: CSVBlob
  })
});

Note

It is generally better to use the DefineFileBehaviors rather than to use a File Model to add file data to your Model. A File Model with a source property adds 2 Message Parts to your Message; using DefineFileBehaviors to add a source property to your own Model adds only 1 Message Part. This may seem trivial, but can have a significant impact when sending a Carousel of these messages.

The File Model is used here simply as an example of using any arbitrary sub-model.

import { Core } from '@layerhq/web-xdk';
const { Client, MessagePart, Root, MessageTypeModel } = Core;

class PieChartModel extends MessageTypeModel {
  generateParts(callback) {
    const body = this.initBodyWithMetadata(['title']);

    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    const parts = [this.part];
    this.addChildModel(this.fileModel, 'csv', (newParts) => {
      // Add all Message Parts used by the child model to the parts array
      newParts.forEach(p => parts.push(p));

      // Parse the CSV data from our input data
      this._parseCSV(this.fileModel);

      callback(parts);
    });
  }

  parseModelChildParts({ changes, isEdit }) {
    super.parseModelChildParts({ changes, isEdit });

    // Set the fileModel property to point to the csv File Model
    this.fileModel = this.getModelsByRole('csv')[0];

    this._parseCSV(this.fileModel);
  }

  // getSourceBody: fetches Rich Content and populates the MessagePart body if its unset.
  _parseCSV(fileModel) {
    fileModel.getSourceBody((body) => {
      const oldData = this.data;
      this.data = body.split(/\n/).map(line => line.split(/\s*,\s*/));

      // Replace string data that results from splitting a string with numerical data,
      // omitting headers
      this.data.forEach((row, index) => {
        if (index) this.data[index] = row.map((value, index) => index ? Number(value) : value);
      });
      this._triggerAsync('message-type-model:change', {
        property: 'data',
        newValue: this.data,
        oldValue: oldData
      });
    });
  }

  getOneLineSummary() {
    return 'Its a Pie Chart';
  }
}

PieChartModel.prototype.fileModel = null;
PieChartModel.prototype.title = '';
PieChartModel.prototype.data = null;

// Static property specifies the preferred Message Type View for representing this Model
PieChartModel.messageRenderer = 'vnd-customco-pie-chart-message-type-view';

// Static property defines the MIME Type that will be used when creating new Messages from this Model
PieChartModel.MIMEType = 'application/vnd.customco.pie+json';

Core.Root.initClass.apply(PieChartModel, [PieChartModel, 'PieChartModel']);
Client.registerMessageTypeModelClass(PieChartModel, 'PieChartModel');

MessagePart.TextualMimeTypes.push('text/csv');

export default PieChartModel;

Some utilties are used here:

  1. When the app calls either MessageTypeModelsend() or MessageTypeModelgenerateMessage() those methods will call MessageTypeModeladdChildModel(). MessageTypeModeladdChildModel() takes an input MessageTypeModel, calls generateParts() on it to gather its MessagePart objects, and adds them to the Message Part Tree. In this case MessageTypeModeladdChildModel() is told to assign MessageTypeModelrole to be csv while adding the CSV file to the Message Part Tree.
  2. MessageTypeModelparseModelChildParts() calls MessageTypeModelgetModelsByRole()) to find the Child Model whose MessageTypeModelrole is csv
  3. MessageTypeModelparseModelChildParts() and MessageTypeModelgenerateParts() setup the data property with its CSV data so that the Pie Message View can get the data and render it.
  4. MessagePart needs to be updated to understand that application/csv is a textual MIME Type and not a Blob. This is done by registering the MIME Type with the static MessagePartTextualMimeTypes property.

The Pie Chart Message Type View

This Message Type View is much like ones we’ve seen before, except that:

This viewer looks a lot like the Opinion Message Type View, but has a very different template property and onRerender() method.

Step 1: Quick Start

Lets get things setup using what we learned in the Opinion Message Type View, and setup a basic starting point that works with the Titled Message View Container:

import Layer from '@layerhq/web-xdk';
import './pie-chart-message-type-model';

const registerComponent = Layer.UI.registerComponent;
const MessageViewMixin = Layer.UI.mixins.MessageViewMixin;
const Widths = Layer.UI.Constants.WIDTH;

registerComponent('vnd-customco-pie-chart-message-type-view', {
  mixins: [MessageViewMixin],
  template: `
    <div class='vnd-customco-pie' layer-id='pie'></div>
    <layer-message-viewer layer-id='viewer'></layer-message-viewer>
  `,

  // Every UI Component must define an initial display style
  style: `vnd-customco-pie-chart-message-type-view {
    display: block;
  }
  vnd-customco-pie-chart-message-type-view .vnd-customco-pie {
    height: 100%;
  }
  vnd-customco-pie-chart-message-type-view > layer-message-viewer {
    position: absolute;
    bottom: 0px;
    right: 0px;
    width: 50px !important;
    min-width: 50px !important;
    height: 70px;
  }
  `,

  properties: {
    height: {
      value: 250,
    },
    minWidth: {
      value: 350
    },
    // Wrap this UI in a Titled Message View Container
    messageViewContainerTagName: {
      noGetterFromSetter: true,
      value: 'layer-titled-message-view-container'
    }
  },
  methods: {

    // Get the CSS Class for a title bar icon, used by <layer-titled-message-view-container />
    getIconClass() {
      return 'layer-poll-message-view-icon';
    },

    // Get the Title Text for a title bar, used by <layer-titled-message-view-container />
    getTitle() {
      return this.model.title || 'Pie Chart';
    },

    // Take care of any DOM setup that should be done before any properties are set
    // and setterse are called.
    onCreate() {
      // Disable wrapping this File Message in a Standard Message Container View
      // which would add a title and other text below the file.  Set this before any properties
      // are assigned and any rendering is done
      this.nodes.viewer.messageViewContainerTagName = '';

      // No borders around the message-viewer for the File Message
      this.nodes.viewer.cardBorderStyle = 'none';
    },

    // Basic setup to be done after properties are all available
    onAfterCreate() {
      // Pass the File Model to the Viewer for it to render
      this.nodes.viewer.model = this.model.fileModel;
    },

    // Note that the change event in the Model's _parseCSV method
    // will automatically cause onRerender to be called. This is also
    // called automatically after onRender completes.
    onRerender() {
      this._renderPieData();
    },

    // Saved for next step
    _renderPieData() {
    }
  }
});

Working with a Titled Message View Container requires this View to provide:

  • getIconClass() which returns a CSS class that you will need to configure to load a suitable icon for your title bar
  • getTitle() which will return text for the title bar. This text can come from the Model, or be hard coded text chosen by your UI Component.

Working with a Sub Message Viewer allows us to leave the rendering of any Sub Model to the Viewer, and requires us to:

  1. (Optional) Override the Message Viewer for the File Message’s default use of a Standard Message View Container by setting handlers.message.MessageViewermessageViewContainerTagName on the Message Viewer. This should be done in onCreate after the DOM structure is generated but before the model property is assigned, and subviewers generated.
  2. (Optional) Override the Message Viewer of the File Message’s default use of a surrounding border. A surrounding border for a Message Viewer is good when the Message Viewer is a Carousel Item, but not desirable when trying to present the Message Viewer content as a part of the parent Message. Again, this is done in onCreate before any properties are assigned or rendering is done.
  3. Provide the child model (File Model) to the MessageViewer; this is done in onAfterCreate as all properties should be assigned by thi spoint.

Everything this Message does with its subviewer (rendering a file icon, and allowing the user to click on that file to download the CSV file) could have been done by reimplementing features provided by the File Message. This custom message takes advantage of the fact that the capabilities are already implemented in a separate message.

Note

This example should benefit from the File Model’s support for clicking to download CSV content; however, sample CSV may well be small enough that it does not get stored on a remote server (See Rich Content/External Content) and therefore will not at this time actually download the CSV file.

Step 2: Rendering the Chart

One could put <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script> into your HTML file. Alternatively, you could simply insure it gets loaded by anyone who imports your Custom Message Component:

import Layer from '@layerhq/web-xdk';
import './pie-chart-message-type-model';

// Insure that the google charting library is loaded
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'https://www.gstatic.com/charts/loader.js';
document.head.appendChild(script);

registerComponent('vnd-customco-pie-chart-message-type-view', {
  ...
});

Complicating matters… we need to:

  1. Wait for google’s library to load
  2. Use google’s library to request it load its Visualization library
  3. Wait for the Visualization Library
  4. Render the chart
registerComponent('vnd-customco-pie-chart-message-type-view', {
  methods: {
    _renderPieData() {
      /* eslint-disable */

      // Make sure that the google visualization library has loaded
      if (typeof google === 'undefined') {
        google.charts.setOnLoadCallback(this._renderPieData.bind(this));
      }

      // If the visualization library hasn't yet loaded, make sure its loaded, and then wait until its ready
      else if (!google.visualization || !google.visualization.PieChart) {
        if (!this.properties.loadCalled) {
          google.charts.load('current', {'packages':['corechart']});
          this.properties.loadCalled = true;
        }
        setTimeout(this._renderPieData.bind(this), 500);
      }

      // If ready to draw, then instantiate google's chart and pass it the data
      else {
        this._drawPieData();
      }
    },

    // Adapted from https://google-developers.appspot.com/chart/interactive/docs/gallery/piechart
    _drawPieData() {
      if (!this.properties.chart) this.properties.chart = new google.visualization.PieChart(this.nodes.pie);
      const data = google.visualization.arrayToDataTable(this.model.data);
      this.properties.chart.draw(data, {});
    }
  }
});

Note

Google may have better best practices than this for using their charts.

Finally: if the charting library is already loaded, it may try rendering its data before this View has been inserted into the document; that means the view does not yet have dimensions. This results in less than optimal rendering. So we add one more method: onAttach().

registerComponent('vnd-customco-pie-chart-message-type-view', {
  methods: {
    // Note that the change event in the Model's _parseCSV method
    // will automatically cause onRerender to be called. This is also
    // called automatically after onRender completes.
    onRerender() {
      this._renderPieData();
    },

    // The UI does not know its size until this method is called;
    // use this opportunity to rerender the chart
    onAttach() {
      this._renderPieData();
    },
  }
});

Status Messages

Typical Messages are rendered as any other message: with a timestamp, avatar, sent/delivered/read indicators, etc…

What if you want to be able to send a Message from any user, and have all users see that message simply show as a status message without any Avatar, Sender or timestamp? You might start by looking at the default Status Message. But if you need something more than simple text as your Status Message, then you may define your own Message Type and register it to be presented as a Status Message (presented without Avatar, timestamp, etc…).

The following status message presents a large “Process Completed” stamp that users can click on for details.

Status Message Example: Process Completed Model

import { Core, UI } from '@layerhq/web-xdk';
const { Client, MessagePart, Root, MessageTypeModel } = Core;

class ProcessCompletedModel extends MessageTypeModel {

  generateParts(callback) {
    const body = this.initBodyWithMetadata(['processId']);

    this.part = new MessagePart({
      mimeType: this.constructor.MIMEType,
      body: JSON.stringify(body),
    });

    callback([this.part]);
  }

  getOneLineSummary() {
    return 'Process Completed';
  }
}

ProcessCompletedModel.prototype.processId = '';

// Static property defines the MIME Type that will be used when creating new Messages from this Model
// This MIME Type represents some sample mimetype produced by some hypothetical company
ProcessCompletedModel.MIMEType = 'application/vnd.customco.processcompleted+json';

// Static property specifies the preferred Message Type View for representing this Model
ProcessCompletedModel.messageRenderer = 'process-completed-view';

// Register the class, setting up all property setters/getters/event handling, etc...
Root.initClass.apply(ProcessCompletedModel, [ProcessCompletedModel, 'ProcessCompletedModel']);

// Register the Message Type Model Class with the Client; this allows this ProcessCompletedModel to be found and used by the Message Viewer
Client.registerMessageTypeModelClass(ProcessCompletedModel, 'ProcessCompletedModel');

// Register this Message Type to be handled as a Status Message
UI.registerStatusModel(ProcessCompletedModel);

export default ProcessCompletedModel;

Key to the above is the call to Layer.UI.registerStatusModel(ProcessCompletedModel) which tells the UI to treat Messages driven by the above Model differently from other Messages.

Building a View for the above model is left as an exercise; just note that your view will be shown centered in the Message List rather than on the right or left edge of the Message List.

Concepts Introduction