diff --git a/CHANGELOG.md b/CHANGELOG.md index 97dda666de..3b9ecdb325 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,154 @@ +Changes in [0.8.9](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9) (2017-05-22) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.9-rc.1...v0.8.9) + + * No changes + + +Changes in [0.8.9-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.9-rc.1) (2017-05-19) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8...v0.8.9-rc.1) + + * Prevent an exception getting scroll node + [\#902](https://github.com/matrix-org/matrix-react-sdk/pull/902) + * Fix a few remaining snags with country dd + [\#901](https://github.com/matrix-org/matrix-react-sdk/pull/901) + * Add left_aligned class to CountryDropdown + [\#900](https://github.com/matrix-org/matrix-react-sdk/pull/900) + * Swap to new flag files (which are stored as GB.png) + [\#899](https://github.com/matrix-org/matrix-react-sdk/pull/899) + * Improve phone number country dropdown for registration and login (Act. 2, + Return of the Prefix) + [\#897](https://github.com/matrix-org/matrix-react-sdk/pull/897) + * Support for pasting files into normal composer + [\#892](https://github.com/matrix-org/matrix-react-sdk/pull/892) + * tell guests they can't use filepanel until they register + [\#887](https://github.com/matrix-org/matrix-react-sdk/pull/887) + * Prevent reskindex -w from running when file names have not changed + [\#888](https://github.com/matrix-org/matrix-react-sdk/pull/888) + * I broke UserSettings for webpack-dev-server + [\#884](https://github.com/matrix-org/matrix-react-sdk/pull/884) + * various fixes to RoomHeader + [\#880](https://github.com/matrix-org/matrix-react-sdk/pull/880) + * remove /me whether or not it has a space after it + [\#885](https://github.com/matrix-org/matrix-react-sdk/pull/885) + * show error if we can't set a filter because no room + [\#883](https://github.com/matrix-org/matrix-react-sdk/pull/883) + * Fix RM not updating if RR event unpaginated + [\#874](https://github.com/matrix-org/matrix-react-sdk/pull/874) + * change roomsettings wording + [\#878](https://github.com/matrix-org/matrix-react-sdk/pull/878) + * make reskindex windows friendly + [\#875](https://github.com/matrix-org/matrix-react-sdk/pull/875) + * Fixes 2 issues with Dialog closing + [\#867](https://github.com/matrix-org/matrix-react-sdk/pull/867) + * Automatic Reskindex + [\#871](https://github.com/matrix-org/matrix-react-sdk/pull/871) + * Put room name in 'leave room' confirmation dialog + [\#873](https://github.com/matrix-org/matrix-react-sdk/pull/873) + * Fix this/self fail in LeftPanel + [\#872](https://github.com/matrix-org/matrix-react-sdk/pull/872) + * Don't show null URL previews + [\#870](https://github.com/matrix-org/matrix-react-sdk/pull/870) + * Fix keys for AddressSelector + [\#869](https://github.com/matrix-org/matrix-react-sdk/pull/869) + * Make left panel better for new users (mk II) + [\#859](https://github.com/matrix-org/matrix-react-sdk/pull/859) + * Explicitly save composer content onUnload + [\#866](https://github.com/matrix-org/matrix-react-sdk/pull/866) + * Warn on unload + [\#851](https://github.com/matrix-org/matrix-react-sdk/pull/851) + * Log deviceid at login + [\#862](https://github.com/matrix-org/matrix-react-sdk/pull/862) + * Guests can't send RR so no point trying + [\#860](https://github.com/matrix-org/matrix-react-sdk/pull/860) + * Remove babelcheck + [\#861](https://github.com/matrix-org/matrix-react-sdk/pull/861) + * T3chguy/settings versions improvements + [\#857](https://github.com/matrix-org/matrix-react-sdk/pull/857) + * Change max-len 90->120 + [\#852](https://github.com/matrix-org/matrix-react-sdk/pull/852) + * Remove DM-guessing code + [\#829](https://github.com/matrix-org/matrix-react-sdk/pull/829) + * Fix jumping to an unread event when in MELS + [\#855](https://github.com/matrix-org/matrix-react-sdk/pull/855) + * Validate phone number on login + [\#856](https://github.com/matrix-org/matrix-react-sdk/pull/856) + * Failed to enable HTML5 Notifications Error Dialogs + [\#827](https://github.com/matrix-org/matrix-react-sdk/pull/827) + * Pin filesize ver to fix break upstream + [\#854](https://github.com/matrix-org/matrix-react-sdk/pull/854) + * Improve RoomDirectory Look & Feel + [\#848](https://github.com/matrix-org/matrix-react-sdk/pull/848) + * Only show jumpToReadMarker bar when RM !== RR + [\#845](https://github.com/matrix-org/matrix-react-sdk/pull/845) + * Allow MELS to have its own RM + [\#846](https://github.com/matrix-org/matrix-react-sdk/pull/846) + * Use document.onkeydown instead of onkeypress + [\#844](https://github.com/matrix-org/matrix-react-sdk/pull/844) + * (Room)?Avatar: Request 96x96 avatars on high DPI screens + [\#808](https://github.com/matrix-org/matrix-react-sdk/pull/808) + * Add mx_EventTile_emote class + [\#842](https://github.com/matrix-org/matrix-react-sdk/pull/842) + * Fix dialog reappearing after hitting Enter + [\#841](https://github.com/matrix-org/matrix-react-sdk/pull/841) + * Fix spinner that shows until the first sync + [\#840](https://github.com/matrix-org/matrix-react-sdk/pull/840) + * Show spinner until first sync has completed + [\#839](https://github.com/matrix-org/matrix-react-sdk/pull/839) + * Style fixes for LoggedInView + [\#838](https://github.com/matrix-org/matrix-react-sdk/pull/838) + * Fix specifying custom server for registration + [\#834](https://github.com/matrix-org/matrix-react-sdk/pull/834) + * Improve country dropdown UX and expose +prefix + [\#833](https://github.com/matrix-org/matrix-react-sdk/pull/833) + * Fix user settings store + [\#836](https://github.com/matrix-org/matrix-react-sdk/pull/836) + * show the room name in the UDE Dialog + [\#832](https://github.com/matrix-org/matrix-react-sdk/pull/832) + * summarise profile changes in MELS + [\#826](https://github.com/matrix-org/matrix-react-sdk/pull/826) + * Transform h1 and h2 tags to h3 tags + [\#820](https://github.com/matrix-org/matrix-react-sdk/pull/820) + * limit our keyboard shortcut modifiers correctly + [\#825](https://github.com/matrix-org/matrix-react-sdk/pull/825) + * Specify cross platform regexes and add olm to noParse + [\#823](https://github.com/matrix-org/matrix-react-sdk/pull/823) + * Remember element that was in focus before rendering dialog + [\#822](https://github.com/matrix-org/matrix-react-sdk/pull/822) + * move user settings outward and use built in read receipts disabling + [\#824](https://github.com/matrix-org/matrix-react-sdk/pull/824) + * File Download Consistency + [\#802](https://github.com/matrix-org/matrix-react-sdk/pull/802) + * Show Access Token under Advanced in Settings + [\#806](https://github.com/matrix-org/matrix-react-sdk/pull/806) + * Link tags/commit hashes in the UserSettings version section + [\#810](https://github.com/matrix-org/matrix-react-sdk/pull/810) + * On return to RoomView from auxPanel, send focus back to Composer + [\#813](https://github.com/matrix-org/matrix-react-sdk/pull/813) + * Change presence status labels to 'for' instead of 'ago' + [\#817](https://github.com/matrix-org/matrix-react-sdk/pull/817) + * Disable Scalar Integrations if urls passed to it are falsey + [\#816](https://github.com/matrix-org/matrix-react-sdk/pull/816) + * Add option to hide other people's read receipts. + [\#818](https://github.com/matrix-org/matrix-react-sdk/pull/818) + * Add option to not send typing notifications + [\#819](https://github.com/matrix-org/matrix-react-sdk/pull/819) + * Sync RM across instances of Riot + [\#805](https://github.com/matrix-org/matrix-react-sdk/pull/805) + * First iteration on improving login UI + [\#811](https://github.com/matrix-org/matrix-react-sdk/pull/811) + * focus on composer after jumping to bottom + [\#809](https://github.com/matrix-org/matrix-react-sdk/pull/809) + * Improve RoomList performance via side-stepping React + [\#807](https://github.com/matrix-org/matrix-react-sdk/pull/807) + * Don't show link preview when link is inside of a quote + [\#762](https://github.com/matrix-org/matrix-react-sdk/pull/762) + * Escape closes UserSettings + [\#765](https://github.com/matrix-org/matrix-react-sdk/pull/765) + * Implement user power-level changes in timeline + [\#794](https://github.com/matrix-org/matrix-react-sdk/pull/794) + Changes in [0.8.8](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.8.8) (2017-04-25) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.8.8-rc.2...v0.8.8) diff --git a/code_style.md b/code_style.md index f0eca75ffc..2cac303e54 100644 --- a/code_style.md +++ b/code_style.md @@ -69,25 +69,41 @@ General Style console.log("I am a fish"); // Bad } ``` +- No new line before else, catch, finally, etc: + + ```javascript + if (x) { + console.log("I am a fish"); + } else { + console.log("I am a chimp"); // Good + } + + if (x) { + console.log("I am a fish"); + } + else { + console.log("I am a chimp"); // Bad + } + ``` - Declare one variable per var statement (consistent with Node). Unless they are simple and closely related. If you put the next declaration on a new line, treat yourself to another `var`: ```javascript - var key = "foo", + const key = "foo", comparator = function(x, y) { return x - y; }; // Bad - var key = "foo"; - var comparator = function(x, y) { + const key = "foo"; + const comparator = function(x, y) { return x - y; }; // Good - var x = 0, y = 0; // Fine + let x = 0, y = 0; // Fine - var x = 0; - var y = 0; // Also fine + let x = 0; + let y = 0; // Also fine ``` - A single line `if` is fine, all others have braces. This prevents errors when adding to the code.: diff --git a/package.json b/package.json index 21add8ccb7..059fdd390f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "0.8.8", + "version": "0.8.9", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -63,14 +63,13 @@ "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.3", "lodash": "^4.13.1", - "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "0.7.8", "optimist": "^0.6.1", - "prop-types": "^15.5.8", "q": "^1.4.1", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", "react-dom": "^15.4.0", - "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#39d858c", + "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.11.1", "text-encoding-utf-8": "^1.0.1", "velocity-vector": "vector-im/velocity#059e3b2", diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 1db22f9e10..833151a298 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -6,14 +6,22 @@ var args = require('optimist').argv; var chokidar = require('chokidar'); var componentIndex = path.join('src', 'component-index.js'); +var componentIndexTmp = componentIndex+".tmp"; var componentsDir = path.join('src', 'components'); var componentGlob = '**/*.js'; +var prevFiles = []; function reskindex() { + var files = glob.sync(componentGlob, {cwd: componentsDir}).sort(); + if (!filesHaveChanged(files, prevFiles)) { + return; + } + prevFiles = files; + var header = args.h || args.header; var packageJson = JSON.parse(fs.readFileSync('./package.json')); - var strm = fs.createWriteStream(componentIndex); + var strm = fs.createWriteStream(componentIndexTmp); if (header) { strm.write(fs.readFileSync(header)); @@ -28,16 +36,18 @@ function reskindex() { strm.write(" */\n\n"); if (packageJson['matrix-react-parent']) { + const parentIndex = packageJson['matrix-react-parent'] + + '/lib/component-index'; strm.write( - "module.exports.components = require('"+ - packageJson['matrix-react-parent']+ - "/lib/component-index').components;\n\n" - ); +`let components = require('${parentIndex}').components; +if (!components) { + throw new Error("'${parentIndex}' didn't export components"); +} +`); } else { - strm.write("module.exports.components = {};\n"); + strm.write("let components = {};\n"); } - var files = glob.sync(componentGlob, {cwd: componentsDir}).sort(); for (var i = 0; i < files.length; ++i) { var file = files[i].replace('.js', ''); @@ -45,13 +55,34 @@ function reskindex() { var importName = moduleName.replace(/\./g, "$"); strm.write("import " + importName + " from './components/" + file + "';\n"); - strm.write(importName + " && (module.exports.components['"+moduleName+"'] = " + importName + ");"); + strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");"); strm.write('\n'); strm.uncork(); } + strm.write("export {components};\n"); strm.end(); - console.log('Reskindex: completed'); + fs.rename(componentIndexTmp, componentIndex, function(err) { + if(err) { + console.error("Error moving new index into place: " + err); + } else { + console.log('Reskindex: completed'); + } + }); +} + +// Expects both arrays of file names to be sorted +function filesHaveChanged(files, prevFiles) { + if (files.length !== prevFiles.length) { + return true; + } + // Check for name changes + for (var i = 0; i < files.length; i++) { + if (prevFiles[i] !== files[i]) { + return true; + } + } + return false; } // -w indicates watch mode where any FS events will trigger reskindex diff --git a/src/ConstantTimeDispatcher.js b/src/ConstantTimeDispatcher.js deleted file mode 100644 index 6c2c3266aa..0000000000 --- a/src/ConstantTimeDispatcher.js +++ /dev/null @@ -1,62 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// singleton which dispatches invocations of a given type & argument -// rather than just a type (as per EventEmitter and Flux's dispatcher etc) -// -// This means you can have a single point which listens for an EventEmitter event -// and then dispatches out to one of thousands of RoomTiles (for instance) rather than -// having each RoomTile register for the EventEmitter event and having to -// iterate over all of them. -class ConstantTimeDispatcher { - constructor() { - // type -> arg -> [ listener(arg, params) ] - this.listeners = {}; - } - - register(type, arg, listener) { - if (!this.listeners[type]) this.listeners[type] = {}; - if (!this.listeners[type][arg]) this.listeners[type][arg] = []; - this.listeners[type][arg].push(listener); - } - - unregister(type, arg, listener) { - if (this.listeners[type] && this.listeners[type][arg]) { - var i = this.listeners[type][arg].indexOf(listener); - if (i > -1) { - this.listeners[type][arg].splice(i, 1); - } - } - else { - console.warn("Unregistering unrecognised listener (type=" + type + ", arg=" + arg + ")"); - } - } - - dispatch(type, arg, params) { - if (!this.listeners[type] || !this.listeners[type][arg]) { - //console.warn("No registered listeners for dispatch (type=" + type + ", arg=" + arg + ")"); - return; - } - this.listeners[type][arg].forEach(listener=>{ - listener.call(arg, params); - }); - } -} - -if (!global.constantTimeDispatcher) { - global.constantTimeDispatcher = new ConstantTimeDispatcher(); -} -module.exports = global.constantTimeDispatcher; diff --git a/src/DateUtils.js b/src/DateUtils.js index 07bab4ae7b..c58c09d4de 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -19,13 +19,14 @@ limitations under the License. var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +function pad(n) { + return (n < 10 ? '0' : '') + n; +} + module.exports = { formatDate: function(date) { // date.toLocaleTimeString is completely system dependent. // just go 24h for now - function pad(n) { - return (n < 10 ? '0' : '') + n; - } var now = new Date(); if (date.toDateString() === now.toDateString()) { @@ -34,19 +35,20 @@ module.exports = { else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); } - else /* if (now.getFullYear() === date.getFullYear()) */ { + else if (now.getFullYear() === date.getFullYear()) { return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); } - /* else { - return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); + return this.formatFullDate(date); } - */ + }, + + formatFullDate: function(date) { + return days[date.getDay()] + ", " + months[date.getMonth()] + " " + date.getDate() + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes()); }, formatTime: function(date) { - //return pad(date.getHours()) + ':' + pad(date.getMinutes()); - return ('00' + date.getHours()).slice(-2) + ':' + ('00' + date.getMinutes()).slice(-2); + return pad(date.getHours()) + ':' + pad(date.getMinutes()); } }; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index a31601790f..4acb314c2f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -148,17 +148,18 @@ var sanitizeHtmlParams = { attribs.href = m[1]; delete attribs.target; } - - m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); - if (m) { - var entity = m[1]; - if (entity[0] === '@') { - attribs.href = '#/user/' + entity; + else { + m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN); + if (m) { + var entity = m[1]; + if (entity[0] === '@') { + attribs.href = '#/user/' + entity; + } + else if (entity[0] === '#' || entity[0] === '!') { + attribs.href = '#/room/' + entity; + } + delete attribs.target; } - else if (entity[0] === '#' || entity[0] === '!') { - attribs.href = '#/room/' + entity; - } - delete attribs.target; } } attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ diff --git a/src/KeyCode.js b/src/KeyCode.js index f164dbc15c..c9cac01239 100644 --- a/src/KeyCode.js +++ b/src/KeyCode.js @@ -32,5 +32,4 @@ module.exports = { DELETE: 46, KEY_D: 68, KEY_E: 69, - KEY_K: 75, }; diff --git a/src/Unread.js b/src/Unread.js index d7490c8632..67166dc24f 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -25,7 +25,9 @@ module.exports = { eventTriggersUnreadCount: function(ev) { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { return false; - } else if (ev.getType() == "m.room.member") { + } else if (ev.getType() == 'm.room.member') { + return false; + } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { return false; } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { return false; diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index fc4cbd9423..d83b6b5564 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -59,6 +59,8 @@ var FilePanel = React.createClass({ var client = MatrixClientPeg.get(); var room = client.getRoom(roomId); + this.noRoom = !room; + if (room) { var filter = new Matrix.Filter(client.credentials.userId); filter.setDefinition( @@ -82,13 +84,22 @@ var FilePanel = React.createClass({ console.error("Failed to get or create file panel filter", error); } ); - } - else { + } else { console.error("Failed to add filtered timelineSet for FilePanel as no room!"); } }, render: function() { + if (MatrixClientPeg.get().isGuest()) { + return
+
You must register to use this functionality
+
; + } else if (this.noRoom) { + return
+
You must join the room to see its files
+
; + } + // wrap a TimelinePanel with the jump-to-event bits turned off. var TimelinePanel = sdk.getComponent("structures.TimelinePanel"); var Loader = sdk.getComponent("elements.Spinner"); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index a64ae0a25c..e559a21e1a 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -127,18 +127,6 @@ export default React.createClass({ var handled = false; switch (ev.keyCode) { - case KeyCode.ESCAPE: - - // Implemented this way so possible handling for other pages is neater - switch (this.props.page_type) { - case PageTypes.UserSettings: - this.props.onUserSettingsClose(); - handled = true; - break; - } - - break; - case KeyCode.UP: case KeyCode.DOWN: if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ee7236bb15..85c12979f6 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -17,27 +17,24 @@ limitations under the License. import q from 'q'; -var React = require('react'); -var Matrix = require("matrix-js-sdk"); +import React from 'react'; +import Matrix from "matrix-js-sdk"; -var MatrixClientPeg = require("../../MatrixClientPeg"); -var PlatformPeg = require("../../PlatformPeg"); -var SdkConfig = require("../../SdkConfig"); -var ContextualMenu = require("./ContextualMenu"); -var RoomListSorter = require("../../RoomListSorter"); -var UserActivity = require("../../UserActivity"); -var Presence = require("../../Presence"); -var dis = require("../../dispatcher"); +import MatrixClientPeg from "../../MatrixClientPeg"; +import PlatformPeg from "../../PlatformPeg"; +import SdkConfig from "../../SdkConfig"; +import * as RoomListSorter from "../../RoomListSorter"; +import dis from "../../dispatcher"; -var Modal = require("../../Modal"); -var Tinter = require("../../Tinter"); -var sdk = require('../../index'); -var Rooms = require('../../Rooms'); -var linkifyMatrix = require("../../linkify-matrix"); -var Lifecycle = require('../../Lifecycle'); -var PageTypes = require('../../PageTypes'); +import Modal from "../../Modal"; +import Tinter from "../../Tinter"; +import sdk from '../../index'; +import * as Rooms from '../../Rooms'; +import linkifyMatrix from "../../linkify-matrix"; +import * as Lifecycle from '../../Lifecycle'; +import PageTypes from '../../PageTypes'; -var createRoom = require("../../createRoom"); +import createRoom from "../../createRoom"; import * as UDEHandler from '../../UnknownDeviceErrorHandler'; module.exports = React.createClass({ @@ -89,7 +86,7 @@ module.exports = React.createClass({ }, getInitialState: function() { - var s = { + const s = { loading: true, screen: undefined, screenAfterLogin: this.props.initialScreenAfterLogin, @@ -156,11 +153,9 @@ module.exports = React.createClass({ return this.state.register_hs_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getHomeserverUrl(); - } - else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) { + } else if (window.localStorage && window.localStorage.getItem("mx_hs_url")) { return window.localStorage.getItem("mx_hs_url"); - } - else { + } else { return this.getDefaultHsUrl(); } }, @@ -178,11 +173,9 @@ module.exports = React.createClass({ return this.state.register_is_url; } else if (MatrixClientPeg.get()) { return MatrixClientPeg.get().getIdentityServerUrl(); - } - else if (window.localStorage && window.localStorage.getItem("mx_is_url")) { + } else if (window.localStorage && window.localStorage.getItem("mx_is_url")) { return window.localStorage.getItem("mx_is_url"); - } - else { + } else { return this.getDefaultIsUrl(); } }, @@ -324,28 +317,14 @@ module.exports = React.createClass({ onAction: function(payload) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - var roomIndexDelta = 1; + const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - var self = this; switch (payload.action) { case 'logout': Lifecycle.logout(); break; case 'start_registration': - const params = payload.params || {}; - this.setStateForNewScreen({ - screen: 'register', - // these params may be undefined, but if they are, - // unset them from our state: we don't want to - // resume a previous registration session if the - // user just clicked 'register' - register_client_secret: params.client_secret, - register_session_id: params.session_id, - register_hs_url: params.hs_url, - register_is_url: params.is_url, - register_id_sid: params.sid, - }); - this.notifyNewScreen('register'); + this._startRegistration(payload.params || {}); break; case 'start_login': if (MatrixClientPeg.get() && @@ -362,7 +341,7 @@ module.exports = React.createClass({ break; case 'start_post_registration': this.setState({ // don't clobber loggedIn status - screen: 'post_registration' + screen: 'post_registration', }); break; case 'start_upgrade_registration': @@ -392,34 +371,7 @@ module.exports = React.createClass({ this.notifyNewScreen('forgot_password'); break; case 'leave_room': - const roomToLeave = MatrixClientPeg.get().getRoom(payload.room_id); - Modal.createDialog(QuestionDialog, { - title: "Leave room", - description: Are you sure you want to leave the room {roomToLeave.name}?, - onFinished: (should_leave) => { - if (should_leave) { - const d = MatrixClientPeg.get().leave(payload.room_id); - - // FIXME: controller shouldn't be loading a view :( - const Loader = sdk.getComponent("elements.Spinner"); - const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); - - d.then(() => { - modal.close(); - if (this.currentRoomId === payload.room_id) { - dis.dispatch({action: 'view_next_room'}); - } - }, (err) => { - modal.close(); - console.error("Failed to leave room " + payload.room_id + " " + err); - Modal.createDialog(ErrorDialog, { - title: "Failed to leave room", - description: (err && err.message ? err.message : "Server may be unavailable, overloaded, or you hit a bug."), - }); - }); - } - } - }); + this._leaveRoom(payload.room_id); break; case 'reject_invite': Modal.createDialog(QuestionDialog, { @@ -440,11 +392,11 @@ module.exports = React.createClass({ modal.close(); Modal.createDialog(ErrorDialog, { title: "Failed to reject invitation", - description: err.toString() + description: err.toString(), }); }); } - } + }, }); break; case 'view_user': @@ -469,30 +421,13 @@ module.exports = React.createClass({ this._viewRoom(payload); break; case 'view_prev_room': - roomIndexDelta = -1; + this._viewNextRoom(-1); + break; case 'view_next_room': - var allRooms = RoomListSorter.mostRecentActivityFirst( - MatrixClientPeg.get().getRooms() - ); - var roomIndex = -1; - for (var i = 0; i < allRooms.length; ++i) { - if (allRooms[i].roomId == this.state.currentRoomId) { - roomIndex = i; - break; - } - } - roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; - if (roomIndex < 0) roomIndex = allRooms.length - 1; - this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + this._viewNextRoom(1); break; case 'view_indexed_room': - var allRooms = RoomListSorter.mostRecentActivityFirst( - MatrixClientPeg.get().getRooms() - ); - var roomIndex = payload.roomIndex; - if (allRooms[roomIndex]) { - this._viewRoom({ room_id: allRooms[roomIndex].roomId }); - } + this._viewIndexedRoom(payload.roomIndex); break; case 'view_user_settings': this._setPage(PageTypes.UserSettings); @@ -589,7 +524,7 @@ module.exports = React.createClass({ case 'new_version': this.onVersion( payload.currentVersion, payload.newVersion, - payload.releaseNotes + payload.releaseNotes, ); break; } @@ -601,6 +536,47 @@ module.exports = React.createClass({ }); }, + _startRegistration: function(params) { + this.setStateForNewScreen({ + screen: 'register', + // these params may be undefined, but if they are, + // unset them from our state: we don't want to + // resume a previous registration session if the + // user just clicked 'register' + register_client_secret: params.client_secret, + register_session_id: params.session_id, + register_hs_url: params.hs_url, + register_is_url: params.is_url, + register_id_sid: params.sid, + }); + this.notifyNewScreen('register'); + }, + + _viewNextRoom: function(roomIndexDelta) { + const allRooms = RoomListSorter.mostRecentActivityFirst( + MatrixClientPeg.get().getRooms(), + ); + let roomIndex = -1; + for (let i = 0; i < allRooms.length; ++i) { + if (allRooms[i].roomId == this.state.currentRoomId) { + roomIndex = i; + break; + } + } + roomIndex = (roomIndex + roomIndexDelta) % allRooms.length; + if (roomIndex < 0) roomIndex = allRooms.length - 1; + this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + }, + + _viewIndexedRoom: function(roomIndex) { + const allRooms = RoomListSorter.mostRecentActivityFirst( + MatrixClientPeg.get().getRooms(), + ); + if (allRooms[roomIndex]) { + this._viewRoom({ room_id: allRooms[roomIndex].roomId }); + } + }, + // switch view to the given room // // @param {Object} room_info Object containing data about the room to be joined @@ -620,7 +596,7 @@ module.exports = React.createClass({ _viewRoom: function(room_info) { this.focusComposer = true; - var newState = { + const newState = { initialEventId: room_info.event_id, highlightedEventId: room_info.event_id, initialEventPixelOffset: undefined, @@ -640,7 +616,7 @@ module.exports = React.createClass({ // // TODO: do this in RoomView rather than here if (!room_info.event_id && this.refs.loggedInView) { - var scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id); + const scrollState = this.refs.loggedInView.getScrollStateForRoom(room_info.room_id); if (scrollState) { newState.initialEventId = scrollState.focussedEvent; newState.initialEventPixelOffset = scrollState.pixelOffset; @@ -710,7 +686,7 @@ module.exports = React.createClass({ }, _invite: function(roomId) { - var ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); + const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); Modal.createDialog(ChatInviteDialog, { title: "Invite new room members", button: "Send Invites", @@ -719,6 +695,41 @@ module.exports = React.createClass({ }); }, + _leaveRoom: function(roomId) { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + + const roomToLeave = MatrixClientPeg.get().getRoom(roomId); + Modal.createDialog(QuestionDialog, { + title: "Leave room", + description: Are you sure you want to leave the room {roomToLeave.name}?, + onFinished: (shouldLeave) => { + if (shouldLeave) { + const d = MatrixClientPeg.get().leave(roomId); + + // FIXME: controller shouldn't be loading a view :( + const Loader = sdk.getComponent("elements.Spinner"); + const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + + d.then(() => { + modal.close(); + if (this.currentRoomId === roomId) { + dis.dispatch({action: 'view_next_room'}); + } + }, (err) => { + modal.close(); + console.error("Failed to leave room " + roomId + " " + err); + Modal.createDialog(ErrorDialog, { + title: "Failed to leave room", + description: (err && err.message ? err.message : + "Server may be unavailable, overloaded, or you hit a bug."), + }); + }); + } + }, + }); + }, + /** * Called when the sessionloader has finished */ @@ -737,6 +748,8 @@ module.exports = React.createClass({ /** * Called whenever someone changes the theme + * + * @param {string} theme new theme */ _onSetTheme: function(theme) { if (!theme) { @@ -745,12 +758,12 @@ module.exports = React.createClass({ // look for the stylesheet elements. // styleElements is a map from style name to HTMLLinkElement. - var styleElements = Object.create(null); - var i, a; - for (i = 0; (a = document.getElementsByTagName("link")[i]); i++) { - var href = a.getAttribute("href"); + const styleElements = Object.create(null); + let a; + for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) { + const href = a.getAttribute("href"); // shouldn't we be using the 'title' tag rather than the href? - var match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); + const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/); if (match) { styleElements[match[1]] = a; } @@ -773,14 +786,15 @@ module.exports = React.createClass({ // abuse the tinter to change all the SVG's #fff to #2d2d2d // XXX: obviously this shouldn't be hardcoded here. Tinter.tintSvgWhite('#2d2d2d'); - } - else { + } else { Tinter.tintSvgWhite('#ffffff'); } }, /** * Called when a new logged in session has started + * + * @param {string} teamToken */ _onLoggedIn: function(teamToken) { this.setState({ @@ -811,7 +825,7 @@ module.exports = React.createClass({ if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { this.showScreen( this.state.screenAfterLogin.screen, - this.state.screenAfterLogin.params + this.state.screenAfterLogin.params, ); this.notifyNewScreen(this.state.screenAfterLogin.screen); this.setState({screenAfterLogin: null}); @@ -852,8 +866,8 @@ module.exports = React.createClass({ * (useful for setting listeners) */ _onWillStartClient() { - var self = this; - var cli = MatrixClientPeg.get(); + const self = this; + const cli = MatrixClientPeg.get(); // Allow the JS SDK to reap timeline events. This reduces the amount of // memory consumed as the JS SDK stores multiple distinct copies of room @@ -894,17 +908,17 @@ module.exports = React.createClass({ cli.on('Call.incoming', function(call) { dis.dispatch({ action: 'incoming_call', - call: call + call: call, }); }); cli.on('Session.logged_out', function(call) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createDialog(ErrorDialog, { title: "Signed Out", - description: "For security, this session has been signed out. Please sign in again." + description: "For security, this session has been signed out. Please sign in again.", }); dis.dispatch({ - action: 'logout' + action: 'logout', }); }); cli.on("accountData", function(ev) { @@ -927,17 +941,17 @@ module.exports = React.createClass({ if (screen == 'register') { dis.dispatch({ action: 'start_registration', - params: params + params: params, }); } else if (screen == 'login') { dis.dispatch({ action: 'start_login', - params: params + params: params, }); } else if (screen == 'forgot_password') { dis.dispatch({ action: 'start_password_recovery', - params: params + params: params, }); } else if (screen == 'new') { dis.dispatch({ @@ -960,26 +974,26 @@ module.exports = React.createClass({ action: 'start_post_registration', }); } else if (screen.indexOf('room/') == 0) { - var segments = screen.substring(5).split('/'); - var roomString = segments[0]; - var eventId = segments[1]; // undefined if no event id given + const segments = screen.substring(5).split('/'); + const roomString = segments[0]; + const eventId = segments[1]; // undefined if no event id given // FIXME: sort_out caseConsistency - var third_party_invite = { + const thirdPartyInvite = { inviteSignUrl: params.signurl, invitedEmail: params.email, }; - var oob_data = { + const oobData = { name: params.room_name, avatarUrl: params.room_avatar_url, inviterName: params.inviter_name, }; - var payload = { + const payload = { action: 'view_room', event_id: eventId, - third_party_invite: third_party_invite, - oob_data: oob_data, + third_party_invite: thirdPartyInvite, + oob_data: oobData, }; if (roomString[0] == '#') { payload.room_alias = roomString; @@ -993,19 +1007,18 @@ module.exports = React.createClass({ dis.dispatch(payload); } } else if (screen.indexOf('user/') == 0) { - var userId = screen.substring(5); + const userId = screen.substring(5); this.setState({ viewUserId: userId }); this._setPage(PageTypes.UserView); this.notifyNewScreen('user/' + userId); - var member = new Matrix.RoomMember(null, userId); + const member = new Matrix.RoomMember(null, userId); if (member) { dis.dispatch({ action: 'view_user', member: member, }); } - } - else { + } else { console.info("Ignoring showScreen for '%s'", screen); } }, @@ -1024,7 +1037,7 @@ module.exports = React.createClass({ onUserClick: function(event, userId) { event.preventDefault(); - var member = new Matrix.RoomMember(null, userId); + const member = new Matrix.RoomMember(null, userId); if (!member) { return; } dis.dispatch({ action: 'view_user', @@ -1034,17 +1047,17 @@ module.exports = React.createClass({ onLogoutClick: function(event) { dis.dispatch({ - action: 'logout' + action: 'logout', }); event.stopPropagation(); event.preventDefault(); }, handleResize: function(e) { - var hideLhsThreshold = 1000; - var showLhsThreshold = 1000; - var hideRhsThreshold = 820; - var showRhsThreshold = 820; + const hideLhsThreshold = 1000; + const showLhsThreshold = 1000; + const hideRhsThreshold = 820; + const showRhsThreshold = 820; if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { dis.dispatch({ action: 'hide_left_panel' }); @@ -1062,10 +1075,10 @@ module.exports = React.createClass({ this.setState({width: window.innerWidth}); }, - onRoomCreated: function(room_id) { + onRoomCreated: function(roomId) { dis.dispatch({ action: "view_room", - room_id: room_id, + room_id: roomId, }); }, @@ -1099,7 +1112,7 @@ module.exports = React.createClass({ onFinishPostRegistration: function() { // Don't confuse this with "PageType" which is the middle window to show this.setState({ - screen: undefined + screen: undefined, }); this.showScreen("settings"); }, @@ -1114,10 +1127,10 @@ module.exports = React.createClass({ }, updateStatusIndicator: function(state, prevState) { - var notifCount = 0; + let notifCount = 0; - var rooms = MatrixClientPeg.get().getRooms(); - for (var i = 0; i < rooms.length; ++i) { + const rooms = MatrixClientPeg.get().getRooms(); + for (let i = 0; i < rooms.length; ++i) { if (rooms[i].hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite')) { notifCount++; } else if (rooms[i].getUnreadNotificationCount()) { @@ -1144,19 +1157,18 @@ module.exports = React.createClass({ action: 'view_room', room_id: this.state.currentRoomId, }); - } - else { + } else { dis.dispatch({ action: 'view_room_directory', }); } }, - onRoomIdResolved: function(room_id) { + onRoomIdResolved: function(roomId) { // It's the RoomView's resposibility to look up room aliases, but we need the // ID to pass into things like the Member List, so the Room View tells us when // its done that resolution so we can display things that take a room ID. - this.setState({currentRoomId: room_id}); + this.setState({currentRoomId: roomId}); }, _makeRegistrationUrl: function(params) { @@ -1179,14 +1191,20 @@ module.exports = React.createClass({ ); } + // needs to be before normal PageTypes as you are logged in technically - else if (this.state.screen == 'post_registration') { + if (this.state.screen == 'post_registration') { const PostRegistration = sdk.getComponent('structures.login.PostRegistration'); return ( ); - } else if (this.state.loggedIn && this.state.ready) { + } + + // `ready` and `loggedIn` may be set before `page_type` (because the + // latter is set via the dispatcher). If we don't yet have a `page_type`, + // keep showing the spinner for now. + if (this.state.loggedIn && this.state.ready && this.state.page_type) { /* for now, we stuff the entirety of our props and state into the LoggedInView. * we should go through and figure out what we actually need to pass down, as well * as using something like redux to avoid having a billion bits of state kicking around. @@ -1269,5 +1287,5 @@ module.exports = React.createClass({ /> ); } - } + }, }); diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 92049bb113..55cefc89af 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1760,6 +1760,7 @@ module.exports = React.createClass({ oobData={this.props.oobData} editing={this.state.editingRoomSettings} saving={this.state.uploadingRoomSettings} + inRoom={myMember && myMember.membership === 'join'} collapsedRhs={ this.props.collapsedRhs } onSearchClick={this.onSearchClick} onSettingsClick={this.onSettingsClick} diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 7c89694a29..8794713501 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -177,8 +177,8 @@ var TimelinePanel = React.createClass({ componentWillMount: function() { debuglog("TimelinePanel: mounting"); - this.last_rr_sent_event_id = undefined; - this.last_rm_sent_event_id = undefined; + this.lastRRSentEventId = undefined; + this.lastRMSentEventId = undefined; this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); @@ -504,12 +504,13 @@ var TimelinePanel = React.createClass({ // very possible have logged out within that timeframe, so check // we still have a client. const cli = MatrixClientPeg.get(); - // if no client or client is guest don't send RR + // if no client or client is guest don't send RR or RM if (!cli || cli.isGuest()) return; - var currentReadUpToEventId = this._getCurrentReadReceipt(true); - var currentReadUpToEventIndex = this._indexForEventId(currentReadUpToEventId); + let shouldSendRR = true; + const currentRREventId = this._getCurrentReadReceipt(true); + const currentRREventIndex = this._indexForEventId(currentRREventId); // We want to avoid sending out read receipts when we are looking at // events in the past which are before the latest RR. // @@ -523,43 +524,60 @@ var TimelinePanel = React.createClass({ // RRs) - but that is a bit of a niche case. It will sort itself out when // the user eventually hits the live timeline. // - if (currentReadUpToEventId && currentReadUpToEventIndex === null && + if (currentRREventId && currentRREventIndex === null && this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - return; + shouldSendRR = false; } - var lastReadEventIndex = this._getLastDisplayedEventIndex({ - ignoreOwn: true + const lastReadEventIndex = this._getLastDisplayedEventIndex({ + ignoreOwn: true, }); - if (lastReadEventIndex === null) return; + if (lastReadEventIndex === null) { + shouldSendRR = false; + } + let lastReadEvent = this.state.events[lastReadEventIndex]; + shouldSendRR = shouldSendRR && + // Only send a RR if the last read event is ahead in the timeline relative to + // the current RR event. + lastReadEventIndex > currentRREventIndex && + // Only send a RR if the last RR set != the one we would send + this.lastRRSentEventId != lastReadEvent.getId(); - var lastReadEvent = this.state.events[lastReadEventIndex]; + // Only send a RM if the last RM sent != the one we would send + const shouldSendRM = + this.lastRMSentEventId != this.state.readMarkerEventId; // we also remember the last read receipt we sent to avoid spamming the // same one at the server repeatedly - if ((lastReadEventIndex > currentReadUpToEventIndex && - this.last_rr_sent_event_id != lastReadEvent.getId()) || - this.last_rm_sent_event_id != this.state.readMarkerEventId) { - - this.last_rr_sent_event_id = lastReadEvent.getId(); - this.last_rm_sent_event_id = this.state.readMarkerEventId; + if (shouldSendRR || shouldSendRM) { + if (shouldSendRR) { + this.lastRRSentEventId = lastReadEvent.getId(); + } else { + lastReadEvent = null; + } + this.lastRMSentEventId = this.state.readMarkerEventId; + debuglog('TimelinePanel: Sending Read Markers for ', + this.props.timelineSet.room.roomId, + 'rm', this.state.readMarkerEventId, + lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', + ); MatrixClientPeg.get().setRoomReadMarkers( this.props.timelineSet.room.roomId, this.state.readMarkerEventId, - lastReadEvent + lastReadEvent, // Could be null, in which case no RR is sent ).catch((e) => { // /read_markers API is not implemented on this HS, fallback to just RR - if (e.errcode === 'M_UNRECOGNIZED') { + if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) { return MatrixClientPeg.get().sendReadReceipt( - lastReadEvent + lastReadEvent, ).catch(() => { - this.last_rr_sent_event_id = undefined; + this.lastRRSentEventId = undefined; }); } // it failed, so allow retries next time the user is active - this.last_rr_sent_event_id = undefined; - this.last_rm_sent_event_id = undefined; + this.lastRRSentEventId = undefined; + this.lastRMSentEventId = undefined; }); // do a quick-reset of our unreadNotificationCount to avoid having @@ -572,7 +590,6 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', - room: this.props.timelineSet.room, }); } } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 998199b598..ededd8de8b 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -29,6 +29,7 @@ const Email = require('../../email'); const AddThreepid = require('../../AddThreepid'); const SdkConfig = require('../../SdkConfig'); import AccessibleButton from '../views/elements/AccessibleButton'; +import * as FormattingUtils from '../../utils/FormattingUtils'; // 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 @@ -36,7 +37,7 @@ const REACT_SDK_VERSION = 'dist' in packageJson ? packageJson.version : packageJ // Simple method to help prettify GH Release Tags and Commit Hashes. const semVerRegex = /^v?(\d+\.\d+\.\d+(?:-rc.+)?)(?:-(?:\d+-g)?([0-9a-fA-F]+))?(?:-dirty)?$/i; -const gHVersionLabel = function(repo, token) { +const gHVersionLabel = function(repo, token='') { const match = token.match(semVerRegex); let url; if (match && match[1]) { // basic semVer string possibly with commit hash @@ -151,10 +152,10 @@ module.exports = React.createClass({ getInitialState: function() { return { avatarUrl: null, - threePids: [], + threepids: [], phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, - vectorVersion: null, + vectorVersion: undefined, rejectingInvites: false, }; }, @@ -600,7 +601,12 @@ module.exports = React.createClass({ _renderCryptoInfo: function() { const client = MatrixClientPeg.get(); const deviceId = client.deviceId; - const identityKey = client.getDeviceEd25519Key() || ""; + let identityKey = client.getDeviceEd25519Key(); + if (!identityKey) { + identityKey = ""; + } else { + identityKey = FormattingUtils.formatCryptoKey(identityKey); + } let importExportButtons = null; @@ -848,6 +854,7 @@ module.exports = React.createClass({ addEmailSection = (
+
- riot-web version: {(this.state.vectorVersion !== null) + riot-web version: {(this.state.vectorVersion !== undefined) ? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion) : 'unknown' }
diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 02460148b3..0b2ca5225d 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -47,35 +47,18 @@ export default React.createClass({ children: React.PropTypes.node, }, - componentWillMount: function() { - this.priorActiveElement = document.activeElement; - }, - - componentWillUnmount: function() { - if (this.priorActiveElement !== null) { - this.priorActiveElement.focus(); - } - }, - - // Don't let key{down,press} events escape the modal. Consume them all. - _eatKeyEvent: function(e) { - e.stopPropagation(); - }, - - // Must be when the key is released (and not pressed) otherwise componentWillUnmount - // will focus another element which will receive future key events - _onKeyUp: function(e) { + _onKeyDown: function(e) { if (e.keyCode === KeyCode.ESCAPE) { + e.stopPropagation(); e.preventDefault(); this.props.onFinished(); } else if (e.keyCode === KeyCode.ENTER) { if (this.props.onEnterPressed) { + e.stopPropagation(); e.preventDefault(); this.props.onEnterPressed(e); } } - // Consume all keyup events while Modal is open - e.stopPropagation(); }, _onCancelClick: function(e) { @@ -84,13 +67,9 @@ export default React.createClass({ render: function() { const TintableSvg = sdk.getComponent("elements.TintableSvg"); - + return ( -
+
diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js new file mode 100644 index 0000000000..f9feb718b0 --- /dev/null +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -0,0 +1,76 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; +import * as FormattingUtils from '../../../utils/FormattingUtils'; + +export default function DeviceVerifyDialog(props) { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + const key = FormattingUtils.formatCryptoKey(props.device.getFingerprint()); + const body = ( +
+

+ To verify that this device can be trusted, please contact its + owner using some other means (e.g. in person or a phone call) + and ask them whether the key they see in their User Settings + for this device matches the key below: +

+
+
    +
  • { props.device.getDisplayName() }
  • +
  • { props.device.deviceId}
  • +
  • { key }
  • +
+
+

+ If it matches, press the verify button below. + If it doesnt, then someone else is intercepting this device + and you probably want to press the blacklist button instead. +

+

+ In future this verification process will be more sophisticated. +

+
+ ); + + function onFinished(confirm) { + if (confirm) { + MatrixClientPeg.get().setDeviceVerified( + props.userId, props.device.deviceId, true, + ); + } + props.onFinished(confirm); + } + + return ( + + ); +} + +DeviceVerifyDialog.propTypes = { + userId: React.PropTypes.string.isRequired, + device: React.PropTypes.object.isRequired, + onFinished: React.PropTypes.func.isRequired, +}; diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 8e20b0d2bc..6012541b94 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -47,12 +47,6 @@ export default React.createClass({ this.props.onFinished(false); }, - componentDidMount: function() { - if (this.props.focus) { - this.refs.button.focus(); - } - }, - render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const cancelButton = this.props.hasCancelButton ? ( @@ -69,7 +63,7 @@ export default React.createClass({ {this.props.description}
- {this.props.extraButtons} diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js deleted file mode 100644 index 267388daf6..0000000000 --- a/src/components/views/elements/ActionButton.js +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import AccessibleButton from './AccessibleButton'; -import dis from '../../../dispatcher'; -import sdk from '../../../index'; - -export default React.createClass({ - displayName: 'RoleButton', - - propTypes: { - size: PropTypes.string, - tooltip: PropTypes.bool, - action: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - iconPath: PropTypes.string.isRequired, - }, - - getDefaultProps: function() { - return { - size: "25", - tooltip: false, - }; - }, - - getInitialState: function() { - return { - showTooltip: false, - }; - }, - - _onClick: function(ev) { - ev.stopPropagation(); - dis.dispatch({action: this.props.action}); - }, - - _onMouseEnter: function() { - if (this.props.tooltip) this.setState({showTooltip: true}); - }, - - _onMouseLeave: function() { - this.setState({showTooltip: false}); - }, - - render: function() { - const TintableSvg = sdk.getComponent("elements.TintableSvg"); - - let tooltip; - if (this.state.showTooltip) { - const RoomTooltip = sdk.getComponent("rooms.RoomTooltip"); - tooltip = ; - } - - return ( - - - {tooltip} - - ); - } -}); diff --git a/src/components/views/elements/CreateRoomButton.js b/src/components/views/elements/CreateRoomButton.js deleted file mode 100644 index 73c984a860..0000000000 --- a/src/components/views/elements/CreateRoomButton.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import sdk from '../../../index'; -import PropTypes from 'prop-types'; - -const CreateRoomButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -CreateRoomButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default CreateRoomButton; diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index fdd34e6ad2..28a36c429e 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -50,42 +50,10 @@ export default React.createClass({ }, onVerifyClick: function() { - var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { - title: "Verify device", - description: ( -
-

- To verify that this device can be trusted, please contact its - owner using some other means (e.g. in person or a phone call) - and ask them whether the key they see in their User Settings - for this device matches the key below: -

-
-
    -
  • { this.state.device.getDisplayName() }
  • -
  • { this.state.device.deviceId}
  • -
  • { this.state.device.getFingerprint() }
  • -
-
-

- If it matches, press the verify button below. - If it doesnt, then someone else is intercepting this device - and you probably want to press the blacklist button instead. -

-

- In future this verification process will be more sophisticated. -

-
- ), - button: "I verify that the keys match", - onFinished: confirm=>{ - if (confirm) { - MatrixClientPeg.get().setDeviceVerified( - this.props.userId, this.state.device.deviceId, true - ); - } - }, + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createDialog(DeviceVerifyDialog, { + userId: this.props.userId, + device: this.state.device, }); }, diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index b4d2545e04..82f8d753a9 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -152,10 +152,12 @@ export default class Dropdown extends React.Component { } _onInputClick(ev) { - this.setState({ - expanded: !this.state.expanded, - }); - ev.preventDefault(); + if (!this.state.expanded) { + this.setState({ + expanded: true, + }); + ev.preventDefault(); + } } _onMenuOptionClick(dropdownKey) { @@ -252,7 +254,7 @@ export default class Dropdown extends React.Component { ); }); if (options.length === 0) { - return [
+ return [
No results
]; } diff --git a/src/components/views/elements/HomeButton.js b/src/components/views/elements/HomeButton.js deleted file mode 100644 index 5c446f24c9..0000000000 --- a/src/components/views/elements/HomeButton.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import sdk from '../../../index'; -import PropTypes from 'prop-types'; - -const HomeButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -HomeButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default HomeButton; diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index ae8678894d..dcf1810468 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -269,7 +269,7 @@ module.exports = React.createClass({ ); }); return ( - + {avatars} ); diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js deleted file mode 100644 index 5e68776a15..0000000000 --- a/src/components/views/elements/RoomDirectoryButton.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import sdk from '../../../index'; -import PropTypes from 'prop-types'; - -const RoomDirectoryButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -RoomDirectoryButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default RoomDirectoryButton; diff --git a/src/components/views/elements/SettingsButton.js b/src/components/views/elements/SettingsButton.js deleted file mode 100644 index c6438da277..0000000000 --- a/src/components/views/elements/SettingsButton.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import sdk from '../../../index'; -import PropTypes from 'prop-types'; - -const SettingsButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -SettingsButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default SettingsButton; diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js deleted file mode 100644 index 747f75d1b3..0000000000 --- a/src/components/views/elements/StartChatButton.js +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2017 Vector Creations Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import sdk from '../../../index'; -import PropTypes from 'prop-types'; - -const StartChatButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -StartChatButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default StartChatButton; diff --git a/src/components/views/login/CountryDropdown.js b/src/components/views/login/CountryDropdown.js index 6323b3f558..7024db339c 100644 --- a/src/components/views/login/CountryDropdown.js +++ b/src/components/views/login/CountryDropdown.js @@ -19,7 +19,6 @@ import React from 'react'; import sdk from '../../../index'; import { COUNTRIES } from '../../../phonenumber'; -import { charactersToImageNode } from '../../../HtmlUtils'; const COUNTRIES_BY_ISO2 = new Object(null); for (const c of COUNTRIES) { @@ -27,9 +26,14 @@ for (const c of COUNTRIES) { } function countryMatchesSearchQuery(query, country) { + // Remove '+' if present (when searching for a prefix) + if (query[0] === '+') { + query = query.slice(1); + } + if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; if (country.iso2 == query.toUpperCase()) return true; - if (country.prefix == query) return true; + if (country.prefix.indexOf(query) !== -1) return true; return false; } @@ -38,10 +42,11 @@ export default class CountryDropdown extends React.Component { super(props); this._onSearchChange = this._onSearchChange.bind(this); this._onOptionChange = this._onOptionChange.bind(this); + this._getShortOption = this._getShortOption.bind(this); this.state = { searchQuery: '', - } + }; } componentWillMount() { @@ -64,13 +69,21 @@ export default class CountryDropdown extends React.Component { } _flagImgForIso2(iso2) { - // Unicode Regional Indicator Symbol letter 'A' - const RIS_A = 0x1F1E6; - const ASCII_A = 65; - return charactersToImageNode(iso2, true, - RIS_A + (iso2.charCodeAt(0) - ASCII_A), - RIS_A + (iso2.charCodeAt(1) - ASCII_A), - ); + return ; + } + + _getShortOption(iso2) { + if (!this.props.isSmall) { + return undefined; + } + let countryPrefix; + if (this.props.showPrefix) { + countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix; + } + return + { this._flagImgForIso2(iso2) } + { countryPrefix } + ; } render() { @@ -99,7 +112,7 @@ export default class CountryDropdown extends React.Component { const options = displayedCountries.map((country) => { return
{this._flagImgForIso2(country.iso2)} - {country.name} + {country.name} (+{country.prefix})
; }); @@ -107,21 +120,21 @@ export default class CountryDropdown extends React.Component { // values between mounting and the initial value propgating const value = this.props.value || COUNTRIES[0].iso2; - const getShortOption = this.props.isSmall ? this._flagImgForIso2 : undefined; - - return {options} - + ; } } CountryDropdown.propTypes = { className: React.PropTypes.string, isSmall: React.PropTypes.bool, + // if isSmall, show +44 in the selected value + showPrefix: React.PropTypes.bool, onOptionChange: React.PropTypes.func.isRequired, value: React.PropTypes.string, }; diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 349dd0d139..46c9598751 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -149,28 +149,26 @@ class PasswordLogin extends React.Component {
; case PasswordLogin.LOGIN_FIELD_PHONE: const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); - const prefix = this.state.phonePrefix; return
+ -
-
+{prefix}
- -
; } } diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 2bc2b8946a..e55a224531 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -314,24 +314,23 @@ module.exports = React.createClass({ const phoneSection = (
+ -
-
+{this.state.phonePrefix}
- -
); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d1486d22c9..44c4051995 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -295,16 +295,6 @@ module.exports = WithMatrixClient(React.createClass({ const receiptOffset = 15; let left = 0; - // It's possible that the receipt was sent several days AFTER the event. - // If it is, we want to display the complete date along with the HH:MM:SS, - // rather than just HH:MM:SS. - let dayAfterEvent = new Date(this.props.mxEvent.getTs()); - dayAfterEvent.setDate(dayAfterEvent.getDate() + 1); - dayAfterEvent.setHours(0); - dayAfterEvent.setMinutes(0); - dayAfterEvent.setSeconds(0); - let dayAfterEventTime = dayAfterEvent.getTime(); - var receipts = this.props.readReceipts || []; for (var i = 0; i < receipts.length; ++i) { var receipt = receipts[i]; @@ -340,7 +330,6 @@ module.exports = WithMatrixClient(React.createClass({ suppressAnimation={this._suppressReadReceiptAnimation} onClick={this.toggleAllReadAvatars} timestamp={receipt.ts} - showFullTimestamp={receipt.ts >= dayAfterEventTime} /> ); } @@ -492,22 +481,22 @@ module.exports = WithMatrixClient(React.createClass({ var e2e; // cosmetic padlocks: if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') { - e2e = ; + e2e = Encrypted by verified device; } // real padlocks else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) { if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') { - e2e = ; + e2e = Undecryptable; } else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) { - e2e = ; + e2e = Encrypted by verified device; } else { - e2e = ; + e2e = Encrypted by unverified device; } } else if (e2eEnabled) { - e2e = ; + e2e = Unencrypted message; } const timestamp = this.props.mxEvent.getTs() ? : null; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1f286e9e12..0914c59cb8 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -713,8 +713,16 @@ module.exports = WithMatrixClient(React.createClass({ const memberName = this.props.member.name; + if (this.props.member.user) { + var presenceState = this.props.member.user.presence; + var presenceLastActiveAgo = this.props.member.user.lastActiveAgo; + var presenceLastTs = this.props.member.user.lastPresenceTs; + var presenceCurrentlyActive = this.props.member.user.currentlyActive; + } + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); + var PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); const EmojiText = sdk.getComponent('elements.EmojiText'); return (
@@ -732,6 +740,11 @@ module.exports = WithMatrixClient(React.createClass({
Level:
+
+ +
{ adminTools } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index df7d0c3640..46ad476a33 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -33,6 +33,7 @@ export default class MessageComposer extends React.Component { this.onHangupClick = this.onHangupClick.bind(this); this.onUploadClick = this.onUploadClick.bind(this); this.onUploadFileSelected = this.onUploadFileSelected.bind(this); + this.uploadFiles = this.uploadFiles.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this); this.onInputContentChanged = this.onInputContentChanged.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -97,10 +98,11 @@ export default class MessageComposer extends React.Component { this.refs.uploadInput.click(); } - onUploadFileSelected(files, isPasted) { - if (!isPasted) - files = files.target.files; + onUploadFileSelected(files) { + this.uploadFiles(files.target.files); + } + uploadFiles(files) { let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -306,7 +308,7 @@ export default class MessageComposer extends React.Component { tryComplete={this._tryComplete} onUpArrow={this.onUpArrow} onDownArrow={this.onDownArrow} - onUploadFileSelected={this.onUploadFileSelected} + onFilesPasted={this.uploadFiles} tabComplete={this.props.tabComplete} // used for old messagecomposerinput/tabcomplete onContentChanged={this.onInputContentChanged} onInputStateChanged={this.onInputStateChanged} />, diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 8efd2fa579..af361db235 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -84,7 +84,6 @@ export default class MessageComposerInput extends React.Component { this.onAction = this.onAction.bind(this); this.handleReturn = this.handleReturn.bind(this); this.handleKeyCommand = this.handleKeyCommand.bind(this); - this.handlePastedFiles = this.handlePastedFiles.bind(this); this.onEditorContentChanged = this.onEditorContentChanged.bind(this); this.setEditorState = this.setEditorState.bind(this); this.onUpArrow = this.onUpArrow.bind(this); @@ -477,10 +476,6 @@ export default class MessageComposerInput extends React.Component { return false; } - handlePastedFiles(files) { - this.props.onUploadFileSelected(files, true); - } - handleReturn(ev) { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); @@ -542,9 +537,9 @@ export default class MessageComposerInput extends React.Component { let sendTextFn = this.client.sendTextMessage; if (contentText.startsWith('/me')) { - contentText = contentText.replace('/me ', ''); + contentText = contentText.substring(4); // bit of a hack, but the alternative would be quite complicated - if (contentHTML) contentHTML = contentHTML.replace('/me ', ''); + if (contentHTML) contentHTML = contentHTML.replace(/\/me ?/, ''); sendHtmlFn = this.client.sendHtmlEmote; sendTextFn = this.client.sendEmoteMessage; } @@ -734,7 +729,7 @@ export default class MessageComposerInput extends React.Component { keyBindingFn={MessageComposerInput.getKeyBinding} handleKeyCommand={this.handleKeyCommand} handleReturn={this.handleReturn} - handlePastedFiles={this.handlePastedFiles} + handlePastedFiles={this.props.onFilesPasted} stripPastedStyles={!this.state.isRichtextEnabled} onTab={this.onTab} onUpArrow={this.onUpArrow} @@ -764,7 +759,7 @@ MessageComposerInput.propTypes = { onDownArrow: React.PropTypes.func, - onUploadFileSelected: React.PropTypes.func, + onFilesPasted: React.PropTypes.func, // attempts to confirm currently selected completion, returns whether actually confirmed tryComplete: React.PropTypes.func, diff --git a/src/components/views/rooms/MessageComposerInputOld.js b/src/components/views/rooms/MessageComposerInputOld.js index 378644478c..adc6bc2c91 100644 --- a/src/components/views/rooms/MessageComposerInputOld.js +++ b/src/components/views/rooms/MessageComposerInputOld.js @@ -69,6 +69,9 @@ export default React.createClass({ // The text to use a placeholder in the input box placeholder: React.PropTypes.string.isRequired, + + // callback to handle files pasted into the composer + onFilesPasted: React.PropTypes.func, }, componentWillMount: function() { @@ -439,10 +442,27 @@ export default React.createClass({ this.refs.textarea.focus(); }, + _onPaste: function(ev) { + const items = ev.clipboardData.items; + const files = []; + for (const item of items) { + if (item.kind === 'file') { + files.push(item.getAsFile()); + } + } + if (files.length && this.props.onFilesPasted) { + this.props.onFilesPasted(files); + return true; + } + return false; + }, + render: function() { return (
-