There’s nothing easier than sending plain/text Message Parts:

conversation.createMessage("Hi everyone").send();

This should be the easiest content to render. However, users have learned to expect a lot more from their text than just raw ASCII. This Guide gives examples and structures for doing more with text.

Text Plugins

A brief look at the table of contents for this guide should hint at the number and diversity of text processing you may want to do. In the life of your application, you may be asked to add many more types of processing. A simple plugin mechanism for registering text plugins is recommended. This guide will use the following plugin mechanism; we recommend you investigate a plugin mechanism that makes sense for your architecture and framework.

The code below uses Backbone, but tries to be as framework agnostic as possible:

var TextPlugins = [];
module.exports.TextMessagePartView = Backbone.View.extend({
  tagName: 'div',
  className: 'message-part-text-plain',
  render: function() {

    // Get the Message Part's text
    var part = this.model;
    var text = part.body;

    // Run all plugins that modify the part's text
    TextPlugins.forEach(function(textProcessor) {
      text = textProcessor(text);
    });

    // Render it
    this.$el.innerHTML = text;
  }
});

// Register new Text Plugins
module.exports.registerPlugin(textProcessor, optionalIndex) {
  // Sometimes you need to mess with the order of plugins;
  if (optionalIndex !== undefined) {
    TextPlugins.splice(optionalIndex, 0, textProcessor);
  } else {
    TextPlugins.push(textProcessor);
  }
}

This code will let you register new text processors that will take in the current state of the message text and update it for improved rendering.

HTML Safe Rendering

The first priority in any processing is to replace any unsafe characters with safe characters. Unless your application explicitly sends html tags, replacing < with &lt;:

  1. Necessary if you want users to actually be able to type < and > and have it render
  2. Necessary if you want to insure users can’t send <script> tags
  3. Do this before any other Text Plugins because your other plugins may introduce html tags such as <img> and <a>
var registerPlugin = require('text-message-part-view').registerPlugin;

registerPlugin(function(text) {
    text = text.replace(/\</g, '&lt;');
    text = text.replace(/\>/g, '&gt;');
    return text;
}, 0);

Registering this plugin will replace all < and > with &lt; and &gt; whenever the TextMessagePartView renders.

The registerPlugin function is called with an optionalIndex of 0, as this should always be the first processing to happen.

Multiline Content

Working with Newline Characters starts off simple as shown below, but can require additional parsing and processing as you introduce new Text Plugins.

var registerPlugin = require('text-message-part-view').registerPlugin;

registerPlugin(function(text) {
  return text.replace(/\n/g, '<br/>');
});

In the Code Blocks section below, we will see that just replacing any newline with a <br/> is not always effective.

Lengthy Text

Unlike other sections in this guide, working with text longer than 2KB uses the Rich Content APIs rather than text processing plugins.

A Large Text Message Part is any Message Part whose body size was larger than 2kb when it was sent. If it is greater than 2kb, then the body property will arrive empty, and will instead have the hasContent property set to true.

var TextPlugins = [];
module.exports.TextMessagePartView = Backbone.View.extend({
  tagName: 'div',
  className: 'text-message-part',
  render: function() {
    var part = this.model;
    var text = part.body;
    if (text === '' && part.hasContent) {
      part.fetchContent();
    } else {
      TextPlugins.forEach(function(textProcessor) {
        text = textProcessor(text);
      });
      this.$el.innerHTML = text;
    }
  }
});

The call to fetchContent on a MessagePart that is text/plain will trigger the following process:

  1. It will download the text from cloud storage
  2. It will set the MessagePart body property to the downloaded text
  3. It will trigger a messages:change event
  4. The event will cause the layer.Query to trigger a change event
  5. It is assumed that this application will rerender this view any time there is a change event.
  6. The second time this render method is called, text will have a value, and all text processor plugins can run and the innerHTML can be set.

Emojis

Your users will appreciate having emojis be rendered rather than left as :-) or :smile:. There are many ways to accomplish this; the example below uses two third party libraries:

import Twemoji from 'twemoji';
import RemarkableParser from 'remarkable-emoji/setEmoji';
var registerPlugin = require('text-message-part-view').registerPlugin;

registerPlugin(function(text) {
    text = text.replace(/<br\/>/g, ' <br/> ');
    text = Twemoji.parse(RemarkableParser(text), { size: '16x16' });
    text = text.replace(/ <br\/> /g, '<br/>');
    return text;
});

The Remarkable Emoji Parser will find all emoticons of the form :-) or :smile: and replace them with unicode characters for the emoji.

The Twemoji Parser will find all unicode characters for emoji, and replace them with <img> tags to load the corresponding emoticon.

The manipulation of the <br/> tags is an unfortunate workaround for a flaw in the Remarkable Emoji Parser that adds space around the <br/> tags and then strips them away after parsing is done.

Image URLs

Its usually nice (and these days, expected) that a URL to an image that is in your text message will render the image.

var registerPlugin = require('text-message-part-view').registerPlugin;
var isUrl = require('is-url');

registerPlugin(function(text) {
  return text.replace(isURL(['png', 'jpg', 'jpeg', 'gif']), function(imageUrl) {
    return `<a href="${imageUrl}" style="display: block" target="_new">${imageUrl}<br/>
      <img src="${imageUrl}" style="height: 350px"></img></a>`;
  });
});

If curious, you can see an isUrl example.

The above code defines a plugin for replacing any Image URLs with

  1. An <img> tag to render that image
  2. An <a> tag to open that image in a new window
  3. Some arbitrary height constraints to make rendering more predictable
  4. Rendering the actual URL that was posted above the image it points to

Linking URLs

Anyone who posts a URL in a text message can reasonably expect that URL to be clickable by the recipient, rather than plain unlinked text. Making that happen is up to you.

The Auto Linker module takes care of this:

var registerPlugin = require('text-message-part-view').registerPlugin;
var AutoLinker = require('autolinker');

registerPlugin(function(text) {
  return Autolinker.link(text);
});

YouTube Videos

Being able to render YouTube videos that are identified in a text message is also nice. The following plugin handles a few different formats for youtube video URLs, and suggestions for handling more are welcome.

var registerPlugin = require('text-message-part-view').registerPlugin;

registerPlugin(function(text) {
  return text.replace(/https:\/\/(www\.)?(youtu\.be|youtube\.com)\/(watch\?.*v=)?([a-zA-Z0-9\-]+)/g,
    function(match1, match2, match3, match4, videoId) {
      return '<iframe width="560" height="315" src="https://www.youtube.com/embed/' + videoId +
        '" frameborder="0" allowfullscreen></iframe>';
    });
});

Code Blocks

Typical applications won’t need users to be able to send code blocks. But there are still lessons to learn about challenges in text processing here. The following replaces inline code and code blocks with suitable html wrappers:

var registerPlugin = require('text-message-part-view').registerPlugin;

registerPlugin(function(text) {
   // note .* means single line; [\s\S]* means multiline
   text = text.replace(/```[\s\S]*?```/g, function(block) {
     return `<pre class='code_block'>${block.substring(3, block.length-3)}</pre>`
   });
   text = text.replace(/`.*?`/g, function(block) {
     return `<code>${block.substring(1, block.length-1)}</code>`
   });
   return text;
});

Note however, that this plugin does not work with the Multiline Content solution posted above; it will place <br/> tags within your code blocks which will render as “
”. One possible solution to this:

var registerPlugin = require('text-message-part-view').registerPlugin;

registerPlugin(function(text) {
   var codeBlockIndices = [];
   var codeBlocks = [];
   var lastIndex = 0;
   while (lastIndex !== -1) {
     lastIndex = body.indexOf('```', lastIndex);
     if (lastIndex !== -1) {
       codeBlockIndices.push(lastIndex);
       lastIndex += 3;
     }
   }

   for (let i = 1; i < codeBlockIndices.length; i++) {
     codeBlocks.push([codeBlockIndices[i - 1], codeBlockIndices[i]]);
   }

   function isInCodeBlock(index) {
     return Boolean(codeBlocks.filter(block => index > block[0] && index < block[1]).length);
   }

   return text.replace(/\n/g, function(text, index) {
     if (isInCodeBlock(index)) {
       return text;
     } else {
       return '<br/>';
     }
   });
}, 1);

Note that the optionalIndex is used in calling registerPlugin to insure that this is run BEFORE we replace tick characters with code blocks.

Changelog Rich Content Guide