Merge branch 'develop' into dialog-a11y

This commit is contained in:
Peter Vágner 2018-02-06 23:04:15 +01:00
commit 14991afbe5
214 changed files with 8934 additions and 3589 deletions

View File

@ -8,7 +8,6 @@ src/CallHandler.js
src/component-index.js src/component-index.js
src/components/structures/ContextualMenu.js src/components/structures/ContextualMenu.js
src/components/structures/CreateRoom.js src/components/structures/CreateRoom.js
src/components/structures/FilePanel.js
src/components/structures/LoggedInView.js src/components/structures/LoggedInView.js
src/components/structures/login/ForgotPassword.js src/components/structures/login/ForgotPassword.js
src/components/structures/login/Login.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/DeactivateAccountDialog.js
src/components/views/dialogs/UnknownDeviceDialog.js src/components/views/dialogs/UnknownDeviceDialog.js
src/components/views/elements/AddressSelector.js src/components/views/elements/AddressSelector.js
src/components/views/elements/CreateRoomButton.js
src/components/views/elements/DeviceVerifyButtons.js src/components/views/elements/DeviceVerifyButtons.js
src/components/views/elements/DirectorySearchBox.js src/components/views/elements/DirectorySearchBox.js
src/components/views/elements/EditableText.js src/components/views/elements/EditableText.js
src/components/views/elements/HomeButton.js
src/components/views/elements/MemberEventListSummary.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/TintableSvg.js
src/components/views/elements/UserSelector.js src/components/views/elements/UserSelector.js
src/components/views/login/CountryDropdown.js src/components/views/login/CountryDropdown.js
@ -93,7 +86,6 @@ src/RichText.js
src/Roles.js src/Roles.js
src/Rooms.js src/Rooms.js
src/ScalarAuthClient.js src/ScalarAuthClient.js
src/Tinter.js
src/UiEffects.js src/UiEffects.js
src/Unread.js src/Unread.js
src/utils/DecryptFile.js src/utils/DecryptFile.js

View File

@ -3,7 +3,10 @@ dist: trusty
# we don't need sudo, so can run in a container, which makes startup much # we don't need sudo, so can run in a container, which makes startup much
# quicker. # 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 language: node_js
node_js: node_js:

View File

@ -56,7 +56,7 @@
"browser-encrypt-attachment": "^0.3.0", "browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"classnames": "^2.1.2", "classnames": "^2.1.2",
"commonmark": "^0.27.0", "commonmark": "^0.28.1",
"counterpart": "^0.18.0", "counterpart": "^0.18.0",
"draft-js": "^0.11.0-alpha", "draft-js": "^0.11.0-alpha",
"draft-js-export-html": "^0.6.0", "draft-js-export-html": "^0.6.0",
@ -78,6 +78,7 @@
"querystring": "^0.2.0", "querystring": "^0.2.0",
"react": "^15.4.0", "react": "^15.4.0",
"react-addons-css-transition-group": "15.3.2", "react-addons-css-transition-group": "15.3.2",
"react-beautiful-dnd": "^4.0.0",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"sanitize-html": "^1.14.1", "sanitize-html": "^1.14.1",

View File

@ -14,25 +14,54 @@
limitations under the License. limitations under the License.
*/ */
import { getCurrentLanguage } from './languageHandler'; import { getCurrentLanguage, _t, _td } from './languageHandler';
import PlatformPeg from './PlatformPeg'; 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/<redacted>");
}
function getRedactedUrl() { function getRedactedUrl() {
const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
// hardcoded url to make piwik happy // hardcoded url to make piwik happy
return 'https://riot.im/app/' + redactedHash; return 'https://riot.im/app/' + getRedactedHash();
} }
const customVariables = { const customVariables = {
'App Platform': 1, 'App Platform': {
'App Version': 2, id: 1,
'User Type': 3, expl: _td('The platform you\'re on'),
'Chosen Language': 4, },
'Instance': 5, 'App Version': {
'RTE: Uses Richtext Mode': 6, id: 2,
'Homeserver URL': 7, expl: _td('The version of Riot.im'),
'Identity Server URL': 8, },
'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) { function whitelistRedact(whitelist, str) {
@ -40,9 +69,6 @@ function whitelistRedact(whitelist, str) {
return '<redacted>'; return '<redacted>';
} }
const whitelistedHSUrls = ["https://matrix.org"];
const whitelistedISUrls = ["https://vector.im"];
class Analytics { class Analytics {
constructor() { constructor() {
this._paq = null; this._paq = null;
@ -140,11 +166,16 @@ class Analytics {
} }
_setVisitVariable(key, value) { _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) { setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
if (this.disabled) return; 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('User Type', isGuest ? 'Guest' : 'Logged In');
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl)); this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
@ -154,6 +185,44 @@ class Analytics {
if (this.disabled) return; if (this.disabled) return;
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off'); 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: <div>
<div>
{ _t('The information being sent to us to help make Riot.im better includes:') }
</div>
<table>
{ rows.map((row) => <tr key={row[0]}>
<td>{ _t(customVariables[row[0]].expl) }</td>
<td><code>{ row[1] }</code></td>
</tr>) }
</table>
<br />
<div>
{ _t('We also record each page you use in the app (currently <CurrentPageHash>), your User Agent'
+ ' (<CurrentUserAgent>) and your device resolution (<CurrentDeviceResolution>).',
{},
{
CurrentPageHash: <code>{ getRedactedHash() }</code>,
CurrentUserAgent: <code>{ navigator.userAgent }</code>,
CurrentDeviceResolution: <code>{ resolution }</code>,
},
) }
{ _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.') }
</div>
</div>,
});
}
} }
if (!global.mxAnalytics) { if (!global.mxAnalytics) {

View File

@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
function getDaysArray() { function getDaysArray() {
@ -59,47 +58,70 @@ function twelveHourTime(date) {
return `${hours}:${minutes}${ampm}`; return `${hours}:${minutes}${ampm}`;
} }
module.exports = { export function formatDate(date, showTwelveHour=false) {
formatDate: function(date, showTwelveHour=false) { const now = new Date();
const now = new Date(); const days = getDaysArray();
const days = getDaysArray(); const months = getMonthsArray();
const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) {
if (date.toDateString() === now.toDateString()) { return formatTime(date, showTwelveHour);
return this.formatTime(date, showTwelveHour); } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { // TODO: use standard date localize function provided in counterpart
// TODO: use standard date localize function provided in counterpart return _t('%(weekDayName)s %(time)s', {
return _t('%(weekDayName)s %(time)s', { weekDayName: days[date.getDay()],
weekDayName: days[date.getDay()], time: formatTime(date, showTwelveHour),
time: this.formatTime(date, showTwelveHour), });
}); } else if (now.getFullYear() === date.getFullYear()) {
} else if (now.getFullYear() === date.getFullYear()) { // TODO: use standard date localize function provided in counterpart
// TODO: use standard date localize function provided in counterpart return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', {
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', {
weekDayName: days[date.getDay()], weekDayName: days[date.getDay()],
monthName: months[date.getMonth()], monthName: months[date.getMonth()],
day: date.getDate(), day: date.getDate(),
fullYear: date.getFullYear(), time: formatTime(date, showTwelveHour),
time: this.formatTime(date, showTwelveHour),
}); });
}, }
return formatFullDate(date, showTwelveHour);
}
formatTime: function(date, showTwelveHour=false) { export function formatFullDateNoTime(date) {
if (showTwelveHour) { const days = getDaysArray();
return twelveHourTime(date); const months = getMonthsArray();
} return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
return pad(date.getHours()) + ':' + pad(date.getMinutes()); 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();
}

View File

@ -68,3 +68,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) {
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey; 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;
}
}

View File

@ -55,25 +55,6 @@ function is_multi_line(node) {
return par.firstChild != par.lastChild; 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 * Class that wraps commonmark, adding the ability to see whether
* a given message actually uses any markdown syntax or whether * a given message actually uses any markdown syntax or whether
@ -81,7 +62,7 @@ function linkifyMarkdown(s) {
*/ */
export default class Markdown { export default class Markdown {
constructor(input) { constructor(input) {
this.input = linkifyMarkdown(input); this.input = input;
const parser = new commonmark.Parser(); const parser = new commonmark.Parser();
this.parsed = parser.parse(this.input); this.parsed = parser.parse(this.input);

View File

@ -1,6 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd. Copyright 2017 Vector Creations Ltd.
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 EventTimelineSet from 'matrix-js-sdk/lib/models/event-timeline-set';
import createMatrixClient from './utils/createMatrixClient'; import createMatrixClient from './utils/createMatrixClient';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import MatrixActionCreators from './actions/MatrixActionCreators';
interface MatrixClientCreds { interface MatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
@ -68,6 +70,8 @@ class MatrixClientPeg {
unset() { unset() {
this.matrixClient = null; this.matrixClient = null;
MatrixActionCreators.stop();
} }
/** /**
@ -108,6 +112,9 @@ class MatrixClientPeg {
// regardless of errors, start the client. If we did error out, we'll // regardless of errors, start the client. If we did error out, we'll
// just end up doing a full initial /sync. // 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`); console.log(`MatrixClientPeg: really starting MatrixClient`);
this.get().startClient(opts); this.get().startClient(opts);
console.log(`MatrixClientPeg: MatrixClient started`); console.log(`MatrixClientPeg: MatrixClient started`);

View File

@ -19,6 +19,7 @@ limitations under the License.
const React = require('react'); const React = require('react');
const ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
import PropTypes from 'prop-types';
import Analytics from './Analytics'; import Analytics from './Analytics';
import sdk from './index'; import sdk from './index';
@ -33,7 +34,7 @@ const AsyncWrapper = React.createClass({
/** A function which takes a 'callback' argument which it will call /** A function which takes a 'callback' argument which it will call
* with the real component once it loads. * with the real component once it loads.
*/ */
loader: React.PropTypes.func.isRequired, loader: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -135,6 +135,10 @@ const Notifier = {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
if (!plaf) return; 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); Analytics.trackEvent('Notifier', 'Set Enabled', enable);
// make sure that we persist the current setting audio_enabled setting // 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 // clear the notifications_hidden flag, so that if notifications are
// disabled again in the future, we will show the banner again. // disabled again in the future, we will show the banner again.
this.setToolbarHidden(false); this.setToolbarHidden(true);
} else { } else {
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",

View File

@ -85,9 +85,7 @@ function _onStartChatFinished(shouldInvite, addrs) {
if (rooms.length > 0) { if (rooms.length > 0) {
// A Direct Message room already exists for this user, so select a // 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 // room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent( const ChatCreateOrReuseDialog = sdk.getComponent("views.dialogs.ChatCreateOrReuseDialog");
"views.dialogs.ChatCreateOrReuseDialog",
);
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, { const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
userId: addrTexts[0], userId: addrTexts[0],
onNewDMClick: () => { 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 { } else {
// Start multi user chat // Start multi user chat
let room; let room;

View File

@ -34,7 +34,14 @@ export function getRoomNotifsState(roomId) {
} }
// for everything else, look at the room rule. // 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 // 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 // (in particular this will be 'wrong' for one to one rooms because
@ -130,6 +137,11 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
} }
function findOverrideMuteRule(roomId) { 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) { for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
if (isRuleForRoom(roomId, rule)) { if (isRuleForRoom(roomId, rule)) {
if (isMuteRule(rule) && rule.enabled) { if (isMuteRule(rule) && rule.enabled) {

View File

@ -43,7 +43,7 @@ export function getOnlyOtherMember(room, me) {
return null; return null;
} }
export function isConfCallRoom(room, me, conferenceHandler) { function _isConfCallRoom(room, me, conferenceHandler) {
if (!conferenceHandler) return false; if (!conferenceHandler) return false;
if (me.membership != "join") { if (me.membership != "join") {
@ -58,6 +58,26 @@ export function isConfCallRoom(room, me, conferenceHandler) {
if (conferenceHandler.isConferenceUser(otherMember.userId)) { if (conferenceHandler.isConferenceUser(otherMember.userId)) {
return true; 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) { export function looksLikeDirectMessageRoom(room, me) {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import Promise from 'bluebird'; import Promise from 'bluebird';
import SettingsStore from "./settings/SettingsStore";
const request = require('browser-request'); const request = require('browser-request');
const SdkConfig = require('./SdkConfig'); const SdkConfig = require('./SdkConfig');
@ -38,11 +39,53 @@ class ScalarAuthClient {
// Returns a scalar_token string // Returns a scalar_token string
getScalarToken() { getScalarToken() {
const tok = window.localStorage.getItem("mx_scalar_token"); const token = window.localStorage.getItem("mx_scalar_token");
if (tok) return Promise.resolve(tok);
// No saved token, so do the dance to get one. First, we if (!token) {
// need an openid bearer token from the HS. 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) => { return MatrixClientPeg.get().getOpenIdToken().then((token_object) => {
// Now we can send that to scalar and exchange it for a scalar token // Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(token_object); return this.exchangeForScalarToken(token_object);
@ -109,6 +152,7 @@ class ScalarAuthClient {
let url = SdkConfig.get().integrations_ui_url; let url = SdkConfig.get().integrations_ui_url;
url += "?scalar_token=" + encodeURIComponent(this.scalarToken); url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
url += "&room_id=" + encodeURIComponent(roomId); url += "&room_id=" + encodeURIComponent(roomId);
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
if (id) { if (id) {
url += '&integ_id=' + encodeURIComponent(id); url += '&integ_id=' + encodeURIComponent(id);
} }

View File

@ -557,8 +557,16 @@ const onMessage = function(event) {
// //
// All strings start with the empty string, so for sanity return if the length // All strings start with the empty string, so for sanity return if the length
// of the event origin is 0. // 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; 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 return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
} }

View File

@ -21,6 +21,13 @@ const DEFAULTS = {
integrations_rest_url: "https://scalar.vector.im/api", integrations_rest_url: "https://scalar.vector.im/api",
// Where to send bug reports. If not specified, bugs cannot be sent. // Where to send bug reports. If not specified, bugs cannot be sent.
bug_report_endpoint_url: null, 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 { class SdkConfig {
@ -45,3 +52,4 @@ class SdkConfig {
} }
module.exports = SdkConfig; module.exports = SdkConfig;
module.exports.DEFAULTS = DEFAULTS;

View File

@ -96,6 +96,8 @@ const commands = {
colorScheme.primary_color = matches[1]; colorScheme.primary_color = matches[1];
if (matches[4]) { if (matches[4]) {
colorScheme.secondary_color = matches[4]; colorScheme.secondary_color = matches[4];
} else {
colorScheme.secondary_color = colorScheme.primary_color;
} }
return success( return success(
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme), SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
@ -295,7 +297,7 @@ const commands = {
// Define the power level of a user // Define the power level of a user
op: new Command("op", "<userId> [<power level>]", function(roomId, args) { op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+?)( +(\d+))?$/); const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
let powerLevel = 50; // default power level for op let powerLevel = 50; // default power level for op
if (matches) { if (matches) {
const userId = matches[1]; const userId = matches[1];

View File

@ -52,8 +52,7 @@ function textForMemberEvent(ev) {
case 'join': case 'join':
if (prevContent && prevContent.membership === 'join') { if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', { return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
senderName,
oldDisplayName: prevContent.displayname, oldDisplayName: prevContent.displayname,
displayName: content.displayname, displayName: content.displayname,
}); });

View File

@ -1,5 +1,6 @@
const React = require('react'); const React = require('react');
const ReactDom = require('react-dom'); const ReactDom = require('react-dom');
import PropTypes from 'prop-types';
const Velocity = require('velocity-vector'); const Velocity = require('velocity-vector');
/** /**
@ -14,16 +15,16 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// either a list of child nodes, or a single child. // either a list of child nodes, or a single child.
children: React.PropTypes.any, children: PropTypes.any,
// optional transition information for changing existing children // 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 // 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 // a list of transition options from the corresponding startStyle
enterTransitionOpts: React.PropTypes.array, enterTransitionOpts: PropTypes.array,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -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;

View File

@ -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());
},
};

View File

@ -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;

View File

@ -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});
});
};
}

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
const React = require("react"); const React = require("react");
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const sdk = require('../../../index'); const sdk = require('../../../index');
const MatrixClientPeg = require("../../../MatrixClientPeg"); const MatrixClientPeg = require("../../../MatrixClientPeg");
@ -23,8 +24,8 @@ module.exports = React.createClass({
displayName: 'EncryptedEventDialog', displayName: 'EncryptedEventDialog',
propTypes: { propTypes: {
event: React.PropTypes.object.isRequired, event: PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -16,6 +16,7 @@ limitations under the License.
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
@ -29,8 +30,8 @@ export default React.createClass({
displayName: 'ExportE2eKeysDialog', displayName: 'ExportE2eKeysDialog',
propTypes: { propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption'; import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
@ -40,8 +41,8 @@ export default React.createClass({
displayName: 'ImportE2eKeysDialog', displayName: 'ImportE2eKeysDialog',
propTypes: { propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
/* These were earlier stateless functional components but had to be converted /* These were earlier stateless functional components but had to be converted
@ -42,10 +43,10 @@ export class TextualCompletion extends React.Component {
} }
} }
TextualCompletion.propTypes = { TextualCompletion.propTypes = {
title: React.PropTypes.string, title: PropTypes.string,
subtitle: React.PropTypes.string, subtitle: PropTypes.string,
description: React.PropTypes.string, description: PropTypes.string,
className: React.PropTypes.string, className: PropTypes.string,
}; };
export class PillCompletion extends React.Component { export class PillCompletion extends React.Component {
@ -69,9 +70,9 @@ export class PillCompletion extends React.Component {
} }
} }
PillCompletion.propTypes = { PillCompletion.propTypes = {
title: React.PropTypes.string, title: PropTypes.string,
subtitle: React.PropTypes.string, subtitle: PropTypes.string,
description: React.PropTypes.string, description: PropTypes.string,
initialComponent: React.PropTypes.element, initialComponent: PropTypes.element,
className: React.PropTypes.string, className: PropTypes.string,
}; };

View File

@ -25,6 +25,7 @@ import {PillCompletion} from './Components';
import {getDisplayAliasForRoom} from '../Rooms'; import {getDisplayAliasForRoom} from '../Rooms';
import sdk from '../index'; import sdk from '../index';
import _sortBy from 'lodash/sortBy'; import _sortBy from 'lodash/sortBy';
import {makeRoomPermalink} from "../matrix-to";
const ROOM_REGEX = /(?=#)(\S*)/g; const ROOM_REGEX = /(?=#)(\S*)/g;
@ -78,7 +79,7 @@ export default class RoomProvider extends AutocompleteProvider {
return { return {
completion: displayAlias, completion: displayAlias,
suffix: ' ', suffix: ' ',
href: 'https://matrix.to/#/' + displayAlias, href: makeRoomPermalink(displayAlias),
component: ( component: (
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} /> <PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
), ),

View File

@ -28,6 +28,7 @@ import _sortBy from 'lodash/sortBy';
import MatrixClientPeg from '../MatrixClientPeg'; import MatrixClientPeg from '../MatrixClientPeg';
import type {Room, RoomMember} from 'matrix-js-sdk'; import type {Room, RoomMember} from 'matrix-js-sdk';
import {makeUserPermalink} from "../matrix-to";
const USER_REGEX = /@\S*/g; 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. // relies on the length of the entity === length of the text in the decoration.
completion: user.rawDisplayName.replace(' (IRC)', ''), completion: user.rawDisplayName.replace(' (IRC)', ''),
suffix: range.start === 0 ? ': ' : ' ', suffix: range.start === 0 ? ': ' : ' ',
href: 'https://matrix.to/#/' + user.userId, href: makeUserPermalink(user.userId),
component: ( component: (
<PillCompletion <PillCompletion
initialComponent={<MemberAvatar member={user} width={24} height={24} />} initialComponent={<MemberAvatar member={user} width={24} height={24} />}

View File

@ -20,6 +20,7 @@ limitations under the License.
const classNames = require('classnames'); const classNames = require('classnames');
const React = require('react'); const React = require('react');
const ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
import PropTypes from 'prop-types';
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
@ -29,11 +30,11 @@ module.exports = {
ContextualMenuContainerId: "mx_ContextualMenu_Container", ContextualMenuContainerId: "mx_ContextualMenu_Container",
propTypes: { propTypes: {
menuWidth: React.PropTypes.number, menuWidth: PropTypes.number,
menuHeight: React.PropTypes.number, menuHeight: PropTypes.number,
chevronOffset: React.PropTypes.number, chevronOffset: PropTypes.number,
menuColour: React.PropTypes.string, menuColour: PropTypes.string,
chevronFace: React.PropTypes.string, // top, bottom, left, right chevronFace: PropTypes.string, // top, bottom, left, right
}, },
getOrCreateContainer: function() { getOrCreateContainer: function() {

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import sdk from '../../index'; import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
@ -30,8 +31,8 @@ module.exports = React.createClass({
displayName: 'CreateRoom', displayName: 'CreateRoom',
propTypes: { propTypes: {
onRoomCreated: React.PropTypes.func, onRoomCreated: PropTypes.func,
collapsedRhs: React.PropTypes.bool, collapsedRhs: PropTypes.bool,
}, },
phases: { phases: {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import sdk from '../../index'; import sdk from '../../index';
@ -28,7 +29,7 @@ const FilePanel = React.createClass({
displayName: 'FilePanel', displayName: 'FilePanel',
propTypes: { propTypes: {
roomId: React.PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -31,6 +31,7 @@ import GroupStoreCache from '../../stores/GroupStoreCache';
import GroupStore from '../../stores/GroupStore'; import GroupStore from '../../stores/GroupStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
const LONG_DESC_PLACEHOLDER = _td( const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1> `<h1>HTML for your community's page</h1>
@ -209,7 +210,7 @@ const FeaturedRoom = React.createClass({
let permalink = null; let permalink = null;
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) { 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; let roomNameNode = null;
@ -366,7 +367,7 @@ const FeaturedUser = React.createClass({
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id; 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 = <a href={permalink} onClick={this.onClick}>{ name }</a>; const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
const httpUrl = MatrixClientPeg.get() const httpUrl = MatrixClientPeg.get()
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64); .mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
@ -390,7 +391,7 @@ const FeaturedUser = React.createClass({
}); });
const GroupContext = { const GroupContext = {
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired, groupStore: PropTypes.instanceOf(GroupStore).isRequired,
}; };
CategoryRoomList.contextTypes = GroupContext; CategoryRoomList.contextTypes = GroupContext;
@ -408,7 +409,7 @@ export default React.createClass({
}, },
childContextTypes: { childContextTypes: {
groupStore: React.PropTypes.instanceOf(GroupStore), groupStore: PropTypes.instanceOf(GroupStore),
}, },
getChildContext: function() { getChildContext: function() {

View File

@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk';
const InteractiveAuth = Matrix.InteractiveAuth; const InteractiveAuth = Matrix.InteractiveAuth;
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents'; import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
@ -26,18 +27,18 @@ export default React.createClass({
propTypes: { propTypes: {
// matrix client to use for UI auth requests // 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 // response from initial request. If not supplied, will do a request on
// mount. // mount.
authData: React.PropTypes.shape({ authData: PropTypes.shape({
flows: React.PropTypes.array, flows: PropTypes.array,
params: React.PropTypes.object, params: PropTypes.object,
session: React.PropTypes.string, session: PropTypes.string,
}), }),
// callback // callback
makeRequest: React.PropTypes.func.isRequired, makeRequest: PropTypes.func.isRequired,
// callback called when the auth process has finished, // callback called when the auth process has finished,
// successfully or unsuccessfully. // successfully or unsuccessfully.
@ -51,22 +52,22 @@ export default React.createClass({
// the auth session. // the auth session.
// * clientSecret {string} The client secret used in auth // * clientSecret {string} The client secret used in auth
// sessions with the ID server. // sessions with the ID server.
onAuthFinished: React.PropTypes.func.isRequired, onAuthFinished: PropTypes.func.isRequired,
// Inputs provided by the user to the auth process // Inputs provided by the user to the auth process
// and used by various stages. As passed to js-sdk // and used by various stages. As passed to js-sdk
// interactive-auth // interactive-auth
inputs: React.PropTypes.object, inputs: PropTypes.object,
// As js-sdk interactive-auth // As js-sdk interactive-auth
makeRegistrationUrl: React.PropTypes.func, makeRegistrationUrl: PropTypes.func,
sessionId: React.PropTypes.string, sessionId: PropTypes.string,
clientSecret: React.PropTypes.string, clientSecret: PropTypes.string,
emailSid: React.PropTypes.string, emailSid: PropTypes.string,
// If true, poll to see if the auth flow has been completed // If true, poll to see if the auth flow has been completed
// out-of-band // out-of-band
poll: React.PropTypes.bool, poll: PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -18,6 +18,7 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import Notifier from '../../Notifier'; 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. * Components mounted below us can access the matrix client via the react context.
*/ */
export default React.createClass({ const LoggedInView = React.createClass({
displayName: 'LoggedInView', displayName: 'LoggedInView',
propTypes: { propTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
page_type: React.PropTypes.string.isRequired, page_type: PropTypes.string.isRequired,
onRoomCreated: React.PropTypes.func, onRoomCreated: PropTypes.func,
onUserSettingsClose: React.PropTypes.func, onUserSettingsClose: PropTypes.func,
// Called with the credentials of a registered user (if they were a ROU that // Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU) // transitioned to PWLU)
onRegistered: React.PropTypes.func, onRegistered: PropTypes.func,
teamToken: React.PropTypes.string, teamToken: PropTypes.string,
// and lots and lots of other stuff. // and lots and lots of other stuff.
}, },
childContextTypes: { childContextTypes: {
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient), matrixClient: PropTypes.instanceOf(Matrix.MatrixClient),
authCache: React.PropTypes.object, authCache: PropTypes.object,
}, },
getChildContext: function() { getChildContext: function() {
@ -331,7 +332,6 @@ export default React.createClass({
<div className={bodyClasses}> <div className={bodyClasses}>
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> } { SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
<LeftPanel <LeftPanel
selectedRoom={this.props.currentRoomId}
collapsed={this.props.collapseLhs || false} collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled} disabled={this.props.leftDisabled}
/> />
@ -344,3 +344,5 @@ export default React.createClass({
); );
}, },
}); });
export default LoggedInView;

View File

@ -19,6 +19,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
@ -83,7 +84,7 @@ const ONBOARDING_FLOW_STARTERS = [
'view_create_group', 'view_create_group',
]; ];
module.exports = React.createClass({ export default React.createClass({
// we export this so that the integration tests can use it :-S // we export this so that the integration tests can use it :-S
statics: { statics: {
VIEWS: VIEWS, VIEWS: VIEWS,
@ -92,38 +93,38 @@ module.exports = React.createClass({
displayName: 'MatrixChat', displayName: 'MatrixChat',
propTypes: { propTypes: {
config: React.PropTypes.object, config: PropTypes.object,
ConferenceHandler: React.PropTypes.any, ConferenceHandler: PropTypes.any,
onNewScreen: React.PropTypes.func, onNewScreen: PropTypes.func,
registrationUrl: React.PropTypes.string, registrationUrl: PropTypes.string,
enableGuest: React.PropTypes.bool, enableGuest: PropTypes.bool,
// the queryParams extracted from the [real] query-string of the URI // 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 // 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 // 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 // Represents the screen to display as a result of parsing the initial
// window.location // window.location
initialScreenAfterLogin: React.PropTypes.shape({ initialScreenAfterLogin: PropTypes.shape({
screen: React.PropTypes.string.isRequired, screen: PropTypes.string.isRequired,
params: React.PropTypes.object, params: PropTypes.object,
}), }),
// displayname, if any, to set on the device when logging // displayname, if any, to set on the device when logging
// in/registering. // in/registering.
defaultDeviceDisplayName: React.PropTypes.string, defaultDeviceDisplayName: PropTypes.string,
// A function that makes a registration URL // A function that makes a registration URL
makeRegistrationUrl: React.PropTypes.func.isRequired, makeRegistrationUrl: PropTypes.func.isRequired,
}, },
childContextTypes: { childContextTypes: {
appConfig: React.PropTypes.object, appConfig: PropTypes.object,
}, },
AuxPanel: { AuxPanel: {
@ -846,16 +847,36 @@ module.exports = React.createClass({
}).close; }).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((
<span className="warning" key="non_public_warning">
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
</span>
));
}
}
return warnings;
},
_leaveRoom: function(roomId) { _leaveRoom: function(roomId) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this._leaveRoomWarnings(roomId);
Modal.createTrackedDialog('Leave room', '', QuestionDialog, { Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
title: _t("Leave room"), title: _t("Leave room"),
description: ( description: (
<span> <span>
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) } { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
{ warnings }
</span> </span>
), ),
onFinished: (shouldLeave) => { 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 // 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 // the timeline panel. If the timeline panel doesn't exist, then we assume
// it is safe to reset the timeline. // it is safe to reset the timeline.
if (!self.refs.loggedInView) { if (!self._loggedInView || !self._loggedInView.child) {
return true; return true;
} }
return self.refs.loggedInView.canResetTimelineInRoom(roomId); return self._loggedInView.child.canResetTimelineInRoom(roomId);
}); });
cli.on('sync', function(state, prevState) { cli.on('sync', function(state, prevState) {
@ -1487,6 +1508,10 @@ module.exports = React.createClass({
return this.props.makeRegistrationUrl(params); return this.props.makeRegistrationUrl(params);
}, },
_collectLoggedInView: function(ref) {
this._loggedInView = ref;
},
render: function() { render: function() {
// console.log(`Rendering MatrixChat with view ${this.state.view}`); // console.log(`Rendering MatrixChat with view ${this.state.view}`);
@ -1519,7 +1544,7 @@ module.exports = React.createClass({
*/ */
const LoggedInView = sdk.getComponent('structures.LoggedInView'); const LoggedInView = sdk.getComponent('structures.LoggedInView');
return ( return (
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()} <LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
onRoomCreated={this.onRoomCreated} onRoomCreated={this.onRoomCreated}
onUserSettingsClose={this.onUserSettingsClose} onUserSettingsClose={this.onUserSettingsClose}
onRegistered={this.onRegistered} onRegistered={this.onRegistered}

View File

@ -16,15 +16,15 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils';
import dis from "../../dispatcher"; import dis from "../../dispatcher";
import sdk from '../../index'; import sdk from '../../index';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
const MILLIS_IN_DAY = 86400000;
/* (almost) stateless UI component which builds the event tiles in the room timeline. /* (almost) stateless UI component which builds the event tiles in the room timeline.
*/ */
module.exports = React.createClass({ module.exports = React.createClass({
@ -32,63 +32,63 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// true to give the component a 'display: none' style. // true to give the component a 'display: none' style.
hidden: React.PropTypes.bool, hidden: PropTypes.bool,
// true to show a spinner at the top of the timeline to indicate // true to show a spinner at the top of the timeline to indicate
// back-pagination in progress // back-pagination in progress
backPaginating: React.PropTypes.bool, backPaginating: PropTypes.bool,
// true to show a spinner at the end of the timeline to indicate // true to show a spinner at the end of the timeline to indicate
// forward-pagination in progress // forward-pagination in progress
forwardPaginating: React.PropTypes.bool, forwardPaginating: PropTypes.bool,
// the list of MatrixEvents to display // the list of MatrixEvents to display
events: React.PropTypes.array.isRequired, events: PropTypes.array.isRequired,
// ID of an event to highlight. If undefined, no event will be highlighted. // ID of an event to highlight. If undefined, no event will be highlighted.
highlightedEventId: React.PropTypes.string, highlightedEventId: PropTypes.string,
// Should we show URL Previews // Should we show URL Previews
showUrlPreview: React.PropTypes.bool, showUrlPreview: PropTypes.bool,
// event after which we should show a read marker // event after which we should show a read marker
readMarkerEventId: React.PropTypes.string, readMarkerEventId: PropTypes.string,
// whether the read marker should be visible // whether the read marker should be visible
readMarkerVisible: React.PropTypes.bool, readMarkerVisible: PropTypes.bool,
// the userid of our user. This is used to suppress the read marker // the userid of our user. This is used to suppress the read marker
// for pending messages. // for pending messages.
ourUserId: React.PropTypes.string, ourUserId: PropTypes.string,
// true to suppress the date at the start of the timeline // true to suppress the date at the start of the timeline
suppressFirstDateSeparator: React.PropTypes.bool, suppressFirstDateSeparator: PropTypes.bool,
// whether to show read receipts // whether to show read receipts
showReadReceipts: React.PropTypes.bool, showReadReceipts: PropTypes.bool,
// true if updates to the event list should cause the scroll panel to // true if updates to the event list should cause the scroll panel to
// scroll down when we are at the bottom of the window. See ScrollPanel // scroll down when we are at the bottom of the window. See ScrollPanel
// for more details. // for more details.
stickyBottom: React.PropTypes.bool, stickyBottom: PropTypes.bool,
// callback which is called when the panel is scrolled. // callback which is called when the panel is scrolled.
onScroll: React.PropTypes.func, onScroll: PropTypes.func,
// callback which is called when more content is needed. // callback which is called when more content is needed.
onFillRequest: React.PropTypes.func, onFillRequest: PropTypes.func,
// className for the panel // className for the panel
className: React.PropTypes.string.isRequired, className: PropTypes.string.isRequired,
// shape parameter to be passed to EventTiles // shape parameter to be passed to EventTiles
tileShape: React.PropTypes.string, tileShape: PropTypes.string,
// show twelve hour timestamps // show twelve hour timestamps
isTwelveHour: React.PropTypes.bool, isTwelveHour: PropTypes.bool,
// show timestamps always // show timestamps always
alwaysShowTimestamps: React.PropTypes.bool, alwaysShowTimestamps: PropTypes.bool,
}, },
componentWillMount: function() { componentWillMount: function() {
@ -325,7 +325,7 @@ module.exports = React.createClass({
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial"); const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) { if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>; const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
} }
@ -479,7 +479,7 @@ module.exports = React.createClass({
// do we need a date separator since the last event? // do we need a date separator since the last event?
if (this._wantsDateSeparator(prevEvent, eventDate)) { if (this._wantsDateSeparator(prevEvent, eventDate)) {
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} showTwelveHour={this.props.isTwelveHour} /></li>; const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
ret.push(dateSeparator); ret.push(dateSeparator);
continuation = false; continuation = false;
} }
@ -522,17 +522,7 @@ module.exports = React.createClass({
// here. // here.
return !this.props.suppressFirstDateSeparator; return !this.props.suppressFirstDateSeparator;
} }
const prevEventDate = prevEvent.getDate(); return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
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();
}, },
// get a list of read receipts that should be shown next to this event // get a list of read receipts that should be shown next to this event

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import sdk from '../../index'; import sdk from '../../index';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
@ -26,7 +27,7 @@ export default withMatrixClient(React.createClass({
displayName: 'MyGroups', displayName: 'MyGroups',
propTypes: { propTypes: {
matrixClient: React.PropTypes.object.isRequired, matrixClient: PropTypes.object.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import sdk from '../../index'; import sdk from '../../index';
@ -23,7 +24,7 @@ import WhoIsTyping from '../../WhoIsTyping';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import MemberAvatar from '../views/avatars/MemberAvatar'; import MemberAvatar from '../views/avatars/MemberAvatar';
import Resend from '../../Resend'; import Resend from '../../Resend';
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices'; import * as cryptodevices from '../../cryptodevices';
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -41,59 +42,59 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// the room this statusbar is representing. // 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 // 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 // this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. // 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. // 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 // 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 // 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 // the 'Active Call' text in the status bar if there is nothing
// more interesting) // more interesting)
hasActiveCall: React.PropTypes.bool, hasActiveCall: PropTypes.bool,
// Number of names to display in typing indication. E.g. set to 3, will // Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing." // 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 // callback for when the user clicks on the 'resend all' button in the
// 'unsent messages' bar // 'unsent messages' bar
onResendAllClick: React.PropTypes.func, onResendAllClick: PropTypes.func,
// callback for when the user clicks on the 'cancel all' button in the // callback for when the user clicks on the 'cancel all' button in the
// 'unsent messages' bar // 'unsent messages' bar
onCancelAllClick: React.PropTypes.func, onCancelAllClick: PropTypes.func,
// callback for when the user clicks on the 'invite others' button in the // callback for when the user clicks on the 'invite others' button in the
// 'you are alone' bar // '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 // callback for when the user clicks on the 'stop warning me' button in the
// 'you are alone' bar // 'you are alone' bar
onStopWarningClick: React.PropTypes.func, onStopWarningClick: PropTypes.func,
// callback for when the user clicks on the 'scroll to bottom' button // 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 // 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 // status bar. This is used to trigger a re-layout in the parent
// component. // component.
onResize: React.PropTypes.func, onResize: PropTypes.func,
// callback for when the status bar can be hidden from view, as it is // callback for when the status bar can be hidden from view, as it is
// not displaying anything // not displaying anything
onHidden: React.PropTypes.func, onHidden: PropTypes.func,
// callback for when the status bar is displaying something and should // callback for when the status bar is displaying something and should
// be visible // be visible
onVisible: React.PropTypes.func, onVisible: PropTypes.func,
}, },
getDefaultProps: function() { 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() { _onResendAllClick: function() {
Resend.resendUnsentEvents(this.props.room); Resend.resendUnsentEvents(this.props.room);
}, },
@ -156,7 +164,7 @@ module.exports = React.createClass({
}, },
_onShowDevicesClick: function() { _onShowDevicesClick: function() {
showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room); cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
}, },
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) { _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 // Check whether current size is greater than 0, if yes call props.onVisible
_checkSize: function() { _checkSize: function() {
if (this.props.onVisible && this._getSize()) { if (this._getSize()) {
this.props.onVisible(); 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) { if (hasUDE) {
title = _t("Message not sent due to unknown devices being present"); title = _t("Message not sent due to unknown devices being present");
content = _t( content = _t(
"<showDevicesText>Show devices</showDevicesText> or <cancelText>cancel all</cancelText>.", "<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.",
{}, {},
{ {
'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>, 'showDevicesText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onShowDevicesClick}>{ sub }</a>,
'sendAnywayText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="sendAnyway" onClick={this._onSendWithoutVerifyingClick}>{ sub }</a>,
'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>, 'cancelText': (sub) => <a className="mx_RoomStatusBar_resend_link" key="cancel" onClick={this._onCancelAllClick}>{ sub }</a>,
}, },
); );
@ -302,11 +313,11 @@ module.exports = React.createClass({
) { ) {
title = unsentMessages[0].error.data.error; title = unsentMessages[0].error.data.error;
} else { } 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("<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " + content = _t("%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. " +
"You can also select individual messages to resend or cancel.", "You can also select individual messages to resend or cancel.",
{}, { count: unsentMessages.length },
{ {
'resendText': (sub) => 'resendText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>, <a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,

View File

@ -24,6 +24,7 @@ import shouldHideEvent from "../../shouldHideEvent";
const React = require("react"); const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
import Promise from 'bluebird'; import Promise from 'bluebird';
const classNames = require("classnames"); const classNames = require("classnames");
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
@ -58,18 +59,18 @@ if (DEBUG) {
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomView', displayName: 'RoomView',
propTypes: { propTypes: {
ConferenceHandler: React.PropTypes.any, ConferenceHandler: PropTypes.any,
// Called with the credentials of a registered user (if they were a ROU that // Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU) // transitioned to PWLU)
onRegistered: React.PropTypes.func, onRegistered: PropTypes.func,
// An object representing a third party invite to join this room // An object representing a third party invite to join this room
// Fields: // Fields:
// * inviteSignUrl (string) The URL used to join this room from an email invite // * inviteSignUrl (string) The URL used to join this room from an email invite
// (given as part of the link in the invite email) // (given as part of the link in the invite email)
// * invitedEmail (string) The email address that was invited to this room // * 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 // 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 // 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 // * avatarUrl (string) The mxc:// avatar URL for the room
// * inviterName (string) The display name of the person who // * inviterName (string) The display name of the person who
// * invited us tovthe room // * invited us tovthe room
oobData: React.PropTypes.object, oobData: PropTypes.object,
// is the RightPanel collapsed? // is the RightPanel collapsed?
collapsedRhs: React.PropTypes.bool, collapsedRhs: PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
@ -854,9 +855,13 @@ module.exports = React.createClass({
ev.dataTransfer.dropEffect = 'none'; ev.dataTransfer.dropEffect = 'none';
const items = ev.dataTransfer.items; const items = [...ev.dataTransfer.items];
if (items.length == 1) { if (items.length >= 1) {
if (items[0].kind == 'file') { const isDraggingFiles = items.every(function(item) {
return item.kind == 'file';
});
if (isDraggingFiles) {
this.setState({ draggingFile: true }); this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy'; ev.dataTransfer.dropEffect = 'copy';
} }
@ -867,10 +872,8 @@ module.exports = React.createClass({
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.setState({ draggingFile: false }); this.setState({ draggingFile: false });
const files = ev.dataTransfer.files; const files = [...ev.dataTransfer.files];
if (files.length == 1) { files.forEach(this.uploadFile);
this.uploadFile(files[0]);
}
}, },
onDragLeaveOrEnd: function(ev) { onDragLeaveOrEnd: function(ev) {
@ -1345,10 +1348,12 @@ module.exports = React.createClass({
}, },
onStatusBarHidden: function() { 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({ this.setState({
statusBarVisible: false, statusBarVisible: false,
}); });*/
}, },
showSettings: function(show) { showSettings: function(show) {

View File

@ -16,6 +16,7 @@ limitations under the License.
const React = require("react"); const React = require("react");
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const GeminiScrollbar = require('react-gemini-scrollbar'); const GeminiScrollbar = require('react-gemini-scrollbar');
import Promise from 'bluebird'; import Promise from 'bluebird';
import { KeyCode } from '../../Keyboard'; import { KeyCode } from '../../Keyboard';
@ -86,7 +87,7 @@ module.exports = React.createClass({
* scroll down to show the new element, rather than preserving the * scroll down to show the new element, rather than preserving the
* existing view. * existing view.
*/ */
stickyBottom: React.PropTypes.bool, stickyBottom: PropTypes.bool,
/* startAtBottom: if set to true, the view is assumed to start /* startAtBottom: if set to true, the view is assumed to start
* scrolled to the bottom. * scrolled to the bottom.
@ -95,7 +96,7 @@ module.exports = React.createClass({
* behaviour stays the same for other uses of ScrollPanel. * behaviour stays the same for other uses of ScrollPanel.
* If so, let's remove this parameter down the line. * 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 /* onFillRequest(backwards): a callback which is called on scroll when
* the user nears the start (backwards = true) or end (backwards = * 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 * directon (at this time) - which will stop the pagination cycle until
* the user scrolls again. * the user scrolls again.
*/ */
onFillRequest: React.PropTypes.func, onFillRequest: PropTypes.func,
/* onUnfillRequest(backwards): a callback which is called on scroll when /* onUnfillRequest(backwards): a callback which is called on scroll when
* there are children elements that are far out of view and could be removed * 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 * first element to remove if removing from the front/bottom, and last element
* to remove if removing from the back/top. * 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: 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 /* onResize: a callback which is called whenever the Gemini scroll
* panel is resized * panel is resized
*/ */
onResize: React.PropTypes.func, onResize: PropTypes.func,
/* className: classnames to add to the top-level div /* 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: styles to add to the top-level div
*/ */
style: React.PropTypes.object, style: PropTypes.object,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -17,79 +17,17 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import classNames from 'classnames'; import TagOrderStore from '../../stores/TagOrderStore';
import FilterStore from '../../stores/FilterStore';
import FlairStore from '../../stores/FlairStore'; import GroupActions from '../../actions/GroupActions';
import TagOrderActions from '../../actions/TagOrderActions';
import sdk from '../../index'; import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
const TagTile = React.createClass({ import { DragDropContext, Droppable } from 'react-beautiful-dnd';
displayName: 'TagTile',
propTypes: { const TagPanel = React.createClass({
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 ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
{ tip }
</div>
</AccessibleButton>;
},
});
export default React.createClass({
displayName: 'TagPanel', displayName: 'TagPanel',
contextTypes: { contextTypes: {
@ -98,7 +36,7 @@ export default React.createClass({
getInitialState() { getInitialState() {
return { return {
joinedGroupProfiles: [], orderedTags: [],
selectedTags: [], selectedTags: [],
}; };
}, },
@ -106,22 +44,25 @@ export default React.createClass({
componentWillMount: function() { componentWillMount: function() {
this.unmounted = false; this.unmounted = false;
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership); 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) { if (this.unmounted) {
return; return;
} }
this.setState({ this.setState({
selectedTags: FilterStore.getSelectedTags(), orderedTags: TagOrderStore.getOrderedTags() || [],
selectedTags: TagOrderStore.getSelectedTags(),
}); });
}); });
// This could be done by anything with a matrix client
this._fetchJoinedRooms(); dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient));
}, },
componentWillUnmount() { componentWillUnmount() {
this.unmounted = true; this.unmounted = true;
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
this.context.matrixClient.removeListener("sync", this.onClientSync);
if (this._filterStoreToken) { if (this._filterStoreToken) {
this._filterStoreToken.remove(); this._filterStoreToken.remove();
} }
@ -129,10 +70,22 @@ export default React.createClass({
_onGroupMyMembership() { _onGroupMyMembership() {
if (this.unmounted) return; 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'}); dis.dispatch({action: 'deselect_tags'});
}, },
@ -141,36 +94,58 @@ export default React.createClass({
dis.dispatch({action: 'view_create_group'}); dis.dispatch({action: 'view_create_group'});
}, },
async _fetchJoinedRooms() { onTagTileEndDrag(result) {
const joinedGroupResponse = await this.context.matrixClient.getJoinedGroups(); // Dragged to an invalid destination, not onto a droppable
const joinedGroupIds = joinedGroupResponse.groups; if (!result.destination) {
const joinedGroupProfiles = await Promise.all(joinedGroupIds.map( return;
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId), }
));
dis.dispatch({ // Dispatch synchronously so that the TagPanel receives an
action: 'all_tags', // optimistic update from TagOrderStore before the previous
tags: joinedGroupIds, // state is shown.
}); dis.dispatch(TagOrderActions.moveTag(
this.setState({joinedGroupProfiles}); this.context.matrixClient,
result.draggableId,
result.destination.index,
), true);
}, },
render() { render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg'); const TintableSvg = sdk.getComponent('elements.TintableSvg');
const tags = this.state.joinedGroupProfiles.map((groupProfile, index) => { const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
return <TagTile
key={groupProfile.groupId + '_' + index} const tags = this.state.orderedTags.map((tag, index) => {
groupProfile={groupProfile} return <DNDTagTile
selected={this.state.selectedTags.includes(groupProfile.groupId)} key={tag}
tag={tag}
index={index}
selected={this.state.selectedTags.includes(tag)}
/>; />;
}); });
return <div className="mx_TagPanel" onClick={this.onClick}> return <div className="mx_TagPanel">
<div className="mx_TagPanel_tagTileContainer"> <DragDropContext onDragEnd={this.onTagTileEndDrag}>
{ tags } <Droppable droppableId="tag-panel-droppable">
</div> { (provided, snapshot) => (
<div
className="mx_TagPanel_tagTileContainer"
ref={provided.innerRef}
// react-beautiful-dnd has a bug that emits a click to the parent
// of draggables upon dropping
// https://github.com/atlassian/react-beautiful-dnd/issues/273
// so we use onMouseDown here as a workaround.
onMouseDown={this.onClick}
>
{ tags }
{ provided.placeholder }
</div>
) }
</Droppable>
</DragDropContext>
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}> <AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" /> <TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
</AccessibleButton> </AccessibleButton>
</div>; </div>;
}, },
}); });
export default TagPanel;

View File

@ -19,6 +19,7 @@ import SettingsStore from "../../settings/SettingsStore";
const React = require('react'); const React = require('react');
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
import Promise from 'bluebird'; import Promise from 'bluebird';
const Matrix = require("matrix-js-sdk"); 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 // 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 // a timeline representing. If it has a room, we maintain RRs etc for
// that room. // 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. // Enable managing RRs and RMs. These require the timelineSet to have a room.
manageReadReceipts: React.PropTypes.bool, manageReadReceipts: PropTypes.bool,
manageReadMarkers: React.PropTypes.bool, manageReadMarkers: PropTypes.bool,
// true to give the component a 'display: none' style. // 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. // ID of an event to highlight. If undefined, no event will be highlighted.
// typically this will be either 'eventId' or undefined. // 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 // id of an event to jump to. If not given, will go to the end of the
// live timeline. // live timeline.
eventId: React.PropTypes.string, eventId: PropTypes.string,
// where to position the event given by eventId, in pixels from the // 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 // bottom of the viewport. If not given, will try to put the event
// half way down the viewport. // half way down the viewport.
eventPixelOffset: React.PropTypes.number, eventPixelOffset: PropTypes.number,
// Should we show URL Previews // Should we show URL Previews
showUrlPreview: React.PropTypes.bool, showUrlPreview: PropTypes.bool,
// callback which is called when the panel is scrolled. // 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. // 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 // maximum number of events to show in a timeline
timelineCap: React.PropTypes.number, timelineCap: PropTypes.number,
// classname to use for the messagepanel // classname to use for the messagepanel
className: React.PropTypes.string, className: PropTypes.string,
// shape property to be passed to EventTiles // shape property to be passed to EventTiles
tileShape: React.PropTypes.string, tileShape: PropTypes.string,
// placeholder text to use if the timeline is empty // placeholder text to use if the timeline is empty
empty: React.PropTypes.string, empty: PropTypes.string,
}, },
statics: { statics: {
@ -301,6 +302,8 @@ var TimelinePanel = React.createClass({
// set off a pagination request. // set off a pagination request.
onMessageListFillRequest: function(backwards) { onMessageListFillRequest: function(backwards) {
if (!this._shouldPaginate()) return Promise.resolve(false);
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate'; const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating'; const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
@ -1090,6 +1093,17 @@ var TimelinePanel = React.createClass({
}, this.props.onReadMarkerUpdated); }, 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() { render: function() {
const MessagePanel = sdk.getComponent("structures.MessagePanel"); const MessagePanel = sdk.getComponent("structures.MessagePanel");
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types';
const ContentMessages = require('../../ContentMessages'); const ContentMessages = require('../../ContentMessages');
const dis = require('../../dispatcher'); const dis = require('../../dispatcher');
const filesize = require('filesize'); const filesize = require('filesize');
@ -22,7 +23,7 @@ import { _t } from '../../languageHandler';
module.exports = React.createClass({displayName: 'UploadBar', module.exports = React.createClass({displayName: 'UploadBar',
propTypes: { propTypes: {
room: React.PropTypes.object, room: PropTypes.object,
}, },
componentDidMount: function() { componentDidMount: function() {

View File

@ -19,6 +19,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
const React = require('react'); const React = require('react');
const ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
import PropTypes from 'prop-types';
const sdk = require('../../index'); const sdk = require('../../index');
const MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
const PlatformPeg = require("../../PlatformPeg"); const PlatformPeg = require("../../PlatformPeg");
@ -125,8 +126,8 @@ const THEMES = [
const IgnoredUser = React.createClass({ const IgnoredUser = React.createClass({
propTypes: { propTypes: {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
onUnignored: React.PropTypes.func.isRequired, onUnignored: PropTypes.func.isRequired,
}, },
_onUnignoreClick: function() { _onUnignoreClick: function() {
@ -155,16 +156,16 @@ module.exports = React.createClass({
displayName: 'UserSettings', displayName: 'UserSettings',
propTypes: { propTypes: {
onClose: React.PropTypes.func, onClose: PropTypes.func,
// The brand string given when creating email pushers // 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. // 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 // Team token for the referral link. If falsy, the referral section will
// not appear // not appear
teamToken: React.PropTypes.string, teamToken: PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -375,7 +376,7 @@ module.exports = React.createClass({
{ _t("For security, logging out will delete any end-to-end " + { _t("For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " + "encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " + "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.") }
</div>, </div>,
button: _t("Sign out"), button: _t("Sign out"),
extraButtons: [ extraButtons: [
@ -811,6 +812,12 @@ module.exports = React.createClass({
<h3>{ _t('Analytics') }</h3> <h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ _t('Riot collects anonymous analytics to allow us to improve the application.') } { _t('Riot collects anonymous analytics to allow us to improve the application.') }
<br />
{ _t('Privacy is important to us, so we don\'t collect any personal'
+ ' or identifiable data for our analytics.') }
<div className="mx_UserSettings_advanced_spoiler" onClick={Analytics.showDetailsModal}>
{ _t('Learn more about how we use analytics.') }
</div>
{ ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) } { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
</div> </div>
</div>; </div>;

View File

@ -18,6 +18,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -29,13 +30,13 @@ module.exports = React.createClass({
displayName: 'ForgotPassword', displayName: 'ForgotPassword',
propTypes: { propTypes: {
defaultHsUrl: React.PropTypes.string, defaultHsUrl: PropTypes.string,
defaultIsUrl: React.PropTypes.string, defaultIsUrl: PropTypes.string,
customHsUrl: React.PropTypes.string, customHsUrl: PropTypes.string,
customIsUrl: React.PropTypes.string, customIsUrl: PropTypes.string,
onLoginClick: React.PropTypes.func, onLoginClick: PropTypes.func,
onRegisterClick: React.PropTypes.func, onRegisterClick: PropTypes.func,
onComplete: React.PropTypes.func.isRequired, onComplete: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -18,6 +18,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
@ -36,27 +37,27 @@ module.exports = React.createClass({
displayName: 'Login', displayName: 'Login',
propTypes: { propTypes: {
onLoggedIn: React.PropTypes.func.isRequired, onLoggedIn: PropTypes.func.isRequired,
enableGuest: React.PropTypes.bool, enableGuest: PropTypes.bool,
customHsUrl: React.PropTypes.string, customHsUrl: PropTypes.string,
customIsUrl: React.PropTypes.string, customIsUrl: PropTypes.string,
defaultHsUrl: React.PropTypes.string, defaultHsUrl: PropTypes.string,
defaultIsUrl: React.PropTypes.string, defaultIsUrl: PropTypes.string,
// Secondary HS which we try to log into if the user is using // Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a // the default HS but login fails. Useful for migrating to a
// different home server without confusing users. // 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. // 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. // login shouldn't care how password recovery is done.
onForgotPasswordClick: React.PropTypes.func, onForgotPasswordClick: PropTypes.func,
onCancelClick: React.PropTypes.func, onCancelClick: PropTypes.func,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -25,7 +26,7 @@ module.exports = React.createClass({
displayName: 'PostRegistration', displayName: 'PostRegistration',
propTypes: { propTypes: {
onComplete: React.PropTypes.func.isRequired, onComplete: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -19,6 +19,7 @@ import Matrix from 'matrix-js-sdk';
import Promise from 'bluebird'; import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import ServerConfig from '../../views/login/ServerConfig'; import ServerConfig from '../../views/login/ServerConfig';
@ -35,31 +36,31 @@ module.exports = React.createClass({
displayName: 'Registration', displayName: 'Registration',
propTypes: { propTypes: {
onLoggedIn: React.PropTypes.func.isRequired, onLoggedIn: PropTypes.func.isRequired,
clientSecret: React.PropTypes.string, clientSecret: PropTypes.string,
sessionId: React.PropTypes.string, sessionId: PropTypes.string,
makeRegistrationUrl: React.PropTypes.func.isRequired, makeRegistrationUrl: PropTypes.func.isRequired,
idSid: React.PropTypes.string, idSid: PropTypes.string,
customHsUrl: React.PropTypes.string, customHsUrl: PropTypes.string,
customIsUrl: React.PropTypes.string, customIsUrl: PropTypes.string,
defaultHsUrl: React.PropTypes.string, defaultHsUrl: PropTypes.string,
defaultIsUrl: React.PropTypes.string, defaultIsUrl: PropTypes.string,
brand: React.PropTypes.string, brand: PropTypes.string,
email: React.PropTypes.string, email: PropTypes.string,
referrer: React.PropTypes.string, referrer: PropTypes.string,
teamServerConfig: React.PropTypes.shape({ teamServerConfig: PropTypes.shape({
// Email address to request new teams // 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 // 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. // registration shouldn't know or care how login is done.
onLoginClick: React.PropTypes.func.isRequired, onLoginClick: PropTypes.func.isRequired,
onCancelClick: React.PropTypes.func, onCancelClick: PropTypes.func,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk';
import AvatarLogic from '../../../Avatar'; import AvatarLogic from '../../../Avatar';
import sdk from '../../../index'; import sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -23,16 +25,20 @@ module.exports = React.createClass({
displayName: 'BaseAvatar', displayName: 'BaseAvatar',
propTypes: { propTypes: {
name: React.PropTypes.string.isRequired, // The name (first initial used as default) name: PropTypes.string.isRequired, // The name (first initial used as default)
idName: React.PropTypes.string, // ID for generating hash colours idName: PropTypes.string, // ID for generating hash colours
title: React.PropTypes.string, // onHover title text title: PropTypes.string, // onHover title text
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0] url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority] urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
width: React.PropTypes.number, width: PropTypes.number,
height: React.PropTypes.number, height: PropTypes.number,
// XXX resizeMethod not actually used. // XXX resizeMethod not actually used.
resizeMethod: React.PropTypes.string, resizeMethod: PropTypes.string,
defaultToInitialLetter: React.PropTypes.bool, // true to add default url defaultToInitialLetter: PropTypes.bool, // true to add default url
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -48,6 +54,16 @@ module.exports = React.createClass({
return this._getState(this.props); 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) { componentWillReceiveProps: function(nextProps) {
// work out if we need to call setState (if the image URLs array has changed) // work out if we need to call setState (if the image URLs array has changed)
const newState = this._getState(nextProps); 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) { _getState: function(props) {
// work out the full set of urls to try to load. This is formed like so: // work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, props.urls, default image ] // imageUrls: [ props.url, props.urls, default image ]

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types';
const Avatar = require('../../../Avatar'); const Avatar = require('../../../Avatar');
const sdk = require("../../../index"); const sdk = require("../../../index");
const dispatcher = require("../../../dispatcher"); const dispatcher = require("../../../dispatcher");
@ -25,15 +26,15 @@ module.exports = React.createClass({
displayName: 'MemberAvatar', displayName: 'MemberAvatar',
propTypes: { propTypes: {
member: React.PropTypes.object.isRequired, member: PropTypes.object.isRequired,
width: React.PropTypes.number, width: PropTypes.number,
height: React.PropTypes.number, height: PropTypes.number,
resizeMethod: React.PropTypes.string, resizeMethod: PropTypes.string,
// The onClick to give the avatar // 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' // Whether the onClick of the avatar should be overriden to dispatch 'view_user'
viewUserOnClick: React.PropTypes.bool, viewUserOnClick: PropTypes.bool,
title: React.PropTypes.string, title: PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -17,6 +17,7 @@
'use strict'; 'use strict';
import React from "react"; import React from "react";
import PropTypes from 'prop-types';
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -30,10 +31,10 @@ module.exports = React.createClass({
displayName: 'MemberPresenceAvatar', displayName: 'MemberPresenceAvatar',
propTypes: { propTypes: {
member: React.PropTypes.object.isRequired, member: PropTypes.object.isRequired,
width: React.PropTypes.number, width: PropTypes.number,
height: React.PropTypes.number, height: PropTypes.number,
resizeMethod: React.PropTypes.string, resizeMethod: PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from 'prop-types';
import {ContentRepo} from "matrix-js-sdk"; import {ContentRepo} from "matrix-js-sdk";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import sdk from "../../../index"; import sdk from "../../../index";
@ -25,11 +26,11 @@ module.exports = React.createClass({
// oobData.avatarUrl should be set (else there // oobData.avatarUrl should be set (else there
// would be nowhere to get the avatar from) // would be nowhere to get the avatar from)
propTypes: { propTypes: {
room: React.PropTypes.object, room: PropTypes.object,
oobData: React.PropTypes.object, oobData: PropTypes.object,
width: React.PropTypes.number, width: PropTypes.number,
height: React.PropTypes.number, height: PropTypes.number,
resizeMethod: React.PropTypes.string, resizeMethod: PropTypes.string,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -17,11 +17,12 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CreateRoomButton', displayName: 'CreateRoomButton',
propTypes: { propTypes: {
onCreateRoom: React.PropTypes.func, onCreateRoom: PropTypes.func,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const Presets = { const Presets = {
@ -28,8 +29,8 @@ const Presets = {
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CreateRoomPresets', displayName: 'CreateRoomPresets',
propTypes: { propTypes: {
onChange: React.PropTypes.func, onChange: PropTypes.func,
preset: React.PropTypes.string, preset: PropTypes.string,
}, },
Presets: Presets, Presets: Presets,

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
@ -22,9 +23,9 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// Specifying a homeserver will make magical things happen when you, // Specifying a homeserver will make magical things happen when you,
// e.g. start typing in the room alias box. // e.g. start typing in the room alias box.
homeserver: React.PropTypes.string, homeserver: PropTypes.string,
alias: React.PropTypes.string, alias: PropTypes.string,
onChange: React.PropTypes.func, onChange: PropTypes.func,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import AccessibleButton from '../elements/AccessibleButton';
import Promise from 'bluebird'; import Promise from 'bluebird';
import { addressTypes, getAddressType } from '../../../UserAddress.js'; import { addressTypes, getAddressType } from '../../../UserAddress.js';
import GroupStoreCache from '../../../stores/GroupStoreCache'; import GroupStoreCache from '../../../stores/GroupStoreCache';
@ -507,7 +506,8 @@ module.exports = React.createClass({
}, },
render: function() { 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"); const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null; this.scrollElement = null;
@ -580,14 +580,8 @@ module.exports = React.createClass({
} }
return ( return (
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}> <BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
<div className="mx_Dialog_title"> onFinished={this.props.onFinished} title={this.props.title}>
{ this.props.title }
</div>
<AccessibleButton className="mx_ChatInviteDialog_cancel"
onClick={this.onCancel} >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton>
<div className="mx_ChatInviteDialog_label"> <div className="mx_ChatInviteDialog_label">
<label htmlFor="textinput">{ this.props.description }</label> <label htmlFor="textinput">{ this.props.description }</label>
</div> </div>
@ -597,12 +591,10 @@ module.exports = React.createClass({
{ addressSelector } { addressSelector }
{ this.props.extraNode } { this.props.extraNode }
</div> </div>
<div className="mx_Dialog_buttons"> <DialogButtons primaryButton={this.props.button}
<button className="mx_Dialog_primary" onClick={this.onButtonClick}> onPrimaryButtonClick={this.onButtonClick}
{ this.props.button } onCancel={this.onCancel} />
</button> </BaseDialog>
</div>
</div>
); );
}, },
}); });

View File

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import FocusTrap from 'focus-trap-react'; import FocusTrap from 'focus-trap-react';
import PropTypes from 'prop-types';
import { KeyCode } from '../../../Keyboard'; import { KeyCode } from '../../../Keyboard';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
@ -32,17 +33,20 @@ export default React.createClass({
propTypes: { propTypes: {
// onFinished callback to call when Escape is pressed // 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 // CSS class to apply to dialog div
className: React.PropTypes.string, className: PropTypes.string,
// Title for the dialog. // Title for the dialog.
// (could probably actually be something more complicated than a string if desired) // (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 should be the content of the dialog
children: React.PropTypes.node, children: PropTypes.node,
// Id of content element // Id of content element
// If provided, this is used to add a aria-describedby attribute // If provided, this is used to add a aria-describedby attribute
@ -50,6 +54,9 @@ export default React.createClass({
}, },
_onKeyDown: function(e) { _onKeyDown: function(e) {
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
if (e.keyCode === KeyCode.ESCAPE) { if (e.keyCode === KeyCode.ESCAPE) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
@ -76,7 +83,7 @@ export default React.createClass({
> >
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" /> <TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
</AccessibleButton> </AccessibleButton>
<div className='mx_Dialog_title' id='mx_BaseDialog_title'> <div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
{ this.props.title } { this.props.title }
</div> </div>
{ this.props.children } { this.props.children }

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
@ -137,6 +138,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
} else { } else {
// Show the avatar, name and a button to confirm that a new chat is requested // Show the avatar, name and a button to confirm that a new chat is requested
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Spinner = sdk.getComponent('elements.Spinner'); const Spinner = sdk.getComponent('elements.Spinner');
title = _t('Start chatting'); title = _t('Start chatting');
@ -166,11 +168,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
</p> </p>
{ profile } { profile }
</div> </div>
<div className="mx_Dialog_buttons"> <DialogButtons primaryButton={_t('Start Chatting')}
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick} autoFocus="true"> onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
{ _t('Start Chatting') }
</button>
</div>
</div>; </div>;
} }
@ -187,10 +186,10 @@ export default class ChatCreateOrReuseDialog extends React.Component {
} }
} }
ChatCreateOrReuseDialog.propTypes = { ChatCreateOrReuseDialog.propTyps = {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
// Called when clicking outside of the dialog // Called when clicking outside of the dialog
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
onNewDMClick: React.PropTypes.func.isRequired, onNewDMClick: PropTypes.func.isRequired,
onExistingRoomSelected: React.PropTypes.func.isRequired, onExistingRoomSelected: PropTypes.func.isRequired,
}; };

View File

@ -15,10 +15,10 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk'; import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import classnames from 'classnames';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';
/* /*
@ -33,20 +33,20 @@ export default React.createClass({
displayName: 'ConfirmUserActionDialog', displayName: 'ConfirmUserActionDialog',
propTypes: { propTypes: {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember' // 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' // group member object. Supply either this or 'member'
groupMember: GroupMemberType, groupMember: GroupMemberType,
// needed if a group member is specified // needed if a group member is specified
matrixClient: React.PropTypes.instanceOf(MatrixClient), matrixClient: PropTypes.instanceOf(MatrixClient),
action: React.PropTypes.string.isRequired, // eg. 'Ban' action: PropTypes.string.isRequired, // eg. 'Ban'
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?' title: PropTypes.string.isRequired, // eg. 'Ban this user?'
// Whether to display a text field for a reason // Whether to display a text field for a reason
// If true, the second argument to onFinished will // If true, the second argument to onFinished will
// be the string entered. // be the string entered.
askReason: React.PropTypes.bool, askReason: PropTypes.bool,
danger: React.PropTypes.bool, danger: PropTypes.bool,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
defaultProps: { defaultProps: {
@ -76,13 +76,11 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar"); const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
const confirmButtonClass = classnames({ const confirmButtonClass = this.props.danger ? 'danger' : '';
'mx_Dialog_primary': true,
'danger': this.props.danger,
});
let reasonBox; let reasonBox;
if (this.props.askReason) { if (this.props.askReason) {
@ -127,17 +125,11 @@ export default React.createClass({
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div> <div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
</div> </div>
{ reasonBox } { reasonBox }
<div className="mx_Dialog_buttons"> <DialogButtons primaryButton={this.props.action}
<button className={confirmButtonClass} onPrimaryButtonClick={this.onOk}
onClick={this.onOk} autoFocus={!this.props.askReason} primaryButtonClass={confirmButtonClass}
> focus={!this.props.askReason}
{ this.props.action } onCancel={this.onCancel} />
</button>
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
</div>
</BaseDialog> </BaseDialog>
); );
}, },

View File

@ -55,11 +55,15 @@ export default React.createClass({
_checkGroupId: function(e) { _checkGroupId: function(e) {
let error = null; 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 '=_-./'"); error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
} }
this.setState({ this.setState({
groupIdError: error, groupIdError: error,
// Reset createError to get rid of now stale error message
createError: null,
}); });
return error; return error;
}, },
@ -158,10 +162,10 @@ export default React.createClass({
{ createErrorNode } { createErrorNode }
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
<button onClick={this._onCancel}> <button onClick={this._onCancel}>
{ _t("Cancel") } { _t("Cancel") }
</button> </button>
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
</div> </div>
</form> </form>
</BaseDialog> </BaseDialog>

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -22,7 +23,7 @@ import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'CreateRoomDialog', displayName: 'CreateRoomDialog',
propTypes: { propTypes: {
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
componentDidMount: function() { componentDidMount: function() {
@ -41,6 +42,7 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
title={_t('Create Room')} title={_t('Create Room')}
@ -53,27 +55,11 @@ export default React.createClass({
<div> <div>
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" /> <input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
</div> </div>
<br />
<details className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ _t('Advanced options') }</summary>
<div>
<input type="checkbox" id="checkbox" ref="checkbox" defaultChecked={this.defaultNoFederate} />
<label htmlFor="checkbox">
{ _t('Block users on other matrix homeservers from joining this room') }
<br />
({ _t('This setting cannot be changed later!') })
</label>
</div>
</details>
</div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t('Cancel') }
</button>
<input type="submit" className="mx_Dialog_primary" value={_t('Create Room')} />
</div> </div>
</form> </form>
<DialogButtons primaryButton={_t('Create Room')}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />
</BaseDialog> </BaseDialog>
); );
}, },

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import Analytics from '../../../Analytics'; import Analytics from '../../../Analytics';
@ -77,6 +78,7 @@ export default class DeactivateAccountDialog extends React.Component {
} }
render() { render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
let passwordBoxClass = ''; let passwordBoxClass = '';
@ -99,10 +101,11 @@ export default class DeactivateAccountDialog extends React.Component {
} }
return ( return (
<div className="mx_DeactivateAccountDialog"> <BaseDialog className="mx_DeactivateAccountDialog"
<div className="mx_Dialog_title danger"> onFinished={this.props.onFinished}
{ _t("Deactivate Account") } onEnterPressed={this.onOk}
</div> titleClass="danger"
title={_t("Deactivate Account")}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p> <p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
@ -130,11 +133,11 @@ export default class DeactivateAccountDialog extends React.Component {
{ cancelButton } { cancelButton }
</div> </div>
</div> </BaseDialog>
); );
} }
} }
DeactivateAccountDialog.propTypes = { DeactivateAccountDialog.propTypes = {
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}; };

View File

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import * as FormattingUtils from '../../../utils/FormattingUtils'; import * as FormattingUtils from '../../../utils/FormattingUtils';
@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) {
} }
DeviceVerifyDialog.propTypes = { DeviceVerifyDialog.propTypes = {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired, device: PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}; };

View File

@ -26,20 +26,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'ErrorDialog', displayName: 'ErrorDialog',
propTypes: { propTypes: {
title: React.PropTypes.string, title: PropTypes.string,
description: React.PropTypes.oneOfType([ description: PropTypes.oneOfType([
React.PropTypes.element, PropTypes.element,
React.PropTypes.string, PropTypes.string,
]), ]),
button: React.PropTypes.string, button: PropTypes.string,
focus: React.PropTypes.bool, focus: PropTypes.bool,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -27,22 +28,22 @@ export default React.createClass({
propTypes: { propTypes: {
// matrix client to use for UI auth requests // 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 // response from initial request. If not supplied, will do a request on
// mount. // mount.
authData: React.PropTypes.shape({ authData: PropTypes.shape({
flows: React.PropTypes.array, flows: PropTypes.array,
params: React.PropTypes.object, params: PropTypes.object,
session: React.PropTypes.string, session: PropTypes.string,
}), }),
// callback // 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() { getInitialState: function() {

View File

@ -16,6 +16,7 @@ limitations under the License.
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
@ -30,10 +31,10 @@ import { _t, _td } from '../../../languageHandler';
*/ */
export default React.createClass({ export default React.createClass({
propTypes: { propTypes: {
matrixClient: React.PropTypes.object.isRequired, matrixClient: PropTypes.object.isRequired,
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
deviceId: React.PropTypes.string.isRequired, deviceId: PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -16,20 +16,20 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import classnames from 'classnames';
export default React.createClass({ export default React.createClass({
displayName: 'QuestionDialog', displayName: 'QuestionDialog',
propTypes: { propTypes: {
title: React.PropTypes.string, title: PropTypes.string,
description: React.PropTypes.node, description: PropTypes.node,
extraButtons: React.PropTypes.node, extraButtons: PropTypes.node,
button: React.PropTypes.string, button: PropTypes.string,
danger: React.PropTypes.bool, danger: PropTypes.bool,
focus: React.PropTypes.bool, focus: PropTypes.bool,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -53,15 +53,11 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const cancelButton = this.props.hasCancelButton ? ( const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
<button onClick={this.onCancel}> let primaryButtonClass = "";
{ _t("Cancel") } if (this.props.danger) {
</button> primaryButtonClass = "danger";
) : null; }
const buttonClasses = classnames({
mx_Dialog_primary: true,
danger: this.props.danger,
});
return ( return (
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
title={this.props.title} title={this.props.title}
@ -70,13 +66,14 @@ export default React.createClass({
<div className="mx_Dialog_content" id='mx_Dialog_content'> <div className="mx_Dialog_content" id='mx_Dialog_content'>
{ this.props.description } { this.props.description }
</div> </div>
<div className="mx_Dialog_buttons"> <DialogButtons primaryButton={this.props.button || _t('OK')}
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}> onPrimaryButtonClick={this.onOk}
{ this.props.button || _t('OK') } primaryButtonClass={primaryButtonClass}
</button> focus={this.props.focus}
onCancel={this.onCancel}
>
{ this.props.extraButtons } { this.props.extraButtons }
{ cancelButton } </DialogButtons>
</div>
</BaseDialog> </BaseDialog>
); );
}, },

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -25,8 +26,8 @@ export default React.createClass({
displayName: 'SessionRestoreErrorDialog', displayName: 'SessionRestoreErrorDialog',
propTypes: { propTypes: {
error: React.PropTypes.string.isRequired, error: PropTypes.string.isRequired,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
componentDidMount: function() { componentDidMount: function() {
@ -46,6 +47,7 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
let bugreport; let bugreport;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
@ -78,11 +80,9 @@ export default React.createClass({
{ bugreport } { bugreport }
</div> </div>
<div className="mx_Dialog_buttons"> <DialogButtons primaryButton={_t("Continue anyway")}
<button className="mx_Dialog_primary" onClick={this._continueClicked} autoFocus={shouldFocusContinueButton}> onPrimaryButtonClick={this._continueClicked} focus={shouldFocusContinueButton}
{ _t("Continue anyway") } onCancel={this.props.onFinished} />
</button>
</div>
</BaseDialog> </BaseDialog>
); );
}, },

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import Email from '../../../email'; import Email from '../../../email';
import AddThreepid from '../../../AddThreepid'; import AddThreepid from '../../../AddThreepid';
@ -30,7 +31,7 @@ import Modal from '../../../Modal';
export default React.createClass({ export default React.createClass({
displayName: 'SetEmailDialog', displayName: 'SetEmailDialog',
propTypes: { propTypes: {
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -17,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import classnames from 'classnames'; import classnames from 'classnames';
@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
export default React.createClass({ export default React.createClass({
displayName: 'SetMxIdDialog', displayName: 'SetMxIdDialog',
propTypes: { propTypes: {
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
// Called when the user requests to register with a different homeserver // 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 // Called if the user wants to switch to login instead
onLoginClick: React.PropTypes.func.isRequired, onLoginClick: PropTypes.func.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -15,21 +15,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler';
export default React.createClass({ export default React.createClass({
displayName: 'TextInputDialog', displayName: 'TextInputDialog',
propTypes: { propTypes: {
title: React.PropTypes.string, title: PropTypes.string,
description: React.PropTypes.oneOfType([ description: PropTypes.oneOfType([
React.PropTypes.element, PropTypes.element,
React.PropTypes.string, PropTypes.string,
]), ]),
value: React.PropTypes.string, value: PropTypes.string,
button: React.PropTypes.string, button: PropTypes.string,
focus: React.PropTypes.bool, focus: PropTypes.bool,
onFinished: React.PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, },
getDefaultProps: function() { getDefaultProps: function() {
@ -58,6 +58,7 @@ export default React.createClass({
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
title={this.props.title} title={this.props.title}
@ -71,13 +72,10 @@ export default React.createClass({
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" /> <input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
</div> </div>
</div> </div>
<div className="mx_Dialog_buttons">
<button onClick={this.onCancel}>
{ _t("Cancel") }
</button>
<input type="submit" className="mx_Dialog_primary" value={this.props.button} />
</div>
</form> </form>
<DialogButtons primaryButton={this.props.button}
onPrimaryButtonClick={this.onOk}
onCancel={this.onCancel} />
</BaseDialog> </BaseDialog>
); );
}, },

View File

@ -23,14 +23,7 @@ import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { markAllDevicesKnown } from '../../../cryptodevices';
function markAllDevicesKnown(devices) {
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
});
});
}
function DeviceListEntry(props) { function DeviceListEntry(props) {
const {userId, device} = props; const {userId, device} = props;
@ -141,7 +134,7 @@ export default React.createClass({
}, },
_onSendAnywayClicked: function() { _onSendAnywayClicked: function() {
markAllDevicesKnown(this.props.devices); markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
this.props.onFinished(); this.props.onFinished();
this.props.onSend(); this.props.onSend();
@ -187,18 +180,11 @@ export default React.createClass({
} }
}); });
}); });
let sendButton; const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
if (haveUnknownDevices) { const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
sendButton = <button onClick={this._onSendAnywayClicked}>
{ this.props.sendAnywayLabel }
</button>;
} else {
sendButton = <button onClick={this._onSendClicked}>
{ this.props.sendLabel }
</button>;
}
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return ( return (
<BaseDialog className='mx_UnknownDeviceDialog' <BaseDialog className='mx_UnknownDeviceDialog'
onFinished={this.props.onFinished} onFinished={this.props.onFinished}
@ -214,14 +200,9 @@ export default React.createClass({
<UnknownDeviceList devices={this.props.devices} /> <UnknownDeviceList devices={this.props.devices} />
</GeminiScrollbar> </GeminiScrollbar>
<div className="mx_Dialog_buttons"> <DialogButtons primaryButton={sendButtonLabel}
{sendButton} onPrimaryButtonClick={sendButtonOnClick}
<button className="mx_Dialog_primary" autoFocus={true} onCancel={this._onDismissClicked} />
onClick={this._onDismissClicked}
>
{_t("Dismiss")}
</button>
</div>
</BaseDialog> </BaseDialog>
); );
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point? // XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?

View File

@ -15,6 +15,7 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { KeyCode } from '../../../Keyboard'; import { KeyCode } from '../../../Keyboard';
@ -72,9 +73,9 @@ export default function AccessibleButton(props) {
* implemented exactly like a normal onClick handler. * implemented exactly like a normal onClick handler.
*/ */
AccessibleButton.propTypes = { AccessibleButton.propTypes = {
children: React.PropTypes.node, children: PropTypes.node,
element: React.PropTypes.string, element: PropTypes.string,
onClick: React.PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
}; };
AccessibleButton.defaultProps = { AccessibleButton.defaultProps = {

View File

@ -18,6 +18,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import classNames from 'classnames'; import classNames from 'classnames';
import { UserAddressType } from '../../../UserAddress'; import { UserAddressType } from '../../../UserAddress';
@ -26,17 +27,17 @@ export default React.createClass({
displayName: 'AddressSelector', displayName: 'AddressSelector',
propTypes: { propTypes: {
onSelected: React.PropTypes.func.isRequired, onSelected: PropTypes.func.isRequired,
// List of the addresses to display // 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 // Whether to show the address on the address tiles
showAddress: React.PropTypes.bool, showAddress: PropTypes.bool,
truncateAt: React.PropTypes.number.isRequired, truncateAt: PropTypes.number.isRequired,
selected: React.PropTypes.number, selected: PropTypes.number,
// Element to put as a header on top of the list // Element to put as a header on top of the list
header: React.PropTypes.node, header: PropTypes.node,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import sdk from "../../../index"; import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
@ -28,9 +29,9 @@ export default React.createClass({
propTypes: { propTypes: {
address: UserAddressType.isRequired, address: UserAddressType.isRequired,
canDismiss: React.PropTypes.bool, canDismiss: PropTypes.bool,
onDismissed: React.PropTypes.func, onDismissed: PropTypes.func,
justified: React.PropTypes.bool, justified: PropTypes.bool,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -19,6 +19,7 @@ limitations under the License.
import url from 'url'; import url from 'url';
import qs from 'querystring'; import qs from 'querystring';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarAuthClient from '../../../ScalarAuthClient';
@ -40,19 +41,19 @@ export default React.createClass({
displayName: 'AppTile', displayName: 'AppTile',
propTypes: { propTypes: {
id: React.PropTypes.string.isRequired, id: PropTypes.string.isRequired,
url: React.PropTypes.string.isRequired, url: PropTypes.string.isRequired,
name: React.PropTypes.string.isRequired, name: PropTypes.string.isRequired,
room: React.PropTypes.object.isRequired, room: PropTypes.object.isRequired,
type: React.PropTypes.string.isRequired, type: PropTypes.string.isRequired,
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer. // 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. // 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 of the current user
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
// UserId of the entity that added / modified the widget // UserId of the entity that added / modified the widget
creatorUserId: React.PropTypes.string, creatorUserId: PropTypes.string,
waitForIframeLoad: React.PropTypes.bool, waitForIframeLoad: PropTypes.bool,
}, },
getDefaultProps() { getDefaultProps() {

View File

@ -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 <div>
<Draggable
key={props.tag}
draggableId={props.tag}
index={props.index}
>
{ (provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<TagTile {...props} />
</div>
{ provided.placeholder }
</div>
) }
</Draggable>
</div>;
}

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index'; import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -24,8 +25,8 @@ export default React.createClass({
displayName: 'DeviceVerifyButtons', displayName: 'DeviceVerifyButtons',
propTypes: { propTypes: {
userId: React.PropTypes.string.isRequired, userId: PropTypes.string.isRequired,
device: React.PropTypes.object.isRequired, device: PropTypes.object.isRequired,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -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 (
<div className="mx_Dialog_buttons">
<button className={primaryButtonClassName}
onClick={this.props.onPrimaryButtonClick}
autoFocus={this.props.focus}
>
{ this.props.primaryButton }
</button>
{ this.props.children }
<button onClick={this.props.onCancel}>
{ _t("Cancel") }
</button>
</div>
);
},
});

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
export default class DirectorySearchBox extends React.Component { export default class DirectorySearchBox extends React.Component {
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
} }
DirectorySearchBox.propTypes = { DirectorySearchBox.propTypes = {
className: React.PropTypes.string, className: PropTypes.string,
onChange: React.PropTypes.func, onChange: PropTypes.func,
onClear: React.PropTypes.func, onClear: PropTypes.func,
onJoinClick: React.PropTypes.func, onJoinClick: PropTypes.func,
placeholder: React.PropTypes.string, placeholder: PropTypes.string,
showJoinButton: React.PropTypes.bool, showJoinButton: PropTypes.bool,
}; };

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -56,14 +57,14 @@ class MenuOption extends React.Component {
} }
MenuOption.propTypes = { MenuOption.propTypes = {
children: React.PropTypes.oneOfType([ children: PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node), PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node, PropTypes.node,
]), ]),
highlighted: React.PropTypes.bool, highlighted: PropTypes.bool,
dropdownKey: React.PropTypes.string, dropdownKey: PropTypes.string,
onClick: React.PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
onMouseEnter: React.PropTypes.func.isRequired, onMouseEnter: PropTypes.func.isRequired,
}; };
/* /*
@ -322,20 +323,20 @@ Dropdown.propTypes = {
// The width that the dropdown should be. If specified, // The width that the dropdown should be. If specified,
// the dropped-down part of the menu will be set to this // the dropped-down part of the menu will be set to this
// width. // width.
menuWidth: React.PropTypes.number, menuWidth: PropTypes.number,
// Called when the selected option changes // Called when the selected option changes
onOptionChange: React.PropTypes.func.isRequired, onOptionChange: PropTypes.func.isRequired,
// Called when the value of the search field changes // Called when the value of the search field changes
onSearchChange: React.PropTypes.func, onSearchChange: PropTypes.func,
searchEnabled: React.PropTypes.bool, searchEnabled: PropTypes.bool,
// Function that, given the key of an option, returns // Function that, given the key of an option, returns
// a node representing that option to be displayed in the // a node representing that option to be displayed in the
// box itself as the currently-selected option (ie. as // box itself as the currently-selected option (ie. as
// opposed to in the actual dropped-down part). If // opposed to in the actual dropped-down part). If
// unspecified, the appropriate child element is used as // unspecified, the appropriate child element is used as
// in the dropped-down menu. // in the dropped-down menu.
getShortOption: React.PropTypes.func, getShortOption: PropTypes.func,
value: React.PropTypes.string, value: PropTypes.string,
// negative for consistency with HTML // negative for consistency with HTML
disabled: React.PropTypes.bool, disabled: PropTypes.bool,
}; };

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types';
const KEY_TAB = 9; const KEY_TAB = 9;
const KEY_SHIFT = 16; const KEY_SHIFT = 16;
@ -26,18 +27,18 @@ module.exports = React.createClass({
displayName: 'EditableText', displayName: 'EditableText',
propTypes: { propTypes: {
onValueChanged: React.PropTypes.func, onValueChanged: PropTypes.func,
initialValue: React.PropTypes.string, initialValue: PropTypes.string,
label: React.PropTypes.string, label: PropTypes.string,
placeholder: React.PropTypes.string, placeholder: PropTypes.string,
className: React.PropTypes.string, className: PropTypes.string,
labelClassName: React.PropTypes.string, labelClassName: PropTypes.string,
placeholderClassName: React.PropTypes.string, placeholderClassName: PropTypes.string,
// Overrides blurToSubmit if true // Overrides blurToSubmit if true
blurToCancel: React.PropTypes.bool, blurToCancel: PropTypes.bool,
// Will cause onValueChanged(value, true) to fire on blur // Will cause onValueChanged(value, true) to fire on blur
blurToSubmit: React.PropTypes.bool, blurToSubmit: PropTypes.bool,
editable: React.PropTypes.bool, editable: PropTypes.bool,
}, },
Phases: { Phases: {

View File

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import Promise from 'bluebird'; import Promise from 'bluebird';
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
EditableTextContainer.propTypes = { EditableTextContainer.propTypes = {
/* callback to retrieve the initial value. */ /* callback to retrieve the initial value. */
getInitialValue: React.PropTypes.func, getInitialValue: PropTypes.func,
/* initial value; used if getInitialValue is not given */ /* 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 /* placeholder text to use when the value is empty (and not being
* edited) */ * edited) */
placeholder: React.PropTypes.string, placeholder: PropTypes.string,
/* callback to update the value. Called with a single argument: the new /* callback to update the value. Called with a single argument: the new
* value. */ * value. */
onSubmit: React.PropTypes.func, onSubmit: PropTypes.func,
/* should the input submit when focus is lost? */ /* should the input submit when focus is lost? */
blurToSubmit: React.PropTypes.bool, blurToSubmit: PropTypes.bool,
}; };

View File

@ -16,6 +16,7 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {emojifyText, containsEmoji} from '../../../HtmlUtils'; import {emojifyText, containsEmoji} from '../../../HtmlUtils';
export default function EmojiText(props) { export default function EmojiText(props) {
@ -32,8 +33,8 @@ export default function EmojiText(props) {
} }
EmojiText.propTypes = { EmojiText.propTypes = {
element: React.PropTypes.string, element: PropTypes.string,
children: React.PropTypes.string.isRequired, children: PropTypes.string.isRequired,
}; };
EmojiText.defaultProps = { EmojiText.defaultProps = {

View File

@ -63,7 +63,7 @@ FlairAvatar.propTypes = {
}; };
FlairAvatar.contextTypes = { FlairAvatar.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
}; };
export default class Flair extends React.Component { export default class Flair extends React.Component {
@ -107,7 +107,11 @@ export default class Flair extends React.Component {
} }
const profiles = await this._getGroupProfiles(groups); const profiles = await this._getGroupProfiles(groups);
if (!this.unmounted) { 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. // this.context.matrixClient everywhere instead of this.props.matrixClient.
// See https://github.com/vector-im/riot-web/issues/4951. // See https://github.com/vector-im/riot-web/issues/4951.
Flair.contextTypes = { Flair.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
}; };

View File

@ -16,6 +16,7 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
@ -114,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
} }
LanguageDropdown.propTypes = { LanguageDropdown.propTypes = {
className: React.PropTypes.string, className: PropTypes.string,
onOptionChange: React.PropTypes.func.isRequired, onOptionChange: PropTypes.func.isRequired,
value: React.PropTypes.string, value: PropTypes.string,
}; };

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index'; import sdk from '../../../index';
const MemberAvatar = require('../avatars/MemberAvatar.js'); const MemberAvatar = require('../avatars/MemberAvatar.js');
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -23,19 +24,19 @@ module.exports = React.createClass({
propTypes: { propTypes: {
// An array of member events to summarise // An array of member events to summarise
events: React.PropTypes.array.isRequired, events: PropTypes.array.isRequired,
// An array of EventTiles to render when expanded // 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" // 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 // 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 // The minimum number of events needed to trigger summarisation
threshold: React.PropTypes.number, threshold: PropTypes.number,
// Called when the MELS expansion is toggled // Called when the MELS expansion is toggled
onToggle: React.PropTypes.func, onToggle: PropTypes.func,
// Whether or not to begin with state.expanded=true // Whether or not to begin with state.expanded=true
startExpanded: React.PropTypes.bool, startExpanded: PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -17,7 +17,7 @@ import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import classNames from 'classnames'; 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 PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix'; import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
@ -61,6 +61,17 @@ const Pill = React.createClass({
shouldShowPillAvatar: PropTypes.bool, shouldShowPillAvatar: PropTypes.bool,
}, },
childContextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient),
},
getChildContext() {
return {
matrixClient: this._matrixClient,
};
},
getInitialState() { getInitialState() {
return { return {
// ID/alias of the room/user // ID/alias of the room/user
@ -135,6 +146,7 @@ const Pill = React.createClass({
componentWillMount() { componentWillMount() {
this._unmounted = false; this._unmounted = false;
this._matrixClient = MatrixClientPeg.get();
this.componentWillReceiveProps(this.props); this.componentWillReceiveProps(this.props);
}, },

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -24,23 +25,23 @@ module.exports = React.createClass({
displayName: 'PowerSelector', displayName: 'PowerSelector',
propTypes: { propTypes: {
value: React.PropTypes.number.isRequired, value: PropTypes.number.isRequired,
// The maximum value that can be set with the power selector // 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 // Default user power level for the room
usersDefault: React.PropTypes.number.isRequired, usersDefault: PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React // if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform. // to reflect the current value, rather than left freeform.
// MemberInfo uses controlled; RoomSettings uses non-controlled. // MemberInfo uses controlled; RoomSettings uses non-controlled.
// //
// ignored if disabled is truthy. false by default. // ignored if disabled is truthy. false by default.
controlled: React.PropTypes.bool, controlled: PropTypes.bool,
// should the user be able to change the value? false by default. // should the user be able to change the value? false by default.
disabled: React.PropTypes.bool, disabled: PropTypes.bool,
onChange: React.PropTypes.func, onChange: PropTypes.func,
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -17,12 +17,13 @@ limitations under the License.
'use strict'; 'use strict';
const React = require('react'); const React = require('react');
import PropTypes from 'prop-types';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ProgressBar', displayName: 'ProgressBar',
propTypes: { propTypes: {
value: React.PropTypes.number, value: PropTypes.number,
max: React.PropTypes.number, max: PropTypes.number,
}, },
render: function() { render: function() {

View File

@ -0,0 +1,188 @@
/*
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 React from 'react';
import sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk';
import {makeUserPermalink} from "../../../matrix-to";
// For URLs of matrix.to links in the timeline which have been reformatted by
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
const REGEX_LOCAL_MATRIXTO = /^#\/room\/([\#\!][^\/]*)\/(\$[^\/]*)$/;
export default class Quote extends React.Component {
static isMessageUrl(url) {
return !!REGEX_LOCAL_MATRIXTO.exec(url);
}
static childContextTypes = {
matrixClient: PropTypes.object,
addRichQuote: PropTypes.func,
};
static propTypes = {
// The matrix.to url of the event
url: PropTypes.string,
// The original node that was rendered
node: PropTypes.instanceOf(Element),
// The parent event
parentEv: PropTypes.instanceOf(MatrixEvent),
};
constructor(props, context) {
super(props, context);
this.state = {
// The event related to this quote and their nested rich quotes
events: [],
// Whether the top (oldest) event should be shown or spoilered
show: true,
// Whether an error was encountered fetching nested older event, show node if it does
err: false,
};
this.onQuoteClick = this.onQuoteClick.bind(this);
this.addRichQuote = this.addRichQuote.bind(this);
}
getChildContext() {
return {
matrixClient: MatrixClientPeg.get(),
addRichQuote: this.addRichQuote,
};
}
parseUrl(url) {
if (!url) return;
// Default to the empty array if no match for simplicity
// resource and prefix will be undefined instead of throwing
const matrixToMatch = REGEX_LOCAL_MATRIXTO.exec(url) || [];
const [, roomIdentifier, eventId] = matrixToMatch;
return {roomIdentifier, eventId};
}
componentWillReceiveProps(nextProps) {
const {roomIdentifier, eventId} = this.parseUrl(nextProps.url);
if (!roomIdentifier || !eventId) return;
const room = this.getRoom(roomIdentifier);
if (!room) return;
// Only try and load the event if we know about the room
// otherwise we just leave a `Quote` anchor which can be used to navigate/join the room manually.
this.setState({ events: [] });
if (room) this.getEvent(room, eventId, true);
}
componentWillMount() {
this.componentWillReceiveProps(this.props);
}
getRoom(id) {
const cli = MatrixClientPeg.get();
if (id[0] === '!') return cli.getRoom(id);
return cli.getRooms().find((r) => {
return r.getAliases().includes(id);
});
}
async getEvent(room, eventId, show) {
const event = room.findEventById(eventId);
if (event) {
this.addEvent(event, show);
return;
}
await MatrixClientPeg.get().getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
this.addEvent(room.findEventById(eventId), show);
}
addEvent(event, show) {
const events = [event].concat(this.state.events);
this.setState({events, show});
}
// addRichQuote(roomId, eventId) {
addRichQuote(href) {
const {roomIdentifier, eventId} = this.parseUrl(href);
if (!roomIdentifier || !eventId) {
this.setState({ err: true });
return;
}
const room = this.getRoom(roomIdentifier);
if (!room) {
this.setState({ err: true });
return;
}
this.getEvent(room, eventId, false);
}
onQuoteClick() {
this.setState({ show: true });
}
render() {
const events = this.state.events.slice();
if (events.length) {
const evTiles = [];
if (!this.state.show) {
const oldestEv = events.shift();
const Pill = sdk.getComponent('elements.Pill');
const room = MatrixClientPeg.get().getRoom(oldestEv.getRoomId());
evTiles.push(<blockquote className="mx_Quote" key="load">
{
_t('<a>In reply to</a> <pill>', {}, {
'a': (sub) => <a onClick={this.onQuoteClick} className="mx_Quote_show">{ sub }</a>,
'pill': <Pill type={Pill.TYPE_USER_MENTION} room={room}
url={makeUserPermalink(oldestEv.getSender())} shouldShowPillAvatar={true} />,
})
}
</blockquote>);
}
const EventTile = sdk.getComponent('views.rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
events.forEach((ev) => {
let dateSep = null;
if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
dateSep = <a href={this.props.url}><DateSeparator ts={ev.getTs()} /></a>;
}
evTiles.push(<blockquote className="mx_Quote" key={ev.getId()}>
{ dateSep }
<EventTile mxEvent={ev} tileShape="quote" />
</blockquote>);
});
return <div>{ evTiles }</div>;
}
// Deliberately render nothing if the URL isn't recognised
// in case we get an undefined/falsey node, replace it with null to make React happy
return this.props.node || null;
}
}

View File

@ -15,23 +15,24 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import PropTypes from 'prop-types';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'SettingsFlag', displayName: 'SettingsFlag',
propTypes: { propTypes: {
name: React.PropTypes.string.isRequired, name: PropTypes.string.isRequired,
level: React.PropTypes.string.isRequired, level: PropTypes.string.isRequired,
roomId: React.PropTypes.string, // for per-room settings roomId: PropTypes.string, // for per-room settings
label: React.PropTypes.string, // untranslated label: PropTypes.string, // untranslated
onChange: React.PropTypes.func, onChange: PropTypes.func,
isExplicit: React.PropTypes.bool, isExplicit: PropTypes.bool,
manualSave: React.PropTypes.bool, manualSave: PropTypes.bool,
// If group is supplied, then this will create a radio button instead. // If group is supplied, then this will create a radio button instead.
group: React.PropTypes.string, group: PropTypes.string,
value: React.PropTypes.any, // the value for the radio button value: PropTypes.any, // the value for the radio button
}, },
getInitialState: function() { getInitialState: function() {

View File

@ -0,0 +1,119 @@
/*
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 React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { MatrixClient } from 'matrix-js-sdk';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
import FlairStore from '../../../stores/FlairStore';
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
// - Rooms that are part of the group
// - Direct messages with members of the group
// with the intention that this could be expanded to arbitrary tags in future.
export default React.createClass({
displayName: 'TagTile',
propTypes: {
// A string tag such as "m.favourite" or a group ID such as "+groupid:domain.bla"
// For now, only group IDs are handled.
tag: PropTypes.string,
},
contextTypes: {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
},
getInitialState() {
return {
// Whether the mouse is over the tile
hover: false,
// The profile data of the group if this.props.tag is a group ID
profile: null,
};
},
componentWillMount() {
this.unmounted = false;
if (this.props.tag[0] === '+') {
FlairStore.getGroupProfileCached(
this.context.matrixClient,
this.props.tag,
).then((profile) => {
if (this.unmounted) return;
this.setState({profile});
}).catch((err) => {
console.warn('Could not fetch group profile for ' + this.props.tag, err);
});
}
},
componentWillUnmount() {
this.unmounted = true;
},
onClick: function(e) {
e.preventDefault();
e.stopPropagation();
dis.dispatch({
action: 'select_tag',
tag: this.props.tag,
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(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.state.profile || {};
const name = profile.name || this.props.tag;
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 ?
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
<div />;
return <AccessibleButton className={className} onClick={this.onClick}>
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
<BaseAvatar name={name} url={httpUrl} width={avatarHeight} height={avatarHeight} />
{ tip }
</div>
</AccessibleButton>;
},
});

View File

@ -18,16 +18,17 @@ limitations under the License.
const React = require('react'); const React = require('react');
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
const Tinter = require("../../../Tinter"); const Tinter = require("../../../Tinter");
var TintableSvg = React.createClass({ var TintableSvg = React.createClass({
displayName: 'TintableSvg', displayName: 'TintableSvg',
propTypes: { propTypes: {
src: React.PropTypes.string.isRequired, src: PropTypes.string.isRequired,
width: React.PropTypes.string.isRequired, width: PropTypes.string.isRequired,
height: React.PropTypes.string.isRequired, height: PropTypes.string.isRequired,
className: React.PropTypes.string, className: PropTypes.string,
}, },
statics: { statics: {

View File

@ -17,14 +17,15 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'UserSelector', displayName: 'UserSelector',
propTypes: { propTypes: {
onChange: React.PropTypes.func, onChange: PropTypes.func,
selected_users: React.PropTypes.arrayOf(React.PropTypes.string), selected_users: PropTypes.arrayOf(React.PropTypes.string),
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -66,7 +66,7 @@ const GroupRoomTile = React.createClass({
}); });
GroupRoomTile.contextTypes = { GroupRoomTile.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
}; };

View File

@ -33,7 +33,7 @@ const GroupTile = React.createClass({
}, },
contextTypes: { contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
}, },
getInitialState() { getInitialState() {

View File

@ -18,6 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
@ -29,10 +30,10 @@ module.exports = React.createClass({
displayName: 'CaptchaForm', displayName: 'CaptchaForm',
propTypes: { propTypes: {
sitePublicKey: React.PropTypes.string, sitePublicKey: PropTypes.string,
// called with the captcha response // called with the captcha response
onCaptchaResponse: React.PropTypes.func, onCaptchaResponse: PropTypes.func,
}, },
getDefaultProps: function() { getDefaultProps: function() {

View File

@ -17,13 +17,14 @@ limitations under the License.
'use strict'; 'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'CasLogin', displayName: 'CasLogin',
propTypes: { propTypes: {
onSubmit: React.PropTypes.func, // fn() onSubmit: PropTypes.func, // fn()
}, },
render: function() { render: function() {

Some files were not shown because too many files have changed in this diff Show More