diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js
index 5b1b8a4590..1e6de43ab5 100644
--- a/src/components/views/rooms/PinnedEventsPanel.js
+++ b/src/components/views/rooms/PinnedEventsPanel.js
@@ -19,6 +19,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
+import PinningUtils from "../../../utils/PinningUtils";
module.exports = React.createClass({
displayName: 'PinnedEventsPanel',
@@ -61,20 +62,39 @@ module.exports = React.createClass({
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;
- });
+ const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
this.setState({ loading: false, pinned });
});
}
+
+ this._updateReadState();
+ },
+
+ _updateReadState: function() {
+ const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
+ if (!pinnedEvents) return; // nothing to read
+
+ let readStateEvents = null;
+ const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
+ if (readPinsEvent && readPinsEvent.getContent()) {
+ readStateEvents = readPinsEvent.getContent().event_ids || [];
+ }
+
+ if (!readStateEvents.includes(pinnedEvents.getId())) {
+ readStateEvents.push(pinnedEvents.getId());
+
+ // Only keep the last 10 event IDs to avoid infinite growth
+ readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
+
+ MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
+ event_ids: readStateEvents,
+ });
+ }
},
_getPinnedTiles: function() {
- if (this.state.pinned.length == 0) {
+ if (this.state.pinned.length === 0) {
return (
{ _t("No pinned messages.") }
);
}
diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js
index 4dfbdb3644..f558f44b4e 100644
--- a/src/components/views/rooms/RoomHeader.js
+++ b/src/components/views/rooms/RoomHeader.js
@@ -65,6 +65,7 @@ module.exports = React.createClass({
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
+ cli.on("Room.accountData", this._onRoomAccountData);
// When a room name occurs, RoomState.events is fired *before*
// room.name is updated. So we have to listen to Room.name as well as
@@ -87,6 +88,7 @@ module.exports = React.createClass({
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
+ cli.removeListener("Room.accountData", this._onRoomAccountData);
}
},
@@ -99,6 +101,13 @@ module.exports = React.createClass({
this._rateLimitedUpdate();
},
+ _onRoomAccountData: function(event, room) {
+ if (!this.props.room || room.roomId !== this.props.room.roomId) return;
+ if (event.getType() !== "im.vector.room.read_pins") return;
+
+ this._rateLimitedUpdate();
+ },
+
_rateLimitedUpdate: new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
@@ -139,6 +148,32 @@ module.exports = React.createClass({
dis.dispatch({ action: 'show_right_panel' });
},
+ _hasUnreadPins: function() {
+ const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
+ if (!currentPinEvent) return false;
+ if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
+ return false; // no pins == nothing to read
+ }
+
+ const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
+ if (readPinsEvent && readPinsEvent.getContent()) {
+ const readStateEvents = readPinsEvent.getContent().event_ids || [];
+ if (readStateEvents) {
+ return !readStateEvents.includes(currentPinEvent.getId());
+ }
+ }
+
+ // There's pins, and we haven't read any of them
+ return true;
+ },
+
+ _hasPins: function() {
+ const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
+ if (!currentPinEvent) return false;
+
+ return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
+ },
+
/**
* After editing the settings, get the new name for the room
*
@@ -305,8 +340,17 @@ module.exports = React.createClass({
}
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
+ let pinsIndicator = null;
+ if (this._hasUnreadPins()) {
+ pinsIndicator = ();
+ } else if (this._hasPins()) {
+ pinsIndicator = ();
+ }
+
pinnedEventsButton =
-
+
+ { pinsIndicator }
;
}
diff --git a/src/utils/PinningUtils.js b/src/utils/PinningUtils.js
new file mode 100644
index 0000000000..90d26cc988
--- /dev/null
+++ b/src/utils/PinningUtils.js
@@ -0,0 +1,30 @@
+/*
+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.
+*/
+
+export default class PinningUtils {
+ /**
+ * Determines if the given event may be pinned.
+ * @param {MatrixEvent} event The event to check.
+ * @return {boolean} True if the event may be pinned, false otherwise.
+ */
+ static isPinnable(event) {
+ if (!event) return false;
+ if (event.getType() !== "m.room.message") return false;
+ if (event.isRedacted()) return false;
+
+ return true;
+ }
+}