diff --git a/ts/components/conversation/AddNewLines.md b/ts/components/conversation/AddNewLines.md
new file mode 100644
index 00000000000..14d2b35ec64
--- /dev/null
+++ b/ts/components/conversation/AddNewLines.md
@@ -0,0 +1,35 @@
+### All newlines
+
+```jsx
+<AddNewLines text="\n\n\n" />
+```
+
+### Starting and ending with newlines
+
+```jsx
+<AddNewLines text="\nin between\n" />
+```
+
+### With newlines in the middle
+
+```jsx
+<AddNewLines text="Before \n\n after" />
+```
+
+### No newlines
+
+```jsx
+<AddNewLines text="This is the text" />
+```
+
+### Providing custom non-newline render function
+
+```jsx
+const renderNonNewLine = ({ text, key }) => (
+  <span key={key}>This is my custom content!</span>
+);
+<AddNewLines
+  text="\n first \n second \n"
+  renderNonNewLine={renderNonNewLine}
+/>;
+```
diff --git a/ts/components/conversation/AddNewLines.tsx b/ts/components/conversation/AddNewLines.tsx
index 7e9647b9b88..b4bdfe13528 100644
--- a/ts/components/conversation/AddNewLines.tsx
+++ b/ts/components/conversation/AddNewLines.tsx
@@ -1,27 +1,43 @@
 import React from 'react';
 
+import { RenderTextCallback } from '../../types/Util';
+
 interface Props {
   text: string;
+  /** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
+  renderNonNewLine?: RenderTextCallback;
 }
 
 export class AddNewLines extends React.Component<Props, {}> {
+  public static defaultProps: Partial<Props> = {
+    renderNonNewLine: ({ text, key }) => <span key={key}>{text}</span>,
+  };
+
   public render() {
-    const { text } = this.props;
+    const { text, renderNonNewLine } = this.props;
     const results: Array<any> = [];
     const FIND_NEWLINES = /\n/g;
 
+    // We have to do this, because renderNonNewLine is not required in our Props object,
+    //  but it is always provided via defaultProps.
+    if (!renderNonNewLine) {
+      return;
+    }
+
     let match = FIND_NEWLINES.exec(text);
     let last = 0;
     let count = 1;
 
     if (!match) {
-      return <span>{text}</span>;
+      return renderNonNewLine({ text, key: 0 });
     }
 
     while (match) {
       if (last < match.index) {
         const textWithNoNewline = text.slice(last, match.index);
-        results.push(<span key={count++}>{textWithNoNewline}</span>);
+        results.push(
+          renderNonNewLine({ text: textWithNoNewline, key: count++ })
+        );
       }
 
       results.push(<br key={count++} />);
@@ -32,9 +48,9 @@ export class AddNewLines extends React.Component<Props, {}> {
     }
 
     if (last < text.length) {
-      results.push(<span key={count++}>{text.slice(last)}</span>);
+      results.push(renderNonNewLine({ text: text.slice(last), key: count++ }));
     }
 
-    return <span>{results}</span>;
+    return results;
   }
 }
diff --git a/ts/components/conversation/ContactDetail.tsx b/ts/components/conversation/ContactDetail.tsx
index 4437f6559d0..1ea63b65af9 100644
--- a/ts/components/conversation/ContactDetail.tsx
+++ b/ts/components/conversation/ContactDetail.tsx
@@ -17,7 +17,7 @@ import {
   renderSendMessage,
 } from './EmbeddedContact';
 
-type Localizer = (key: string, values?: Array<string>) => string;
+import { Localizer } from '../../types/Util';
 
 interface Props {
   contact: Contact;
diff --git a/ts/components/conversation/Emojify.md b/ts/components/conversation/Emojify.md
new file mode 100644
index 00000000000..17e2c9e1a6c
--- /dev/null
+++ b/ts/components/conversation/Emojify.md
@@ -0,0 +1,60 @@
+### All emoji
+
+```jsx
+<Emojify text="🔥🔥🔥" />
+```
+
+### With skin color modifier
+
+```jsx
+<Emojify text="👍🏾" />
+```
+
+### With `sizeClass` provided
+
+```jsx
+<Emojify text="🔥" sizeClass="jumbo" />
+```
+
+```jsx
+<Emojify text="🔥" sizeClass="large" />
+```
+
+```jsx
+<Emojify text="🔥" sizeClass="medium" />
+```
+
+```jsx
+<Emojify text="🔥" sizeClass="small" />
+```
+
+```jsx
+<Emojify text="🔥" sizeClass="" />
+```
+
+### Starting and ending with emoji
+
+```jsx
+<Emojify text="🔥in between🔥" />
+```
+
+### With emoji in the middle
+
+```jsx
+<Emojify text="Before 🔥🔥 after" />
+```
+
+### No emoji
+
+```jsx
+<Emojify text="This is the text" />
+```
+
+### Providing custom non-link render function
+
+```jsx
+const renderNonEmoji = ({ text, key }) => (
+  <span key={key}>This is my custom content</span>
+);
+<Emojify text="Before 🔥🔥 after" renderNonEmoji={renderNonEmoji} />;
+```
diff --git a/ts/components/conversation/Emojify.tsx b/ts/components/conversation/Emojify.tsx
index 6a323d95999..591b30522fa 100644
--- a/ts/components/conversation/Emojify.tsx
+++ b/ts/components/conversation/Emojify.tsx
@@ -9,7 +9,8 @@ import {
   getReplacementData,
   getTitle,
 } from '../../util/emoji';
-import { AddNewLines } from './AddNewLines';
+
+import { RenderTextCallback } from '../../types/Util';
 
 // Some of this logic taken from emoji-js/replacement
 function getImageTag({
@@ -43,27 +44,40 @@ function getImageTag({
 
 interface Props {
   text: string;
-  sizeClass?: string;
+  /** A class name to be added to the generated emoji images */
+  sizeClass?: '' | 'small' | 'medium' | 'large' | 'jumbo';
+  /** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
+  renderNonEmoji?: RenderTextCallback;
 }
 
 export class Emojify extends React.Component<Props, {}> {
+  public static defaultProps: Partial<Props> = {
+    renderNonEmoji: ({ text, key }) => <span key={key}>{text}</span>,
+  };
+
   public render() {
-    const { text, sizeClass } = this.props;
+    const { text, sizeClass, renderNonEmoji } = this.props;
     const results: Array<any> = [];
     const regex = getRegex();
 
+    // We have to do this, because renderNonEmoji is not required in our Props object,
+    //  but it is always provided via defaultProps.
+    if (!renderNonEmoji) {
+      return;
+    }
+
     let match = regex.exec(text);
     let last = 0;
     let count = 1;
 
     if (!match) {
-      return <AddNewLines text={text} />;
+      return renderNonEmoji({ text, key: 0 });
     }
 
     while (match) {
       if (last < match.index) {
         const textWithNoEmoji = text.slice(last, match.index);
-        results.push(<AddNewLines key={count++} text={textWithNoEmoji} />);
+        results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ }));
       }
 
       results.push(getImageTag({ match, sizeClass, key: count++ }));
@@ -73,9 +87,9 @@ export class Emojify extends React.Component<Props, {}> {
     }
 
     if (last < text.length) {
-      results.push(<AddNewLines key={count++} text={text.slice(last)} />);
+      results.push(renderNonEmoji({ text: text.slice(last), key: count++ }));
     }
 
-    return <span>{results}</span>;
+    return results;
   }
 }
diff --git a/ts/components/conversation/Linkify.md b/ts/components/conversation/Linkify.md
new file mode 100644
index 00000000000..aa53d40c2da
--- /dev/null
+++ b/ts/components/conversation/Linkify.md
@@ -0,0 +1,44 @@
+### All link
+
+```jsx
+<Linkify text="https://somewhere.com" />
+```
+
+### Starting and ending with link
+
+```jsx
+<Linkify text="https://somewhere.com Yes? No? https://anotherlink.com" />
+```
+
+### With a link in the middle
+
+```jsx
+<Linkify text="Before. https://somewhere.com After." />
+```
+
+### No link
+
+```jsx
+<Linkify text="Plain text" />
+```
+
+### Should not render as link
+
+```jsx
+<Linkify text="smailto:someone@somewhere.com - ftp://something.com - //local/share - \\local\share" />
+```
+
+### Should render as link
+
+```jsx
+<Linkify text="github.com - https://blah.com" />
+```
+
+### Providing custom non-link render function
+
+```jsx
+const renderNonLink = ({ text, key }) => (
+  <span key={key}>This is my custom non-link content!</span>
+);
+<Linkify text="Before github.com After" renderNonLink={renderNonLink} />;
+```
diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx
new file mode 100644
index 00000000000..bc5217e02b5
--- /dev/null
+++ b/ts/components/conversation/Linkify.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+
+import createLinkify from 'linkify-it';
+
+import { RenderTextCallback } from '../../types/Util';
+
+const linkify = createLinkify();
+
+interface Props {
+  text: string;
+  /** Allows you to customize now non-links are rendered. Simplest is just a <span>. */
+  renderNonLink?: RenderTextCallback;
+}
+
+const SUPPORTED_PROTOCOLS = /^(http|https):/i;
+
+export class Linkify extends React.Component<Props, {}> {
+  public static defaultProps: Partial<Props> = {
+    renderNonLink: ({ text, key }) => <span key={key}>{text}</span>,
+  };
+
+  public render() {
+    const { text, renderNonLink } = this.props;
+    const matchData = linkify.match(text) || [];
+    const results: Array<any> = [];
+    let last = 0;
+    let count = 1;
+
+    // We have to do this, because renderNonLink is not required in our Props object,
+    //  but it is always provided via defaultProps.
+    if (!renderNonLink) {
+      return;
+    }
+
+    if (matchData.length === 0) {
+      return renderNonLink({ text, key: 0 });
+    }
+
+    matchData.forEach(
+      (match: {
+        index: number;
+        url: string;
+        lastIndex: number;
+        text: string;
+      }) => {
+        if (last < match.index) {
+          const textWithNoLink = text.slice(last, match.index);
+          results.push(renderNonLink({ text: textWithNoLink, key: count++ }));
+        }
+
+        const { url, text: originalText } = match;
+        if (SUPPORTED_PROTOCOLS.test(url)) {
+          results.push(
+            <a key={count++} href={url}>
+              {originalText}
+            </a>
+          );
+        } else {
+          results.push(renderNonLink({ text: originalText, key: count++ }));
+        }
+
+        last = match.lastIndex;
+      }
+    );
+
+    if (last < text.length) {
+      results.push(renderNonLink({ text: text.slice(last), key: count++ }));
+    }
+
+    return results;
+  }
+}
diff --git a/ts/components/conversation/MessageBody.md b/ts/components/conversation/MessageBody.md
index be171b7ce29..8c1f3634904 100644
--- a/ts/components/conversation/MessageBody.md
+++ b/ts/components/conversation/MessageBody.md
@@ -1,11 +1,7 @@
-### Plain text
+### All components: emoji, links, newline
 
 ```jsx
-<MessageBody text="Plain text message" />
-```
-
-```jsx
-<MessageBody text="Plain text message\n\nWith a new line." />
+<MessageBody text="Fire 🔥 http://somewhere.com\nSecond Line" />
 ```
 
 ### Jumbo emoji
@@ -31,33 +27,17 @@
 ```
 
 ```jsx
-<MessageBody text="With skin color modifier: 👍🏾" />
+<MessageBody text="🔥 text disables jumbomoji" />
 ```
 
-### Text and emoji
+### Jumbomoji disabled
 
 ```jsx
-<MessageBody text="Plain text 🔥message. With 🔥emoji🔥 sprinkled 🔥about" />
+<MessageBody text="🔥" disableJumbomoji />
 ```
 
+### Links disabled
+
 ```jsx
-<MessageBody text="🔥Message starting and ending with emoji🔥" />
-```
-
-### Links
-
-```jsx
-<MessageBody text="This before and after link. Before. https://somewhere.com After." />
-```
-
-```jsx
-<MessageBody text="Link https://somewhere.com\nWhat do you think? How about this one? \n\nhttps://anotherlink.com" />
-```
-
-```jsx
-<MessageBody text="Link https://somewhere.com\nWhat do you think? How about this one? \n\nhttps://anotherlink.com" />
-```
-
-```jsx
-<MessageBody text="should not render as link:\nmailto:someone@somewhere.com\nftp://something.com\n//local/share\n\\local\share\n\nshould render as link:\ngithub.com\nhttps://blah.com" />
+<MessageBody text="http://somewhere.com" disableLinks />
 ```
diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx
index 703d902c9ee..058c79368f8 100644
--- a/ts/components/conversation/MessageBody.tsx
+++ b/ts/components/conversation/MessageBody.tsx
@@ -1,67 +1,46 @@
 import React from 'react';
 
-import createLinkify from 'linkify-it';
-
 import { getSizeClass } from '../../util/emoji';
 import { Emojify } from './Emojify';
+import { AddNewLines } from './AddNewLines';
+import { Linkify } from './Linkify';
 
-const linkify = createLinkify();
+import { RenderTextCallback } from '../../types/Util';
 
 interface Props {
   text: string;
+  /** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */
   disableJumbomoji?: boolean;
+  /** If set, links will be left alone instead of turned into clickable `<a>` tags. */
   disableLinks?: boolean;
 }
 
-const SUPPORTED_PROTOCOLS = /^(http|https):/i;
+const renderNewLines: RenderTextCallback = ({
+  text: textWithNewLines,
+  key,
+}) => <AddNewLines key={key} text={textWithNewLines} />;
 
+const renderLinks: RenderTextCallback = ({ text: textWithLinks, key }) => (
+  <Linkify key={key} text={textWithLinks} renderNonLink={renderNewLines} />
+);
+
+/**
+ * This component makes it very easy to use all three of our message formatting
+ * components: `Emojify`, `Linkify`, and `AddNewLines`. Because each of them is fully
+ * configurable with their `renderXXX` props, this component will assemble all three of
+ * them for you.
+ */
 export class MessageBody extends React.Component<Props, {}> {
   public render() {
     const { text, disableJumbomoji, disableLinks } = this.props;
-    const matchData = linkify.match(text) || [];
-    const results: Array<any> = [];
-    let last = 0;
-    let count = 1;
-
-    // We only use this sizeClass if there was no link detected, because jumbo emoji
-    //   only fire when there's no other text in the message.
     const sizeClass = disableJumbomoji ? '' : getSizeClass(text);
 
-    if (disableLinks || matchData.length === 0) {
-      return <Emojify text={text} sizeClass={sizeClass} />;
-    }
-
-    matchData.forEach(
-      (match: {
-        index: number;
-        url: string;
-        lastIndex: number;
-        text: string;
-      }) => {
-        if (last < match.index) {
-          const textWithNoLink = text.slice(last, match.index);
-          results.push(<Emojify key={count++} text={textWithNoLink} />);
-        }
-
-        const { url, text: originalText } = match;
-        if (SUPPORTED_PROTOCOLS.test(url)) {
-          results.push(
-            <a key={count++} href={url}>
-              {originalText}
-            </a>
-          );
-        } else {
-          results.push(<Emojify key={count++} text={originalText} />);
-        }
-
-        last = match.lastIndex;
-      }
+    return (
+      <Emojify
+        text={text}
+        sizeClass={sizeClass}
+        renderNonEmoji={disableLinks ? renderNewLines : renderLinks}
+      />
     );
-
-    if (last < text.length) {
-      results.push(<Emojify key={count++} text={text.slice(last)} />);
-    }
-
-    return <span>{results}</span>;
   }
 }
diff --git a/ts/styleguide/StyleGuideUtil.ts b/ts/styleguide/StyleGuideUtil.ts
index fdc9d628a08..d7ec2528549 100644
--- a/ts/styleguide/StyleGuideUtil.ts
+++ b/ts/styleguide/StyleGuideUtil.ts
@@ -218,11 +218,3 @@ parent.storage.put('regionCode', 'US');
 // Telling Lodash to relinquish _ for use by underscore
 // @ts-ignore
 _.noConflict();
-
-parent.emoji.signalReplace = (html: string): string => {
-  return html.replace(
-    /🔥/g,
-    '<img src="node_modules/emoji-datasource-apple/img/apple/64/1f525.png"' +
-      'class="emoji" data-codepoints="1f525" title=":fire:">'
-  );
-};
diff --git a/ts/types/Util.ts b/ts/types/Util.ts
new file mode 100644
index 00000000000..9819b5a8547
--- /dev/null
+++ b/ts/types/Util.ts
@@ -0,0 +1,8 @@
+export type RenderTextCallback = (
+  options: {
+    text: string;
+    key: number;
+  }
+) => JSX.Element;
+
+export type Localizer = (key: string, values?: Array<string>) => string;