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; + } +}