diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 6345403f09..51e3eb8dc9 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -259,6 +259,11 @@ function textForPowerEvent(event) {
});
}
+function textForPinnedEvent(event) {
+ const senderName = event.getSender();
+ return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
+}
+
function textForWidgetEvent(event) {
const senderName = event.getSender();
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
@@ -304,6 +309,7 @@ const stateHandlers = {
'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent,
'm.room.power_levels': textForPowerEvent,
+ 'm.room.pinned_events': textForPinnedEvent,
'im.vector.modular.widgets': textForWidgetEvent,
};
diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js
index f9d0a9cda8..b274e6a594 100644
--- a/src/UserSettingsStore.js
+++ b/src/UserSettingsStore.js
@@ -30,6 +30,10 @@ const FEATURES = [
id: 'feature_groups',
name: _td("Groups"),
},
+ {
+ id: 'feature_pinning',
+ name: _td("Message Pinning"),
+ },
];
export default {
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index db40380636..83ca987276 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -1177,6 +1177,10 @@ module.exports = React.createClass({
return ret;
},
+ onPinnedClick: function() {
+ this.setState({showingPinned: !this.state.showingPinned, searching: false});
+ },
+
onSettingsClick: function() {
this.showSettings(true);
},
@@ -1296,7 +1300,7 @@ module.exports = React.createClass({
},
onSearchClick: function() {
- this.setState({ searching: true });
+ this.setState({ searching: true, showingPinned: false });
},
onCancelSearchClick: function() {
@@ -1495,6 +1499,7 @@ module.exports = React.createClass({
const RoomSettings = sdk.getComponent("rooms.RoomSettings");
const AuxPanel = sdk.getComponent("rooms.AuxPanel");
const SearchBar = sdk.getComponent("rooms.SearchBar");
+ const PinnedEventsPanel = sdk.getComponent("rooms.PinnedEventsPanel");
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
@@ -1639,6 +1644,9 @@ module.exports = React.createClass({
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = ;
+ } else if (this.state.showingPinned) {
+ hideCancel = true; // has own cancel
+ aux = ;
} else if (!myMember || myMember.membership !== "join") {
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
@@ -1812,6 +1820,7 @@ module.exports = React.createClass({
collapsedRhs={this.props.collapsedRhs}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
+ onPinnedClick={this.onPinnedClick}
onSaveClick={this.onSettingsSaveClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index f92dc0b97a..499d0ec09a 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -44,6 +44,7 @@ const eventTileTypes = {
'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent',
+ 'm.room.pinned_events' : 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent',
};
diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js
new file mode 100644
index 0000000000..e4b5b1ff96
--- /dev/null
+++ b/src/components/views/rooms/PinnedEventTile.js
@@ -0,0 +1,89 @@
+/*
+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.
+*/
+
+import React from "react";
+import MatrixClientPeg from "../../../MatrixClientPeg";
+import dis from "../../../dispatcher";
+import AccessibleButton from "../elements/AccessibleButton";
+import MessageEvent from "../messages/MessageEvent";
+import MemberAvatar from "../avatars/MemberAvatar";
+import { _t } from '../../../languageHandler';
+
+module.exports = React.createClass({
+ displayName: 'PinnedEventTile',
+ propTypes: {
+ mxRoom: React.PropTypes.object.isRequired,
+ mxEvent: React.PropTypes.object.isRequired,
+ onUnpinned: React.PropTypes.func,
+ },
+ onTileClicked: function() {
+ dis.dispatch({
+ action: 'view_room',
+ event_id: this.props.mxEvent.getId(),
+ highlighted: true,
+ room_id: this.props.mxEvent.getRoomId(),
+ });
+ },
+ onUnpinClicked: function() {
+ const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
+ if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
+ // Nothing to do: already unpinned
+ if (this.props.onUnpinned) this.props.onUnpinned();
+ } else {
+ const pinned = pinnedEvents.getContent().pinned;
+ const index = pinned.indexOf(this.props.mxEvent.getId());
+ if (index !== -1) {
+ pinned.splice(index, 1);
+ MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '').then(() => {
+ if (this.props.onUnpinned) this.props.onUnpinned();
+ });
+ } else if (this.props.onUnpinned) this.props.onUnpinned();
+ }
+ },
+ _canUnpin: function() {
+ return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
+ },
+ render: function() {
+ const sender = this.props.mxRoom.getMember(this.props.mxEvent.getSender());
+ const avatarSize = 40;
+
+ let unpinButton = null;
+ if (this._canUnpin()) {
+ unpinButton = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ { _t("Jump to message") }
+
+ { unpinButton }
+
+
+
+
+ {sender.name}
+
+
+
+ );
+ }
+});
diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js
new file mode 100644
index 0000000000..02df29fc72
--- /dev/null
+++ b/src/components/views/rooms/PinnedEventsPanel.js
@@ -0,0 +1,101 @@
+/*
+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.
+*/
+
+import React from "react";
+import MatrixClientPeg from "../../../MatrixClientPeg";
+import AccessibleButton from "../elements/AccessibleButton";
+import PinnedEventTile from "./PinnedEventTile";
+import { _t } from '../../../languageHandler';
+
+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() {
+ this._updatePinnedMessages();
+ },
+
+ _updatePinnedMessages: 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 => {
+ const event = timeline.getEvents().find(e => e.getId() === eventId);
+ return {eventId, timeline, event};
+ }).catch(err => {
+ console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
+ console.error(err);
+ return null; // return lack of context to avoid unhandled errors
+ }));
+ });
+
+ Promise.all(promises).then(contexts => {
+ // Filter out the messages before we try to render them
+ const pinned = contexts.filter(context => {
+ if (!context) return false; // no context == not applicable for the room
+ if (context.event.getType() !== "m.room.message") return false;
+ if (context.event.isRedacted()) return false;
+ return true;
+ });
+
+ this.setState({ loading: false, pinned });
+ });
+ }
+ },
+
+ _getPinnedTiles: function() {
+ if (this.state.pinned.length == 0) {
+ return ({ _t("No pinned messages.") }
);
+ }
+
+ return this.state.pinned.map(context => {
+ return ();
+ });
+ },
+
+ render: function() {
+ let tiles = { _t("Loading...") }
;
+ if (this.state && !this.state.loading) {
+ tiles = this._getPinnedTiles();
+ }
+
+ return (
+
+
+
+
{_t("Pinned Messages")}
+ { tiles }
+
+
+ );
+ }
+});
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 3d2dee9e64..4df0ff738c 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -31,6 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
+import UserSettingsStore from "../../../UserSettingsStore";
linkifyMatrix(linkify);
@@ -45,6 +46,7 @@ module.exports = React.createClass({
inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
+ onPinnedClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
onSearchClick: React.PropTypes.func,
onLeaveClick: React.PropTypes.func,
@@ -176,6 +178,7 @@ module.exports = React.createClass({
let spinner = null;
let saveButton = null;
let settingsButton = null;
+ let pinnedEventsButton = null;
let canSetRoomName;
let canSetRoomAvatar;
@@ -298,6 +301,13 @@ module.exports = React.createClass({
;
}
+ if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
+ pinnedEventsButton =
+
+
+ ;
+ }
+
// var leave_button;
// if (this.props.onLeaveClick) {
// leave_button =
@@ -342,6 +352,7 @@ module.exports = React.createClass({
rightRow =
{ settingsButton }
+ { pinnedEventsButton }
{ manageIntegsButton }
{ forgetButton }
{ searchButton }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 16743e5ad0..df236636a2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -619,6 +619,7 @@
"(~%(count)s results)|other": "(~%(count)s results)",
"Cancel": "Cancel",
"or": "or",
+ "Message Pinning": "Message Pinning",
"Active call": "Active call",
"Monday": "Monday",
"Tuesday": "Tuesday",
@@ -889,6 +890,8 @@
"Add rooms to the group summary": "Add rooms to the group summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
"Room name or alias": "Room name or alias",
+ "Pinned Messages": "Pinned Messages",
+ "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"You are an administrator of this group": "You are an administrator of this group",
"Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:",
"Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s",
diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json
index 6161728fb1..7326b54799 100644
--- a/src/i18n/strings/en_US.json
+++ b/src/i18n/strings/en_US.json
@@ -844,6 +844,7 @@
"+example:%(domain)s": "+example:%(domain)s",
"Group IDs must be of the form +localpart:%(domain)s": "Group IDs must be of the form +localpart:%(domain)s",
"Room creation failed": "Room creation failed",
+ "Pinned Messages": "Pinned Messages",
"You are a member of these groups:": "You are a member of these groups:",
"Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.": "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.",
"Join an existing group": "Join an existing group",
@@ -859,6 +860,7 @@
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Robot check is currently unavailable on desktop - please use a
web browser": "Robot check is currently unavailable on desktop - please use a
web browser",
"Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple",
+ "%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s": "It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s",
"To join an existing group you'll have to know its group identifier; this will look something like
+example:matrix.org.": "To join an existing group you'll have to know its group identifier; this will look something like
+example:matrix.org.",
"%(weekDayName)s, %(monthName)s %(day)s": "%(weekDayName)s, %(monthName)s %(day)s",