diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index f66d2c69d3..f501f373cd 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -8,7 +8,6 @@ src/CallHandler.js src/component-index.js src/components/structures/ContextualMenu.js src/components/structures/CreateRoom.js -src/components/structures/FilePanel.js src/components/structures/LoggedInView.js src/components/structures/login/ForgotPassword.js src/components/structures/login/Login.js @@ -27,16 +26,10 @@ src/components/views/dialogs/ChatCreateOrReuseDialog.js src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/UnknownDeviceDialog.js src/components/views/elements/AddressSelector.js -src/components/views/elements/CreateRoomButton.js src/components/views/elements/DeviceVerifyButtons.js src/components/views/elements/DirectorySearchBox.js src/components/views/elements/EditableText.js -src/components/views/elements/HomeButton.js src/components/views/elements/MemberEventListSummary.js -src/components/views/elements/PowerSelector.js -src/components/views/elements/RoomDirectoryButton.js -src/components/views/elements/SettingsButton.js -src/components/views/elements/StartChatButton.js src/components/views/elements/TintableSvg.js src/components/views/elements/UserSelector.js src/components/views/login/CountryDropdown.js @@ -93,7 +86,6 @@ src/RichText.js src/Roles.js src/Rooms.js src/ScalarAuthClient.js -src/Tinter.js src/UiEffects.js src/Unread.js src/utils/DecryptFile.js diff --git a/.travis.yml b/.travis.yml index 4137d754bf..954f14a4da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,10 @@ dist: trusty # we don't need sudo, so can run in a container, which makes startup much # quicker. -sudo: false +# +# unfortunately we do temporarily require sudo as a workaround for +# https://github.com/travis-ci/travis-ci/issues/8836 +sudo: required language: node_js node_js: diff --git a/package.json b/package.json index 6735f58300..68f77b427f 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", "classnames": "^2.1.2", - "commonmark": "^0.27.0", + "commonmark": "^0.28.1", "counterpart": "^0.18.0", "draft-js": "^0.11.0-alpha", "draft-js-export-html": "^0.6.0", @@ -78,6 +78,7 @@ "querystring": "^0.2.0", "react": "^15.4.0", "react-addons-css-transition-group": "15.3.2", + "react-beautiful-dnd": "^4.0.0", "react-dom": "^15.4.0", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.14.1", diff --git a/src/Analytics.js b/src/Analytics.js index 1b4f45bc6b..5c39b48a35 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -14,25 +14,54 @@ limitations under the License. */ -import { getCurrentLanguage } from './languageHandler'; +import { getCurrentLanguage, _t, _td } from './languageHandler'; import PlatformPeg from './PlatformPeg'; -import SdkConfig from './SdkConfig'; +import SdkConfig, { DEFAULTS } from './SdkConfig'; +import Modal from './Modal'; +import sdk from './index'; + +function getRedactedHash() { + return window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/"); +} function getRedactedUrl() { - const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/"); // hardcoded url to make piwik happy - return 'https://riot.im/app/' + redactedHash; + return 'https://riot.im/app/' + getRedactedHash(); } const customVariables = { - 'App Platform': 1, - 'App Version': 2, - 'User Type': 3, - 'Chosen Language': 4, - 'Instance': 5, - 'RTE: Uses Richtext Mode': 6, - 'Homeserver URL': 7, - 'Identity Server URL': 8, + 'App Platform': { + id: 1, + expl: _td('The platform you\'re on'), + }, + 'App Version': { + id: 2, + expl: _td('The version of Riot.im'), + }, + 'User Type': { + id: 3, + expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'), + }, + 'Chosen Language': { + id: 4, + expl: _td('Your language of choice'), + }, + 'Instance': { + id: 5, + expl: _td('Which officially provided instance you are using, if any'), + }, + 'RTE: Uses Richtext Mode': { + id: 6, + expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'), + }, + 'Homeserver URL': { + id: 7, + expl: _td('Your homeserver\'s URL'), + }, + 'Identity Server URL': { + id: 8, + expl: _td('Your identity server\'s URL'), + }, }; function whitelistRedact(whitelist, str) { @@ -40,9 +69,6 @@ function whitelistRedact(whitelist, str) { return ''; } -const whitelistedHSUrls = ["https://matrix.org"]; -const whitelistedISUrls = ["https://vector.im"]; - class Analytics { constructor() { this._paq = null; @@ -140,11 +166,16 @@ class Analytics { } _setVisitVariable(key, value) { - this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']); + this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']); } setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { if (this.disabled) return; + + const config = SdkConfig.get(); + const whitelistedHSUrls = config.piwik.whitelistedHSUrls || DEFAULTS.piwik.whitelistedHSUrls; + const whitelistedISUrls = config.piwik.whitelistedISUrls || DEFAULTS.piwik.whitelistedISUrls; + this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl)); @@ -154,6 +185,44 @@ class Analytics { if (this.disabled) return; this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off'); } + + showDetailsModal() { + const Tracker = window.Piwik.getAsyncTracker(); + const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean); + + const resolution = `${window.screen.width}x${window.screen.height}`; + + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, { + title: _t('Analytics'), + description:
+
+ { _t('The information being sent to us to help make Riot.im better includes:') } +
+ + { rows.map((row) => + + + ) } +
{ _t(customVariables[row[0]].expl) }{ row[1] }
+
+
+ { _t('We also record each page you use in the app (currently ), your User Agent' + + ' () and your device resolution ().', + {}, + { + CurrentPageHash: { getRedactedHash() }, + CurrentUserAgent: { navigator.userAgent }, + CurrentDeviceResolution: { resolution }, + }, + ) } + + { _t('Where this page includes identifiable information, such as a room, ' + + 'user or group ID, that data is removed before being sent to the server.') } +
+
, + }); + } } if (!global.mxAnalytics) { diff --git a/src/DateUtils.js b/src/DateUtils.js index 77f3644f6f..986525eec8 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; import { _t } from './languageHandler'; function getDaysArray() { @@ -59,47 +58,70 @@ function twelveHourTime(date) { return `${hours}:${minutes}${ampm}`; } -module.exports = { - formatDate: function(date, showTwelveHour=false) { - const now = new Date(); - const days = getDaysArray(); - const months = getMonthsArray(); - if (date.toDateString() === now.toDateString()) { - return this.formatTime(date, showTwelveHour); - } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - // TODO: use standard date localize function provided in counterpart - return _t('%(weekDayName)s %(time)s', { - weekDayName: days[date.getDay()], - time: this.formatTime(date, showTwelveHour), - }); - } else if (now.getFullYear() === date.getFullYear()) { - // TODO: use standard date localize function provided in counterpart - return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - time: this.formatTime(date, showTwelveHour), - }); - } - return this.formatFullDate(date, showTwelveHour); - }, - - formatFullDate: function(date, showTwelveHour=false) { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { +export function formatDate(date, showTwelveHour=false) { + const now = new Date(); + const days = getDaysArray(); + const months = getMonthsArray(); + if (date.toDateString() === now.toDateString()) { + return formatTime(date, showTwelveHour); + } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s %(time)s', { + weekDayName: days[date.getDay()], + time: formatTime(date, showTwelveHour), + }); + } else if (now.getFullYear() === date.getFullYear()) { + // TODO: use standard date localize function provided in counterpart + return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { weekDayName: days[date.getDay()], monthName: months[date.getMonth()], day: date.getDate(), - fullYear: date.getFullYear(), - time: this.formatTime(date, showTwelveHour), + time: formatTime(date, showTwelveHour), }); - }, + } + return formatFullDate(date, showTwelveHour); +} - formatTime: function(date, showTwelveHour=false) { - if (showTwelveHour) { - return twelveHourTime(date); - } - return pad(date.getHours()) + ':' + pad(date.getMinutes()); - }, -}; +export function formatFullDateNoTime(date) { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + }); +} + +export function formatFullDate(date, showTwelveHour=false) { + const days = getDaysArray(); + const months = getMonthsArray(); + return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', { + weekDayName: days[date.getDay()], + monthName: months[date.getMonth()], + day: date.getDate(), + fullYear: date.getFullYear(), + time: formatTime(date, showTwelveHour), + }); +} + +export function formatTime(date, showTwelveHour=false) { + if (showTwelveHour) { + return twelveHourTime(date); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()); +} + +const MILLIS_IN_DAY = 86400000; +export function wantsDateSeparator(prevEventDate, nextEventDate) { + if (!nextEventDate || !prevEventDate) { + return false; + } + // Return early for events that are > 24h apart + if (Math.abs(prevEventDate.getTime() - nextEventDate.getTime()) > MILLIS_IN_DAY) { + return true; + } + + // Compare weekdays + return prevEventDate.getDay() !== nextEventDate.getDay(); +} diff --git a/src/Keyboard.js b/src/Keyboard.js index 9c872e1c66..bf83a1a05f 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.js @@ -68,3 +68,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) { return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; } } + +export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) { + const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + if (isMac) { + return ev.metaKey && !ev.altKey && !ev.ctrlKey; + } else { + return ev.ctrlKey && !ev.altKey && !ev.metaKey; + } +} diff --git a/src/Markdown.js b/src/Markdown.js index e05f163ba5..aa1c7e45b1 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -55,25 +55,6 @@ function is_multi_line(node) { return par.firstChild != par.lastChild; } -import linkifyMatrix from './linkify-matrix'; -import * as linkify from 'linkifyjs'; -linkifyMatrix(linkify); - -// Thieved from draft-js-export-markdown -function escapeMarkdown(s) { - return s.replace(/[*_`]/g, '\\$&'); -} - -// Replace URLs, room aliases and user IDs with md-escaped URLs -function linkifyMarkdown(s) { - const links = linkify.find(s); - links.forEach((l) => { - // This may replace several instances of `l.value` at once, but that's OK - s = s.replace(l.value, escapeMarkdown(l.value)); - }); - return s; -} - /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -81,7 +62,7 @@ function linkifyMarkdown(s) { */ export default class Markdown { constructor(input) { - this.input = linkifyMarkdown(input); + this.input = input; const parser = new commonmark.Parser(); this.parsed = parser.parse(this.input); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index a6012f5213..14dfa91fa4 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd. +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,6 +23,7 @@ import EventTimeline from 'matrix-js-sdk/lib/models/event-timeline'; import EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set'; import createMatrixClient from './utils/createMatrixClient'; import SettingsStore from './settings/SettingsStore'; +import MatrixActionCreators from './actions/MatrixActionCreators'; interface MatrixClientCreds { homeserverUrl: string, @@ -68,6 +70,8 @@ class MatrixClientPeg { unset() { this.matrixClient = null; + + MatrixActionCreators.stop(); } /** @@ -108,6 +112,9 @@ class MatrixClientPeg { // regardless of errors, start the client. If we did error out, we'll // just end up doing a full initial /sync. + // Connect the matrix client to the dispatcher + MatrixActionCreators.start(this.matrixClient); + console.log(`MatrixClientPeg: really starting MatrixClient`); this.get().startClient(opts); console.log(`MatrixClientPeg: MatrixClient started`); diff --git a/src/Modal.js b/src/Modal.js index 69ff806045..8e3b394f87 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -19,6 +19,7 @@ limitations under the License. const React = require('react'); const ReactDOM = require('react-dom'); +import PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; @@ -33,7 +34,7 @@ const AsyncWrapper = React.createClass({ /** A function which takes a 'callback' argument which it will call * with the real component once it loads. */ - loader: React.PropTypes.func.isRequired, + loader: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/Notifier.js b/src/Notifier.js index 75b698862c..e69bdf4461 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -135,6 +135,10 @@ const Notifier = { const plaf = PlatformPeg.get(); if (!plaf) return; + // Dev note: We don't set the "notificationsEnabled" setting to true here because it is a + // calculated value. It is determined based upon whether or not the master rule is enabled + // and other flags. Setting it here would cause a circular reference. + Analytics.trackEvent('Notifier', 'Set Enabled', enable); // make sure that we persist the current setting audio_enabled setting @@ -168,7 +172,7 @@ const Notifier = { }); // clear the notifications_hidden flag, so that if notifications are // disabled again in the future, we will show the banner again. - this.setToolbarHidden(false); + this.setToolbarHidden(true); } else { dis.dispatch({ action: "notifier_enabled", diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 1979c6d111..31541148d9 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -85,9 +85,7 @@ function _onStartChatFinished(shouldInvite, addrs) { if (rooms.length > 0) { // A Direct Message room already exists for this user, so select a // room from a list that is similar to the one in MemberInfo panel - const ChatCreateOrReuseDialog = sdk.getComponent( - "views.dialogs.ChatCreateOrReuseDialog", - ); + const ChatCreateOrReuseDialog = sdk.getComponent("views.dialogs.ChatCreateOrReuseDialog"); const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, { userId: addrTexts[0], onNewDMClick: () => { @@ -115,6 +113,15 @@ function _onStartChatFinished(shouldInvite, addrs) { }); }); } + } else if (addrTexts.length === 1) { + // Start a new DM chat + createRoom({dmUserId: addrTexts[0]}).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { + title: _t("Failed to invite user"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); } else { // Start multi user chat let room; diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 5cc078dc59..91e49fe09b 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -34,7 +34,14 @@ export function getRoomNotifsState(roomId) { } // for everything else, look at the room rule. - const roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId); + let roomRule = null; + try { + roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId); + } catch (err) { + // Possible that the client doesn't have pushRules yet. If so, it + // hasn't started eiher, so indicate that this room is not notifying. + return null; + } // XXX: We have to assume the default is to notify for all messages // (in particular this will be 'wrong' for one to one rooms because @@ -130,6 +137,11 @@ function setRoomNotifsStateUnmuted(roomId, newState) { } function findOverrideMuteRule(roomId) { + if (!MatrixClientPeg.get().pushRules || + !MatrixClientPeg.get().pushRules['global'] || + !MatrixClientPeg.get().pushRules['global'].override) { + return null; + } for (const rule of MatrixClientPeg.get().pushRules['global'].override) { if (isRuleForRoom(roomId, rule)) { if (isMuteRule(rule) && rule.enabled) { diff --git a/src/Rooms.js b/src/Rooms.js index 6cc2d867a6..ffa39141ff 100644 --- a/src/Rooms.js +++ b/src/Rooms.js @@ -43,7 +43,7 @@ export function getOnlyOtherMember(room, me) { return null; } -export function isConfCallRoom(room, me, conferenceHandler) { +function _isConfCallRoom(room, me, conferenceHandler) { if (!conferenceHandler) return false; if (me.membership != "join") { @@ -58,6 +58,26 @@ export function isConfCallRoom(room, me, conferenceHandler) { if (conferenceHandler.isConferenceUser(otherMember.userId)) { return true; } + + return false; +} + +// Cache whether a room is a conference call. Assumes that rooms will always +// either will or will not be a conference call room. +const isConfCallRoomCache = { + // $roomId: bool +}; + +export function isConfCallRoom(room, me, conferenceHandler) { + if (isConfCallRoomCache[room.roomId] !== undefined) { + return isConfCallRoomCache[room.roomId]; + } + + const result = _isConfCallRoom(room, me, conferenceHandler); + + isConfCallRoomCache[room.roomId] = result; + + return result; } export function looksLikeDirectMessageRoom(room, me) { diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 3e775a94ab..568dd6d185 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -15,6 +15,7 @@ limitations under the License. */ import Promise from 'bluebird'; +import SettingsStore from "./settings/SettingsStore"; const request = require('browser-request'); const SdkConfig = require('./SdkConfig'); @@ -38,11 +39,53 @@ class ScalarAuthClient { // Returns a scalar_token string getScalarToken() { - const tok = window.localStorage.getItem("mx_scalar_token"); - if (tok) return Promise.resolve(tok); + const token = window.localStorage.getItem("mx_scalar_token"); - // No saved token, so do the dance to get one. First, we - // need an openid bearer token from the HS. + if (!token) { + return this.registerForToken(); + } else { + return this.validateToken(token).then(userId => { + const me = MatrixClientPeg.get().getUserId(); + if (userId !== me) { + throw new Error("Scalar token is owned by someone else: " + me); + } + return token; + }).catch(err => { + console.error(err); + + // Something went wrong - try to get a new token. + console.warn("Registering for new scalar token"); + return this.registerForToken(); + }) + } + } + + validateToken(token) { + let url = SdkConfig.get().integrations_rest_url + "/account"; + + const defer = Promise.defer(); + request({ + method: "GET", + uri: url, + qs: {scalar_token: token}, + json: true, + }, (err, response, body) => { + if (err) { + defer.reject(err); + } else if (response.statusCode / 100 !== 2) { + defer.reject({statusCode: response.statusCode}); + } else if (!body || !body.user_id) { + defer.reject(new Error("Missing user_id in response")); + } else { + defer.resolve(body.user_id); + } + }); + + return defer.promise; + } + + registerForToken() { + // Get openid bearer token from the HS as the first part of our dance return MatrixClientPeg.get().getOpenIdToken().then((token_object) => { // Now we can send that to scalar and exchange it for a scalar token return this.exchangeForScalarToken(token_object); @@ -109,6 +152,7 @@ class ScalarAuthClient { let url = SdkConfig.get().integrations_ui_url; url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "&room_id=" + encodeURIComponent(roomId); + url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme")); if (id) { url += '&integ_id=' + encodeURIComponent(id); } diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 7bde607451..3c164c6551 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -557,8 +557,16 @@ const onMessage = function(event) { // // All strings start with the empty string, so for sanity return if the length // of the event origin is 0. + // + // TODO -- Scalar postMessage API should be namespaced with event.data.api field + // Fix following "if" statement to respond only to specific API messages. const url = SdkConfig.get().integrations_ui_url; - if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) { + if ( + event.origin.length === 0 || + !url.startsWith(event.origin) || + !event.data.action || + event.data.api // Ignore messages with specific API set + ) { return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise } diff --git a/src/SdkConfig.js b/src/SdkConfig.js index 8df725a913..64bf21ecf8 100644 --- a/src/SdkConfig.js +++ b/src/SdkConfig.js @@ -21,6 +21,13 @@ const DEFAULTS = { integrations_rest_url: "https://scalar.vector.im/api", // Where to send bug reports. If not specified, bugs cannot be sent. bug_report_endpoint_url: null, + + piwik: { + url: "https://piwik.riot.im/", + whitelistedHSUrls: ["https://matrix.org"], + whitelistedISUrls: ["https://vector.im", "https://matrix.org"], + siteId: 1, + }, }; class SdkConfig { @@ -45,3 +52,4 @@ class SdkConfig { } module.exports = SdkConfig; +module.exports.DEFAULTS = DEFAULTS; diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 344bac1ddb..d45e45e84c 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -96,6 +96,8 @@ const commands = { colorScheme.primary_color = matches[1]; if (matches[4]) { colorScheme.secondary_color = matches[4]; + } else { + colorScheme.secondary_color = colorScheme.primary_color; } return success( SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme), @@ -295,7 +297,7 @@ const commands = { // Define the power level of a user op: new Command("op", " []", function(roomId, args) { if (args) { - const matches = args.match(/^(\S+?)( +(\d+))?$/); + const matches = args.match(/^(\S+?)( +(-?\d+))?$/); let powerLevel = 50; // default power level for op if (matches) { const userId = matches[1]; diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 1bdf5ad90c..e60bde4094 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -52,8 +52,7 @@ function textForMemberEvent(ev) { case 'join': if (prevContent && prevContent.membership === 'join') { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { - return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', { - senderName, + return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', { oldDisplayName: prevContent.displayname, displayName: content.displayname, }); diff --git a/src/Velociraptor.js b/src/Velociraptor.js index 9a674d4f09..af4e6dcb60 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -1,5 +1,6 @@ const React = require('react'); const ReactDom = require('react-dom'); +import PropTypes from 'prop-types'; const Velocity = require('velocity-vector'); /** @@ -14,16 +15,16 @@ module.exports = React.createClass({ propTypes: { // either a list of child nodes, or a single child. - children: React.PropTypes.any, + children: PropTypes.any, // optional transition information for changing existing children - transition: React.PropTypes.object, + transition: PropTypes.object, // a list of state objects to apply to each child node in turn - startStyles: React.PropTypes.array, + startStyles: PropTypes.array, // a list of transition options from the corresponding startStyle - enterTransitionOpts: React.PropTypes.array, + enterTransitionOpts: PropTypes.array, }, getDefaultProps: function() { diff --git a/src/actions/GroupActions.js b/src/actions/GroupActions.js new file mode 100644 index 0000000000..006c2da5b8 --- /dev/null +++ b/src/actions/GroupActions.js @@ -0,0 +1,34 @@ +/* +Copyright 2017 New Vector 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 { asyncAction } from './actionCreators'; + +const GroupActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to fetch + * the groups to which a user is joined. + * + * @param {MatrixClient} matrixClient the matrix client to query. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +GroupActions.fetchJoinedGroups = function(matrixClient) { + return asyncAction('GroupActions.fetchJoinedGroups', () => matrixClient.getJoinedGroups()); +}; + +export default GroupActions; diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js new file mode 100644 index 0000000000..33bdb53799 --- /dev/null +++ b/src/actions/MatrixActionCreators.js @@ -0,0 +1,108 @@ +/* +Copyright 2017 New Vector 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 dis from '../dispatcher'; + +// TODO: migrate from sync_state to MatrixActions.sync so that more js-sdk events +// become dispatches in the same place. +/** + * Create a MatrixActions.sync action that represents a MatrixClient `sync` event, + * each parameter mapping to a key-value in the action. + * + * @param {MatrixClient} matrixClient the matrix client + * @param {string} state the current sync state. + * @param {string} prevState the previous sync state. + * @returns {Object} an action of type MatrixActions.sync. + */ +function createSyncAction(matrixClient, state, prevState) { + return { + action: 'MatrixActions.sync', + state, + prevState, + matrixClient, + }; +} + +/** + * @typedef AccountDataAction + * @type {Object} + * @property {string} action 'MatrixActions.accountData'. + * @property {MatrixEvent} event the MatrixEvent that triggered the dispatch. + * @property {string} event_type the type of the MatrixEvent, e.g. "m.direct". + * @property {Object} event_content the content of the MatrixEvent. + */ + +/** + * Create a MatrixActions.accountData action that represents a MatrixClient `accountData` + * matrix event. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} accountDataEvent the account data event. + * @returns {AccountDataAction} an action of type MatrixActions.accountData. + */ +function createAccountDataAction(matrixClient, accountDataEvent) { + return { + action: 'MatrixActions.accountData', + event: accountDataEvent, + event_type: accountDataEvent.getType(), + event_content: accountDataEvent.getContent(), + }; +} + +/** + * This object is responsible for dispatching actions when certain events are emitted by + * the given MatrixClient. + */ +export default { + // A list of callbacks to call to unregister all listeners added + _matrixClientListenersStop: [], + + /** + * Start listening to certain events from the MatrixClient and dispatch actions when + * they are emitted. + * @param {MatrixClient} matrixClient the MatrixClient to listen to events from + */ + start(matrixClient) { + this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); + this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + }, + + /** + * Start listening to events of type eventName on matrixClient and when they are emitted, + * dispatch an action created by the actionCreator function. + * @param {MatrixClient} matrixClient a MatrixClient to register a listener with. + * @param {string} eventName the event to listen to on MatrixClient. + * @param {function} actionCreator a function that should return an action to dispatch + * when given the MatrixClient as an argument as well as + * arguments emitted in the MatrixClient event. + */ + _addMatrixClientListener(matrixClient, eventName, actionCreator) { + const listener = (...args) => { + dis.dispatch(actionCreator(matrixClient, ...args)); + }; + matrixClient.on(eventName, listener); + this._matrixClientListenersStop.push(() => { + matrixClient.removeListener(eventName, listener); + }); + }, + + /** + * Stop listening to events. + */ + stop() { + this._matrixClientListenersStop.forEach((stopListener) => stopListener()); + }, +}; diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js new file mode 100644 index 0000000000..dd4df6a9d4 --- /dev/null +++ b/src/actions/TagOrderActions.js @@ -0,0 +1,59 @@ +/* +Copyright 2017 New Vector 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 Analytics from '../Analytics'; +import { asyncAction } from './actionCreators'; +import TagOrderStore from '../stores/TagOrderStore'; + +const TagOrderActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to + * move a tag in TagOrderStore to destinationIx. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to move. + * @param {number} destinationIx the new position of the tag. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) { + // Only commit tags if the state is ready, i.e. not null + let tags = TagOrderStore.getOrderedTags(); + if (!tags) { + return; + } + + tags = tags.filter((t) => t !== tag); + tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.moveTag', () => { + Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {tags}; + }); +}; + +export default TagOrderActions; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js new file mode 100644 index 0000000000..0238eee8c0 --- /dev/null +++ b/src/actions/actionCreators.js @@ -0,0 +1,48 @@ +/* +Copyright 2017 New Vector 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. +*/ + +/** + * Create an action thunk that will dispatch actions indicating the current + * status of the Promise returned by fn. + * + * @param {string} id the id to give the dispatched actions. This is given a + * suffix determining whether it is pending, successful or + * a failure. + * @param {function} fn a function that returns a Promise. + * @param {function?} pendingFn a function that returns an object to assign + * to the `request` key of the ${id}.pending + * payload. + * @returns {function} an action thunk - a function that uses its single + * argument as a dispatch function to dispatch the + * following actions: + * `${id}.pending` and either + * `${id}.success` or + * `${id}.failure`. + */ +export function asyncAction(id, fn, pendingFn) { + return (dispatch) => { + dispatch({ + action: id + '.pending', + request: + typeof pendingFn === 'function' ? pendingFn() : undefined, + }); + fn().then((result) => { + dispatch({action: id + '.success', result}); + }).catch((err) => { + dispatch({action: id + '.failure', err}); + }); + }; +} diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index a8f588d39a..5db8b2365f 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ const React = require("react"); +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const sdk = require('../../../index'); const MatrixClientPeg = require("../../../MatrixClientPeg"); @@ -23,8 +24,8 @@ module.exports = React.createClass({ displayName: 'EncryptedEventDialog', propTypes: { - event: React.PropTypes.object.isRequired, - onFinished: React.PropTypes.func.isRequired, + event: PropTypes.object.isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 04274442c2..06fb0668d5 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -16,6 +16,7 @@ limitations under the License. import FileSaver from 'file-saver'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as Matrix from 'matrix-js-sdk'; @@ -29,8 +30,8 @@ export default React.createClass({ displayName: 'ExportE2eKeysDialog', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - onFinished: React.PropTypes.func.isRequired, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index a01b6580f1..10744a8911 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import * as Matrix from 'matrix-js-sdk'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; @@ -40,8 +41,8 @@ export default React.createClass({ displayName: 'ImportE2eKeysDialog', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - onFinished: React.PropTypes.func.isRequired, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/autocomplete/Components.js b/src/autocomplete/Components.js index a27533f7c2..b09f4e963e 100644 --- a/src/autocomplete/Components.js +++ b/src/autocomplete/Components.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; /* These were earlier stateless functional components but had to be converted @@ -42,10 +43,10 @@ export class TextualCompletion extends React.Component { } } TextualCompletion.propTypes = { - title: React.PropTypes.string, - subtitle: React.PropTypes.string, - description: React.PropTypes.string, - className: React.PropTypes.string, + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + className: PropTypes.string, }; export class PillCompletion extends React.Component { @@ -69,9 +70,9 @@ export class PillCompletion extends React.Component { } } PillCompletion.propTypes = { - title: React.PropTypes.string, - subtitle: React.PropTypes.string, - description: React.PropTypes.string, - initialComponent: React.PropTypes.element, - className: React.PropTypes.string, + title: PropTypes.string, + subtitle: PropTypes.string, + description: PropTypes.string, + initialComponent: PropTypes.element, + className: PropTypes.string, }; diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 1e1928a1ee..31599703c2 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -25,6 +25,7 @@ import {PillCompletion} from './Components'; import {getDisplayAliasForRoom} from '../Rooms'; import sdk from '../index'; import _sortBy from 'lodash/sortBy'; +import {makeRoomPermalink} from "../matrix-to"; const ROOM_REGEX = /(?=#)(\S*)/g; @@ -78,7 +79,7 @@ export default class RoomProvider extends AutocompleteProvider { return { completion: displayAlias, suffix: ' ', - href: 'https://matrix.to/#/' + displayAlias, + href: makeRoomPermalink(displayAlias), component: ( } title={room.name} description={displayAlias} /> ), diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 794f507d21..fefe77f6cd 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -28,6 +28,7 @@ import _sortBy from 'lodash/sortBy'; import MatrixClientPeg from '../MatrixClientPeg'; import type {Room, RoomMember} from 'matrix-js-sdk'; +import {makeUserPermalink} from "../matrix-to"; const USER_REGEX = /@\S*/g; @@ -106,7 +107,7 @@ export default class UserProvider extends AutocompleteProvider { // relies on the length of the entity === length of the text in the decoration. completion: user.rawDisplayName.replace(' (IRC)', ''), suffix: range.start === 0 ? ': ' : ' ', - href: 'https://matrix.to/#/' + user.userId, + href: makeUserPermalink(user.userId), component: ( } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 3c2308e6a7..94f5713a79 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -20,6 +20,7 @@ limitations under the License. const classNames = require('classnames'); const React = require('react'); const ReactDOM = require('react-dom'); +import PropTypes from 'prop-types'; // Shamelessly ripped off Modal.js. There's probably a better way // of doing reusable widgets like dialog boxes & menus where we go and @@ -29,11 +30,11 @@ module.exports = { ContextualMenuContainerId: "mx_ContextualMenu_Container", propTypes: { - menuWidth: React.PropTypes.number, - menuHeight: React.PropTypes.number, - chevronOffset: React.PropTypes.number, - menuColour: React.PropTypes.string, - chevronFace: React.PropTypes.string, // top, bottom, left, right + menuWidth: PropTypes.number, + menuHeight: PropTypes.number, + chevronOffset: PropTypes.number, + menuColour: PropTypes.string, + chevronFace: PropTypes.string, // top, bottom, left, right }, getOrCreateContainer: function() { diff --git a/src/components/structures/CreateRoom.js b/src/components/structures/CreateRoom.js index 26454c5ea6..2bb9adb544 100644 --- a/src/components/structures/CreateRoom.js +++ b/src/components/structures/CreateRoom.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; @@ -30,8 +31,8 @@ module.exports = React.createClass({ displayName: 'CreateRoom', propTypes: { - onRoomCreated: React.PropTypes.func, - collapsedRhs: React.PropTypes.bool, + onRoomCreated: PropTypes.func, + collapsedRhs: PropTypes.bool, }, phases: { diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index ffa5e45249..e86b76333d 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import Matrix from 'matrix-js-sdk'; import sdk from '../../index'; @@ -28,7 +29,7 @@ const FilePanel = React.createClass({ displayName: 'FilePanel', propTypes: { - roomId: React.PropTypes.string.isRequired, + roomId: PropTypes.string.isRequired, }, getInitialState: function() { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 5ffb97c6ed..de96935838 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -31,6 +31,7 @@ import GroupStoreCache from '../../stores/GroupStoreCache'; import GroupStore from '../../stores/GroupStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GeminiScrollbar from 'react-gemini-scrollbar'; +import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -209,7 +210,7 @@ const FeaturedRoom = React.createClass({ let permalink = null; if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) { - permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias; + permalink = makeGroupPermalink(this.props.summaryInfo.profile.canonical_alias); } let roomNameNode = null; @@ -366,7 +367,7 @@ const FeaturedUser = React.createClass({ const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id; - const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id; + const permalink = makeUserPermalink(this.props.summaryInfo.user_id); const userNameNode = { name }; const httpUrl = MatrixClientPeg.get() .mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64); @@ -390,7 +391,7 @@ const FeaturedUser = React.createClass({ }); const GroupContext = { - groupStore: React.PropTypes.instanceOf(GroupStore).isRequired, + groupStore: PropTypes.instanceOf(GroupStore).isRequired, }; CategoryRoomList.contextTypes = GroupContext; @@ -408,7 +409,7 @@ export default React.createClass({ }, childContextTypes: { - groupStore: React.PropTypes.instanceOf(GroupStore), + groupStore: PropTypes.instanceOf(GroupStore), }, getChildContext: function() { diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 8a2c1b8c79..8428e3c714 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk'; const InteractiveAuth = Matrix.InteractiveAuth; import React from 'react'; +import PropTypes from 'prop-types'; import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents'; @@ -26,18 +27,18 @@ export default React.createClass({ propTypes: { // matrix client to use for UI auth requests - matrixClient: React.PropTypes.object.isRequired, + matrixClient: PropTypes.object.isRequired, // response from initial request. If not supplied, will do a request on // mount. - authData: React.PropTypes.shape({ - flows: React.PropTypes.array, - params: React.PropTypes.object, - session: React.PropTypes.string, + authData: PropTypes.shape({ + flows: PropTypes.array, + params: PropTypes.object, + session: PropTypes.string, }), // callback - makeRequest: React.PropTypes.func.isRequired, + makeRequest: PropTypes.func.isRequired, // callback called when the auth process has finished, // successfully or unsuccessfully. @@ -51,22 +52,22 @@ export default React.createClass({ // the auth session. // * clientSecret {string} The client secret used in auth // sessions with the ID server. - onAuthFinished: React.PropTypes.func.isRequired, + onAuthFinished: PropTypes.func.isRequired, // Inputs provided by the user to the auth process // and used by various stages. As passed to js-sdk // interactive-auth - inputs: React.PropTypes.object, + inputs: PropTypes.object, // As js-sdk interactive-auth - makeRegistrationUrl: React.PropTypes.func, - sessionId: React.PropTypes.string, - clientSecret: React.PropTypes.string, - emailSid: React.PropTypes.string, + makeRegistrationUrl: PropTypes.func, + sessionId: PropTypes.string, + clientSecret: PropTypes.string, + emailSid: PropTypes.string, // If true, poll to see if the auth flow has been completed // out-of-band - poll: React.PropTypes.bool, + poll: PropTypes.bool, }, getInitialState: function() { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 01abf966f9..e97d9dd0a1 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -18,6 +18,7 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; +import PropTypes from 'prop-types'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import Notifier from '../../Notifier'; @@ -38,27 +39,27 @@ import SettingsStore from "../../settings/SettingsStore"; * * Components mounted below us can access the matrix client via the react context. */ -export default React.createClass({ +const LoggedInView = React.createClass({ displayName: 'LoggedInView', propTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, - page_type: React.PropTypes.string.isRequired, - onRoomCreated: React.PropTypes.func, - onUserSettingsClose: React.PropTypes.func, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired, + page_type: PropTypes.string.isRequired, + onRoomCreated: PropTypes.func, + onUserSettingsClose: PropTypes.func, // Called with the credentials of a registered user (if they were a ROU that // transitioned to PWLU) - onRegistered: React.PropTypes.func, + onRegistered: PropTypes.func, - teamToken: React.PropTypes.string, + teamToken: PropTypes.string, // and lots and lots of other stuff. }, childContextTypes: { - matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient), - authCache: React.PropTypes.object, + matrixClient: PropTypes.instanceOf(Matrix.MatrixClient), + authCache: PropTypes.object, }, getChildContext: function() { @@ -331,7 +332,6 @@ export default React.createClass({
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? :
} @@ -344,3 +344,5 @@ export default React.createClass({ ); }, }); + +export default LoggedInView; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ba7251b603..d6d0b00c84 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -19,6 +19,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; +import PropTypes from 'prop-types'; import Matrix from "matrix-js-sdk"; import Analytics from "../../Analytics"; @@ -83,7 +84,7 @@ const ONBOARDING_FLOW_STARTERS = [ 'view_create_group', ]; -module.exports = React.createClass({ +export default React.createClass({ // we export this so that the integration tests can use it :-S statics: { VIEWS: VIEWS, @@ -92,38 +93,38 @@ module.exports = React.createClass({ displayName: 'MatrixChat', propTypes: { - config: React.PropTypes.object, - ConferenceHandler: React.PropTypes.any, - onNewScreen: React.PropTypes.func, - registrationUrl: React.PropTypes.string, - enableGuest: React.PropTypes.bool, + config: PropTypes.object, + ConferenceHandler: PropTypes.any, + onNewScreen: PropTypes.func, + registrationUrl: PropTypes.string, + enableGuest: PropTypes.bool, // the queryParams extracted from the [real] query-string of the URI - realQueryParams: React.PropTypes.object, + realQueryParams: PropTypes.object, // the initial queryParams extracted from the hash-fragment of the URI - startingFragmentQueryParams: React.PropTypes.object, + startingFragmentQueryParams: PropTypes.object, // called when we have completed a token login - onTokenLoginCompleted: React.PropTypes.func, + onTokenLoginCompleted: PropTypes.func, // Represents the screen to display as a result of parsing the initial // window.location - initialScreenAfterLogin: React.PropTypes.shape({ - screen: React.PropTypes.string.isRequired, - params: React.PropTypes.object, + initialScreenAfterLogin: PropTypes.shape({ + screen: PropTypes.string.isRequired, + params: PropTypes.object, }), // displayname, if any, to set on the device when logging // in/registering. - defaultDeviceDisplayName: React.PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // A function that makes a registration URL - makeRegistrationUrl: React.PropTypes.func.isRequired, + makeRegistrationUrl: PropTypes.func.isRequired, }, childContextTypes: { - appConfig: React.PropTypes.object, + appConfig: PropTypes.object, }, AuxPanel: { @@ -846,16 +847,36 @@ module.exports = React.createClass({ }).close; }, + _leaveRoomWarnings: function(roomId) { + const roomToLeave = MatrixClientPeg.get().getRoom(roomId); + // Show a warning if there are additional complications. + const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', ''); + const warnings = []; + if (joinRules) { + const rule = joinRules.getContent().join_rule; + if (rule !== "public") { + warnings.push(( + + { _t("This room is not public. You will not be able to rejoin without an invite.") } + + )); + } + } + return warnings; + }, + _leaveRoom: function(roomId) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const roomToLeave = MatrixClientPeg.get().getRoom(roomId); + const warnings = this._leaveRoomWarnings(roomId); + Modal.createTrackedDialog('Leave room', '', QuestionDialog, { title: _t("Leave room"), description: ( { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) } + { warnings } ), onFinished: (shouldLeave) => { @@ -1065,10 +1086,10 @@ module.exports = React.createClass({ // this if we are not scrolled up in the view. To find out, delegate to // the timeline panel. If the timeline panel doesn't exist, then we assume // it is safe to reset the timeline. - if (!self.refs.loggedInView) { + if (!self._loggedInView || !self._loggedInView.child) { return true; } - return self.refs.loggedInView.canResetTimelineInRoom(roomId); + return self._loggedInView.child.canResetTimelineInRoom(roomId); }); cli.on('sync', function(state, prevState) { @@ -1487,6 +1508,10 @@ module.exports = React.createClass({ return this.props.makeRegistrationUrl(params); }, + _collectLoggedInView: function(ref) { + this._loggedInView = ref; + }, + render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); @@ -1519,7 +1544,7 @@ module.exports = React.createClass({ */ const LoggedInView = sdk.getComponent('structures.LoggedInView'); return ( - ; + const dateSeparator =
  • ; ret.push(dateSeparator); } @@ -479,7 +479,7 @@ module.exports = React.createClass({ // do we need a date separator since the last event? if (this._wantsDateSeparator(prevEvent, eventDate)) { - const dateSeparator =
  • ; + const dateSeparator =
  • ; ret.push(dateSeparator); continuation = false; } @@ -522,17 +522,7 @@ module.exports = React.createClass({ // here. return !this.props.suppressFirstDateSeparator; } - const prevEventDate = prevEvent.getDate(); - if (!nextEventDate || !prevEventDate) { - return false; - } - // Return early for events that are > 24h apart - if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) { - return true; - } - - // Compare weekdays - return prevEventDate.getDay() !== nextEventDate.getDay(); + return wantsDateSeparator(prevEvent.getDate(), nextEventDate); }, // get a list of read receipts that should be shown next to this event diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 9281fb199e..22157beaca 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import GeminiScrollbar from 'react-gemini-scrollbar'; import sdk from '../../index'; import { _t } from '../../languageHandler'; @@ -26,7 +27,7 @@ export default withMatrixClient(React.createClass({ displayName: 'MyGroups', propTypes: { - matrixClient: React.PropTypes.object.isRequired, + matrixClient: PropTypes.object.isRequired, }, getInitialState: function() { diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 77d506d9af..8034923158 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import Matrix from 'matrix-js-sdk'; import { _t } from '../../languageHandler'; import sdk from '../../index'; @@ -23,7 +24,7 @@ import WhoIsTyping from '../../WhoIsTyping'; import MatrixClientPeg from '../../MatrixClientPeg'; import MemberAvatar from '../views/avatars/MemberAvatar'; import Resend from '../../Resend'; -import { showUnknownDeviceDialogForMessages } from '../../cryptodevices'; +import * as cryptodevices from '../../cryptodevices'; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -41,59 +42,59 @@ module.exports = React.createClass({ propTypes: { // the room this statusbar is representing. - room: React.PropTypes.object.isRequired, + room: PropTypes.object.isRequired, // the number of messages which have arrived since we've been scrolled up - numUnreadMessages: React.PropTypes.number, + numUnreadMessages: PropTypes.number, // this is true if we are fully scrolled-down, and are looking at // the end of the live timeline. - atEndOfLiveTimeline: React.PropTypes.bool, + atEndOfLiveTimeline: PropTypes.bool, // This is true when the user is alone in the room, but has also sent a message. // Used to suggest to the user to invite someone - sentMessageAndIsAlone: React.PropTypes.bool, + sentMessageAndIsAlone: PropTypes.bool, // true if there is an active call in this room (means we show // the 'Active Call' text in the status bar if there is nothing // more interesting) - hasActiveCall: React.PropTypes.bool, + hasActiveCall: PropTypes.bool, // Number of names to display in typing indication. E.g. set to 3, will // result in "X, Y, Z and 100 others are typing." - whoIsTypingLimit: React.PropTypes.number, + whoIsTypingLimit: PropTypes.number, // callback for when the user clicks on the 'resend all' button in the // 'unsent messages' bar - onResendAllClick: React.PropTypes.func, + onResendAllClick: PropTypes.func, // callback for when the user clicks on the 'cancel all' button in the // 'unsent messages' bar - onCancelAllClick: React.PropTypes.func, + onCancelAllClick: PropTypes.func, // callback for when the user clicks on the 'invite others' button in the // 'you are alone' bar - onInviteClick: React.PropTypes.func, + onInviteClick: PropTypes.func, // callback for when the user clicks on the 'stop warning me' button in the // 'you are alone' bar - onStopWarningClick: React.PropTypes.func, + onStopWarningClick: PropTypes.func, // callback for when the user clicks on the 'scroll to bottom' button - onScrollToBottomClick: React.PropTypes.func, + onScrollToBottomClick: PropTypes.func, // callback for when we do something that changes the size of the // status bar. This is used to trigger a re-layout in the parent // component. - onResize: React.PropTypes.func, + onResize: PropTypes.func, // callback for when the status bar can be hidden from view, as it is // not displaying anything - onHidden: React.PropTypes.func, + onHidden: PropTypes.func, // callback for when the status bar is displaying something and should // be visible - onVisible: React.PropTypes.func, + onVisible: PropTypes.func, }, getDefaultProps: function() { @@ -147,6 +148,13 @@ module.exports = React.createClass({ }); }, + _onSendWithoutVerifyingClick: function() { + cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => { + cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices); + Resend.resendUnsentEvents(this.props.room); + }); + }, + _onResendAllClick: function() { Resend.resendUnsentEvents(this.props.room); }, @@ -156,7 +164,7 @@ module.exports = React.createClass({ }, _onShowDevicesClick: function() { - showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room); + cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room); }, _onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) { @@ -169,8 +177,10 @@ module.exports = React.createClass({ // Check whether current size is greater than 0, if yes call props.onVisible _checkSize: function() { - if (this.props.onVisible && this._getSize()) { - this.props.onVisible(); + if (this._getSize()) { + if (this.props.onVisible) this.props.onVisible(); + } else { + if (this.props.onHidden) this.props.onHidden(); } }, @@ -286,10 +296,11 @@ module.exports = React.createClass({ if (hasUDE) { title = _t("Message not sent due to unknown devices being present"); content = _t( - "Show devices or cancel all.", + "Show devices, send anyway or cancel.", {}, { 'showDevicesText': (sub) => { sub }, + 'sendAnywayText': (sub) => { sub }, 'cancelText': (sub) => { sub }, }, ); @@ -302,11 +313,11 @@ module.exports = React.createClass({ ) { title = unsentMessages[0].error.data.error; } else { - title = _t("Some of your messages have not been sent."); + title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); } - content = _t("Resend all or cancel all now. " + + content = _t("%(count)s Resend all or cancel all now. " + "You can also select individual messages to resend or cancel.", - {}, + { count: unsentMessages.length }, { 'resendText': (sub) => { sub }, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 138c110c4f..27a70fdb10 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -24,6 +24,7 @@ import shouldHideEvent from "../../shouldHideEvent"; const React = require("react"); const ReactDOM = require("react-dom"); +import PropTypes from 'prop-types'; import Promise from 'bluebird'; const classNames = require("classnames"); import { _t } from '../../languageHandler'; @@ -58,18 +59,18 @@ if (DEBUG) { module.exports = React.createClass({ displayName: 'RoomView', propTypes: { - ConferenceHandler: React.PropTypes.any, + ConferenceHandler: PropTypes.any, // Called with the credentials of a registered user (if they were a ROU that // transitioned to PWLU) - onRegistered: React.PropTypes.func, + onRegistered: PropTypes.func, // An object representing a third party invite to join this room // Fields: // * inviteSignUrl (string) The URL used to join this room from an email invite // (given as part of the link in the invite email) // * invitedEmail (string) The email address that was invited to this room - thirdPartyInvite: React.PropTypes.object, + thirdPartyInvite: PropTypes.object, // Any data about the room that would normally come from the Home Server // but has been passed out-of-band, eg. the room name and avatar URL @@ -80,10 +81,10 @@ module.exports = React.createClass({ // * avatarUrl (string) The mxc:// avatar URL for the room // * inviterName (string) The display name of the person who // * invited us tovthe room - oobData: React.PropTypes.object, + oobData: PropTypes.object, // is the RightPanel collapsed? - collapsedRhs: React.PropTypes.bool, + collapsedRhs: PropTypes.bool, }, getInitialState: function() { @@ -854,9 +855,13 @@ module.exports = React.createClass({ ev.dataTransfer.dropEffect = 'none'; - const items = ev.dataTransfer.items; - if (items.length == 1) { - if (items[0].kind == 'file') { + const items = [...ev.dataTransfer.items]; + if (items.length >= 1) { + const isDraggingFiles = items.every(function(item) { + return item.kind == 'file'; + }); + + if (isDraggingFiles) { this.setState({ draggingFile: true }); ev.dataTransfer.dropEffect = 'copy'; } @@ -867,10 +872,8 @@ module.exports = React.createClass({ ev.stopPropagation(); ev.preventDefault(); this.setState({ draggingFile: false }); - const files = ev.dataTransfer.files; - if (files.length == 1) { - this.uploadFile(files[0]); - } + const files = [...ev.dataTransfer.files]; + files.forEach(this.uploadFile); }, onDragLeaveOrEnd: function(ev) { @@ -1345,10 +1348,12 @@ module.exports = React.createClass({ }, onStatusBarHidden: function() { - if (this.unmounted) return; + // This is currently not desired as it is annoying if it keeps expanding and collapsing + // TODO: Find a less annoying way of hiding the status bar + /*if (this.unmounted) return; this.setState({ statusBarVisible: false, - }); + });*/ }, showSettings: function(show) { diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 37cb2977aa..cbb6001d5f 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -16,6 +16,7 @@ limitations under the License. const React = require("react"); const ReactDOM = require("react-dom"); +import PropTypes from 'prop-types'; const GeminiScrollbar = require('react-gemini-scrollbar'); import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; @@ -86,7 +87,7 @@ module.exports = React.createClass({ * scroll down to show the new element, rather than preserving the * existing view. */ - stickyBottom: React.PropTypes.bool, + stickyBottom: PropTypes.bool, /* startAtBottom: if set to true, the view is assumed to start * scrolled to the bottom. @@ -95,7 +96,7 @@ module.exports = React.createClass({ * behaviour stays the same for other uses of ScrollPanel. * If so, let's remove this parameter down the line. */ - startAtBottom: React.PropTypes.bool, + startAtBottom: PropTypes.bool, /* onFillRequest(backwards): a callback which is called on scroll when * the user nears the start (backwards = true) or end (backwards = @@ -110,7 +111,7 @@ module.exports = React.createClass({ * directon (at this time) - which will stop the pagination cycle until * the user scrolls again. */ - onFillRequest: React.PropTypes.func, + onFillRequest: PropTypes.func, /* onUnfillRequest(backwards): a callback which is called on scroll when * there are children elements that are far out of view and could be removed @@ -121,24 +122,24 @@ module.exports = React.createClass({ * first element to remove if removing from the front/bottom, and last element * to remove if removing from the back/top. */ - onUnfillRequest: React.PropTypes.func, + onUnfillRequest: PropTypes.func, /* onScroll: a callback which is called whenever any scroll happens. */ - onScroll: React.PropTypes.func, + onScroll: PropTypes.func, /* onResize: a callback which is called whenever the Gemini scroll * panel is resized */ - onResize: React.PropTypes.func, + onResize: PropTypes.func, /* className: classnames to add to the top-level div */ - className: React.PropTypes.string, + className: PropTypes.string, /* style: styles to add to the top-level div */ - style: React.PropTypes.object, + style: PropTypes.object, }, getDefaultProps: function() { diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 0107ad1db1..49a7a4020a 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -17,79 +17,17 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; -import classNames from 'classnames'; -import FilterStore from '../../stores/FilterStore'; -import FlairStore from '../../stores/FlairStore'; +import TagOrderStore from '../../stores/TagOrderStore'; + +import GroupActions from '../../actions/GroupActions'; +import TagOrderActions from '../../actions/TagOrderActions'; + import sdk from '../../index'; import dis from '../../dispatcher'; -import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; -const TagTile = React.createClass({ - displayName: 'TagTile', +import { DragDropContext, Droppable } from 'react-beautiful-dnd'; - propTypes: { - groupProfile: PropTypes.object, - }, - - contextTypes: { - matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, - }, - - getInitialState() { - return { - hover: false, - }; - }, - - onClick: function(e) { - e.preventDefault(); - e.stopPropagation(); - dis.dispatch({ - action: 'select_tag', - tag: this.props.groupProfile.groupId, - ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e), - shiftKey: e.shiftKey, - }); - }, - - onMouseOver: function() { - this.setState({hover: true}); - }, - - onMouseOut: function() { - this.setState({hover: false}); - }, - - render: function() { - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const RoomTooltip = sdk.getComponent('rooms.RoomTooltip'); - const profile = this.props.groupProfile || {}; - const name = profile.name || profile.groupId; - const avatarHeight = 35; - - const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( - profile.avatarUrl, avatarHeight, avatarHeight, "crop", - ) : null; - - const className = classNames({ - mx_TagTile: true, - mx_TagTile_selected: this.props.selected, - }); - - const tip = this.state.hover ? - : -
    ; - return -
    - - { tip } -
    -
    ; - }, -}); - -export default React.createClass({ +const TagPanel = React.createClass({ displayName: 'TagPanel', contextTypes: { @@ -98,7 +36,7 @@ export default React.createClass({ getInitialState() { return { - joinedGroupProfiles: [], + orderedTags: [], selectedTags: [], }; }, @@ -106,22 +44,25 @@ export default React.createClass({ componentWillMount: function() { this.unmounted = false; this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership); + this.context.matrixClient.on("sync", this.onClientSync); - this._filterStoreToken = FilterStore.addListener(() => { + this._tagOrderStoreToken = TagOrderStore.addListener(() => { if (this.unmounted) { return; } this.setState({ - selectedTags: FilterStore.getSelectedTags(), + orderedTags: TagOrderStore.getOrderedTags() || [], + selectedTags: TagOrderStore.getSelectedTags(), }); }); - - this._fetchJoinedRooms(); + // This could be done by anything with a matrix client + dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); }, componentWillUnmount() { this.unmounted = true; this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); + this.context.matrixClient.removeListener("sync", this.onClientSync); if (this._filterStoreToken) { this._filterStoreToken.remove(); } @@ -129,10 +70,22 @@ export default React.createClass({ _onGroupMyMembership() { if (this.unmounted) return; - this._fetchJoinedRooms(); + dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); }, - onClick() { + onClientSync(syncState, prevState) { + // Consider the client reconnected if there is no error with syncing. + // This means the state could be RECONNECTING, SYNCING or PREPARED. + const reconnected = syncState !== "ERROR" && prevState !== syncState; + if (reconnected) { + // Load joined groups + dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); + } + }, + + onClick(e) { + // Ignore clicks on children + if (e.target !== e.currentTarget) return; dis.dispatch({action: 'deselect_tags'}); }, @@ -141,36 +94,58 @@ export default React.createClass({ dis.dispatch({action: 'view_create_group'}); }, - async _fetchJoinedRooms() { - const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups(); - const joinedGroupIds = joinedGroupResponse.groups; - const joinedGroupProfiles = await Promise.all(joinedGroupIds.map( - (groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), - )); - dis.dispatch({ - action: 'all_tags', - tags: joinedGroupIds, - }); - this.setState({joinedGroupProfiles}); + onTagTileEndDrag(result) { + // Dragged to an invalid destination, not onto a droppable + if (!result.destination) { + return; + } + + // Dispatch synchronously so that the TagPanel receives an + // optimistic update from TagOrderStore before the previous + // state is shown. + dis.dispatch(TagOrderActions.moveTag( + this.context.matrixClient, + result.draggableId, + result.destination.index, + ), true); }, render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); - const tags = this.state.joinedGroupProfiles.map((groupProfile, index) => { - return { + return ; }); - return
    -
    - { tags } -
    + return
    + + + { (provided, snapshot) => ( +
    + { tags } + { provided.placeholder } +
    + ) } +
    +
    ; }, }); +export default TagPanel; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 98f57a60b5..4ade78af85 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -19,6 +19,7 @@ import SettingsStore from "../../settings/SettingsStore"; const React = require('react'); const ReactDOM = require("react-dom"); +import PropTypes from 'prop-types'; import Promise from 'bluebird'; const Matrix = require("matrix-js-sdk"); @@ -58,49 +59,49 @@ var TimelinePanel = React.createClass({ // representing. This may or may not have a room, depending on what it's // a timeline representing. If it has a room, we maintain RRs etc for // that room. - timelineSet: React.PropTypes.object.isRequired, + timelineSet: PropTypes.object.isRequired, - showReadReceipts: React.PropTypes.bool, + showReadReceipts: PropTypes.bool, // Enable managing RRs and RMs. These require the timelineSet to have a room. - manageReadReceipts: React.PropTypes.bool, - manageReadMarkers: React.PropTypes.bool, + manageReadReceipts: PropTypes.bool, + manageReadMarkers: PropTypes.bool, // true to give the component a 'display: none' style. - hidden: React.PropTypes.bool, + hidden: PropTypes.bool, // ID of an event to highlight. If undefined, no event will be highlighted. // typically this will be either 'eventId' or undefined. - highlightedEventId: React.PropTypes.string, + highlightedEventId: PropTypes.string, // id of an event to jump to. If not given, will go to the end of the // live timeline. - eventId: React.PropTypes.string, + eventId: PropTypes.string, // where to position the event given by eventId, in pixels from the // bottom of the viewport. If not given, will try to put the event // half way down the viewport. - eventPixelOffset: React.PropTypes.number, + eventPixelOffset: PropTypes.number, // Should we show URL Previews - showUrlPreview: React.PropTypes.bool, + showUrlPreview: PropTypes.bool, // callback which is called when the panel is scrolled. - onScroll: React.PropTypes.func, + onScroll: PropTypes.func, // callback which is called when the read-up-to mark is updated. - onReadMarkerUpdated: React.PropTypes.func, + onReadMarkerUpdated: PropTypes.func, // maximum number of events to show in a timeline - timelineCap: React.PropTypes.number, + timelineCap: PropTypes.number, // classname to use for the messagepanel - className: React.PropTypes.string, + className: PropTypes.string, // shape property to be passed to EventTiles - tileShape: React.PropTypes.string, + tileShape: PropTypes.string, // placeholder text to use if the timeline is empty - empty: React.PropTypes.string, + empty: PropTypes.string, }, statics: { @@ -301,6 +302,8 @@ var TimelinePanel = React.createClass({ // set off a pagination request. onMessageListFillRequest: function(backwards) { + if (!this._shouldPaginate()) return Promise.resolve(false); + const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate'; const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating'; @@ -1090,6 +1093,17 @@ var TimelinePanel = React.createClass({ }, this.props.onReadMarkerUpdated); }, + _shouldPaginate: function() { + // don't try to paginate while events in the timeline are + // still being decrypted. We don't render events while they're + // being decrypted, so they don't take up space in the timeline. + // This means we can pull quite a lot of events into the timeline + // and end up trying to render a lot of events. + return !this.state.events.some((e) => { + return e.isBeingDecrypted(); + }); + }, + render: function() { const MessagePanel = sdk.getComponent("structures.MessagePanel"); const Loader = sdk.getComponent("elements.Spinner"); diff --git a/src/components/structures/UploadBar.js b/src/components/structures/UploadBar.js index ca566d3a64..fed4ff33b3 100644 --- a/src/components/structures/UploadBar.js +++ b/src/components/structures/UploadBar.js @@ -15,6 +15,7 @@ limitations under the License. */ const React = require('react'); +import PropTypes from 'prop-types'; const ContentMessages = require('../../ContentMessages'); const dis = require('../../dispatcher'); const filesize = require('filesize'); @@ -22,7 +23,7 @@ import { _t } from '../../languageHandler'; module.exports = React.createClass({displayName: 'UploadBar', propTypes: { - room: React.PropTypes.object, + room: PropTypes.object, }, componentDidMount: function() { diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 09844c3d63..b1eedd1a90 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -19,6 +19,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; const React = require('react'); const ReactDOM = require('react-dom'); +import PropTypes from 'prop-types'; const sdk = require('../../index'); const MatrixClientPeg = require("../../MatrixClientPeg"); const PlatformPeg = require("../../PlatformPeg"); @@ -125,8 +126,8 @@ const THEMES = [ const IgnoredUser = React.createClass({ propTypes: { - userId: React.PropTypes.string.isRequired, - onUnignored: React.PropTypes.func.isRequired, + userId: PropTypes.string.isRequired, + onUnignored: PropTypes.func.isRequired, }, _onUnignoreClick: function() { @@ -155,16 +156,16 @@ module.exports = React.createClass({ displayName: 'UserSettings', propTypes: { - onClose: React.PropTypes.func, + onClose: PropTypes.func, // The brand string given when creating email pushers - brand: React.PropTypes.string, + brand: PropTypes.string, // The base URL to use in the referral link. Defaults to window.location.origin. - referralBaseUrl: React.PropTypes.string, + referralBaseUrl: PropTypes.string, // Team token for the referral link. If falsy, the referral section will // not appear - teamToken: React.PropTypes.string, + teamToken: PropTypes.string, }, getDefaultProps: function() { @@ -375,7 +376,7 @@ module.exports = React.createClass({ { _t("For security, logging out will delete any end-to-end " + "encryption keys from this browser. If you want to be able " + "to decrypt your conversation history from future Riot sessions, " + - "please export your room keys for safe-keeping.") }. + "please export your room keys for safe-keeping.") }
    , button: _t("Sign out"), extraButtons: [ @@ -811,6 +812,12 @@ module.exports = React.createClass({

    { _t('Analytics') }

    { _t('Riot collects anonymous analytics to allow us to improve the application.') } +
    + { _t('Privacy is important to us, so we don\'t collect any personal' + + ' or identifiable data for our analytics.') } +
    + { _t('Learn more about how we use analytics.') } +
    { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
    ; diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 43753bfd38..53688ee6c3 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -18,6 +18,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from "../../../Modal"; @@ -29,13 +30,13 @@ module.exports = React.createClass({ displayName: 'ForgotPassword', propTypes: { - defaultHsUrl: React.PropTypes.string, - defaultIsUrl: React.PropTypes.string, - customHsUrl: React.PropTypes.string, - customIsUrl: React.PropTypes.string, - onLoginClick: React.PropTypes.func, - onRegisterClick: React.PropTypes.func, - onComplete: React.PropTypes.func.isRequired, + defaultHsUrl: PropTypes.string, + defaultIsUrl: PropTypes.string, + customHsUrl: PropTypes.string, + customIsUrl: PropTypes.string, + onLoginClick: PropTypes.func, + onRegisterClick: PropTypes.func, + onComplete: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 9ed710534b..f4c08e8362 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -18,6 +18,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler'; import sdk from '../../../index'; @@ -36,27 +37,27 @@ module.exports = React.createClass({ displayName: 'Login', propTypes: { - onLoggedIn: React.PropTypes.func.isRequired, + onLoggedIn: PropTypes.func.isRequired, - enableGuest: React.PropTypes.bool, + enableGuest: PropTypes.bool, - customHsUrl: React.PropTypes.string, - customIsUrl: React.PropTypes.string, - defaultHsUrl: React.PropTypes.string, - defaultIsUrl: React.PropTypes.string, + customHsUrl: PropTypes.string, + customIsUrl: PropTypes.string, + defaultHsUrl: PropTypes.string, + defaultIsUrl: PropTypes.string, // Secondary HS which we try to log into if the user is using // the default HS but login fails. Useful for migrating to a // different home server without confusing users. - fallbackHsUrl: React.PropTypes.string, + fallbackHsUrl: PropTypes.string, - defaultDeviceDisplayName: React.PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // login shouldn't know or care how registration is done. - onRegisterClick: React.PropTypes.func.isRequired, + onRegisterClick: PropTypes.func.isRequired, // login shouldn't care how password recovery is done. - onForgotPasswordClick: React.PropTypes.func, - onCancelClick: React.PropTypes.func, + onForgotPasswordClick: PropTypes.func, + onCancelClick: PropTypes.func, }, getInitialState: function() { diff --git a/src/components/structures/login/PostRegistration.js b/src/components/structures/login/PostRegistration.js index 184356e852..f6165348bd 100644 --- a/src/components/structures/login/PostRegistration.js +++ b/src/components/structures/login/PostRegistration.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; @@ -25,7 +26,7 @@ module.exports = React.createClass({ displayName: 'PostRegistration', propTypes: { - onComplete: React.PropTypes.func.isRequired, + onComplete: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index e57b7fd0c2..b8a85c5f82 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -19,6 +19,7 @@ import Matrix from 'matrix-js-sdk'; import Promise from 'bluebird'; import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import ServerConfig from '../../views/login/ServerConfig'; @@ -35,31 +36,31 @@ module.exports = React.createClass({ displayName: 'Registration', propTypes: { - onLoggedIn: React.PropTypes.func.isRequired, - clientSecret: React.PropTypes.string, - sessionId: React.PropTypes.string, - makeRegistrationUrl: React.PropTypes.func.isRequired, - idSid: React.PropTypes.string, - customHsUrl: React.PropTypes.string, - customIsUrl: React.PropTypes.string, - defaultHsUrl: React.PropTypes.string, - defaultIsUrl: React.PropTypes.string, - brand: React.PropTypes.string, - email: React.PropTypes.string, - referrer: React.PropTypes.string, - teamServerConfig: React.PropTypes.shape({ + onLoggedIn: PropTypes.func.isRequired, + clientSecret: PropTypes.string, + sessionId: PropTypes.string, + makeRegistrationUrl: PropTypes.func.isRequired, + idSid: PropTypes.string, + customHsUrl: PropTypes.string, + customIsUrl: PropTypes.string, + defaultHsUrl: PropTypes.string, + defaultIsUrl: PropTypes.string, + brand: PropTypes.string, + email: PropTypes.string, + referrer: PropTypes.string, + teamServerConfig: PropTypes.shape({ // Email address to request new teams - supportEmail: React.PropTypes.string.isRequired, + supportEmail: PropTypes.string.isRequired, // URL of the riot-team-server to get team configurations and track referrals - teamServerURL: React.PropTypes.string.isRequired, + teamServerURL: PropTypes.string.isRequired, }), - teamSelected: React.PropTypes.object, + teamSelected: PropTypes.object, - defaultDeviceDisplayName: React.PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // registration shouldn't know or care how login is done. - onLoginClick: React.PropTypes.func.isRequired, - onCancelClick: React.PropTypes.func, + onLoginClick: PropTypes.func.isRequired, + onCancelClick: PropTypes.func, }, getInitialState: function() { diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index f68e98ec3d..5735a99125 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -15,6 +15,8 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; +import { MatrixClient } from 'matrix-js-sdk'; import AvatarLogic from '../../../Avatar'; import sdk from '../../../index'; import AccessibleButton from '../elements/AccessibleButton'; @@ -23,16 +25,20 @@ module.exports = React.createClass({ displayName: 'BaseAvatar', propTypes: { - name: React.PropTypes.string.isRequired, // The name (first initial used as default) - idName: React.PropTypes.string, // ID for generating hash colours - title: React.PropTypes.string, // onHover title text - url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0] - urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority] - width: React.PropTypes.number, - height: React.PropTypes.number, + name: PropTypes.string.isRequired, // The name (first initial used as default) + idName: PropTypes.string, // ID for generating hash colours + title: PropTypes.string, // onHover title text + url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0] + urls: PropTypes.array, // [highest_priority, ... , lowest_priority] + width: PropTypes.number, + height: PropTypes.number, // XXX resizeMethod not actually used. - resizeMethod: React.PropTypes.string, - defaultToInitialLetter: React.PropTypes.bool, // true to add default url + resizeMethod: PropTypes.string, + defaultToInitialLetter: PropTypes.bool, // true to add default url + }, + + contextTypes: { + matrixClient: PropTypes.instanceOf(MatrixClient), }, getDefaultProps: function() { @@ -48,6 +54,16 @@ module.exports = React.createClass({ return this._getState(this.props); }, + componentWillMount() { + this.unmounted = false; + this.context.matrixClient.on('sync', this.onClientSync); + }, + + componentWillUnmount() { + this.unmounted = true; + this.context.matrixClient.removeListener('sync', this.onClientSync); + }, + componentWillReceiveProps: function(nextProps) { // work out if we need to call setState (if the image URLs array has changed) const newState = this._getState(nextProps); @@ -66,6 +82,23 @@ module.exports = React.createClass({ } }, + onClientSync(syncState, prevState) { + if (this.unmounted) return; + + // Consider the client reconnected if there is no error with syncing. + // This means the state could be RECONNECTING, SYNCING or PREPARED. + const reconnected = syncState !== "ERROR" && prevState !== syncState; + if (reconnected && + // Did we fall back? + this.state.urlsIndex > 0 + ) { + // Start from the highest priority URL again + this.setState({ + urlsIndex: 0, + }); + } + }, + _getState: function(props) { // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, props.urls, default image ] diff --git a/src/components/views/avatars/MemberAvatar.js b/src/components/views/avatars/MemberAvatar.js index 89047cd69c..a4fe5e280f 100644 --- a/src/components/views/avatars/MemberAvatar.js +++ b/src/components/views/avatars/MemberAvatar.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; const React = require('react'); +import PropTypes from 'prop-types'; const Avatar = require('../../../Avatar'); const sdk = require("../../../index"); const dispatcher = require("../../../dispatcher"); @@ -25,15 +26,15 @@ module.exports = React.createClass({ displayName: 'MemberAvatar', propTypes: { - member: React.PropTypes.object.isRequired, - width: React.PropTypes.number, - height: React.PropTypes.number, - resizeMethod: React.PropTypes.string, + member: PropTypes.object.isRequired, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, // The onClick to give the avatar - onClick: React.PropTypes.func, + onClick: PropTypes.func, // Whether the onClick of the avatar should be overriden to dispatch 'view_user' - viewUserOnClick: React.PropTypes.bool, - title: React.PropTypes.string, + viewUserOnClick: PropTypes.bool, + title: PropTypes.string, }, getDefaultProps: function() { diff --git a/src/components/views/avatars/MemberPresenceAvatar.js b/src/components/views/avatars/MemberPresenceAvatar.js index 49cfee2cff..aa6def00ae 100644 --- a/src/components/views/avatars/MemberPresenceAvatar.js +++ b/src/components/views/avatars/MemberPresenceAvatar.js @@ -17,6 +17,7 @@ 'use strict'; import React from "react"; +import PropTypes from 'prop-types'; import * as sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; import AccessibleButton from '../elements/AccessibleButton'; @@ -30,10 +31,10 @@ module.exports = React.createClass({ displayName: 'MemberPresenceAvatar', propTypes: { - member: React.PropTypes.object.isRequired, - width: React.PropTypes.number, - height: React.PropTypes.number, - resizeMethod: React.PropTypes.string, + member: PropTypes.object.isRequired, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, }, getDefaultProps: function() { diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index 11554b2379..cae02ac408 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from "react"; +import PropTypes from 'prop-types'; import {ContentRepo} from "matrix-js-sdk"; import MatrixClientPeg from "../../../MatrixClientPeg"; import sdk from "../../../index"; @@ -25,11 +26,11 @@ module.exports = React.createClass({ // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) propTypes: { - room: React.PropTypes.object, - oobData: React.PropTypes.object, - width: React.PropTypes.number, - height: React.PropTypes.number, - resizeMethod: React.PropTypes.string, + room: PropTypes.object, + oobData: PropTypes.object, + width: PropTypes.number, + height: PropTypes.number, + resizeMethod: PropTypes.string, }, getDefaultProps: function() { diff --git a/src/components/views/create_room/CreateRoomButton.js b/src/components/views/create_room/CreateRoomButton.js index 8a5f00d942..25f71f542d 100644 --- a/src/components/views/create_room/CreateRoomButton.js +++ b/src/components/views/create_room/CreateRoomButton.js @@ -17,11 +17,12 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; module.exports = React.createClass({ displayName: 'CreateRoomButton', propTypes: { - onCreateRoom: React.PropTypes.func, + onCreateRoom: PropTypes.func, }, getDefaultProps: function() { diff --git a/src/components/views/create_room/Presets.js b/src/components/views/create_room/Presets.js index 2073896d87..c9607c0082 100644 --- a/src/components/views/create_room/Presets.js +++ b/src/components/views/create_room/Presets.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; const React = require('react'); +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; const Presets = { @@ -28,8 +29,8 @@ const Presets = { module.exports = React.createClass({ displayName: 'CreateRoomPresets', propTypes: { - onChange: React.PropTypes.func, - preset: React.PropTypes.string, + onChange: PropTypes.func, + preset: PropTypes.string, }, Presets: Presets, diff --git a/src/components/views/create_room/RoomAlias.js b/src/components/views/create_room/RoomAlias.js index d4228a8bca..6262db7833 100644 --- a/src/components/views/create_room/RoomAlias.js +++ b/src/components/views/create_room/RoomAlias.js @@ -15,6 +15,7 @@ limitations under the License. */ const React = require('react'); +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; module.exports = React.createClass({ @@ -22,9 +23,9 @@ module.exports = React.createClass({ propTypes: { // Specifying a homeserver will make magical things happen when you, // e.g. start typing in the room alias box. - homeserver: React.PropTypes.string, - alias: React.PropTypes.string, - onChange: React.PropTypes.func, + homeserver: PropTypes.string, + alias: PropTypes.string, + onChange: PropTypes.func, }, getDefaultProps: function() { diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 837d2f5349..685c4fcde3 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -20,7 +20,6 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import AccessibleButton from '../elements/AccessibleButton'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStoreCache from '../../../stores/GroupStoreCache'; @@ -507,7 +506,8 @@ module.exports = React.createClass({ }, render: function() { - const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AddressSelector = sdk.getComponent("elements.AddressSelector"); this.scrollElement = null; @@ -580,14 +580,8 @@ module.exports = React.createClass({ } return ( -
    -
    - { this.props.title } -
    - - - +
    @@ -597,12 +591,10 @@ module.exports = React.createClass({ { addressSelector } { this.props.extraNode }
    -
    - -
    -
    + + ); }, }); diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 7fb642b560..08fd972621 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; import FocusTrap from 'focus-trap-react'; +import PropTypes from 'prop-types'; import { KeyCode } from '../../../Keyboard'; import AccessibleButton from '../elements/AccessibleButton'; @@ -32,17 +33,20 @@ export default React.createClass({ propTypes: { // onFinished callback to call when Escape is pressed - onFinished: React.PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, + + // called when a key is pressed + onKeyDown: PropTypes.func, // CSS class to apply to dialog div - className: React.PropTypes.string, + className: PropTypes.string, // Title for the dialog. // (could probably actually be something more complicated than a string if desired) - title: React.PropTypes.string.isRequired, + title: PropTypes.string.isRequired, // children should be the content of the dialog - children: React.PropTypes.node, + children: PropTypes.node, // Id of content element // If provided, this is used to add a aria-describedby attribute @@ -50,6 +54,9 @@ export default React.createClass({ }, _onKeyDown: function(e) { + if (this.props.onKeyDown) { + this.props.onKeyDown(e); + } if (e.keyCode === KeyCode.ESCAPE) { e.stopPropagation(); e.preventDefault(); @@ -76,7 +83,7 @@ export default React.createClass({ > -
    +
    { this.props.title }
    { this.props.children } diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index 0623177e1a..b3549bb8bf 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -137,6 +138,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { } else { // Show the avatar, name and a button to confirm that a new chat is requested const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const Spinner = sdk.getComponent('elements.Spinner'); title = _t('Start chatting'); @@ -166,11 +168,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {

    { profile }
    -
    - -
    +
    ; } @@ -187,10 +186,10 @@ export default class ChatCreateOrReuseDialog extends React.Component { } } -ChatCreateOrReuseDialog.propTypes = { - userId: React.PropTypes.string.isRequired, +ChatCreateOrReuseDialog.propTyps = { + userId: PropTypes.string.isRequired, // Called when clicking outside of the dialog - onFinished: React.PropTypes.func.isRequired, - onNewDMClick: React.PropTypes.func.isRequired, - onExistingRoomSelected: React.PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, + onNewDMClick: PropTypes.func.isRequired, + onExistingRoomSelected: PropTypes.func.isRequired, }; diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index 1c246a580b..b65d98d78d 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -15,10 +15,10 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import classnames from 'classnames'; import { GroupMemberType } from '../../../groups'; /* @@ -33,20 +33,20 @@ export default React.createClass({ displayName: 'ConfirmUserActionDialog', propTypes: { // matrix-js-sdk (room) member object. Supply either this or 'groupMember' - member: React.PropTypes.object, + member: PropTypes.object, // group member object. Supply either this or 'member' groupMember: GroupMemberType, // needed if a group member is specified - matrixClient: React.PropTypes.instanceOf(MatrixClient), - action: React.PropTypes.string.isRequired, // eg. 'Ban' - title: React.PropTypes.string.isRequired, // eg. 'Ban this user?' + matrixClient: PropTypes.instanceOf(MatrixClient), + action: PropTypes.string.isRequired, // eg. 'Ban' + title: PropTypes.string.isRequired, // eg. 'Ban this user?' // Whether to display a text field for a reason // If true, the second argument to onFinished will // be the string entered. - askReason: React.PropTypes.bool, - danger: React.PropTypes.bool, - onFinished: React.PropTypes.func.isRequired, + askReason: PropTypes.bool, + danger: PropTypes.bool, + onFinished: PropTypes.func.isRequired, }, defaultProps: { @@ -76,13 +76,11 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); - const confirmButtonClass = classnames({ - 'mx_Dialog_primary': true, - 'danger': this.props.danger, - }); + const confirmButtonClass = this.props.danger ? 'danger' : ''; let reasonBox; if (this.props.askReason) { @@ -127,17 +125,11 @@ export default React.createClass({
    { userId }
    { reasonBox } -
    - - - -
    + ); }, diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 00b9e99021..04f99a0e15 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -55,11 +55,15 @@ export default React.createClass({ _checkGroupId: function(e) { let error = null; - if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) { + if (!this.state.groupId) { + error = _t("Community IDs cannot not be empty."); + } else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) { error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'"); } this.setState({ groupIdError: error, + // Reset createError to get rid of now stale error message + createError: null, }); return error; }, @@ -158,10 +162,10 @@ export default React.createClass({ { createErrorNode }
    + -
    diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index cc04f4202b..7a8852bc00 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import { _t } from '../../../languageHandler'; @@ -22,7 +23,7 @@ import { _t } from '../../../languageHandler'; export default React.createClass({ displayName: 'CreateRoomDialog', propTypes: { - onFinished: React.PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, }, componentDidMount: function() { @@ -41,6 +42,7 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( -
    - -
    - { _t('Advanced options') } -
    - - -
    -
    - -
    - -
    +
    ); }, diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index c45e072d72..87228b4733 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import Analytics from '../../../Analytics'; @@ -77,6 +78,7 @@ export default class DeactivateAccountDialog extends React.Component { } render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const Loader = sdk.getComponent("elements.Spinner"); let passwordBoxClass = ''; @@ -99,10 +101,11 @@ export default class DeactivateAccountDialog extends React.Component { } return ( -
    -
    - { _t("Deactivate Account") } -
    +

    { _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }

    @@ -130,11 +133,11 @@ export default class DeactivateAccountDialog extends React.Component { { cancelButton }
    -
    + ); } } DeactivateAccountDialog.propTypes = { - onFinished: React.PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, }; diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index ba31d2a8c2..6bec933389 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import * as FormattingUtils from '../../../utils/FormattingUtils'; @@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) { } DeviceVerifyDialog.propTypes = { - userId: React.PropTypes.string.isRequired, - device: React.PropTypes.object.isRequired, - onFinished: React.PropTypes.func.isRequired, + userId: PropTypes.string.isRequired, + device: PropTypes.object.isRequired, + onFinished: PropTypes.func.isRequired, }; diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 0910264cef..a055f07629 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -26,20 +26,21 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; export default React.createClass({ displayName: 'ErrorDialog', propTypes: { - title: React.PropTypes.string, - description: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.string, + title: PropTypes.string, + description: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.string, ]), - button: React.PropTypes.string, - focus: React.PropTypes.bool, - onFinished: React.PropTypes.func.isRequired, + button: PropTypes.string, + focus: PropTypes.bool, + onFinished: PropTypes.func.isRequired, }, getDefaultProps: function() { diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index 5b0e34df2c..b682156072 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -27,22 +28,22 @@ export default React.createClass({ propTypes: { // matrix client to use for UI auth requests - matrixClient: React.PropTypes.object.isRequired, + matrixClient: PropTypes.object.isRequired, // response from initial request. If not supplied, will do a request on // mount. - authData: React.PropTypes.shape({ - flows: React.PropTypes.array, - params: React.PropTypes.object, - session: React.PropTypes.string, + authData: PropTypes.shape({ + flows: PropTypes.array, + params: PropTypes.object, + session: PropTypes.string, }), // callback - makeRequest: React.PropTypes.func.isRequired, + makeRequest: PropTypes.func.isRequired, - onFinished: React.PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, - title: React.PropTypes.string, + title: PropTypes.string, }, getInitialState: function() { diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index 821939ff0d..b9b64a69d2 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -16,6 +16,7 @@ limitations under the License. import Modal from '../../../Modal'; import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; @@ -30,10 +31,10 @@ import { _t, _td } from '../../../languageHandler'; */ export default React.createClass({ propTypes: { - matrixClient: React.PropTypes.object.isRequired, - userId: React.PropTypes.string.isRequired, - deviceId: React.PropTypes.string.isRequired, - onFinished: React.PropTypes.func.isRequired, + matrixClient: PropTypes.object.isRequired, + userId: PropTypes.string.isRequired, + deviceId: PropTypes.string.isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/components/views/dialogs/QuestionDialog.js b/src/components/views/dialogs/QuestionDialog.js index 41733470a1..7e11677310 100644 --- a/src/components/views/dialogs/QuestionDialog.js +++ b/src/components/views/dialogs/QuestionDialog.js @@ -16,20 +16,20 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import classnames from 'classnames'; export default React.createClass({ displayName: 'QuestionDialog', propTypes: { - title: React.PropTypes.string, - description: React.PropTypes.node, - extraButtons: React.PropTypes.node, - button: React.PropTypes.string, - danger: React.PropTypes.bool, - focus: React.PropTypes.bool, - onFinished: React.PropTypes.func.isRequired, + title: PropTypes.string, + description: PropTypes.node, + extraButtons: PropTypes.node, + button: PropTypes.string, + danger: PropTypes.bool, + focus: PropTypes.bool, + onFinished: PropTypes.func.isRequired, }, getDefaultProps: function() { @@ -53,15 +53,11 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const cancelButton = this.props.hasCancelButton ? ( - - ) : null; - const buttonClasses = classnames({ - mx_Dialog_primary: true, - danger: this.props.danger, - }); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + let primaryButtonClass = ""; + if (this.props.danger) { + primaryButtonClass = "danger"; + } return ( { this.props.description } -
    - + { this.props.extraButtons } - { cancelButton } -
    +
    ); }, diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index f0006f3c1a..451785197e 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import SdkConfig from '../../../SdkConfig'; import Modal from '../../../Modal'; @@ -25,8 +26,8 @@ export default React.createClass({ displayName: 'SessionRestoreErrorDialog', propTypes: { - error: React.PropTypes.string.isRequired, - onFinished: React.PropTypes.func.isRequired, + error: PropTypes.string.isRequired, + onFinished: PropTypes.func.isRequired, }, componentDidMount: function() { @@ -46,6 +47,7 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let bugreport; if (SdkConfig.get().bug_report_endpoint_url) { @@ -78,11 +80,9 @@ export default React.createClass({ { bugreport } -
    - -
    + ); }, diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index ba054b0c27..d80574804f 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import Email from '../../../email'; import AddThreepid from '../../../AddThreepid'; @@ -30,7 +31,7 @@ import Modal from '../../../Modal'; export default React.createClass({ displayName: 'SetEmailDialog', propTypes: { - onFinished: React.PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index b3ab499876..c6427001ad 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -17,6 +17,7 @@ limitations under the License. import Promise from 'bluebird'; import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; @@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250; export default React.createClass({ displayName: 'SetMxIdDialog', propTypes: { - onFinished: React.PropTypes.func.isRequired, + onFinished: PropTypes.func.isRequired, // Called when the user requests to register with a different homeserver - onDifferentServerClicked: React.PropTypes.func.isRequired, + onDifferentServerClicked: PropTypes.func.isRequired, // Called if the user wants to switch to login instead - onLoginClick: React.PropTypes.func.isRequired, + onLoginClick: PropTypes.func.isRequired, }, getInitialState: function() { diff --git a/src/components/views/dialogs/TextInputDialog.js b/src/components/views/dialogs/TextInputDialog.js index 4d73752641..f28b16ef6f 100644 --- a/src/components/views/dialogs/TextInputDialog.js +++ b/src/components/views/dialogs/TextInputDialog.js @@ -15,21 +15,21 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; -import { _t } from '../../../languageHandler'; export default React.createClass({ displayName: 'TextInputDialog', propTypes: { - title: React.PropTypes.string, - description: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.string, + title: PropTypes.string, + description: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.string, ]), - value: React.PropTypes.string, - button: React.PropTypes.string, - focus: React.PropTypes.bool, - onFinished: React.PropTypes.func.isRequired, + value: PropTypes.string, + button: PropTypes.string, + focus: PropTypes.bool, + onFinished: PropTypes.func.isRequired, }, getDefaultProps: function() { @@ -58,6 +58,7 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( -
    - - -
    +
    ); }, diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 21aaa2ed4c..26aa5d3ecb 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -23,14 +23,7 @@ import GeminiScrollbar from 'react-gemini-scrollbar'; import Resend from '../../../Resend'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; - -function markAllDevicesKnown(devices) { - Object.keys(devices).forEach((userId) => { - Object.keys(devices[userId]).map((deviceId) => { - MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true); - }); - }); -} +import { markAllDevicesKnown } from '../../../cryptodevices'; function DeviceListEntry(props) { const {userId, device} = props; @@ -141,7 +134,7 @@ export default React.createClass({ }, _onSendAnywayClicked: function() { - markAllDevicesKnown(this.props.devices); + markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices); this.props.onFinished(); this.props.onSend(); @@ -187,18 +180,11 @@ export default React.createClass({ } }); }); - let sendButton; - if (haveUnknownDevices) { - sendButton = ; - } else { - sendButton = ; - } + const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked; + const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel; const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( -
    - {sendButton} - -
    +
    ); // XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point? diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index ee8fd20d6f..e30ceb85fa 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -15,6 +15,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import { KeyCode } from '../../../Keyboard'; @@ -72,9 +73,9 @@ export default function AccessibleButton(props) { * implemented exactly like a normal onClick handler. */ AccessibleButton.propTypes = { - children: React.PropTypes.node, - element: React.PropTypes.string, - onClick: React.PropTypes.func.isRequired, + children: PropTypes.node, + element: PropTypes.string, + onClick: PropTypes.func.isRequired, }; AccessibleButton.defaultProps = { diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.js index 9330206a39..b4279c7f70 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.js @@ -18,6 +18,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import classNames from 'classnames'; import { UserAddressType } from '../../../UserAddress'; @@ -26,17 +27,17 @@ export default React.createClass({ displayName: 'AddressSelector', propTypes: { - onSelected: React.PropTypes.func.isRequired, + onSelected: PropTypes.func.isRequired, // List of the addresses to display - addressList: React.PropTypes.arrayOf(UserAddressType).isRequired, + addressList: PropTypes.arrayOf(UserAddressType).isRequired, // Whether to show the address on the address tiles - showAddress: React.PropTypes.bool, - truncateAt: React.PropTypes.number.isRequired, - selected: React.PropTypes.number, + showAddress: PropTypes.bool, + truncateAt: PropTypes.number.isRequired, + selected: PropTypes.number, // Element to put as a header on top of the list - header: React.PropTypes.node, + header: PropTypes.node, }, getInitialState: function() { diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index c8ea4062b1..16e340756a 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import classNames from 'classnames'; import sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; @@ -28,9 +29,9 @@ export default React.createClass({ propTypes: { address: UserAddressType.isRequired, - canDismiss: React.PropTypes.bool, - onDismissed: React.PropTypes.func, - justified: React.PropTypes.bool, + canDismiss: PropTypes.bool, + onDismissed: PropTypes.func, + justified: PropTypes.bool, }, getDefaultProps: function() { diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 0d67b4c814..a63823555f 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -19,6 +19,7 @@ limitations under the License. import url from 'url'; import qs from 'querystring'; import React from 'react'; +import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; @@ -40,19 +41,19 @@ export default React.createClass({ displayName: 'AppTile', propTypes: { - id: React.PropTypes.string.isRequired, - url: React.PropTypes.string.isRequired, - name: React.PropTypes.string.isRequired, - room: React.PropTypes.object.isRequired, - type: React.PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + url: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + room: PropTypes.object.isRequired, + type: PropTypes.string.isRequired, // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer. // This should be set to true when there is only one widget in the app drawer, otherwise it should be false. - fullWidth: React.PropTypes.bool, + fullWidth: PropTypes.bool, // UserId of the current user - userId: React.PropTypes.string.isRequired, + userId: PropTypes.string.isRequired, // UserId of the entity that added / modified the widget - creatorUserId: React.PropTypes.string, - waitForIframeLoad: React.PropTypes.bool, + creatorUserId: PropTypes.string, + waitForIframeLoad: PropTypes.bool, }, getDefaultProps() { diff --git a/src/components/views/elements/DNDTagTile.js b/src/components/views/elements/DNDTagTile.js new file mode 100644 index 0000000000..e17ea52976 --- /dev/null +++ b/src/components/views/elements/DNDTagTile.js @@ -0,0 +1,43 @@ +/* eslint new-cap: "off" */ +/* +Copyright 2017 New Vector 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 TagTile from './TagTile'; + +import { Draggable } from 'react-beautiful-dnd'; + +export default function DNDTagTile(props) { + return
    + + { (provided, snapshot) => ( +
    +
    + +
    + { provided.placeholder } +
    + ) } +
    +
    ; +} diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index bb89efaa30..c775cba610 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; import sdk from '../../../index'; import Modal from '../../../Modal'; @@ -24,8 +25,8 @@ export default React.createClass({ displayName: 'DeviceVerifyButtons', propTypes: { - userId: React.PropTypes.string.isRequired, - device: React.PropTypes.object.isRequired, + userId: PropTypes.string.isRequired, + device: PropTypes.object.isRequired, }, getInitialState: function() { diff --git a/src/components/views/elements/DialogButtons.js b/src/components/views/elements/DialogButtons.js new file mode 100644 index 0000000000..c159324c1d --- /dev/null +++ b/src/components/views/elements/DialogButtons.js @@ -0,0 +1,62 @@ +/* +Copyright 2017 Aidan Gauland + +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. +*/ + +"use strict"; + +import React from "react"; +import PropTypes from "prop-types"; +import { _t } from '../../../languageHandler'; + +/** + * Basic container for buttons in modal dialogs. + */ +module.exports = React.createClass({ + displayName: "DialogButtons", + + propTypes: { + // The primary button which is styled differently and has default focus. + primaryButton: PropTypes.node.isRequired, + + // onClick handler for the primary button. + onPrimaryButtonClick: PropTypes.func.isRequired, + + // onClick handler for the cancel button. + onCancel: PropTypes.func.isRequired, + + focus: PropTypes.bool, + }, + + render: function() { + let primaryButtonClassName = "mx_Dialog_primary"; + if (this.props.primaryButtonClass) { + primaryButtonClassName += " " + this.props.primaryButtonClass; + } + return ( +
    + + { this.props.children } + +
    + ); + }, +}); diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js index 7132162d5c..14f79687e1 100644 --- a/src/components/views/elements/DirectorySearchBox.js +++ b/src/components/views/elements/DirectorySearchBox.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import classnames from 'classnames'; export default class DirectorySearchBox extends React.Component { @@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component { } DirectorySearchBox.propTypes = { - className: React.PropTypes.string, - onChange: React.PropTypes.func, - onClear: React.PropTypes.func, - onJoinClick: React.PropTypes.func, - placeholder: React.PropTypes.string, - showJoinButton: React.PropTypes.bool, + className: PropTypes.string, + onChange: PropTypes.func, + onClear: PropTypes.func, + onJoinClick: PropTypes.func, + placeholder: PropTypes.string, + showJoinButton: PropTypes.bool, }; diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index b1291710b7..10111e415e 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import classnames from 'classnames'; import AccessibleButton from './AccessibleButton'; import { _t } from '../../../languageHandler'; @@ -56,14 +57,14 @@ class MenuOption extends React.Component { } MenuOption.propTypes = { - children: React.PropTypes.oneOfType([ - React.PropTypes.arrayOf(React.PropTypes.node), - React.PropTypes.node, + children: PropTypes.oneOfType([ + PropTypes.arrayOf(React.PropTypes.node), + PropTypes.node, ]), - highlighted: React.PropTypes.bool, - dropdownKey: React.PropTypes.string, - onClick: React.PropTypes.func.isRequired, - onMouseEnter: React.PropTypes.func.isRequired, + highlighted: PropTypes.bool, + dropdownKey: PropTypes.string, + onClick: PropTypes.func.isRequired, + onMouseEnter: PropTypes.func.isRequired, }; /* @@ -322,20 +323,20 @@ Dropdown.propTypes = { // The width that the dropdown should be. If specified, // the dropped-down part of the menu will be set to this // width. - menuWidth: React.PropTypes.number, + menuWidth: PropTypes.number, // Called when the selected option changes - onOptionChange: React.PropTypes.func.isRequired, + onOptionChange: PropTypes.func.isRequired, // Called when the value of the search field changes - onSearchChange: React.PropTypes.func, - searchEnabled: React.PropTypes.bool, + onSearchChange: PropTypes.func, + searchEnabled: PropTypes.bool, // Function that, given the key of an option, returns // a node representing that option to be displayed in the // box itself as the currently-selected option (ie. as // opposed to in the actual dropped-down part). If // unspecified, the appropriate child element is used as // in the dropped-down menu. - getShortOption: React.PropTypes.func, - value: React.PropTypes.string, + getShortOption: PropTypes.func, + value: PropTypes.string, // negative for consistency with HTML - disabled: React.PropTypes.bool, + disabled: PropTypes.bool, }; diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index ac8821a0c2..ce1817c272 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; const React = require('react'); +import PropTypes from 'prop-types'; const KEY_TAB = 9; const KEY_SHIFT = 16; @@ -26,18 +27,18 @@ module.exports = React.createClass({ displayName: 'EditableText', propTypes: { - onValueChanged: React.PropTypes.func, - initialValue: React.PropTypes.string, - label: React.PropTypes.string, - placeholder: React.PropTypes.string, - className: React.PropTypes.string, - labelClassName: React.PropTypes.string, - placeholderClassName: React.PropTypes.string, + onValueChanged: PropTypes.func, + initialValue: PropTypes.string, + label: PropTypes.string, + placeholder: PropTypes.string, + className: PropTypes.string, + labelClassName: PropTypes.string, + placeholderClassName: PropTypes.string, // Overrides blurToSubmit if true - blurToCancel: React.PropTypes.bool, + blurToCancel: PropTypes.bool, // Will cause onValueChanged(value, true) to fire on blur - blurToSubmit: React.PropTypes.bool, - editable: React.PropTypes.bool, + blurToSubmit: PropTypes.bool, + editable: PropTypes.bool, }, Phases: { diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 0025862967..064d2f1c39 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import Promise from 'bluebird'; @@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component { EditableTextContainer.propTypes = { /* callback to retrieve the initial value. */ - getInitialValue: React.PropTypes.func, + getInitialValue: PropTypes.func, /* initial value; used if getInitialValue is not given */ - initialValue: React.PropTypes.string, + initialValue: PropTypes.string, /* placeholder text to use when the value is empty (and not being * edited) */ - placeholder: React.PropTypes.string, + placeholder: PropTypes.string, /* callback to update the value. Called with a single argument: the new * value. */ - onSubmit: React.PropTypes.func, + onSubmit: PropTypes.func, /* should the input submit when focus is lost? */ - blurToSubmit: React.PropTypes.bool, + blurToSubmit: PropTypes.bool, }; diff --git a/src/components/views/elements/EmojiText.js b/src/components/views/elements/EmojiText.js index faab0241ae..9fb650b2c3 100644 --- a/src/components/views/elements/EmojiText.js +++ b/src/components/views/elements/EmojiText.js @@ -16,6 +16,7 @@ */ import React from 'react'; +import PropTypes from 'prop-types'; import {emojifyText, containsEmoji} from '../../../HtmlUtils'; export default function EmojiText(props) { @@ -32,8 +33,8 @@ export default function EmojiText(props) { } EmojiText.propTypes = { - element: React.PropTypes.string, - children: React.PropTypes.string.isRequired, + element: PropTypes.string, + children: PropTypes.string.isRequired, }; EmojiText.defaultProps = { diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 25a8d538e1..76566e8c4d 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -63,7 +63,7 @@ FlairAvatar.propTypes = { }; FlairAvatar.contextTypes = { - matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, + matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, }; export default class Flair extends React.Component { @@ -107,7 +107,11 @@ export default class Flair extends React.Component { } const profiles = await this._getGroupProfiles(groups); if (!this.unmounted) { - this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})}); + this.setState({ + profiles: profiles.filter((profile) => { + return profile ? profile.avatarUrl : false; + }), + }); } } @@ -134,5 +138,5 @@ Flair.propTypes = { // this.context.matrixClient everywhere instead of this.props.matrixClient. // See https://github.com/vector-im/riot-web/issues/4951. Flair.contextTypes = { - matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, + matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 6c86296a38..365f9ded61 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -16,6 +16,7 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; import * as languageHandler from '../../../languageHandler'; @@ -114,7 +115,7 @@ export default class LanguageDropdown extends React.Component { } LanguageDropdown.propTypes = { - className: React.PropTypes.string, - onOptionChange: React.PropTypes.func.isRequired, - value: React.PropTypes.string, + className: PropTypes.string, + onOptionChange: PropTypes.func.isRequired, + value: PropTypes.string, }; diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index e2368a2fe3..3c58f90a2b 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import sdk from '../../../index'; const MemberAvatar = require('../avatars/MemberAvatar.js'); import { _t } from '../../../languageHandler'; @@ -23,19 +24,19 @@ module.exports = React.createClass({ propTypes: { // An array of member events to summarise - events: React.PropTypes.array.isRequired, + events: PropTypes.array.isRequired, // An array of EventTiles to render when expanded - children: React.PropTypes.array.isRequired, + children: PropTypes.array.isRequired, // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" - summaryLength: React.PropTypes.number, + summaryLength: PropTypes.number, // The maximum number of avatars to display in the summary - avatarsMaxLength: React.PropTypes.number, + avatarsMaxLength: PropTypes.number, // The minimum number of events needed to trigger summarisation - threshold: React.PropTypes.number, + threshold: PropTypes.number, // Called when the MELS expansion is toggled - onToggle: React.PropTypes.func, + onToggle: PropTypes.func, // Whether or not to begin with state.expanded=true - startExpanded: React.PropTypes.bool, + startExpanded: PropTypes.bool, }, getInitialState: function() { diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index a85f83d78c..067c377eaa 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -17,7 +17,7 @@ import React from 'react'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import classNames from 'classnames'; -import { Room, RoomMember } from 'matrix-js-sdk'; +import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix'; @@ -61,6 +61,17 @@ const Pill = React.createClass({ shouldShowPillAvatar: PropTypes.bool, }, + + childContextTypes: { + matrixClient: PropTypes.instanceOf(MatrixClient), + }, + + getChildContext() { + return { + matrixClient: this._matrixClient, + }; + }, + getInitialState() { return { // ID/alias of the room/user @@ -135,6 +146,7 @@ const Pill = React.createClass({ componentWillMount() { this._unmounted = false; + this._matrixClient = MatrixClientPeg.get(); this.componentWillReceiveProps(this.props); }, diff --git a/src/components/views/elements/PowerSelector.js b/src/components/views/elements/PowerSelector.js index d5c167fac9..f8443c6ecd 100644 --- a/src/components/views/elements/PowerSelector.js +++ b/src/components/views/elements/PowerSelector.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import React from 'react'; +import PropTypes from 'prop-types'; import * as Roles from '../../../Roles'; import { _t } from '../../../languageHandler'; @@ -24,23 +25,23 @@ module.exports = React.createClass({ displayName: 'PowerSelector', propTypes: { - value: React.PropTypes.number.isRequired, + value: PropTypes.number.isRequired, // The maximum value that can be set with the power selector - maxValue: React.PropTypes.number.isRequired, + maxValue: PropTypes.number.isRequired, // Default user power level for the room - usersDefault: React.PropTypes.number.isRequired, + usersDefault: PropTypes.number.isRequired, // if true, the