diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 460ed43e82..5ce36b4b82 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -65,7 +65,7 @@ module.exports = React.createClass({
suppressFirstDateSeparator: React.PropTypes.bool,
// whether to show read receipts
- manageReadReceipts: React.PropTypes.bool,
+ showReadReceipts: React.PropTypes.bool,
// true if updates to the event list should cause the scroll panel to
// scroll down when we are at the bottom of the window. See ScrollPanel
@@ -154,15 +154,15 @@ module.exports = React.createClass({
// 0: read marker is within the window
// +1: read marker is below the window
getReadMarkerPosition: function() {
- var readMarker = this.refs.readMarkerNode;
- var messageWrapper = this.refs.scrollPanel;
+ const readMarker = this.refs.readMarkerNode;
+ const messageWrapper = this.refs.scrollPanel;
if (!readMarker || !messageWrapper) {
return null;
}
- var wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
- var readMarkerRect = readMarker.getBoundingClientRect();
+ const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
+ const readMarkerRect = readMarker.getBoundingClientRect();
// the read-marker pretends to have zero height when it is actually
// two pixels high; +2 here to account for that.
@@ -241,6 +241,10 @@ module.exports = React.createClass({
// TODO: Implement granular (per-room) hide options
_shouldShowEvent: function(mxEv) {
+ if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
+ return false; // ignored = no show (only happens if the ignore happens after an event was received)
+ }
+
const EventTile = sdk.getComponent('rooms.EventTile');
if (!EventTile.haveTileForEvent(mxEv)) {
return false; // no tile = no show
@@ -258,7 +262,7 @@ module.exports = React.createClass({
this.eventNodes = {};
- var i;
+ let i;
// first figure out which is the last event in the list which we're
// actually going to show; this allows us to behave slightly
@@ -268,9 +272,9 @@ module.exports = React.createClass({
// a local echo, to manage the read-marker.
let lastShownEvent;
- var lastShownNonLocalEchoIndex = -1;
+ let lastShownNonLocalEchoIndex = -1;
for (i = this.props.events.length-1; i >= 0; i--) {
- var mxEv = this.props.events[i];
+ const mxEv = this.props.events[i];
if (!this._shouldShowEvent(mxEv)) {
continue;
}
@@ -288,12 +292,12 @@ module.exports = React.createClass({
break;
}
- var ret = [];
+ const ret = [];
- var prevEvent = null; // the last event we showed
+ let prevEvent = null; // the last event we showed
// assume there is no read marker until proven otherwise
- var readMarkerVisible = false;
+ let readMarkerVisible = false;
// if the readmarker has moved, cancel any active ghost.
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
@@ -305,16 +309,16 @@ module.exports = React.createClass({
const isMembershipChange = (e) => e.getType() === 'm.room.member';
for (i = 0; i < this.props.events.length; i++) {
- let mxEv = this.props.events[i];
- let eventId = mxEv.getId();
- let last = (mxEv === lastShownEvent);
+ const mxEv = this.props.events[i];
+ const eventId = mxEv.getId();
+ const last = (mxEv === lastShownEvent);
const wantTile = this._shouldShowEvent(mxEv);
// Wrap consecutive member events in a ListSummary, ignore if redacted
if (isMembershipChange(mxEv) && wantTile) {
let readMarkerInMels = false;
- let ts1 = mxEv.getTs();
+ const 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
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
@@ -326,7 +330,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
- let dateSeparator =
;
+ const dateSeparator =
;
ret.push(dateSeparator);
}
@@ -335,7 +339,7 @@ module.exports = React.createClass({
readMarkerInMels = true;
}
- let summarisedEvents = [mxEv];
+ const summarisedEvents = [mxEv];
for (;i + 1 < this.props.events.length; i++) {
const collapsedMxEv = this.props.events[i + 1];
@@ -361,8 +365,13 @@ module.exports = React.createClass({
summarisedEvents.push(collapsedMxEv);
}
+ let highlightInMels = false;
+
// At this point, i = the index of the last event in the summary sequence
let eventTiles = summarisedEvents.map((e) => {
+ if (e.getId() === this.props.highlightedEventId) {
+ highlightInMels = 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
@@ -376,15 +385,13 @@ module.exports = React.createClass({
eventTiles = null;
}
- ret.push(
-
- {eventTiles}
-
- );
+ ret.push(
+ { eventTiles }
+ );
if (readMarkerInMels) {
ret.push(this._getReadMarkerTile(visible));
@@ -401,7 +408,7 @@ module.exports = React.createClass({
prevEvent = mxEv;
}
- var isVisibleReadMarker = false;
+ let isVisibleReadMarker = false;
if (eventId == this.props.readMarkerEventId) {
var visible = this.props.readMarkerVisible;
@@ -441,10 +448,10 @@ module.exports = React.createClass({
_getTilesForEvent: function(prevEvent, mxEv, last) {
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
- var ret = [];
+ const ret = [];
// is this a continuation of the previous message?
- var continuation = false;
+ let continuation = false;
if (prevEvent !== null
&& prevEvent.sender && mxEv.sender
@@ -469,8 +476,8 @@ module.exports = React.createClass({
// local echoes have a fake date, which could even be yesterday. Treat them
// as 'today' for the date separators.
- var ts1 = mxEv.getTs();
- var eventDate = mxEv.getDate();
+ let ts1 = mxEv.getTs();
+ let eventDate = mxEv.getDate();
if (mxEv.status) {
eventDate = new Date();
ts1 = eventDate.getTime();
@@ -478,20 +485,20 @@ module.exports = React.createClass({
// do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) {
- var dateSeparator =
;
+ const dateSeparator =
;
ret.push(dateSeparator);
continuation = false;
}
- var eventId = mxEv.getId();
- var highlight = (eventId == this.props.highlightedEventId);
+ const eventId = mxEv.getId();
+ const highlight = (eventId == this.props.highlightedEventId);
// we can't use local echoes as scroll tokens, because their event IDs change.
// Local echos have a send "status".
- var scrollToken = mxEv.status ? undefined : eventId;
+ const scrollToken = mxEv.status ? undefined : eventId;
- var readReceipts;
- if (this.props.manageReadReceipts) {
+ let readReceipts;
+ if (this.props.showReadReceipts) {
readReceipts = this._getReadReceiptsForEvent(mxEv);
}
ret.push(
@@ -508,8 +515,8 @@ module.exports = React.createClass({
eventSendStatus={mxEv.status}
tileShape={this.props.tileShape}
isTwelveHour={this.props.isTwelveHour}
- last={last} isSelectedEvent={highlight}/>
-
+ last={last} isSelectedEvent={highlight} />
+ ,
);
return ret;
@@ -544,12 +551,15 @@ module.exports = React.createClass({
if (!room) {
return null;
}
- let receipts = [];
+ const receipts = [];
room.getReceiptsForEvent(event).forEach((r) => {
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
}
- let member = room.getMember(r.userId);
+ if (MatrixClientPeg.get().isUserIgnored(r.userId)) {
+ return; // ignore ignored users
+ }
+ const member = room.getMember(r.userId);
if (!member) {
return; // ignore unknown user IDs
}
@@ -565,7 +575,7 @@ module.exports = React.createClass({
},
_getReadMarkerTile: function(visible) {
- var hr;
+ let hr;
if (visible) {
hr =
- {hr}
+ { hr }
);
},
@@ -594,7 +604,7 @@ module.exports = React.createClass({
},
_getReadMarkerGhostTile: function() {
- var hr = ;
@@ -605,7 +615,7 @@ module.exports = React.createClass({
return (
- {hr}
+ { hr }
);
},
@@ -617,7 +627,7 @@ module.exports = React.createClass({
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
_onWidgetLoad: function() {
- var scrollPanel = this.refs.scrollPanel;
+ const scrollPanel = this.refs.scrollPanel;
if (scrollPanel) {
scrollPanel.forceUpdate();
}
@@ -628,9 +638,9 @@ module.exports = React.createClass({
},
render: function() {
- var ScrollPanel = sdk.getComponent("structures.ScrollPanel");
- var Spinner = sdk.getComponent("elements.Spinner");
- var topSpinner, bottomSpinner;
+ const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
+ const Spinner = sdk.getComponent("elements.Spinner");
+ let topSpinner, bottomSpinner;
if (this.props.backPaginating) {
topSpinner =
- {_t('Error whilst fetching joined groups')}
+ { _t('Error whilst fetching joined communities') }
;
} else {
content = ;
}
return
-
+
- {_t('Create a new group')}
+ { _t('Create a new community') }
- {_t(
- 'Create a group to represent your community! '+
+ { _t(
+ 'Create a community to represent your community! '+
'Define a set of rooms and your own custom homepage '+
'to mark out your space in the Matrix universe.',
- )}
+ ) }
- {_t('Join an existing group')}
+ { _t('Join an existing community') }
- {_tJsx(
- 'To join an exisitng group you\'ll have to '+
- 'know its group identifier; this will look '+
+ { _tJsx(
+ 'To join an existing community you\'ll have to '+
+ 'know its community identifier; this will look '+
'something like +example:matrix.org.',
/(.*)<\/i>/,
- (sub) => {sub},
- )}
+ (sub) => { sub },
+ ) }
- {content}
+ { content }
;
},
diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.js
index 21eccbdff6..3c8f34fb86 100644
--- a/src/components/structures/NotificationPanel.js
+++ b/src/components/structures/NotificationPanel.js
@@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-var React = require('react');
-var ReactDOM = require("react-dom");
+const React = require('react');
+const ReactDOM = require("react-dom");
import { _t } from '../../languageHandler';
-var Matrix = require("matrix-js-sdk");
-var sdk = require('../../index');
-var MatrixClientPeg = require("../../MatrixClientPeg");
-var dis = require("../../dispatcher");
+const Matrix = require("matrix-js-sdk");
+const sdk = require('../../index');
+const MatrixClientPeg = require("../../MatrixClientPeg");
+const dis = require("../../dispatcher");
/*
* Component which shows the global notification list using a TimelinePanel
*/
-var NotificationPanel = React.createClass({
+const NotificationPanel = React.createClass({
displayName: 'NotificationPanel',
propTypes: {
@@ -33,10 +33,10 @@ var NotificationPanel = React.createClass({
render: function() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
- var TimelinePanel = sdk.getComponent("structures.TimelinePanel");
- var Loader = sdk.getComponent("elements.Spinner");
+ const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
+ const Loader = sdk.getComponent("elements.Spinner");
- var timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
+ const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
);
- }
- else {
+ } else {
console.error("No notifTimelineSet available!");
return (
-
+
);
}
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index 2a81605a78..cad55351d1 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -43,6 +43,10 @@ module.exports = React.createClass({
// the end of the live timeline.
atEndOfLiveTimeline: React.PropTypes.bool,
+ // This is true when the user is alone in the room, but has also sent a message.
+ // Used to suggest to the user to invite someone
+ sentMessageAndIsAlone: React.PropTypes.bool,
+
// true if there is an active call in this room (means we show
// the 'Active Call' text in the status bar if there is nothing
// more interesting)
@@ -60,6 +64,14 @@ module.exports = React.createClass({
// 'unsent messages' bar
onCancelAllClick: React.PropTypes.func,
+ // callback for when the user clicks on the 'invite others' button in the
+ // 'you are alone' bar
+ onInviteClick: React.PropTypes.func,
+
+ // callback for when the user clicks on the 'stop warning me' button in the
+ // 'you are alone' bar
+ onStopWarningClick: React.PropTypes.func,
+
// callback for when the user clicks on the 'scroll to bottom' button
onScrollToBottomClick: React.PropTypes.func,
@@ -103,7 +115,7 @@ module.exports = React.createClass({
componentWillUnmount: function() {
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
- var client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.get();
if (client) {
client.removeListener("sync", this.onSyncStateChange);
client.removeListener("RoomMember.typing", this.onRoomMemberTyping);
@@ -115,18 +127,18 @@ module.exports = React.createClass({
return;
}
this.setState({
- syncState: state
+ syncState: state,
});
},
onRoomMemberTyping: function(ev, member) {
this.setState({
- usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
+ usersTyping: WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room),
});
},
// Check whether current size is greater than 0, if yes call props.onVisible
- _checkSize: function () {
+ _checkSize: function() {
if (this.props.onVisible && this._getSize()) {
this.props.onVisible();
}
@@ -140,7 +152,8 @@ module.exports = React.createClass({
(this.state.usersTyping.length > 0) ||
this.props.numUnreadMessages ||
!this.props.atEndOfLiveTimeline ||
- this.props.hasActiveCall
+ this.props.hasActiveCall ||
+ this.props.sentMessageAndIsAlone
) {
return STATUS_BAR_EXPANDED;
} else if (this.props.unsentMessageError) {
@@ -157,9 +170,9 @@ module.exports = React.createClass({
if (this.props.numUnreadMessages) {
return (
- {_t('Connectivity to the server has been lost.')}
+ { _t('Connectivity to the server has been lost.') }
- {_t('Sent messages will be stored until your connection has returned.')}
+ { _t('Sent messages will be stored until your connection has returned.') }
);
@@ -275,24 +288,24 @@ module.exports = React.createClass({
// set when you've scrolled up
if (this.props.numUnreadMessages) {
// MUST use var name "count" for pluralization to kick in
- var unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
+ const unreadMsgs = _t("%(count)s new messages", {count: this.props.numUnreadMessages});
return (
);
},
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index f825d1efbb..83ca987276 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -20,33 +20,36 @@ limitations under the License.
// - Drag and drop
// - File uploading - uploadFile()
-var React = require("react");
-var ReactDOM = require("react-dom");
+import shouldHideEvent from "../../shouldHideEvent";
+
+const React = require("react");
+const ReactDOM = require("react-dom");
import Promise from 'bluebird';
-var classNames = require("classnames");
-var Matrix = require("matrix-js-sdk");
+const classNames = require("classnames");
+const Matrix = require("matrix-js-sdk");
import { _t } from '../../languageHandler';
-var UserSettingsStore = require('../../UserSettingsStore');
-var MatrixClientPeg = require("../../MatrixClientPeg");
-var ContentMessages = require("../../ContentMessages");
-var Modal = require("../../Modal");
-var sdk = require('../../index');
-var CallHandler = require('../../CallHandler');
-var Resend = require("../../Resend");
-var dis = require("../../dispatcher");
-var Tinter = require("../../Tinter");
-var rate_limited_func = require('../../ratelimitedfunc');
-var ObjectUtils = require('../../ObjectUtils');
-var Rooms = require('../../Rooms');
+const UserSettingsStore = require('../../UserSettingsStore');
+const MatrixClientPeg = require("../../MatrixClientPeg");
+const ContentMessages = require("../../ContentMessages");
+const Modal = require("../../Modal");
+const sdk = require('../../index');
+const CallHandler = require('../../CallHandler');
+const Resend = require("../../Resend");
+const dis = require("../../dispatcher");
+const Tinter = require("../../Tinter");
+const rate_limited_func = require('../../ratelimitedfunc');
+const ObjectUtils = require('../../ObjectUtils');
+const Rooms = require('../../Rooms');
import KeyCode from '../../KeyCode';
import UserProvider from '../../autocomplete/UserProvider';
import RoomViewStore from '../../stores/RoomViewStore';
+import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
-let DEBUG = false;
+const DEBUG = false;
let debuglog = function() {};
const BROWSER_SUPPORTS_SANDBOX = 'sandbox' in document.createElement('iframe');
@@ -114,12 +117,17 @@ module.exports = React.createClass({
guestsCanJoin: false,
canPeek: false,
showApps: false,
+ isAlone: false,
+ isPeeking: false,
// error object, as from the matrix client/server API
// If we failed to load information about the room,
// store the error here.
roomLoadError: null,
+ // Have we sent a request to join the room that we're waiting to complete?
+ joining: false,
+
// this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. It has the effect of hiding the
// 'scroll to bottom' knob, among a couple of other things.
@@ -143,6 +151,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData);
+ this._syncedSettings = UserSettingsStore.getSyncedSettings();
+
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
@@ -152,6 +162,22 @@ module.exports = React.createClass({
if (this.unmounted) {
return;
}
+
+ if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
+ // RoomView explicitly does not support changing what room
+ // is being viewed: instead it should just be re-mounted when
+ // switching rooms. Therefore, if the room ID changes, we
+ // ignore this. We either need to do this or add code to handle
+ // saving the scroll position (otherwise we end up saving the
+ // scroll position against the wrong room).
+
+ // Given that doing the setState here would cause a bunch of
+ // unnecessary work, we just ignore the change since we know
+ // that if the current room ID has changed from what we thought
+ // it was, it means we're about to be unmounted.
+ return;
+ }
+
const newState = {
roomId: RoomViewStore.getRoomId(),
roomAlias: RoomViewStore.getRoomAlias(),
@@ -159,16 +185,11 @@ module.exports = React.createClass({
roomLoadError: RoomViewStore.getRoomLoadError(),
joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(),
- initialEventPixelOffset: RoomViewStore.getInitialEventPixelOffset(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(),
};
- // finished joining, start waiting for a room and show a spinner. See onRoom.
- newState.waitingForRoom = this.state.joining && !newState.joining &&
- !RoomViewStore.getJoinError();
-
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
console.log(
'RVS update:',
@@ -177,7 +198,6 @@ module.exports = React.createClass({
'loading?', newState.roomLoading,
'joining?', newState.joining,
'initial?', initial,
- 'waiting?', newState.waitingForRoom,
'shouldPeek?', newState.shouldPeek,
);
@@ -185,6 +205,25 @@ module.exports = React.createClass({
// the RoomView instance
if (initial) {
newState.room = MatrixClientPeg.get().getRoom(newState.roomId);
+ if (newState.room) {
+ newState.unsentMessageError = this._getUnsentMessageError(newState.room);
+ newState.showApps = this._shouldShowApps(newState.room);
+ this._onRoomLoaded(newState.room);
+ }
+ }
+
+ if (this.state.roomId === null && newState.roomId !== null) {
+ // Get the scroll state for the new room
+
+ // If an event ID wasn't specified, default to the one saved for this room
+ // in the scroll state store. Assume initialEventPixelOffset should be set.
+ if (!newState.initialEventId) {
+ const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId);
+ if (roomScrollState) {
+ newState.initialEventId = roomScrollState.focussedEvent;
+ newState.initialEventPixelOffset = roomScrollState.pixelOffset;
+ }
+ }
}
// Clear the search results when clicking a search result (which changes the
@@ -193,22 +232,20 @@ module.exports = React.createClass({
newState.searchResults = null;
}
- // Store the scroll state for the previous room so that we can return to this
- // position when viewing this room in future.
- if (this.state.roomId !== newState.roomId) {
- this._updateScrollMap(this.state.roomId);
- }
+ this.setState(newState);
+ // At this point, newState.roomId could be null (e.g. the alias might not
+ // have been resolved yet) so anything called here must handle this case.
- this.setState(newState, () => {
- // At this point, this.state.roomId could be null (e.g. the alias might not
- // have been resolved yet) so anything called here must handle this case.
- if (initial) {
- this._onHaveRoom();
- }
- });
+ // We pass the new state into this function for it to read: it needs to
+ // observe the new state but we don't want to put it in the setState
+ // callback because this would prevent the setStates from being batched,
+ // ie. cause it to render RoomView twice rather than the once that is necessary.
+ if (initial) {
+ this._setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek);
+ }
},
- _onHaveRoom: function() {
+ _setupRoom: function(room, roomId, joining, shouldPeek) {
// if this is an unknown room then we're in one of three states:
// - This is a room we can peek into (search engine) (we can /peek)
// - This is a room we can publicly join or were invited to. (we can /join)
@@ -224,29 +261,27 @@ module.exports = React.createClass({
// about it). We don't peek in the historical case where we were joined but are
// now not joined because the js-sdk peeking API will clobber our historical room,
// making it impossible to indicate a newly joined room.
- const room = this.state.room;
- if (room) {
- this.setState({
- unsentMessageError: this._getUnsentMessageError(room),
- showApps: this._shouldShowApps(room),
- });
- this._onRoomLoaded(room);
- }
- if (!this.state.joining && this.state.roomId) {
+ if (!joining && roomId) {
if (this.props.autoJoin) {
this.onJoinButtonClicked();
- } else if (!room && this.state.shouldPeek) {
- console.log("Attempting to peek into room %s", this.state.roomId);
+ } else if (!room && shouldPeek) {
+ console.log("Attempting to peek into room %s", roomId);
this.setState({
peekLoading: true,
+ isPeeking: true, // this will change to false if peeking fails
});
- MatrixClientPeg.get().peekInRoom(this.state.roomId).then((room) => {
+ MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
this.setState({
room: room,
peekLoading: false,
});
this._onRoomLoaded(room);
}, (err) => {
+ // Stop peeking if anything went wrong
+ this.setState({
+ isPeeking: false,
+ });
+
// This won't necessarily be a MatrixError, but we duck-type
// here and say if it's got an 'errcode' key with the right value,
// it means we can't peek.
@@ -263,6 +298,7 @@ module.exports = React.createClass({
} else if (room) {
// Stop peeking because we have joined this room previously
MatrixClientPeg.get().stopPeeking();
+ this.setState({isPeeking: false});
}
},
@@ -280,10 +316,10 @@ module.exports = React.createClass({
},
componentDidMount: function() {
- var call = this._getCallForRoom();
- var callState = call ? call.call_state : "ended";
+ const call = this._getCallForRoom();
+ const callState = call ? call.call_state : "ended";
this.setState({
- callState: callState
+ callState: callState,
});
this._updateConfCallNotification();
@@ -300,9 +336,8 @@ module.exports = React.createClass({
this.state.room.getJoinedMembers().length == 1 &&
this.state.room.getLiveTimeline() &&
this.state.room.getLiveTimeline().getEvents() &&
- this.state.room.getLiveTimeline().getEvents().length <= 6)
- {
- var inviteBox = document.getElementById("mx_SearchableEntityList_query");
+ this.state.room.getLiveTimeline().getEvents().length <= 6) {
+ const inviteBox = document.getElementById("mx_SearchableEntityList_query");
setTimeout(function() {
if (inviteBox) {
inviteBox.focus();
@@ -318,7 +353,7 @@ module.exports = React.createClass({
componentDidUpdate: function() {
if (this.refs.roomView) {
- var roomView = ReactDOM.findDOMNode(this.refs.roomView);
+ const roomView = ReactDOM.findDOMNode(this.refs.roomView);
if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver);
@@ -336,14 +371,16 @@ module.exports = React.createClass({
this.unmounted = true;
// update the scroll map before we get unmounted
- this._updateScrollMap(this.state.roomId);
+ if (this.state.roomId) {
+ RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
+ }
if (this.refs.roomView) {
// disconnect the D&D event listeners from the room view. This
// is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up.
- var roomView = ReactDOM.findDOMNode(this.refs.roomView);
+ const roomView = ReactDOM.findDOMNode(this.refs.roomView);
roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
@@ -425,6 +462,8 @@ module.exports = React.createClass({
switch (payload.action) {
case 'message_send_failed':
case 'message_sent':
+ this._checkIfAlone(this.state.room);
+ // no break; to intentionally fall through
case 'message_send_cancelled':
this.setState({
unsentMessageError: this._getUnsentMessageError(this.state.room),
@@ -449,8 +488,7 @@ module.exports = React.createClass({
if (call) {
callState = call.call_state;
- }
- else {
+ } else {
callState = "ended";
}
@@ -497,8 +535,7 @@ module.exports = React.createClass({
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
- }
- else {
+ } else if (!shouldHideEvent(ev, this._syncedSettings)) {
this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1};
});
@@ -563,17 +600,17 @@ module.exports = React.createClass({
},
_calculatePeekRules: function(room) {
- var guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
+ const guestAccessEvent = room.currentState.getStateEvents("m.room.guest_access", "");
if (guestAccessEvent && guestAccessEvent.getContent().guest_access === "can_join") {
this.setState({
- guestsCanJoin: true
+ guestsCanJoin: true,
});
}
- var historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", "");
+ const historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", "");
if (historyVisibility && historyVisibility.getContent().history_visibility === "world_readable") {
this.setState({
- canPeek: true
+ canPeek: true,
});
}
},
@@ -582,47 +619,35 @@ module.exports = React.createClass({
// console.log("_updatePreviewUrlVisibility");
// check our per-room overrides
- var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
+ const roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) {
this.setState({
- showUrlPreview: !roomPreviewUrls.getContent().disable
+ showUrlPreview: !roomPreviewUrls.getContent().disable,
});
return;
}
// check our global disable override
- var userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
+ const userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) {
this.setState({
- showUrlPreview: false
+ showUrlPreview: false,
});
return;
}
// check the room state event
- var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
+ const roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) {
this.setState({
- showUrlPreview: false
+ showUrlPreview: false,
});
return;
}
// otherwise, we assume they're on.
this.setState({
- showUrlPreview: true
- });
- },
-
- _updateScrollMap(roomId) {
- // No point updating scroll state if the room ID hasn't been resolved yet
- if (!roomId) {
- return;
- }
- dis.dispatch({
- action: 'update_scroll_state',
- room_id: roomId,
- scroll_state: this._getScrollState(),
+ showUrlPreview: true,
});
},
@@ -632,18 +657,17 @@ module.exports = React.createClass({
}
this.setState({
room: room,
- waitingForRoom: false,
}, () => {
this._onRoomLoaded(room);
});
},
updateTint: function() {
- var room = this.state.room;
+ const room = this.state.room;
if (!room) return;
- var color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
- var color_scheme = {};
+ const color_scheme_event = room.getAccountData("org.matrix.room.color_scheme");
+ let color_scheme = {};
if (color_scheme_event) {
color_scheme = color_scheme_event.getContent();
// XXX: we should validate the event
@@ -661,12 +685,11 @@ module.exports = React.createClass({
onRoomAccountData: function(event, room) {
if (room.roomId == this.state.roomId) {
if (event.getType() === "org.matrix.room.color_scheme") {
- var color_scheme = event.getContent();
+ const color_scheme = event.getContent();
// XXX: we should validate the event
console.log("Tinter.tint from onRoomAccountData");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
- }
- else if (event.getType() === "org.matrix.room.preview_urls") {
+ } else if (event.getType() === "org.matrix.room.preview_urls") {
this._updatePreviewUrlVisibility(room);
}
}
@@ -688,14 +711,7 @@ module.exports = React.createClass({
onRoomMemberMembership: function(ev, member, oldMembership) {
if (member.userId == MatrixClientPeg.get().credentials.userId) {
-
- if (member.membership === 'join') {
- this.setState({
- waitingForRoom: false,
- });
- } else {
- this.forceUpdate();
- }
+ this.forceUpdate();
}
},
@@ -712,7 +728,7 @@ module.exports = React.createClass({
// if we are now a member of the room, where we were not before, that
// means we have finished joining a room we were previously peeking
// into.
- var me = MatrixClientPeg.get().credentials.userId;
+ const me = MatrixClientPeg.get().credentials.userId;
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
// Having just joined a room, check to see if it looks like a DM room, and if so,
// mark it as one. This is to work around the fact that some clients don't support
@@ -727,9 +743,34 @@ module.exports = React.createClass({
}
}, 500),
+ _checkIfAlone: function(room) {
+ let warnedAboutLonelyRoom = false;
+ if (localStorage) {
+ warnedAboutLonelyRoom = localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId);
+ }
+ if (warnedAboutLonelyRoom) {
+ if (this.state.isAlone) this.setState({isAlone: false});
+ return;
+ }
+
+ const joinedMembers = room.currentState.getMembers().filter(m => m.membership === "join" || m.membership === "invite");
+ this.setState({isAlone: joinedMembers.length === 1});
+ },
+
_getUnsentMessageError: function(room) {
const unsentMessages = this._getUnsentMessages(room);
if (!unsentMessages.length) return "";
+
+ if (
+ unsentMessages.length === 1 &&
+ unsentMessages[0].error &&
+ unsentMessages[0].error.data &&
+ unsentMessages[0].error.data.error &&
+ unsentMessages[0].error.name !== "UnknownDeviceError"
+ ) {
+ return unsentMessages[0].error.data.error;
+ }
+
for (const event of unsentMessages) {
if (!event.error || event.error.name !== "UnknownDeviceError") {
return _t("Some of your messages have not been sent.");
@@ -746,18 +787,18 @@ module.exports = React.createClass({
},
_updateConfCallNotification: function() {
- var room = this.state.room;
+ const room = this.state.room;
if (!room || !this.props.ConferenceHandler) {
return;
}
- var confMember = room.getMember(
- this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId)
+ const confMember = room.getMember(
+ this.props.ConferenceHandler.getConferenceUserIdForRoom(room.roomId),
);
if (!confMember) {
return;
}
- var confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId);
+ const confCall = this.props.ConferenceHandler.getConferenceCallForRoom(confMember.roomId);
// A conf call notification should be displayed if there is an ongoing
// conf call but this cilent isn't a part of it.
@@ -765,7 +806,7 @@ module.exports = React.createClass({
displayConfCallNotification: (
(!confCall || confCall.call_state === "ended") &&
confMember.membership === "join"
- )
+ ),
});
},
@@ -780,7 +821,7 @@ module.exports = React.createClass({
if (this.state.searchResults.next_batch) {
debuglog("requesting more search results");
- var searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
+ const searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch(
this.state.searchResults);
return this._handleSearchResult(searchPromise);
} else {
@@ -797,6 +838,22 @@ module.exports = React.createClass({
Resend.cancelUnsentEvents(this.state.room);
},
+ onInviteButtonClick: function() {
+ // call AddressPickerDialog
+ dis.dispatch({
+ action: 'view_invite',
+ roomId: this.state.room.roomId,
+ });
+ this.setState({isAlone: false}); // there's a good chance they'll invite someone
+ },
+
+ onStopAloneWarningClick: function() {
+ if (localStorage) {
+ localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, true);
+ }
+ this.setState({isAlone: false});
+ },
+
onJoinButtonClicked: function(ev) {
const cli = MatrixClientPeg.get();
@@ -875,8 +932,7 @@ module.exports = React.createClass({
numUnreadMessages: 0,
atEndOfLiveTimeline: true,
});
- }
- else {
+ } else {
this.setState({
atEndOfLiveTimeline: false,
});
@@ -890,10 +946,10 @@ module.exports = React.createClass({
ev.dataTransfer.dropEffect = 'none';
- var items = ev.dataTransfer.items;
+ const items = ev.dataTransfer.items;
if (items.length == 1) {
if (items[0].kind == 'file') {
- this.setState({ draggingFile : true });
+ this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy';
}
}
@@ -902,8 +958,8 @@ module.exports = React.createClass({
onDrop: function(ev) {
ev.stopPropagation();
ev.preventDefault();
- this.setState({ draggingFile : false });
- var files = ev.dataTransfer.files;
+ this.setState({ draggingFile: false });
+ const files = ev.dataTransfer.files;
if (files.length == 1) {
this.uploadFile(files[0]);
}
@@ -912,7 +968,7 @@ module.exports = React.createClass({
onDragLeaveOrEnd: function(ev) {
ev.stopPropagation();
ev.preventDefault();
- this.setState({ draggingFile : false });
+ this.setState({ draggingFile: false });
},
uploadFile: function(file) {
@@ -922,7 +978,7 @@ module.exports = React.createClass({
}
ContentMessages.sendContentToRoom(
- file, this.state.room.roomId, MatrixClientPeg.get()
+ file, this.state.room.roomId, MatrixClientPeg.get(),
).done(undefined, (error) => {
if (error.name === "UnknownDeviceError") {
dis.dispatch({
@@ -961,19 +1017,19 @@ module.exports = React.createClass({
// todo: should cancel any previous search requests.
this.searchId = new Date().getTime();
- var filter;
+ let filter;
if (scope === "Room") {
filter = {
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
rooms: [
- this.state.room.roomId
- ]
+ this.state.room.roomId,
+ ],
};
}
debuglog("sending search request");
- var searchPromise = MatrixClientPeg.get().searchRoomEvents({
+ const searchPromise = MatrixClientPeg.get().searchRoomEvents({
filter: filter,
term: term,
});
@@ -981,11 +1037,11 @@ module.exports = React.createClass({
},
_handleSearchResult: function(searchPromise) {
- var self = this;
+ const self = this;
// keep a record of the current search id, so that if the search terms
// change before we get a response, we can ignore the results.
- var localSearchId = this.searchId;
+ const localSearchId = this.searchId;
this.setState({
searchInProgress: true,
@@ -1004,7 +1060,7 @@ module.exports = React.createClass({
// In either case, we want to highlight the literal search term
// whether it was used by the search engine or not.
- var highlights = results.highlights;
+ let highlights = results.highlights;
if (highlights.indexOf(self.state.searchTerm) < 0) {
highlights = highlights.concat(self.state.searchTerm);
}
@@ -1012,14 +1068,15 @@ module.exports = React.createClass({
// For overlapping highlights,
// favour longer (more specific) terms first
highlights = highlights.sort(function(a, b) {
- return b.length - a.length; });
+ return b.length - a.length;
+});
self.setState({
searchHighlights: highlights,
searchResults: results,
});
}, function(error) {
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Search failed: " + error);
Modal.createTrackedDialog('Search failed', '', ErrorDialog, {
title: _t("Search failed"),
@@ -1027,17 +1084,17 @@ module.exports = React.createClass({
});
}).finally(function() {
self.setState({
- searchInProgress: false
+ searchInProgress: false,
});
});
},
getSearchResultTiles: function() {
- var EventTile = sdk.getComponent('rooms.EventTile');
- var SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
- var Spinner = sdk.getComponent("elements.Spinner");
+ const EventTile = sdk.getComponent('rooms.EventTile');
+ const SearchResultTile = sdk.getComponent('rooms.SearchResultTile');
+ const Spinner = sdk.getComponent("elements.Spinner");
- var cli = MatrixClientPeg.get();
+ const cli = MatrixClientPeg.get();
// XXX: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work?
@@ -1047,7 +1104,7 @@ module.exports = React.createClass({
return [];
}
- var ret = [];
+ const ret = [];
if (this.state.searchInProgress) {
ret.push(
+ ,
);
}
}
// once dynamic content in the search results load, make the scrollPanel check
// the scroll offsets.
- var onWidgetLoad = () => {
- var scrollPanel = this.refs.searchResultsPanel;
+ const onWidgetLoad = () => {
+ const scrollPanel = this.refs.searchResultsPanel;
if (scrollPanel) {
scrollPanel.checkScroll();
}
};
- var lastRoomId;
+ let lastRoomId;
- for (var i = this.state.searchResults.results.length - 1; i >= 0; i--) {
- var result = this.state.searchResults.results[i];
+ for (let i = this.state.searchResults.results.length - 1; i >= 0; i--) {
+ const result = this.state.searchResults.results[i];
- var mxEv = result.context.getEvent();
- var roomId = mxEv.getRoomId();
+ const mxEv = result.context.getEvent();
+ const roomId = mxEv.getRoomId();
if (!EventTile.haveTileForEvent(mxEv)) {
// XXX: can this ever happen? It will make the result count
@@ -1094,13 +1151,13 @@ module.exports = React.createClass({
if (this.state.searchScope === 'All') {
if(roomId != lastRoomId) {
- var room = cli.getRoom(roomId);
+ const room = cli.getRoom(roomId);
// XXX: if we've left the room, we might not know about
// it. We should tell the js sdk to go and find out about
// it. But that's not an issue currently, as synapse only
// returns results for rooms we're joined to.
- var roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
+ const roomName = room ? room.name : _t("Unknown room %(roomId)s", { roomId: roomId });
ret.push(
{ _t("Room") }: { roomName }
@@ -1109,17 +1166,21 @@ module.exports = React.createClass({
}
}
- var resultLink = "#/room/"+roomId+"/"+mxEv.getId();
+ const resultLink = "#/room/"+roomId+"/"+mxEv.getId();
ret.push();
+ onWidgetLoad={onWidgetLoad} />);
}
return ret;
},
+ onPinnedClick: function() {
+ this.setState({showingPinned: !this.state.showingPinned, searching: false});
+ },
+
onSettingsClick: function() {
this.showSettings(true);
},
@@ -1131,38 +1192,37 @@ module.exports = React.createClass({
uploadingRoomSettings: true,
});
- var newName = this.refs.header.getEditedName();
+ const newName = this.refs.header.getEditedName();
if (newName !== undefined) {
this.refs.room_settings.setName(newName);
}
- var newTopic = this.refs.header.getEditedTopic();
+ const newTopic = this.refs.header.getEditedTopic();
if (newTopic !== undefined) {
this.refs.room_settings.setTopic(newTopic);
}
this.refs.room_settings.save().then((results) => {
- var fails = results.filter(function(result) { return result.state !== "fulfilled"; });
+ const fails = results.filter(function(result) { return result.state !== "fulfilled"; });
console.log("Settings saved with %s errors", fails.length);
if (fails.length) {
fails.forEach(function(result) {
console.error(result.reason);
});
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, {
title: _t("Failed to save settings"),
description: fails.map(function(result) { return result.reason; }).join("\n"),
});
// still editing room settings
- }
- else {
+ } else {
this.setState({
- editingRoomSettings: false
+ editingRoomSettings: false,
});
}
}).finally(() => {
this.setState({
uploadingRoomSettings: false,
- editingRoomSettings: false
+ editingRoomSettings: false,
});
}).done();
},
@@ -1193,8 +1253,8 @@ module.exports = React.createClass({
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
}, function(err) {
- var errCode = err.errcode || _t("unknown error code");
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const errCode = err.errcode || _t("unknown error code");
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t("Error"),
description: _t("Failed to forget room %(errCode)s", { errCode: errCode }),
@@ -1203,20 +1263,20 @@ module.exports = React.createClass({
},
onRejectButtonClicked: function(ev) {
- var self = this;
+ const self = this;
this.setState({
- rejecting: true
+ rejecting: true,
});
MatrixClientPeg.get().leave(this.state.roomId).done(function() {
dis.dispatch({ action: 'view_next_room' });
self.setState({
- rejecting: false
+ rejecting: false,
});
}, function(error) {
console.error("Failed to reject invite: %s", error);
- var msg = error.message ? error.message : JSON.stringify(error);
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const msg = error.message ? error.message : JSON.stringify(error);
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, {
title: _t("Failed to reject invite"),
description: msg,
@@ -1224,7 +1284,7 @@ module.exports = React.createClass({
self.setState({
rejecting: false,
- rejectError: error
+ rejectError: error,
});
});
},
@@ -1240,7 +1300,7 @@ module.exports = React.createClass({
},
onSearchClick: function() {
- this.setState({ searching: true });
+ this.setState({ searching: true, showingPinned: false });
},
onCancelSearchClick: function() {
@@ -1284,7 +1344,7 @@ module.exports = React.createClass({
// restored when we switch back to it.
//
_getScrollState: function() {
- var messagePanel = this.refs.messagePanel;
+ const messagePanel = this.refs.messagePanel;
if (!messagePanel) return null;
// if we're following the live timeline, we want to return null; that
@@ -1299,7 +1359,7 @@ module.exports = React.createClass({
return null;
}
- var scrollState = messagePanel.getScrollState();
+ const scrollState = messagePanel.getScrollState();
if (scrollState.stuckAtBottom) {
// we don't really expect to be in this state, but it will
@@ -1326,7 +1386,7 @@ module.exports = React.createClass({
// a maxHeight on the underlying remote video tag.
// header + footer + status + give us at least 120px of scrollback at all times.
- var auxPanelMaxHeight = window.innerHeight -
+ let auxPanelMaxHeight = window.innerHeight -
(83 + // height of RoomHeader
36 + // height of the status area
72 + // minimum height of the message compmoser
@@ -1345,26 +1405,26 @@ module.exports = React.createClass({
onFullscreenClick: function() {
dis.dispatch({
action: 'video_fullscreen',
- fullscreen: true
+ fullscreen: true,
}, true);
},
onMuteAudioClick: function() {
- var call = this._getCallForRoom();
+ const call = this._getCallForRoom();
if (!call) {
return;
}
- var newState = !call.isMicrophoneMuted();
+ const newState = !call.isMicrophoneMuted();
call.setMicrophoneMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons
},
onMuteVideoClick: function() {
- var call = this._getCallForRoom();
+ const call = this._getCallForRoom();
if (!call) {
return;
}
- var newState = !call.isLocalVideoMuted();
+ const newState = !call.isLocalVideoMuted();
call.setLocalVideoMuted(newState);
this.forceUpdate(); // TODO: just update the voip buttons
},
@@ -1400,7 +1460,7 @@ module.exports = React.createClass({
* We pass it down to the scroll panel.
*/
handleScrollKey: function(ev) {
- var panel;
+ let panel;
if(this.refs.searchResultsPanel) {
panel = this.refs.searchResultsPanel;
} else if(this.refs.messagePanel) {
@@ -1439,16 +1499,13 @@ 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");
const Loader = sdk.getComponent("elements.Spinner");
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
- // Whether the preview bar spinner should be shown. We do this when joining or
- // when waiting for a room to be returned by js-sdk when joining
- const previewBarSpinner = this.state.joining || this.state.waitingForRoom;
-
if (!this.state.room) {
if (this.state.roomLoading || this.state.peekLoading) {
return (
@@ -1474,15 +1531,15 @@ module.exports = React.createClass({
-
);
} else {
- var inviteEvent = myMember.events.member;
+ const inviteEvent = myMember.events.member;
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
// We deliberately don't try to peek into invites, even if we have permission to peek
@@ -1517,15 +1574,15 @@ module.exports = React.createClass({
-
@@ -1538,33 +1595,36 @@ module.exports = React.createClass({
// We have successfully loaded this room, and are not previewing.
// Display the "normal" room view.
- var call = this._getCallForRoom();
- var inCall = false;
+ const call = this._getCallForRoom();
+ let inCall = false;
if (call && (this.state.callState !== 'ended' && this.state.callState !== 'ringing')) {
inCall = true;
}
- var scrollheader_classes = classNames({
+ const scrollheader_classes = classNames({
mx_RoomView_scrollheader: true,
});
- var statusBar;
+ let statusBar;
let isStatusAreaExpanded = true;
if (ContentMessages.getCurrentUploads().length > 0) {
- var UploadBar = sdk.getComponent('structures.UploadBar');
+ const UploadBar = sdk.getComponent('structures.UploadBar');
statusBar = ;
} else if (!this.state.searchResults) {
- var RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
+ const RoomStatusBar = sdk.getComponent('structures.RoomStatusBar');
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = ;
} else if (this.state.uploadingRoomSettings) {
- aux = ;
+ aux = ;
} else if (this.state.forwardingEvent !== null) {
aux = ;
} else if (this.state.searching) {
hideCancel = true; // has own cancel
- aux = ;
+ 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.
@@ -1598,9 +1661,9 @@ module.exports = React.createClass({
hideCancel = true;
aux = (
);
- var messageComposer, searchInfo;
- var canSpeak = (
+ let messageComposer, searchInfo;
+ const canSpeak = (
// joined and not showing search results
myMember && (myMember.membership == 'join') && !this.state.searchResults
);
@@ -1634,8 +1697,8 @@ module.exports = React.createClass({
onResize={this.onChildResize}
uploadFile={this.uploadFile}
callState={this.state.callState}
- opacity={ this.props.opacity }
- showApps={ this.state.showApps }
+ opacity={this.props.opacity}
+ showApps={this.state.showApps}
/>;
}
@@ -1643,19 +1706,19 @@ module.exports = React.createClass({
// in this.state if this is what RoomHeader desires?
if (this.state.searchResults) {
searchInfo = {
- searchTerm : this.state.searchTerm,
- searchScope : this.state.searchScope,
- searchCount : this.state.searchResults.count,
+ searchTerm: this.state.searchTerm,
+ searchScope: this.state.searchScope,
+ searchCount: this.state.searchResults.count,
};
}
if (inCall) {
- var zoomButton, voiceMuteButton, videoMuteButton;
+ let zoomButton, voiceMuteButton, videoMuteButton;
if (call.type === "video") {
zoomButton = (
-
;
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
@@ -1680,25 +1743,25 @@ module.exports = React.createClass({
{ videoMuteButton }
{ zoomButton }
{ statusBar }
-
+
;
}
// if we have search results, we keep the messagepanel (so that it preserves its
// scroll state), but hide it.
- var searchResultsPanel;
- var hideMessagePanel = false;
+ let searchResultsPanel;
+ let hideMessagePanel = false;
if (this.state.searchResults) {
searchResultsPanel = (
- {this.getSearchResultTiles()}
+ { this.getSearchResultTiles() }
);
hideMessagePanel = true;
@@ -1713,25 +1776,26 @@ module.exports = React.createClass({
}
// console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview);
- var messagePanel = (
+ const messagePanel = (
);
- var topUnreadMessagesBar = null;
+ let topUnreadMessagesBar = null;
if (this.state.showTopUnreadMessagesBar) {
- var TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
+ const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar');
topUnreadMessagesBar = (
+
= 0; --i) {
- var m = messages[i];
+ let node;
+ const messages = this.refs.itemlist.children;
+ for (let i = messages.length-1; i >= 0; --i) {
+ const m = messages[i];
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
@@ -564,10 +565,10 @@ module.exports = React.createClass({
return;
}
- var scrollNode = this._getScrollNode();
- var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
- var boundingRect = node.getBoundingClientRect();
- var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
+ const scrollNode = this._getScrollNode();
+ const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
+ const boundingRect = node.getBoundingClientRect();
+ const scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom;
debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" +
pixelOffset + " (delta: "+scrollDelta+")");
@@ -575,7 +576,6 @@ module.exports = React.createClass({
if(scrollDelta != 0) {
this._setScrollTop(scrollNode.scrollTop + scrollDelta);
}
-
},
_saveScrollState: function() {
@@ -585,16 +585,16 @@ module.exports = React.createClass({
return;
}
- var itemlist = this.refs.itemlist;
- var wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
- var messages = itemlist.children;
+ const itemlist = this.refs.itemlist;
+ const wrapperRect = ReactDOM.findDOMNode(this).getBoundingClientRect();
+ const messages = itemlist.children;
let newScrollState = null;
- for (var i = messages.length-1; i >= 0; --i) {
- var node = messages[i];
+ for (let i = messages.length-1; i >= 0; --i) {
+ const node = messages[i];
if (!node.dataset.scrollTokens) continue;
- var boundingRect = node.getBoundingClientRect();
+ const boundingRect = node.getBoundingClientRect();
newScrollState = {
stuckAtBottom: false,
trackedScrollToken: node.dataset.scrollTokens.split(',')[0],
@@ -619,8 +619,8 @@ module.exports = React.createClass({
},
_restoreSavedScrollState: function() {
- var scrollState = this.scrollState;
- var scrollNode = this._getScrollNode();
+ const scrollState = this.scrollState;
+ const scrollNode = this._getScrollNode();
if (scrollState.stuckAtBottom) {
this._setScrollTop(Number.MAX_VALUE);
@@ -631,9 +631,9 @@ module.exports = React.createClass({
},
_setScrollTop: function(scrollTop) {
- var scrollNode = this._getScrollNode();
+ const scrollNode = this._getScrollNode();
- var prevScroll = scrollNode.scrollTop;
+ const prevScroll = scrollNode.scrollTop;
// FF ignores attempts to set scrollTop to very large numbers
scrollNode.scrollTop = Math.min(scrollTop, scrollNode.scrollHeight);
@@ -676,7 +676,7 @@ module.exports = React.createClass({
className={this.props.className} style={this.props.style}>
- {this.props.children}
+ { this.props.children }
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 6f72fcc767..e3b3b66f97 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -15,27 +15,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-var React = require('react');
-var ReactDOM = require("react-dom");
+const React = require('react');
+const ReactDOM = require("react-dom");
import Promise from 'bluebird';
-var Matrix = require("matrix-js-sdk");
-var EventTimeline = Matrix.EventTimeline;
+const Matrix = require("matrix-js-sdk");
+const EventTimeline = Matrix.EventTimeline;
-var sdk = require('../../index');
+const sdk = require('../../index');
import { _t } from '../../languageHandler';
-var MatrixClientPeg = require("../../MatrixClientPeg");
-var dis = require("../../dispatcher");
-var ObjectUtils = require('../../ObjectUtils');
-var Modal = require("../../Modal");
-var UserActivity = require("../../UserActivity");
-var KeyCode = require('../../KeyCode');
+const MatrixClientPeg = require("../../MatrixClientPeg");
+const dis = require("../../dispatcher");
+const ObjectUtils = require('../../ObjectUtils');
+const Modal = require("../../Modal");
+const UserActivity = require("../../UserActivity");
+const KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
-var PAGINATE_SIZE = 20;
-var INITIAL_SIZE = 20;
+const PAGINATE_SIZE = 20;
+const INITIAL_SIZE = 20;
-var DEBUG = false;
+const DEBUG = false;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
@@ -59,6 +59,7 @@ var TimelinePanel = React.createClass({
// that room.
timelineSet: React.PropTypes.object.isRequired,
+ showReadReceipts: React.PropTypes.bool,
// Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: React.PropTypes.bool,
manageReadMarkers: React.PropTypes.bool,
@@ -259,7 +260,7 @@ var TimelinePanel = React.createClass({
dis.unregister(this.dispatcherRef);
- var client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.get();
if (client) {
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Room.timelineReset", this.onRoomTimelineReset);
@@ -274,20 +275,20 @@ var TimelinePanel = React.createClass({
onMessageListUnfillRequest: function(backwards, scrollToken) {
// If backwards, unpaginate from the back (i.e. the start of the timeline)
- let dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
+ const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
debuglog("TimelinePanel: unpaginating events in direction", dir);
// All tiles are inserted by MessagePanel to have a scrollToken === eventId, and
// this particular event should be the first or last to be unpaginated.
- let eventId = scrollToken;
+ const eventId = scrollToken;
- let marker = this.state.events.findIndex(
+ const marker = this.state.events.findIndex(
(ev) => {
return ev.getId() === eventId;
- }
+ },
);
- let count = backwards ? marker + 1 : this.state.events.length - marker;
+ const count = backwards ? marker + 1 : this.state.events.length - marker;
if (count > 0) {
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
@@ -304,9 +305,9 @@ var TimelinePanel = React.createClass({
// set off a pagination request.
onMessageListFillRequest: function(backwards) {
- var dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
- var canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
- var paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
+ const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
+ const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
+ const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
if (!this.state[canPaginateKey]) {
debuglog("TimelinePanel: have given up", dir, "paginating this timeline");
@@ -327,7 +328,7 @@ var TimelinePanel = React.createClass({
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
- var newState = {
+ const newState = {
[paginatingKey]: false,
[canPaginateKey]: r,
events: this._getEvents(),
@@ -335,17 +336,24 @@ var TimelinePanel = React.createClass({
// moving the window in this direction may mean that we can now
// paginate in the other where we previously could not.
- var otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
- var canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
+ const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
+ const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
if (!this.state[canPaginateOtherWayKey] &&
this._timelineWindow.canPaginate(otherDirection)) {
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
newState[canPaginateOtherWayKey] = true;
}
- this.setState(newState);
-
- return r;
+ // Don't resolve until the setState has completed: we need to let
+ // the component update before we consider the pagination completed,
+ // otherwise we'll end up paginating in all the history the js-sdk
+ // has in memory because we never gave the component a chance to scroll
+ // itself into the right place
+ return new Promise((resolve) => {
+ this.setState(newState, () => {
+ resolve(r);
+ });
+ });
});
},
@@ -376,6 +384,9 @@ var TimelinePanel = React.createClass({
this.sendReadReceipt();
this.updateReadMarker();
break;
+ case 'ignore_state_changed':
+ this.forceUpdate();
+ break;
}
},
@@ -409,15 +420,15 @@ var TimelinePanel = React.createClass({
this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).done(() => {
if (this.unmounted) { return; }
- var events = this._timelineWindow.getEvents();
- var lastEv = events[events.length-1];
+ const events = this._timelineWindow.getEvents();
+ const lastEv = events[events.length-1];
// if we're at the end of the live timeline, append the pending events
if (this.props.timelineSet.room && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(...this.props.timelineSet.room.getPendingEvents());
}
- var updatedState = {events: events};
+ const updatedState = {events: events};
if (this.props.manageReadMarkers) {
// when a new event arrives when the user is not watching the
@@ -428,8 +439,8 @@ var TimelinePanel = React.createClass({
// read-marker when a remote echo of an event we have just sent takes
// more than the timeout on userCurrentlyActive.
//
- var myUserId = MatrixClientPeg.get().credentials.userId;
- var sender = ev.sender ? ev.sender.userId : null;
+ const myUserId = MatrixClientPeg.get().credentials.userId;
+ const sender = ev.sender ? ev.sender.userId : null;
var callback = null;
if (sender != myUserId && !UserActivity.userCurrentlyActive()) {
updatedState.readMarkerVisible = true;
@@ -635,7 +646,7 @@ var TimelinePanel = React.createClass({
// and we'll get confused when their ID changes and we can't figure out
// where the RM is pointing to. The read marker will be invisible for
// now anyway, so this doesn't really matter.
- var lastDisplayedIndex = this._getLastDisplayedEventIndex({
+ const lastDisplayedIndex = this._getLastDisplayedEventIndex({
allowPartial: true,
ignoreEchoes: true,
});
@@ -644,7 +655,7 @@ var TimelinePanel = React.createClass({
return;
}
- var lastDisplayedEvent = this.state.events[lastDisplayedIndex];
+ const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
this._setReadMarker(lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs());
@@ -665,7 +676,7 @@ var TimelinePanel = React.createClass({
// we call _timelineWindow.getEvents() rather than using
// this.state.events, because react batches the update to the latter, so it
// may not have been updated yet.
- var events = this._timelineWindow.getEvents();
+ const events = this._timelineWindow.getEvents();
// first find where the current RM is
for (var i = 0; i < events.length; i++) {
@@ -678,7 +689,7 @@ var TimelinePanel = React.createClass({
}
// now think about advancing it
- var myUserId = MatrixClientPeg.get().credentials.userId;
+ const myUserId = MatrixClientPeg.get().credentials.userId;
for (i++; i < events.length; i++) {
var ev = events[i];
if (!ev.sender || ev.sender.userId != myUserId) {
@@ -723,7 +734,7 @@ var TimelinePanel = React.createClass({
//
// a quick way to figure out if we've loaded the relevant event is
// simply to check if the messagepanel knows where the read-marker is.
- var ret = this.refs.messagePanel.getReadMarkerPosition();
+ const ret = this.refs.messagePanel.getReadMarkerPosition();
if (ret !== null) {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
@@ -744,13 +755,13 @@ var TimelinePanel = React.createClass({
forgetReadMarker: function() {
if (!this.props.manageReadMarkers) return;
- var rmId = this._getCurrentReadReceipt();
+ const rmId = this._getCurrentReadReceipt();
// see if we know the timestamp for the rr event
- var tl = this.props.timelineSet.getTimelineForEvent(rmId);
- var rmTs;
+ const tl = this.props.timelineSet.getTimelineForEvent(rmId);
+ let rmTs;
if (tl) {
- var event = tl.getEvents().find((e) => { return e.getId() == rmId; });
+ const event = tl.getEvents().find((e) => { return e.getId() == rmId; });
if (event) {
rmTs = event.getTs();
}
@@ -790,7 +801,7 @@ var TimelinePanel = React.createClass({
if (!this.props.manageReadMarkers) return null;
if (!this.refs.messagePanel) return null;
- var ret = this.refs.messagePanel.getReadMarkerPosition();
+ const ret = this.refs.messagePanel.getReadMarkerPosition();
if (ret !== null) {
return ret;
}
@@ -833,8 +844,7 @@ var TimelinePanel = React.createClass({
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey &&
- ev.keyCode == KeyCode.END)
- {
+ ev.keyCode == KeyCode.END) {
this.jumpToLiveTimeline();
} else {
this.refs.messagePanel.handleScrollKey(ev);
@@ -842,12 +852,12 @@ var TimelinePanel = React.createClass({
},
_initTimeline: function(props) {
- var initialEvent = props.eventId;
- var pixelOffset = props.eventPixelOffset;
+ const initialEvent = props.eventId;
+ const pixelOffset = props.eventPixelOffset;
// if a pixelOffset is given, it is relative to the bottom of the
// container. If not, put the event in the middle of the container.
- var offsetBase = 1;
+ let offsetBase = 1;
if (pixelOffset == null) {
offsetBase = 0.5;
}
@@ -876,7 +886,7 @@ var TimelinePanel = React.createClass({
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
- var onLoaded = () => {
+ const onLoaded = () => {
this._reloadEvents();
// If we switched away from the room while there were pending
@@ -911,15 +921,15 @@ var TimelinePanel = React.createClass({
});
};
- var onError = (error) => {
+ const onError = (error) => {
this.setState({timelineLoading: false});
console.error(
`Error loading timeline panel at ${eventId}: ${error}`,
);
- var msg = error.message ? error.message : JSON.stringify(error);
- var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
+ const msg = error.message ? error.message : JSON.stringify(error);
+ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
- var onFinished;
+ let onFinished;
// if we were given an event ID, then when the user closes the
// dialog, let's jump to the end of the timeline. If we weren't,
@@ -934,7 +944,7 @@ var TimelinePanel = React.createClass({
});
};
}
- var message = (error.errcode == 'M_FORBIDDEN')
+ const message = (error.errcode == 'M_FORBIDDEN')
? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.")
: _t("Tried to load a specific point in this room's timeline, but was unable to find it.");
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
@@ -944,7 +954,7 @@ var TimelinePanel = React.createClass({
});
};
- var prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
+ let prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
// if we already have the event in question, TimelineWindow.load
// returns a resolved promise.
@@ -985,7 +995,7 @@ var TimelinePanel = React.createClass({
// get the list of events from the timeline window and the pending event list
_getEvents: function() {
- var events = this._timelineWindow.getEvents();
+ const events = this._timelineWindow.getEvents();
// if we're at the end of the live timeline, append the pending events
if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
@@ -996,7 +1006,7 @@ var TimelinePanel = React.createClass({
},
_indexForEventId: function(evId) {
- for (var i = 0; i < this.state.events.length; ++i) {
+ for (let i = 0; i < this.state.events.length; ++i) {
if (evId == this.state.events[i].getId()) {
return i;
}
@@ -1006,18 +1016,18 @@ var TimelinePanel = React.createClass({
_getLastDisplayedEventIndex: function(opts) {
opts = opts || {};
- var ignoreOwn = opts.ignoreOwn || false;
- var ignoreEchoes = opts.ignoreEchoes || false;
- var allowPartial = opts.allowPartial || false;
+ const ignoreOwn = opts.ignoreOwn || false;
+ const ignoreEchoes = opts.ignoreEchoes || false;
+ const allowPartial = opts.allowPartial || false;
- var messagePanel = this.refs.messagePanel;
+ const messagePanel = this.refs.messagePanel;
if (messagePanel === undefined) return null;
- var wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
- var myUserId = MatrixClientPeg.get().credentials.userId;
+ const wrapperRect = ReactDOM.findDOMNode(messagePanel).getBoundingClientRect();
+ const myUserId = MatrixClientPeg.get().credentials.userId;
- for (var i = this.state.events.length-1; i >= 0; --i) {
- var ev = this.state.events[i];
+ for (let i = this.state.events.length-1; i >= 0; --i) {
+ const ev = this.state.events[i];
if (ignoreOwn && ev.sender && ev.sender.userId == myUserId) {
continue;
@@ -1028,10 +1038,10 @@ var TimelinePanel = React.createClass({
continue;
}
- var node = messagePanel.getNodeForEventId(ev.getId());
+ const node = messagePanel.getNodeForEventId(ev.getId());
if (!node) continue;
- var boundingRect = node.getBoundingClientRect();
+ const boundingRect = node.getBoundingClientRect();
if ((allowPartial && boundingRect.top < wrapperRect.bottom) ||
(!allowPartial && boundingRect.bottom < wrapperRect.bottom)) {
return i;
@@ -1049,18 +1059,18 @@ var TimelinePanel = React.createClass({
* SDK.
*/
_getCurrentReadReceipt: function(ignoreSynthesized) {
- var client = MatrixClientPeg.get();
+ const client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null) {
return null;
}
- var myUserId = client.credentials.userId;
+ const myUserId = client.credentials.userId;
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
},
_setReadMarker: function(eventId, eventTs, inhibitSetState) {
- var roomId = this.props.timelineSet.room.roomId;
+ const roomId = this.props.timelineSet.room.roomId;
// don't update the state (and cause a re-render) if there is
// no change to the RM.
@@ -1085,8 +1095,8 @@ var TimelinePanel = React.createClass({
},
render: function() {
- var MessagePanel = sdk.getComponent("structures.MessagePanel");
- var Loader = sdk.getComponent("elements.Spinner");
+ const MessagePanel = sdk.getComponent("structures.MessagePanel");
+ const Loader = sdk.getComponent("elements.Spinner");
// just show a spinner while the timeline loads.
//
@@ -1101,7 +1111,7 @@ var TimelinePanel = React.createClass({
// exist.
if (this.state.timelineLoading) {
return (
-