diff --git a/.eslintrc.js b/.eslintrc.js index 6cd0e1015e..74790a2964 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -64,7 +64,7 @@ module.exports = { // to JSX. ignorePattern: '^\\s*<', ignoreComments: true, - code: 90, + code: 120, }], "valid-jsdoc": ["warn"], "new-cap": ["warn"], diff --git a/package.json b/package.json index 5c96a74f5b..836f7fd353 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ }, "scripts": { "reskindex": "scripts/reskindex.js -h header", - "build": "node scripts/babelcheck.js && babel src -d lib --source-maps", - "start": "node scripts/babelcheck.js && babel src -w -d lib --source-maps", + "build": "babel src -d lib --source-maps", + "start": "babel src -w -d lib --source-maps", "lint": "eslint src/", "lintall": "eslint src/ test/", "clean": "rimraf lib", @@ -53,7 +53,7 @@ "draft-js-export-markdown": "^0.2.0", "emojione": "2.2.3", "file-saver": "^1.3.3", - "filesize": "^3.1.2", + "filesize": "3.5.6", "flux": "^2.0.3", "fuse.js": "^2.2.0", "glob": "^5.0.14", diff --git a/scripts/babelcheck.js b/scripts/babelcheck.js deleted file mode 100644 index 14e4a28a70..0000000000 --- a/scripts/babelcheck.js +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env node - -var exec = require('child_process').exec; - -// Makes sure the babel executable in the path is babel 6 (or greater), not -// babel 5, which it is if you upgrade from an older version of react-sdk and -// run 'npm install' since the package has changed to babel-cli, so 'babel' -// remains installed and the executable in node_modules/.bin remains as babel -// 5. - -exec("babel -V", function (error, stdout, stderr) { - if ((error && error.code) || parseInt(stdout.substr(0,1), 10) < 6) { - console.log("\033[31m\033[1m"+ - '*****************************************\n'+ - '* matrix-react-sdk has moved to babel 6 *\n'+ - '* Please "rm -rf node_modules && npm i" *\n'+ - '* then restore links as appropriate *\n'+ - '*****************************************\n'+ - "\033[91m"); - process.exit(1); - } -}); diff --git a/src/Avatar.js b/src/Avatar.js index 76f5e55ff0..c0127d49af 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -22,8 +22,8 @@ module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { var url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), - width, - height, + Math.floor(width * window.devicePixelRatio), + Math.floor(height * window.devicePixelRatio), resizeMethod, false, false @@ -40,7 +40,9 @@ module.exports = { avatarUrlForUser: function(user, width, height, resizeMethod) { var url = ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, - width, height, resizeMethod + Math.floor(width * window.devicePixelRatio), + Math.floor(height * window.devicePixelRatio), + resizeMethod ); if (!url || url.length === 0) { return null; @@ -57,4 +59,3 @@ module.exports = { return 'img/' + images[total % images.length] + '.png'; } }; - diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js index 265ee11fd4..6c2c3266aa 100644 --- a/src/ConstantTimeDispatcher.js +++ b/src/ConstantTimeDispatcher.js @@ -47,7 +47,7 @@ class ConstantTimeDispatcher { dispatch(type, arg, params) { if (!this.listeners[type] || !this.listeners[type][arg]) { - console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")"); + //console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")"); return; } this.listeners[type][arg].forEach(listener=>{ diff --git a/src/Lifecycle.js b/src/Lifecycle.js index f20716cae6..f34aeae0e5 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -49,7 +49,7 @@ import sdk from './index'; * If any of steps 1-4 are successful, it will call {setLoggedIn}, which in * turn will raise on_logged_in and will_start_client events. * - * It returns a promise which resolves when the above process completes. + * @param {object} opts * * @param {object} opts.realQueryParams: string->string map of the * query-parameters extracted from the real query-string of the starting @@ -67,6 +67,7 @@ import sdk from './index'; * @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is * true; defines the IS to use. * + * @returns {Promise} a promise which resolves when the above process completes. */ export function loadSession(opts) { const realQueryParams = opts.realQueryParams || {}; @@ -127,7 +128,7 @@ export function loadSession(opts) { function _loginWithToken(queryParams, defaultDeviceDisplayName) { // create a temporary MatrixClient to do the login - var client = Matrix.createClient({ + const client = Matrix.createClient({ baseUrl: queryParams.homeserver, }); @@ -159,7 +160,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { // Not really sure where the right home for it is. // create a temporary MatrixClient to do the login - var client = Matrix.createClient({ + const client = Matrix.createClient({ baseUrl: hsUrl, }); @@ -188,30 +189,30 @@ function _restoreFromLocalStorage() { if (!localStorage) { return q(false); } - const hs_url = localStorage.getItem("mx_hs_url"); - const is_url = localStorage.getItem("mx_is_url") || 'https://matrix.org'; - const access_token = localStorage.getItem("mx_access_token"); - const user_id = localStorage.getItem("mx_user_id"); - const device_id = localStorage.getItem("mx_device_id"); + const hsUrl = localStorage.getItem("mx_hs_url"); + const isUrl = localStorage.getItem("mx_is_url") || 'https://matrix.org'; + const accessToken = localStorage.getItem("mx_access_token"); + const userId = localStorage.getItem("mx_user_id"); + const deviceId = localStorage.getItem("mx_device_id"); - let is_guest; + let isGuest; if (localStorage.getItem("mx_is_guest") !== null) { - is_guest = localStorage.getItem("mx_is_guest") === "true"; + isGuest = localStorage.getItem("mx_is_guest") === "true"; } else { // legacy key name - is_guest = localStorage.getItem("matrix-is-guest") === "true"; + isGuest = localStorage.getItem("matrix-is-guest") === "true"; } - if (access_token && user_id && hs_url) { - console.log("Restoring session for %s", user_id); + if (accessToken && userId && hsUrl) { + console.log("Restoring session for %s", userId); try { setLoggedIn({ - userId: user_id, - deviceId: device_id, - accessToken: access_token, - homeserverUrl: hs_url, - identityServerUrl: is_url, - guest: is_guest, + userId: userId, + deviceId: deviceId, + accessToken: accessToken, + homeserverUrl: hsUrl, + identityServerUrl: isUrl, + guest: isGuest, }); return q(true); } catch (e) { @@ -273,9 +274,13 @@ export function initRtsClient(url) { */ export function setLoggedIn(credentials) { credentials.guest = Boolean(credentials.guest); - console.log("setLoggedIn => %s (guest=%s) hs=%s", - credentials.userId, credentials.guest, - credentials.homeserverUrl); + + console.log( + "setLoggedIn: mxid:", credentials.userId, + "deviceId:", credentials.deviceId, + "guest:", credentials.guest, + "hs:", credentials.homeserverUrl, + ); // This is dispatched to indicate that the user is still in the process of logging in // because `teamPromise` may take some time to resolve, breaking the assumption that // `setLoggedIn` takes an "instant" to complete, and dispatch `on_logged_in` a few ms @@ -352,7 +357,7 @@ export function logout() { return; } - return MatrixClientPeg.get().logout().then(onLoggedOut, + MatrixClientPeg.get().logout().then(onLoggedOut, (err) => { // Just throwing an error here is going to be very unhelpful // if you're trying to log out because your server's down and @@ -363,8 +368,8 @@ export function logout() { // change your password). console.log("Failed to call logout API: token will not be invalidated"); onLoggedOut(); - } - ); + }, + ).done(); } /** @@ -420,7 +425,7 @@ export function stopMatrixClient() { UserActivity.stop(); Presence.stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); - var cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); if (cli) { cli.stopClient(); cli.removeAllListeners(); diff --git a/src/Notifier.js b/src/Notifier.js index 92770877b7..6473ab4d9c 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -15,11 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require("./MatrixClientPeg"); -var PlatformPeg = require("./PlatformPeg"); -var TextForEvent = require('./TextForEvent'); -var Avatar = require('./Avatar'); -var dis = require("./dispatcher"); +import MatrixClientPeg from './MatrixClientPeg'; +import PlatformPeg from './PlatformPeg'; +import TextForEvent from './TextForEvent'; +import Avatar from './Avatar'; +import dis from './dispatcher'; +import sdk from './index'; +import Modal from './Modal'; /* * Dispatches: @@ -29,7 +31,7 @@ var dis = require("./dispatcher"); * } */ -var Notifier = { +const Notifier = { notifsByRoom: {}, notificationMessageForEvent: function(ev) { @@ -48,16 +50,16 @@ var Notifier = { return; } - var msg = this.notificationMessageForEvent(ev); + let msg = this.notificationMessageForEvent(ev); if (!msg) return; - var title; - if (!ev.sender || room.name == ev.sender.name) { + let title; + if (!ev.sender || room.name === ev.sender.name) { title = room.name; // notificationMessageForEvent includes sender, // but we already have the sender here if (ev.getContent().body) msg = ev.getContent().body; - } else if (ev.getType() == 'm.room.member') { + } else if (ev.getType() === 'm.room.member') { // context is all in the message here, we don't need // to display sender info title = room.name; @@ -68,7 +70,7 @@ var Notifier = { if (ev.getContent().body) msg = ev.getContent().body; } - var avatarUrl = ev.sender ? Avatar.avatarUrlForMember( + const avatarUrl = ev.sender ? Avatar.avatarUrlForMember( ev.sender, 40, 40, 'crop' ) : null; @@ -83,7 +85,7 @@ var Notifier = { }, _playAudioNotification: function(ev, room) { - var e = document.getElementById("messageAudio"); + const e = document.getElementById("messageAudio"); if (e) { e.load(); e.play(); @@ -95,7 +97,7 @@ var Notifier = { this.boundOnSyncStateChange = this.onSyncStateChange.bind(this); this.boundOnRoomReceipt = this.onRoomReceipt.bind(this); MatrixClientPeg.get().on('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().on("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().on("sync", this.boundOnSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; @@ -104,7 +106,7 @@ var Notifier = { stop: function() { if (MatrixClientPeg.get() && this.boundOnRoomTimeline) { MatrixClientPeg.get().removeListener('Room.timeline', this.boundOnRoomTimeline); - MatrixClientPeg.get().removeListener("Room.receipt", this.boundOnRoomReceipt); + MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt); MatrixClientPeg.get().removeListener('sync', this.boundOnSyncStateChange); } this.isSyncing = false; @@ -121,7 +123,7 @@ var Notifier = { // make sure that we persist the current setting audio_enabled setting // before changing anything if (global.localStorage) { - if(global.localStorage.getItem('audio_notifications_enabled') == null) { + if (global.localStorage.getItem('audio_notifications_enabled') === null) { this.setAudioEnabled(this.isEnabled()); } } @@ -131,6 +133,16 @@ var Notifier = { plaf.requestNotificationPermission().done((result) => { if (result !== 'granted') { // The permission request was dismissed or denied + const description = result === 'denied' + ? 'Riot does not have permission to send you notifications' + + ' - please check your browser settings' + : 'Riot was not given permission to send notifications' + + ' - please try again'; + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createDialog(ErrorDialog, { + title: 'Unable to enable Notifications', + description, + }); return; } @@ -141,7 +153,7 @@ var Notifier = { if (callback) callback(); dis.dispatch({ action: "notifier_enabled", - value: true + value: true, }); }); // clear the notifications_hidden flag, so that if notifications are @@ -152,7 +164,7 @@ var Notifier = { global.localStorage.setItem('notifications_enabled', 'false'); dis.dispatch({ action: "notifier_enabled", - value: false + value: false, }); } }, @@ -165,7 +177,7 @@ var Notifier = { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem('notifications_enabled'); + const enabled = global.localStorage.getItem('notifications_enabled'); if (enabled === null) return true; return enabled === 'true'; }, @@ -173,12 +185,12 @@ var Notifier = { setAudioEnabled: function(enable) { if (!global.localStorage) return; global.localStorage.setItem('audio_notifications_enabled', - enable ? 'true' : 'false'); + enable ? 'true' : 'false'); }, isAudioEnabled: function(enable) { if (!global.localStorage) return true; - var enabled = global.localStorage.getItem( + const enabled = global.localStorage.getItem( 'audio_notifications_enabled'); // default to true if the popups are enabled if (enabled === null) return this.isEnabled(); @@ -192,7 +204,7 @@ var Notifier = { // this is nothing to do with notifier_enabled dis.dispatch({ action: "notifier_enabled", - value: this.isEnabled() + value: this.isEnabled(), }); // update the info to localStorage for persistent settings @@ -215,8 +227,7 @@ var Notifier = { onSyncStateChange: function(state) { if (state === "SYNCING") { this.isSyncing = true; - } - else if (state === "STOPPED" || state === "ERROR") { + } else if (state === "STOPPED" || state === "ERROR") { this.isSyncing = false; } }, @@ -225,10 +236,10 @@ var Notifier = { if (toStartOfTimeline) return; if (!room) return; if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) return; + if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; - var actions = MatrixClientPeg.get().getPushActionsForEvent(ev); + const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { if (this.isEnabled()) { this._displayPopupNotification(ev, room); @@ -240,7 +251,7 @@ var Notifier = { }, onRoomReceipt: function(ev, room) { - if (room.getUnreadNotificationCount() == 0) { + if (room.getUnreadNotificationCount() === 0) { // ideally we would clear each notification when it was read, // but we have no way, given a read receipt, to know whether // the receipt comes before or after an event, so we can't @@ -255,7 +266,7 @@ var Notifier = { } delete this.notifsByRoom[room.roomId]; } - } + }, }; if (!global.mxNotifier) { diff --git a/src/UserActivity.js b/src/UserActivity.js index e7338e17e9..1ae272f5df 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -32,7 +32,7 @@ class UserActivity { start() { document.onmousedown = this._onUserActivity.bind(this); document.onmousemove = this._onUserActivity.bind(this); - document.onkeypress = this._onUserActivity.bind(this); + document.onkeydown = this._onUserActivity.bind(this); // can't use document.scroll here because that's only the document // itself being scrolled. Need to use addEventListener's useCapture. // also this needs to be the wheel event, not scroll, as scroll is @@ -50,7 +50,7 @@ class UserActivity { stop() { document.onmousedown = undefined; document.onmousemove = undefined; - document.onkeypress = undefined; + document.onkeydown = undefined; window.removeEventListener('wheel', this._onUserActivity.bind(this), { passive: true, capture: true }); } diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 8d18e92a0d..fd6599dd00 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -226,10 +226,8 @@ export default React.createClass({ case PageTypes.RoomDirectory: page_element = ; - if (!this.props.collapse_rhs) right_panel = ; break; case PageTypes.HomePage: diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 87f444d607..d4bf147ad5 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -282,15 +282,16 @@ module.exports = React.createClass({ var isMembershipChange = (e) => e.getType() === 'm.room.member'; for (i = 0; i < this.props.events.length; i++) { - var mxEv = this.props.events[i]; - var wantTile = true; - var eventId = mxEv.getId(); + let mxEv = this.props.events[i]; + let wantTile = true; + let eventId = mxEv.getId(); + let readMarkerInMels = false; if (!EventTile.haveTileForEvent(mxEv)) { wantTile = false; } - var last = (i == lastShownEventIndex); + let last = (i == lastShownEventIndex); // Wrap consecutive member events in a ListSummary, ignore if redacted if (isMembershipChange(mxEv) && @@ -332,6 +333,9 @@ module.exports = React.createClass({ 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 @@ -350,12 +354,16 @@ module.exports = React.createClass({ {eventTiles} ); + + if (readMarkerInMels) { + ret.push(this._getReadMarkerTile(visible)); + } + continue; } @@ -464,7 +472,7 @@ module.exports = React.createClass({ ret.push(
  • + data-scroll-tokens={scrollToken}> excessHeight) { break; @@ -419,7 +423,8 @@ module.exports = React.createClass({ * scroll. false if we are tracking a particular child. * * string trackedScrollToken: undefined if stuckAtBottom is true; if it is - * false, the data-scroll-token of the child which we are tracking. + * false, the first token in data-scroll-tokens of the child which we are + * tracking. * * number pixelOffset: undefined if stuckAtBottom is true; if it is false, * the number of pixels the bottom of the tracked child is above the @@ -551,8 +556,10 @@ module.exports = React.createClass({ var messages = this.refs.itemlist.children; for (var i = messages.length-1; i >= 0; --i) { var m = messages[i]; - if (!m.dataset.scrollToken) continue; - if (m.dataset.scrollToken == scrollToken) { + // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens + // There might only be one scroll token + if (m.dataset.scrollTokens && + m.dataset.scrollTokens.split(',').indexOf(scrollToken) !== -1) { node = m; break; } @@ -568,7 +575,7 @@ module.exports = React.createClass({ var boundingRect = node.getBoundingClientRect(); var scrollDelta = boundingRect.bottom + pixelOffset - wrapperRect.bottom; - debuglog("ScrollPanel: scrolling to token '" + node.dataset.scrollToken + "'+" + + debuglog("ScrollPanel: scrolling to token '" + scrollToken + "'+" + pixelOffset + " (delta: "+scrollDelta+")"); if(scrollDelta != 0) { @@ -591,12 +598,12 @@ module.exports = React.createClass({ for (var i = messages.length-1; i >= 0; --i) { var node = messages[i]; - if (!node.dataset.scrollToken) continue; + if (!node.dataset.scrollTokens) continue; var boundingRect = node.getBoundingClientRect(); newScrollState = { stuckAtBottom: false, - trackedScrollToken: node.dataset.scrollToken, + trackedScrollToken: node.dataset.scrollTokens.split(',')[0], pixelOffset: wrapperRect.bottom - boundingRect.bottom, }; // If the bottom of the panel intersects the ClientRect of node, use this node @@ -608,7 +615,7 @@ module.exports = React.createClass({ break; } } - // This is only false if there were no nodes with `node.dataset.scrollToken` set. + // This is only false if there were no nodes with `node.dataset.scrollTokens` set. if (newScrollState) { this.scrollState = newScrollState; debuglog("ScrollPanel: saved scroll state", this.scrollState); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index f07bad0052..7c89694a29 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -170,7 +170,7 @@ var TimelinePanel = React.createClass({ forwardPaginating: false, // cache of matrixClient.getSyncState() (but from the 'sync' event) - clientSyncState: null, + clientSyncState: MatrixClientPeg.get().getSyncState(), }; }, @@ -503,7 +503,9 @@ var TimelinePanel = React.createClass({ // This happens on user_activity_end which is delayed, and it's // very possible have logged out within that timeframe, so check // we still have a client. - if (!MatrixClientPeg.get()) return; + const cli = MatrixClientPeg.get(); + // if no client or client is guest don't send RR + if (!cli || cli.isGuest()) return; var currentReadUpToEventId = this._getCurrentReadReceipt(true); var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); @@ -766,6 +768,19 @@ var TimelinePanel = React.createClass({ return null; }, + canJumpToReadMarker: function() { + // 1. Do not show jump bar if neither the RM nor the RR are set. + // 2. Only show jump bar if RR !== RM. If they are the same, there are only fully + // read messages and unread messages. We already have a badge count and the bottom + // bar to jump to "live" when we have unread messages. + // 3. We want to show the bar if the read-marker is off the top of the screen. + // 4. Also, if pos === null, the event might not be paginated - show the unread bar + const pos = this.getReadMarkerPosition(); + return this.state.readMarkerEventId !== null && // 1. + this.state.readMarkerEventId !== this._getCurrentReadReceipt() && // 2. + (pos < 0 || pos === null); // 3., 4. + }, + /** * called by the parent component when PageUp/Down/etc is pressed. * diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 05410e866f..06103eae55 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -14,32 +14,40 @@ 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. */ -var React = require('react'); -var ReactDOM = require('react-dom'); -var sdk = require('../../index'); -var MatrixClientPeg = require("../../MatrixClientPeg"); -var PlatformPeg = require("../../PlatformPeg"); -var Modal = require('../../Modal'); -var dis = require("../../dispatcher"); -var q = require('q'); -var package_json = require('../../../package.json'); -var UserSettingsStore = require('../../UserSettingsStore'); -var CallMediaHandler = require('../../CallMediaHandler'); -var GeminiScrollbar = require('react-gemini-scrollbar'); -var Email = require('../../email'); -var AddThreepid = require('../../AddThreepid'); -var SdkConfig = require('../../SdkConfig'); +const React = require('react'); +const ReactDOM = require('react-dom'); +const sdk = require('../../index'); +const MatrixClientPeg = require("../../MatrixClientPeg"); +const PlatformPeg = require("../../PlatformPeg"); +const Modal = require('../../Modal'); +const dis = require("../../dispatcher"); +const q = require('q'); +const packageJson = require('../../../package.json'); +const UserSettingsStore = require('../../UserSettingsStore'); +const GeminiScrollbar = require('react-gemini-scrollbar'); +const Email = require('../../email'); +const AddThreepid = require('../../AddThreepid'); +const SdkConfig = require('../../SdkConfig'); import AccessibleButton from '../views/elements/AccessibleButton'; // if this looks like a release, use the 'version' from package.json; else use // the git sha. Prepend version with v, to look like riot-web version -const REACT_SDK_VERSION = 'dist' in package_json ? `v${package_json.version}` : package_json.gitHead || ''; +const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJson.gitHead || ''; // Simple method to help prettify GH Release Tags and Commit Hashes. -const GHVersionUrl = function(repo, token) { - const uriTail = (token.startsWith('v') && token.includes('.')) ? `releases/tag/${token}` : `commit/${token}`; - return `https://github.com/${repo}/${uriTail}`; -} +const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; +const gHVersionLabel = function(repo, token) { + const match = token.match(semVerRegex); + let url; + if (match && match[1]) { // basic semVer string possibly with commit hash + url = (match.length > 1 && match[2]) + ? `https://github.com/${repo}/commit/${match[2]}` + : `https://github.com/${repo}/releases/tag/v${match[1]}`; + } else { + url = `https://github.com/${repo}/commit/${token.split('-')[0]}`; + } + return {token}; +}; // Enumerate some simple 'flip a bit' UI settings (if any). // 'id' gives the key name in the im.vector.web.settings account data event @@ -51,7 +59,7 @@ const SETTINGS_LABELS = [ }, { id: 'hideReadReceipts', - label: 'Hide read receipts' + label: 'Hide read receipts', }, { id: 'dontSendTypingNotifications', @@ -107,7 +115,7 @@ const THEMES = [ id: 'theme', label: 'Dark theme', value: 'dark', - } + }, ]; module.exports = React.createClass({ @@ -193,7 +201,7 @@ module.exports = React.createClass({ }); this._refreshFromServer(); - var syncedSettings = UserSettingsStore.getSyncedSettings(); + const syncedSettings = UserSettingsStore.getSyncedSettings(); if (!syncedSettings.theme) { syncedSettings.theme = 'light'; } @@ -217,16 +225,16 @@ module.exports = React.createClass({ middleOpacity: 1.0, }); dis.unregister(this.dispatcherRef); - let cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomMember.membership", this._onInviteStateChange); } }, _refreshFromServer: function() { - var self = this; + const self = this; q.all([ - UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids() + UserSettingsStore.loadProfileInfo(), UserSettingsStore.loadThreePids(), ]).done(function(resps) { self.setState({ avatarUrl: resps[0].avatar_url, @@ -234,7 +242,7 @@ module.exports = React.createClass({ phase: "UserSettings.DISPLAY", }); }, function(error) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to load user settings: " + error); Modal.createDialog(ErrorDialog, { title: "Can't load user settings", @@ -251,7 +259,7 @@ module.exports = React.createClass({ onAvatarPickerClick: function(ev) { if (MatrixClientPeg.get().isGuest()) { - var NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); + const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog"); Modal.createDialog(NeedToRegisterDialog, { title: "Please Register", description: "Guests can't set avatars. Please register.", @@ -265,8 +273,8 @@ module.exports = React.createClass({ }, onAvatarSelected: function(ev) { - var self = this; - var changeAvatar = this.refs.changeAvatar; + const self = this; + const changeAvatar = this.refs.changeAvatar; if (!changeAvatar) { console.error("No ChangeAvatar found to upload image to!"); return; @@ -275,9 +283,9 @@ module.exports = React.createClass({ // dunno if the avatar changed, re-check it. self._refreshFromServer(); }, function(err) { - var errMsg = (typeof err === "string") ? err : (err.error || ""); + // const errMsg = (typeof err === "string") ? err : (err.error || ""); console.error("Failed to set avatar: " + err); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Failed to set avatar", description: ((err && err.message) ? err.message : "Operation failed"), @@ -286,7 +294,7 @@ module.exports = React.createClass({ }, onLogoutClicked: function(ev) { - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createDialog(QuestionDialog, { title: "Sign out?", description: @@ -301,7 +309,7 @@ module.exports = React.createClass({ + , ], onFinished: (confirmed) => { if (confirmed) { @@ -315,34 +323,33 @@ module.exports = React.createClass({ }, onPasswordChangeError: function(err) { - var errMsg = err.error || ""; + let errMsg = err.error || ""; if (err.httpStatus === 403) { errMsg = "Failed to change password. Is your password correct?"; - } - else if (err.httpStatus) { + } else if (err.httpStatus) { errMsg += ` (HTTP status ${err.httpStatus})`; } - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to change password: " + errMsg); Modal.createDialog(ErrorDialog, { title: "Error", - description: errMsg + description: errMsg, }); }, onPasswordChanged: function() { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Success", description: `Your password was successfully changed. You will not receive push notifications on other devices until you - log back in to them.` + log back in to them.`, }); }, onUpgradeClicked: function() { dis.dispatch({ - action: "start_upgrade_registration" + action: "start_upgrade_registration", }); }, @@ -356,11 +363,11 @@ module.exports = React.createClass({ }, _addEmail: function() { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var email_address = this.refs.add_email_input.value; - if (!Email.looksValid(email_address)) { + const emailAddress = this.refs.add_email_input.value; + if (!Email.looksValid(emailAddress)) { Modal.createDialog(ErrorDialog, { title: "Invalid Email Address", description: "This doesn't appear to be a valid email address", @@ -370,7 +377,7 @@ module.exports = React.createClass({ this._addThreepid = new AddThreepid(); // we always bind emails when registering, so let's do the // same here. - this._addThreepid.addEmailAddress(email_address, true).done(() => { + this._addThreepid.addEmailAddress(emailAddress, true).done(() => { Modal.createDialog(QuestionDialog, { title: "Verification Pending", description: "Please check your email and click on the link it contains. Once this is done, click continue.", @@ -379,7 +386,7 @@ module.exports = React.createClass({ }); }, (err) => { this.setState({email_add_pending: false}); - console.error("Unable to add email address " + email_address + " " + err); + console.error("Unable to add email address " + emailAddress + " " + err); Modal.createDialog(ErrorDialog, { title: "Unable to add email address", description: ((err && err.message) ? err.message : "Operation failed"), @@ -433,9 +440,9 @@ module.exports = React.createClass({ this.setState({email_add_pending: false}); }, (err) => { this.setState({email_add_pending: false}); - if (err.errcode == 'M_THREEPID_AUTH_FAILED') { - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var message = "Unable to verify email address. "; + if (err.errcode === 'M_THREEPID_AUTH_FAILED') { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + let message = "Unable to verify email address. "; message += "Please check your email and click on the link it contains. Once this is done, click continue."; Modal.createDialog(QuestionDialog, { title: "Verification Pending", @@ -444,7 +451,7 @@ module.exports = React.createClass({ onFinished: this.onEmailDialogFinished, }); } else { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to verify email address: " + err); Modal.createDialog(ErrorDialog, { title: "Unable to verify email address", @@ -484,17 +491,17 @@ module.exports = React.createClass({ _onRejectAllInvitesClicked: function(rooms, ev) { this.setState({ - rejectingInvites: true + rejectingInvites: true, }); // reject the invites - let promises = rooms.map((room) => { + const promises = rooms.map((room) => { return MatrixClientPeg.get().leave(room.roomId); }); // purposefully drop errors to the floor: we'll just have a non-zero number on the UI // after trying to reject all the invites. q.allSettled(promises).then(() => { this.setState({ - rejectingInvites: false + rejectingInvites: false, }); }).done(); }, @@ -507,7 +514,7 @@ module.exports = React.createClass({ }, "e2e-export"); }, { matrixClient: MatrixClientPeg.get(), - } + }, ); }, @@ -519,7 +526,7 @@ module.exports = React.createClass({ }, "e2e-export"); }, { matrixClient: MatrixClientPeg.get(), - } + }, ); }, @@ -545,8 +552,6 @@ module.exports = React.createClass({ }, _renderUserInterfaceSettings: function() { - var client = MatrixClientPeg.get(); - return (

    User Interface

    @@ -564,7 +569,7 @@ module.exports = React.createClass({ UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } + onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } />
  • +
  • {ret}
  • ); }, diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.js index 40995d2a72..a6f342af86 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.js @@ -19,6 +19,7 @@ limitations under the License. import React from 'react'; import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton'; +import sdk from '../../../index'; // cancel button which is shared between room header and simple room header export function CancelButton(props) { @@ -45,6 +46,9 @@ export default React.createClass({ // is the RightPanel collapsed? collapsedRhs: React.PropTypes.bool, + + // `src` to a TintableSvg. Optional. + icon: React.PropTypes.string, }, onShowRhsClick: function(ev) { @@ -53,9 +57,17 @@ export default React.createClass({ render: function() { let cancelButton; + let icon; if (this.props.onCancelClick) { cancelButton = ; } + if (this.props.icon) { + const TintableSvg = sdk.getComponent('elements.TintableSvg'); + icon = ; + } let showRhsButton; /* // don't bother cluttering things up with this for now. @@ -73,6 +85,7 @@ export default React.createClass({
    + { icon } { this.props.title } { showRhsButton } { cancelButton } diff --git a/test/components/structures/ScrollPanel-test.js b/test/components/structures/ScrollPanel-test.js index eacaeb5fb4..7ecb74be6f 100644 --- a/test/components/structures/ScrollPanel-test.js +++ b/test/components/structures/ScrollPanel-test.js @@ -115,7 +115,7 @@ var Tester = React.createClass({ // // there is an extra 50 pixels of margin at the bottom. return ( -
  • +
  • {key}