diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js
new file mode 100644
index 0000000000..4e5efdd35e
--- /dev/null
+++ b/src/components/views/rooms/PinnedEventsPanel.js
@@ -0,0 +1,106 @@
+/*
+Copyright 2017 Travis Ralston
+
+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.
+*/
+
+'use strict';
+
+var React = require('react');
+var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
+var sdk = require('matrix-react-sdk');
+var AccessibleButton = require('matrix-react-sdk/lib/components/views/elements/AccessibleButton');
+import { _t } from "matrix-react-sdk/lib/languageHandler";
+import { EventTimeline } from "matrix-js-sdk";
+
+module.exports = React.createClass({
+ displayName: 'PinnedEventsPanel',
+ propTypes: {
+ // The Room from the js-sdk we're going to show pinned events for
+ room: React.PropTypes.object.isRequired,
+
+ onCancelClick: React.PropTypes.func,
+ },
+
+ getInitialState: function() {
+ return {
+ loading: true,
+ };
+ },
+
+ componentDidMount: function() {
+ const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
+ if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
+ this.setState({ loading: false, pinned: [] });
+ } else {
+ const promises = [];
+ const cli = MatrixClientPeg.get();
+
+ pinnedEvents.getContent().pinned.map(eventId => {
+ promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(timeline => {
+ return {eventId, timeline};
+ }));
+ });
+
+ Promise.all(promises).then(contexts => {
+ this.setState({ loading: false, pinned: contexts });
+ });
+ }
+ },
+
+ _getPinnedTiles: function() {
+ const MessageEvent = sdk.getComponent("views.messages.MessageEvent");
+ const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
+
+ if (this.state.pinned.length == 0) {
+ return
No pinned messages.
;
+ }
+
+ return this.state.pinned.map(pinnedEvent => {
+ const event = pinnedEvent.timeline.getEvents().find(e => e.getId() === pinnedEvent.eventId);
+ const sender = this.props.room.getMember(event.getSender());
+ const avatarSize = 40;
+
+ // Don't show non-messages. Technically users can pin state/custom events, but we won't
+ // support those events.
+ if (event.getType() !== "m.room.message") return '';
+
+ return (
+
+
+
+ {sender.name}
+
+
+
+ );
+ });
+ },
+
+ render: function() {
+ let tiles = Loading...
;
+ if (this.state && !this.state.loading) {
+ tiles = this._getPinnedTiles();
+ }
+
+ return (
+
+
+
+
{_t("Pinned Messages")}
+ { tiles }
+
+
+ );
+ }
+});
diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss
index 61bfa104aa..1d351c82d7 100644
--- a/src/skins/vector/css/_components.scss
+++ b/src/skins/vector/css/_components.scss
@@ -88,4 +88,5 @@
@import "./vector-web/views/rooms/_RoomDropTarget.scss";
@import "./vector-web/views/rooms/_RoomTooltip.scss";
@import "./vector-web/views/rooms/_SearchBar.scss";
+@import "./vector-web/views/rooms/_PinnedEventsPanel.scss";
@import "./vector-web/views/settings/_Notifications.scss";
diff --git a/src/skins/vector/css/vector-web/views/rooms/_PinnedEventsPanel.scss b/src/skins/vector/css/vector-web/views/rooms/_PinnedEventsPanel.scss
new file mode 100644
index 0000000000..883eaa26e2
--- /dev/null
+++ b/src/skins/vector/css/vector-web/views/rooms/_PinnedEventsPanel.scss
@@ -0,0 +1,67 @@
+/*
+Copyright 2017 Travis Ralston
+
+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.
+*/
+
+.mx_PinnedEventsPanel {
+ border-top: 1px solid $primary-hairline-color;
+}
+
+.mx_PinnedEventsPanel_body {
+ max-height: 300px;
+ overflow-y: scroll;
+}
+
+.mx_PinnedEventsPanel_header {
+ margin: 0;
+ padding-top: 8px;
+ padding-bottom: 15px;
+}
+
+.mx_PinnedEventsPanel_pinnedEvent {
+ min-height: 40px;
+ margin-bottom: 5px;
+ cursor: pointer;
+ width: 100%;
+ border-radius: 5px; // for the hover
+}
+
+.mx_PinnedEventsPanel_pinnedEvent:hover {
+ background-color: $event-selected-color;
+}
+
+.mx_PinnedEventsPanel_pinnedEvent .mx_PinnedEventsPanel_sender {
+ color: #868686;
+ font-size: 0.8em;
+ vertical-align: top;
+ display: block;
+}
+
+.mx_PinnedEventsPanel_pinnedEvent .mx_EventTile_content {
+ margin-left: 50px;
+ position: relative;
+ top: 0;
+ left: 0;
+}
+
+.mx_PinnedEventsPanel_pinnedEvent .mx_BaseAvatar {
+ float: left;
+ margin-right: 10px;
+}
+
+.mx_PinnedEventsPanel_cancel {
+ margin: 12px;
+ float: right;
+ display: inline-block;
+}
diff --git a/src/skins/vector/img/icons-pin.svg b/src/skins/vector/img/icons-pin.svg
new file mode 100644
index 0000000000..a6fbf13baa
--- /dev/null
+++ b/src/skins/vector/img/icons-pin.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file