diff --git a/res/css/_components.scss b/res/css/_components.scss
index f627fe3a29..561b1b4820 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -88,12 +88,12 @@
 @import "./views/elements/_Dropdown.scss";
 @import "./views/elements/_EditableItemList.scss";
 @import "./views/elements/_ErrorBoundary.scss";
+@import "./views/elements/_EventListSummary";
 @import "./views/elements/_Field.scss";
 @import "./views/elements/_ImageView.scss";
 @import "./views/elements/_InlineSpinner.scss";
 @import "./views/elements/_InteractiveTooltip.scss";
 @import "./views/elements/_ManageIntegsButton.scss";
-@import "./views/elements/_MemberEventListSummary.scss";
 @import "./views/elements/_PowerSelector.scss";
 @import "./views/elements/_ProgressBar.scss";
 @import "./views/elements/_ReplyThread.scss";
diff --git a/res/css/views/elements/_MemberEventListSummary.scss b/res/css/views/elements/_EventListSummary.scss
similarity index 75%
rename from res/css/views/elements/_MemberEventListSummary.scss
rename to res/css/views/elements/_EventListSummary.scss
index 02ecb5d84a..99a5c06a5f 100644
--- a/res/css/views/elements/_MemberEventListSummary.scss
+++ b/res/css/views/elements/_EventListSummary.scss
@@ -14,28 +14,28 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-.mx_MemberEventListSummary {
+.mx_EventListSummary {
     position: relative;
 }
 
-.mx_TextualEvent.mx_MemberEventListSummary_summary {
+.mx_TextualEvent.mx_EventListSummary_summary {
     font-size: 14px;
     display: inline-flex;
 }
 
-.mx_MemberEventListSummary_avatars {
+.mx_EventListSummary_avatars {
     display: inline-block;
     margin-right: 8px;
     padding-top: 8px;
     line-height: 12px;
 }
 
-.mx_MemberEventListSummary_avatars .mx_BaseAvatar {
+.mx_EventListSummary_avatars .mx_BaseAvatar {
     margin-right: -4px;
     cursor: pointer;
 }
 
-.mx_MemberEventListSummary_toggle {
+.mx_EventListSummary_toggle {
     color: $accent-color;
     cursor: pointer;
     float: right;
@@ -43,29 +43,29 @@ limitations under the License.
     margin-top: 8px;
 }
 
-.mx_MemberEventListSummary_line {
+.mx_EventListSummary_line {
     border-bottom: 1px solid $primary-hairline-color;
     margin-left: 63px;
     line-height: 30px;
 }
 
 .mx_MatrixChat_useCompactLayout {
-    .mx_MemberEventListSummary {
+    .mx_EventListSummary {
         font-size: 13px;
         .mx_EventTile_line {
             line-height: 20px;
         }
     }
 
-    .mx_MemberEventListSummary_line {
+    .mx_EventListSummary_line {
         line-height: 22px;
     }
 
-    .mx_MemberEventListSummary_toggle {
+    .mx_EventListSummary_toggle {
         margin-top: 3px;
     }
 
-    .mx_TextualEvent.mx_MemberEventListSummary_summary {
+    .mx_TextualEvent.mx_EventListSummary_summary {
         font-size: 13px;
     }
 }
diff --git a/src/components/views/elements/EventListSummary.js b/src/components/views/elements/EventListSummary.js
new file mode 100644
index 0000000000..d6971334d4
--- /dev/null
+++ b/src/components/views/elements/EventListSummary.js
@@ -0,0 +1,95 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, {useEffect} from 'react';
+import PropTypes from 'prop-types';
+import MemberAvatar from '../avatars/MemberAvatar';
+import { _t } from '../../../languageHandler';
+import {MatrixEvent, RoomMember} from "matrix-js-sdk";
+import {useStateToggle} from "../../../hooks/useStateToggle";
+
+const EventListSummary = ({events, children, threshold=3, onToggle, startExpanded, summaryMembers=[], summaryText}) => {
+    const [expanded, toggleExpanded] = useStateToggle(startExpanded);
+
+    // Whenever expanded changes call onToggle
+    useEffect(() => {
+        if (onToggle) {
+            onToggle();
+        }
+    }, [expanded]);
+
+    const eventIds = events.map((e) => e.getId()).join(',');
+
+    // If we are only given few events then just pass them through
+    if (events.length < threshold) {
+        return (
+            <div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
+                { children }
+            </div>
+        );
+    }
+
+    if (expanded) {
+        return (
+            <div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
+                <div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
+                    { _t('collapse') }
+                </div>
+                <div className="mx_EventListSummary_line">&nbsp;</div>
+                { children }
+            </div>
+        );
+    }
+
+    const avatars = summaryMembers.map((m) => <MemberAvatar key={m.userId} member={m} width={14} height={14} />);
+    return (
+        <div className="mx_EventListSummary" data-scroll-tokens={eventIds}>
+            <div className={"mx_EventListSummary_toggle"} onClick={toggleExpanded}>
+                { _t('expand') }
+            </div>
+            <div className="mx_EventTile_line">
+                <div className="mx_EventTile_info">
+            <span className="mx_EventListSummary_avatars" onClick={toggleExpanded}>
+                { avatars }
+            </span>
+                    <span className="mx_TextualEvent mx_EventListSummary_summary">
+                { summaryText }
+            </span>
+                </div>
+            </div>
+        </div>
+    );
+};
+
+EventListSummary.propTypes = {
+    // An array of member events to summarise
+    events: PropTypes.arrayOf(PropTypes.instanceOf(MatrixEvent)).isRequired,
+    // An array of EventTiles to render when expanded
+    children: PropTypes.arrayOf(PropTypes.element).isRequired,
+    // The minimum number of events needed to trigger summarisation
+    threshold: PropTypes.number,
+    // Called when the event list expansion is toggled
+    onToggle: PropTypes.func,
+    // Whether or not to begin with state.expanded=true
+    startExpanded: PropTypes.bool,
+
+    // The list of room members for which to show avatars next to the summary
+    summaryMembers: PropTypes.arrayOf(PropTypes.instanceOf(RoomMember)),
+    // The text to show as the summary of this event list
+    summaryText: PropTypes.string.isRequired,
+};
+
+export default EventListSummary;
diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js
index ba31eb5a38..98adbb2e5c 100644
--- a/src/components/views/elements/MemberEventListSummary.js
+++ b/src/components/views/elements/MemberEventListSummary.js
@@ -19,9 +19,9 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import createReactClass from 'create-react-class';
-import MemberAvatar from '../avatars/MemberAvatar';
 import { _t } from '../../../languageHandler';
 import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
+import sdk from "../../../index";
 
 module.exports = createReactClass({
     displayName: 'MemberEventListSummary',
@@ -43,12 +43,6 @@ module.exports = createReactClass({
         startExpanded: PropTypes.bool,
     },
 
-    getInitialState: function() {
-        return {
-            expanded: Boolean(this.props.startExpanded),
-        };
-    },
-
     getDefaultProps: function() {
         return {
             summaryLength: 1,
@@ -57,37 +51,27 @@ module.exports = createReactClass({
         };
     },
 
-    shouldComponentUpdate: function(nextProps, nextState) {
+    shouldComponentUpdate: function(nextProps) {
         // Update if
         //  - The number of summarised events has changed
-        //  - or if the summary is currently expanded
         //  - or if the summary is about to toggle to become collapsed
         //  - or if there are fewEvents, meaning the child eventTiles are shown as-is
         return (
             nextProps.events.length !== this.props.events.length ||
-            this.state.expanded || nextState.expanded ||
             nextProps.events.length < this.props.threshold
         );
     },
 
-    _toggleSummary: function() {
-        this.setState({
-            expanded: !this.state.expanded,
-        });
-        this.props.onToggle();
-    },
-
     /**
-     * Render the JSX for users aggregated by their transition sequences (`eventAggregates`) where
+     * Generate the text for users aggregated by their transition sequences (`eventAggregates`) where
      * the sequences are ordered by `orderedTransitionSequences`.
      * @param {object[]} eventAggregates a map of transition sequence to array of user display names
      * or user IDs.
      * @param {string[]} orderedTransitionSequences an array which is some ordering of
      * `Object.keys(eventAggregates)`.
-     * @returns {ReactElement} a single <span> containing the textual summary of the aggregated
-     * events that occurred.
+     * @returns {string} the textual summary of the aggregated events that occurred.
      */
-    _renderSummary: function(eventAggregates, orderedTransitionSequences) {
+    _generateSummary: function(eventAggregates, orderedTransitionSequences) {
         const summaries = orderedTransitionSequences.map((transitions) => {
             const userNames = eventAggregates[transitions];
             const nameList = this._renderNameList(userNames);
@@ -118,11 +102,7 @@ module.exports = createReactClass({
             return null;
         }
 
-        return (
-            <span className="mx_TextualEvent mx_MemberEventListSummary_summary">
-                { summaries.join(", ") }
-            </span>
-        );
+        return summaries.join(", ");
     },
 
     /**
@@ -208,7 +188,7 @@ module.exports = createReactClass({
      * For a certain transition, t, describe what happened to the users that
      * underwent the transition.
      * @param {string} t the transition type.
-     * @param {integer} userCount number of usernames
+     * @param {number} userCount number of usernames
      * @param {number} repeats the number of times the transition was repeated in a row.
      * @returns {string} the written Human Readable equivalent of the transition.
      */
@@ -288,19 +268,6 @@ module.exports = createReactClass({
         return res;
     },
 
-    _renderAvatars: function(roomMembers) {
-        const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => {
-            return (
-                <MemberAvatar key={m.userId} member={m} width={14} height={14} />
-            );
-        });
-        return (
-            <span className="mx_MemberEventListSummary_avatars" onClick={this._toggleSummary}>
-                { avatars }
-            </span>
-        );
-    },
-
     _getTransitionSequence: function(events) {
         return events.map(this._getTransition);
     },
@@ -396,22 +363,6 @@ module.exports = createReactClass({
 
     render: function() {
         const eventsToRender = this.props.events;
-        const eventIds = eventsToRender.map((e) => e.getId()).join(',');
-        const fewEvents = eventsToRender.length < this.props.threshold;
-        const expanded = this.state.expanded || fewEvents;
-
-        let expandedEvents = null;
-        if (expanded) {
-            expandedEvents = this.props.children;
-        }
-
-        if (fewEvents) {
-            return (
-                <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
-                    { expandedEvents }
-                </div>
-            );
-        }
 
         // Map user IDs to an array of objects:
         const userEvents = {
@@ -455,30 +406,14 @@ module.exports = createReactClass({
             (seq1, seq2) => aggregate.indices[seq1] > aggregate.indices[seq2],
         );
 
-        let summaryContainer = null;
-        if (!expanded) {
-            summaryContainer = (
-                <div className="mx_EventTile_line">
-                    <div className="mx_EventTile_info">
-                        { this._renderAvatars(avatarMembers) }
-                        { this._renderSummary(aggregate.names, orderedTransitionSequences) }
-                    </div>
-                </div>
-            );
-        }
-        const toggleButton = (
-            <div className={"mx_MemberEventListSummary_toggle"} onClick={this._toggleSummary}>
-                { expanded ? _t('collapse') : _t('expand') }
-            </div>
-        );
-
-        return (
-            <div className="mx_MemberEventListSummary" data-scroll-tokens={eventIds}>
-                { toggleButton }
-                { summaryContainer }
-                { expanded ? <div className="mx_MemberEventListSummary_line">&nbsp;</div> : null }
-                { expandedEvents }
-            </div>
-        );
+        const EventListSummary = sdk.getComponent("views.elements.EventListSummary");
+        return <EventListSummary
+            events={this.props.events}
+            threshold={this.props.threshold}
+            onToggle={this.props.onToggle}
+            startExpanded={this.props.startExpanded}
+            children={this.props.children}
+            summaryMembers={avatarMembers}
+            summaryText={this._generateSummary(aggregate.names, orderedTransitionSequences)} />;
     },
 });
diff --git a/src/hooks/useStateToggle.js b/src/hooks/useStateToggle.js
new file mode 100644
index 0000000000..58cf123bfb
--- /dev/null
+++ b/src/hooks/useStateToggle.js
@@ -0,0 +1,27 @@
+/*
+Copyright 2019 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {useState} from 'react';
+
+// Hook to simplify toggling of a boolean state value
+// Returns value, method to toggle boolean value and method to set the boolean value
+export const useStateToggle = (initialValue) => {
+    const [value, setValue] = useState(Boolean(initialValue));
+    const toggleValue = () => {
+        setValue(!value);
+    };
+    return [value, toggleValue, setValue];
+};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 02d1d0e8d6..f7016d7fad 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1131,6 +1131,8 @@
     "Yes": "Yes",
     "No": "No",
     "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
+    "collapse": "collapse",
+    "expand": "expand",
     "Communities": "Communities",
     "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
     "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s",
@@ -1193,8 +1195,6 @@
     "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes",
     "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times",
     "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes",
-    "collapse": "collapse",
-    "expand": "expand",
     "Power level": "Power level",
     "Custom level": "Custom level",
     "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.",
diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js
index 09a4739f06..db86b2ffab 100644
--- a/test/components/views/elements/MemberEventListSummary-test.js
+++ b/test/components/views/elements/MemberEventListSummary-test.js
@@ -162,7 +162,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -198,7 +198,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -246,7 +246,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -299,7 +299,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -358,7 +358,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -396,7 +396,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -447,7 +447,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -521,7 +521,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -568,7 +568,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -604,7 +604,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -632,7 +632,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -659,7 +659,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;
 
@@ -684,7 +684,7 @@ describe('MemberEventListSummary', function() {
             <MemberEventListSummary {...props} />,
         );
         const summary = ReactTestUtils.findRenderedDOMComponentWithClass(
-            instance, "mx_MemberEventListSummary_summary",
+            instance, "mx_EventListSummary_summary",
         );
         const summaryText = summary.innerText;