diff --git a/jenkins.sh b/jenkins.sh index a0e8d2e893..0979edfa13 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -4,7 +4,7 @@ set -e export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -nvm use 4 +nvm use 6 set -x diff --git a/karma.conf.js b/karma.conf.js index d8a6c25cc6..164cd9ce59 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -93,7 +93,18 @@ module.exports = function (config) { // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter - reporters: ['progress', 'junit'], + reporters: ['logcapture', 'spec', 'junit', 'summary'], + + specReporter: { + suppressErrorSummary: false, // do print error summary + suppressFailed: false, // do print information about failed tests + suppressPassed: false, // do print information about passed tests + showSpecTiming: true, // print the time elapsed for each spec + }, + + client: { + captureLogs: true, + }, // web server port port: 9876, @@ -104,7 +115,10 @@ module.exports = function (config) { // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - logLevel: config.LOG_INFO, + // + // This is strictly for logs that would be generated by the browser itself and we + // don't want to log about missing images, which are emitted on LOG_WARN. + logLevel: config.LOG_ERROR, // enable / disable watching file and executing tests whenever any file // changes diff --git a/package.json b/package.json index 9daaa38da7..661db4b6bc 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "classnames": "^2.1.2", "commonmark": "^0.27.0", "counterpart": "^0.18.0", - "draft-js": "^0.9.1", - "draft-js-export-html": "^0.5.0", - "draft-js-export-markdown": "^0.2.0", + "draft-js": "^0.11.0-alpha", + "draft-js-export-html": "^0.6.0", + "draft-js-export-markdown": "^0.3.0", "emojione": "2.2.7", "file-saver": "^1.3.3", "filesize": "3.5.6", @@ -102,12 +102,15 @@ "eslint-plugin-react": "^6.9.0", "expect": "^1.16.0", "json-loader": "^0.5.3", - "karma": "^0.13.22", + "karma": "^1.7.0", "karma-chrome-launcher": "^0.2.3", "karma-cli": "^0.1.2", "karma-junit-reporter": "^0.4.1", + "karma-logcapture-reporter": "0.0.1", "karma-mocha": "^0.2.2", "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "^0.0.31", + "karma-summary-reporter": "^1.3.3", "karma-webpack": "^1.7.0", "matrix-react-test-utils": "^0.1.1", "mocha": "^2.4.5", diff --git a/src/Markdown.js b/src/Markdown.js index 5730e42a09..6e735c6f0e 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -55,6 +55,25 @@ function is_multi_line(node) { return par.firstChild != par.lastChild; } +import linkifyMatrix from './linkify-matrix'; +import * as linkify from 'linkifyjs'; +linkifyMatrix(linkify); + +// Thieved from draft-js-export-markdown +function escapeMarkdown(s) { + return s.replace(/[*_`]/g, '\\$&'); +} + +// Replace URLs, room aliases and user IDs with md-escaped URLs +function linkifyMarkdown(s) { + const links = linkify.find(s); + links.forEach((l) => { + // This may replace several instances of `l.value` at once, but that's OK + s = s.replace(l.value, escapeMarkdown(l.value)); + }); + return s; +} + /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -62,7 +81,7 @@ function is_multi_line(node) { */ export default class Markdown { constructor(input) { - this.input = input; + this.input = linkifyMarkdown(input); const parser = new commonmark.Parser(); this.parsed = parser.parse(this.input); diff --git a/src/RichText.js b/src/RichText.js index c060565e2f..9876fcc93f 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -51,7 +51,8 @@ export const contentStateToHTML = (contentState: ContentState) => { }; export function htmlToContentState(html: string): ContentState { - return ContentState.createFromBlockArray(convertFromHTML(html)); + const blockArray = convertFromHTML(html).contentBlocks; + return ContentState.createFromBlockArray(blockArray); } function unicodeToEmojiUri(str) { @@ -90,7 +91,7 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb // Workaround for https://github.com/facebook/draft-js/issues/414 let emojiDecorator = { - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { findWithRegex(EMOJI_REGEX, contentBlock, callback); }, component: (props) => { @@ -119,7 +120,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { export function getScopedMDDecorators(scope: any): CompositeDecorator { let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map( (style) => ({ - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); }, component: (props) => ( @@ -130,7 +131,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { })); markdownDecorators.push({ - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback); }, component: (props) => ( @@ -201,10 +202,8 @@ export function selectionStateToTextOffsets(selectionState: SelectionState, export function textOffsetsToSelectionState({start, end}: SelectionRange, contentBlocks: Array): SelectionState { let selectionState = SelectionState.createEmpty(); - - for (let block of contentBlocks) { - let blockLength = block.getLength(); - + for (const block of contentBlocks) { + const blockLength = block.getLength(); if (start !== -1 && start < blockLength) { selectionState = selectionState.merge({ anchorKey: block.getKey(), @@ -212,9 +211,8 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange, }); start = -1; } else { - start -= blockLength; + start -= blockLength + 1; // +1 to account for newline between blocks } - if (end !== -1 && end <= blockLength) { selectionState = selectionState.merge({ focusKey: block.getKey(), @@ -222,10 +220,9 @@ export function textOffsetsToSelectionState({start, end}: SelectionRange, }); end = -1; } else { - end -= blockLength; + end -= blockLength + 1; // +1 to account for newline between blocks } } - return selectionState; } @@ -242,7 +239,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor const existingEntityKey = block.getEntityAt(start); if (existingEntityKey) { // avoid manipulation in case the emoji already has an entity - const entity = Entity.get(existingEntityKey); + const entity = newContentState.getEntity(existingEntityKey); if (entity && entity.get('type') === 'emoji') { return; } @@ -252,7 +249,10 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor .set('anchorOffset', start) .set('focusOffset', end); const emojiText = plainText.substring(start, end); - const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText }); + newContentState = newContentState.createEntity( + 'emoji', 'IMMUTABLE', { emojiUnicode: emojiText } + ); + const entityKey = newContentState.getLastCreatedEntityKey(); newContentState = Modifier.replaceText( newContentState, selection, diff --git a/src/Unread.js b/src/Unread.js index 8a70291cf2..8fffc2a429 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -15,6 +15,8 @@ limitations under the License. */ var MatrixClientPeg = require('./MatrixClientPeg'); +import UserSettingsStore from './UserSettingsStore'; +import shouldHideEvent from './shouldHideEvent'; var sdk = require('./index'); module.exports = { @@ -63,6 +65,7 @@ module.exports = { // we have and the read receipt. We could fetch more history to try & find out, // but currently we just guess. + const syncedSettings = UserSettingsStore.getSyncedSettings(); // Loop through messages, starting with the most recent... for (var i = room.timeline.length - 1; i >= 0; --i) { var ev = room.timeline[i]; @@ -72,7 +75,7 @@ module.exports = { // that counts and we can stop looking because the user's read // this and everything before. return false; - } else if (this.eventTriggersUnreadCount(ev)) { + } else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) { // We've found a message that counts before we hit // the read marker, so this room is definitely unread. return true; diff --git a/src/WidgetUtils.js b/src/WidgetUtils.js new file mode 100644 index 0000000000..34c998978d --- /dev/null +++ b/src/WidgetUtils.js @@ -0,0 +1,58 @@ +/* +Copyright 2017 Vector Creations Ltd + +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 MatrixClientPeg from './MatrixClientPeg'; + +export default class WidgetUtils { + + /* Returns true if user is able to send state events to modify widgets in this room + * @param roomId -- The ID of the room to check + * @return Boolean -- true if the user can modify widgets in this room + * @throws Error -- specifies the error reason + */ + static canUserModifyWidgets(roomId) { + if (!roomId) { + console.warn('No room ID specified'); + return false; + } + + const client = MatrixClientPeg.get(); + if (!client) { + console.warn('User must be be logged in'); + return false; + } + + const room = client.getRoom(roomId); + if (!room) { + console.warn(`Room ID ${roomId} is not recognised`); + return false; + } + + const me = client.credentials.userId; + if (!me) { + console.warn('Failed to get user ID'); + return false; + } + + const member = room.getMember(me); + if (!member || member.membership !== "join") { + console.warn(`User ${me} is not in room ${roomId}`); + return false; + } + + return room.currentState.maySendStateEvent('im.vector.modular.widgets', me); + } +} diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index d58587d423..16e0347a5b 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -71,6 +71,15 @@ const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sor let instance = null; +function score(query, space) { + const index = space.indexOf(query); + if (index === -1) { + return Infinity; + } else { + return index; + } +} + export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); @@ -104,8 +113,20 @@ export default class EmojiProvider extends AutocompleteProvider { // Do second match with shouldMatchWordsOnly in order to match against 'name' completions = completions.concat(this.nameMatcher.match(matchedString)); - // Reinstate original order - completions = _sortBy(_uniq(completions), '_orderBy'); + + const sorters = []; + // First, sort by score (Infinity if matchedString not in shortname) + sorters.push((c) => score(matchedString, c.shortname)); + // If the matchedString is not empty, sort by length of shortname. Example: + // matchedString = ":bookmark" + // completions = [":bookmark:", ":bookmark_tabs:", ...] + if (matchedString.length > 1) { + sorters.push((c) => c.shortname.length); + } + // Finally, sort by original ordering + sorters.push((c) => c._orderBy); + completions = _sortBy(_uniq(completions), sorters); + completions = completions.map((result) => { const {shortname} = result; const unicode = shortnameToUnicode(shortname); diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 09d2d79833..1770089eb2 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -23,35 +23,58 @@ import FuzzyMatcher from './FuzzyMatcher'; import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../Rooms'; import sdk from '../index'; +import _sortBy from 'lodash/sortBy'; const ROOM_REGEX = /(?=#)(\S*)/g; let instance = null; +function score(query, space) { + const index = space.indexOf(query); + if (index === -1) { + return Infinity; + } else { + return index; + } +} + export default class RoomProvider extends AutocompleteProvider { constructor() { super(ROOM_REGEX); this.matcher = new FuzzyMatcher([], { - keys: ['name', 'roomId', 'aliases'], + keys: ['displayedAlias', 'name'], }); } async getCompletions(query: string, selection: {start: number, end: number}, force = false) { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); - let client = MatrixClientPeg.get(); + // Disable autocompletions when composing commands because of various issues + // (see https://github.com/vector-im/riot-web/issues/4762) + if (/^(\/join|\/leave)/.test(query)) { + return []; + } + + const client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); if (command) { // the only reason we need to do this is because Fuse only matches on properties - this.matcher.setObjects(client.getRooms().filter(room => !!room && !!getDisplayAliasForRoom(room)).map(room => { + this.matcher.setObjects(client.getRooms().filter( + (room) => !!room && !!getDisplayAliasForRoom(room), + ).map((room) => { return { room: room, name: room.name, - aliases: room.getAliases(), + displayedAlias: getDisplayAliasForRoom(room), }; })); - completions = this.matcher.match(command[0]).map(room => { + const matchedString = command[0]; + completions = this.matcher.match(matchedString); + completions = _sortBy(completions, [ + (c) => score(matchedString, c.displayedAlias), + (c) => c.displayedAlias.length, + ]).map((room) => { const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId; return { completion: displayAlias, @@ -62,7 +85,9 @@ export default class RoomProvider extends AutocompleteProvider { ), range, }; - }).filter(completion => !!completion.completion && completion.completion.length > 0).slice(0, 4); + }) + .filter((completion) => !!completion.completion && completion.completion.length > 0) + .slice(0, 4); } return completions; } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 9c93cf537f..69b80dade4 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -48,13 +48,21 @@ export default class UserProvider extends AutocompleteProvider { async getCompletions(query: string, selection: {start: number, end: number}, force = false) { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); + // Disable autocompletions when composing commands because of various issues + // (see https://github.com/vector-im/riot-web/issues/4762) + if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) { + return []; + } + let completions = []; let {command, range} = this.getCurrentCommand(query, selection, force); if (command) { completions = this.matcher.match(command[0]).map((user) => { const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { - completion: displayName, + // Length of completion should equal length of text in decorator. draft-js + // relies on the length of the entity === length of the text in the decoration. + completion: user.rawDisplayName.replace(' (IRC)', ''), suffix: range.start === 0 ? ': ' : ' ', href: 'https://matrix.to/#/' + user.userId, component: ( diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b90cb53435..cb6419c9e8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -131,9 +131,6 @@ module.exports = React.createClass({ // the master view we are showing. view: VIEWS.LOADING, - // a thing to call showScreen with once login completes. - screenAfterLogin: this.props.initialScreenAfterLogin, - // What the LoggedInView would be showing if visible page_type: null, @@ -147,8 +144,6 @@ module.exports = React.createClass({ collapse_lhs: false, collapse_rhs: false, - ready: false, - width: 10000, leftOpacity: 1.0, middleOpacity: 1.0, rightOpacity: 1.0, @@ -274,6 +269,15 @@ module.exports = React.createClass({ register_hs_url: paramHs, }); } + + // a thing to call showScreen with once login completes. this is kept + // outside this.state because updating it should never trigger a + // rerender. + this._screenAfterLogin = this.props.initialScreenAfterLogin; + + this._windowWidth = 10000; + this.handleResize(); + window.addEventListener('resize', this.handleResize); }, componentDidMount: function() { @@ -294,9 +298,6 @@ module.exports = React.createClass({ linkifyMatrix.onGroupClick = this.onGroupClick; } - window.addEventListener('resize', this.handleResize); - this.handleResize(); - const teamServerConfig = this.props.config.teamServerConfig || {}; Lifecycle.initRtsClient(teamServerConfig.teamServerURL); @@ -312,13 +313,12 @@ module.exports = React.createClass({ // if the user has followed a login or register link, don't reanimate // the old creds, but rather go straight to the relevant page - const firstScreen = this.state.screenAfterLogin ? - this.state.screenAfterLogin.screen : null; + const firstScreen = this._screenAfterLogin ? + this._screenAfterLogin.screen : null; if (firstScreen === 'login' || firstScreen === 'register' || firstScreen === 'forgot_password') { - this.setState({loading: false}); this._showScreenAfterLogin(); return; } @@ -367,9 +367,9 @@ module.exports = React.createClass({ } const newState = { viewUserId: null, - }; - Object.assign(newState, state); - this.setState(newState); + }; + Object.assign(newState, state); + this.setState(newState); }, onAction: function(payload) { @@ -992,14 +992,12 @@ module.exports = React.createClass({ _showScreenAfterLogin: function() { // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory - if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { + if (this._screenAfterLogin && this._screenAfterLogin.screen) { this.showScreen( - this.state.screenAfterLogin.screen, - this.state.screenAfterLogin.params, + this._screenAfterLogin.screen, + this._screenAfterLogin.params, ); - // XXX: is this necessary? `showScreen` should do it for us. - this.notifyNewScreen(this.state.screenAfterLogin.screen); - this.setState({screenAfterLogin: null}); + this._screenAfterLogin = null; } else if (localStorage && localStorage.getItem('mx_last_room_id')) { // Before defaulting to directory, show the last viewed room dis.dispatch({ @@ -1276,20 +1274,20 @@ module.exports = React.createClass({ const hideRhsThreshold = 820; const showRhsThreshold = 820; - if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { + if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { dis.dispatch({ action: 'hide_left_panel' }); } - if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) { + if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) { dis.dispatch({ action: 'show_left_panel' }); } - if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { + if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { dis.dispatch({ action: 'hide_right_panel' }); } - if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) { + if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) { dis.dispatch({ action: 'show_right_panel' }); } - this.setState({width: window.innerWidth}); + this._windowWidth = window.innerWidth; }, onRoomCreated: function(roomId) { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 9251ff2bdb..6f4a5460f6 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -14,12 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require('react'); -var ReactDOM = require("react-dom"); -var dis = require("../../dispatcher"); -var sdk = require('../../index'); +import React from 'react'; +import ReactDOM from 'react-dom'; +import UserSettingsStore from '../../UserSettingsStore'; +import shouldHideEvent from '../../shouldHideEvent'; +import dis from "../../dispatcher"; +import sdk from '../../index'; -var MatrixClientPeg = require('../../MatrixClientPeg'); +import MatrixClientPeg from '../../MatrixClientPeg'; const MILLIS_IN_DAY = 86400000; @@ -90,9 +92,6 @@ module.exports = React.createClass({ // show timestamps always alwaysShowTimestamps: React.PropTypes.bool, - - // hide redacted events as per old behaviour - hideRedactions: React.PropTypes.bool, }, componentWillMount: function() { @@ -113,6 +112,8 @@ module.exports = React.createClass({ // Velocity requires this._readMarkerGhostNode = null; + this._syncedSettings = UserSettingsStore.getSyncedSettings(); + this._isMounted = true; }, @@ -238,8 +239,20 @@ module.exports = React.createClass({ return !this._isMounted; }, - _getEventTiles: function() { + // TODO: Implement granular (per-room) hide options + _shouldShowEvent: function(mxEv) { const EventTile = sdk.getComponent('rooms.EventTile'); + if (!EventTile.haveTileForEvent(mxEv)) { + return false; // no tile = no show + } + + // Always show highlighted event + if (this.props.highlightedEventId === mxEv.getId()) return true; + + return !shouldHideEvent(mxEv, this._syncedSettings); + }, + + _getEventTiles: function() { const DateSeparator = sdk.getComponent('messages.DateSeparator'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); @@ -249,20 +262,21 @@ module.exports = React.createClass({ // first figure out which is the last event in the list which we're // actually going to show; this allows us to behave slightly - // differently for the last event in the list. + // differently for the last event in the list. (eg show timestamp) // // we also need to figure out which is the last event we show which isn't // a local echo, to manage the read-marker. - var lastShownEventIndex = -1; + let lastShownEvent; + var lastShownNonLocalEchoIndex = -1; for (i = this.props.events.length-1; i >= 0; i--) { var mxEv = this.props.events[i]; - if (!EventTile.haveTileForEvent(mxEv)) { + if (!this._shouldShowEvent(mxEv)) { continue; } - if (lastShownEventIndex < 0) { - lastShownEventIndex = i; + if (lastShownEvent === undefined) { + lastShownEvent = mxEv; } if (mxEv.status) { @@ -288,25 +302,18 @@ module.exports = React.createClass({ this.currentGhostEventId = null; } - var isMembershipChange = (e) => e.getType() === 'm.room.member'; + const isMembershipChange = (e) => e.getType() === 'm.room.member'; for (i = 0; i < this.props.events.length; i++) { let mxEv = this.props.events[i]; - let wantTile = true; let eventId = mxEv.getId(); let readMarkerInMels = false; + let last = (mxEv === lastShownEvent); - if (!EventTile.haveTileForEvent(mxEv)) { - wantTile = false; - } - - let last = (i == lastShownEventIndex); + const wantTile = this._shouldShowEvent(mxEv); // Wrap consecutive member events in a ListSummary, ignore if redacted - if (isMembershipChange(mxEv) && - EventTile.haveTileForEvent(mxEv) && - !mxEv.isRedacted() - ) { + if (isMembershipChange(mxEv) && wantTile) { let ts1 = mxEv.getTs(); // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and @@ -325,35 +332,36 @@ module.exports = React.createClass({ let summarisedEvents = [mxEv]; for (;i + 1 < this.props.events.length; i++) { - let collapsedMxEv = this.props.events[i + 1]; - - // Ignore redacted member events - if (!EventTile.haveTileForEvent(collapsedMxEv)) { - continue; - } + const collapsedMxEv = this.props.events[i + 1]; if (!isMembershipChange(collapsedMxEv) || this._wantsDateSeparator(this.props.events[i], collapsedMxEv.getDate())) { break; } + + // If RM event is in MELS mark it as such and the RM will be appended after MELS. + if (collapsedMxEv.getId() === this.props.readMarkerEventId) { + readMarkerInMels = true; + } + + // Ignore redacted/hidden member events + if (!this._shouldShowEvent(collapsedMxEv)) { + continue; + } + summarisedEvents.push(collapsedMxEv); } - // At this point, i = the index of the last event in the summary sequence - let eventTiles = summarisedEvents.map( - (e) => { - if (e.getId() === this.props.readMarkerEventId) { - readMarkerInMels = true; - } - // In order to prevent DateSeparators from appearing in the expanded form - // of MemberEventListSummary, render each member event as if the previous - // one was itself. This way, the timestamp of the previous event === the - // timestamp of the current event, and no DateSeperator is inserted. - let ret = this._getTilesForEvent(e, e); - prevEvent = e; - return ret; - } - ).reduce((a, b) => a.concat(b)); + // At this point, i = the index of the last event in the summary sequence + let eventTiles = summarisedEvents.map((e) => { + // In order to prevent DateSeparators from appearing in the expanded form + // of MemberEventListSummary, render each member event as if the previous + // one was itself. This way, the timestamp of the previous event === the + // timestamp of the current event, and no DateSeperator is inserted. + const ret = this._getTilesForEvent(e, e, e === lastShownEvent); + prevEvent = e; + return ret; + }).reduce((a, b) => a.concat(b)); if (eventTiles.length === 0) { eventTiles = null; @@ -466,8 +474,6 @@ module.exports = React.createClass({ continuation = false; } - if (mxEv.isRedacted() && this.props.hideRedactions) return ret; - var eventId = mxEv.getId(); var highlight = (eventId == this.props.highlightedEventId); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index dc9c0fc9ba..0aee19545c 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -181,9 +181,6 @@ var TimelinePanel = React.createClass({ // always show timestamps on event tiles? alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps, - - // hide redacted events as per old behaviour - hideRedactions: syncedSettings.hideRedactions, }; }, @@ -1122,7 +1119,6 @@ var TimelinePanel = React.createClass({ return (