mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-17 22:14:58 +08:00
Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into rxl881/snapshot
This commit is contained in:
commit
b2bf4d4709
@ -8,7 +8,6 @@ src/CallHandler.js
|
||||
src/component-index.js
|
||||
src/components/structures/ContextualMenu.js
|
||||
src/components/structures/CreateRoom.js
|
||||
src/components/structures/FilePanel.js
|
||||
src/components/structures/LoggedInView.js
|
||||
src/components/structures/login/ForgotPassword.js
|
||||
src/components/structures/login/Login.js
|
||||
@ -27,16 +26,10 @@ src/components/views/dialogs/ChatCreateOrReuseDialog.js
|
||||
src/components/views/dialogs/DeactivateAccountDialog.js
|
||||
src/components/views/dialogs/UnknownDeviceDialog.js
|
||||
src/components/views/elements/AddressSelector.js
|
||||
src/components/views/elements/CreateRoomButton.js
|
||||
src/components/views/elements/DeviceVerifyButtons.js
|
||||
src/components/views/elements/DirectorySearchBox.js
|
||||
src/components/views/elements/EditableText.js
|
||||
src/components/views/elements/HomeButton.js
|
||||
src/components/views/elements/MemberEventListSummary.js
|
||||
src/components/views/elements/PowerSelector.js
|
||||
src/components/views/elements/RoomDirectoryButton.js
|
||||
src/components/views/elements/SettingsButton.js
|
||||
src/components/views/elements/StartChatButton.js
|
||||
src/components/views/elements/TintableSvg.js
|
||||
src/components/views/elements/UserSelector.js
|
||||
src/components/views/login/CountryDropdown.js
|
||||
@ -93,7 +86,6 @@ src/RichText.js
|
||||
src/Roles.js
|
||||
src/Rooms.js
|
||||
src/ScalarAuthClient.js
|
||||
src/Tinter.js
|
||||
src/UiEffects.js
|
||||
src/Unread.js
|
||||
src/utils/DecryptFile.js
|
||||
|
@ -3,7 +3,10 @@ dist: trusty
|
||||
|
||||
# we don't need sudo, so can run in a container, which makes startup much
|
||||
# quicker.
|
||||
sudo: false
|
||||
#
|
||||
# unfortunately we do temporarily require sudo as a workaround for
|
||||
# https://github.com/travis-ci/travis-ci/issues/8836
|
||||
sudo: required
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
|
@ -1,3 +1,9 @@
|
||||
Changes in [0.11.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.4) (2018-02-09)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.3...v0.11.4)
|
||||
|
||||
* Add isUrlPermitted function to sanity check URLs
|
||||
|
||||
Changes in [0.11.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.3) (2017-12-04)
|
||||
=====================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.2...v0.11.3)
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.4",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
@ -56,7 +56,7 @@
|
||||
"browser-encrypt-attachment": "^0.3.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"classnames": "^2.1.2",
|
||||
"commonmark": "^0.27.0",
|
||||
"commonmark": "^0.28.1",
|
||||
"counterpart": "^0.18.0",
|
||||
"draft-js": "^0.11.0-alpha",
|
||||
"draft-js-export-html": "^0.6.0",
|
||||
@ -77,8 +77,7 @@
|
||||
"querystring": "^0.2.0",
|
||||
"react": "^15.4.0",
|
||||
"react-addons-css-transition-group": "15.3.2",
|
||||
"react-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-beautiful-dnd": "^4.0.0",
|
||||
"react-dom": "^15.4.0",
|
||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||
"sanitize-html": "^1.14.1",
|
||||
|
101
src/Analytics.js
101
src/Analytics.js
@ -14,25 +14,54 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getCurrentLanguage } from './languageHandler';
|
||||
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import SdkConfig, { DEFAULTS } from './SdkConfig';
|
||||
import Modal from './Modal';
|
||||
import sdk from './index';
|
||||
|
||||
function getRedactedHash() {
|
||||
return window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
|
||||
}
|
||||
|
||||
function getRedactedUrl() {
|
||||
const redactedHash = window.location.hash.replace(/#\/(group|room|user)\/(.+)/, "#/$1/<redacted>");
|
||||
// hardcoded url to make piwik happy
|
||||
return 'https://riot.im/app/' + redactedHash;
|
||||
return 'https://riot.im/app/' + getRedactedHash();
|
||||
}
|
||||
|
||||
const customVariables = {
|
||||
'App Platform': 1,
|
||||
'App Version': 2,
|
||||
'User Type': 3,
|
||||
'Chosen Language': 4,
|
||||
'Instance': 5,
|
||||
'RTE: Uses Richtext Mode': 6,
|
||||
'Homeserver URL': 7,
|
||||
'Identity Server URL': 8,
|
||||
'App Platform': {
|
||||
id: 1,
|
||||
expl: _td('The platform you\'re on'),
|
||||
},
|
||||
'App Version': {
|
||||
id: 2,
|
||||
expl: _td('The version of Riot.im'),
|
||||
},
|
||||
'User Type': {
|
||||
id: 3,
|
||||
expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'),
|
||||
},
|
||||
'Chosen Language': {
|
||||
id: 4,
|
||||
expl: _td('Your language of choice'),
|
||||
},
|
||||
'Instance': {
|
||||
id: 5,
|
||||
expl: _td('Which officially provided instance you are using, if any'),
|
||||
},
|
||||
'RTE: Uses Richtext Mode': {
|
||||
id: 6,
|
||||
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
||||
},
|
||||
'Homeserver URL': {
|
||||
id: 7,
|
||||
expl: _td('Your homeserver\'s URL'),
|
||||
},
|
||||
'Identity Server URL': {
|
||||
id: 8,
|
||||
expl: _td('Your identity server\'s URL'),
|
||||
},
|
||||
};
|
||||
|
||||
function whitelistRedact(whitelist, str) {
|
||||
@ -40,9 +69,6 @@ function whitelistRedact(whitelist, str) {
|
||||
return '<redacted>';
|
||||
}
|
||||
|
||||
const whitelistedHSUrls = ["https://matrix.org"];
|
||||
const whitelistedISUrls = ["https://vector.im"];
|
||||
|
||||
class Analytics {
|
||||
constructor() {
|
||||
this._paq = null;
|
||||
@ -140,11 +166,16 @@ class Analytics {
|
||||
}
|
||||
|
||||
_setVisitVariable(key, value) {
|
||||
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
|
||||
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
|
||||
}
|
||||
|
||||
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const config = SdkConfig.get();
|
||||
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || DEFAULTS.piwik.whitelistedHSUrls;
|
||||
const whitelistedISUrls = config.piwik.whitelistedISUrls || DEFAULTS.piwik.whitelistedISUrls;
|
||||
|
||||
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
|
||||
@ -154,6 +185,44 @@ class Analytics {
|
||||
if (this.disabled) return;
|
||||
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
|
||||
}
|
||||
|
||||
showDetailsModal() {
|
||||
const Tracker = window.Piwik.getAsyncTracker();
|
||||
const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
||||
|
||||
const resolution = `${window.screen.width}x${window.screen.height}`;
|
||||
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
|
||||
title: _t('Analytics'),
|
||||
description: <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) {
|
||||
|
@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
function getDaysArray() {
|
||||
@ -51,26 +50,29 @@ function pad(n) {
|
||||
return (n < 10 ? '0' : '') + n;
|
||||
}
|
||||
|
||||
function twelveHourTime(date) {
|
||||
function twelveHourTime(date, showSeconds=false) {
|
||||
let hours = date.getHours() % 12;
|
||||
const minutes = pad(date.getMinutes());
|
||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||
hours = hours ? hours : 12; // convert 0 -> 12
|
||||
if (showSeconds) {
|
||||
const seconds = pad(date.getSeconds());
|
||||
return `${hours}:${minutes}:${seconds}${ampm}`;
|
||||
}
|
||||
return `${hours}:${minutes}${ampm}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatDate: function(date, showTwelveHour=false) {
|
||||
export function formatDate(date, showTwelveHour=false) {
|
||||
const now = new Date();
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
if (date.toDateString() === now.toDateString()) {
|
||||
return this.formatTime(date, showTwelveHour);
|
||||
return formatTime(date, showTwelveHour);
|
||||
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
|
||||
// TODO: use standard date localize function provided in counterpart
|
||||
return _t('%(weekDayName)s %(time)s', {
|
||||
weekDayName: days[date.getDay()],
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
time: formatTime(date, showTwelveHour),
|
||||
});
|
||||
} else if (now.getFullYear() === date.getFullYear()) {
|
||||
// TODO: use standard date localize function provided in counterpart
|
||||
@ -78,13 +80,24 @@ module.exports = {
|
||||
weekDayName: days[date.getDay()],
|
||||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
time: formatTime(date, showTwelveHour),
|
||||
});
|
||||
}
|
||||
return this.formatFullDate(date, showTwelveHour);
|
||||
},
|
||||
return formatFullDate(date, showTwelveHour);
|
||||
}
|
||||
|
||||
formatFullDate: function(date, showTwelveHour=false) {
|
||||
export function formatFullDateNoTime(date) {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
|
||||
weekDayName: days[date.getDay()],
|
||||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
fullYear: date.getFullYear(),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatFullDate(date, showTwelveHour=false) {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
|
||||
@ -92,14 +105,34 @@ module.exports = {
|
||||
monthName: months[date.getMonth()],
|
||||
day: date.getDate(),
|
||||
fullYear: date.getFullYear(),
|
||||
time: this.formatTime(date, showTwelveHour),
|
||||
time: formatFullTime(date, showTwelveHour),
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
formatTime: function(date, showTwelveHour=false) {
|
||||
export function formatFullTime(date, showTwelveHour=false) {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date, true);
|
||||
}
|
||||
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 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.
|
||||
@ -25,6 +25,7 @@ import escape from 'lodash/escape';
|
||||
import emojione from 'emojione';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import url from 'url';
|
||||
|
||||
emojione.imagePathSVG = 'emojione/svg/';
|
||||
// Store PNG path for displaying many flags at once (for increased performance over SVG)
|
||||
@ -44,6 +45,8 @@ const SYMBOL_PATTERN = /([\u2100-\u2bff])/;
|
||||
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi");
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
* Uses a much, much simpler regex than emojione's so will give false
|
||||
@ -152,6 +155,25 @@ export function sanitizedHtmlNode(insaneHtml) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a URL from an untrusted source may be safely put into the DOM
|
||||
* The biggest threat here is javascript: URIs.
|
||||
* Note that the HTML sanitiser library has its own internal logic for
|
||||
* doing this, to which we pass the same list of schemes. This is used in
|
||||
* other places we need to sanitise URLs.
|
||||
* @return true if permitted, otherwise false
|
||||
*/
|
||||
export function isUrlPermitted(inputUrl) {
|
||||
try {
|
||||
const parsed = url.parse(inputUrl);
|
||||
if (!parsed.protocol) return false;
|
||||
// URL parser protocol includes the trailing colon
|
||||
return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizeHtmlParams = {
|
||||
allowedTags: [
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
@ -172,7 +194,7 @@ const sanitizeHtmlParams = {
|
||||
// Lots of these won't come up by default because we don't allow them
|
||||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||
// URL schemes we permit
|
||||
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
|
||||
allowedSchemes: PERMITTED_URL_SCHEMES,
|
||||
|
||||
allowProtocolRelative: false,
|
||||
|
||||
|
@ -68,3 +68,12 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) {
|
||||
return ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
|
||||
}
|
||||
}
|
||||
|
||||
export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
if (isMac) {
|
||||
return ev.metaKey && !ev.altKey && !ev.ctrlKey;
|
||||
} else {
|
||||
return ev.ctrlKey && !ev.altKey && !ev.metaKey;
|
||||
}
|
||||
}
|
||||
|
@ -362,7 +362,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
dis.dispatch({action: 'on_logged_in', teamToken: teamToken});
|
||||
});
|
||||
|
||||
startMatrixClient();
|
||||
await startMatrixClient();
|
||||
return MatrixClientPeg.get();
|
||||
}
|
||||
|
||||
@ -423,7 +423,7 @@ export function logout() {
|
||||
* Starts the matrix client and all other react-sdk services that
|
||||
* listen for events while a session is logged in.
|
||||
*/
|
||||
function startMatrixClient() {
|
||||
async function startMatrixClient() {
|
||||
console.log(`Lifecycle: Starting MatrixClient`);
|
||||
|
||||
// dispatch this before starting the matrix client: it's used
|
||||
@ -437,7 +437,7 @@ function startMatrixClient() {
|
||||
Presence.start();
|
||||
DMRoomMap.makeShared().start();
|
||||
|
||||
MatrixClientPeg.start();
|
||||
await MatrixClientPeg.start();
|
||||
|
||||
// dispatch that we finished starting up to wire up any other bits
|
||||
// of the matrix client that cannot be set prior to starting up.
|
||||
|
@ -55,25 +55,6 @@ function is_multi_line(node) {
|
||||
return par.firstChild != par.lastChild;
|
||||
}
|
||||
|
||||
import linkifyMatrix from './linkify-matrix';
|
||||
import * as linkify from 'linkifyjs';
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
// Thieved from draft-js-export-markdown
|
||||
function escapeMarkdown(s) {
|
||||
return s.replace(/[*_`]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Replace URLs, room aliases and user IDs with md-escaped URLs
|
||||
function linkifyMarkdown(s) {
|
||||
const links = linkify.find(s);
|
||||
links.forEach((l) => {
|
||||
// This may replace several instances of `l.value` at once, but that's OK
|
||||
s = s.replace(l.value, escapeMarkdown(l.value));
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that wraps commonmark, adding the ability to see whether
|
||||
* a given message actually uses any markdown syntax or whether
|
||||
@ -81,7 +62,7 @@ function linkifyMarkdown(s) {
|
||||
*/
|
||||
export default class Markdown {
|
||||
constructor(input) {
|
||||
this.input = linkifyMarkdown(input);
|
||||
this.input = input;
|
||||
|
||||
const parser = new commonmark.Parser();
|
||||
this.parsed = parser.parse(this.input);
|
||||
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import PropTypes from 'prop-types';
|
||||
import Analytics from './Analytics';
|
||||
import sdk from './index';
|
||||
|
||||
@ -33,7 +34,7 @@ const AsyncWrapper = React.createClass({
|
||||
/** A function which takes a 'callback' argument which it will call
|
||||
* with the real component once it loads.
|
||||
*/
|
||||
loader: React.PropTypes.func.isRequired,
|
||||
loader: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -135,6 +135,10 @@ const Notifier = {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (!plaf) return;
|
||||
|
||||
// Dev note: We don't set the "notificationsEnabled" setting to true here because it is a
|
||||
// calculated value. It is determined based upon whether or not the master rule is enabled
|
||||
// and other flags. Setting it here would cause a circular reference.
|
||||
|
||||
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
|
||||
|
||||
// make sure that we persist the current setting audio_enabled setting
|
||||
@ -168,7 +172,7 @@ const Notifier = {
|
||||
});
|
||||
// clear the notifications_hidden flag, so that if notifications are
|
||||
// disabled again in the future, we will show the banner again.
|
||||
this.setToolbarHidden(false);
|
||||
this.setToolbarHidden(true);
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
|
@ -85,9 +85,7 @@ function _onStartChatFinished(shouldInvite, addrs) {
|
||||
if (rooms.length > 0) {
|
||||
// A Direct Message room already exists for this user, so select a
|
||||
// room from a list that is similar to the one in MemberInfo panel
|
||||
const ChatCreateOrReuseDialog = sdk.getComponent(
|
||||
"views.dialogs.ChatCreateOrReuseDialog",
|
||||
);
|
||||
const ChatCreateOrReuseDialog = sdk.getComponent("views.dialogs.ChatCreateOrReuseDialog");
|
||||
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
|
||||
userId: addrTexts[0],
|
||||
onNewDMClick: () => {
|
||||
@ -115,6 +113,15 @@ function _onStartChatFinished(shouldInvite, addrs) {
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if (addrTexts.length === 1) {
|
||||
// Start a new DM chat
|
||||
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
|
||||
title: _t("Failed to invite user"),
|
||||
description: ((err && err.message) ? err.message : _t("Operation failed")),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Start multi user chat
|
||||
let room;
|
||||
|
@ -34,7 +34,14 @@ export function getRoomNotifsState(roomId) {
|
||||
}
|
||||
|
||||
// for everything else, look at the room rule.
|
||||
const roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId);
|
||||
let roomRule = null;
|
||||
try {
|
||||
roomRule = MatrixClientPeg.get().getRoomPushRule('global', roomId);
|
||||
} catch (err) {
|
||||
// Possible that the client doesn't have pushRules yet. If so, it
|
||||
// hasn't started eiher, so indicate that this room is not notifying.
|
||||
return null;
|
||||
}
|
||||
|
||||
// XXX: We have to assume the default is to notify for all messages
|
||||
// (in particular this will be 'wrong' for one to one rooms because
|
||||
@ -130,6 +137,11 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
|
||||
}
|
||||
|
||||
function findOverrideMuteRule(roomId) {
|
||||
if (!MatrixClientPeg.get().pushRules ||
|
||||
!MatrixClientPeg.get().pushRules['global'] ||
|
||||
!MatrixClientPeg.get().pushRules['global'].override) {
|
||||
return null;
|
||||
}
|
||||
for (const rule of MatrixClientPeg.get().pushRules['global'].override) {
|
||||
if (isRuleForRoom(roomId, rule)) {
|
||||
if (isMuteRule(rule) && rule.enabled) {
|
||||
|
22
src/Rooms.js
22
src/Rooms.js
@ -43,7 +43,7 @@ export function getOnlyOtherMember(room, me) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isConfCallRoom(room, me, conferenceHandler) {
|
||||
function _isConfCallRoom(room, me, conferenceHandler) {
|
||||
if (!conferenceHandler) return false;
|
||||
|
||||
if (me.membership != "join") {
|
||||
@ -58,6 +58,26 @@ export function isConfCallRoom(room, me, conferenceHandler) {
|
||||
if (conferenceHandler.isConferenceUser(otherMember.userId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cache whether a room is a conference call. Assumes that rooms will always
|
||||
// either will or will not be a conference call room.
|
||||
const isConfCallRoomCache = {
|
||||
// $roomId: bool
|
||||
};
|
||||
|
||||
export function isConfCallRoom(room, me, conferenceHandler) {
|
||||
if (isConfCallRoomCache[room.roomId] !== undefined) {
|
||||
return isConfCallRoomCache[room.roomId];
|
||||
}
|
||||
|
||||
const result = _isConfCallRoom(room, me, conferenceHandler);
|
||||
|
||||
isConfCallRoomCache[room.roomId] = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function looksLikeDirectMessageRoom(room, me) {
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
const request = require('browser-request');
|
||||
|
||||
const SdkConfig = require('./SdkConfig');
|
||||
@ -38,11 +39,53 @@ class ScalarAuthClient {
|
||||
|
||||
// Returns a scalar_token string
|
||||
getScalarToken() {
|
||||
const tok = window.localStorage.getItem("mx_scalar_token");
|
||||
if (tok) return Promise.resolve(tok);
|
||||
const token = window.localStorage.getItem("mx_scalar_token");
|
||||
|
||||
// No saved token, so do the dance to get one. First, we
|
||||
// need an openid bearer token from the HS.
|
||||
if (!token) {
|
||||
return this.registerForToken();
|
||||
} else {
|
||||
return this.validateToken(token).then(userId => {
|
||||
const me = MatrixClientPeg.get().getUserId();
|
||||
if (userId !== me) {
|
||||
throw new Error("Scalar token is owned by someone else: " + me);
|
||||
}
|
||||
return token;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
|
||||
// Something went wrong - try to get a new token.
|
||||
console.warn("Registering for new scalar token");
|
||||
return this.registerForToken();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
validateToken(token) {
|
||||
let url = SdkConfig.get().integrations_rest_url + "/account";
|
||||
|
||||
const defer = Promise.defer();
|
||||
request({
|
||||
method: "GET",
|
||||
uri: url,
|
||||
qs: {scalar_token: token},
|
||||
json: true,
|
||||
}, (err, response, body) => {
|
||||
if (err) {
|
||||
defer.reject(err);
|
||||
} else if (response.statusCode / 100 !== 2) {
|
||||
defer.reject({statusCode: response.statusCode});
|
||||
} else if (!body || !body.user_id) {
|
||||
defer.reject(new Error("Missing user_id in response"));
|
||||
} else {
|
||||
defer.resolve(body.user_id);
|
||||
}
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
registerForToken() {
|
||||
// Get openid bearer token from the HS as the first part of our dance
|
||||
return MatrixClientPeg.get().getOpenIdToken().then((token_object) => {
|
||||
// Now we can send that to scalar and exchange it for a scalar token
|
||||
return this.exchangeForScalarToken(token_object);
|
||||
@ -112,6 +155,7 @@ class ScalarAuthClient {
|
||||
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
|
||||
url += "&room_id=" + encodeURIComponent(roomId);
|
||||
url += "&room_name=" + encodeURIComponent(roomName);
|
||||
url += "&theme=" + encodeURIComponent(SettingsStore.getValue("theme"));
|
||||
if (id) {
|
||||
url += '&integ_id=' + encodeURIComponent(id);
|
||||
}
|
||||
|
@ -602,8 +602,16 @@ const onMessage = function(event) {
|
||||
//
|
||||
// All strings start with the empty string, so for sanity return if the length
|
||||
// of the event origin is 0.
|
||||
//
|
||||
// TODO -- Scalar postMessage API should be namespaced with event.data.api field
|
||||
// Fix following "if" statement to respond only to specific API messages.
|
||||
const url = SdkConfig.get().integrations_ui_url;
|
||||
if (event.origin.length === 0 || !url.startsWith(event.origin) || !event.data.action) {
|
||||
if (
|
||||
event.origin.length === 0 ||
|
||||
!url.startsWith(event.origin + '/') ||
|
||||
!event.data.action ||
|
||||
event.data.api // Ignore messages with specific API set
|
||||
) {
|
||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,13 @@ const DEFAULTS = {
|
||||
integrations_rest_url: "https://scalar.vector.im/api",
|
||||
// Where to send bug reports. If not specified, bugs cannot be sent.
|
||||
bug_report_endpoint_url: null,
|
||||
|
||||
piwik: {
|
||||
url: "https://piwik.riot.im/",
|
||||
whitelistedHSUrls: ["https://matrix.org"],
|
||||
whitelistedISUrls: ["https://vector.im", "https://matrix.org"],
|
||||
siteId: 1,
|
||||
},
|
||||
};
|
||||
|
||||
class SdkConfig {
|
||||
@ -45,3 +52,4 @@ class SdkConfig {
|
||||
}
|
||||
|
||||
module.exports = SdkConfig;
|
||||
module.exports.DEFAULTS = DEFAULTS;
|
||||
|
@ -96,6 +96,8 @@ const commands = {
|
||||
colorScheme.primary_color = matches[1];
|
||||
if (matches[4]) {
|
||||
colorScheme.secondary_color = matches[4];
|
||||
} else {
|
||||
colorScheme.secondary_color = colorScheme.primary_color;
|
||||
}
|
||||
return success(
|
||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||
@ -295,7 +297,7 @@ const commands = {
|
||||
// Define the power level of a user
|
||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(\d+))?$/);
|
||||
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||
let powerLevel = 50; // default power level for op
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
|
@ -52,8 +52,7 @@ function textForMemberEvent(ev) {
|
||||
case 'join':
|
||||
if (prevContent && prevContent.membership === 'join') {
|
||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
|
||||
senderName,
|
||||
return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
||||
oldDisplayName: prevContent.displayname,
|
||||
displayName: content.displayname,
|
||||
});
|
||||
|
@ -28,6 +28,8 @@ module.exports = {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.member') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.room.third_party_invite') {
|
||||
return false;
|
||||
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
||||
return false;
|
||||
} else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
||||
|
@ -1,5 +1,6 @@
|
||||
const React = require('react');
|
||||
const ReactDom = require('react-dom');
|
||||
import PropTypes from 'prop-types';
|
||||
const Velocity = require('velocity-vector');
|
||||
|
||||
/**
|
||||
@ -14,16 +15,16 @@ module.exports = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
// either a list of child nodes, or a single child.
|
||||
children: React.PropTypes.any,
|
||||
children: PropTypes.any,
|
||||
|
||||
// optional transition information for changing existing children
|
||||
transition: React.PropTypes.object,
|
||||
transition: PropTypes.object,
|
||||
|
||||
// a list of state objects to apply to each child node in turn
|
||||
startStyles: React.PropTypes.array,
|
||||
startStyles: PropTypes.array,
|
||||
|
||||
// a list of transition options from the corresponding startStyle
|
||||
enterTransitionOpts: React.PropTypes.array,
|
||||
enterTransitionOpts: PropTypes.array,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -62,6 +62,127 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.Room'.
|
||||
* @property {Room} room the Room that was stored.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Room action that represents a MatrixClient `Room`
|
||||
* matrix event, emitted when a Room is stored in the client.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {Room} room the Room that was stored.
|
||||
* @returns {RoomAction} an action of type `MatrixActions.Room`.
|
||||
*/
|
||||
function createRoomAction(matrixClient, room) {
|
||||
return { action: 'MatrixActions.Room', room };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomTagsAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.Room.tags'.
|
||||
* @property {Room} room the Room whose tags changed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Room.tags action that represents a MatrixClient
|
||||
* `Room.tags` matrix event, emitted when the m.tag room account data
|
||||
* event is updated.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {MatrixEvent} roomTagsEvent the m.tag event.
|
||||
* @param {Room} room the Room whose tags were changed.
|
||||
* @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`.
|
||||
*/
|
||||
function createRoomTagsAction(matrixClient, roomTagsEvent, room) {
|
||||
return { action: 'MatrixActions.Room.tags', room };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomTimelineAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.Room.timeline'.
|
||||
* @property {boolean} isLiveEvent whether the event was attached to a
|
||||
* live timeline.
|
||||
* @property {boolean} isLiveUnfilteredRoomTimelineEvent whether the
|
||||
* event was attached to a timeline in the set of unfiltered timelines.
|
||||
* @property {Room} room the Room whose tags changed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Room.timeline action that represents a
|
||||
* MatrixClient `Room.timeline` matrix event, emitted when an event
|
||||
* is added to or removed from a timeline of a room.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {MatrixEvent} timelineEvent the event that was added/removed.
|
||||
* @param {Room} room the Room that was stored.
|
||||
* @param {boolean} toStartOfTimeline whether the event is being added
|
||||
* to the start (and not the end) of the timeline.
|
||||
* @param {boolean} removed whether the event was removed from the
|
||||
* timeline.
|
||||
* @param {Object} data
|
||||
* @param {boolean} data.liveEvent whether the event is a live event,
|
||||
* belonging to a live timeline.
|
||||
* @param {EventTimeline} data.timeline the timeline being altered.
|
||||
* @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`.
|
||||
*/
|
||||
function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) {
|
||||
return {
|
||||
action: 'MatrixActions.Room.timeline',
|
||||
event: timelineEvent,
|
||||
isLiveEvent: data.liveEvent,
|
||||
isLiveUnfilteredRoomTimelineEvent:
|
||||
room && data.timeline.getTimelineSet() === room.getUnfilteredTimelineSet(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RoomMembershipAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.RoomMember.membership'.
|
||||
* @property {RoomMember} member the member whose membership was updated.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.RoomMember.membership action that represents
|
||||
* a MatrixClient `RoomMember.membership` matrix event, emitted when a
|
||||
* member's membership is updated.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {MatrixEvent} membershipEvent the m.room.member event.
|
||||
* @param {RoomMember} member the member whose membership was updated.
|
||||
* @param {string} oldMembership the member's previous membership.
|
||||
* @returns {RoomMembershipAction} an action of type `MatrixActions.RoomMember.membership`.
|
||||
*/
|
||||
function createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) {
|
||||
return { action: 'MatrixActions.RoomMember.membership', member };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef EventDecryptedAction
|
||||
* @type {Object}
|
||||
* @property {string} action 'MatrixActions.Event.decrypted'.
|
||||
* @property {MatrixEvent} event the matrix event that was decrypted.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a MatrixActions.Event.decrypted action that represents
|
||||
* a MatrixClient `Event.decrypted` matrix event, emitted when a
|
||||
* matrix event is decrypted.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client.
|
||||
* @param {MatrixEvent} event the matrix event that was decrypted.
|
||||
* @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`.
|
||||
*/
|
||||
function createEventDecryptedAction(matrixClient, event) {
|
||||
return { action: 'MatrixActions.Event.decrypted', event };
|
||||
}
|
||||
|
||||
/**
|
||||
* This object is responsible for dispatching actions when certain events are emitted by
|
||||
* the given MatrixClient.
|
||||
@ -78,6 +199,11 @@ export default {
|
||||
start(matrixClient) {
|
||||
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
|
||||
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);
|
||||
this._addMatrixClientListener(matrixClient, 'RoomMember.membership', createRoomMembershipAction);
|
||||
this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -91,7 +217,7 @@ export default {
|
||||
*/
|
||||
_addMatrixClientListener(matrixClient, eventName, actionCreator) {
|
||||
const listener = (...args) => {
|
||||
dis.dispatch(actionCreator(matrixClient, ...args));
|
||||
dis.dispatch(actionCreator(matrixClient, ...args), true);
|
||||
};
|
||||
matrixClient.on(eventName, listener);
|
||||
this._matrixClientListenersStop.push(() => {
|
||||
|
146
src/actions/RoomListActions.js
Normal file
146
src/actions/RoomListActions.js
Normal file
@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2018 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';
|
||||
import RoomListStore from '../stores/RoomListStore';
|
||||
|
||||
import Modal from '../Modal';
|
||||
import Rooms from '../Rooms';
|
||||
import { _t } from '../languageHandler';
|
||||
import sdk from '../index';
|
||||
|
||||
const RoomListActions = {};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* tag room.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {Room} room the room to tag.
|
||||
* @param {string} oldTag the tag to remove (unless oldTag ==== newTag)
|
||||
* @param {string} newTag the tag with which to tag the room.
|
||||
* @param {?number} oldIndex the previous position of the room in the
|
||||
* list of rooms.
|
||||
* @param {?number} newIndex the new position of the room in the list
|
||||
* of rooms.
|
||||
* @returns {function} an action thunk.
|
||||
* @see asyncAction
|
||||
*/
|
||||
RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) {
|
||||
let metaData = null;
|
||||
|
||||
// Is the tag ordered manually?
|
||||
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) {
|
||||
const lists = RoomListStore.getRoomLists();
|
||||
const newList = [...lists[newTag]];
|
||||
|
||||
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
|
||||
|
||||
// If the room was moved "down" (increasing index) in the same list we
|
||||
// need to use the orders of the tiles with indices shifted by +1
|
||||
const offset = (
|
||||
newTag === oldTag && oldIndex < newIndex
|
||||
) ? 1 : 0;
|
||||
|
||||
const indexBefore = offset + newIndex - 1;
|
||||
const indexAfter = offset + newIndex;
|
||||
|
||||
const prevOrder = indexBefore <= 0 ?
|
||||
0 : newList[indexBefore].tags[newTag].order;
|
||||
const nextOrder = indexAfter >= newList.length ?
|
||||
1 : newList[indexAfter].tags[newTag].order;
|
||||
|
||||
metaData = {
|
||||
order: (prevOrder + nextOrder) / 2.0,
|
||||
};
|
||||
}
|
||||
|
||||
return asyncAction('RoomListActions.tagRoom', () => {
|
||||
const promises = [];
|
||||
const roomId = room.roomId;
|
||||
|
||||
// Evil hack to get DMs behaving
|
||||
if ((oldTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||
(oldTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||
) {
|
||||
return Rooms.guessAndSetDMRoom(
|
||||
room, newTag === 'im.vector.fake.direct',
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, {
|
||||
title: _t('Failed to set direct chat tag'),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const hasChangedSubLists = oldTag !== newTag;
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||
//
|
||||
// if we moved lists, remove the old tag
|
||||
if (oldTag && oldTag !== 'im.vector.fake.direct' &&
|
||||
hasChangedSubLists
|
||||
) {
|
||||
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||
roomId, oldTag,
|
||||
).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to remove tag " + oldTag + " from room: " + err);
|
||||
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
|
||||
title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
});
|
||||
|
||||
promises.push(promiseToDelete);
|
||||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||
(hasChangedSubLists || metaData)
|
||||
) {
|
||||
// metaData is the body of the PUT to set the tag, so it must
|
||||
// at least be an empty object.
|
||||
metaData = metaData || {};
|
||||
|
||||
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to add tag " + newTag + " to room: " + err);
|
||||
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {
|
||||
title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}),
|
||||
description: ((err && err.message) ? err.message : _t('Operation failed')),
|
||||
});
|
||||
|
||||
throw err;
|
||||
});
|
||||
|
||||
promises.push(promiseToAdd);
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {
|
||||
room, oldTag, newTag, metaData,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default RoomListActions;
|
@ -22,25 +22,87 @@ const TagOrderActions = {};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* commit TagOrderStore.getOrderedTags() to account data and dispatch
|
||||
* actions to indicate the status of the request.
|
||||
* 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.commitTagOrdering = function(matrixClient) {
|
||||
return asyncAction('TagOrderActions.commitTagOrdering', () => {
|
||||
TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) {
|
||||
// Only commit tags if the state is ready, i.e. not null
|
||||
const tags = TagOrderStore.getOrderedTags();
|
||||
let tags = TagOrderStore.getOrderedTags();
|
||||
let removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
|
||||
tags = tags.filter((t) => t !== tag);
|
||||
tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)];
|
||||
|
||||
removedTags = removedTags.filter((t) => t !== tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.moveTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||
return matrixClient.setAccountData('im.vector.web.tag_ordering', {tags});
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {tags, removedTags};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* label a tag as removed in im.vector.web.tag_ordering account data.
|
||||
*
|
||||
* The reason this is implemented with new state `removedTags` is that
|
||||
* we incrementally and initially populate `tags` with groups that
|
||||
* have been joined. If we remove a group from `tags`, it will just
|
||||
* get added (as it looks like a group we've recently joined).
|
||||
*
|
||||
* NB: If we ever support adding of tags (which is planned), we should
|
||||
* take special care to remove the tag from `removedTags` when we add
|
||||
* it.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
* @param {string} tag the tag to remove.
|
||||
* @returns {function} an action thunk that will dispatch actions
|
||||
* indicating the status of the request.
|
||||
* @see asyncAction
|
||||
*/
|
||||
TagOrderActions.removeTag = function(matrixClient, tag) {
|
||||
// Don't change tags, just removedTags
|
||||
const tags = TagOrderStore.getOrderedTags();
|
||||
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
|
||||
if (removedTags.includes(tag)) {
|
||||
// Return a thunk that doesn't do anything, we don't even need
|
||||
// an asynchronous action here, the tag is already removed.
|
||||
return () => {};
|
||||
}
|
||||
|
||||
removedTags.push(tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.removeTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'removeTag');
|
||||
return matrixClient.setAccountData(
|
||||
'im.vector.web.tag_ordering',
|
||||
{tags, removedTags, _storeId: storeId},
|
||||
);
|
||||
}, () => {
|
||||
// For an optimistic update
|
||||
return {removedTags};
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -22,16 +22,32 @@ limitations under the License.
|
||||
* 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`.
|
||||
*
|
||||
* The shape of each are:
|
||||
* { action: '${id}.pending', request }
|
||||
* { action: '${id}.success', result }
|
||||
* { action: '${id}.failure', err }
|
||||
*
|
||||
* where `request` is returned by `pendingFn` and
|
||||
* result is the result of the promise returned by
|
||||
* `fn`.
|
||||
*/
|
||||
export function asyncAction(id, fn) {
|
||||
export function asyncAction(id, fn, pendingFn) {
|
||||
return (dispatch) => {
|
||||
dispatch({action: id + '.pending'});
|
||||
dispatch({
|
||||
action: id + '.pending',
|
||||
request:
|
||||
typeof pendingFn === 'function' ? pendingFn() : undefined,
|
||||
});
|
||||
fn().then((result) => {
|
||||
dispatch({action: id + '.success', result});
|
||||
}).catch((err) => {
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require("react");
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
@ -23,8 +24,8 @@ module.exports = React.createClass({
|
||||
displayName: 'EncryptedEventDialog',
|
||||
|
||||
propTypes: {
|
||||
event: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
event: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
@ -29,8 +30,8 @@ export default React.createClass({
|
||||
displayName: 'ExportE2eKeysDialog',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import * as MegolmExportEncryption from '../../../utils/MegolmExportEncryption';
|
||||
@ -40,8 +41,8 @@ export default React.createClass({
|
||||
displayName: 'ImportE2eKeysDialog',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/* These were earlier stateless functional components but had to be converted
|
||||
@ -42,10 +43,10 @@ export class TextualCompletion extends React.Component {
|
||||
}
|
||||
}
|
||||
TextualCompletion.propTypes = {
|
||||
title: React.PropTypes.string,
|
||||
subtitle: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export class PillCompletion extends React.Component {
|
||||
@ -69,9 +70,9 @@ export class PillCompletion extends React.Component {
|
||||
}
|
||||
}
|
||||
PillCompletion.propTypes = {
|
||||
title: React.PropTypes.string,
|
||||
subtitle: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
initialComponent: React.PropTypes.element,
|
||||
className: React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
initialComponent: PropTypes.element,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
@ -25,6 +25,7 @@ import {PillCompletion} from './Components';
|
||||
import {getDisplayAliasForRoom} from '../Rooms';
|
||||
import sdk from '../index';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {makeRoomPermalink} from "../matrix-to";
|
||||
|
||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||
|
||||
@ -78,7 +79,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
return {
|
||||
completion: displayAlias,
|
||||
suffix: ' ',
|
||||
href: 'https://matrix.to/#/' + displayAlias,
|
||||
href: makeRoomPermalink(displayAlias),
|
||||
component: (
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||
),
|
||||
|
@ -28,6 +28,7 @@ import _sortBy from 'lodash/sortBy';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
|
||||
import type {Room, RoomMember} from 'matrix-js-sdk';
|
||||
import {makeUserPermalink} from "../matrix-to";
|
||||
|
||||
const USER_REGEX = /@\S*/g;
|
||||
|
||||
@ -106,7 +107,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
// relies on the length of the entity === length of the text in the decoration.
|
||||
completion: user.rawDisplayName.replace(' (IRC)', ''),
|
||||
suffix: range.start === 0 ? ': ' : ' ',
|
||||
href: 'https://matrix.to/#/' + user.userId,
|
||||
href: makeUserPermalink(user.userId),
|
||||
component: (
|
||||
<PillCompletion
|
||||
initialComponent={<MemberAvatar member={user} width={24} height={24} />}
|
||||
@ -157,7 +158,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ limitations under the License.
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
@ -29,11 +30,11 @@ module.exports = {
|
||||
ContextualMenuContainerId: "mx_ContextualMenu_Container",
|
||||
|
||||
propTypes: {
|
||||
menuWidth: React.PropTypes.number,
|
||||
menuHeight: React.PropTypes.number,
|
||||
chevronOffset: React.PropTypes.number,
|
||||
menuColour: React.PropTypes.string,
|
||||
chevronFace: React.PropTypes.string, // top, bottom, left, right
|
||||
menuWidth: PropTypes.number,
|
||||
menuHeight: PropTypes.number,
|
||||
chevronOffset: PropTypes.number,
|
||||
menuColour: PropTypes.string,
|
||||
chevronFace: PropTypes.string, // top, bottom, left, right
|
||||
},
|
||||
|
||||
getOrCreateContainer: function() {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
@ -30,8 +31,8 @@ module.exports = React.createClass({
|
||||
displayName: 'CreateRoom',
|
||||
|
||||
propTypes: {
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
onRoomCreated: PropTypes.func,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
},
|
||||
|
||||
phases: {
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import sdk from '../../index';
|
||||
@ -28,7 +29,7 @@ const FilePanel = React.createClass({
|
||||
displayName: 'FilePanel',
|
||||
|
||||
propTypes: {
|
||||
roomId: React.PropTypes.string.isRequired,
|
||||
roomId: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -31,6 +31,7 @@ import GroupStoreCache from '../../stores/GroupStoreCache';
|
||||
import GroupStore from '../../stores/GroupStore';
|
||||
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
|
||||
|
||||
const LONG_DESC_PLACEHOLDER = _td(
|
||||
`<h1>HTML for your community's page</h1>
|
||||
@ -209,7 +210,7 @@ const FeaturedRoom = React.createClass({
|
||||
|
||||
let permalink = null;
|
||||
if (this.props.summaryInfo.profile && this.props.summaryInfo.profile.canonical_alias) {
|
||||
permalink = 'https://matrix.to/#/' + this.props.summaryInfo.profile.canonical_alias;
|
||||
permalink = makeGroupPermalink(this.props.summaryInfo.profile.canonical_alias);
|
||||
}
|
||||
|
||||
let roomNameNode = null;
|
||||
@ -366,7 +367,7 @@ const FeaturedUser = React.createClass({
|
||||
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
|
||||
const name = this.props.summaryInfo.displayname || this.props.summaryInfo.user_id;
|
||||
|
||||
const permalink = 'https://matrix.to/#/' + this.props.summaryInfo.user_id;
|
||||
const permalink = makeUserPermalink(this.props.summaryInfo.user_id);
|
||||
const userNameNode = <a href={permalink} onClick={this.onClick}>{ name }</a>;
|
||||
const httpUrl = MatrixClientPeg.get()
|
||||
.mxcUrlToHttp(this.props.summaryInfo.avatar_url, 64, 64);
|
||||
@ -390,7 +391,7 @@ const FeaturedUser = React.createClass({
|
||||
});
|
||||
|
||||
const GroupContext = {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore).isRequired,
|
||||
groupStore: PropTypes.instanceOf(GroupStore).isRequired,
|
||||
};
|
||||
|
||||
CategoryRoomList.contextTypes = GroupContext;
|
||||
@ -408,7 +409,7 @@ export default React.createClass({
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
groupStore: React.PropTypes.instanceOf(GroupStore),
|
||||
groupStore: PropTypes.instanceOf(GroupStore),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
|
@ -18,6 +18,7 @@ import Matrix from 'matrix-js-sdk';
|
||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||
|
||||
@ -26,18 +27,18 @@ export default React.createClass({
|
||||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
authData: PropTypes.shape({
|
||||
flows: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
session: PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
makeRequest: PropTypes.func.isRequired,
|
||||
|
||||
// callback called when the auth process has finished,
|
||||
// successfully or unsuccessfully.
|
||||
@ -51,22 +52,22 @@ export default React.createClass({
|
||||
// the auth session.
|
||||
// * clientSecret {string} The client secret used in auth
|
||||
// sessions with the ID server.
|
||||
onAuthFinished: React.PropTypes.func.isRequired,
|
||||
onAuthFinished: PropTypes.func.isRequired,
|
||||
|
||||
// Inputs provided by the user to the auth process
|
||||
// and used by various stages. As passed to js-sdk
|
||||
// interactive-auth
|
||||
inputs: React.PropTypes.object,
|
||||
inputs: PropTypes.object,
|
||||
|
||||
// As js-sdk interactive-auth
|
||||
makeRegistrationUrl: React.PropTypes.func,
|
||||
sessionId: React.PropTypes.string,
|
||||
clientSecret: React.PropTypes.string,
|
||||
emailSid: React.PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func,
|
||||
sessionId: PropTypes.string,
|
||||
clientSecret: PropTypes.string,
|
||||
emailSid: PropTypes.string,
|
||||
|
||||
// If true, poll to see if the auth flow has been completed
|
||||
// out-of-band
|
||||
poll: React.PropTypes.bool,
|
||||
poll: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -18,8 +18,8 @@ limitations under the License.
|
||||
|
||||
import * as Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||
import Notifier from '../../Notifier';
|
||||
@ -31,6 +31,9 @@ import sessionStore from '../../stores/SessionStore';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
@ -44,23 +47,23 @@ const LoggedInView = React.createClass({
|
||||
displayName: 'LoggedInView',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: React.PropTypes.string.isRequired,
|
||||
onRoomCreated: React.PropTypes.func,
|
||||
onUserSettingsClose: React.PropTypes.func,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient).isRequired,
|
||||
page_type: PropTypes.string.isRequired,
|
||||
onRoomCreated: PropTypes.func,
|
||||
onUserSettingsClose: PropTypes.func,
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: React.PropTypes.func,
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
teamToken: React.PropTypes.string,
|
||||
teamToken: PropTypes.string,
|
||||
|
||||
// and lots and lots of other stuff.
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: React.PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
authCache: React.PropTypes.object,
|
||||
matrixClient: PropTypes.instanceOf(Matrix.MatrixClient),
|
||||
authCache: PropTypes.object,
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
@ -208,8 +211,51 @@ const LoggedInView = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
_onDragEnd: function(result) {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dest = result.destination.droppableId;
|
||||
|
||||
if (dest === 'tag-panel-droppable') {
|
||||
// Could be "GroupTile +groupId:domain"
|
||||
const draggableId = result.draggableId.split(' ').pop();
|
||||
|
||||
// Dispatch synchronously so that the TagPanel receives an
|
||||
// optimistic update from TagOrderStore before the previous
|
||||
// state is shown.
|
||||
dis.dispatch(TagOrderActions.moveTag(
|
||||
this._matrixClient,
|
||||
draggableId,
|
||||
result.destination.index,
|
||||
), true);
|
||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||
this._onRoomTileEndDrag(result);
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomTileEndDrag: function(result) {
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
if (prevTag === 'undefined') prevTag = undefined;
|
||||
|
||||
const roomId = result.draggableId.split('_')[1];
|
||||
|
||||
const oldIndex = result.source.index;
|
||||
const newIndex = result.destination.index;
|
||||
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
this._matrixClient,
|
||||
this._matrixClient.getRoom(roomId),
|
||||
prevTag, newTag,
|
||||
oldIndex, newIndex,
|
||||
), true);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TagPanel = sdk.getComponent('structures.TagPanel');
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
@ -330,10 +376,9 @@ const LoggedInView = React.createClass({
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper'>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
{ SettingsStore.isFeatureEnabled("feature_tag_panel") ? <TagPanel /> : <div /> }
|
||||
<LeftPanel
|
||||
selectedRoom={this.props.currentRoomId}
|
||||
collapsed={this.props.collapseLhs || false}
|
||||
disabled={this.props.leftDisabled}
|
||||
/>
|
||||
@ -342,9 +387,10 @@ const LoggedInView = React.createClass({
|
||||
</main>
|
||||
{ right_panel }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default DragDropContext(HTML5Backend)(LoggedInView);
|
||||
export default LoggedInView;
|
||||
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
@ -92,38 +93,38 @@ export default React.createClass({
|
||||
displayName: 'MatrixChat',
|
||||
|
||||
propTypes: {
|
||||
config: React.PropTypes.object,
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
onNewScreen: React.PropTypes.func,
|
||||
registrationUrl: React.PropTypes.string,
|
||||
enableGuest: React.PropTypes.bool,
|
||||
config: PropTypes.object,
|
||||
ConferenceHandler: PropTypes.any,
|
||||
onNewScreen: PropTypes.func,
|
||||
registrationUrl: PropTypes.string,
|
||||
enableGuest: PropTypes.bool,
|
||||
|
||||
// the queryParams extracted from the [real] query-string of the URI
|
||||
realQueryParams: React.PropTypes.object,
|
||||
realQueryParams: PropTypes.object,
|
||||
|
||||
// the initial queryParams extracted from the hash-fragment of the URI
|
||||
startingFragmentQueryParams: React.PropTypes.object,
|
||||
startingFragmentQueryParams: PropTypes.object,
|
||||
|
||||
// called when we have completed a token login
|
||||
onTokenLoginCompleted: React.PropTypes.func,
|
||||
onTokenLoginCompleted: PropTypes.func,
|
||||
|
||||
// Represents the screen to display as a result of parsing the initial
|
||||
// window.location
|
||||
initialScreenAfterLogin: React.PropTypes.shape({
|
||||
screen: React.PropTypes.string.isRequired,
|
||||
params: React.PropTypes.object,
|
||||
initialScreenAfterLogin: PropTypes.shape({
|
||||
screen: PropTypes.string.isRequired,
|
||||
params: PropTypes.object,
|
||||
}),
|
||||
|
||||
// displayname, if any, to set on the device when logging
|
||||
// in/registering.
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// A function that makes a registration URL
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
appConfig: React.PropTypes.object,
|
||||
appConfig: PropTypes.object,
|
||||
},
|
||||
|
||||
AuxPanel: {
|
||||
@ -617,18 +618,26 @@ export default React.createClass({
|
||||
},
|
||||
|
||||
_startRegistration: function(params) {
|
||||
this.setStateForNewView({
|
||||
const newState = {
|
||||
view: VIEWS.REGISTER,
|
||||
// these params may be undefined, but if they are,
|
||||
// unset them from our state: we don't want to
|
||||
// resume a previous registration session if the
|
||||
// user just clicked 'register'
|
||||
register_client_secret: params.client_secret,
|
||||
register_session_id: params.session_id,
|
||||
register_hs_url: params.hs_url,
|
||||
register_is_url: params.is_url,
|
||||
register_id_sid: params.sid,
|
||||
});
|
||||
};
|
||||
|
||||
// Only honour params if they are all present, otherwise we reset
|
||||
// HS and IS URLs when switching to registration.
|
||||
if (params.client_secret &&
|
||||
params.session_id &&
|
||||
params.hs_url &&
|
||||
params.is_url &&
|
||||
params.sid
|
||||
) {
|
||||
newState.register_client_secret = params.client_secret;
|
||||
newState.register_session_id = params.session_id;
|
||||
newState.register_hs_url = params.hs_url;
|
||||
newState.register_is_url = params.is_url;
|
||||
newState.register_id_sid = params.sid;
|
||||
}
|
||||
|
||||
this.setStateForNewView(newState);
|
||||
this.notifyNewScreen('register');
|
||||
},
|
||||
|
||||
@ -846,16 +855,36 @@ export default React.createClass({
|
||||
}).close;
|
||||
},
|
||||
|
||||
_leaveRoomWarnings: function(roomId) {
|
||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
// Show a warning if there are additional complications.
|
||||
const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', '');
|
||||
const warnings = [];
|
||||
if (joinRules) {
|
||||
const rule = joinRules.getContent().join_rule;
|
||||
if (rule !== "public") {
|
||||
warnings.push((
|
||||
<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) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||
const warnings = this._leaveRoomWarnings(roomId);
|
||||
|
||||
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
|
||||
title: _t("Leave room"),
|
||||
description: (
|
||||
<span>
|
||||
{ _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
|
||||
{ warnings }
|
||||
</span>
|
||||
),
|
||||
onFinished: (shouldLeave) => {
|
||||
@ -1065,10 +1094,10 @@ export default React.createClass({
|
||||
// this if we are not scrolled up in the view. To find out, delegate to
|
||||
// the timeline panel. If the timeline panel doesn't exist, then we assume
|
||||
// it is safe to reset the timeline.
|
||||
if (!self.refs.loggedInView) {
|
||||
if (!self._loggedInView || !self._loggedInView.child) {
|
||||
return true;
|
||||
}
|
||||
return self.refs.loggedInView.canResetTimelineInRoom(roomId);
|
||||
return self._loggedInView.child.canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState) {
|
||||
@ -1480,6 +1509,17 @@ export default React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
onServerConfigChange(config) {
|
||||
const newState = {};
|
||||
if (config.hsUrl) {
|
||||
newState.register_hs_url = config.hsUrl;
|
||||
}
|
||||
if (config.isUrl) {
|
||||
newState.register_is_url = config.isUrl;
|
||||
}
|
||||
this.setState(newState);
|
||||
},
|
||||
|
||||
_makeRegistrationUrl: function(params) {
|
||||
if (this.props.startingFragmentQueryParams.referrer) {
|
||||
params.referrer = this.props.startingFragmentQueryParams.referrer;
|
||||
@ -1487,6 +1527,10 @@ export default React.createClass({
|
||||
return this.props.makeRegistrationUrl(params);
|
||||
},
|
||||
|
||||
_collectLoggedInView: function(ref) {
|
||||
this._loggedInView = ref;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
@ -1519,7 +1563,7 @@ export default React.createClass({
|
||||
*/
|
||||
const LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
return (
|
||||
<LoggedInView ref="loggedInView" matrixClient={MatrixClientPeg.get()}
|
||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
onRegistered={this.onRegistered}
|
||||
@ -1564,6 +1608,7 @@ export default React.createClass({
|
||||
onLoginClick={this.onLoginClick}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1598,6 +1643,7 @@ export default React.createClass({
|
||||
onForgotPasswordClick={this.onForgotPasswordClick}
|
||||
enableGuest={this.props.enableGuest}
|
||||
onCancelClick={MatrixClientPeg.get() ? this.onReturnToAppClick : null}
|
||||
onServerConfigChange={this.onServerConfigChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -16,15 +16,15 @@ limitations under the License.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import shouldHideEvent from '../../shouldHideEvent';
|
||||
import {wantsDateSeparator} from '../../DateUtils';
|
||||
import dis from "../../dispatcher";
|
||||
import sdk from '../../index';
|
||||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
const MILLIS_IN_DAY = 86400000;
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
@ -32,63 +32,63 @@ module.exports = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
// 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
|
||||
// back-pagination in progress
|
||||
backPaginating: React.PropTypes.bool,
|
||||
backPaginating: PropTypes.bool,
|
||||
|
||||
// true to show a spinner at the end of the timeline to indicate
|
||||
// forward-pagination in progress
|
||||
forwardPaginating: React.PropTypes.bool,
|
||||
forwardPaginating: PropTypes.bool,
|
||||
|
||||
// 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.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
highlightedEventId: PropTypes.string,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
// event after which we should show a read marker
|
||||
readMarkerEventId: React.PropTypes.string,
|
||||
readMarkerEventId: PropTypes.string,
|
||||
|
||||
// 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
|
||||
// for pending messages.
|
||||
ourUserId: React.PropTypes.string,
|
||||
ourUserId: PropTypes.string,
|
||||
|
||||
// true to suppress the date at the start of the timeline
|
||||
suppressFirstDateSeparator: React.PropTypes.bool,
|
||||
suppressFirstDateSeparator: PropTypes.bool,
|
||||
|
||||
// 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
|
||||
// scroll down when we are at the bottom of the window. See ScrollPanel
|
||||
// for more details.
|
||||
stickyBottom: React.PropTypes.bool,
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
// 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.
|
||||
onFillRequest: React.PropTypes.func,
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
// className for the panel
|
||||
className: React.PropTypes.string.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
|
||||
// shape parameter to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// show twelve hour timestamps
|
||||
isTwelveHour: React.PropTypes.bool,
|
||||
isTwelveHour: PropTypes.bool,
|
||||
|
||||
// show timestamps always
|
||||
alwaysShowTimestamps: React.PropTypes.bool,
|
||||
alwaysShowTimestamps: PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
@ -325,7 +325,7 @@ module.exports = React.createClass({
|
||||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -479,7 +479,7 @@ module.exports = React.createClass({
|
||||
|
||||
// do we need a date separator since the last event?
|
||||
if (this._wantsDateSeparator(prevEvent, eventDate)) {
|
||||
const dateSeparator = <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);
|
||||
continuation = false;
|
||||
}
|
||||
@ -522,17 +522,7 @@ module.exports = React.createClass({
|
||||
// here.
|
||||
return !this.props.suppressFirstDateSeparator;
|
||||
}
|
||||
const prevEventDate = prevEvent.getDate();
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
||||
// Return early for events that are > 24h apart
|
||||
if (Math.abs(prevEvent.getTs() - nextEventDate.getTime()) > MILLIS_IN_DAY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compare weekdays
|
||||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
return wantsDateSeparator(prevEvent.getDate(), nextEventDate);
|
||||
},
|
||||
|
||||
// get a list of read receipts that should be shown next to this event
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import sdk from '../../index';
|
||||
import { _t } from '../../languageHandler';
|
||||
@ -26,7 +27,7 @@ export default withMatrixClient(React.createClass({
|
||||
displayName: 'MyGroups',
|
||||
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@ -72,8 +73,10 @@ export default withMatrixClient(React.createClass({
|
||||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
<GeminiScrollbar className="mx_MyGroups_joinedGroups">
|
||||
<GeminiScrollbar>
|
||||
<div className="mx_MyGroups_joinedGroups">
|
||||
{ groupNodes }
|
||||
</div>
|
||||
</GeminiScrollbar> :
|
||||
<div className="mx_MyGroups_placeholder">
|
||||
{ _t(
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
@ -23,7 +24,7 @@ import WhoIsTyping from '../../WhoIsTyping';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import { showUnknownDeviceDialogForMessages } from '../../cryptodevices';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
@ -41,59 +42,59 @@ module.exports = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
// the room this statusbar is representing.
|
||||
room: React.PropTypes.object.isRequired,
|
||||
room: PropTypes.object.isRequired,
|
||||
|
||||
// the number of messages which have arrived since we've been scrolled up
|
||||
numUnreadMessages: React.PropTypes.number,
|
||||
numUnreadMessages: PropTypes.number,
|
||||
|
||||
// this is true if we are fully scrolled-down, and are looking at
|
||||
// the end of the live timeline.
|
||||
atEndOfLiveTimeline: React.PropTypes.bool,
|
||||
atEndOfLiveTimeline: PropTypes.bool,
|
||||
|
||||
// This is true when the user is alone in the room, but has also sent a message.
|
||||
// Used to suggest to the user to invite someone
|
||||
sentMessageAndIsAlone: React.PropTypes.bool,
|
||||
sentMessageAndIsAlone: PropTypes.bool,
|
||||
|
||||
// true if there is an active call in this room (means we show
|
||||
// the 'Active Call' text in the status bar if there is nothing
|
||||
// more interesting)
|
||||
hasActiveCall: React.PropTypes.bool,
|
||||
hasActiveCall: PropTypes.bool,
|
||||
|
||||
// Number of names to display in typing indication. E.g. set to 3, will
|
||||
// result in "X, Y, Z and 100 others are typing."
|
||||
whoIsTypingLimit: React.PropTypes.number,
|
||||
whoIsTypingLimit: PropTypes.number,
|
||||
|
||||
// callback for when the user clicks on the 'resend all' button in the
|
||||
// 'unsent messages' bar
|
||||
onResendAllClick: React.PropTypes.func,
|
||||
onResendAllClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'cancel all' button in the
|
||||
// 'unsent messages' bar
|
||||
onCancelAllClick: React.PropTypes.func,
|
||||
onCancelAllClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'invite others' button in the
|
||||
// 'you are alone' bar
|
||||
onInviteClick: React.PropTypes.func,
|
||||
onInviteClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'stop warning me' button in the
|
||||
// 'you are alone' bar
|
||||
onStopWarningClick: React.PropTypes.func,
|
||||
onStopWarningClick: PropTypes.func,
|
||||
|
||||
// callback for when the user clicks on the 'scroll to bottom' button
|
||||
onScrollToBottomClick: React.PropTypes.func,
|
||||
onScrollToBottomClick: PropTypes.func,
|
||||
|
||||
// callback for when we do something that changes the size of the
|
||||
// status bar. This is used to trigger a re-layout in the parent
|
||||
// component.
|
||||
onResize: React.PropTypes.func,
|
||||
onResize: PropTypes.func,
|
||||
|
||||
// callback for when the status bar can be hidden from view, as it is
|
||||
// not displaying anything
|
||||
onHidden: React.PropTypes.func,
|
||||
onHidden: PropTypes.func,
|
||||
|
||||
// callback for when the status bar is displaying something and should
|
||||
// be visible
|
||||
onVisible: React.PropTypes.func,
|
||||
onVisible: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -147,6 +148,13 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
_onSendWithoutVerifyingClick: function() {
|
||||
cryptodevices.getUnknownDevicesForRoom(MatrixClientPeg.get(), this.props.room).then((devices) => {
|
||||
cryptodevices.markAllDevicesKnown(MatrixClientPeg.get(), devices);
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
});
|
||||
},
|
||||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
},
|
||||
@ -156,7 +164,7 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||
cryptodevices.showUnknownDeviceDialogForMessages(MatrixClientPeg.get(), this.props.room);
|
||||
},
|
||||
|
||||
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {
|
||||
@ -169,8 +177,10 @@ module.exports = React.createClass({
|
||||
|
||||
// Check whether current size is greater than 0, if yes call props.onVisible
|
||||
_checkSize: function() {
|
||||
if (this.props.onVisible && this._getSize()) {
|
||||
this.props.onVisible();
|
||||
if (this._getSize()) {
|
||||
if (this.props.onVisible) this.props.onVisible();
|
||||
} else {
|
||||
if (this.props.onHidden) this.props.onHidden();
|
||||
}
|
||||
},
|
||||
|
||||
@ -286,10 +296,11 @@ module.exports = React.createClass({
|
||||
if (hasUDE) {
|
||||
title = _t("Message not sent due to unknown devices being present");
|
||||
content = _t(
|
||||
"<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>,
|
||||
'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>,
|
||||
},
|
||||
);
|
||||
@ -302,11 +313,11 @@ module.exports = React.createClass({
|
||||
) {
|
||||
title = unsentMessages[0].error.data.error;
|
||||
} else {
|
||||
title = _t("Some of your messages have not been sent.");
|
||||
title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
|
||||
}
|
||||
content = _t("<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.",
|
||||
{},
|
||||
{ count: unsentMessages.length },
|
||||
{
|
||||
'resendText': (sub) =>
|
||||
<a className="mx_RoomStatusBar_resend_link" key="resend" onClick={this._onResendAllClick}>{ sub }</a>,
|
||||
|
@ -24,6 +24,7 @@ import shouldHideEvent from "../../shouldHideEvent";
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
const classNames = require("classnames");
|
||||
import { _t } from '../../languageHandler';
|
||||
@ -58,18 +59,18 @@ if (DEBUG) {
|
||||
module.exports = React.createClass({
|
||||
displayName: 'RoomView',
|
||||
propTypes: {
|
||||
ConferenceHandler: React.PropTypes.any,
|
||||
ConferenceHandler: PropTypes.any,
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that
|
||||
// transitioned to PWLU)
|
||||
onRegistered: React.PropTypes.func,
|
||||
onRegistered: PropTypes.func,
|
||||
|
||||
// An object representing a third party invite to join this room
|
||||
// Fields:
|
||||
// * inviteSignUrl (string) The URL used to join this room from an email invite
|
||||
// (given as part of the link in the invite email)
|
||||
// * invitedEmail (string) The email address that was invited to this room
|
||||
thirdPartyInvite: React.PropTypes.object,
|
||||
thirdPartyInvite: PropTypes.object,
|
||||
|
||||
// Any data about the room that would normally come from the Home Server
|
||||
// but has been passed out-of-band, eg. the room name and avatar URL
|
||||
@ -80,10 +81,10 @@ module.exports = React.createClass({
|
||||
// * avatarUrl (string) The mxc:// avatar URL for the room
|
||||
// * inviterName (string) The display name of the person who
|
||||
// * invited us tovthe room
|
||||
oobData: React.PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
|
||||
// is the RightPanel collapsed?
|
||||
collapsedRhs: React.PropTypes.bool,
|
||||
collapsedRhs: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@ -263,12 +264,19 @@ module.exports = React.createClass({
|
||||
isPeeking: true, // this will change to false if peeking fails
|
||||
});
|
||||
MatrixClientPeg.get().peekInRoom(roomId).then((room) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
room: room,
|
||||
peekLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop peeking if anything went wrong
|
||||
this.setState({
|
||||
isPeeking: false,
|
||||
@ -285,7 +293,7 @@ module.exports = React.createClass({
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}).done();
|
||||
});
|
||||
}
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
@ -628,8 +636,8 @@ module.exports = React.createClass({
|
||||
const room = this.state.room;
|
||||
if (!room) return;
|
||||
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
|
||||
console.log("Tinter.tint from updateTint");
|
||||
const color_scheme = SettingsStore.getValue("roomColor", room.roomId);
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
},
|
||||
|
||||
@ -678,23 +686,7 @@ module.exports = React.createClass({
|
||||
// a member state changed in this room
|
||||
// refresh the conf call notification state
|
||||
this._updateConfCallNotification();
|
||||
|
||||
// if we are now a member of the room, where we were not before, that
|
||||
// means we have finished joining a room we were previously peeking
|
||||
// into.
|
||||
const me = MatrixClientPeg.get().credentials.userId;
|
||||
if (this.state.joining && this.state.room.hasMembershipState(me, "join")) {
|
||||
// Having just joined a room, check to see if it looks like a DM room, and if so,
|
||||
// mark it as one. This is to work around the fact that some clients don't support
|
||||
// is_direct. We should remove this once they do.
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (Rooms.looksLikeDirectMessageRoom(this.state.room, me)) {
|
||||
// XXX: There's not a whole lot we can really do if this fails: at best
|
||||
// perhaps we could try a couple more times, but since it's a temporary
|
||||
// compatability workaround, let's not bother.
|
||||
Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done();
|
||||
}
|
||||
}
|
||||
this._updateDMState();
|
||||
}, 500),
|
||||
|
||||
_checkIfAlone: function(room) {
|
||||
@ -735,6 +727,44 @@ module.exports = React.createClass({
|
||||
});
|
||||
},
|
||||
|
||||
_updateDMState() {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (!me || me.membership !== "join") {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have accepted an invite with is_direct set
|
||||
if (me.events.member.getPrevContent().membership === "invite" &&
|
||||
me.events.member.getPrevContent().is_direct
|
||||
) {
|
||||
// This is a DM with the sender of the invite event (which we assume
|
||||
// preceded the join event)
|
||||
Rooms.setDMRoom(
|
||||
this.state.room.roomId,
|
||||
me.events.member.getUnsigned().prev_sender,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const invitedMembers = this.state.room.getMembersWithMembership("invite");
|
||||
const joinedMembers = this.state.room.getMembersWithMembership("join");
|
||||
|
||||
// There must be one invited member and one joined member
|
||||
if (invitedMembers.length !== 1 || joinedMembers.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The user may have sent an invite with is_direct sent
|
||||
const other = invitedMembers[0];
|
||||
if (other &&
|
||||
other.membership === "invite" &&
|
||||
other.events.member.getContent().is_direct
|
||||
) {
|
||||
Rooms.setDMRoom(this.state.room.roomId, other.userId);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
onSearchResultsResize: function() {
|
||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
||||
},
|
||||
@ -827,18 +857,6 @@ module.exports = React.createClass({
|
||||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl },
|
||||
});
|
||||
|
||||
// if this is an invite and has the 'direct' hint set, mark it as a DM room now.
|
||||
if (this.state.room) {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (me && me.membership == 'invite') {
|
||||
if (me.events.member.getContent().is_direct) {
|
||||
// The 'direct' hint is there, so declare that this is a DM room for
|
||||
// whoever invited us.
|
||||
return Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
},
|
||||
@ -863,9 +881,13 @@ module.exports = React.createClass({
|
||||
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
|
||||
const items = ev.dataTransfer.items;
|
||||
if (items.length == 1) {
|
||||
if (items[0].kind == 'file') {
|
||||
const items = [...ev.dataTransfer.items];
|
||||
if (items.length >= 1) {
|
||||
const isDraggingFiles = items.every(function(item) {
|
||||
return item.kind == 'file';
|
||||
});
|
||||
|
||||
if (isDraggingFiles) {
|
||||
this.setState({ draggingFile: true });
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
@ -876,10 +898,8 @@ module.exports = React.createClass({
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.setState({ draggingFile: false });
|
||||
const files = ev.dataTransfer.files;
|
||||
if (files.length == 1) {
|
||||
this.uploadFile(files[0]);
|
||||
}
|
||||
const files = [...ev.dataTransfer.files];
|
||||
files.forEach(this.uploadFile);
|
||||
},
|
||||
|
||||
onDragLeaveOrEnd: function(ev) {
|
||||
@ -1369,10 +1389,12 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
onStatusBarHidden: function() {
|
||||
if (this.unmounted) return;
|
||||
// This is currently not desired as it is annoying if it keeps expanding and collapsing
|
||||
// TODO: Find a less annoying way of hiding the status bar
|
||||
/*if (this.unmounted) return;
|
||||
this.setState({
|
||||
statusBarVisible: false,
|
||||
});
|
||||
});*/
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
const GeminiScrollbar = require('react-gemini-scrollbar');
|
||||
import Promise from 'bluebird';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
@ -86,7 +87,7 @@ module.exports = React.createClass({
|
||||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom: React.PropTypes.bool,
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
@ -95,7 +96,7 @@ module.exports = React.createClass({
|
||||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
*/
|
||||
startAtBottom: React.PropTypes.bool,
|
||||
startAtBottom: PropTypes.bool,
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
@ -110,7 +111,7 @@ module.exports = React.createClass({
|
||||
* directon (at this time) - which will stop the pagination cycle until
|
||||
* the user scrolls again.
|
||||
*/
|
||||
onFillRequest: React.PropTypes.func,
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||
* there are children elements that are far out of view and could be removed
|
||||
@ -121,24 +122,24 @@ module.exports = React.createClass({
|
||||
* first element to remove if removing from the front/bottom, and last element
|
||||
* to remove if removing from the back/top.
|
||||
*/
|
||||
onUnfillRequest: React.PropTypes.func,
|
||||
onUnfillRequest: PropTypes.func,
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
/* onResize: a callback which is called whenever the Gemini scroll
|
||||
* panel is resized
|
||||
*/
|
||||
onResize: React.PropTypes.func,
|
||||
onResize: PropTypes.func,
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: React.PropTypes.object,
|
||||
style: PropTypes.object,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -17,15 +17,16 @@ limitations under the License.
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import FilterStore from '../../stores/FilterStore';
|
||||
import FlairStore from '../../stores/FlairStore';
|
||||
import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
|
||||
import sdk from '../../index';
|
||||
import dis from '../../dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
displayName: 'TagPanel',
|
||||
@ -36,17 +37,7 @@ const TagPanel = React.createClass({
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
// A list of group profiles for tags that are group IDs. The intention in future
|
||||
// is to allow arbitrary tags to be selected in the TagPanel, not just groups.
|
||||
// For now, it suffices to maintain a list of ordered group profiles.
|
||||
orderedGroupTagProfiles: [
|
||||
// {
|
||||
// groupId: '+awesome:foo.bar',{
|
||||
// name: 'My Awesome Community',
|
||||
// avatarUrl: 'mxc://...',
|
||||
// shortDescription: 'Some description...',
|
||||
// },
|
||||
],
|
||||
orderedTags: [],
|
||||
selectedTags: [],
|
||||
};
|
||||
},
|
||||
@ -54,28 +45,15 @@ const TagPanel = React.createClass({
|
||||
componentWillMount: function() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.on("sync", this.onClientSync);
|
||||
|
||||
this._filterStoreToken = FilterStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedTags: FilterStore.getSelectedTags(),
|
||||
});
|
||||
});
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orderedTags = TagOrderStore.getOrderedTags() || [];
|
||||
const orderedGroupTags = orderedTags.filter((t) => t[0] === '+');
|
||||
// XXX: One profile lookup failing will bring the whole lot down
|
||||
Promise.all(orderedGroupTags.map(
|
||||
(groupId) => FlairStore.getGroupProfileCached(this.context.matrixClient, groupId),
|
||||
)).then((orderedGroupTagProfiles) => {
|
||||
if (this.unmounted) return;
|
||||
this.setState({orderedGroupTagProfiles});
|
||||
this.setState({
|
||||
orderedTags: TagOrderStore.getOrderedTags() || [],
|
||||
selectedTags: TagOrderStore.getSelectedTags(),
|
||||
});
|
||||
});
|
||||
// This could be done by anything with a matrix client
|
||||
@ -85,6 +63,7 @@ const TagPanel = React.createClass({
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.matrixClient.removeListener("sync", this.onClientSync);
|
||||
if (this._filterStoreToken) {
|
||||
this._filterStoreToken.remove();
|
||||
}
|
||||
@ -95,7 +74,17 @@ const TagPanel = React.createClass({
|
||||
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) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
@ -104,30 +93,62 @@ const TagPanel = React.createClass({
|
||||
dis.dispatch({action: 'view_create_group'});
|
||||
},
|
||||
|
||||
onTagTileEndDrag() {
|
||||
dis.dispatch(TagOrderActions.commitTagOrdering(this.context.matrixClient));
|
||||
onClearFilterClick(ev) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
},
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const TintableSvg = sdk.getComponent('elements.TintableSvg');
|
||||
const GroupsButton = sdk.getComponent('elements.GroupsButton');
|
||||
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
const tags = this.state.orderedGroupTagProfiles.map((groupProfile, index) => {
|
||||
const tags = this.state.orderedTags.map((tag, index) => {
|
||||
return <DNDTagTile
|
||||
key={groupProfile.groupId + '_' + index}
|
||||
groupProfile={groupProfile}
|
||||
selected={this.state.selectedTags.includes(groupProfile.groupId)}
|
||||
onEndDrag={this.onTagTileEndDrag}
|
||||
key={tag}
|
||||
tag={tag}
|
||||
index={index}
|
||||
selected={this.state.selectedTags.includes(tag)}
|
||||
/>;
|
||||
});
|
||||
return <div className="mx_TagPanel" onClick={this.onClick}>
|
||||
<div className="mx_TagPanel_tagTileContainer">
|
||||
{ tags }
|
||||
</div>
|
||||
<AccessibleButton className="mx_TagPanel_createGroupButton" onClick={this.onCreateGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="25" height="25" />
|
||||
|
||||
const clearButton = this.state.selectedTags.length > 0 ?
|
||||
<img
|
||||
src="img/icons-close.svg"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
width="24"
|
||||
height="24" /> :
|
||||
<div />;
|
||||
|
||||
return <div className="mx_TagPanel">
|
||||
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
{ clearButton }
|
||||
</AccessibleButton>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbar
|
||||
className="mx_TagPanel_scroller"
|
||||
autoshow={true}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
<Droppable
|
||||
droppableId="tag-panel-droppable"
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_TagPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ tags }
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<div className="mx_TagPanel_createGroupButton">
|
||||
<GroupsButton tooltip={true} />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const Matrix = require("matrix-js-sdk");
|
||||
@ -58,49 +59,49 @@ var TimelinePanel = React.createClass({
|
||||
// representing. This may or may not have a room, depending on what it's
|
||||
// a timeline representing. If it has a room, we maintain RRs etc for
|
||||
// that room.
|
||||
timelineSet: React.PropTypes.object.isRequired,
|
||||
timelineSet: PropTypes.object.isRequired,
|
||||
|
||||
showReadReceipts: React.PropTypes.bool,
|
||||
showReadReceipts: PropTypes.bool,
|
||||
// Enable managing RRs and RMs. These require the timelineSet to have a room.
|
||||
manageReadReceipts: React.PropTypes.bool,
|
||||
manageReadMarkers: React.PropTypes.bool,
|
||||
manageReadReceipts: PropTypes.bool,
|
||||
manageReadMarkers: PropTypes.bool,
|
||||
|
||||
// true to give the component a 'display: none' style.
|
||||
hidden: React.PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
|
||||
// ID of an event to highlight. If undefined, no event will be highlighted.
|
||||
// typically this will be either 'eventId' or undefined.
|
||||
highlightedEventId: React.PropTypes.string,
|
||||
highlightedEventId: PropTypes.string,
|
||||
|
||||
// id of an event to jump to. If not given, will go to the end of the
|
||||
// live timeline.
|
||||
eventId: React.PropTypes.string,
|
||||
eventId: PropTypes.string,
|
||||
|
||||
// where to position the event given by eventId, in pixels from the
|
||||
// bottom of the viewport. If not given, will try to put the event
|
||||
// half way down the viewport.
|
||||
eventPixelOffset: React.PropTypes.number,
|
||||
eventPixelOffset: PropTypes.number,
|
||||
|
||||
// Should we show URL Previews
|
||||
showUrlPreview: React.PropTypes.bool,
|
||||
showUrlPreview: PropTypes.bool,
|
||||
|
||||
// callback which is called when the panel is scrolled.
|
||||
onScroll: React.PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
// callback which is called when the read-up-to mark is updated.
|
||||
onReadMarkerUpdated: React.PropTypes.func,
|
||||
onReadMarkerUpdated: PropTypes.func,
|
||||
|
||||
// maximum number of events to show in a timeline
|
||||
timelineCap: React.PropTypes.number,
|
||||
timelineCap: PropTypes.number,
|
||||
|
||||
// classname to use for the messagepanel
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
// shape property to be passed to EventTiles
|
||||
tileShape: React.PropTypes.string,
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
// placeholder text to use if the timeline is empty
|
||||
empty: React.PropTypes.string,
|
||||
empty: PropTypes.string,
|
||||
},
|
||||
|
||||
statics: {
|
||||
@ -301,6 +302,8 @@ var TimelinePanel = React.createClass({
|
||||
|
||||
// set off a pagination request.
|
||||
onMessageListFillRequest: function(backwards) {
|
||||
if (!this._shouldPaginate()) return Promise.resolve(false);
|
||||
|
||||
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
|
||||
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
|
||||
const paginatingKey = backwards ? 'backPaginating' : 'forwardPaginating';
|
||||
@ -1090,6 +1093,17 @@ var TimelinePanel = React.createClass({
|
||||
}, this.props.onReadMarkerUpdated);
|
||||
},
|
||||
|
||||
_shouldPaginate: function() {
|
||||
// don't try to paginate while events in the timeline are
|
||||
// still being decrypted. We don't render events while they're
|
||||
// being decrypted, so they don't take up space in the timeline.
|
||||
// This means we can pull quite a lot of events into the timeline
|
||||
// and end up trying to render a lot of events.
|
||||
return !this.state.events.some((e) => {
|
||||
return e.isBeingDecrypted();
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MessagePanel = sdk.getComponent("structures.MessagePanel");
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
@ -1107,7 +1121,7 @@ var TimelinePanel = React.createClass({
|
||||
// exist.
|
||||
if (this.state.timelineLoading) {
|
||||
return (
|
||||
<div className={this.props.className + " mx_RoomView_messageListWrapper"}>
|
||||
<div className="mx_RoomView_messagePanelSpinner">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const ContentMessages = require('../../ContentMessages');
|
||||
const dis = require('../../dispatcher');
|
||||
const filesize = require('filesize');
|
||||
@ -22,7 +23,7 @@ import { _t } from '../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({displayName: 'UploadBar',
|
||||
propTypes: {
|
||||
room: React.PropTypes.object,
|
||||
room: PropTypes.object,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -19,6 +19,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require('../../index');
|
||||
const MatrixClientPeg = require("../../MatrixClientPeg");
|
||||
const PlatformPeg = require("../../PlatformPeg");
|
||||
@ -125,8 +126,8 @@ const THEMES = [
|
||||
|
||||
const IgnoredUser = React.createClass({
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
onUnignored: React.PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
onUnignored: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
_onUnignoreClick: function() {
|
||||
@ -155,16 +156,16 @@ module.exports = React.createClass({
|
||||
displayName: 'UserSettings',
|
||||
|
||||
propTypes: {
|
||||
onClose: React.PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
// The brand string given when creating email pushers
|
||||
brand: React.PropTypes.string,
|
||||
brand: PropTypes.string,
|
||||
|
||||
// The base URL to use in the referral link. Defaults to window.location.origin.
|
||||
referralBaseUrl: React.PropTypes.string,
|
||||
referralBaseUrl: PropTypes.string,
|
||||
|
||||
// Team token for the referral link. If falsy, the referral section will
|
||||
// not appear
|
||||
teamToken: React.PropTypes.string,
|
||||
teamToken: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -375,7 +376,7 @@ module.exports = React.createClass({
|
||||
{ _t("For security, logging out will delete any end-to-end " +
|
||||
"encryption keys from this browser. If you want to be able " +
|
||||
"to decrypt your conversation history from future Riot sessions, " +
|
||||
"please export your room keys for safe-keeping.") }.
|
||||
"please export your room keys for safe-keeping.") }
|
||||
</div>,
|
||||
button: _t("Sign out"),
|
||||
extraButtons: [
|
||||
@ -811,6 +812,12 @@ module.exports = React.createClass({
|
||||
<h3>{ _t('Analytics') }</h3>
|
||||
<div className="mx_UserSettings_section">
|
||||
{ _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 ) }
|
||||
</div>
|
||||
</div>;
|
||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Modal from "../../../Modal";
|
||||
@ -29,13 +30,13 @@ module.exports = React.createClass({
|
||||
displayName: 'ForgotPassword',
|
||||
|
||||
propTypes: {
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
onLoginClick: React.PropTypes.func,
|
||||
onRegisterClick: React.PropTypes.func,
|
||||
onComplete: React.PropTypes.func.isRequired,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
onLoginClick: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
@ -36,27 +37,28 @@ module.exports = React.createClass({
|
||||
displayName: 'Login',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
|
||||
enableGuest: React.PropTypes.bool,
|
||||
enableGuest: PropTypes.bool,
|
||||
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
// Secondary HS which we try to log into if the user is using
|
||||
// the default HS but login fails. Useful for migrating to a
|
||||
// different home server without confusing users.
|
||||
fallbackHsUrl: React.PropTypes.string,
|
||||
fallbackHsUrl: PropTypes.string,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// login shouldn't know or care how registration is done.
|
||||
onRegisterClick: React.PropTypes.func.isRequired,
|
||||
onRegisterClick: PropTypes.func.isRequired,
|
||||
|
||||
// login shouldn't care how password recovery is done.
|
||||
onForgotPasswordClick: React.PropTypes.func,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@ -217,6 +219,8 @@ module.exports = React.createClass({
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.enteredIdentityServerUrl = config.isUrl;
|
||||
}
|
||||
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
self._initLoginLogic(config.hsUrl || null, config.isUrl);
|
||||
});
|
||||
@ -427,10 +431,10 @@ module.exports = React.createClass({
|
||||
// FIXME: remove status.im theme tweaks
|
||||
const theme = SettingsStore.getValue("theme");
|
||||
if (theme !== "status") {
|
||||
header = <h2>{ _t('Sign in') }</h2>;
|
||||
header = <h2>{ _t('Sign in') } { loader }</h2>;
|
||||
} else {
|
||||
if (!this.state.errorText) {
|
||||
header = <h2>{ _t('Sign in to get started') }</h2>;
|
||||
header = <h2>{ _t('Sign in to get started') } { loader }</h2>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
@ -25,7 +26,7 @@ module.exports = React.createClass({
|
||||
displayName: 'PostRegistration',
|
||||
|
||||
propTypes: {
|
||||
onComplete: React.PropTypes.func.isRequired,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -19,6 +19,7 @@ import Matrix from 'matrix-js-sdk';
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import ServerConfig from '../../views/login/ServerConfig';
|
||||
@ -35,31 +36,32 @@ module.exports = React.createClass({
|
||||
displayName: 'Registration',
|
||||
|
||||
propTypes: {
|
||||
onLoggedIn: React.PropTypes.func.isRequired,
|
||||
clientSecret: React.PropTypes.string,
|
||||
sessionId: React.PropTypes.string,
|
||||
makeRegistrationUrl: React.PropTypes.func.isRequired,
|
||||
idSid: React.PropTypes.string,
|
||||
customHsUrl: React.PropTypes.string,
|
||||
customIsUrl: React.PropTypes.string,
|
||||
defaultHsUrl: React.PropTypes.string,
|
||||
defaultIsUrl: React.PropTypes.string,
|
||||
brand: React.PropTypes.string,
|
||||
email: React.PropTypes.string,
|
||||
referrer: React.PropTypes.string,
|
||||
teamServerConfig: React.PropTypes.shape({
|
||||
onLoggedIn: PropTypes.func.isRequired,
|
||||
clientSecret: PropTypes.string,
|
||||
sessionId: PropTypes.string,
|
||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
||||
idSid: PropTypes.string,
|
||||
customHsUrl: PropTypes.string,
|
||||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
referrer: PropTypes.string,
|
||||
teamServerConfig: PropTypes.shape({
|
||||
// Email address to request new teams
|
||||
supportEmail: React.PropTypes.string.isRequired,
|
||||
supportEmail: PropTypes.string.isRequired,
|
||||
// URL of the riot-team-server to get team configurations and track referrals
|
||||
teamServerURL: React.PropTypes.string.isRequired,
|
||||
teamServerURL: PropTypes.string.isRequired,
|
||||
}),
|
||||
teamSelected: React.PropTypes.object,
|
||||
teamSelected: PropTypes.object,
|
||||
|
||||
defaultDeviceDisplayName: React.PropTypes.string,
|
||||
defaultDeviceDisplayName: PropTypes.string,
|
||||
|
||||
// registration shouldn't know or care how login is done.
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
onCancelClick: React.PropTypes.func,
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
@ -130,6 +132,7 @@ module.exports = React.createClass({
|
||||
if (config.isUrl !== undefined) {
|
||||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
this._replaceClient();
|
||||
});
|
||||
|
@ -15,6 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import AvatarLogic from '../../../Avatar';
|
||||
import sdk from '../../../index';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
@ -23,16 +25,20 @@ module.exports = React.createClass({
|
||||
displayName: 'BaseAvatar',
|
||||
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: React.PropTypes.string, // ID for generating hash colours
|
||||
title: React.PropTypes.string, // onHover title text
|
||||
url: React.PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: React.PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
name: PropTypes.string.isRequired, // The name (first initial used as default)
|
||||
idName: PropTypes.string, // ID for generating hash colours
|
||||
title: PropTypes.string, // onHover title text
|
||||
url: PropTypes.string, // highest priority of them all, shortcut to set in urls[0]
|
||||
urls: PropTypes.array, // [highest_priority, ... , lowest_priority]
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
// XXX resizeMethod not actually used.
|
||||
resizeMethod: React.PropTypes.string,
|
||||
defaultToInitialLetter: React.PropTypes.bool, // true to add default url
|
||||
resizeMethod: PropTypes.string,
|
||||
defaultToInitialLetter: PropTypes.bool, // true to add default url
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -48,6 +54,16 @@ module.exports = React.createClass({
|
||||
return this._getState(this.props);
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this.unmounted = false;
|
||||
this.context.matrixClient.on('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// work out if we need to call setState (if the image URLs array has changed)
|
||||
const newState = this._getState(nextProps);
|
||||
@ -66,6 +82,23 @@ module.exports = React.createClass({
|
||||
}
|
||||
},
|
||||
|
||||
onClientSync(syncState, prevState) {
|
||||
if (this.unmounted) return;
|
||||
|
||||
// Consider the client reconnected if there is no error with syncing.
|
||||
// This means the state could be RECONNECTING, SYNCING or PREPARED.
|
||||
const reconnected = syncState !== "ERROR" && prevState !== syncState;
|
||||
if (reconnected &&
|
||||
// Did we fall back?
|
||||
this.state.urlsIndex > 0
|
||||
) {
|
||||
// Start from the highest priority URL again
|
||||
this.setState({
|
||||
urlsIndex: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_getState: function(props) {
|
||||
// work out the full set of urls to try to load. This is formed like so:
|
||||
// imageUrls: [ props.url, props.urls, default image ]
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const Avatar = require('../../../Avatar');
|
||||
const sdk = require("../../../index");
|
||||
const dispatcher = require("../../../dispatcher");
|
||||
@ -25,15 +26,15 @@ module.exports = React.createClass({
|
||||
displayName: 'MemberAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
member: PropTypes.object.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
// The onClick to give the avatar
|
||||
onClick: React.PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
// Whether the onClick of the avatar should be overriden to dispatch 'view_user'
|
||||
viewUserOnClick: React.PropTypes.bool,
|
||||
title: React.PropTypes.string,
|
||||
viewUserOnClick: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -17,6 +17,7 @@
|
||||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
@ -30,10 +31,10 @@ module.exports = React.createClass({
|
||||
displayName: 'MemberPresenceAvatar',
|
||||
|
||||
propTypes: {
|
||||
member: React.PropTypes.object.isRequired,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
member: PropTypes.object.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import {ContentRepo} from "matrix-js-sdk";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import sdk from "../../../index";
|
||||
@ -25,11 +26,11 @@ module.exports = React.createClass({
|
||||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
propTypes: {
|
||||
room: React.PropTypes.object,
|
||||
oobData: React.PropTypes.object,
|
||||
width: React.PropTypes.number,
|
||||
height: React.PropTypes.number,
|
||||
resizeMethod: React.PropTypes.string,
|
||||
room: PropTypes.object,
|
||||
oobData: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
resizeMethod: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -17,11 +17,12 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoomButton',
|
||||
propTypes: {
|
||||
onCreateRoom: React.PropTypes.func,
|
||||
onCreateRoom: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
const Presets = {
|
||||
@ -28,8 +29,8 @@ const Presets = {
|
||||
module.exports = React.createClass({
|
||||
displayName: 'CreateRoomPresets',
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
preset: React.PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
preset: PropTypes.string,
|
||||
},
|
||||
|
||||
Presets: Presets,
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
@ -22,9 +23,9 @@ module.exports = React.createClass({
|
||||
propTypes: {
|
||||
// Specifying a homeserver will make magical things happen when you,
|
||||
// e.g. start typing in the room alias box.
|
||||
homeserver: React.PropTypes.string,
|
||||
alias: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
homeserver: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -20,7 +20,6 @@ import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Promise from 'bluebird';
|
||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
||||
import GroupStoreCache from '../../../stores/GroupStoreCache';
|
||||
@ -507,7 +506,8 @@ module.exports = React.createClass({
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
@ -580,14 +580,8 @@ module.exports = React.createClass({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}>
|
||||
<div className="mx_Dialog_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>
|
||||
<BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
|
||||
onFinished={this.props.onFinished} title={this.props.title}>
|
||||
<div className="mx_ChatInviteDialog_label">
|
||||
<label htmlFor="textinput">{ this.props.description }</label>
|
||||
</div>
|
||||
@ -597,12 +591,10 @@ module.exports = React.createClass({
|
||||
{ addressSelector }
|
||||
{ this.props.extraNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.onButtonClick}>
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onButtonClick}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
@ -15,10 +15,14 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import { KeyCode } from '../../../Keyboard';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
@ -31,23 +35,43 @@ export default React.createClass({
|
||||
|
||||
propTypes: {
|
||||
// onFinished callback to call when Escape is pressed
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
// callback to call when Enter is pressed
|
||||
onEnterPressed: React.PropTypes.func,
|
||||
onEnterPressed: PropTypes.func,
|
||||
|
||||
// called when a key is pressed
|
||||
onKeyDown: PropTypes.func,
|
||||
|
||||
// CSS class to apply to dialog div
|
||||
className: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
|
||||
// Title for the dialog.
|
||||
// (could probably actually be something more complicated than a string if desired)
|
||||
title: React.PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
|
||||
// children should be the content of the dialog
|
||||
children: React.PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext: function() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
},
|
||||
|
||||
_onKeyDown: function(e) {
|
||||
if (this.props.onKeyDown) {
|
||||
this.props.onKeyDown(e);
|
||||
}
|
||||
if (e.keyCode === KeyCode.ESCAPE) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@ -75,7 +99,7 @@ export default React.createClass({
|
||||
>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>
|
||||
<div className='mx_Dialog_title'>
|
||||
<div className={'mx_Dialog_title ' + this.props.titleClass}>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
@ -137,6 +138,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
} else {
|
||||
// Show the avatar, name and a button to confirm that a new chat is requested
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
title = _t('Start chatting');
|
||||
|
||||
@ -166,11 +168,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
</p>
|
||||
{ profile }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this.props.onNewDMClick}>
|
||||
{ _t('Start Chatting') }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@ -187,9 +186,9 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
||||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onNewDMClick: React.PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
onNewDMClick: PropTypes.func.isRequired,
|
||||
onExistingRoomSelected: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -15,10 +15,10 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
import { GroupMemberType } from '../../../groups';
|
||||
|
||||
/*
|
||||
@ -33,20 +33,20 @@ export default React.createClass({
|
||||
displayName: 'ConfirmUserActionDialog',
|
||||
propTypes: {
|
||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||
member: React.PropTypes.object,
|
||||
member: PropTypes.object,
|
||||
// group member object. Supply either this or 'member'
|
||||
groupMember: GroupMemberType,
|
||||
// needed if a group member is specified
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient),
|
||||
action: React.PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: React.PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
action: PropTypes.string.isRequired, // eg. 'Ban'
|
||||
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
|
||||
|
||||
// Whether to display a text field for a reason
|
||||
// If true, the second argument to onFinished will
|
||||
// be the string entered.
|
||||
askReason: React.PropTypes.bool,
|
||||
danger: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
askReason: PropTypes.bool,
|
||||
danger: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
defaultProps: {
|
||||
@ -76,13 +76,11 @@ export default React.createClass({
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
||||
|
||||
const confirmButtonClass = classnames({
|
||||
'mx_Dialog_primary': true,
|
||||
'danger': this.props.danger,
|
||||
});
|
||||
const confirmButtonClass = this.props.danger ? 'danger' : '';
|
||||
|
||||
let reasonBox;
|
||||
if (this.props.askReason) {
|
||||
@ -127,17 +125,11 @@ export default React.createClass({
|
||||
<div className="mx_ConfirmUserActionDialog_userId">{ userId }</div>
|
||||
</div>
|
||||
{ reasonBox }
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={confirmButtonClass}
|
||||
onClick={this.onOk} autoFocus={!this.props.askReason}
|
||||
>
|
||||
{ this.props.action }
|
||||
</button>
|
||||
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.action}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={confirmButtonClass}
|
||||
focus={!this.props.askReason}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
@ -55,11 +55,15 @@ export default React.createClass({
|
||||
|
||||
_checkGroupId: function(e) {
|
||||
let error = null;
|
||||
if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot not be empty.");
|
||||
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
this.setState({
|
||||
groupIdError: error,
|
||||
// Reset createError to get rid of now stale error message
|
||||
createError: null,
|
||||
});
|
||||
return error;
|
||||
},
|
||||
@ -159,10 +163,10 @@ export default React.createClass({
|
||||
{ createErrorNode }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
<button onClick={this._onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<input type="submit" value={_t('Create')} className="mx_Dialog_primary" />
|
||||
</div>
|
||||
</form>
|
||||
</BaseDialog>
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { _t } from '../../../languageHandler';
|
||||
@ -22,7 +23,7 @@ import { _t } from '../../../languageHandler';
|
||||
export default React.createClass({
|
||||
displayName: 'CreateRoomDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@ -41,6 +42,7 @@ export default React.createClass({
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
@ -67,14 +69,9 @@ export default React.createClass({
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t('Cancel') }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{ _t('Create Room') }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Create Room')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
@ -77,6 +78,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
let passwordBoxClass = '';
|
||||
|
||||
@ -99,10 +101,11 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DeactivateAccountDialog">
|
||||
<div className="mx_Dialog_title danger">
|
||||
{ _t("Deactivate Account") }
|
||||
</div>
|
||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||
onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
titleClass="danger"
|
||||
title={_t("Deactivate Account")}>
|
||||
<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>
|
||||
|
||||
@ -130,11 +133,11 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeactivateAccountDialog.propTypes = {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
@ -71,7 +72,7 @@ export default function DeviceVerifyDialog(props) {
|
||||
}
|
||||
|
||||
DeviceVerifyDialog.propTypes = {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -26,20 +26,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'ErrorDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
@ -27,22 +28,22 @@ export default React.createClass({
|
||||
|
||||
propTypes: {
|
||||
// matrix client to use for UI auth requests
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
authData: PropTypes.shape({
|
||||
flows: PropTypes.array,
|
||||
params: PropTypes.object,
|
||||
session: PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
makeRequest: PropTypes.func.isRequired,
|
||||
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
||||
title: React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import Modal from '../../../Modal';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
@ -30,10 +31,10 @@ import { _t, _td } from '../../../languageHandler';
|
||||
*/
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
matrixClient: React.PropTypes.object.isRequired,
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
deviceId: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
matrixClient: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
deviceId: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -16,20 +16,20 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'QuestionDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.node,
|
||||
extraButtons: React.PropTypes.node,
|
||||
button: React.PropTypes.string,
|
||||
danger: React.PropTypes.bool,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
extraButtons: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
danger: PropTypes.bool,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -53,15 +53,11 @@ export default React.createClass({
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const cancelButton = this.props.hasCancelButton ? (
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
) : null;
|
||||
const buttonClasses = classnames({
|
||||
mx_Dialog_primary: true,
|
||||
danger: this.props.danger,
|
||||
});
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
let primaryButtonClass = "";
|
||||
if (this.props.danger) {
|
||||
primaryButtonClass = "danger";
|
||||
}
|
||||
return (
|
||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
@ -70,13 +66,14 @@ export default React.createClass({
|
||||
<div className="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className={buttonClasses} onClick={this.onOk} autoFocus={this.props.focus}>
|
||||
{ this.props.button || _t('OK') }
|
||||
</button>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
>
|
||||
{ this.props.extraButtons }
|
||||
{ cancelButton }
|
||||
</div>
|
||||
</DialogButtons>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
@ -25,8 +26,8 @@ export default React.createClass({
|
||||
displayName: 'SessionRestoreErrorDialog',
|
||||
|
||||
propTypes: {
|
||||
error: React.PropTypes.string.isRequired,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
_sendBugReport: function() {
|
||||
@ -40,6 +41,7 @@ export default React.createClass({
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
let bugreport;
|
||||
|
||||
if (SdkConfig.get().bug_report_endpoint_url) {
|
||||
@ -68,11 +70,9 @@ export default React.createClass({
|
||||
|
||||
{ bugreport }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button className="mx_Dialog_primary" onClick={this._continueClicked}>
|
||||
{ _t("Continue anyway") }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Continue anyway")}
|
||||
onPrimaryButtonClick={this._continueClicked}
|
||||
onCancel={this.props.onFinished} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Email from '../../../email';
|
||||
import AddThreepid from '../../../AddThreepid';
|
||||
@ -30,7 +31,7 @@ import Modal from '../../../Modal';
|
||||
export default React.createClass({
|
||||
displayName: 'SetEmailDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import classnames from 'classnames';
|
||||
@ -35,11 +36,11 @@ const USERNAME_CHECK_DEBOUNCE_MS = 250;
|
||||
export default React.createClass({
|
||||
displayName: 'SetMxIdDialog',
|
||||
propTypes: {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
// Called when the user requests to register with a different homeserver
|
||||
onDifferentServerClicked: React.PropTypes.func.isRequired,
|
||||
onDifferentServerClicked: PropTypes.func.isRequired,
|
||||
// Called if the user wants to switch to login instead
|
||||
onLoginClick: React.PropTypes.func.isRequired,
|
||||
onLoginClick: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -15,21 +15,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'TextInputDialog',
|
||||
propTypes: {
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.oneOfType([
|
||||
React.PropTypes.element,
|
||||
React.PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.oneOfType([
|
||||
PropTypes.element,
|
||||
PropTypes.string,
|
||||
]),
|
||||
value: React.PropTypes.string,
|
||||
button: React.PropTypes.string,
|
||||
focus: React.PropTypes.bool,
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -58,6 +58,7 @@ export default React.createClass({
|
||||
|
||||
render: function() {
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className="mx_TextInputDialog" onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
@ -71,14 +72,9 @@ export default React.createClass({
|
||||
<input id="textinput" ref="textinput" className="mx_TextInputDialog_input" defaultValue={this.props.value} autoFocus={this.props.focus} size="64" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
<button onClick={this.onCancel}>
|
||||
{ _t("Cancel") }
|
||||
</button>
|
||||
<button className="mx_Dialog_primary" onClick={this.onOk}>
|
||||
{ this.props.button }
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
onCancel={this.onCancel} />
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
|
@ -23,14 +23,7 @@ import GeminiScrollbar from 'react-gemini-scrollbar';
|
||||
import Resend from '../../../Resend';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
function markAllDevicesKnown(devices) {
|
||||
Object.keys(devices).forEach((userId) => {
|
||||
Object.keys(devices[userId]).map((deviceId) => {
|
||||
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
import { markAllDevicesKnown } from '../../../cryptodevices';
|
||||
|
||||
function DeviceListEntry(props) {
|
||||
const {userId, device} = props;
|
||||
@ -141,7 +134,7 @@ export default React.createClass({
|
||||
},
|
||||
|
||||
_onSendAnywayClicked: function() {
|
||||
markAllDevicesKnown(this.props.devices);
|
||||
markAllDevicesKnown(MatrixClientPeg.get(), this.props.devices);
|
||||
|
||||
this.props.onFinished();
|
||||
this.props.onSend();
|
||||
@ -187,18 +180,11 @@ export default React.createClass({
|
||||
}
|
||||
});
|
||||
});
|
||||
let sendButton;
|
||||
if (haveUnknownDevices) {
|
||||
sendButton = <button onClick={this._onSendAnywayClicked}>
|
||||
{ this.props.sendAnywayLabel }
|
||||
</button>;
|
||||
} else {
|
||||
sendButton = <button onClick={this._onSendClicked}>
|
||||
{ this.props.sendLabel }
|
||||
</button>;
|
||||
}
|
||||
const sendButtonOnClick = haveUnknownDevices ? this._onSendAnywayClicked : this._onSendClicked;
|
||||
const sendButtonLabel = haveUnknownDevices ? this.props.sendAnywayLabel : this.props.sendAnywayLabel;
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
return (
|
||||
<BaseDialog className='mx_UnknownDeviceDialog'
|
||||
onFinished={this.props.onFinished}
|
||||
@ -213,14 +199,9 @@ export default React.createClass({
|
||||
|
||||
<UnknownDeviceList devices={this.props.devices} />
|
||||
</GeminiScrollbar>
|
||||
<div className="mx_Dialog_buttons">
|
||||
{sendButton}
|
||||
<button className="mx_Dialog_primary" autoFocus={true}
|
||||
onClick={this._onDismissClicked}
|
||||
>
|
||||
{_t("Dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
<DialogButtons primaryButton={sendButtonLabel}
|
||||
onPrimaryButtonClick={sendButtonOnClick}
|
||||
onCancel={this._onDismissClicked} />
|
||||
</BaseDialog>
|
||||
);
|
||||
// XXX: do we want to give the user the option to enable blacklistUnverifiedDevices for this room (or globally) at this point?
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* AccessibleButton is a generic wrapper for any element that should be treated
|
||||
@ -44,9 +45,9 @@ export default function AccessibleButton(props) {
|
||||
* implemented exactly like a normal onClick handler.
|
||||
*/
|
||||
AccessibleButton.propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
element: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
element: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AccessibleButton.defaultProps = {
|
||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import classNames from 'classnames';
|
||||
import { UserAddressType } from '../../../UserAddress';
|
||||
@ -26,17 +27,17 @@ export default React.createClass({
|
||||
displayName: 'AddressSelector',
|
||||
|
||||
propTypes: {
|
||||
onSelected: React.PropTypes.func.isRequired,
|
||||
onSelected: PropTypes.func.isRequired,
|
||||
|
||||
// List of the addresses to display
|
||||
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
addressList: PropTypes.arrayOf(UserAddressType).isRequired,
|
||||
// Whether to show the address on the address tiles
|
||||
showAddress: React.PropTypes.bool,
|
||||
truncateAt: React.PropTypes.number.isRequired,
|
||||
selected: React.PropTypes.number,
|
||||
showAddress: PropTypes.bool,
|
||||
truncateAt: PropTypes.number.isRequired,
|
||||
selected: PropTypes.number,
|
||||
|
||||
// Element to put as a header on top of the list
|
||||
header: React.PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import sdk from "../../../index";
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
@ -28,9 +29,9 @@ export default React.createClass({
|
||||
|
||||
propTypes: {
|
||||
address: UserAddressType.isRequired,
|
||||
canDismiss: React.PropTypes.bool,
|
||||
onDismissed: React.PropTypes.func,
|
||||
justified: React.PropTypes.bool,
|
||||
canDismiss: PropTypes.bool,
|
||||
onDismissed: PropTypes.func,
|
||||
justified: PropTypes.bool,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||
import url from 'url';
|
||||
import qs from 'querystring';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
@ -463,6 +464,10 @@ export default class AppTile extends React.Component {
|
||||
const sandboxFlags = "allow-forms allow-popups allow-popups-to-escape-sandbox "+
|
||||
"allow-same-origin allow-scripts allow-presentation";
|
||||
|
||||
// Additional iframe feature pemissions
|
||||
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||
const iframeFeatures = "microphone; camera; encrypted-media;";
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className='mx_AppTileBody mx_AppLoading'>
|
||||
@ -482,7 +487,13 @@ export default class AppTile extends React.Component {
|
||||
appTileBody = (
|
||||
<div className={this.state.loading ? 'mx_AppTileBody mx_AppLoading' : 'mx_AppTileBody'}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
"allow" attribute, which is unknown to react 15.
|
||||
*/ }
|
||||
<iframe
|
||||
is
|
||||
allow={iframeFeatures}
|
||||
ref="appFrame"
|
||||
src={this._getSafeUrl()}
|
||||
allowFullScreen="true"
|
||||
|
@ -15,71 +15,30 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
|
||||
import TagTile from './TagTile';
|
||||
import dis from '../../../dispatcher';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
|
||||
const tagTileSource = {
|
||||
canDrag: function(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
beginDrag: function(props) {
|
||||
// Return the data describing the dragged item
|
||||
return {
|
||||
tag: props.groupProfile.groupId,
|
||||
};
|
||||
},
|
||||
|
||||
endDrag: function(props, monitor, component) {
|
||||
const dropResult = monitor.getDropResult();
|
||||
if (!monitor.didDrop() || !dropResult) {
|
||||
return;
|
||||
}
|
||||
props.onEndDrag();
|
||||
},
|
||||
};
|
||||
|
||||
const tagTileTarget = {
|
||||
canDrop(props, monitor) {
|
||||
return true;
|
||||
},
|
||||
|
||||
hover(props, monitor, component) {
|
||||
if (!monitor.canDrop()) return;
|
||||
const draggedY = monitor.getClientOffset().y;
|
||||
const {top, bottom} = findDOMNode(component).getBoundingClientRect();
|
||||
const targetY = (top + bottom) / 2;
|
||||
dis.dispatch({
|
||||
action: 'order_tag',
|
||||
tag: monitor.getItem().tag,
|
||||
targetTag: props.groupProfile.groupId,
|
||||
// Note: we indicate that the tag should be after the target when
|
||||
// it's being dragged over the top half of the target.
|
||||
after: draggedY < targetY,
|
||||
});
|
||||
},
|
||||
|
||||
drop(props) {
|
||||
// Return the data to be returned by getDropResult
|
||||
return {
|
||||
tag: props.groupProfile.groupId,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default
|
||||
DropTarget('TagTile', tagTileTarget, (connect, monitor) => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}))(DragSource('TagTile', tagTileSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
}))((props) => {
|
||||
const { connectDropTarget, connectDragSource, ...otherProps } = props;
|
||||
return connectDropTarget(connectDragSource(
|
||||
export default function DNDTagTile(props) {
|
||||
return <div>
|
||||
<Draggable
|
||||
key={props.tag}
|
||||
draggableId={props.tag}
|
||||
index={props.index}
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div>
|
||||
<TagTile {...otherProps} />
|
||||
</div>,
|
||||
));
|
||||
}));
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<TagTile {...props} />
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Draggable>
|
||||
</div>;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
@ -24,8 +25,8 @@ export default React.createClass({
|
||||
displayName: 'DeviceVerifyButtons',
|
||||
|
||||
propTypes: {
|
||||
userId: React.PropTypes.string.isRequired,
|
||||
device: React.PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
device: PropTypes.object.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
62
src/components/views/elements/DialogButtons.js
Normal file
62
src/components/views/elements/DialogButtons.js
Normal 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>
|
||||
);
|
||||
},
|
||||
});
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class DirectorySearchBox extends React.Component {
|
||||
@ -105,10 +106,10 @@ export default class DirectorySearchBox extends React.Component {
|
||||
}
|
||||
|
||||
DirectorySearchBox.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
onClear: React.PropTypes.func,
|
||||
onJoinClick: React.PropTypes.func,
|
||||
placeholder: React.PropTypes.string,
|
||||
showJoinButton: React.PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onClear: PropTypes.func,
|
||||
onJoinClick: PropTypes.func,
|
||||
placeholder: PropTypes.string,
|
||||
showJoinButton: PropTypes.bool,
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import AccessibleButton from './AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
@ -56,14 +57,14 @@ class MenuOption extends React.Component {
|
||||
}
|
||||
|
||||
MenuOption.propTypes = {
|
||||
children: React.PropTypes.oneOfType([
|
||||
React.PropTypes.arrayOf(React.PropTypes.node),
|
||||
React.PropTypes.node,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(React.PropTypes.node),
|
||||
PropTypes.node,
|
||||
]),
|
||||
highlighted: React.PropTypes.bool,
|
||||
dropdownKey: React.PropTypes.string,
|
||||
onClick: React.PropTypes.func.isRequired,
|
||||
onMouseEnter: React.PropTypes.func.isRequired,
|
||||
highlighted: PropTypes.bool,
|
||||
dropdownKey: PropTypes.string,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/*
|
||||
@ -322,20 +323,20 @@ Dropdown.propTypes = {
|
||||
// The width that the dropdown should be. If specified,
|
||||
// the dropped-down part of the menu will be set to this
|
||||
// width.
|
||||
menuWidth: React.PropTypes.number,
|
||||
menuWidth: PropTypes.number,
|
||||
// Called when the selected option changes
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
// Called when the value of the search field changes
|
||||
onSearchChange: React.PropTypes.func,
|
||||
searchEnabled: React.PropTypes.bool,
|
||||
onSearchChange: PropTypes.func,
|
||||
searchEnabled: PropTypes.bool,
|
||||
// Function that, given the key of an option, returns
|
||||
// a node representing that option to be displayed in the
|
||||
// box itself as the currently-selected option (ie. as
|
||||
// opposed to in the actual dropped-down part). If
|
||||
// unspecified, the appropriate child element is used as
|
||||
// in the dropped-down menu.
|
||||
getShortOption: React.PropTypes.func,
|
||||
value: React.PropTypes.string,
|
||||
getShortOption: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
// negative for consistency with HTML
|
||||
disabled: React.PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
@ -26,18 +27,18 @@ module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
|
||||
propTypes: {
|
||||
onValueChanged: React.PropTypes.func,
|
||||
initialValue: React.PropTypes.string,
|
||||
label: React.PropTypes.string,
|
||||
placeholder: React.PropTypes.string,
|
||||
className: React.PropTypes.string,
|
||||
labelClassName: React.PropTypes.string,
|
||||
placeholderClassName: React.PropTypes.string,
|
||||
onValueChanged: PropTypes.func,
|
||||
initialValue: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
labelClassName: PropTypes.string,
|
||||
placeholderClassName: PropTypes.string,
|
||||
// Overrides blurToSubmit if true
|
||||
blurToCancel: React.PropTypes.bool,
|
||||
blurToCancel: PropTypes.bool,
|
||||
// Will cause onValueChanged(value, true) to fire on blur
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
editable: React.PropTypes.bool,
|
||||
blurToSubmit: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
},
|
||||
|
||||
Phases: {
|
||||
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
@ -126,21 +127,21 @@ export default class EditableTextContainer extends React.Component {
|
||||
|
||||
EditableTextContainer.propTypes = {
|
||||
/* callback to retrieve the initial value. */
|
||||
getInitialValue: React.PropTypes.func,
|
||||
getInitialValue: PropTypes.func,
|
||||
|
||||
/* initial value; used if getInitialValue is not given */
|
||||
initialValue: React.PropTypes.string,
|
||||
initialValue: PropTypes.string,
|
||||
|
||||
/* placeholder text to use when the value is empty (and not being
|
||||
* edited) */
|
||||
placeholder: React.PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
|
||||
/* callback to update the value. Called with a single argument: the new
|
||||
* value. */
|
||||
onSubmit: React.PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
|
||||
/* should the input submit when focus is lost? */
|
||||
blurToSubmit: React.PropTypes.bool,
|
||||
blurToSubmit: PropTypes.bool,
|
||||
};
|
||||
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {emojifyText, containsEmoji} from '../../../HtmlUtils';
|
||||
|
||||
export default function EmojiText(props) {
|
||||
@ -32,8 +33,8 @@ export default function EmojiText(props) {
|
||||
}
|
||||
|
||||
EmojiText.propTypes = {
|
||||
element: React.PropTypes.string,
|
||||
children: React.PropTypes.string.isRequired,
|
||||
element: PropTypes.string,
|
||||
children: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
EmojiText.defaultProps = {
|
||||
|
@ -63,7 +63,7 @@ FlairAvatar.propTypes = {
|
||||
};
|
||||
|
||||
FlairAvatar.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
||||
export default class Flair extends React.Component {
|
||||
@ -107,7 +107,11 @@ export default class Flair extends React.Component {
|
||||
}
|
||||
const profiles = await this._getGroupProfiles(groups);
|
||||
if (!this.unmounted) {
|
||||
this.setState({profiles: profiles.filter((profile) => {return profile.avatarUrl;})});
|
||||
this.setState({
|
||||
profiles: profiles.filter((profile) => {
|
||||
return profile ? profile.avatarUrl : false;
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,5 +138,5 @@ Flair.propTypes = {
|
||||
// this.context.matrixClient everywhere instead of this.props.matrixClient.
|
||||
// See https://github.com/vector-im/riot-web/issues/4951.
|
||||
Flair.contextTypes = {
|
||||
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
@ -114,7 +115,7 @@ export default class LanguageDropdown extends React.Component {
|
||||
}
|
||||
|
||||
LanguageDropdown.propTypes = {
|
||||
className: React.PropTypes.string,
|
||||
onOptionChange: React.PropTypes.func.isRequired,
|
||||
value: React.PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onOptionChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
const MemberAvatar = require('../avatars/MemberAvatar.js');
|
||||
import { _t } from '../../../languageHandler';
|
||||
@ -23,19 +24,19 @@ module.exports = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
// An array of member events to summarise
|
||||
events: React.PropTypes.array.isRequired,
|
||||
events: PropTypes.array.isRequired,
|
||||
// An array of EventTiles to render when expanded
|
||||
children: React.PropTypes.array.isRequired,
|
||||
children: PropTypes.array.isRequired,
|
||||
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
|
||||
summaryLength: React.PropTypes.number,
|
||||
summaryLength: PropTypes.number,
|
||||
// The maximum number of avatars to display in the summary
|
||||
avatarsMaxLength: React.PropTypes.number,
|
||||
avatarsMaxLength: PropTypes.number,
|
||||
// The minimum number of events needed to trigger summarisation
|
||||
threshold: React.PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
// Called when the MELS expansion is toggled
|
||||
onToggle: React.PropTypes.func,
|
||||
onToggle: PropTypes.func,
|
||||
// Whether or not to begin with state.expanded=true
|
||||
startExpanded: React.PropTypes.bool,
|
||||
startExpanded: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -17,7 +17,7 @@ import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||
@ -61,6 +61,17 @@ const Pill = React.createClass({
|
||||
shouldShowPillAvatar: PropTypes.bool,
|
||||
},
|
||||
|
||||
|
||||
childContextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
matrixClient: this._matrixClient,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
// ID/alias of the room/user
|
||||
@ -135,6 +146,7 @@ const Pill = React.createClass({
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
this._matrixClient = MatrixClientPeg.get();
|
||||
this.componentWillReceiveProps(this.props);
|
||||
},
|
||||
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Roles from '../../../Roles';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
@ -24,23 +25,23 @@ module.exports = React.createClass({
|
||||
displayName: 'PowerSelector',
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired,
|
||||
// The maximum value that can be set with the power selector
|
||||
maxValue: React.PropTypes.number.isRequired,
|
||||
maxValue: PropTypes.number.isRequired,
|
||||
|
||||
// Default user power level for the room
|
||||
usersDefault: React.PropTypes.number.isRequired,
|
||||
usersDefault: PropTypes.number.isRequired,
|
||||
|
||||
// if true, the <select/> should be a 'controlled' form element and updated by React
|
||||
// to reflect the current value, rather than left freeform.
|
||||
// MemberInfo uses controlled; RoomSettings uses non-controlled.
|
||||
//
|
||||
// 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.
|
||||
disabled: React.PropTypes.bool,
|
||||
onChange: React.PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -17,12 +17,13 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ProgressBar',
|
||||
propTypes: {
|
||||
value: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
value: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
188
src/components/views/elements/Quote.js
Normal file
188
src/components/views/elements/Quote.js
Normal 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;
|
||||
}
|
||||
}
|
@ -15,23 +15,24 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'SettingsFlag',
|
||||
propTypes: {
|
||||
name: React.PropTypes.string.isRequired,
|
||||
level: React.PropTypes.string.isRequired,
|
||||
roomId: React.PropTypes.string, // for per-room settings
|
||||
label: React.PropTypes.string, // untranslated
|
||||
onChange: React.PropTypes.func,
|
||||
isExplicit: React.PropTypes.bool,
|
||||
manualSave: React.PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
level: PropTypes.string.isRequired,
|
||||
roomId: PropTypes.string, // for per-room settings
|
||||
label: PropTypes.string, // untranslated
|
||||
onChange: PropTypes.func,
|
||||
isExplicit: PropTypes.bool,
|
||||
manualSave: PropTypes.bool,
|
||||
|
||||
// If group is supplied, then this will create a radio button instead.
|
||||
group: React.PropTypes.string,
|
||||
value: React.PropTypes.any, // the value for the radio button
|
||||
group: PropTypes.string,
|
||||
value: PropTypes.any, // the value for the radio button
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -20,36 +20,97 @@ import classNames from 'classnames';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard';
|
||||
import ContextualMenu from '../../structures/ContextualMenu';
|
||||
|
||||
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: {
|
||||
groupProfile: PropTypes.object,
|
||||
// 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: React.PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
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.groupProfile.groupId,
|
||||
ctrlOrCmdKey: isOnlyCtrlOrCmdKeyEvent(e),
|
||||
tag: this.props.tag,
|
||||
ctrlOrCmdKey: isOnlyCtrlOrCmdIgnoreShiftKeyEvent(e),
|
||||
shiftKey: e.shiftKey,
|
||||
});
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Hide the (...) immediately
|
||||
this.setState({ hover: false });
|
||||
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
const self = this;
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
this.setState({hover: true});
|
||||
},
|
||||
@ -62,8 +123,8 @@ export default React.createClass({
|
||||
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 profile = this.state.profile || {};
|
||||
const name = profile.name || this.props.tag;
|
||||
const avatarHeight = 35;
|
||||
|
||||
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
|
||||
@ -78,10 +139,15 @@ export default React.createClass({
|
||||
const tip = this.state.hover ?
|
||||
<RoomTooltip className="mx_TagTile_tooltip" label={name} /> :
|
||||
<div />;
|
||||
const contextButton = this.state.hover || this.state.menuDisplayed ?
|
||||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <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 }
|
||||
{ contextButton }
|
||||
</div>
|
||||
</AccessibleButton>;
|
||||
},
|
||||
|
@ -18,16 +18,17 @@ limitations under the License.
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
const Tinter = require("../../../Tinter");
|
||||
|
||||
var TintableSvg = React.createClass({
|
||||
displayName: 'TintableSvg',
|
||||
|
||||
propTypes: {
|
||||
src: React.PropTypes.string.isRequired,
|
||||
width: React.PropTypes.string.isRequired,
|
||||
height: React.PropTypes.string.isRequired,
|
||||
className: React.PropTypes.string,
|
||||
src: PropTypes.string.isRequired,
|
||||
width: PropTypes.string.isRequired,
|
||||
height: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
},
|
||||
|
||||
statics: {
|
||||
|
@ -17,14 +17,15 @@ limitations under the License.
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'UserSelector',
|
||||
|
||||
propTypes: {
|
||||
onChange: React.PropTypes.func,
|
||||
selected_users: React.PropTypes.arrayOf(React.PropTypes.string),
|
||||
onChange: PropTypes.func,
|
||||
selected_users: PropTypes.arrayOf(React.PropTypes.string),
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -132,7 +132,9 @@ module.exports = React.createClass({
|
||||
render: function() {
|
||||
if (this.state.removingUser) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
return <div className="mx_MemberInfo">
|
||||
<Spinner />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let adminTools;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user