Merge remote-tracking branch 'origin/develop' into matthew/e2e_backups
This commit is contained in:
commit
8fd7c4a66b
231 changed files with 13885 additions and 11701 deletions
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -15,12 +16,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -61,6 +60,54 @@ export default class ContextualMenu extends React.Component {
|
|||
// If true, insert an invisible screen-sized element behind the
|
||||
// menu that when clicked will close it.
|
||||
hasBackground: PropTypes.bool,
|
||||
|
||||
// The component to render as the context menu
|
||||
elementClass: PropTypes.element.isRequired,
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
// method to close menu
|
||||
closeMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
contextMenuRect: null,
|
||||
};
|
||||
|
||||
this.onContextMenu = this.onContextMenu.bind(this);
|
||||
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
|
||||
}
|
||||
|
||||
collectContextMenuRect(element) {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
this.setState({
|
||||
contextMenuRect: element.getBoundingClientRect(),
|
||||
});
|
||||
}
|
||||
|
||||
onContextMenu(e) {
|
||||
if (this.props.closeMenu) {
|
||||
this.props.closeMenu();
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
|
||||
// a context menu and its click-guard are up without completely rewriting how the context menus work.
|
||||
setImmediate(() => {
|
||||
const clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent(
|
||||
'contextmenu', true, true, window, 0,
|
||||
0, 0, x, y, false, false,
|
||||
false, false, 0, null,
|
||||
);
|
||||
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -83,6 +130,9 @@ export default class ContextualMenu extends React.Component {
|
|||
chevronFace = 'right';
|
||||
}
|
||||
|
||||
const contextMenuRect = this.state.contextMenuRect || null;
|
||||
const padding = 10;
|
||||
|
||||
const chevronOffset = {};
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
|
@ -90,7 +140,19 @@ export default class ContextualMenu extends React.Component {
|
|||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
chevronOffset.top = props.chevronOffset;
|
||||
const target = position.top;
|
||||
|
||||
// By default, no adjustment is made
|
||||
let adjusted = target;
|
||||
|
||||
// If we know the dimensions of the context menu, adjust its position
|
||||
// such that it does not leave the (padded) window.
|
||||
if (contextMenuRect) {
|
||||
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
|
||||
}
|
||||
|
||||
position.top = adjusted;
|
||||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||
}
|
||||
|
||||
// To override the default chevron colour, if it's been set
|
||||
|
@ -112,7 +174,7 @@ export default class ContextualMenu extends React.Component {
|
|||
`;
|
||||
}
|
||||
|
||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
|
||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} />;
|
||||
const className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
const menuClasses = classNames({
|
||||
|
@ -154,17 +216,18 @@ export default class ContextualMenu extends React.Component {
|
|||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
return <div className={className} style={position}>
|
||||
<div className={menuClasses} style={menuStyle}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
|
||||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu}></div> }
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background"
|
||||
onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export function createMenu(ElementClass, props) {
|
||||
export function createMenu(ElementClass, props, hasBackground=true) {
|
||||
const closeMenu = function(...args) {
|
||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||
|
||||
|
@ -175,8 +238,8 @@ export function createMenu(ElementClass, props) {
|
|||
|
||||
// We only reference closeMenu once per call to createMenu
|
||||
const menu = <ContextualMenu
|
||||
hasBackground={hasBackground}
|
||||
{...props}
|
||||
hasBackground={true}
|
||||
elementClass={ElementClass}
|
||||
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
|
|
|
@ -68,8 +68,8 @@ const FilePanel = React.createClass({
|
|||
"room": {
|
||||
"timeline": {
|
||||
"contains_url": true,
|
||||
"not_types": [
|
||||
"m.sticker",
|
||||
"types": [
|
||||
"m.room.message",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -432,11 +432,14 @@ export default React.createClass({
|
|||
|
||||
this._changeAvatarComponent = null;
|
||||
this._initGroupStore(this.props.groupId, true);
|
||||
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
|
@ -559,16 +562,33 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onShareClick: function() {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share community dialog', '', ShareDialog, {
|
||||
target: this._matrixClient.getGroup(this.props.groupId),
|
||||
});
|
||||
},
|
||||
|
||||
_onCancelClick: function() {
|
||||
this._closeSettings();
|
||||
},
|
||||
|
||||
_onAction(payload) {
|
||||
switch (payload.action) {
|
||||
// NOTE: close_settings is an app-wide dispatch; as it is dispatched from MatrixChat
|
||||
case 'close_settings':
|
||||
this.setState({
|
||||
editing: false,
|
||||
profileForm: null,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_closeSettings() {
|
||||
this.setState({
|
||||
editing: false,
|
||||
profileForm: null,
|
||||
});
|
||||
dis.dispatch({action: 'panel_disable'});
|
||||
dis.dispatch({action: 'close_settings'});
|
||||
},
|
||||
|
||||
_onNameChange: function(value) {
|
||||
|
@ -1039,7 +1059,7 @@ export default React.createClass({
|
|||
<input type="radio"
|
||||
value={GROUP_JOINPOLICY_INVITE}
|
||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
|
||||
onClick={this._onJoinableChange}
|
||||
onChange={this._onJoinableChange}
|
||||
/>
|
||||
<div className="mx_GroupView_label_text">
|
||||
{ _t('Only people who have been invited') }
|
||||
|
@ -1051,7 +1071,7 @@ export default React.createClass({
|
|||
<input type="radio"
|
||||
value={GROUP_JOINPOLICY_OPEN}
|
||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
|
||||
onClick={this._onJoinableChange}
|
||||
onChange={this._onJoinableChange}
|
||||
/>
|
||||
<div className="mx_GroupView_label_text">
|
||||
{ _t('Everyone') }
|
||||
|
@ -1114,10 +1134,6 @@ export default React.createClass({
|
|||
let avatarNode;
|
||||
let nameNode;
|
||||
let shortDescNode;
|
||||
const bodyNodes = [
|
||||
this._getMembershipSection(),
|
||||
this._getGroupSection(),
|
||||
];
|
||||
const rightButtons = [];
|
||||
if (this.state.editing && this.state.isUserPrivileged) {
|
||||
let avatarImage;
|
||||
|
@ -1194,6 +1210,7 @@ export default React.createClass({
|
|||
shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.editing) {
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||
|
@ -1218,6 +1235,11 @@ export default React.createClass({
|
|||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button" onClick={this._onShareClick} title={_t('Share Community')} key="_shareButton">
|
||||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
if (this.props.collapsedRhs) {
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button"
|
||||
|
@ -1256,7 +1278,8 @@ export default React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
{ bodyNodes }
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -82,17 +82,26 @@ var LeftPanel = React.createClass({
|
|||
|
||||
_onKeyDown: function(ev) {
|
||||
if (!this.focusedElement) return;
|
||||
let handled = false;
|
||||
let handled = true;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.TAB:
|
||||
this._onMoveFocus(ev.shiftKey);
|
||||
break;
|
||||
case KeyCode.UP:
|
||||
this._onMoveFocus(true);
|
||||
handled = true;
|
||||
break;
|
||||
case KeyCode.DOWN:
|
||||
this._onMoveFocus(false);
|
||||
handled = true;
|
||||
break;
|
||||
case KeyCode.ENTER:
|
||||
this._onMoveFocus(false);
|
||||
if (this.focusedElement) {
|
||||
this.focusedElement.click();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
|
@ -102,37 +111,33 @@ var LeftPanel = React.createClass({
|
|||
},
|
||||
|
||||
_onMoveFocus: function(up) {
|
||||
var element = this.focusedElement;
|
||||
let element = this.focusedElement;
|
||||
|
||||
// unclear why this isn't needed
|
||||
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
|
||||
// this.focusDirection = up;
|
||||
|
||||
var descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
var classes;
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes;
|
||||
|
||||
do {
|
||||
var child = up ? element.lastElementChild : element.firstElementChild;
|
||||
var sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
}
|
||||
else if (sibling) {
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
@ -144,8 +149,7 @@ var LeftPanel = React.createClass({
|
|||
descending = true;
|
||||
}
|
||||
}
|
||||
|
||||
} while(element && !(
|
||||
} while (element && !(
|
||||
classes.contains("mx_RoomTile") ||
|
||||
classes.contains("mx_SearchBox_search") ||
|
||||
classes.contains("mx_RoomSubList_ellipsis")));
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations 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.
|
||||
|
@ -30,10 +30,16 @@ import dis from '../../dispatcher';
|
|||
import sessionStore from '../../stores/SessionStore';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import RoomListStore from "../../stores/RoomListStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
// NB. this is just for server notices rather than pinned messages in general.
|
||||
const MAX_PINNED_NOTICES_PER_ROOM = 2;
|
||||
|
||||
/**
|
||||
* This is what our MatrixChat shows when we are logged in. The precise view is
|
||||
* determined by the page_type property.
|
||||
|
@ -80,6 +86,8 @@ const LoggedInView = React.createClass({
|
|||
return {
|
||||
// use compact timeline view
|
||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||
// any currently active server notice events
|
||||
serverNoticeEvents: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -97,12 +105,18 @@ const LoggedInView = React.createClass({
|
|||
);
|
||||
this._setStateFromSessionStore();
|
||||
|
||||
this._updateServerNoticeEvents();
|
||||
|
||||
this._matrixClient.on("accountData", this.onAccountData);
|
||||
this._matrixClient.on("sync", this.onSync);
|
||||
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||
this._matrixClient.removeListener("sync", this.onSync);
|
||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||
if (this._sessionStoreToken) {
|
||||
this._sessionStoreToken.remove();
|
||||
}
|
||||
|
@ -142,6 +156,56 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSync: function(syncState, oldSyncState, data) {
|
||||
const oldErrCode = this.state.syncErrorData && this.state.syncErrorData.error && this.state.syncErrorData.error.errcode;
|
||||
const newErrCode = data && data.error && data.error.errcode;
|
||||
if (syncState === oldSyncState && oldErrCode === newErrCode) return;
|
||||
|
||||
if (syncState === 'ERROR') {
|
||||
this.setState({
|
||||
syncErrorData: data,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
syncErrorData: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
|
||||
this._updateServerNoticeEvents();
|
||||
}
|
||||
},
|
||||
|
||||
_updateServerNoticeEvents: async function() {
|
||||
const roomLists = RoomListStore.getRoomLists();
|
||||
if (!roomLists['m.server_notice']) return [];
|
||||
|
||||
const pinnedEvents = [];
|
||||
for (const room of roomLists['m.server_notice']) {
|
||||
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
|
||||
|
||||
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
|
||||
|
||||
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
|
||||
for (const eventId of pinnedEventIds) {
|
||||
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
|
||||
const ev = timeline.getEvents().find(ev => ev.getId() === eventId);
|
||||
if (ev) pinnedEvents.push(ev);
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
serverNoticeEvents: pinnedEvents,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
_onKeyDown: function(ev) {
|
||||
/*
|
||||
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
|
||||
|
@ -255,6 +319,22 @@ const LoggedInView = React.createClass({
|
|||
), true);
|
||||
},
|
||||
|
||||
_onClick: function(ev) {
|
||||
// When the panels are disabled, clicking on them results in a mouse event
|
||||
// which bubbles to certain elements in the tree. When this happens, close
|
||||
// any settings page that is currently open (user/room/group).
|
||||
if (this.props.leftDisabled && this.props.rightDisabled) {
|
||||
const targetClasses = new Set(ev.target.className.split(' '));
|
||||
if (
|
||||
targetClasses.has('mx_MatrixChat') ||
|
||||
targetClasses.has('mx_MatrixChat_middlePanel') ||
|
||||
targetClasses.has('mx_RoomView')
|
||||
) {
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
|
@ -270,6 +350,7 @@ const LoggedInView = React.createClass({
|
|||
const NewVersionBar = sdk.getComponent('globals.NewVersionBar');
|
||||
const UpdateCheckBar = sdk.getComponent('globals.UpdateCheckBar');
|
||||
const PasswordNagBar = sdk.getComponent('globals.PasswordNagBar');
|
||||
const ServerLimitBar = sdk.getComponent('globals.ServerLimitBar');
|
||||
|
||||
let page_element;
|
||||
let right_panel = '';
|
||||
|
@ -295,7 +376,7 @@ const LoggedInView = React.createClass({
|
|||
|
||||
case PageTypes.UserSettings:
|
||||
page_element = <UserSettings
|
||||
onClose={this.props.onUserSettingsClose}
|
||||
onClose={this.props.onCloseAllSettings}
|
||||
brand={this.props.config.brand}
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
|
@ -352,9 +433,26 @@ const LoggedInView = React.createClass({
|
|||
break;
|
||||
}
|
||||
|
||||
const usageLimitEvent = this.state.serverNoticeEvents.find((e) => {
|
||||
return (
|
||||
e && e.getType() === 'm.room.message' &&
|
||||
e.getContent()['server_notice_type'] === 'm.server_notice.usage_limit_reached'
|
||||
);
|
||||
});
|
||||
|
||||
let topBar;
|
||||
const isGuest = this.props.matrixClient.isGuest();
|
||||
if (this.props.showCookieBar &&
|
||||
if (this.state.syncErrorData && this.state.syncErrorData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
topBar = <ServerLimitBar kind='hard'
|
||||
adminContact={this.state.syncErrorData.error.data.admin_contact}
|
||||
limitType={this.state.syncErrorData.error.data.limit_type}
|
||||
/>;
|
||||
} else if (usageLimitEvent) {
|
||||
topBar = <ServerLimitBar kind='soft'
|
||||
adminContact={usageLimitEvent.getContent().admin_contact}
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
|
@ -380,7 +478,7 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers}>
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onClick={this._onClick}>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
|
|
|
@ -23,6 +23,7 @@ import PropTypes from 'prop-types';
|
|||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
@ -398,6 +399,9 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
startPageChangeTimer() {
|
||||
// Tor doesn't support performance
|
||||
if (!performance || !performance.mark) return null;
|
||||
|
||||
// This shouldn't happen because componentWillUpdate and componentDidUpdate
|
||||
// are used.
|
||||
if (this._pageChanging) {
|
||||
|
@ -409,6 +413,9 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
stopPageChangeTimer() {
|
||||
// Tor doesn't support performance
|
||||
if (!performance || !performance.mark) return null;
|
||||
|
||||
if (!this._pageChanging) {
|
||||
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
||||
return;
|
||||
|
@ -560,6 +567,27 @@ export default React.createClass({
|
|||
this._setPage(PageTypes.UserSettings);
|
||||
this.notifyNewScreen('settings');
|
||||
break;
|
||||
case 'close_settings':
|
||||
this.setState({
|
||||
leftDisabled: false,
|
||||
rightDisabled: false,
|
||||
middleDisabled: false,
|
||||
});
|
||||
if (this.state.page_type === PageTypes.UserSettings) {
|
||||
// We do this to get setPage and notifyNewScreen
|
||||
if (this.state.currentRoomId) {
|
||||
this._viewRoom({
|
||||
room_id: this.state.currentRoomId,
|
||||
});
|
||||
} else if (this.state.currentGroupId) {
|
||||
this._viewGroup({
|
||||
group_id: this.state.currentGroupId,
|
||||
});
|
||||
} else {
|
||||
this._viewHome();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'view_create_room':
|
||||
this._createRoom();
|
||||
break;
|
||||
|
@ -577,19 +605,10 @@ export default React.createClass({
|
|||
this.notifyNewScreen('groups');
|
||||
break;
|
||||
case 'view_group':
|
||||
{
|
||||
const groupId = payload.group_id;
|
||||
this.setState({
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this._setPage(PageTypes.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
}
|
||||
this._viewGroup(payload);
|
||||
break;
|
||||
case 'view_home_page':
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
this._viewHome();
|
||||
break;
|
||||
case 'view_set_mxid':
|
||||
this._setMxId(payload);
|
||||
|
@ -632,7 +651,8 @@ export default React.createClass({
|
|||
middleDisabled: payload.middleDisabled || false,
|
||||
rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
|
||||
});
|
||||
break; }
|
||||
break;
|
||||
}
|
||||
case 'set_theme':
|
||||
this._onSetTheme(payload.value);
|
||||
break;
|
||||
|
@ -781,7 +801,6 @@ export default React.createClass({
|
|||
// @param {string=} roomInfo.room_id ID of the room to join. One of room_id or room_alias must be given.
|
||||
// @param {string=} roomInfo.room_alias Alias of the room to join. One of room_id or room_alias must be given.
|
||||
// @param {boolean=} roomInfo.auto_join If true, automatically attempt to join the room if not already a member.
|
||||
// @param {boolean=} roomInfo.show_settings Makes RoomView show the room settings dialog.
|
||||
// @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the
|
||||
// context of that particular event.
|
||||
// @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
|
||||
|
@ -848,6 +867,21 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_viewGroup: function(payload) {
|
||||
const groupId = payload.group_id;
|
||||
this.setState({
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this._setPage(PageTypes.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
},
|
||||
|
||||
_viewHome: function() {
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
},
|
||||
|
||||
_setMxId: function(payload) {
|
||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||
|
@ -996,10 +1030,20 @@ export default React.createClass({
|
|||
}, (err) => {
|
||||
modal.close();
|
||||
console.error("Failed to leave room " + roomId + " " + err);
|
||||
let title = _t("Failed to leave room");
|
||||
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
|
||||
if (err.errcode == 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
|
||||
title = _t("Can't leave Server Notices room");
|
||||
message = _t(
|
||||
"This room is used for important messages from the Homeserver, " +
|
||||
"so you cannot leave it.",
|
||||
);
|
||||
} else if (err && err.message) {
|
||||
message = err.message;
|
||||
}
|
||||
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
|
||||
title: _t("Failed to leave room"),
|
||||
description: (err && err.message ? err.message :
|
||||
_t("Server may be unavailable, overloaded, or you hit a bug.")),
|
||||
title: title,
|
||||
description: message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1100,11 +1144,6 @@ export default React.createClass({
|
|||
} else if (this._is_registered) {
|
||||
this._is_registered = false;
|
||||
|
||||
// Set the display name = user ID localpart
|
||||
MatrixClientPeg.get().setDisplayName(
|
||||
MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
);
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
createRoom({
|
||||
dmUserId: this.props.config.welcomeUserId,
|
||||
|
@ -1223,6 +1262,7 @@ export default React.createClass({
|
|||
}, true);
|
||||
});
|
||||
cli.on('Session.logged_out', function(call) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
|
||||
title: _t('Signed Out'),
|
||||
|
@ -1265,6 +1305,32 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
||||
}, (errorCode) => {
|
||||
// Map JS-SDK error codes to tracker codes for aggregation
|
||||
switch (errorCode) {
|
||||
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
|
||||
return 'olm_keys_not_sent_error';
|
||||
case 'OLM_UNKNOWN_MESSAGE_INDEX':
|
||||
return 'olm_index_error';
|
||||
case undefined:
|
||||
return 'unexpected_error';
|
||||
default:
|
||||
return 'unspecified_error';
|
||||
}
|
||||
});
|
||||
|
||||
// Shelved for later date when we have time to think about persisting history of
|
||||
// tracked events across sessions.
|
||||
// dft.loadTrackedEventHashMap();
|
||||
|
||||
dft.start();
|
||||
|
||||
// When logging out, stop tracking failures and destroy state
|
||||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
|
@ -1602,19 +1668,8 @@ export default React.createClass({
|
|||
this._setPageSubtitle(subtitle);
|
||||
},
|
||||
|
||||
onUserSettingsClose: function() {
|
||||
// XXX: use browser history instead to find the previous room?
|
||||
// or maintain a this.state.pageHistory in _setPage()?
|
||||
if (this.state.currentRoomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.currentRoomId,
|
||||
});
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_home_page',
|
||||
});
|
||||
}
|
||||
onCloseAllSettings() {
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
},
|
||||
|
||||
onServerConfigChange(config) {
|
||||
|
@ -1673,7 +1728,7 @@ export default React.createClass({
|
|||
return (
|
||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
onCloseAllSettings={this.onCloseAllSettings}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
teamToken={this._teamToken}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -25,6 +26,9 @@ import sdk from '../../index';
|
|||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
|
@ -189,7 +193,7 @@ module.exports = React.createClass({
|
|||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
* mult: -1 to page up, +1 to page down
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
if (this.refs.scrollPanel) {
|
||||
|
@ -199,6 +203,8 @@ module.exports = React.createClass({
|
|||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
*
|
||||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
if (this.refs.scrollPanel) {
|
||||
|
@ -257,6 +263,7 @@ module.exports = React.createClass({
|
|||
|
||||
this.eventNodes = {};
|
||||
|
||||
let visible = false;
|
||||
let i;
|
||||
|
||||
// first figure out which is the last event in the list which we're
|
||||
|
@ -297,7 +304,7 @@ module.exports = React.createClass({
|
|||
// if the readmarker has moved, cancel any active ghost.
|
||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||
this.props.readMarkerVisible &&
|
||||
this.currentReadMarkerEventId != this.props.readMarkerEventId) {
|
||||
this.currentReadMarkerEventId !== this.props.readMarkerEventId) {
|
||||
this.currentGhostEventId = null;
|
||||
}
|
||||
|
||||
|
@ -404,8 +411,8 @@ module.exports = React.createClass({
|
|||
|
||||
let isVisibleReadMarker = false;
|
||||
|
||||
if (eventId == this.props.readMarkerEventId) {
|
||||
var visible = this.props.readMarkerVisible;
|
||||
if (eventId === this.props.readMarkerEventId) {
|
||||
visible = this.props.readMarkerVisible;
|
||||
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
|
@ -423,11 +430,11 @@ module.exports = React.createClass({
|
|||
|
||||
// XXX: there should be no need for a ghost tile - we should just use a
|
||||
// a dispatch (user_activity_end) to start the RM animation.
|
||||
if (eventId == this.currentGhostEventId) {
|
||||
if (eventId === this.currentGhostEventId) {
|
||||
// if we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
} else if (!isVisibleReadMarker &&
|
||||
eventId == this.currentReadMarkerEventId) {
|
||||
eventId === this.currentReadMarkerEventId) {
|
||||
// there is currently a read-up-to marker at this point, but no
|
||||
// more. Show an animation of it disappearing.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
|
@ -449,16 +456,17 @@ module.exports = React.createClass({
|
|||
|
||||
// Some events should appear as continuations from previous events of
|
||||
// different types.
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
const eventTypeContinues =
|
||||
prevEvent !== null &&
|
||||
continuedTypes.includes(mxEv.getType()) &&
|
||||
continuedTypes.includes(prevEvent.getType());
|
||||
|
||||
if (prevEvent !== null
|
||||
&& prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) {
|
||||
// if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||
(mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||
continuation = true;
|
||||
}
|
||||
|
||||
|
@ -493,7 +501,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId == this.props.highlightedEventId);
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||
// Local echos have a send "status".
|
||||
|
@ -632,7 +640,8 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let topSpinner, bottomSpinner;
|
||||
let topSpinner;
|
||||
let bottomSpinner;
|
||||
if (this.props.backPaginating) {
|
||||
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export default withMatrixClient(React.createClass({
|
|||
if (this.state.groups) {
|
||||
const groupNodes = [];
|
||||
this.state.groups.forEach((g) => {
|
||||
groupNodes.push(<GroupTile groupId={g} />);
|
||||
groupNodes.push(<GroupTile key={g} groupId={g} />);
|
||||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
|
@ -124,7 +124,7 @@ export default withMatrixClient(React.createClass({
|
|||
) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
|
@ -140,7 +140,7 @@ export default withMatrixClient(React.createClass({
|
|||
{ 'i': (sub) => <i>{ sub }</i> })
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>*/}
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{ contentHeader }
|
||||
|
|
|
@ -280,7 +280,7 @@ module.exports = React.createClass({
|
|||
const room = cli.getRoom(this.props.roomId);
|
||||
let isUserInRoom;
|
||||
if (room) {
|
||||
const numMembers = room.getJoinedMembers().length;
|
||||
const numMembers = room.getJoinedMemberCount();
|
||||
membersTitle = _t('%(count)s Members', { count: numMembers });
|
||||
membersBadge = <div title={membersTitle}>{ formatCount(numMembers) }</div>;
|
||||
isUserInRoom = room.hasMembershipState(this.context.matrixClient.credentials.userId, 'join');
|
||||
|
|
|
@ -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.
|
||||
|
@ -18,13 +18,15 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t } from '../../languageHandler';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import sdk from '../../index';
|
||||
import WhoIsTyping from '../../WhoIsTyping';
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -106,6 +108,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
syncState: MatrixClientPeg.get().getSyncState(),
|
||||
syncStateData: MatrixClientPeg.get().getSyncStateData(),
|
||||
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
|
||||
unsentMessages: getUnsentMessages(this.props.room),
|
||||
};
|
||||
|
@ -133,12 +136,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSyncStateChange: function(state, prevState) {
|
||||
onSyncStateChange: function(state, prevState, data) {
|
||||
if (state === "SYNCING" && prevState === "SYNCING") {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
syncState: state,
|
||||
syncStateData: data,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -157,10 +161,12 @@ module.exports = React.createClass({
|
|||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onCancelAllClick: function() {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
|
@ -188,7 +194,7 @@ module.exports = React.createClass({
|
|||
// changed - so we use '0' to indicate normal size, and other values to
|
||||
// indicate other sizes.
|
||||
_getSize: function() {
|
||||
if (this.state.syncState === "ERROR" ||
|
||||
if (this._shouldShowConnectionError() ||
|
||||
(this.state.usersTyping.length > 0) ||
|
||||
this.props.numUnreadMessages ||
|
||||
!this.props.atEndOfLiveTimeline ||
|
||||
|
@ -235,7 +241,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (this.state.syncState === "ERROR") {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -282,6 +288,21 @@ module.exports = React.createClass({
|
|||
return avatars;
|
||||
},
|
||||
|
||||
_shouldShowConnectionError: function() {
|
||||
// no conn bar trumps unread count since you can't get unread messages
|
||||
// without a connection! (technically may already have some but meh)
|
||||
// It also trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
// There's one situation in which we don't show this 'no connection' bar, and that's
|
||||
// if it's a resource limit exceeded error: those are shown in the top bar.
|
||||
const errorIsMauError = Boolean(
|
||||
this.state.syncStateData &&
|
||||
this.state.syncStateData.error &&
|
||||
this.state.syncStateData.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED'
|
||||
);
|
||||
return this.state.syncState === "ERROR" && !errorIsMauError;
|
||||
},
|
||||
|
||||
_getUnsentMessageContent: function() {
|
||||
const unsentMessages = this.state.unsentMessages;
|
||||
if (!unsentMessages.length) return null;
|
||||
|
@ -305,7 +326,43 @@ module.exports = React.createClass({
|
|||
},
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
let consentError = null;
|
||||
let resourceLimitError = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
|
||||
consentError = m.error;
|
||||
break;
|
||||
} else if (m.error && m.error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
resourceLimitError = m.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
title = _t(
|
||||
"You can't send any messages until you review and agree to " +
|
||||
"<consentLink>our terms and conditions</consentLink>.",
|
||||
{},
|
||||
{
|
||||
'consentLink': (sub) =>
|
||||
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else if (resourceLimitError) {
|
||||
title = messageForResourceLimitError(
|
||||
resourceLimitError.data.limit_type,
|
||||
resourceLimitError.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
'': _td(
|
||||
"Your message wasn't sent because this homeserver has exceeded a resource limit. " +
|
||||
"Please <a>contact your service administrator</a> to continue using the service.",
|
||||
),
|
||||
});
|
||||
} else if (
|
||||
unsentMessages.length === 1 &&
|
||||
unsentMessages[0].error &&
|
||||
unsentMessages[0].error.data &&
|
||||
|
@ -329,11 +386,13 @@ module.exports = React.createClass({
|
|||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ content }
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -342,19 +401,17 @@ module.exports = React.createClass({
|
|||
_getContent: function() {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
||||
// no conn bar trumps unread count since you can't get unread messages
|
||||
// without a connection! (technically may already have some but meh)
|
||||
// It also trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
if (this.state.syncState === "ERROR") {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -15,56 +16,53 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var classNames = require('classnames');
|
||||
var sdk = require('../../index');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { _t } from '../../languageHandler';
|
||||
var dis = require('../../dispatcher');
|
||||
var Unread = require('../../Unread');
|
||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
||||
var RoomNotifs = require('../../RoomNotifs');
|
||||
var FormattingUtils = require('../../utils/FormattingUtils');
|
||||
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
||||
import Modal from '../../Modal';
|
||||
import dis from '../../dispatcher';
|
||||
import Unread from '../../Unread';
|
||||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
var debug = false;
|
||||
const debug = false;
|
||||
|
||||
const TRUNCATE_AT = 10;
|
||||
|
||||
var RoomSubList = React.createClass({
|
||||
const RoomSubList = React.createClass({
|
||||
displayName: 'RoomSubList',
|
||||
|
||||
debug: debug,
|
||||
|
||||
propTypes: {
|
||||
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
tagName: React.PropTypes.string,
|
||||
editable: React.PropTypes.bool,
|
||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
editable: PropTypes.bool,
|
||||
|
||||
order: React.PropTypes.string.isRequired,
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: React.PropTypes.bool,
|
||||
isInvite: PropTypes.bool,
|
||||
|
||||
startAsHidden: React.PropTypes.bool,
|
||||
showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: React.PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: React.PropTypes.func,
|
||||
alwaysShowHeader: React.PropTypes.bool,
|
||||
incomingCall: React.PropTypes.object,
|
||||
onShowMoreRooms: React.PropTypes.func,
|
||||
searchFilter: React.PropTypes.string,
|
||||
emptyContent: React.PropTypes.node, // content shown if the list is empty
|
||||
headerItems: React.PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
|
||||
startAsHidden: PropTypes.bool,
|
||||
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: PropTypes.func,
|
||||
alwaysShowHeader: PropTypes.bool,
|
||||
incomingCall: PropTypes.object,
|
||||
onShowMoreRooms: PropTypes.func,
|
||||
searchFilter: PropTypes.string,
|
||||
emptyContent: PropTypes.node, // content shown if the list is empty
|
||||
headerItems: PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
showEmpty: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -77,10 +75,13 @@ var RoomSubList = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {}, // NOP
|
||||
onShowMoreRooms: function() {}, // NOP
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
onShowMoreRooms: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
showEmpty: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -105,15 +106,17 @@ var RoomSubList = React.createClass({
|
|||
|
||||
applySearchFilter: function(list, filter) {
|
||||
if (filter === "") return list;
|
||||
return list.filter((room) => {
|
||||
return room.name && room.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0
|
||||
});
|
||||
const lcFilter = filter.toLowerCase();
|
||||
// case insensitive if room name includes filter,
|
||||
// or if starts with `#` and one of room's aliases starts with filter
|
||||
return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) ||
|
||||
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
||||
},
|
||||
|
||||
// The header is collapsable if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsableOnClick: function() {
|
||||
var stuck = this.refs.header.dataset.stuck;
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
||||
return true;
|
||||
} else {
|
||||
|
@ -139,12 +142,12 @@ var RoomSubList = React.createClass({
|
|||
onClick: function(ev) {
|
||||
if (this.isCollapsableOnClick()) {
|
||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||
var isHidden = !this.state.hidden;
|
||||
this.setState({ hidden : isHidden });
|
||||
const isHidden = !this.state.hidden;
|
||||
this.setState({hidden: isHidden});
|
||||
|
||||
if (isHidden) {
|
||||
// as good a way as any to reset the truncate state
|
||||
this.setState({ truncateAt : TRUNCATE_AT });
|
||||
this.setState({truncateAt: TRUNCATE_AT});
|
||||
}
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
|
@ -159,7 +162,7 @@ var RoomSubList = React.createClass({
|
|||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
|
||||
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -169,17 +172,17 @@ var RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
_shouldShowMentionBadge: function(roomNotifState) {
|
||||
return roomNotifState != RoomNotifs.MUTE;
|
||||
return roomNotifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total up all the notification counts from the rooms
|
||||
*
|
||||
* @param {Number} If supplied will only total notifications for rooms outside the truncation number
|
||||
* @param {Number} truncateAt If supplied will only total notifications for rooms outside the truncation number
|
||||
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
||||
*/
|
||||
roomNotificationCount: function(truncateAt) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
return [0, true];
|
||||
|
@ -187,9 +190,9 @@ var RoomSubList = React.createClass({
|
|||
|
||||
return this.props.list.reduce(function(result, room, index) {
|
||||
if (truncateAt === undefined || index >= truncateAt) {
|
||||
var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
var highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
var notificationCount = room.getUnreadNotificationCount();
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
|
||||
|
@ -238,38 +241,83 @@ var RoomSubList = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onNotifBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// find first room which has notifications and switch to it
|
||||
for (const room of this.state.sortedList) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
|
||||
|
||||
if (notifBadges || mentionBadges) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onInviteBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// switch to first room in sortedList as that'll be the top of the list for the user
|
||||
if (this.state.sortedList && this.state.sortedList.length > 0) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.sortedList[0].roomId,
|
||||
});
|
||||
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
|
||||
// Group Invites are different in that they are all extra tiles and not rooms
|
||||
// XXX: this is a horrible special case because Group Invite sublist is a hack
|
||||
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.extraTiles[0].props.group.groupId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getHeaderJsx: function() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const subListNotifications = this.roomNotificationCount();
|
||||
const subListNotifCount = subListNotifications[0];
|
||||
const subListNotifHighlight = subListNotifications[1];
|
||||
|
||||
var subListNotifications = this.roomNotificationCount();
|
||||
var subListNotifCount = subListNotifications[0];
|
||||
var subListNotifHighlight = subListNotifications[1];
|
||||
const totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
const roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
var roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var chevronClasses = classNames({
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
|
||||
var badge;
|
||||
let badge;
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses}>{ FormattingUtils.formatCount(subListNotifCount) }</div>;
|
||||
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</div>;
|
||||
} else if (this.props.isInvite) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses}>!</div>;
|
||||
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>;
|
||||
}
|
||||
|
||||
// When collapsed, allow a long hover on the header to show user
|
||||
// the full tag name and room count
|
||||
var title;
|
||||
let title;
|
||||
if (this.props.collapsed) {
|
||||
title = this.props.label;
|
||||
if (roomCount !== '') {
|
||||
|
@ -277,63 +325,66 @@ var RoomSubList = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var incomingCall;
|
||||
let incomingCall;
|
||||
if (this.props.incomingCall) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
// Check if the incoming call is for this section
|
||||
var incomingCallRoom = this.props.list.filter(function(room) {
|
||||
const incomingCallRoom = this.props.list.filter(function(room) {
|
||||
return self.props.incomingCall.roomId === room.roomId;
|
||||
});
|
||||
|
||||
if (incomingCallRoom.length === 1) {
|
||||
var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall = <IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={ this.props.incomingCall }/>;
|
||||
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall =
|
||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||
}
|
||||
}
|
||||
|
||||
var tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{ this.props.collapsed ? '' : this.props.label }
|
||||
<div className="mx_RoomSubList_roomCount">{ roomCount }</div>
|
||||
<div className={chevronClasses}></div>
|
||||
{ badge }
|
||||
{ incomingCall }
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header">
|
||||
<AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{this.props.collapsed ? '' : this.props.label}
|
||||
<div className="mx_RoomSubList_roomCount">{roomCount}</div>
|
||||
<div className={chevronClasses} />
|
||||
{badge}
|
||||
{incomingCall}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_createOverflowTile: function(overflowCount, totalCount) {
|
||||
var content = <div className="mx_RoomSubList_chevronDown"></div>;
|
||||
let content = <div className="mx_RoomSubList_chevronDown" />;
|
||||
|
||||
var overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
var overflowNotifCount = overflowNotifications[0];
|
||||
var overflowNotifHighlight = overflowNotifications[1];
|
||||
const overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
const overflowNotifCount = overflowNotifications[0];
|
||||
const overflowNotifHighlight = overflowNotifications[1];
|
||||
if (overflowNotifCount && !this.props.collapsed) {
|
||||
content = FormattingUtils.formatCount(overflowNotifCount);
|
||||
}
|
||||
|
||||
var badgeClasses = classNames({
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_moreBadge': true,
|
||||
'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed,
|
||||
'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed,
|
||||
});
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||
<div className="mx_RoomSubList_line"></div>
|
||||
<div className="mx_RoomSubList_more">{ _t("more") }</div>
|
||||
<div className={ badgeClasses }>{ content }</div>
|
||||
<div className="mx_RoomSubList_line" />
|
||||
<div className="mx_RoomSubList_more">{_t("more")}</div>
|
||||
<div className={badgeClasses}>{content}</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
||||
_showFullMemberList: function() {
|
||||
this.setState({
|
||||
truncateAt: -1
|
||||
truncateAt: -1,
|
||||
});
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
|
@ -341,37 +392,51 @@ var RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
var TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
var label = this.props.collapsed ? null : this.props.label;
|
||||
const TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
let content;
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
|
||||
if (this.props.showEmpty) {
|
||||
// this is new behaviour with still controversial UX in that in hiding RoomSubLists the drop zones for DnD
|
||||
// are also gone so when filtering users can't DnD rooms to some tags but is a lot cleaner otherwise.
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) {
|
||||
// if no search filter is applied and there is a placeholder defined then show it, otherwise show nothing
|
||||
if (!this.props.searchFilter && this.props.emptyContent) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
// don't show an empty sublist
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
||||
var subList;
|
||||
var classes = "mx_RoomSubList";
|
||||
let subList;
|
||||
const classes = "mx_RoomSubList";
|
||||
|
||||
if (!this.state.hidden) {
|
||||
subList = <TruncatedList className={ classes } truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile} >
|
||||
{ content }
|
||||
</TruncatedList>;
|
||||
}
|
||||
else {
|
||||
subList = <TruncatedList className={ classes }>
|
||||
</TruncatedList>;
|
||||
subList = <TruncatedList className={classes} truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{content}
|
||||
</TruncatedList>;
|
||||
} else {
|
||||
subList = <TruncatedList className={classes}>
|
||||
</TruncatedList>;
|
||||
}
|
||||
|
||||
const subListContent = <div>
|
||||
{ this._getHeaderJsx() }
|
||||
{ subList }
|
||||
{this._getHeaderJsx()}
|
||||
{subList}
|
||||
</div>;
|
||||
|
||||
return this.props.editable ?
|
||||
|
@ -379,23 +444,26 @@ var RoomSubList = React.createClass({
|
|||
droppableId={"room-sub-list-droppable_" + this.props.tagName}
|
||||
type="draggable-RoomTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}>
|
||||
{ subListContent }
|
||||
{subListContent}
|
||||
</div>
|
||||
) }
|
||||
)}
|
||||
</Droppable> : subListContent;
|
||||
}
|
||||
else {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
} else {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.props.showSpinner) {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList">
|
||||
{ this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined }
|
||||
{ (this.props.showSpinner && !this.state.hidden) ? <Loader /> : undefined }
|
||||
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
||||
{ this.state.hidden ? undefined : content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = RoomSubList;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -44,7 +45,9 @@ import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
|||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../utils/WidgetUtils';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -115,6 +118,7 @@ module.exports = React.createClass({
|
|||
showApps: false,
|
||||
isAlone: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
|
@ -150,6 +154,8 @@ module.exports = React.createClass({
|
|||
// Start listening for RoomViewStore updates
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._onRoomViewStoreUpdate(true);
|
||||
|
||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(initial) {
|
||||
|
@ -182,6 +188,8 @@ module.exports = React.createClass({
|
|||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
|
||||
editingRoomSettings: RoomViewStore.isEditingSettings(),
|
||||
};
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
|
@ -238,6 +246,12 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onWidgetEchoStoreUpdate: function() {
|
||||
this.setState({
|
||||
showApps: this._shouldShowApps(this.state.room),
|
||||
});
|
||||
},
|
||||
|
||||
_setupRoom: function(room, roomId, joining, shouldPeek) {
|
||||
// if this is an unknown room then we're in one of three states:
|
||||
// - This is a room we can peek into (search engine) (we can /peek)
|
||||
|
@ -294,11 +308,23 @@ module.exports = React.createClass({
|
|||
throw err;
|
||||
}
|
||||
});
|
||||
} else if (room) {
|
||||
//viewing a previously joined room, try to lazy load members
|
||||
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
|
||||
// lazy load members if enabled
|
||||
if (SettingsStore.isFeatureEnabled('feature_lazyloading')) {
|
||||
room.loadMembersIfNeeded().catch((err) => {
|
||||
const errorMessage = `Fetching room members for ${this.roomId} failed.` +
|
||||
" Room members will appear incomplete.";
|
||||
console.error(errorMessage);
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (room) {
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -314,14 +340,9 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
}
|
||||
|
||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
// any valid widget = show apps
|
||||
for (let i = 0; i < appsStateEvents.length; i++) {
|
||||
if (appsStateEvents[i].getContent().type && appsStateEvents[i].getContent().url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
const widgets = WidgetEchoStore.getEchoedRoomWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
|
||||
|
||||
return widgets.length > 0 || WidgetEchoStore.roomHasPendingWidgets(room.roomId, WidgetUtils.getRoomWidgets(room));
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -342,7 +363,7 @@ module.exports = React.createClass({
|
|||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||
// We use the setTimeout to avoid racing with focus_composer.
|
||||
if (this.state.room &&
|
||||
this.state.room.getJoinedMembers().length == 1 &&
|
||||
this.state.room.getJoinedMemberCount() == 1 &&
|
||||
this.state.room.getLiveTimeline() &&
|
||||
this.state.room.getLiveTimeline().getEvents() &&
|
||||
this.state.room.getLiveTimeline().getEvents().length <= 6) {
|
||||
|
@ -416,6 +437,8 @@ module.exports = React.createClass({
|
|||
this._roomStoreToken.remove();
|
||||
}
|
||||
|
||||
WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate);
|
||||
|
||||
// cancel any pending calls to the rate_limited_funcs
|
||||
this._updateRoomMembers.cancelPendingCall();
|
||||
|
||||
|
@ -615,9 +638,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_updatePreviewUrlVisibility: function(room) {
|
||||
_updatePreviewUrlVisibility: function({roomId}) {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -642,19 +667,23 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAccountData: function(event) {
|
||||
if (event.getType() === "org.matrix.preview_urls" && this.state.room) {
|
||||
const type = event.getType();
|
||||
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(this.state.room);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomAccountData: function(event, room) {
|
||||
if (room.roomId == this.state.roomId) {
|
||||
if (event.getType() === "org.matrix.room.color_scheme") {
|
||||
const type = event.getType();
|
||||
if (type === "org.matrix.room.color_scheme") {
|
||||
const color_scheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
} else if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
}
|
||||
}
|
||||
|
@ -687,6 +716,7 @@ module.exports = React.createClass({
|
|||
// refresh the conf call notification state
|
||||
this._updateConfCallNotification();
|
||||
this._updateDMState();
|
||||
this._checkIfAlone(this.state.room);
|
||||
}, 500),
|
||||
|
||||
_checkIfAlone: function(room) {
|
||||
|
@ -699,8 +729,8 @@ module.exports = React.createClass({
|
|||
return;
|
||||
}
|
||||
|
||||
const joinedMembers = room.currentState.getMembers().filter((m) => m.membership === "join" || m.membership === "invite");
|
||||
this.setState({isAlone: joinedMembers.length === 1});
|
||||
const joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount();
|
||||
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
|
||||
},
|
||||
|
||||
_updateConfCallNotification: function() {
|
||||
|
@ -728,40 +758,13 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_updateDMState() {
|
||||
const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (!me || me.membership !== "join") {
|
||||
const room = this.state.room;
|
||||
if (room.getMyMembership() != "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;
|
||||
const dmInviter = room.getDMInviter();
|
||||
if (dmInviter) {
|
||||
Rooms.setDMRoom(room.roomId, dmInviter);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -909,6 +912,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
uploadFile: async function(file) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
|
@ -1135,11 +1140,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onPinnedClick: function() {
|
||||
this.setState({showingPinned: !this.state.showingPinned, searching: false});
|
||||
const nowShowingPinned = !this.state.showingPinned;
|
||||
const roomId = this.state.room.roomId;
|
||||
this.setState({showingPinned: nowShowingPinned, searching: false});
|
||||
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
||||
},
|
||||
|
||||
onSettingsClick: function() {
|
||||
this.showSettings(true);
|
||||
dis.dispatch({ action: 'open_room_settings' });
|
||||
},
|
||||
|
||||
onSettingsSaveClick: function() {
|
||||
|
@ -1172,24 +1180,20 @@ module.exports = React.createClass({
|
|||
});
|
||||
// still editing room settings
|
||||
} else {
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
});
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
uploadingRoomSettings: false,
|
||||
editingRoomSettings: false,
|
||||
});
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}).done();
|
||||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
console.log("updateTint from onCancelClick");
|
||||
this.updateTint();
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
});
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
|
@ -1406,13 +1410,6 @@ module.exports = React.createClass({
|
|||
});*/
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
// XXX: this is a bit naughty; we should be doing this via props
|
||||
if (show) {
|
||||
this.setState({editingRoomSettings: true});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* called by the parent component when PageUp/Down/etc is pressed.
|
||||
*
|
||||
|
@ -1510,9 +1507,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myMember = this.state.room.getMember(myUserId);
|
||||
if (myMember && myMember.membership == 'invite') {
|
||||
const myMembership = this.state.room.getMyMembership();
|
||||
if (myMembership == 'invite') {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
|
@ -1520,6 +1516,8 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
} else {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const myMember = this.state.room.getMember(myUserId);
|
||||
const inviteEvent = myMember.events.member;
|
||||
var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender();
|
||||
|
||||
|
@ -1603,7 +1601,7 @@ module.exports = React.createClass({
|
|||
} else if (this.state.showingPinned) {
|
||||
hideCancel = true; // has own cancel
|
||||
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
|
||||
} else if (!myMember || myMember.membership !== "join") {
|
||||
} else if (myMembership !== "join") {
|
||||
// We do have a room object for this room, but we're not currently in it.
|
||||
// We may have a 3rd party invite to it.
|
||||
var inviterName = undefined;
|
||||
|
@ -1645,7 +1643,7 @@ module.exports = React.createClass({
|
|||
let messageComposer, searchInfo;
|
||||
const canSpeak = (
|
||||
// joined and not showing search results
|
||||
myMember && (myMember.membership == 'join') && !this.state.searchResults
|
||||
myMembership == 'join' && !this.state.searchResults
|
||||
);
|
||||
if (canSpeak) {
|
||||
messageComposer =
|
||||
|
@ -1780,15 +1778,15 @@ module.exports = React.createClass({
|
|||
oobData={this.props.oobData}
|
||||
editing={this.state.editingRoomSettings}
|
||||
saving={this.state.uploadingRoomSettings}
|
||||
inRoom={myMember && myMember.membership === 'join'}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={this.props.collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onSaveClick={this.onSettingsSaveClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMember && myMember.membership === "join") ? this.onLeaveClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
/>
|
||||
{ auxPanel }
|
||||
<div className={fadableSectionClasses}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -26,6 +26,7 @@ import dis from '../../dispatcher';
|
|||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
displayName: 'TagPanel',
|
||||
|
@ -84,7 +85,10 @@ const TagPanel = React.createClass({
|
|||
},
|
||||
|
||||
onMouseDown(e) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
// only dispatch if its not a no-op
|
||||
if (this.state.selectedTags.length > 0) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
}
|
||||
},
|
||||
|
||||
onCreateGroupClick(ev) {
|
||||
|
@ -113,17 +117,26 @@ const TagPanel = React.createClass({
|
|||
/>;
|
||||
});
|
||||
|
||||
const clearButton = this.state.selectedTags.length > 0 ?
|
||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
/> :
|
||||
<div />;
|
||||
const itemsSelected = this.state.selectedTags.length > 0;
|
||||
|
||||
return <div className="mx_TagPanel">
|
||||
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
let clearButton;
|
||||
if (itemsSelected) {
|
||||
clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_TagPanel', {
|
||||
mx_TagPanel_items_selected: itemsSelected,
|
||||
});
|
||||
|
||||
return <div className={classes}>
|
||||
<div className="mx_TagPanel_clearButton_container">
|
||||
{ clearButton }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbarWrapper
|
||||
className="mx_TagPanel_scroller"
|
||||
|
|
|
@ -81,6 +81,7 @@ const SIMPLE_SETTINGS = [
|
|||
{ id: "VideoView.flipVideoHorizontally" },
|
||||
{ id: "TagPanel.disableTagPanel" },
|
||||
{ id: "enableWidgetScreenshots" },
|
||||
{ id: "RoomSubList.showEmpty" },
|
||||
];
|
||||
|
||||
// These settings must be defined in SettingsStore
|
||||
|
@ -284,7 +285,13 @@ module.exports = React.createClass({
|
|||
this.setState({ electron_settings: settings });
|
||||
},
|
||||
|
||||
_refreshMediaDevices: function() {
|
||||
_refreshMediaDevices: function(stream) {
|
||||
if (stream) {
|
||||
// kill stream so that we don't leave it lingering around with webcam enabled etc
|
||||
// as here we called gUM to ask user for permission to their device names only
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
return CallMediaHandler.getDevices();
|
||||
}).then((mediaDevices) => {
|
||||
|
@ -292,6 +299,7 @@ module.exports = React.createClass({
|
|||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
mediaDevices,
|
||||
activeAudioOutput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audiooutput'),
|
||||
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
|
||||
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
|
||||
});
|
||||
|
@ -422,7 +430,6 @@ module.exports = React.createClass({
|
|||
"push notifications on other devices until you log back in to them",
|
||||
) + ".",
|
||||
});
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
},
|
||||
|
||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||
|
@ -837,8 +844,16 @@ module.exports = React.createClass({
|
|||
SettingsStore.getLabsFeatures().forEach((featureId) => {
|
||||
// TODO: this ought to be a separate component so that we don't need
|
||||
// to rebind the onChange each time we render
|
||||
const onChange = (e) => {
|
||||
SettingsStore.setFeatureEnabled(featureId, e.target.checked);
|
||||
const onChange = async (e) => {
|
||||
const checked = e.target.checked;
|
||||
if (featureId === "feature_lazyloading") {
|
||||
const confirmed = await this._onLazyLoadChanging(checked);
|
||||
if (!confirmed) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
await SettingsStore.setFeatureEnabled(featureId, checked);
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
|
@ -848,7 +863,7 @@ module.exports = React.createClass({
|
|||
type="checkbox"
|
||||
id={featureId}
|
||||
name={featureId}
|
||||
defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
|
||||
checked={SettingsStore.isFeatureEnabled(featureId)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
|
||||
|
@ -871,6 +886,30 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_onLazyLoadChanging: async function(enabling) {
|
||||
// don't prevent turning LL off when not supported
|
||||
if (enabling) {
|
||||
const supported = await MatrixClientPeg.get().doesServerSupportLazyLoading();
|
||||
if (!supported) {
|
||||
await new Promise((resolve) => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Lazy loading members not supported"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("Lazy loading is not supported by your " +
|
||||
"current homeserver.") }
|
||||
</div>,
|
||||
button: _t("OK"),
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_renderDeactivateAccount: function() {
|
||||
return <div>
|
||||
<h3>{ _t("Deactivate Account") }</h3>
|
||||
|
@ -970,6 +1009,11 @@ module.exports = React.createClass({
|
|||
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
||||
},
|
||||
|
||||
_setAudioOutput: function(deviceId) {
|
||||
this.setState({activeAudioOutput: deviceId});
|
||||
CallMediaHandler.setAudioOutput(deviceId);
|
||||
},
|
||||
|
||||
_setAudioInput: function(deviceId) {
|
||||
this.setState({activeAudioInput: deviceId});
|
||||
CallMediaHandler.setAudioInput(deviceId);
|
||||
|
@ -1010,6 +1054,7 @@ module.exports = React.createClass({
|
|||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
|
||||
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||
|
||||
|
@ -1018,6 +1063,26 @@ module.exports = React.createClass({
|
|||
label: _t('Default Device'),
|
||||
};
|
||||
|
||||
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
|
||||
if (audioOutputs.length > 0) {
|
||||
let defaultOutput = '';
|
||||
if (!audioOutputs.some((input) => input.deviceId === 'default')) {
|
||||
audioOutputs.unshift(defaultOption);
|
||||
} else {
|
||||
defaultOutput = 'default';
|
||||
}
|
||||
|
||||
speakerDropdown = <div>
|
||||
<h4>{ _t('Audio Output') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeAudioOutput || defaultOutput}
|
||||
onOptionChange={this._setAudioOutput}>
|
||||
{ this._mapWebRtcDevicesToSpans(audioOutputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
|
||||
if (audioInputs.length > 0) {
|
||||
let defaultInput = '';
|
||||
|
@ -1059,8 +1124,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div>
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
{ speakerDropdown }
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
</div>;
|
||||
},
|
||||
|
||||
|
@ -1074,6 +1140,14 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
},
|
||||
|
||||
onSelfShareClick: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share self dialog', '', ShareDialog, {
|
||||
target: cli.getUser(this._me),
|
||||
});
|
||||
},
|
||||
|
||||
_showSpoiler: function(event) {
|
||||
const target = event.target;
|
||||
target.innerHTML = target.getAttribute('data-spoiler');
|
||||
|
@ -1295,10 +1369,13 @@ module.exports = React.createClass({
|
|||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t("Logged in as:") } { this._me }
|
||||
{ _t("Logged in as:") + ' ' }
|
||||
<a onClick={this.onSelfShareClick} className="mx_UserSettings_link">
|
||||
{ this._me }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t('Access Token:') }
|
||||
{ _t('Access Token:') + ' ' }
|
||||
<span className="mx_UserSettings_advanced_spoiler"
|
||||
onClick={this._showSpoiler}
|
||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
|
|
|
@ -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.
|
||||
|
@ -15,8 +15,6 @@ 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';
|
||||
|
@ -45,6 +43,8 @@ module.exports = React.createClass({
|
|||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
progress: null,
|
||||
password: null,
|
||||
password2: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -103,7 +103,7 @@ module.exports = React.createClass({
|
|||
</div>,
|
||||
button: _t('Continue'),
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
<button key="export_keys" className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
|
@ -169,7 +169,8 @@ module.exports = React.createClass({
|
|||
} else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div className="mx_Login_prompt">
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, " +
|
||||
"click below.", { emailAddress: this.state.email }) }
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={_t('I have verified my email address')} />
|
||||
|
@ -179,14 +180,15 @@ module.exports = React.createClass({
|
|||
resetPasswordJsx = (
|
||||
<div className="mx_Login_prompt">
|
||||
<p>{ _t('Your password has been reset') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. ' +
|
||||
'To re-enable notifications, sign in again on each device') }.</p>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
value={_t('Return to login screen')} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
|
@ -199,6 +201,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
|
@ -233,6 +237,7 @@ module.exports = React.createClass({
|
|||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
|
38
src/components/structures/login/LanguageSelector.js
Normal file
38
src/components/structures/login/LanguageSelector.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 SdkConfig from "../../../SdkConfig";
|
||||
import {getCurrentLanguage} from "../../../languageHandler";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import sdk from '../../../index';
|
||||
import React from 'react';
|
||||
|
||||
function onChange(newLang) {
|
||||
if (getCurrentLanguage() !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
}
|
||||
|
||||
export default function LanguageSelector() {
|
||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div className="mx_Login_language_div">
|
||||
<LanguageDropdown onOptionChange={onChange} className="mx_Login_language" value={getCurrentLanguage()} />
|
||||
</div>;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -19,16 +20,15 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
|
@ -94,6 +94,13 @@ module.exports = React.createClass({
|
|||
this._unmounted = true;
|
||||
},
|
||||
|
||||
onPasswordLoginError: function(errorText) {
|
||||
this.setState({
|
||||
errorText,
|
||||
loginIncorrect: Boolean(errorText),
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -113,10 +120,34 @@ module.exports = React.createClass({
|
|||
|
||||
// Some error strings only apply for logging in
|
||||
const usingEmail = username.indexOf("@") > 0;
|
||||
if (error.httpStatus == 400 && usingEmail) {
|
||||
if (error.httpStatus === 400 && usingEmail) {
|
||||
errorText = _t('This Home Server does not support login using email address.');
|
||||
} else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{errorTop}</div>
|
||||
<div className="mx_Login_smallError">{errorDetail}</div>
|
||||
</div>
|
||||
);
|
||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||
if (SdkConfig.get().disable_custom_urls) {
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||
|
@ -143,7 +174,7 @@ module.exports = React.createClass({
|
|||
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
||||
// mentions this (although the bug is for UI auth which is not this)
|
||||
// We treat both as an incorrect password
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if (this._unmounted) {
|
||||
|
@ -231,7 +262,7 @@ module.exports = React.createClass({
|
|||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||
|
||||
const fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
|
||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
|
@ -310,19 +341,27 @@ module.exports = React.createClass({
|
|||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{
|
||||
_t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
|
||||
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
} else {
|
||||
errorText = <span>
|
||||
{
|
||||
_t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
|
||||
{ _t("Can't connect to homeserver - please check your connectivity, ensure your " +
|
||||
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
||||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
}
|
||||
|
@ -350,6 +389,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onError={this.onPasswordLoginError}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
|
@ -370,23 +410,6 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_onLanguageChange: function(newLang) {
|
||||
if (languageHandler.getCurrentLanguage() !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
},
|
||||
|
||||
_renderLanguageSetting: function() {
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div className="mx_Login_language_div">
|
||||
<LanguageDropdown onOptionChange={this._onLanguageChange}
|
||||
className="mx_Login_language"
|
||||
value={languageHandler.getCurrentLanguage()}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
|
@ -399,25 +422,14 @@ module.exports = React.createClass({
|
|||
if (this.props.enableGuest) {
|
||||
loginAsGuestJsx =
|
||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||
{ _t('Login as guest') }
|
||||
{ _t('Try the app first') }
|
||||
</a>;
|
||||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx =
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{ _t('Return to app') }
|
||||
</a>;
|
||||
}
|
||||
*/
|
||||
|
||||
let serverConfig;
|
||||
let header;
|
||||
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfig = <ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
|
@ -447,6 +459,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
return (
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
|
@ -460,8 +474,7 @@ module.exports = React.createClass({
|
|||
{ _t('Create an account') }
|
||||
</a>
|
||||
{ loginAsGuestJsx }
|
||||
{ returnToAppJsx }
|
||||
{ !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -22,13 +23,13 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import ServerConfig from '../../views/login/ServerConfig';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||
import RtsClient from '../../../RtsClient';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
|
@ -62,6 +63,12 @@ module.exports = React.createClass({
|
|||
onLoginClick: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
rtsClient: PropTypes.shape({
|
||||
getTeamsConfig: PropTypes.func.isRequired,
|
||||
trackReferral: PropTypes.func.isRequired,
|
||||
getTeam: PropTypes.func.isRequired,
|
||||
}),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -133,7 +140,7 @@ module.exports = React.createClass({
|
|||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
this.setState(newState, () => {
|
||||
this._replaceClient();
|
||||
});
|
||||
},
|
||||
|
@ -158,12 +165,34 @@ module.exports = React.createClass({
|
|||
if (!success) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdn_available = false;
|
||||
if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
msg = <div>
|
||||
<p>{errorTop}</p>
|
||||
<p>{errorDetail}</p>
|
||||
</div>;
|
||||
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdnAvailable = false;
|
||||
for (const flow of response.available_flows) {
|
||||
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
}
|
||||
if (!msisdn_available) {
|
||||
if (!msisdnAvailable) {
|
||||
msg = _t('This server does not support authentication with a phone number.');
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +271,7 @@ module.exports = React.createClass({
|
|||
return matrixClient.getPushers().then((resp)=>{
|
||||
const pushers = resp.pushers;
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind == 'email') {
|
||||
if (pushers[i].kind === 'email') {
|
||||
const emailPusher = pushers[i];
|
||||
emailPusher.data = { brand: this.props.brand };
|
||||
matrixClient.setPusher(emailPusher).done(() => {
|
||||
|
@ -267,7 +296,7 @@ module.exports = React.createClass({
|
|||
errMsg = _t('Passwords don\'t match.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
|
||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH});
|
||||
break;
|
||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = _t('This doesn\'t look like a valid email address.');
|
||||
|
@ -353,7 +382,7 @@ module.exports = React.createClass({
|
|||
registerBody = <Spinner />;
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
|
@ -385,18 +414,6 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx = (
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{ _t('Return to app') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
let header;
|
||||
let errorText;
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
|
@ -418,6 +435,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
return (
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
|
@ -431,7 +450,7 @@ module.exports = React.createClass({
|
|||
{ registerBody }
|
||||
{ signIn }
|
||||
{ errorText }
|
||||
{ returnToAppJsx }
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</LoginPage>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2018 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import {Group} from 'matrix-js-sdk';
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
||||
export default class GroupInviteTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
group: PropTypes.instanceOf(Group).isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onClickReject() {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
|
||||
title: _t('Reject invitation'),
|
||||
description: _t('Are you sure you want to reject the invitation?'),
|
||||
onFinished: async (shouldLeave) => {
|
||||
if (!shouldLeave) return;
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
try {
|
||||
await GroupStore.leaveGroup(this.props.group.groupId);
|
||||
} catch (e) {
|
||||
console.error("Error rejecting community invite: ", e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Unable to reject invite"),
|
||||
});
|
||||
} finally {
|
||||
modal.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
{ _t('Reject') }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -15,10 +15,9 @@ 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 {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
|
@ -179,7 +178,16 @@ module.exports = React.createClass({
|
|||
onQuoteClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'quote',
|
||||
text: this.props.eventTileOps.getInnerText(),
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onPermalinkClick: function(e: Event) {
|
||||
e.preventDefault();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
@ -211,7 +219,10 @@ module.exports = React.createClass({
|
|||
let replyButton;
|
||||
let collapseReplyThread;
|
||||
|
||||
if (eventStatus === 'not_sent') {
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
|
@ -219,7 +230,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (!eventStatus && this.state.canRedact) {
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
|
@ -227,7 +238,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (eventStatus === "queued" || eventStatus === "not_sent") {
|
||||
if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) {
|
||||
cancelButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
{ _t('Cancel Sending') }
|
||||
|
@ -235,7 +246,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (!eventStatus && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
if (isSent && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
forwardButton = (
|
||||
|
@ -244,13 +255,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_rich_quoting")) {
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
|
@ -290,7 +299,7 @@ module.exports = React.createClass({
|
|||
const permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())}
|
||||
target="_blank" rel="noopener" onClick={this.closeMenu}>{ _t('Permalink') }</a>
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>{ _t('Share Message') }</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -346,20 +346,18 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const myMember = this.props.room.getMember(
|
||||
MatrixClientPeg.get().credentials.userId,
|
||||
);
|
||||
const myMembership = this.props.room.getMyMembership();
|
||||
|
||||
// Can't set notif level or tags on non-join rooms
|
||||
if (myMember.membership !== 'join') {
|
||||
return this._renderLeaveMenu(myMember.membership);
|
||||
if (myMembership !== 'join') {
|
||||
return this._renderLeaveMenu(myMembership);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this._renderNotifMenu() }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderLeaveMenu(myMember.membership) }
|
||||
{ this._renderLeaveMenu(myMembership) }
|
||||
<hr className="mx_RoomTileContextMenu_separator" />
|
||||
{ this._renderRoomTagMenu() }
|
||||
</div>
|
||||
|
|
|
@ -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.
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Promise from 'bluebird';
|
||||
|
@ -27,6 +27,13 @@ import GroupStore from '../../../stores/GroupStore';
|
|||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
const addressTypeName = {
|
||||
'mx-user-id': _td("Matrix ID"),
|
||||
'mx-room-id': _td("Matrix Room ID"),
|
||||
'email': _td("email address"),
|
||||
};
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
|
@ -66,7 +73,7 @@ module.exports = React.createClass({
|
|||
|
||||
// List of UserAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
userList: [],
|
||||
selectedList: [],
|
||||
|
||||
// Whether a search is ongoing
|
||||
busy: false,
|
||||
|
@ -76,10 +83,9 @@ module.exports = React.createClass({
|
|||
serverSupportsUserDirectory: true,
|
||||
// The query being searched for
|
||||
query: "",
|
||||
// List of UserAddressType objects representing
|
||||
// the set of auto-completion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
// List of UserAddressType objects representing the set of
|
||||
// auto-completion results for the current search query.
|
||||
suggestedList: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -91,14 +97,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
let userList = this.state.userList.slice();
|
||||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local userList
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.refs.textinput.value !== '') {
|
||||
userList = this._addInputToList();
|
||||
if (userList === null) return;
|
||||
selectedList = this._addInputToList();
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, userList);
|
||||
this.props.onFinished(true, selectedList);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
|
@ -118,18 +124,18 @@ module.exports = React.createClass({
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onDismissed(this.state.userList.length - 1)();
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.refs.textinput.value == '') {
|
||||
if (this.refs.textinput.value === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
|
@ -148,7 +154,7 @@ module.exports = React.createClass({
|
|||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
if (query.length > 0 && query !== '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
|
@ -170,7 +176,7 @@ module.exports = React.createClass({
|
|||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
queryList: [],
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
|
@ -179,11 +185,11 @@ module.exports = React.createClass({
|
|||
|
||||
onDismissed: function(index) {
|
||||
return () => {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.splice(index, 1);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.splice(index, 1);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
|
@ -197,11 +203,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onSelected: function(index) {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(this.state.queryList[index]);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.state.suggestedList[index]);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
|
@ -379,10 +385,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
const suggestedList = [];
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
queryList.push({
|
||||
suggestedList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
|
@ -399,7 +405,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
queryList.push({
|
||||
suggestedList.push({
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
|
@ -413,18 +419,18 @@ module.exports = React.createClass({
|
|||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.props.validAddressTypes.includes(addrType)) {
|
||||
queryList.unshift({
|
||||
suggestedList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
if (addrType === 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
queryList,
|
||||
suggestedList,
|
||||
error: false,
|
||||
}, () => {
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||
|
@ -442,14 +448,14 @@ module.exports = React.createClass({
|
|||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType == 'mx-room-id') {
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
|
@ -458,15 +464,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(addrObj);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(addrObj);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return userList;
|
||||
return selectedList;
|
||||
},
|
||||
|
||||
_lookupThreepid: function(medium, address) {
|
||||
|
@ -492,7 +498,7 @@ module.exports = React.createClass({
|
|||
if (res === null) return null;
|
||||
if (cancelled) return null;
|
||||
this.setState({
|
||||
queryList: [{
|
||||
suggestedList: [{
|
||||
// a UserAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
|
@ -510,15 +516,27 @@ module.exports = React.createClass({
|
|||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({address, addressType}) => {
|
||||
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
|
||||
selectedAddresses[addressType].add(address);
|
||||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
if (this.state.userList.length > 0) {
|
||||
if (this.state.selectedList.length > 0) {
|
||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.userList[i]}
|
||||
address={this.state.selectedList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
|
@ -528,7 +546,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Add the query at the end
|
||||
query.push(
|
||||
<textarea key={this.state.userList.length}
|
||||
<textarea key={this.state.selectedList.length}
|
||||
rows="1"
|
||||
id="textinput"
|
||||
ref="textinput"
|
||||
|
@ -543,34 +561,22 @@ module.exports = React.createClass({
|
|||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
let tryUsing = '';
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
||||
return {
|
||||
'mx-user-id': _t("Matrix ID"),
|
||||
'mx-room-id': _t("Matrix Room ID"),
|
||||
'email': _t("email address"),
|
||||
}[t];
|
||||
});
|
||||
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
});
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
error = <div className="mx_ChatInviteDialog_error">
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ tryUsing }
|
||||
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
}) }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={this.state.queryList}
|
||||
addressList={filteredSuggestedList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
|
@ -64,7 +65,10 @@ export default React.createClass({
|
|||
|
||||
// Id of content element
|
||||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId: React.PropTypes.string,
|
||||
contentId: PropTypes.string,
|
||||
|
||||
// optional additional class for the title element
|
||||
titleClass: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -105,25 +109,28 @@ export default React.createClass({
|
|||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
cancelButton = <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton">
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FocusTrap onKeyDown={this._onKeyDown}
|
||||
className={this.props.className}
|
||||
role="dialog"
|
||||
aria-labelledby='mx_BaseDialog_title'
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completelly follow this recommendation we'd need to
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
aria-describedby={this.props.contentId}
|
||||
>
|
||||
{ this.props.hasCancel ? <AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton> : null }
|
||||
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
||||
{ cancelButton }
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -28,6 +29,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onFinished = this.onFinished.bind(this);
|
||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
|
@ -52,11 +54,8 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
for (const roomId of dmRooms) {
|
||||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(client.credentials.userId);
|
||||
const highlight = (
|
||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
||||
me.membership == "invite"
|
||||
);
|
||||
const isInvite = room.getMyMembership() === "invite";
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || isInvite;
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
transparent={true}
|
||||
|
@ -64,7 +63,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
isInvite={isInvite}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
@ -110,6 +109,10 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
this.props.onExistingRoomSelected(roomId);
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = '';
|
||||
let content = null;
|
||||
|
@ -170,14 +173,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
{ profile }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus={true} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={this.props.onFinished.bind(false)}
|
||||
onFinished={this.onFinished}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
|
@ -187,7 +190,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
ChatCreateOrReuseDialog.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
|
|
@ -56,7 +56,7 @@ export default React.createClass({
|
|||
_checkGroupId: function(e) {
|
||||
let error = null;
|
||||
if (!this.state.groupId) {
|
||||
error = _t("Community IDs cannot not be empty.");
|
||||
error = _t("Community IDs cannot be empty.");
|
||||
} else if (!/^[a-z0-9=_\-\.\/]*$/.test(this.state.groupId)) {
|
||||
error = _t("Community IDs may only contain characters a-z, 0-9, or '=_-./'");
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ export default React.createClass({
|
|||
onFinished: PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
componentWillMount: function() {
|
||||
const config = SdkConfig.get();
|
||||
// Dialog shows inverse of m.federate (noFederate) strict false check to skip undefined check (default = true)
|
||||
this.defaultNoFederate = config.default_federate === false;
|
||||
|
@ -52,8 +52,8 @@ export default React.createClass({
|
|||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||
<div className="mx_CreateRoomDialog_input_container">
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ class GenericEditor extends DevtoolsComponent {
|
|||
<label htmlFor={id}>{ label }</label>
|
||||
</div>
|
||||
<div className="mx_DevTools_inputCell">
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" />
|
||||
<input id={id} className="mx_TextInputDialog_input" onChange={this._onChange} value={this.state[id]} size="32" autoFocus={true} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
@ -132,17 +132,17 @@ class SendCustomEvent extends GenericEditor {
|
|||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -219,15 +219,15 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -242,6 +242,9 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
}
|
||||
|
||||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
|
@ -249,31 +252,68 @@ class FilteredList extends React.Component {
|
|||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static filterChildren(children, query) {
|
||||
if (!query) return children;
|
||||
const lcQuery = query.toLowerCase();
|
||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onQuery = this.onQuery.bind(this);
|
||||
|
||||
this.state = {
|
||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
};
|
||||
}
|
||||
|
||||
onQuery(ev) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
});
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
this.setState({
|
||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||
});
|
||||
};
|
||||
|
||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||
</button>;
|
||||
};
|
||||
|
||||
onQuery = (ev) => {
|
||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
filterChildren() {
|
||||
if (this.props.query) {
|
||||
const lowerQuery = this.props.query.toLowerCase();
|
||||
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
getChildren = (start: number, end: number) => {
|
||||
return this.state.filteredChildren.slice(start, end);
|
||||
};
|
||||
|
||||
getChildCount = (): number => {
|
||||
return this.state.filteredChildren.length;
|
||||
};
|
||||
|
||||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<input size="64"
|
||||
autoFocus={true}
|
||||
onChange={this.onQuery}
|
||||
value={this.props.query}
|
||||
placeholder={_t('Filter results')}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
||||
{ this.filterChildren() }
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query"
|
||||
// force re-render so that autoFocus is applied when this component is re-used
|
||||
key={this.props.children[0] ? this.props.children[0].key : ''} />
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -377,10 +417,10 @@ class RoomStateExplorer extends DevtoolsComponent {
|
|||
const stateKeys = Object.keys(stateGroup);
|
||||
|
||||
let onClickFn;
|
||||
if (stateKeys.length > 1) {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
} else if (stateKeys.length === 1) {
|
||||
if (stateKeys.length === 1 && stateKeys[0] === '') {
|
||||
onClickFn = this.onViewSourceClick(stateGroup[stateKeys[0]]);
|
||||
} else {
|
||||
onClickFn = this.browseEventType(evType);
|
||||
}
|
||||
|
||||
return <button className={classes} key={evType} onClick={onClickFn}>
|
||||
|
@ -485,7 +525,7 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
}
|
||||
|
||||
return <div className="mx_ViewSource">
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.state.event.event, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
|
|
|
@ -67,9 +67,10 @@ export default React.createClass({
|
|||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
cancelButton={this.props.cancelButton}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -36,7 +37,7 @@ export default React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
emailAddress: null,
|
||||
emailAddress: '',
|
||||
emailBusy: false,
|
||||
};
|
||||
},
|
||||
|
@ -127,6 +128,7 @@ export default React.createClass({
|
|||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
initialValue={this.state.emailAddress}
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
autoFocus="true"
|
||||
placeholder={_t("Email address")}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -79,15 +80,11 @@ export default React.createClass({
|
|||
Modal.createDialog(WarmFuzzy, {
|
||||
didSetEmail: res.didSetEmail,
|
||||
onFinished: () => {
|
||||
this._onContinueClicked();
|
||||
this.props.onFinished();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onContinueClicked: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onPasswordChangeError: function(err) {
|
||||
let errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
|
|
224
src/components/views/dialogs/ShareDialog.js
Normal file
224
src/components/views/dialogs/ShareDialog.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {makeEventPermalink, makeGroupPermalink, makeRoomPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
img: 'img/social/facebook.png',
|
||||
url: (url) => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||
}, {
|
||||
name: 'Twitter',
|
||||
img: 'img/social/twitter-2.png',
|
||||
url: (url) => `https://twitter.com/home?status=${url}`,
|
||||
}, /* // icon missing
|
||||
name: 'Google Plus',
|
||||
img: 'img/social/',
|
||||
url: (url) => `https://plus.google.com/share?url=${url}`,
|
||||
},*/ {
|
||||
name: 'LinkedIn',
|
||||
img: 'img/social/linkedin.png',
|
||||
url: (url) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`,
|
||||
}, {
|
||||
name: 'Reddit',
|
||||
img: 'img/social/reddit.png',
|
||||
url: (url) => `http://www.reddit.com/submit?url=${url}`,
|
||||
}, {
|
||||
name: 'email',
|
||||
img: 'img/social/email-1.png',
|
||||
url: (url) => `mailto:?body=${url}`,
|
||||
},
|
||||
];
|
||||
|
||||
export default class ShareDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
target: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Room),
|
||||
PropTypes.instanceOf(User),
|
||||
PropTypes.instanceOf(Group),
|
||||
PropTypes.instanceOf(RoomMember),
|
||||
PropTypes.instanceOf(MatrixEvent),
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onCopyClick = this.onCopyClick.bind(this);
|
||||
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
// MatrixEvent defaults to share linkSpecificEvent
|
||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||
};
|
||||
}
|
||||
|
||||
static _selectText(target) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
static onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
const {target} = e;
|
||||
ShareDialog._selectText(target);
|
||||
}
|
||||
|
||||
onCopyClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
ShareDialog._selectText(this.refs.link);
|
||||
|
||||
let successful;
|
||||
try {
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||
chevronOffset: 10,
|
||||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
}
|
||||
|
||||
onLinkSpecificEventCheckboxClick() {
|
||||
this.setState({
|
||||
linkSpecificEvent: !this.state.linkSpecificEvent,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
|
||||
let checkbox;
|
||||
|
||||
if (this.props.target instanceof Room) {
|
||||
title = _t('Share Room');
|
||||
|
||||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
if (events.length > 0) {
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to most recent message') }
|
||||
</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.roomId, events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.roomId);
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
title = _t('Share Community');
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to selected message') }
|
||||
</label>
|
||||
</div>;
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.getRoomId(), this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.getRoomId());
|
||||
}
|
||||
}
|
||||
|
||||
const encodedUrl = encodeURIComponent(matrixToUrl);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return <BaseDialog title={title}
|
||||
className='mx_ShareDialog'
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a ref="link"
|
||||
href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
>
|
||||
{ matrixToUrl }
|
||||
</a>
|
||||
<a href={matrixToUrl} className="mx_ShareDialog_matrixto_copy" onClick={this.onCopyClick}>
|
||||
{ _t('COPY') }
|
||||
<div> </div>
|
||||
</a>
|
||||
</div>
|
||||
{ checkbox }
|
||||
<hr />
|
||||
|
||||
<div className="mx_ShareDialog_split">
|
||||
<div className="mx_ShareDialog_qrcode_container">
|
||||
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo="img/matrix-m.svg" />
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
{
|
||||
socials.map((social) => <a rel="noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
name={social.name}
|
||||
href={social.url(encodedUrl)}
|
||||
className="mx_ShareDialog_social_icon"
|
||||
>
|
||||
<img src={social.img} alt={social.name} height={64} width={64} />
|
||||
</a>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
|
||||
export default class AppPermission extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -19,7 +20,7 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
const searchParams = new URLSearchParams(wurl.search);
|
||||
|
||||
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
|
||||
curl = url.parse(searchParams.get('url'));
|
||||
if (curl) {
|
||||
curl.search = curl.query = "";
|
||||
|
@ -33,19 +34,6 @@ export default class AppPermission extends React.Component {
|
|||
return curlString;
|
||||
}
|
||||
|
||||
isScalarWurl(wurl) {
|
||||
if (wurl && wurl.hostname && (
|
||||
wurl.hostname === 'scalar.vector.im' ||
|
||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||
wurl.hostname === 'demo.riot.im' ||
|
||||
wurl.hostname === 'localhost'
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let e2eWarningText;
|
||||
if (this.props.isRoomEncrypted) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/**
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -25,15 +26,15 @@ import PlatformPeg from '../../../PlatformPeg';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
const ENABLE_REACT_PERF = false;
|
||||
|
@ -41,9 +42,13 @@ const ENABLE_REACT_PERF = false;
|
|||
export default class AppTile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// The key used for PersistedElement
|
||||
this._persistKey = 'widget_' + this.props.id;
|
||||
|
||||
this.state = this._getNewState(props);
|
||||
|
||||
this._onWidgetAction = this._onWidgetAction.bind(this);
|
||||
this._onAction = this._onAction.bind(this);
|
||||
this._onMessage = this._onMessage.bind(this);
|
||||
this._onLoaded = this._onLoaded.bind(this);
|
||||
this._onEditClick = this._onEditClick.bind(this);
|
||||
|
@ -51,10 +56,10 @@ export default class AppTile extends React.Component {
|
|||
this._onSnapshotClick = this._onSnapshotClick.bind(this);
|
||||
this.onClickMenuBar = this.onClickMenuBar.bind(this);
|
||||
this._onMinimiseClick = this._onMinimiseClick.bind(this);
|
||||
this._onInitialLoad = this._onInitialLoad.bind(this);
|
||||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,9 +71,12 @@ export default class AppTile extends React.Component {
|
|||
_getNewState(newProps) {
|
||||
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
|
||||
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
|
||||
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
return {
|
||||
initialising: true, // True while we are mangling the widget URL
|
||||
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
|
||||
// True while the iframe content is loading
|
||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||
widgetUrl: this._addWurlParams(newProps.url),
|
||||
widgetPermissionId: widgetPermissionId,
|
||||
// Assume that widget has permission to load if we are the user who
|
||||
|
@ -77,9 +85,6 @@ export default class AppTile extends React.Component {
|
|||
error: null,
|
||||
deleting: false,
|
||||
widgetPageTitle: newProps.widgetPageTitle,
|
||||
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
|
||||
this.props.whitelistCapabilities : [],
|
||||
requestedCapabilities: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -89,7 +94,7 @@ export default class AppTile extends React.Component {
|
|||
* @return {Boolean} True if capability supported
|
||||
*/
|
||||
_hasCapability(capability) {
|
||||
return this.state.allowedCapabilities.some((c) => {return c === capability;});
|
||||
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -112,38 +117,15 @@ export default class AppTile extends React.Component {
|
|||
const params = qs.parse(u.query);
|
||||
// Append widget ID to query parameters
|
||||
params.widgetId = this.props.id;
|
||||
// Append current / parent URL
|
||||
params.parentUrl = window.location.href;
|
||||
// Append current / parent URL, minus the hash because that will change when
|
||||
// we view a different room (ie. may change for persistent widgets)
|
||||
params.parentUrl = window.location.href.split('#', 2)[0];
|
||||
u.search = undefined;
|
||||
u.query = params;
|
||||
|
||||
return u.format();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||
* @param {[type]} url URL to check
|
||||
* @return {Boolean} True if specified URL is a scalar URL
|
||||
*/
|
||||
isScalarUrl(url) {
|
||||
if (!url) {
|
||||
console.error('Scalar URL check failed. No URL specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||
if (!scalarUrls || scalarUrls.length == 0) {
|
||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||
}
|
||||
|
||||
for (let i = 0; i < scalarUrls.length; i++) {
|
||||
if (url.startsWith(scalarUrls[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.url);
|
||||
|
@ -166,30 +148,22 @@ export default class AppTile extends React.Component {
|
|||
window.addEventListener('message', this._onMessage, false);
|
||||
|
||||
// Widget action listeners
|
||||
this.dispatcherRef = dis.register(this._onWidgetAction);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Allow parents to access widget messaging
|
||||
if (this.props.collectWidgetMessaging) {
|
||||
this.props.collectWidgetMessaging(this.widgetMessaging);
|
||||
}
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Widget action listeners
|
||||
dis.unregister(this.dispatcherRef);
|
||||
|
||||
// Widget postMessage listeners
|
||||
try {
|
||||
if (this.widgetMessaging) {
|
||||
this.widgetMessaging.stop();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to stop listening for widgetMessaging events', e.message);
|
||||
}
|
||||
// Jitsi listener
|
||||
window.removeEventListener('message', this._onMessage);
|
||||
|
||||
// if it's not remaining on screen, get rid of the PersistedElement container
|
||||
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -199,7 +173,7 @@ export default class AppTile extends React.Component {
|
|||
setScalarToken() {
|
||||
this.setState({initialising: true});
|
||||
|
||||
if (!this.isScalarUrl(this.props.url)) {
|
||||
if (!WidgetUtils.isScalarUrl(this.props.url)) {
|
||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
|
@ -269,7 +243,12 @@ export default class AppTile extends React.Component {
|
|||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
if (!this.state.widgetUrl.startsWith(event.origin)) {
|
||||
const widgetUrlObj = url.parse(this.state.widgetUrl);
|
||||
const eventOrigin = url.parse(event.origin);
|
||||
if (
|
||||
eventOrigin.protocol !== widgetUrlObj.protocol ||
|
||||
eventOrigin.host !== widgetUrlObj.host
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -305,7 +284,7 @@ export default class AppTile extends React.Component {
|
|||
|
||||
_onSnapshotClick(e) {
|
||||
console.warn("Requesting widget snapshot");
|
||||
this.widgetMessaging.getScreenshot()
|
||||
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
|
||||
.catch((err) => {
|
||||
console.error("Failed to get screenshot", err);
|
||||
})
|
||||
|
@ -338,13 +317,18 @@ export default class AppTile extends React.Component {
|
|||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
|
||||
title: _t('Failed to remove widget'),
|
||||
description: _t('An error ocurred whilst trying to remove the widget from the room'),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({deleting: false});
|
||||
});
|
||||
|
@ -361,19 +345,20 @@ export default class AppTile extends React.Component {
|
|||
* Called when widget iframe has finished loading
|
||||
*/
|
||||
_onLoaded() {
|
||||
if (!this.widgetMessaging) {
|
||||
this._onInitialLoad();
|
||||
if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
|
||||
this._setupWidgetMessaging();
|
||||
}
|
||||
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on initial load of the widget iframe
|
||||
*/
|
||||
_onInitialLoad() {
|
||||
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
|
||||
_setupWidgetMessaging() {
|
||||
// FIXME: There's probably no reason to do this here: it should probably be done entirely
|
||||
// in ActiveWidgetStore.
|
||||
const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
|
||||
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
|
||||
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
|
||||
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
|
||||
requestedCapabilities = requestedCapabilities || [];
|
||||
|
||||
// Allow whitelisted capabilities
|
||||
|
@ -385,16 +370,15 @@ export default class AppTile extends React.Component {
|
|||
}, this.props.whitelistCapabilities);
|
||||
|
||||
if (requestedWhitelistCapabilies.length > 0 ) {
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
|
||||
requestedWhitelistCapabilies);
|
||||
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
|
||||
requestedWhitelistCapabilies,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO -- Add UI to warn about and optionally allow requested capabilities
|
||||
this.setState({
|
||||
requestedCapabilities,
|
||||
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
|
||||
});
|
||||
|
||||
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
|
||||
|
||||
if (this.props.onCapabilityRequest) {
|
||||
this.props.onCapabilityRequest(requestedCapabilities);
|
||||
|
@ -404,7 +388,7 @@ export default class AppTile extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onWidgetAction(payload) {
|
||||
_onAction(payload) {
|
||||
if (payload.widgetId === this.props.id) {
|
||||
switch (payload.action) {
|
||||
case 'm.sticker':
|
||||
|
@ -452,6 +436,11 @@ export default class AppTile extends React.Component {
|
|||
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
|
||||
localStorage.removeItem(this.state.widgetPermissionId);
|
||||
this.setState({hasPermissionToLoad: false});
|
||||
|
||||
// Force the widget to be non-persistent
|
||||
ActiveWidgetStore.destroyPersistentWidget();
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
PersistedElement.destroyElement(this._persistKey);
|
||||
}
|
||||
|
||||
formatAppTileName() {
|
||||
|
@ -519,6 +508,11 @@ export default class AppTile extends React.Component {
|
|||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick(e) {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
this.refs.appFrame.src = this.refs.appFrame.src;
|
||||
}
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
||||
|
@ -539,6 +533,8 @@ export default class AppTile extends React.Component {
|
|||
// (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;";
|
||||
|
||||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className="mx_AppLoading_spinner_fadeIn">
|
||||
|
@ -547,20 +543,20 @@ export default class AppTile extends React.Component {
|
|||
);
|
||||
if (this.state.initialising) {
|
||||
appTileBody = (
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ loadingElement }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.hasPermissionToLoad == true) {
|
||||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass}>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
appTileBody = (
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
|
@ -577,11 +573,24 @@ export default class AppTile extends React.Component {
|
|||
></iframe>
|
||||
</div>
|
||||
);
|
||||
// if the widget would be allowed to remian on screen, we must put it in
|
||||
// a PersistedElement from the get-go, otherwise the iframe will be
|
||||
// re-mounted later when we do.
|
||||
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
|
||||
const PersistedElement = sdk.getComponent("elements.PersistedElement");
|
||||
// Also wrap the PersistedElement in a div to fix the height, otherwise
|
||||
// AppTile's border is in the wrong place
|
||||
appTileBody = <div className="mx_AppTile_persistedWrapper">
|
||||
<PersistedElement persistKey={this._persistKey}>
|
||||
{appTileBody}
|
||||
</PersistedElement>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass}>
|
||||
<AppPermission
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
url={this.state.widgetUrl}
|
||||
|
@ -606,10 +615,20 @@ export default class AppTile extends React.Component {
|
|||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||
const popoutWidgetIcon = 'img/button-new-window.svg';
|
||||
const reloadWidgetIcon = 'img/button-refresh.svg';
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
let appTileClass;
|
||||
if (this.props.miniMode) {
|
||||
appTileClass = 'mx_AppTile_mini';
|
||||
} else if (this.props.fullWidth) {
|
||||
appTileClass = 'mx_AppTileFullWidth';
|
||||
} else {
|
||||
appTileClass = 'mx_AppTile';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
|
||||
<div className={appTileClass} id={this.props.id}>
|
||||
{ this.props.showMenubar &&
|
||||
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
|
||||
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
|
||||
|
@ -624,6 +643,16 @@ export default class AppTile extends React.Component {
|
|||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Reload widget */ }
|
||||
{ this.props.showReload && <TintableSvgButton
|
||||
src={reloadWidgetIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Reload widget')}
|
||||
onClick={this._onReloadWidgetClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Popout widget */ }
|
||||
{ this.props.showPopout && <TintableSvgButton
|
||||
src={popoutWidgetIcon}
|
||||
|
@ -683,6 +712,8 @@ AppTile.propTypes = {
|
|||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: PropTypes.bool,
|
||||
// Optional. If set, renders a smaller view of the widget
|
||||
miniMode: PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
|
@ -707,6 +738,11 @@ AppTile.propTypes = {
|
|||
showDelete: PropTypes.bool,
|
||||
// Optionally hide the popout widget icon
|
||||
showPopout: PropTypes.bool,
|
||||
// Optionally show the reload widget icon
|
||||
// This is not currently intended for use with production widgets. However
|
||||
// it can be useful when developing persistent widgets in order to avoid
|
||||
// having to reload all of riot to get new widget content.
|
||||
showReload: PropTypes.bool,
|
||||
// Widget capabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
|
@ -726,7 +762,9 @@ AppTile.defaultProps = {
|
|||
showMinimise: true,
|
||||
showDelete: true,
|
||||
showPopout: true,
|
||||
showReload: false,
|
||||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import TagTile from './TagTile';
|
||||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
|
|
|
@ -139,8 +139,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
{ editableItems }
|
||||
{ this.props.canEdit ?
|
||||
// This is slightly evil; we want a new instance of
|
||||
// EditableItem when the list grows. To make sure it's
|
||||
// reset to its initial state.
|
||||
<EditableItem
|
||||
key={-1}
|
||||
key={editableItems.length}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,15 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
|
||||
|
@ -66,9 +61,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.initialValue !== this.props.initialValue ||
|
||||
nextProps.initialValue !== this.value
|
||||
) {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
this.showPlaceholder(!this.value);
|
||||
|
@ -139,7 +132,7 @@ module.exports = React.createClass({
|
|||
this.showPlaceholder(false);
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
@ -156,9 +149,9 @@ module.exports = React.createClass({
|
|||
this.value = ev.target.textContent;
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
this.onFinish(ev);
|
||||
} else if (ev.key == "Escape") {
|
||||
} else if (ev.key === "Escape") {
|
||||
this.cancelEdit();
|
||||
}
|
||||
|
||||
|
@ -193,7 +186,7 @@ module.exports = React.createClass({
|
|||
const submit = (ev.key === "Enter") || shouldSubmit;
|
||||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
}, () => {
|
||||
if (this.value !== this.props.initialValue) {
|
||||
self.onValueChanged(submit);
|
||||
}
|
||||
|
@ -204,23 +197,35 @@ module.exports = React.createClass({
|
|||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
if (this.props.blurToCancel) {this.cancelEdit();} else {this.onFinish(ev, this.props.blurToSubmit);}
|
||||
if (this.props.blurToCancel) {
|
||||
this.cancelEdit();
|
||||
} else {
|
||||
this.onFinish(ev, this.props.blurToSubmit);
|
||||
}
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let editable_el;
|
||||
const {className, editable, initialValue, label, labelClassName} = this.props;
|
||||
let editableEl;
|
||||
|
||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||
if (!editable || (this.state.phase === this.Phases.Display && (label || labelClassName) && !this.value)) {
|
||||
// show the label
|
||||
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
||||
editableEl = <div className={className + " " + labelClassName} onClick={this.onClickDiv}>
|
||||
{ label || initialValue }
|
||||
</div>;
|
||||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
|
||||
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
||||
editableEl = <div ref="editable_div"
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur} />;
|
||||
}
|
||||
|
||||
return editable_el;
|
||||
return editableEl;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,30 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
const ContainerId = "mx_PersistedElement";
|
||||
function getContainer(containerId) {
|
||||
return document.getElementById(containerId);
|
||||
}
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContainerId);
|
||||
function getOrCreateContainer(containerId) {
|
||||
let container = getContainer(containerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = ContainerId;
|
||||
container.id = containerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// Greater than that of the ContextualMenu
|
||||
const PE_Z_INDEX = 3000;
|
||||
|
||||
/*
|
||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||
* in a container element appended to document.body.
|
||||
|
@ -50,14 +54,57 @@ const PE_Z_INDEX = 3000;
|
|||
* bounding rect as the parent of PE.
|
||||
*/
|
||||
export default class PersistedElement extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||
this.collectChild = this.collectChild.bind(this);
|
||||
this._repositionChild = this._repositionChild.bind(this);
|
||||
this._onAction = this._onAction.bind(this);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this._repositionChild);
|
||||
// Annoyingly, a resize observer is insufficient, since we also care
|
||||
// about when the element moves on the screen without changing its
|
||||
// dimensions. Doesn't look like there's a ResizeObserver equivalent
|
||||
// for this, so we bodge it by listening for document resize and
|
||||
// the timeline_resize action.
|
||||
window.addEventListener('resize', this._repositionChild);
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the DOM elements created when a PersistedElement with the given
|
||||
* persistKey was mounted. The DOM elements will be re-added if another
|
||||
* PeristedElement is mounted in the future.
|
||||
*
|
||||
* @param {string} persistKey Key used to uniquely identify this PersistedElement
|
||||
*/
|
||||
static destroyElement(persistKey) {
|
||||
const container = getContainer('mx_persistedElement_' + persistKey);
|
||||
if (container) {
|
||||
container.remove();
|
||||
}
|
||||
}
|
||||
|
||||
static isMounted(persistKey) {
|
||||
return Boolean(getContainer('mx_persistedElement_' + persistKey));
|
||||
}
|
||||
|
||||
collectChildContainer(ref) {
|
||||
if (this.childContainer) {
|
||||
this.resizeObserver.unobserve(this.childContainer);
|
||||
}
|
||||
this.childContainer = ref;
|
||||
if (ref) {
|
||||
this.resizeObserver.observe(ref);
|
||||
}
|
||||
}
|
||||
|
||||
collectChild(ref) {
|
||||
|
@ -75,6 +122,19 @@ export default class PersistedElement extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.updateChildVisibility(this.child, false);
|
||||
this.resizeObserver.disconnect();
|
||||
window.removeEventListener('resize', this._repositionChild);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
}
|
||||
|
||||
_onAction(payload) {
|
||||
if (payload.action === 'timeline_resize') {
|
||||
this._repositionChild();
|
||||
}
|
||||
}
|
||||
|
||||
_repositionChild() {
|
||||
this.updateChildPosition(this.child, this.childContainer);
|
||||
}
|
||||
|
||||
updateChild() {
|
||||
|
@ -97,18 +157,16 @@ export default class PersistedElement extends React.Component {
|
|||
left: parentRect.left + 'px',
|
||||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
zIndex: PE_Z_INDEX,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = <div ref={this.collectChild}>
|
||||
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer());
|
||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
96
src/components/views/elements/PersistentApp.js
Normal file
96
src/components/views/elements/PersistentApp.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PersistentApp',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(payload) {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
_onActiveWidgetStoreUpdate: function() {
|
||||
this.setState({
|
||||
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.persistentWidgetId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
if (this.state.roomId !== persistentWidgetInRoomId) {
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
|
||||
);
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
|
||||
const AppTile = sdk.getComponent('elements.AppTile');
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={true}
|
||||
room={persistentWidgetInRoom}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
show={true}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
showDelete={false}
|
||||
showMinimise={false}
|
||||
miniMode={true}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -22,12 +23,13 @@ import PropTypes from 'prop-types';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
|
||||
// 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 = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
|
||||
|
||||
const Pill = React.createClass({
|
||||
statics: {
|
||||
|
@ -45,6 +47,7 @@ const Pill = React.createClass({
|
|||
},
|
||||
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||
TYPE_GROUP_MENTION: 'TYPE_GROUP_MENTION',
|
||||
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
|
||||
},
|
||||
|
||||
|
@ -59,6 +62,8 @@ const Pill = React.createClass({
|
|||
room: PropTypes.instanceOf(Room),
|
||||
// Whether to include an avatar in the pill
|
||||
shouldShowPillAvatar: PropTypes.bool,
|
||||
// Whether to render this pill as if it were highlit by a selection
|
||||
isSelected: PropTypes.bool,
|
||||
},
|
||||
|
||||
|
||||
|
@ -81,12 +86,14 @@ const Pill = React.createClass({
|
|||
|
||||
// The member related to the user pill
|
||||
member: null,
|
||||
// The group related to the group pill
|
||||
group: null,
|
||||
// The room related to the room pill
|
||||
room: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
let regex = REGEX_MATRIXTO;
|
||||
if (nextProps.inMessage) {
|
||||
regex = REGEX_LOCAL_MATRIXTO;
|
||||
|
@ -109,9 +116,11 @@ const Pill = React.createClass({
|
|||
'@': Pill.TYPE_USER_MENTION,
|
||||
'#': Pill.TYPE_ROOM_MENTION,
|
||||
'!': Pill.TYPE_ROOM_MENTION,
|
||||
'+': Pill.TYPE_GROUP_MENTION,
|
||||
}[prefix];
|
||||
|
||||
let member;
|
||||
let group;
|
||||
let room;
|
||||
switch (pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
|
@ -140,8 +149,21 @@ const Pill = React.createClass({
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
try {
|
||||
group = await FlairStore.getGroupProfileCached(cli, resourceId);
|
||||
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||
group = {
|
||||
groupId: resourceId,
|
||||
avatarUrl: null,
|
||||
name: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({resourceId, pillType, member, room});
|
||||
this.setState({resourceId, pillType, member, group, room});
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -165,6 +187,9 @@ const Pill = React.createClass({
|
|||
getContent: () => {
|
||||
return {avatar_url: resp.avatar_url};
|
||||
},
|
||||
getDirectionalContent: function() {
|
||||
return this.getContent();
|
||||
},
|
||||
};
|
||||
this.setState({member});
|
||||
}).catch((err) => {
|
||||
|
@ -179,6 +204,7 @@ const Pill = React.createClass({
|
|||
});
|
||||
},
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
|
@ -229,10 +255,25 @@ const Pill = React.createClass({
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
if (this.state.group) {
|
||||
const {avatarUrl, groupId, name} = this.state.group;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
linkText = groupId;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
||||
}
|
||||
pillClass = 'mx_GroupPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const classes = classNames(pillClass, {
|
||||
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
|
||||
"mx_UserPill_selected": this.props.isSelected,
|
||||
});
|
||||
|
||||
if (this.state.pillType) {
|
||||
|
|
|
@ -160,7 +160,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
static makeThread(parentEv, onWidgetLoad, ref) {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !ReplyThread.getParentEventId(parentEv)) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div />;
|
||||
}
|
||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -103,14 +104,27 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
_openContextMenu: function(x, y, chevronOffset) {
|
||||
// Hide the (...) immediately
|
||||
this.setState({ hover: false });
|
||||
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
|
@ -119,17 +133,14 @@ export default React.createClass({
|
|||
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 });
|
||||
this._openContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
|
@ -164,7 +175,7 @@ export default React.createClass({
|
|||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||
return <AccessibleButton className={className} onClick={this.onClick} onContextMenu={this.onContextMenu}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
|
|
|
@ -54,9 +54,9 @@ export default class CookieBar extends React.Component {
|
|||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ this.props.policyUrl ? _t(
|
||||
"Help improve Riot by sending <UsageDataLink>usage data</UsageDataLink>? " +
|
||||
"This will use a cookie. " +
|
||||
"(See our <PolicyLink>cookie and privacy policies</PolicyLink>).",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie " +
|
||||
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
|
@ -76,10 +76,23 @@ export default class CookieBar extends React.Component {
|
|||
</a>
|
||||
,
|
||||
},
|
||||
) : _t("Help improve Riot by sending usage data? This will use a cookie.") }
|
||||
) : _t(
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie.",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
href="javascript:;"
|
||||
onClick={this.onUsageDataClicked}
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
|
||||
{ _t("Yes please") }
|
||||
{ _t("Yes, I want to help!") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -14,28 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
onUpdateClicked: function() {
|
||||
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog, {
|
||||
onFinished: (passwordChanged) => {
|
||||
if (!passwordChanged) {
|
||||
return;
|
||||
}
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({
|
||||
action: 'password_changed',
|
||||
});
|
||||
},
|
||||
});
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
98
src/components/views/globals/ServerLimitBar.js
Normal file
98
src/components/views/globals/ServerLimitBar.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { _td } from '../../../languageHandler';
|
||||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
|
||||
export default React.createClass({
|
||||
propTypes: {
|
||||
// 'hard' if the logged in user has been locked out, 'soft' if they haven't
|
||||
kind: PropTypes.string,
|
||||
adminContact: PropTypes.string,
|
||||
// The type of limit that has been hit.
|
||||
limitType: PropTypes.string.isRequired,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
kind: 'hard',
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const toolbarClasses = {
|
||||
'mx_MatrixToolbar': true,
|
||||
};
|
||||
|
||||
let adminContact;
|
||||
let limitError;
|
||||
if (this.props.kind === 'hard') {
|
||||
toolbarClasses['mx_MatrixToolbar_error'] = true;
|
||||
|
||||
adminContact = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using the service."),
|
||||
},
|
||||
);
|
||||
limitError = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
|
||||
'': _td("This homeserver has exceeded one of its resource limits."),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
toolbarClasses['mx_MatrixToolbar_info'] = true;
|
||||
adminContact = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to get this limit increased."),
|
||||
},
|
||||
);
|
||||
limitError = messageForResourceLimitError(
|
||||
this.props.limitType,
|
||||
this.props.adminContact,
|
||||
{
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit so " +
|
||||
"<b>some users will not be able to log in</b>.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits so " +
|
||||
"<b>some users will not be able to log in</b>.",
|
||||
),
|
||||
},
|
||||
{'b': sub => <b>{sub}</b>},
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classNames(toolbarClasses)}>
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{limitError}
|
||||
{' '}
|
||||
{adminContact}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,6 +21,9 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {createMenu} from "../../structures/ContextualMenu";
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupInviteTile',
|
||||
|
@ -32,6 +36,15 @@ export default React.createClass({
|
|||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return ({
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
menuDisplayed: false,
|
||||
selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
|
@ -39,6 +52,69 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMouseEnter: function() {
|
||||
const state = {hover: true};
|
||||
// Only allow non-guests to access the context menu
|
||||
if (!this.context.matrixClient.isGuest()) {
|
||||
state.badgeHover = true;
|
||||
}
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
onMouseLeave: function() {
|
||||
this.setState({
|
||||
badgeHover: false,
|
||||
hover: false,
|
||||
});
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
|
||||
|
||||
createMenu(GroupInviteTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
group: this.props.group,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
@ -49,19 +125,40 @@ export default React.createClass({
|
|||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
const label = <EmojiText
|
||||
element="div"
|
||||
title={this.props.group.groupId}
|
||||
className="mx_RoomTile_name mx_RoomTile_badgeShown"
|
||||
dir="auto"
|
||||
>
|
||||
const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', {
|
||||
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
|
||||
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
|
||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_badgeButton': badgeEllipsis,
|
||||
});
|
||||
|
||||
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
|
||||
let tooltip;
|
||||
if (this.props.collapsed && this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
|
||||
<AccessibleButton className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
|
@ -69,6 +166,7 @@ export default React.createClass({
|
|||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -187,7 +187,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
|
|
|
@ -69,7 +69,7 @@ export default React.createClass({
|
|||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
const input = <input type="checkbox"
|
||||
onClick={this._onPublicityToggle}
|
||||
onChange={this._onPublicityToggle}
|
||||
checked={this.state.isGroupPublicised}
|
||||
/>;
|
||||
const labelText = !this.state.ready ? _t("Loading...") :
|
||||
|
|
|
@ -22,6 +22,7 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
function nop() {}
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
@ -81,7 +82,7 @@ const GroupTile = React.createClass({
|
|||
) : null;
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown}>
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
|
|
|
@ -28,6 +28,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
*/
|
||||
class PasswordLogin extends React.Component {
|
||||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
|
@ -56,33 +57,64 @@ class PasswordLogin extends React.Component {
|
|||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
||||
this.isLoginEmpty = this.isLoginEmpty.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._passwordField = null;
|
||||
this._loginField = null;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||
field_input_incorrect(this._passwordField);
|
||||
field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
||||
this.props.onSubmit(
|
||||
'', // XXX: Synapse breaks if you send null here:
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
|
||||
let username = ''; // XXX: Synapse breaks if you send null here:
|
||||
let phoneCountry = null;
|
||||
let phoneNumber = null;
|
||||
let error;
|
||||
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The email field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The user name field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
phoneCountry = this.state.phoneCountry;
|
||||
phoneNumber = this.state.phoneNumber;
|
||||
if (!phoneNumber) {
|
||||
error = _t('The phone number field must not be blank.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.props.onError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.password) {
|
||||
this.props.onError(_t('The password field must not be blank.'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
null,
|
||||
null,
|
||||
username,
|
||||
phoneCountry,
|
||||
phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
}
|
||||
|
@ -93,6 +125,7 @@ class PasswordLogin extends React.Component {
|
|||
}
|
||||
|
||||
onLoginTypeChange(loginType) {
|
||||
this.props.onError(null); // send a null error to clear any error messages
|
||||
this.setState({
|
||||
loginType: loginType,
|
||||
username: "", // Reset because email and username use the same state
|
||||
|
@ -126,8 +159,10 @@ class PasswordLogin extends React.Component {
|
|||
switch (loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
classes.mx_Login_email = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="email_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
|
@ -139,8 +174,10 @@ class PasswordLogin extends React.Component {
|
|||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.mx_Login_username = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="username_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
|
@ -153,14 +190,14 @@ class PasswordLogin extends React.Component {
|
|||
autoFocus
|
||||
disabled={disabled}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
classes.mx_Login_phoneNumberField = true;
|
||||
classes.mx_Login_field_has_prefix = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
|
||||
return <div className="mx_Login_phoneSection">
|
||||
<CountryDropdown
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
ref="phone_country"
|
||||
onOptionChange={this.onPhoneCountryChanged}
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
|
@ -169,7 +206,7 @@ class PasswordLogin extends React.Component {
|
|||
/>
|
||||
<input
|
||||
className={classNames(classes)}
|
||||
ref="phoneNumber"
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="phone_input"
|
||||
type="text"
|
||||
name="phoneNumber"
|
||||
|
@ -180,6 +217,17 @@ class PasswordLogin extends React.Component {
|
|||
disabled={disabled}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLoginEmpty() {
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
return !this.state.username;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
return !this.state.phoneCountry || !this.state.phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,7 +255,7 @@ class PasswordLogin extends React.Component {
|
|||
const pwFieldClass = classNames({
|
||||
mx_Login_field: true,
|
||||
mx_Login_field_disabled: matrixIdText === '',
|
||||
error: this.props.loginIncorrect,
|
||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
@ -258,6 +306,7 @@ PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
|||
|
||||
PasswordLogin.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
|
|
|
@ -327,6 +327,7 @@ module.exports = React.createClass({
|
|||
// will have the correct name when the user tries to download it.
|
||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
rel: "noopener",
|
||||
target: "_blank",
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
}, "*");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,48 +16,43 @@ 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 { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import ImageUtils from '../../../ImageUtils';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class extends React.Component {
|
||||
displayName: 'MImageBody'
|
||||
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* called when the image has loaded */
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
/* the maximum image height to use */
|
||||
maxImageHeight: PropTypes.number,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onImageError = this.onImageError.bind(this);
|
||||
this.onImageLoad = this.onImageLoad.bind(this);
|
||||
this.onImageEnter = this.onImageEnter.bind(this);
|
||||
this.onImageLeave = this.onImageLeave.bind(this);
|
||||
this.onClientSync = this.onClientSync.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.fixupHeight = this.fixupHeight.bind(this);
|
||||
this._isGif = this._isGif.bind(this);
|
||||
|
||||
this.state = {
|
||||
|
@ -65,6 +61,9 @@ export default class extends React.Component {
|
|||
decryptedBlob: null,
|
||||
error: null,
|
||||
imgError: false,
|
||||
imgLoaded: false,
|
||||
loadedImageDimensions: null,
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -88,7 +87,7 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onClick(ev) {
|
||||
if (ev.button == 0 && !ev.metaKey) {
|
||||
if (ev.button === 0 && !ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const httpUrl = this._getContentUrl();
|
||||
|
@ -119,6 +118,8 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onImageEnter(e) {
|
||||
this.setState({ hover: true });
|
||||
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
|
@ -127,6 +128,8 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onImageLeave(e) {
|
||||
this.setState({ hover: false });
|
||||
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
|
@ -142,6 +145,16 @@ export default class extends React.Component {
|
|||
|
||||
onImageLoad() {
|
||||
this.props.onWidgetLoad();
|
||||
|
||||
let loadedImageDimensions;
|
||||
|
||||
if (this.refs.image) {
|
||||
const { naturalWidth, naturalHeight } = this.refs.image;
|
||||
|
||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||
}
|
||||
|
||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||
}
|
||||
|
||||
_getContentUrl() {
|
||||
|
@ -161,9 +174,7 @@ export default class extends React.Component {
|
|||
return this.state.decryptedThumbnailUrl;
|
||||
}
|
||||
return this.state.decryptedUrl;
|
||||
} else if (content.info &&
|
||||
content.info.mimetype == "image/svg+xml" &&
|
||||
content.info.thumbnail_url) {
|
||||
} else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
|
||||
// special case to return client-generated thumbnails for SVGs, if any,
|
||||
// given we deliberately don't thumbnail them serverside to prevent
|
||||
// billion lol attacks and similar
|
||||
|
@ -176,11 +187,10 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
if (content.info && content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
|
@ -207,7 +217,6 @@ export default class extends React.Component {
|
|||
});
|
||||
}).done();
|
||||
}
|
||||
this.fixupHeight();
|
||||
this._afterComponentDidMount();
|
||||
}
|
||||
|
||||
|
@ -218,7 +227,6 @@ export default class extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
this._afterComponentWillUnmount();
|
||||
|
||||
|
@ -235,59 +243,113 @@ export default class extends React.Component {
|
|||
_afterComponentWillUnmount() {
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
if (payload.action === "timeline_resize") {
|
||||
this.fixupHeight();
|
||||
}
|
||||
}
|
||||
|
||||
fixupHeight() {
|
||||
if (!this.refs.image) {
|
||||
console.warn(`Refusing to fix up height on ${this.displayName} with no image element`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const timelineWidth = this.refs.body.offsetWidth;
|
||||
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
|
||||
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
|
||||
|
||||
// FIXME: this will break on clientside generated thumbnails (as per e2e rooms)
|
||||
// which may well be much smaller than the 800x600 bounding box.
|
||||
|
||||
// FIXME: It will also break really badly for images with broken or missing thumbnails
|
||||
|
||||
// FIXME: Because we don't know what size of thumbnail the server's actually going to send
|
||||
// us, we can't even really layout the page nicely for it. Instead we have to assume
|
||||
// it'll target 800x600 and we'll downsize if needed to make things fit.
|
||||
|
||||
// console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
|
||||
let thumbHeight = null;
|
||||
if (content.info) {
|
||||
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
||||
}
|
||||
this.refs.image.style.height = thumbHeight + "px";
|
||||
// console.log("Image height now", thumbHeight);
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let infoWidth;
|
||||
let infoHeight;
|
||||
|
||||
if (content && content.info && content.info.w && content.info.h) {
|
||||
infoWidth = content.info.w;
|
||||
infoHeight = content.info.h;
|
||||
} else {
|
||||
// Whilst the image loads, display nothing.
|
||||
//
|
||||
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||
//
|
||||
// By doing this, the image "pops" into the timeline, but is still restricted
|
||||
// by the same width and height logic below.
|
||||
if (!this.state.loadedImageDimensions) {
|
||||
return this.wrapImage(contentUrl,
|
||||
<img style={{display: 'none'}} src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
infoWidth = this.state.loadedImageDimensions.naturalWidth;
|
||||
infoHeight = this.state.loadedImageDimensions.naturalHeight;
|
||||
}
|
||||
|
||||
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||
// The maximum width of the thumbnail, as dictated by its natural
|
||||
// maximum height.
|
||||
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||
|
||||
let img = null;
|
||||
let placeholder = null;
|
||||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
placeholder = <img src="img/spinner.gif" alt={content.body} width="32" height="32" />;
|
||||
} else if (!this.state.imgLoaded) {
|
||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||
placeholder = this.getPlaceholder();
|
||||
}
|
||||
|
||||
const showPlaceholder = Boolean(placeholder);
|
||||
|
||||
if (thumbUrl && !this.state.imgError) {
|
||||
// Restrict the width of the thumbnail here, otherwise it will fill the container
|
||||
// which has the same width as the timeline
|
||||
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||
img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />;
|
||||
}
|
||||
|
||||
const thumbnail = (
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px" }} >
|
||||
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
|
||||
<div style={{ paddingBottom: (100 * infoHeight / infoWidth) + '%' }} />
|
||||
|
||||
{ showPlaceholder &&
|
||||
<div className="mx_MImageBody_thumbnail" style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: infoWidth + "px",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail_spinner">
|
||||
{ placeholder }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
|
||||
{ img }
|
||||
</div>
|
||||
|
||||
{ this.state.hover && this.getTooltip() }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
return this.wrapImage(contentUrl, thumbnail);
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
wrapImage(contentUrl, children) {
|
||||
return <a href={contentUrl} onClick={this.onClick}>
|
||||
{children}
|
||||
</a>;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getPlaceholder() {
|
||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getTooltip() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getFileBody() {
|
||||
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -302,25 +364,6 @@ export default class extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"alignItems": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
|
||||
"margin": "auto",
|
||||
}} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
|
@ -329,6 +372,12 @@ export default class extends React.Component {
|
|||
thumbUrl = this._getThumbUrl();
|
||||
}
|
||||
|
||||
return this._messageContent(contentUrl, thumbUrl, content);
|
||||
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||
const fileBody = this.getFileBody();
|
||||
|
||||
return <span className="mx_MImageBody" ref="body">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,145 +16,42 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MImageBody from './MImageBody';
|
||||
import sdk from '../../../index';
|
||||
import TintableSVG from '../elements/TintableSvg';
|
||||
|
||||
export default class MStickerBody extends MImageBody {
|
||||
displayName: 'MStickerBody'
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onImageLoad = this._onImageLoad.bind(this);
|
||||
}
|
||||
|
||||
_onMouseEnter() {
|
||||
this.setState({showTooltip: true});
|
||||
}
|
||||
|
||||
_onMouseLeave() {
|
||||
this.setState({showTooltip: false});
|
||||
}
|
||||
|
||||
_onImageLoad() {
|
||||
this.setState({
|
||||
placeholderClasses: 'mx_MStickerBody_placeholder_invisible',
|
||||
});
|
||||
const hidePlaceholderTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
}, 500);
|
||||
this.setState({hidePlaceholderTimer});
|
||||
if (this.props.onWidgetLoad) {
|
||||
this.props.onWidgetLoad();
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentDidMount() {
|
||||
if (this.refs.image.complete) {
|
||||
// Image already loaded
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
placeholderClasses: '.mx_MStickerBody_placeholder_invisible',
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
} else {
|
||||
// Image not already loaded
|
||||
this.setState({
|
||||
placeholderVisible: true,
|
||||
placeholderClasses: '',
|
||||
imageClasses: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentWillUnmount() {
|
||||
if (this.state.hidePlaceholderTimer) {
|
||||
clearTimeout(this.state.hidePlaceholderTimer);
|
||||
this.setState({hidePlaceholderTimer: null});
|
||||
}
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let tooltip;
|
||||
const tooltipBody = (
|
||||
this.props.mxEvent &&
|
||||
this.props.mxEvent.getContent() &&
|
||||
this.props.mxEvent.getContent().body) ?
|
||||
this.props.mxEvent.getContent().body : null;
|
||||
if (this.state.showTooltip && tooltipBody) {
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
tooltip = <RoomTooltip
|
||||
className='mx_RoleButton_tooltip'
|
||||
label={tooltipBody} />;
|
||||
}
|
||||
|
||||
const gutterSize = 0;
|
||||
let placeholderSize = 75;
|
||||
let placeholderFixupHeight = '100px';
|
||||
let placeholderTop = 0;
|
||||
let placeholderLeft = 0;
|
||||
|
||||
if (content.info) {
|
||||
placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px';
|
||||
placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px';
|
||||
placeholderFixupHeight = content.info.h + 'px';
|
||||
}
|
||||
|
||||
// The pixel size of sticker images is generally larger than their intended display
|
||||
// size so they render at native reolution on HiDPI displays. We therefore need to
|
||||
// explicity set the size so they render at the intended size.
|
||||
// XXX: This will be clobberred when we run fixupHeight(), but we need to do it
|
||||
// here otherwise the stickers are momentarily displayed at the pixel size.
|
||||
const imageStyle = {
|
||||
height: content.info.h,
|
||||
// leave the browser the calculate the width automatically
|
||||
};
|
||||
|
||||
placeholderSize = placeholderSize + 'px';
|
||||
|
||||
// Body 'ref' required by MImageBody
|
||||
return (
|
||||
<span className='mx_MStickerBody' ref='body'
|
||||
style={{
|
||||
height: placeholderFixupHeight,
|
||||
}}>
|
||||
<div className={'mx_MStickerBody_image_container'}>
|
||||
{ this.state.placeholderVisible &&
|
||||
<div
|
||||
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
|
||||
style={{
|
||||
top: placeholderTop,
|
||||
left: placeholderLeft,
|
||||
}}
|
||||
>
|
||||
<TintableSVG
|
||||
src={'img/icons-show-stickers.svg'}
|
||||
width={placeholderSize}
|
||||
height={placeholderSize} />
|
||||
</div> }
|
||||
<img
|
||||
className={'mx_MStickerBody_image ' + this.state.imageClasses}
|
||||
src={contentUrl}
|
||||
style={imageStyle}
|
||||
ref='image'
|
||||
alt={content.body}
|
||||
onLoad={this._onImageLoad}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
/>
|
||||
{ tooltip }
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty to prevent default behaviour of MImageBody
|
||||
onClick() {
|
||||
}
|
||||
|
||||
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
|
||||
// which is added by mx_MStickerBody_wrapper
|
||||
wrapImage(contentUrl, children) {
|
||||
return <div className="mx_MStickerBody_wrapper"> { children } </div>;
|
||||
}
|
||||
|
||||
// Placeholder to show in place of the sticker image if
|
||||
// img onLoad hasn't fired yet.
|
||||
getPlaceholder() {
|
||||
const TintableSVG = sdk.getComponent('elements.TintableSvg');
|
||||
return <TintableSVG src="img/icons-show-stickers.svg" width="75" height="75" />;
|
||||
}
|
||||
|
||||
// Tooltip to show on mouse over
|
||||
getTooltip() {
|
||||
const content = this.props.mxEvent && this.props.mxEvent.getContent();
|
||||
|
||||
if (!content || !content.body || !content.info || !content.info.w) return null;
|
||||
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
|
||||
<RoomTooltip label={content.body} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// Don't show "Download this_file.png ..."
|
||||
getFileBody() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,12 +147,7 @@ module.exports = React.createClass({
|
|||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"align-items": "center",
|
||||
"justify-items": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
|
||||
</div>
|
||||
</span>
|
||||
|
|
|
@ -39,8 +39,11 @@ module.exports = React.createClass({
|
|||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: PropTypes.func,
|
||||
|
||||
/* the shsape of the tile, used */
|
||||
/* the shape of the tile, used */
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
/* the maximum image height to use, if the event is an image */
|
||||
maxImageHeight: PropTypes.number,
|
||||
},
|
||||
|
||||
getEventTileOps: function() {
|
||||
|
@ -59,17 +62,24 @@ module.exports = React.createClass({
|
|||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
};
|
||||
const evTypes = {
|
||||
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||
};
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const type = this.props.mxEvent.getType();
|
||||
const msgtype = content.msgtype;
|
||||
let BodyType = UnknownBody;
|
||||
if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (this.props.mxEvent.getType() === 'm.sticker') {
|
||||
BodyType = sdk.getComponent('messages.MStickerBody');
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
if (!this.props.mxEvent.isRedacted()) {
|
||||
// only resolve BodyType if event is not redacted
|
||||
if (type && evTypes[type]) {
|
||||
BodyType = evTypes[type];
|
||||
} else if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
}
|
||||
}
|
||||
|
||||
return <BodyType
|
||||
|
@ -78,6 +88,7 @@ module.exports = React.createClass({
|
|||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -72,14 +72,12 @@ export default React.createClass({
|
|||
|
||||
_updateRelatedGroups() {
|
||||
if (this.unmounted) return;
|
||||
const relatedGroupsEvent = this.context.matrixClient
|
||||
.getRoom(this.props.mxEvent.getRoomId())
|
||||
.currentState
|
||||
.getStateEvents('m.room.related_groups', '');
|
||||
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||
this.setState({
|
||||
relatedGroups: relatedGroupsEvent ?
|
||||
relatedGroupsEvent.getContent().groups || []
|
||||
: [],
|
||||
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import * as ContextualMenu from '../../structures/ContextualMenu';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {host as matrixtoHost} from '../../../matrix-to';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -202,7 +203,7 @@ module.exports = React.createClass({
|
|||
// update the current node with one that's now taken its place
|
||||
node = pillContainer;
|
||||
}
|
||||
} else if (node.nodeType == Node.TEXT_NODE) {
|
||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
||||
const Pill = sdk.getComponent('elements.Pill');
|
||||
|
||||
let currentTextNode = node;
|
||||
|
@ -231,6 +232,12 @@ module.exports = React.createClass({
|
|||
if (atRoomRule && pushProcessor.ruleMatchesEvent(atRoomRule, this.props.mxEvent)) {
|
||||
// Now replace all those nodes with Pills
|
||||
for (const roomNotifTextNode of roomNotifTextNodes) {
|
||||
// Set the next node to be processed to the one after the node
|
||||
// we're adding now, since we've just inserted nodes into the structure
|
||||
// we're iterating over.
|
||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
|
||||
const pillContainer = document.createElement('span');
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pill = <Pill
|
||||
|
@ -242,12 +249,6 @@ module.exports = React.createClass({
|
|||
|
||||
ReactDOM.render(pill, pillContainer);
|
||||
roomNotifTextNode.parentNode.replaceChild(pillContainer, roomNotifTextNode);
|
||||
|
||||
// Set the next node to be processed to the one after the node
|
||||
// we're adding now, since we've just inserted nodes into the structure
|
||||
// we're iterating over.
|
||||
// Note we've checked roomNotifTextNodes.length > 0 so we'll do this at least once
|
||||
node = roomNotifTextNode.nextSibling;
|
||||
}
|
||||
// Nothing else to do for a text node (and we don't need to advance
|
||||
// the loop pointer because we did it above)
|
||||
|
@ -304,7 +305,7 @@ module.exports = React.createClass({
|
|||
// never preview matrix.to links (if anything we should give a smart
|
||||
// preview of the room/user they point to: nobody needs to be reminded
|
||||
// what the matrix.to site looks like).
|
||||
if (host == 'matrix.to') return false;
|
||||
if (host === matrixtoHost) return false;
|
||||
|
||||
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
|
||||
// it's a "foo.pl" style link
|
||||
|
@ -336,10 +337,21 @@ module.exports = React.createClass({
|
|||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
e.target.onmouseout = close;
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
};
|
||||
p.appendChild(button);
|
||||
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
p.parentNode.replaceChild(div, p);
|
||||
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(p);
|
||||
div.appendChild(button);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -422,8 +434,7 @@ module.exports = React.createClass({
|
|||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
const stripReply = SettingsStore.isFeatureEnabled("feature_rich_quoting") &&
|
||||
ReplyThread.getParentEventId(mxEvent);
|
||||
const stripReply = ReplyThread.getParentEventId(mxEvent);
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||
// Part of Replies fallback support
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Travis Ralston
|
||||
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.
|
||||
|
@ -15,6 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require("../../../index");
|
||||
|
@ -29,6 +31,10 @@ module.exports = React.createClass({
|
|||
room: PropTypes.object,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
const promises = [];
|
||||
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||
|
@ -39,42 +45,58 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const roomId = this.props.room.roomId;
|
||||
const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId);
|
||||
|
||||
let previewsForAccount = null;
|
||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
}
|
||||
|
||||
let previewsForRoom = null;
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={this.props.room.roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
|
||||
if (!isEncrypted) {
|
||||
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
|
||||
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||
if (accountEnabled) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||
})
|
||||
);
|
||||
} else if (accountEnabled) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||
})
|
||||
);
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
}
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("In encrypted rooms, like this one, URL previews are disabled by default to ensure that your " +
|
||||
"homeserver (where the previews are generated) cannot gather information about links you see in " +
|
||||
"this room.")
|
||||
);
|
||||
}
|
||||
|
||||
const previewsForRoomAccount = (
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
|
||||
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={this.props.room.roomId}
|
||||
roomId={roomId}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsSelf"
|
||||
/>
|
||||
|
@ -83,8 +105,13 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>{ _t("URL Previews") }</h3>
|
||||
|
||||
<label>{ previewsForAccount }</label>
|
||||
<div>
|
||||
{ _t('When someone puts a URL in their message, a URL preview can be shown to give more ' +
|
||||
'information about that link such as the title, description, and an image from the website.') }
|
||||
</div>
|
||||
<div>
|
||||
{ previewsForAccount }
|
||||
</div>
|
||||
{ previewsForRoom }
|
||||
<label>{ previewsForRoomAccount }</label>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -27,8 +28,8 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
@ -57,6 +58,7 @@ module.exports = React.createClass({
|
|||
componentWillMount: function() {
|
||||
ScalarMessaging.startListening();
|
||||
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
|
||||
WidgetEchoStore.on('update', this._updateApps);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -82,6 +84,7 @@ module.exports = React.createClass({
|
|||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
|
||||
}
|
||||
WidgetEchoStore.removeListener('update', this._updateApps);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
},
|
||||
|
||||
|
@ -94,15 +97,7 @@ module.exports = React.createClass({
|
|||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
// When opening the app drawer when there aren't any apps,
|
||||
// auto-launch the integrations manager to skip the awkward
|
||||
// click on "Add widget"
|
||||
if (action.show) {
|
||||
const apps = this._getApps();
|
||||
if (apps.length === 0) {
|
||||
this._launchManageIntegrations();
|
||||
}
|
||||
|
||||
localStorage.removeItem(hideWidgetKey);
|
||||
} else {
|
||||
// Store hidden state of widget
|
||||
|
@ -114,55 +109,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a URI according to a set of template variables. Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
encodeUri: function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key]),
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
},
|
||||
|
||||
_initAppConfig: function(appId, app, sender) {
|
||||
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||
const params = {
|
||||
'$matrix_user_id': this.props.userId,
|
||||
'$matrix_room_id': this.props.room.roomId,
|
||||
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||
|
||||
// TODO: Namespace themes through some standard
|
||||
'$theme': SettingsStore.getValue("theme"),
|
||||
};
|
||||
|
||||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
|
||||
if (app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
|
||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
||||
}
|
||||
|
||||
app.url = this.encodeUri(app.url, params);
|
||||
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||
|
||||
return app;
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||
return;
|
||||
|
@ -171,15 +117,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getApps: function() {
|
||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
if (!appsStateEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return appsStateEvents.filter((ev) => {
|
||||
return ev.getContent().type && ev.getContent().url;
|
||||
}).map((ev) => {
|
||||
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
const widgets = WidgetEchoStore.getEchoedRoomWidgets(
|
||||
this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
|
||||
);
|
||||
return widgets.map((ev) => {
|
||||
return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -227,26 +169,25 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id);
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
||||
|
||||
const apps = this.state.apps.map(
|
||||
(app, index, arr) => {
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
show={this.props.showApps}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={enableScreenshots ? ["m.capability.screenshot"] : []}
|
||||
/>);
|
||||
});
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={arr.length<2 ? true : false}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
show={this.props.showApps}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
/>);
|
||||
});
|
||||
|
||||
let addWidget;
|
||||
if (this.props.showApps &&
|
||||
|
@ -265,10 +206,22 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
}
|
||||
|
||||
let spinner;
|
||||
if (
|
||||
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
||||
this.props.room.roomId,
|
||||
WidgetUtils.getRoomWidgets(this.props.room),
|
||||
)
|
||||
) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
spinner = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
|
||||
<div id='apps' className='mx_AppsContainer'>
|
||||
{ apps }
|
||||
{ spinner }
|
||||
</div>
|
||||
{ this._canUserModify() && addWidget }
|
||||
</div>
|
||||
|
|
|
@ -114,7 +114,7 @@ export default class Autocomplete extends React.Component {
|
|||
|
||||
processQuery(query, selection) {
|
||||
return this.autocompleter.getCompletions(
|
||||
query, selection, this.state.forceComplete,
|
||||
query, selection, this.state.forceComplete
|
||||
).then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
|
@ -216,12 +216,12 @@ export default class Autocomplete extends React.Component {
|
|||
return done.promise;
|
||||
}
|
||||
|
||||
onCompletionClicked(): boolean {
|
||||
if (this.countCompletions() === 0 || this.state.selectionOffset === COMPOSER_SELECTED) {
|
||||
onCompletionClicked(selectionOffset: number): boolean {
|
||||
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.props.onConfirm(this.state.completionList[this.state.selectionOffset - 1]);
|
||||
this.props.onConfirm(this.state.completionList[selectionOffset - 1]);
|
||||
this.hide();
|
||||
|
||||
return true;
|
||||
|
@ -263,17 +263,14 @@ export default class Autocomplete extends React.Component {
|
|||
const componentPosition = position;
|
||||
position++;
|
||||
|
||||
const onMouseMove = () => this.setSelection(componentPosition);
|
||||
const onClick = () => {
|
||||
this.setSelection(componentPosition);
|
||||
this.onCompletionClicked();
|
||||
this.onCompletionClicked(componentPosition);
|
||||
};
|
||||
|
||||
return React.cloneElement(completion.component, {
|
||||
key: i,
|
||||
ref: `completion${position - 1}`,
|
||||
className,
|
||||
onMouseMove,
|
||||
onClick,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -34,6 +34,7 @@ const ContextualMenu = require('../../structures/ContextualMenu');
|
|||
import dis from '../../../dispatcher';
|
||||
import {makeEventPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
|
@ -55,6 +56,7 @@ const stateEventTileTypes = {
|
|||
'm.room.topic': 'messages.TextualEvent',
|
||||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
'm.room.server_acl': 'messages.TextualEvent',
|
||||
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
};
|
||||
|
@ -442,26 +444,27 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClicked};
|
||||
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
return <E2ePadlockUndecryptable {...props} />;
|
||||
} else if (ev.isEncrypted()) {
|
||||
if (this.state.verified) {
|
||||
return <E2ePadlockVerified {...props} />;
|
||||
} else {
|
||||
return <E2ePadlockUnverified {...props} />;
|
||||
}
|
||||
} else {
|
||||
// XXX: if the event is being encrypted (ie eventSendStatus ===
|
||||
// encrypting), it might be nice to show something other than the
|
||||
// open padlock?
|
||||
}
|
||||
|
||||
// if the event is not encrypted, but it's an e2e room, show the
|
||||
// open padlock
|
||||
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
|
||||
if (e2eEnabled) {
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted()) {
|
||||
return this.state.verified ? <E2ePadlockVerified {...props} /> : <E2ePadlockUnverified {...props} />;
|
||||
}
|
||||
|
||||
if (this.props.matrixClient.isRoomEncrypted(ev.getRoomId())) {
|
||||
// else if room is encrypted
|
||||
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
||||
if (ev.status === EventStatus.ENCRYPTING) {
|
||||
return <E2ePadlockEncrypting {...props} />;
|
||||
}
|
||||
if (ev.status === EventStatus.NOT_SENT) {
|
||||
return <E2ePadlockNotSent {...props} />;
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
}
|
||||
|
||||
// no padlock needed
|
||||
|
@ -482,15 +485,22 @@ module.exports = withMatrixClient(React.createClass({
|
|||
// Info messages are basically information about commands processed on a room
|
||||
const isInfoMessage = (eventType !== 'm.room.message' && eventType !== 'm.sticker');
|
||||
|
||||
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
|
||||
const tileHandler = getHandlerTile(this.props.mxEvent);
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
if (!EventTileType) {
|
||||
throw new Error("Event type not supported");
|
||||
if (!tileHandler) {
|
||||
const {mxEvent} = this.props;
|
||||
console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
|
||||
return <div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
|
||||
<div className="mx_EventTile_line">
|
||||
{ _t('This event could not be displayed') }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
const EventTileType = sdk.getComponent(tileHandler);
|
||||
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -608,13 +618,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</EmojiText>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
|
@ -691,7 +702,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{ avatar }
|
||||
{ sender }
|
||||
<div className="mx_EventTile_line">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
|
@ -708,6 +718,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
{ keyRequestInfo }
|
||||
{ editButton }
|
||||
</div>
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
// the need for further z-indexing chaos)
|
||||
}
|
||||
{ avatar }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -715,9 +731,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
}));
|
||||
|
||||
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||
function isMessageEvent(ev) {
|
||||
return (messageTypes.includes(ev.getType()));
|
||||
}
|
||||
|
||||
module.exports.haveTileForEvent = function(e) {
|
||||
// Only messages have a tile (black-rectangle) if redacted
|
||||
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
|
||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
const handler = getHandlerTile(e);
|
||||
if (handler === undefined) return false;
|
||||
|
@ -736,6 +758,14 @@ function E2ePadlockUndecryptable(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function E2ePadlockEncrypting(props) {
|
||||
return <E2ePadlock alt={_t("Encrypting")} src="img/e2e-encrypting.svg" width="10" height="12" {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockNotSent(props) {
|
||||
return <E2ePadlock alt={_t("Encrypted, not sent")} src="img/e2e-not_sent.svg" width="10" height="12" {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockVerified(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Encrypted by a verified device")}
|
||||
|
|
|
@ -332,13 +332,40 @@ module.exports = withMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMuteToggle: function() {
|
||||
_warnSelfDemote: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
return new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
</div>,
|
||||
button: _t("Demote"),
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onMuteToggle: async function() {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === this.props.matrixClient.getUserId()) {
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
|
@ -436,7 +463,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
onPowerChange: function(powerLevel) {
|
||||
onPowerChange: async function(powerLevel) {
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
|
@ -455,20 +482,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
}
|
||||
},
|
||||
});
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -478,7 +497,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
|
@ -578,7 +598,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
onMemberAvatarClick: function() {
|
||||
const member = this.props.member;
|
||||
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
|
||||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
if (!avatarUrl) return;
|
||||
|
||||
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
|
||||
|
@ -632,6 +652,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
onShareUserClick: function() {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
|
||||
target: this.props.member,
|
||||
});
|
||||
},
|
||||
|
||||
_renderUserOptions: function() {
|
||||
const cli = this.props.matrixClient;
|
||||
const member = this.props.member;
|
||||
|
@ -705,13 +732,18 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null;
|
||||
const shareUserButton = (
|
||||
<AccessibleButton onClick={this.onShareUserClick} className="mx_MemberInfo_field">
|
||||
{ _t('Share Link to User') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("User Options") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{ readReceiptButton }
|
||||
{ shareUserButton }
|
||||
{ insertPillButton }
|
||||
{ ignoreButton }
|
||||
{ inviteUserButton }
|
||||
|
@ -742,15 +774,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
for (const roomId of dmRooms) {
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(this.props.matrixClient.credentials.userId);
|
||||
|
||||
const myMembership = room.getMyMembership();
|
||||
// not a DM room if we have are not joined
|
||||
if (!me.membership || me.membership !== 'join') continue;
|
||||
// not a DM room if they are not joined
|
||||
if (myMembership !== 'join') continue;
|
||||
|
||||
const them = this.props.member;
|
||||
// not a DM room if they are not joined
|
||||
if (!them.membership || them.membership !== 'join') continue;
|
||||
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === 'invite';
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
|
@ -759,7 +791,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership === "invite"}
|
||||
isInvite={false}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
@ -902,7 +934,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -26,6 +26,17 @@ import RoomViewStore from '../../../stores/RoomViewStore';
|
|||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
|
||||
const formatButtonList = [
|
||||
_td("bold"),
|
||||
_td("italic"),
|
||||
_td("deleted"),
|
||||
_td("underlined"),
|
||||
_td("inline-code"),
|
||||
_td("block-quote"),
|
||||
_td("bulleted-list"),
|
||||
_td("numbered-list"),
|
||||
];
|
||||
|
||||
export default class MessageComposer extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -35,7 +46,6 @@ export default class MessageComposer extends React.Component {
|
|||
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
|
||||
this.onInputContentChanged = this.onInputContentChanged.bind(this);
|
||||
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
|
||||
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
|
||||
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
|
||||
|
@ -44,13 +54,10 @@ export default class MessageComposer extends React.Component {
|
|||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||
|
||||
this.state = {
|
||||
autocompleteQuery: '',
|
||||
selection: null,
|
||||
inputState: {
|
||||
style: [],
|
||||
marks: [],
|
||||
blockType: null,
|
||||
isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||
wordCount: 0,
|
||||
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||
},
|
||||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
||||
|
@ -64,6 +71,23 @@ export default class MessageComposer extends React.Component {
|
|||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
||||
MatrixClientPeg.get().on("event", this.onEvent);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
}
|
||||
|
||||
_waitForOwnMember() {
|
||||
// if we have the member already, do that
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
if (me) {
|
||||
this.setState({me});
|
||||
return;
|
||||
}
|
||||
// Otherwise, wait for member loading to finish and then update the member for the avatar.
|
||||
// The members should already be loading, and loadMembersIfNeeded
|
||||
// will return the promise for the existing operation
|
||||
this.props.room.loadMembersIfNeeded().then(() => {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
this.setState({me});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -159,61 +183,20 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
// _startCallApp(isAudioConf) {
|
||||
// dis.dispatch({
|
||||
// action: 'appsDrawer',
|
||||
// show: true,
|
||||
// });
|
||||
|
||||
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
// let appsStateEvent = {};
|
||||
// if (appsStateEvents) {
|
||||
// appsStateEvent = appsStateEvents.getContent();
|
||||
// }
|
||||
// if (!appsStateEvent.videoConf) {
|
||||
// appsStateEvent.videoConf = {
|
||||
// type: 'jitsi',
|
||||
// // FIXME -- This should not be localhost
|
||||
// url: 'http://localhost:8000/jitsi.html',
|
||||
// data: {
|
||||
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||
// isAudioConf: isAudioConf,
|
||||
// },
|
||||
// };
|
||||
// MatrixClientPeg.get().sendStateEvent(
|
||||
// this.props.room.roomId,
|
||||
// 'im.vector.modular.widgets',
|
||||
// appsStateEvent,
|
||||
// '',
|
||||
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||
// }
|
||||
// }
|
||||
|
||||
onCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(false);
|
||||
}
|
||||
|
||||
onVoiceCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: "voice",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(true);
|
||||
}
|
||||
|
||||
onInputContentChanged(content: string, selection: {start: number, end: number}) {
|
||||
this.setState({
|
||||
autocompleteQuery: content,
|
||||
selection,
|
||||
});
|
||||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
|
@ -226,7 +209,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", event) {
|
||||
onFormatButtonClicked(name, event) {
|
||||
event.preventDefault();
|
||||
this.messageComposerInput.onFormatButtonClicked(name, event);
|
||||
}
|
||||
|
@ -238,11 +221,10 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
onToggleMarkdownClicked(e) {
|
||||
e.preventDefault(); // don't steal focus from the editor!
|
||||
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
|
||||
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled);
|
||||
}
|
||||
|
||||
render() {
|
||||
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
const uploadInputStyle = {display: 'none'};
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
@ -250,11 +232,13 @@ export default class MessageComposer extends React.Component {
|
|||
|
||||
const controls = [];
|
||||
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={me} width={24} height={24} />
|
||||
</div>,
|
||||
);
|
||||
if (this.state.me) {
|
||||
controls.push(
|
||||
<div key="controls_avatar" className="mx_MessageComposer_avatar">
|
||||
<MemberAvatar member={this.state.me} width={24} height={24} />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
let e2eImg, e2eTitle, e2eClass;
|
||||
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
|
@ -314,14 +298,14 @@ export default class MessageComposer extends React.Component {
|
|||
</div>
|
||||
);
|
||||
|
||||
const formattingButton = (
|
||||
const formattingButton = this.state.inputState.isRichTextEnabled ? (
|
||||
<img className="mx_MessageComposer_formatting"
|
||||
title={_t("Show Text Formatting Toolbar")}
|
||||
src="img/button-text-formatting.svg"
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
|
||||
key="controls_formatting" />
|
||||
);
|
||||
) : null;
|
||||
|
||||
let placeholderText;
|
||||
if (this.state.isQuoting) {
|
||||
|
@ -348,7 +332,6 @@ export default class MessageComposer extends React.Component {
|
|||
room={this.props.room}
|
||||
placeholder={placeholderText}
|
||||
onFilesPasted={this.uploadFiles}
|
||||
onContentChanged={this.onInputContentChanged}
|
||||
onInputStateChanged={this.onInputStateChanged} />,
|
||||
formattingButton,
|
||||
stickerpickerButton,
|
||||
|
@ -365,11 +348,14 @@ export default class MessageComposer extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
const {style, blockType} = this.state.inputState;
|
||||
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
|
||||
(name) => {
|
||||
const active = style.includes(name) || blockType === name;
|
||||
const suffix = active ? '-o-n' : '';
|
||||
let formatBar;
|
||||
if (this.state.showFormatting && this.state.inputState.isRichTextEnabled) {
|
||||
const {marks, blockType} = this.state.inputState;
|
||||
const formatButtons = formatButtonList.map((name) => {
|
||||
// special-case to match the md serializer and the special-case in MessageComposerInput.js
|
||||
const markName = name === 'inline-code' ? 'code' : name;
|
||||
const active = marks.some(mark => mark.type === markName) || blockType === name;
|
||||
const suffix = active ? '-on' : '';
|
||||
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
|
||||
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
|
||||
return <img className={className}
|
||||
|
@ -378,8 +364,25 @@ export default class MessageComposer extends React.Component {
|
|||
key={name}
|
||||
src={`img/button-text-${name}${suffix}.svg`}
|
||||
height="17" />;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
formatBar =
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar">
|
||||
{ formatButtons }
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={this.state.inputState.isRichTextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichTextEnabled}.png`} />
|
||||
<img title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer">
|
||||
|
@ -388,20 +391,7 @@ export default class MessageComposer extends React.Component {
|
|||
{ controls }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MessageComposer_formatbar_wrapper">
|
||||
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
|
||||
{ formatButtons }
|
||||
<div style={{flex: 1}}></div>
|
||||
<img title={this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
|
||||
onMouseDown={this.onToggleMarkdownClicked}
|
||||
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
|
||||
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
|
||||
<img title={_t("Hide Text Formatting Toolbar")}
|
||||
onClick={this.onToggleFormattingClicked}
|
||||
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
|
||||
src="img/icon-text-cancel.svg" />
|
||||
</div>
|
||||
</div>
|
||||
{ formatBar }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -22,6 +22,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import MessageEvent from "../messages/MessageEvent";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {formatFullDate} from '../../../DateUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PinnedEventTile',
|
||||
|
@ -80,11 +81,20 @@ module.exports = React.createClass({
|
|||
{ unpinButton }
|
||||
</div>
|
||||
|
||||
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
|
||||
<span className="mx_PinnedEventTile_senderAvatar">
|
||||
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
|
||||
</span>
|
||||
<span className="mx_PinnedEventTile_sender">
|
||||
{ sender.name }
|
||||
</span>
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" />
|
||||
<span className="mx_PinnedEventTile_timestamp">
|
||||
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
|
||||
</span>
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150}
|
||||
onWidgetLoad={() => {}} // we need to give this, apparently
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -39,6 +39,19 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._updatePinnedMessages();
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
|
||||
}
|
||||
},
|
||||
|
||||
_onStateEvent: function(ev) {
|
||||
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
|
||||
this._updatePinnedMessages();
|
||||
}
|
||||
},
|
||||
|
||||
_updatePinnedMessages: function() {
|
||||
|
|
|
@ -149,6 +149,13 @@ module.exports = React.createClass({
|
|||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
onShareRoomClick: function(ev) {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
target: this.props.room,
|
||||
});
|
||||
},
|
||||
|
||||
_hasUnreadPins: function() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
|
@ -379,6 +386,14 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let shareRoomButton;
|
||||
if (this.props.inRoom) {
|
||||
shareRoomButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShareRoomClick} title={_t('Share room')}>
|
||||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let rightPanelButtons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanelButtons =
|
||||
|
@ -400,6 +415,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017, 2018 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
'use strict';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -33,7 +35,12 @@ import RoomListStore from '../../../stores/RoomListStore';
|
|||
import GroupStore from '../../../stores/GroupStore';
|
||||
|
||||
const HIDE_CONFERENCE_CHANS = true;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
|
||||
|
||||
function labelForTagName(tagName) {
|
||||
if (tagName.startsWith('u.')) return tagName.slice(2);
|
||||
return tagName;
|
||||
}
|
||||
|
||||
function phraseForSection(section) {
|
||||
switch (section) {
|
||||
|
@ -90,7 +97,7 @@ module.exports = React.createClass({
|
|||
};
|
||||
// All rooms that should be kept in the room list when filtering.
|
||||
// By default, show all rooms.
|
||||
this._visibleRooms = MatrixClientPeg.get().getRooms();
|
||||
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
|
||||
|
||||
// Listen to updates to group data. RoomList cares about members and rooms in order
|
||||
// to filter the room list when group tags are selected.
|
||||
|
@ -295,7 +302,7 @@ module.exports = React.createClass({
|
|||
this._visibleRooms = Array.from(roomSet);
|
||||
} else {
|
||||
// Show all rooms
|
||||
this._visibleRooms = MatrixClientPeg.get().getRooms();
|
||||
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
|
||||
}
|
||||
this._delayedRefreshRoomList();
|
||||
},
|
||||
|
@ -340,8 +347,8 @@ module.exports = React.createClass({
|
|||
if (!taggedRoom) {
|
||||
return;
|
||||
}
|
||||
const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId);
|
||||
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) {
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, myUserId, this.props.ConferenceHandler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -444,6 +451,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.stickies) return;
|
||||
|
||||
const self = this;
|
||||
let scrollStuckOffset = 0;
|
||||
// Scroll to the passed in position, i.e. a header was clicked and in a scroll to state
|
||||
|
@ -583,14 +592,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_makeGroupInviteTiles() {
|
||||
_makeGroupInviteTiles(filter) {
|
||||
const ret = [];
|
||||
const lcFilter = filter && filter.toLowerCase();
|
||||
|
||||
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
|
||||
for (const group of MatrixClientPeg.get().getGroups()) {
|
||||
if (group.myMembership !== 'invite') continue;
|
||||
|
||||
ret.push(<GroupInviteTile key={group.groupId} group={group} />);
|
||||
const {groupId, name, myMembership} = group;
|
||||
// filter to only groups in invite state and group_id starts with filter or group name includes it
|
||||
if (myMembership !== 'invite') continue;
|
||||
if (lcFilter && !groupId.toLowerCase().startsWith(lcFilter) &&
|
||||
!(name && name.toLowerCase().includes(lcFilter))) continue;
|
||||
ret.push(<GroupInviteTile key={groupId} group={group} collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -604,13 +617,17 @@ module.exports = React.createClass({
|
|||
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
// XXX: we can't detect device-level (localStorage) settings onChange as the SettingsStore does not notify
|
||||
// so checking on every render is the sanest thing at this time.
|
||||
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
|
||||
|
||||
const self = this;
|
||||
return (
|
||||
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
autoshow={true} onScroll={self._whenScrolling} onResize={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={[]}
|
||||
extraTiles={this._makeGroupInviteTiles()}
|
||||
extraTiles={this._makeGroupInviteTiles(self.props.searchFilter)}
|
||||
label={_t('Community Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
|
@ -619,6 +636,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||
|
@ -631,6 +649,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['m.favourite']}
|
||||
|
@ -643,7 +662,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
||||
label={_t('People')}
|
||||
|
@ -657,7 +677,8 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
||||
label={_t('Rooms')}
|
||||
|
@ -669,13 +690,14 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(STANDARD_TAGS_REGEX)) {
|
||||
return <RoomSubList list={self.state.lists[tagName]}
|
||||
key={tagName}
|
||||
label={tagName}
|
||||
label={labelForTagName(tagName)}
|
||||
tagName={tagName}
|
||||
emptyContent={this._getEmptyContent(tagName)}
|
||||
editable={true}
|
||||
|
@ -684,7 +706,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />;
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />;
|
||||
}
|
||||
}) }
|
||||
|
||||
|
@ -698,9 +721,17 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
||||
emptyContent={self.props.collapsed ? null :
|
||||
<div className="mx_RoomList_emptySubListTip_container">
|
||||
<div className="mx_RoomList_emptySubListTip">
|
||||
{ _t('You have no historical rooms') }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
label={_t('Historical')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
|
@ -708,10 +739,23 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
startAsHidden={true}
|
||||
showSpinner={self.state.isLoadingLeftRooms}
|
||||
onHeaderClick= {self.onArchivedHeaderClick}
|
||||
onHeaderClick={self.onArchivedHeaderClick}
|
||||
incomingCall={self.state.incomingCall}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['m.server_notice']}
|
||||
label={_t('System Alerts')}
|
||||
tagName="m.lowpriority"
|
||||
editable={false}
|
||||
order="recent"
|
||||
incomingCall={self.state.incomingCall}
|
||||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={false} />
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
|
|
|
@ -98,15 +98,11 @@ module.exports = React.createClass({
|
|||
</div>);
|
||||
}
|
||||
|
||||
const myMember = this.props.room ? this.props.room.currentState.members[
|
||||
MatrixClientPeg.get().credentials.userId
|
||||
] : null;
|
||||
const kicked = (
|
||||
myMember &&
|
||||
myMember.membership == 'leave' &&
|
||||
myMember.events.member.getSender() != MatrixClientPeg.get().credentials.userId
|
||||
);
|
||||
const banned = myMember && myMember.membership == 'ban';
|
||||
const myMember = this.props.room ?
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId()) :
|
||||
null;
|
||||
const kicked = myMember && myMember.isKicked();
|
||||
const banned = myMember && myMember && myMember.membership == 'ban';
|
||||
|
||||
if (this.props.inviterName) {
|
||||
let emailMatchBlock;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -395,7 +396,17 @@ module.exports = React.createClass({
|
|||
powerLevels["events"] = Object.assign({}, this.state.powerLevels["events"] || {});
|
||||
powerLevels["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
|
||||
} else {
|
||||
powerLevels[powerLevelKey] = value;
|
||||
const keyPath = powerLevelKey.split('.');
|
||||
let parentObj;
|
||||
let currentObj = powerLevels;
|
||||
for (const key of keyPath) {
|
||||
if (!currentObj[key]) {
|
||||
currentObj[key] = {};
|
||||
}
|
||||
parentObj = currentObj;
|
||||
currentObj = currentObj[key];
|
||||
}
|
||||
parentObj[keyPath[keyPath.length - 1]] = value;
|
||||
}
|
||||
this.setState({
|
||||
powerLevels,
|
||||
|
@ -664,6 +675,10 @@ module.exports = React.createClass({
|
|||
desc: _t('To remove other users\' messages, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
"notifications.room": {
|
||||
desc: _t('To notify everyone in the room, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const banLevel = parseIntWithDefault(powerLevels.ban, powerLevelDescriptors.ban.defaultValue);
|
||||
|
@ -779,15 +794,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
let leaveButton = null;
|
||||
const myMember = this.props.room.getMember(myUserId);
|
||||
if (myMember) {
|
||||
if (myMember.membership === "join") {
|
||||
const myMemberShip = this.props.room.getMyMembership();
|
||||
if (myMemberShip) {
|
||||
if (myMemberShip === "join") {
|
||||
leaveButton = (
|
||||
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onLeaveClick}>
|
||||
{ _t('Leave room') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (myMember.membership === "leave") {
|
||||
} else if (myMemberShip === "leave") {
|
||||
leaveButton = (
|
||||
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onForgetClick}>
|
||||
{ _t('Forget room') }
|
||||
|
@ -865,7 +880,16 @@ module.exports = React.createClass({
|
|||
const powerSelectors = Object.keys(powerLevelDescriptors).map((key, index) => {
|
||||
const descriptor = powerLevelDescriptors[key];
|
||||
|
||||
const value = parseIntWithDefault(powerLevels[key], descriptor.defaultValue);
|
||||
const keyPath = key.split('.');
|
||||
let currentObj = powerLevels;
|
||||
for (const prop of keyPath) {
|
||||
if (currentObj === undefined) {
|
||||
break;
|
||||
}
|
||||
currentObj = currentObj[prop];
|
||||
}
|
||||
|
||||
const value = parseIntWithDefault(currentObj, descriptor.defaultValue);
|
||||
return <div key={index} className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">
|
||||
{ descriptor.desc }
|
||||
|
@ -1016,7 +1040,8 @@ module.exports = React.createClass({
|
|||
|
||||
<h3>{ _t('Advanced') }</h3>
|
||||
<div className="mx_RoomSettings_settings">
|
||||
{ _t('This room\'s internal ID is') } <code>{ this.props.room.roomId }</code>
|
||||
{ _t('Internal room ID: ') } <code>{ this.props.room.roomId }</code><br />
|
||||
{ _t('Room version number: ') } <code>{ this.props.room.getVersion() }</code>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,19 +16,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
const classNames = require('classnames');
|
||||
import classNames from 'classnames';
|
||||
import dis from '../../../dispatcher';
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
const sdk = require('../../../index');
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
const RoomNotifs = require('../../../RoomNotifs');
|
||||
const FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import sdk from '../../../index';
|
||||
import {createMenu} from '../../structures/ContextualMenu';
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -72,16 +71,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_shouldShowMentionBadge: function() {
|
||||
return this.state.notifState != RoomNotifs.MUTE;
|
||||
return this.state.notifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
_isDirectMessageRoom: function(roomId) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (dmRooms) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return Boolean(dmRooms);
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room) {
|
||||
|
@ -99,7 +94,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAccountData: function(accountDataEvent) {
|
||||
if (accountDataEvent.getType() == 'm.push_rules') {
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
this.setState({
|
||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
});
|
||||
|
@ -187,6 +182,32 @@ module.exports = React.createClass({
|
|||
this.badgeOnMouseLeave();
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
|
||||
createMenu(RoomTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
this.props.refreshSubList();
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
badgeOnMouseEnter: function() {
|
||||
// Only allow non-guests to access the context menu
|
||||
// and only change it if it needs to change
|
||||
|
@ -200,43 +221,29 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Only allow none guests to access the context menu
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
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(RoomTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
self.props.refreshSubList();
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
}
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const me = this.props.room.currentState.members[myUserId];
|
||||
|
||||
const isInvite = this.props.room.getMyMembership() === "invite";
|
||||
const notificationCount = this.state.notificationCount;
|
||||
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
|
||||
|
||||
|
@ -250,7 +257,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||
'mx_RoomTile_invited': isInvite,
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
|
@ -268,7 +275,6 @@ module.exports = React.createClass({
|
|||
let name = this.state.roomName;
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let badge;
|
||||
let badgeContent;
|
||||
|
||||
if (this.state.badgeHover || this.state.menuDisplayed) {
|
||||
|
@ -280,7 +286,7 @@ module.exports = React.createClass({
|
|||
badgeContent = '\u200B';
|
||||
}
|
||||
|
||||
badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let label;
|
||||
|
@ -301,7 +307,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
} else if (this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} dir="auto" />;
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
}
|
||||
|
||||
//var incomingCallBox;
|
||||
|
@ -312,16 +318,22 @@ module.exports = React.createClass({
|
|||
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
let directMessageIndicator;
|
||||
let dmIndicator;
|
||||
if (this._isDirectMessageRoom(this.props.room.roomId)) {
|
||||
directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
|
||||
dmIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
|
||||
}
|
||||
|
||||
return <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
return <AccessibleButton tabIndex="0"
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ directMessageIndicator }
|
||||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
|
|
|
@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var dis = require('../../../dispatcher');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
@ -77,25 +76,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderTooltip: function() {
|
||||
var label = this.props.room ? this.props.room.name : this.props.label;
|
||||
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
// tooltips chevron
|
||||
var parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
var style = {};
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames(
|
||||
"mx_RoomTooltip", this.props.tooltipClassName,
|
||||
);
|
||||
const tooltipClasses = classNames("mx_RoomTooltip", this.props.tooltipClassName);
|
||||
|
||||
var tooltip = (
|
||||
<div className={tooltipClasses} style={style} >
|
||||
<div className="mx_RoomTooltip_chevron"></div>
|
||||
{ label }
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_RoomTooltip_chevron" />
|
||||
{ this.props.label }
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Widgets from '../../../utils/widgets';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -24,9 +23,15 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
// We sit in a context menu, so the persisted element container needs to float
|
||||
// above it, so it needs a greater z-index than the ContextMenu
|
||||
const STICKERPICKER_Z_INDEX = 5000;
|
||||
|
||||
export default class Stickerpicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -39,8 +44,6 @@ export default class Stickerpicker extends React.Component {
|
|||
this._onResize = this._onResize.bind(this);
|
||||
this._onFinished = this._onFinished.bind(this);
|
||||
|
||||
this._collectWidgetMessaging = this._collectWidgetMessaging.bind(this);
|
||||
|
||||
this.popoverWidth = 300;
|
||||
this.popoverHeight = 300;
|
||||
|
||||
|
@ -67,7 +70,7 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
this.setState({showStickers: false});
|
||||
Widgets.removeStickerpickerWidgets().then(() => {
|
||||
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
console.error('Failed to remove sticker picker widget', e);
|
||||
|
@ -119,7 +122,7 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
_updateWidget() {
|
||||
const stickerpickerWidget = Widgets.getStickerpickerWidgets()[0];
|
||||
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
|
||||
this.setState({
|
||||
stickerpickerWidget,
|
||||
widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
|
||||
|
@ -162,17 +165,11 @@ export default class Stickerpicker extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_collectWidgetMessaging(widgetMessaging) {
|
||||
this._appWidgetMessaging = widgetMessaging;
|
||||
|
||||
// Do this now instead of in componentDidMount because we might not have had the
|
||||
// reference to widgetMessaging when mounting
|
||||
this._sendVisibilityToWidget(true);
|
||||
}
|
||||
|
||||
_sendVisibilityToWidget(visible) {
|
||||
if (this._appWidgetMessaging && visible !== this._prevSentVisibility) {
|
||||
this._appWidgetMessaging.sendVisibility(visible);
|
||||
if (!this.state.stickerpickerWidget) return;
|
||||
const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
|
||||
if (widgetMessaging && visible !== this._prevSentVisibility) {
|
||||
widgetMessaging.sendVisibility(visible);
|
||||
this._prevSentVisibility = visible;
|
||||
}
|
||||
}
|
||||
|
@ -211,9 +208,8 @@ export default class Stickerpicker extends React.Component {
|
|||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement>
|
||||
<PersistedElement persistKey="stickerPicker" style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<AppTile
|
||||
collectWidgetMessaging={this._collectWidgetMessaging}
|
||||
id={stickerpickerWidget.id}
|
||||
url={stickerpickerWidget.content.url}
|
||||
name={stickerpickerWidget.content.name}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,36 +15,28 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ChangeDisplayName',
|
||||
|
||||
_getDisplayName: function() {
|
||||
_getDisplayName: async function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId).then(function(result) {
|
||||
let displayname = result.displayname;
|
||||
if (!displayname) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
displayname = "Guest " + MatrixClientPeg.get().getUserIdLocalpart();
|
||||
} else {
|
||||
displayname = MatrixClientPeg.get().getUserIdLocalpart();
|
||||
}
|
||||
}
|
||||
return displayname;
|
||||
}, function(error) {
|
||||
try {
|
||||
const res = await cli.getProfileInfo(cli.getUserId());
|
||||
return res.displayname;
|
||||
} catch (e) {
|
||||
throw new Error("Failed to fetch display name");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_changeDisplayName: function(new_displayname) {
|
||||
_changeDisplayName: function(newDisplayname) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.setDisplayName(new_displayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name");
|
||||
return cli.setDisplayName(newDisplayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name", e);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
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.
|
||||
|
@ -14,14 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const Modal = require("../../../Modal");
|
||||
const sdk = require("../../../index");
|
||||
|
||||
import dis from "../../../dispatcher";
|
||||
import Promise from 'bluebird';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -143,6 +143,9 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this._optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -92,7 +92,8 @@ module.exports = React.createClass({
|
|||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
const PersistentApp = sdk.getComponent('elements.PersistentApp');
|
||||
return <PersistentApp />;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue