Merge remote-tracking branch 'upstream/develop' into feature_confetti#14676

This commit is contained in:
Steffen Kolmer 2020-10-19 13:15:33 +02:00
commit c86964cd5e
478 changed files with 21997 additions and 13673 deletions

View file

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import classNames from 'classnames';
import {Resizable} from "re-resizable";
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import * as ScalarMessaging from '../../../ScalarMessaging';
@ -30,54 +30,50 @@ import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import WidgetStore from "../../../stores/WidgetStore";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
export default createReactClass({
displayName: 'AppsDrawer',
propTypes: {
export default class AppsDrawer extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
room: PropTypes.object.isRequired,
resizeNotifier: PropTypes.instanceOf(ResizeNotifier).isRequired,
showApps: PropTypes.bool, // Should apps be rendered
hide: PropTypes.bool, // If rendered, should apps drawer be visible
},
};
getDefaultProps: () => ({
static defaultProps = {
showApps: true,
hide: false,
}),
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
apps: this._getApps(),
};
},
}
componentDidMount: function() {
componentDidMount() {
ScalarMessaging.startListening();
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
WidgetEchoStore.on('update', this._updateApps);
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
this.dispatcherRef = dis.register(this.onAction);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
ScalarMessaging.stopListening();
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
WidgetEchoStore.removeListener('update', this._updateApps);
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
// Room has changed probably, update apps
this._updateApps();
},
}
onAction: function(action) {
onAction = (action) => {
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
switch (action.action) {
case 'appsDrawer':
@ -93,87 +89,58 @@ export default createReactClass({
break;
}
},
};
onRoomStateEvents: function(ev, state) {
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
return;
}
this._updateApps();
},
_getApps = () => WidgetStore.instance.getApps(this.props.room, true);
_getApps: function() {
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.getSender(), ev.getRoomId(), ev.getId(),
);
});
},
_updateApps: function() {
const apps = this._getApps();
_updateApps = () => {
this.setState({
apps: apps,
apps: this._getApps(),
});
},
};
_canUserModify: function() {
_canUserModify() {
try {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
} catch (err) {
console.error(err);
return false;
}
},
}
_launchManageIntegrations: function() {
_launchManageIntegrations() {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll();
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
}
},
}
onClickAddWidget: function(e) {
onClickAddWidget = (e) => {
e.preventDefault();
// Display a warning dialog if the max number of widgets have already been added to the room
const apps = this._getApps();
if (apps && apps.length >= MAX_WIDGETS) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
console.error(errorMsg);
Modal.createDialog(ErrorDialog, {
title: _t('Cannot add any more widgets'),
description: _t('The maximum permitted number of widgets have already been added to this room.'),
});
return;
}
this._launchManageIntegrations();
},
};
render: function() {
render() {
const apps = this.state.apps.map((app, index, arr) => {
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
return (<AppTile
key={app.id}
app={app}
fullWidth={arr.length<2 ? true : false}
fullWidth={arr.length < 2}
room={this.props.room}
userId={this.props.userId}
show={this.props.showApps}
creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
/>);
});
if (apps.length == 0) {
return <div></div>;
if (apps.length === 0) {
return <div />;
}
let addWidget;
@ -202,14 +169,67 @@ export default createReactClass({
spinner = <Loader />;
}
const classes = classNames({
"mx_AppsDrawer": true,
"mx_AppsDrawer_fullWidth": apps.length < 2,
"mx_AppsDrawer_minimised": !this.props.showApps,
});
return (
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
<div id='apps' className='mx_AppsContainer'>
<div className={classes}>
<PersistentVResizer
id={"apps-drawer_" + this.props.room.roomId}
minHeight={100}
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
handleClass="mx_AppsContainer_resizerHandle"
className="mx_AppsContainer"
resizeNotifier={this.props.resizeNotifier}
>
{ apps }
{ spinner }
</div>
</PersistentVResizer>
{ this._canUserModify() && addWidget }
</div>
);
},
});
}
}
const PersistentVResizer = ({
id,
minHeight,
maxHeight,
className,
handleWrapperClass,
handleClass,
resizeNotifier,
children,
}) => {
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
const [resizing, setResizing] = useState(false);
return <Resizable
size={{height: Math.min(height, maxHeight)}}
minHeight={minHeight}
maxHeight={maxHeight}
onResizeStart={() => {
if (!resizing) setResizing(true);
resizeNotifier.startResizing();
}}
onResize={() => {
resizeNotifier.notifyTimelineHeightChanged();
}}
onResizeStop={(e, dir, ref, d) => {
setHeight(height + d.height);
if (resizing) setResizing(false);
resizeNotifier.stopResizing();
}}
handleWrapperClass={handleWrapperClass}
handleClasses={{bottom: handleClass}}
className={classNames(className, {
mx_AppsDrawer_resizing: resizing,
})}
enable={{bottom: true}}
>
{ children }
</Resizable>;
};

View file

@ -17,7 +17,7 @@ limitations under the License.
import React, {createRef, KeyboardEvent} from 'react';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import {flatMap} from "lodash";
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
import {Room} from 'matrix-js-sdk/src/models/room';

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from '../../../index';
import dis from "../../../dispatcher/dispatcher";
@ -29,27 +28,19 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import CallView from "../voip/CallView";
import {UIFeature} from "../../../settings/UIFeature";
export default createReactClass({
displayName: 'AuxPanel',
propTypes: {
export default class AuxPanel extends React.Component {
static propTypes = {
// js-sdk room object
room: PropTypes.object.isRequired,
userId: PropTypes.string.isRequired,
showApps: PropTypes.bool, // Render apps
hideAppsDrawer: PropTypes.bool, // Do not display apps drawer and content (may still be rendered)
// Conference Handler implementation
conferenceHandler: PropTypes.object,
// set to true to show the file drop target
draggingFile: PropTypes.bool,
// set to true to show the 'active conf call' banner
displayConfCallNotification: PropTypes.bool,
// maxHeight attribute for the aux panel and the video
// therein
maxHeight: PropTypes.number,
@ -58,42 +49,45 @@ export default createReactClass({
// content in a way that is likely to make it change size.
onResize: PropTypes.func,
fullHeight: PropTypes.bool,
},
};
getDefaultProps: () => ({
static defaultProps = {
showApps: true,
hideAppsDrawer: false,
}),
};
getInitialState: function() {
return { counters: this._computeCounters() };
},
constructor(props) {
super(props);
componentDidMount: function() {
this.state = {
counters: this._computeCounters(),
};
}
componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._rateLimitedUpdate);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._rateLimitedUpdate);
}
},
}
shouldComponentUpdate: function(nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState) {
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
!ObjectUtils.shallowEqual(this.state, nextState));
},
}
componentDidUpdate: function(prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
// most changes are likely to cause a resize
if (this.props.onResize) {
this.props.onResize();
}
},
}
onConferenceNotificationClick: function(ev, type) {
onConferenceNotificationClick = (ev, type) => {
dis.dispatch({
action: 'place_call',
type: type,
@ -101,15 +95,15 @@ export default createReactClass({
});
ev.stopPropagation();
ev.preventDefault();
},
};
_rateLimitedUpdate: new RateLimitedFunc(function() {
_rateLimitedUpdate = new RateLimitedFunc(() => {
if (SettingsStore.getValue("feature_state_counters")) {
this.setState({counters: this._computeCounters()});
}
}, 500),
}, 500);
_computeCounters: function() {
_computeCounters() {
let counters = [];
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
@ -140,9 +134,9 @@ export default createReactClass({
}
return counters;
},
}
render: function() {
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let fileDropTarget = null;
@ -159,55 +153,28 @@ export default createReactClass({
);
}
let conferenceCallNotification = null;
if (this.props.displayConfCallNotification) {
let supportedText = '';
let joinNode;
if (!MatrixClientPeg.get().supportsVoip()) {
supportedText = _t(" (unsupported)");
} else {
joinNode = (<span>
{ _t(
"Join as <voiceText>voice</voiceText> or <videoText>video</videoText>.",
{},
{
'voiceText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'voice');}} href="#">{ sub }</a>,
'videoText': (sub) => <a onClick={(event)=>{ this.onConferenceNotificationClick(event, 'video');}} href="#">{ sub }</a>,
},
) }
</span>);
}
// XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages,
// but there are translations for this in the languages we do have so I'm leaving it for now.
conferenceCallNotification = (
<div className="mx_RoomView_ongoingConfCallNotification">
{ _t("Ongoing conference call%(supportedText)s.", {supportedText: supportedText}) }
&nbsp;
{ joinNode }
</div>
);
}
const callView = (
<CallView
room={this.props.room}
ConferenceHandler={this.props.conferenceHandler}
onResize={this.props.onResize}
maxVideoHeight={this.props.maxHeight}
/>
);
const appsDrawer = <AppsDrawer
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
hide={this.props.hideAppsDrawer}
/>;
let appsDrawer;
if (SettingsStore.getValue(UIFeature.Widgets)) {
appsDrawer = <AppsDrawer
room={this.props.room}
userId={this.props.userId}
maxHeight={this.props.maxHeight}
showApps={this.props.showApps}
resizeNotifier={this.props.resizeNotifier}
/>;
}
let stateViews = null;
if (this.state.counters && SettingsStore.getValue("feature_state_counters")) {
let counters = [];
const counters = [];
this.state.counters.forEach((counter, idx) => {
const title = counter.title;
@ -216,7 +183,7 @@ export default createReactClass({
const severity = counter.severity;
const stateKey = counter.stateKey;
let span = <span>{ title }: { value }</span>
let span = <span>{ title }: { value }</span>;
if (link) {
span = (
@ -270,9 +237,8 @@ export default createReactClass({
{ appsDrawer }
{ fileDropTarget }
{ callView }
{ conferenceCallNotification }
{ this.props.children }
</AutoHideScrollbar>
);
},
});
}
}

View file

@ -92,7 +92,7 @@ interface IProps {
label?: string;
initialCaret?: DocumentOffset;
onChange();
onChange?();
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
}
@ -207,7 +207,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
// If the user is entering a command, only consider them typing if it is one which sends a message into the room
if (isTyping && this.props.model.parts[0].type === "command") {
const {cmd} = parseCommandString(this.props.model.parts[0].text);
if (!CommandMap.has(cmd) || CommandMap.get(cmd).category !== CommandCategories.messages) {
const command = CommandMap.get(cmd);
if (!command || !command.isEnabled() || command.category !== CommandCategories.messages) {
isTyping = false;
}
}
@ -618,13 +619,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}
private onFormatAction = (action: Formatting) => {
const range = getRangeForSelection(
this.editorRef.current,
this.props.model,
document.getSelection());
const range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection());
// trim the range as we want it to exclude leading/trailing spaces
range.trim();
if (range.length === 0) {
return;
}
this.historyManager.ensureLastChangesPushed(this.props.model);
this.modifiedFlag = true;
switch (action) {

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
import { _t } from '../../../languageHandler';
@ -51,10 +50,8 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) {
}
}
const EntityTile = createReactClass({
displayName: 'EntityTile',
propTypes: {
class EntityTile extends React.Component {
static propTypes = {
name: PropTypes.string,
title: PropTypes.string,
avatarJsx: PropTypes.any, // <BaseAvatar />
@ -70,33 +67,29 @@ const EntityTile = createReactClass({
showPresence: PropTypes.bool,
subtextLabel: PropTypes.string,
e2eStatus: PropTypes.string,
},
};
getDefaultProps: function() {
return {
shouldComponentUpdate: function(nextProps, nextState) { return true; },
onClick: function() {},
presenceState: "offline",
presenceLastActiveAgo: 0,
presenceLastTs: 0,
showInviteButton: false,
suppressOnHover: false,
showPresence: true,
};
},
static defaultProps = {
shouldComponentUpdate: function(nextProps, nextState) { return true; },
onClick: function() {},
presenceState: "offline",
presenceLastActiveAgo: 0,
presenceLastTs: 0,
showInviteButton: false,
suppressOnHover: false,
showPresence: true,
};
getInitialState: function() {
return {
hover: false,
};
},
state = {
hover: false,
};
shouldComponentUpdate: function(nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState) {
if (this.state.hover !== nextState.hover) return true;
return this.props.shouldComponentUpdate(nextProps, nextState);
},
}
render: function() {
render() {
const mainClassNames = {
"mx_EntityTile": true,
"mx_EntityTile_noHover": this.props.suppressOnHover,
@ -193,8 +186,8 @@ const EntityTile = createReactClass({
</AccessibleButton>
</div>
);
},
});
}
}
EntityTile.POWER_STATUS_MODERATOR = "moderator";
EntityTile.POWER_STATUS_ADMIN = "admin";

View file

@ -20,7 +20,6 @@ limitations under the License.
import ReplyThread from "../elements/ReplyThread";
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from "classnames";
import { _t, _td } from '../../../languageHandler';
import * as TextForEvent from "../../../TextForEvent";
@ -35,6 +34,8 @@ import * as ObjectUtils from "../../../ObjectUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {E2E_STATE} from "./E2EIcon";
import {toRem} from "../../../utils/units";
import {WidgetType} from "../../../widgets/WidgetType";
import RoomAvatar from "../avatars/RoomAvatar";
const eventTileTypes = {
'm.room.message': 'messages.MessageEvent',
@ -45,6 +46,7 @@ const eventTileTypes = {
'm.call.invite': 'messages.TextualEvent',
'm.call.answer': 'messages.TextualEvent',
'm.call.hangup': 'messages.TextualEvent',
'm.call.reject': 'messages.TextualEvent',
};
const stateEventTileTypes = {
@ -111,6 +113,19 @@ export function getHandlerTile(ev) {
}
}
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
if (type === "im.vector.modular.widgets") {
let type = ev.getContent()['type'];
if (!type) {
// deleted/invalid widget - try the past widget type
type = ev.getPrevContent()['type'];
}
if (WidgetType.JITSI.matches(type)) {
return "messages.MJitsiWidgetEvent";
}
}
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
}
@ -127,10 +142,8 @@ const MAX_READ_AVATARS = 5;
// | '--------------------------------------' |
// '----------------------------------------------------------'
export default createReactClass({
displayName: 'EventTile',
propTypes: {
export default class EventTile extends React.Component {
static propTypes = {
/* the MatrixEvent to show */
mxEvent: PropTypes.object.isRequired,
@ -150,6 +163,10 @@ export default createReactClass({
*/
last: PropTypes.bool,
// true if the event is the last event in a section (adds a css class for
// targeting)
lastInSection: PropTypes.bool,
/* true if this is search context (which has the effect of greying out
* the text
*/
@ -209,17 +226,22 @@ export default createReactClass({
// whether to use the irc layout
useIRCLayout: PropTypes.bool,
},
getDefaultProps: function() {
return {
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
onHeightChanged: function() {},
};
},
// whether or not to show flair at all
enableFlair: PropTypes.bool,
};
getInitialState: function() {
return {
static defaultProps = {
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
onHeightChanged: function() {},
};
static contextType = MatrixClientContext;
constructor(props, context) {
super(props, context);
this.state = {
// Whether the action bar is focused.
actionBarFocused: false,
// Whether all read receipts are being displayed. If not, only display
@ -232,23 +254,21 @@ export default createReactClass({
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: this.getReactions(),
};
},
statics: {
contextType: MatrixClientContext,
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// don't do RR animations until we are mounted
this._suppressReadReceiptAnimation = true;
this._verifyEvent(this.props.mxEvent);
this._tile = createRef();
this._replyThread = createRef();
},
}
componentDidMount: function() {
// TODO: [REACT-WARNING] Move into constructor
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this._verifyEvent(this.props.mxEvent);
}
componentDidMount() {
this._suppressReadReceiptAnimation = false;
const client = this.context;
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
@ -257,26 +277,27 @@ export default createReactClass({
if (this.props.showReactions) {
this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated);
}
},
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(nextProps) {
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(nextProps) {
// re-check the sender verification as outgoing events progress through
// the send process.
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
this._verifyEvent(nextProps.mxEvent);
}
},
}
shouldComponentUpdate: function(nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState) {
if (!ObjectUtils.shallowEqual(this.state, nextState)) {
return true;
}
return !this._propsEqual(this.props, nextProps);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
const client = this.context;
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
@ -284,31 +305,31 @@ export default createReactClass({
if (this.props.showReactions) {
this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated);
}
},
}
/** called when the event is decrypted after we show it.
*/
_onDecrypted: function() {
_onDecrypted = () => {
// we need to re-verify the sending device.
// (we call onHeightChanged in _verifyEvent to handle the case where decryption
// has caused a change in size of the event tile)
this._verifyEvent(this.props.mxEvent);
this.forceUpdate();
},
};
onDeviceVerificationChanged: function(userId, device) {
onDeviceVerificationChanged = (userId, device) => {
if (userId === this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent);
}
},
};
onUserVerificationChanged: function(userId, _trustStatus) {
onUserVerificationChanged = (userId, _trustStatus) => {
if (userId === this.props.mxEvent.getSender()) {
this._verifyEvent(this.props.mxEvent);
}
},
};
_verifyEvent: async function(mxEvent) {
async _verifyEvent(mxEvent) {
if (!mxEvent.isEncrypted()) {
return;
}
@ -360,9 +381,9 @@ export default createReactClass({
this.setState({
verified: E2E_STATE.VERIFIED,
}, this.props.onHeightChanged); // Decryption may have caused a change in size
},
}
_propsEqual: function(objA, objB) {
_propsEqual(objA, objB) {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
@ -408,9 +429,9 @@ export default createReactClass({
}
}
return true;
},
}
shouldHighlight: function() {
shouldHighlight() {
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
@ -420,15 +441,15 @@ export default createReactClass({
}
return actions.tweaks.highlight;
},
}
toggleAllReadAvatars: function() {
toggleAllReadAvatars = () => {
this.setState({
allReadAvatars: !this.state.allReadAvatars,
});
},
};
getReadAvatars: function() {
getReadAvatars() {
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
return (<span className="mx_EventTile_readAvatars" />);
@ -494,17 +515,17 @@ export default createReactClass({
{ remText }
{ avatars }
</span>;
},
}
onSenderProfileClick: function(event) {
onSenderProfileClick = event => {
const mxEvent = this.props.mxEvent;
dis.dispatch({
action: 'insert_mention',
user_id: mxEvent.getSender(),
});
},
};
onRequestKeysClick: function() {
onRequestKeysClick = () => {
this.setState({
// Indicate in the UI that the keys have been requested (this is expected to
// be reset if the component is mounted in the future).
@ -515,9 +536,9 @@ export default createReactClass({
// is received for the request with the required keys, the event could be
// decrypted successfully.
this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
},
};
onPermalinkClicked: function(e) {
onPermalinkClicked = e => {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Element when clicked.
e.preventDefault();
@ -527,9 +548,9 @@ export default createReactClass({
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
},
};
_renderE2EPadlock: function() {
_renderE2EPadlock() {
const ev = this.props.mxEvent;
// event could not be decrypted
@ -570,23 +591,19 @@ export default createReactClass({
// no padlock needed
return null;
},
}
onActionBarFocusChange(focused) {
onActionBarFocusChange = focused => {
this.setState({
actionBarFocused: focused,
});
},
};
getTile() {
return this._tile.current;
},
getTile = () => this._tile.current;
getReplyThread() {
return this._replyThread.current;
},
getReplyThread = () => this._replyThread.current;
getReactions() {
getReactions = () => {
if (
!this.props.showReactions ||
!this.props.getRelationsForEvent
@ -602,9 +619,9 @@ export default createReactClass({
console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
}
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
},
};
_onReactionsCreated(relationType, eventType) {
_onReactionsCreated = (relationType, eventType) => {
if (relationType !== "m.annotation" || eventType !== "m.reaction") {
return;
}
@ -612,9 +629,9 @@ export default createReactClass({
this.setState({
reactions: this.getReactions(),
});
},
};
render: function() {
render() {
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
const SenderProfile = sdk.getComponent('messages.SenderProfile');
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
@ -625,22 +642,23 @@ export default createReactClass({
const msgtype = content.msgtype;
const eventType = this.props.mxEvent.getType();
let tileHandler = getHandlerTile(this.props.mxEvent);
// Info messages are basically information about commands processed on a room
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
(eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) ||
(eventType === "m.room.encryption");
(eventType === "m.room.encryption") ||
(tileHandler === "messages.MJitsiWidgetEvent");
let isInfoMessage = (
!isBubbleMessage && eventType !== 'm.room.message' &&
eventType !== 'm.sticker' && eventType !== 'm.room.create'
);
let tileHandler = getHandlerTile(this.props.mxEvent);
// If we're showing hidden events in the timeline, we should use the
// source tile when there's no regular tile for an event and also for
// replace relations (which otherwise would display as a confusing
// duplicate of the thing they are replacing).
const useSource = !tileHandler || this.props.mxEvent.isRelation("m.replace");
if (useSource && SettingsStore.getValue("showHiddenEventsInTimeline")) {
if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) {
tileHandler = "messages.ViewSourceEvent";
// Reuse info message avatar and sender profile styling
isInfoMessage = true;
@ -676,6 +694,7 @@ export default createReactClass({
mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
mx_EventTile_last: this.props.last,
mx_EventTile_lastInSection: this.props.lastInSection,
mx_EventTile_contextual: this.props.contextual,
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
@ -742,10 +761,10 @@ export default createReactClass({
else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file');
sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent}
enableFlair={!text}
enableFlair={this.props.enableFlair && !text}
text={text} />;
} else {
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={true} />;
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={this.props.enableFlair} />;
}
}
@ -824,6 +843,7 @@ export default createReactClass({
return (
<div className={classes} aria-live={ariaLive} aria-atomic="true">
<div className="mx_EventTile_roomName">
<RoomAvatar room={room} width={28} height={28} />
<a href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
@ -898,6 +918,7 @@ export default createReactClass({
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onHeightChanged={this.props.onHeightChanged}
replacingEventId={this.props.replacingEventId}
showUrlPreview={false} />
</div>
</div>
@ -947,8 +968,8 @@ export default createReactClass({
);
}
}
},
});
}
}
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
const messageTypes = ['m.room.message', 'm.sticker'];
@ -1033,11 +1054,7 @@ class E2ePadlock extends React.Component {
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} dir="auto" />;
}
let classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
if (!SettingsStore.getValue("alwaysShowEncryptionIcons")) {
classes += ' mx_EventTile_e2eIcon_hidden';
}
const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
return (
<div
className={classes}

View file

@ -17,49 +17,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import {Key} from '../../../Keyboard';
export default createReactClass({
displayName: 'ForwardMessage',
propTypes: {
export default class ForwardMessage extends React.Component {
static propTypes = {
onCancelClick: PropTypes.func.isRequired,
},
componentDidMount: function() {
dis.dispatch({
action: 'panel_disable',
middleDisabled: true,
});
};
componentDidMount() {
document.addEventListener('keydown', this._onKeyDown);
},
}
componentWillUnmount: function() {
dis.dispatch({
action: 'panel_disable',
middleDisabled: false,
});
componentWillUnmount() {
document.removeEventListener('keydown', this._onKeyDown);
},
}
_onKeyDown: function(ev) {
_onKeyDown = ev => {
switch (ev.key) {
case Key.ESCAPE:
this.props.onCancelClick();
break;
}
},
};
render: function() {
render() {
return (
<div className="mx_ForwardMessage">
<h1>{ _t('Please select the destination room for this message') }</h1>
</div>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { AllHtmlEntities } from 'html-entities';
import {linkifyElement} from '../../../HtmlUtils';
import SettingsStore from "../../../settings/SettingsStore";
@ -27,24 +26,21 @@ import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils";
import { _t } from "../../../languageHandler";
export default createReactClass({
displayName: 'LinkPreviewWidget',
propTypes: {
export default class LinkPreviewWidget extends React.Component {
static propTypes = {
link: PropTypes.string.isRequired, // the URL being previewed
mxEvent: PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onHeightChanged: PropTypes.func, // called when the preview's contents has loaded
},
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
preview: null,
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this.unmounted = false;
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
if (this.unmounted) {
@ -59,25 +55,25 @@ export default createReactClass({
});
this._description = createRef();
},
}
componentDidMount: function() {
componentDidMount() {
if (this._description.current) {
linkifyElement(this._description.current);
}
},
}
componentDidUpdate: function() {
componentDidUpdate() {
if (this._description.current) {
linkifyElement(this._description.current);
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this.unmounted = true;
},
}
onImageClick: function(ev) {
onImageClick = ev => {
const p = this.state.preview;
if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault();
@ -98,9 +94,9 @@ export default createReactClass({
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
};
render: function() {
render() {
const p = this.state.preview;
if (!p || Object.keys(p).length === 0) {
return <div />;
@ -149,5 +145,5 @@ export default createReactClass({
</AccessibleButton>
</div>
);
},
});
}
}

View file

@ -17,16 +17,16 @@ limitations under the License.
*/
import React from 'react';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher/dispatcher';
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {isValid3pidInvite} from "../../../RoomInvite";
import rate_limited_func from "../../../ratelimitedfunc";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import CallHandler from "../../../CallHandler";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import BaseCard from "../right_panel/BaseCard";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@ -36,29 +36,18 @@ const SHOW_MORE_INCREMENT = 100;
// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g;
export default createReactClass({
displayName: 'MemberList',
export default class MemberList extends React.Component {
constructor(props) {
super(props);
getInitialState: function() {
const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) {
// show an empty list
return this._getMembersState([]);
this.state = this._getMembersState([]);
} else {
return this._getMembersState(this.roomMembers());
this.state = this._getMembersState(this.roomMembers());
}
},
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this._mounted = true;
const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) {
this._showMembersAccordingToMembershipWithLL();
cli.on("Room.myMembership", this.onMyMembership);
} else {
this._listenForMembersChanges();
}
cli.on("Room", this.onRoom); // invites & joining after peek
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = MatrixClientPeg.get().baseUrl;
@ -66,9 +55,21 @@ export default createReactClass({
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
this._showPresence = enablePresenceByHsUrl[hsUrl];
}
},
}
_listenForMembersChanges: function() {
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
const cli = MatrixClientPeg.get();
this._mounted = true;
if (cli.hasLazyLoadMembersEnabled()) {
this._showMembersAccordingToMembershipWithLL();
cli.on("Room.myMembership", this.onMyMembership);
} else {
this._listenForMembersChanges();
}
}
_listenForMembersChanges() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName);
@ -80,9 +81,9 @@ export default createReactClass({
cli.on("User.presence", this.onUserPresenceChange);
cli.on("User.currentlyActive", this.onUserPresenceChange);
// cli.on("Room.timeline", this.onRoomTimeline);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this._mounted = false;
const cli = MatrixClientPeg.get();
if (cli) {
@ -98,14 +99,14 @@ export default createReactClass({
// cancel any pending calls to the rate_limited_funcs
this._updateList.cancelPendingCall();
},
}
/**
* If lazy loading is enabled, either:
* show a spinner and load the members if the user is joined,
* or show the members available so far if the user is invited
*/
_showMembersAccordingToMembershipWithLL: async function() {
async _showMembersAccordingToMembershipWithLL() {
const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) {
const cli = MatrixClientPeg.get();
@ -120,14 +121,14 @@ export default createReactClass({
this.setState(this._getMembersState(this.roomMembers()));
this._listenForMembersChanges();
}
} else if (membership === "invite") {
// show the members we've got when invited
} else {
// show the members we already have loaded
this.setState(this._getMembersState(this.roomMembers()));
}
}
},
}
_getMembersState: function(members) {
_getMembersState(members) {
// set the state after determining _showPresence to make sure it's
// taken into account while rerendering
return {
@ -142,9 +143,9 @@ export default createReactClass({
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
searchQuery: "",
};
},
}
onUserPresenceChange(event, user) {
onUserPresenceChange = (event, user) => {
// Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile
// ever attaching their own listener.
@ -153,9 +154,9 @@ export default createReactClass({
if (tile) {
this._updateList(); // reorder the membership list
}
},
};
onRoom: function(room) {
onRoom = room => {
if (room.roomId !== this.props.roomId) {
return;
}
@ -163,40 +164,40 @@ export default createReactClass({
// we need to wait till the room is fully populated with state
// before refreshing the member list else we get a stale list.
this._showMembersAccordingToMembershipWithLL();
},
};
onMyMembership: function(room, membership, oldMembership) {
onMyMembership = (room, membership, oldMembership) => {
if (room.roomId === this.props.roomId && membership === "join") {
this._showMembersAccordingToMembershipWithLL();
}
},
};
onRoomStateMember: function(ev, state, member) {
onRoomStateMember = (ev, state, member) => {
if (member.roomId !== this.props.roomId) {
return;
}
this._updateList();
},
};
onRoomMemberName: function(ev, member) {
onRoomMemberName = (ev, member) => {
if (member.roomId !== this.props.roomId) {
return;
}
this._updateList();
},
};
onRoomStateEvent: function(event, state) {
onRoomStateEvent = (event, state) => {
if (event.getRoomId() === this.props.roomId &&
event.getType() === "m.room.third_party_invite") {
this._updateList();
}
},
};
_updateList: rate_limited_func(function() {
_updateList = rate_limited_func(() => {
this._updateListNow();
}, 500),
}, 500);
_updateListNow: function() {
_updateListNow() {
// console.log("Updating memberlist");
const newState = {
loading: false,
@ -205,9 +206,9 @@ export default createReactClass({
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
this.setState(newState);
},
}
getMembersWithUser: function() {
getMembersWithUser() {
if (!this.props.roomId) return [];
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
@ -228,33 +229,28 @@ export default createReactClass({
});
return allMembers;
},
roomMembers: function() {
const ConferenceHandler = CallHandler.getConferenceHandler();
}
roomMembers() {
const allMembers = this.getMembersWithUser();
const filteredAndSortedMembers = allMembers.filter((m) => {
return (
m.membership === 'join' || m.membership === 'invite'
) && (
!ConferenceHandler ||
(ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
);
});
filteredAndSortedMembers.sort(this.memberSort);
return filteredAndSortedMembers;
},
}
_createOverflowTileJoined: function(overflowCount, totalCount) {
_createOverflowTileJoined = (overflowCount, totalCount) => {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
},
};
_createOverflowTileInvited: function(overflowCount, totalCount) {
_createOverflowTileInvited = (overflowCount, totalCount) => {
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
},
};
_createOverflowTile: function(overflowCount, totalCount, onClick) {
_createOverflowTile = (overflowCount, totalCount, onClick) => {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
@ -265,33 +261,33 @@ export default createReactClass({
} name={text} presenceState="online" suppressOnHover={true}
onClick={onClick} />
);
},
};
_showMoreJoinedMemberList: function() {
_showMoreJoinedMemberList = () => {
this.setState({
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
});
},
};
_showMoreInvitedMemberList: function() {
_showMoreInvitedMemberList = () => {
this.setState({
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
});
},
};
memberString: function(member) {
memberString(member) {
if (!member) {
return "(null)";
} else {
const u = member.user;
return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "<null>") + ", " + (u ? u.getLastActiveTs() : "<null>") + ", " + (u ? u.currentlyActive : "<null>") + ", " + (u ? u.presence : "<null>") + ")";
}
},
}
// returns negative if a comes before b,
// returns 0 if a and b are equivalent in ordering
// returns positive if a comes after b.
memberSort: function(memberA, memberB) {
memberSort = (memberA, memberB) => {
// order by presence, with "active now" first.
// ...and then by power level
// ...and then by last active
@ -348,24 +344,24 @@ export default createReactClass({
ignorePunctuation: true,
sensitivity: "base",
});
},
};
onSearchQueryChanged: function(searchQuery) {
onSearchQueryChanged = searchQuery => {
this.setState({
searchQuery,
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
});
},
};
_onPending3pidInviteClick: function(inviteEvent) {
_onPending3pidInviteClick = inviteEvent => {
dis.dispatch({
action: 'view_3pid_invite',
event: inviteEvent,
});
},
};
_filterMembers: function(members, membership, query) {
_filterMembers(members, membership, query) {
return members.filter((m) => {
if (query) {
query = query.toLowerCase();
@ -379,9 +375,9 @@ export default createReactClass({
return m.membership === membership;
});
},
}
_getPending3PidInvites: function() {
_getPending3PidInvites() {
// include 3pid invites (m.room.third_party_invite) state events.
// The HS may have already converted these into m.room.member invites so
// we shouldn't add them if the 3pid invite state key (token) is in the
@ -399,9 +395,9 @@ export default createReactClass({
return true;
});
}
},
}
_makeMemberTiles: function(members) {
_makeMemberTiles(members) {
const MemberTile = sdk.getComponent("rooms.MemberTile");
const EntityTile = sdk.getComponent("rooms.EntityTile");
@ -415,33 +411,35 @@ export default createReactClass({
onClick={() => this._onPending3pidInviteClick(m)} />;
}
});
},
}
_getChildrenJoined: function(start, end) {
return this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
},
_getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end));
_getChildCountJoined: function() {
return this.state.filteredJoinedMembers.length;
},
_getChildCountJoined = () => this.state.filteredJoinedMembers.length;
_getChildrenInvited: function(start, end) {
_getChildrenInvited = (start, end) => {
let targets = this.state.filteredInvitedMembers;
if (end > this.state.filteredInvitedMembers.length) {
targets = targets.concat(this._getPending3PidInvites());
}
return this._makeMemberTiles(targets.slice(start, end));
},
};
_getChildCountInvited: function() {
_getChildCountInvited = () => {
return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length;
},
}
render: function() {
render() {
if (this.state.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return <div className="mx_MemberList"><Spinner /></div>;
return <BaseCard
className="mx_MemberList"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<Spinner />
</BaseCard>;
}
const SearchBox = sdk.getComponent('structures.SearchBox');
@ -464,10 +462,16 @@ export default createReactClass({
}
}
let inviteButtonText = _t("Invite to this room");
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat && chat.roomId === this.props.roomId) {
inviteButtonText = _t("Invite to this community");
}
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
inviteButton =
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick} disabled={!canInvite}>
<span>{ _t('Invite to this room') }</span>
<span>{ inviteButtonText }</span>
</AccessibleButton>;
}
@ -482,28 +486,32 @@ export default createReactClass({
/>;
}
return (
<div className="mx_MemberList" role="tabpanel">
{ inviteButton }
<AutoHideScrollbar>
<div className="mx_MemberList_wrapper">
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined} />
{ invitedHeader }
{ invitedSection }
</div>
</AutoHideScrollbar>
<SearchBox className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={ _t('Filter room members') }
onSearch={ this.onSearchQueryChanged } />
</div>
const footer = (
<SearchBox
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={ _t('Filter room members') }
onSearch={ this.onSearchQueryChanged } />
);
},
onInviteButtonClick: function() {
return <BaseCard
className="mx_MemberList"
header={inviteButton}
footer={footer}
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<div className="mx_MemberList_wrapper">
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined} />
{ invitedHeader }
{ invitedSection }
</div>
</BaseCard>;
}
onInviteButtonClick = () => {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
return;
@ -514,5 +522,5 @@ export default createReactClass({
action: 'view_invite',
roomId: this.props.roomId,
});
},
});
};
}

View file

@ -18,34 +18,31 @@ limitations under the License.
import SettingsStore from "../../../settings/SettingsStore";
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from "../../../index";
import dis from "../../../dispatcher/dispatcher";
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import {Action} from "../../../dispatcher/actions";
export default createReactClass({
displayName: 'MemberTile',
propTypes: {
export default class MemberTile extends React.Component {
static propTypes = {
member: PropTypes.any.isRequired, // RoomMember
showPresence: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
showPresence: true,
};
},
static defaultProps = {
showPresence: true,
};
getInitialState: function() {
return {
constructor(props) {
super(props);
this.state = {
statusMessage: this.getStatusMessage(),
isRoomEncrypted: false,
e2eStatus: null,
};
},
}
componentDidMount() {
const cli = MatrixClientPeg.get();
@ -72,7 +69,7 @@ export default createReactClass({
cli.on("RoomState.events", this.onRoomStateEvents);
}
}
},
}
componentWillUnmount() {
const cli = MatrixClientPeg.get();
@ -90,9 +87,9 @@ export default createReactClass({
cli.removeListener("userTrustStatusChanged", this.onUserTrustStatusChanged);
cli.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
}
},
}
onRoomStateEvents: function(ev) {
onRoomStateEvents = ev => {
if (ev.getType() !== "m.room.encryption") return;
const { roomId } = this.props.member;
if (ev.getRoomId() !== roomId) return;
@ -104,19 +101,19 @@ export default createReactClass({
isRoomEncrypted: true,
});
this.updateE2EStatus();
},
};
onUserTrustStatusChanged: function(userId, trustStatus) {
onUserTrustStatusChanged = (userId, trustStatus) => {
if (userId !== this.props.member.userId) return;
this.updateE2EStatus();
},
};
onDeviceVerificationChanged: function(userId, deviceId, deviceInfo) {
onDeviceVerificationChanged = (userId, deviceId, deviceInfo) => {
if (userId !== this.props.member.userId) return;
this.updateE2EStatus();
},
};
updateE2EStatus: async function() {
async updateE2EStatus() {
const cli = MatrixClientPeg.get();
const { userId } = this.props.member;
const isMe = userId === cli.getUserId();
@ -142,7 +139,7 @@ export default createReactClass({
this.setState({
e2eStatus: anyDeviceUnverified ? "warning" : "verified",
});
},
}
getStatusMessage() {
const { user } = this.props.member;
@ -150,16 +147,16 @@ export default createReactClass({
return "";
}
return user._unstable_statusMessage;
},
}
_onStatusMessageCommitted() {
_onStatusMessageCommitted = () => {
// The `User` object has observed a status message change.
this.setState({
statusMessage: this.getStatusMessage(),
});
},
};
shouldComponentUpdate: function(nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState) {
if (
this.member_last_modified_time === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
@ -180,27 +177,27 @@ export default createReactClass({
return true;
}
return false;
},
}
onClick: function(e) {
onClick = e => {
dis.dispatch({
action: Action.ViewUser,
member: this.props.member,
});
},
};
_getDisplayName: function() {
_getDisplayName() {
return this.props.member.name;
},
}
getPowerLabel: function() {
getPowerLabel() {
return _t("%(userName)s (power %(powerLevelNumber)s)", {
userName: this.props.member.userId,
powerLevelNumber: this.props.member.powerLevel,
});
},
}
render: function() {
render() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const EntityTile = sdk.getComponent('rooms.EntityTile');
@ -260,5 +257,5 @@ export default createReactClass({
onClick={this.onClick}
/>
);
},
});
}
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -31,6 +32,13 @@ import SettingsStore from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import ReplyPreview from "./ReplyPreview";
import {UIFeature} from "../../../settings/UIFeature";
import WidgetStore from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import { PlaceCallType } from "../../../CallHandler";
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -47,7 +55,7 @@ function CallButton(props) {
const onVoiceCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
type: "voice",
type: PlaceCallType.Voice,
room_id: props.roomId,
});
};
@ -67,7 +75,7 @@ function VideoCallButton(props) {
const onCallClick = (ev) => {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
type: ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video,
room_id: props.roomId,
});
};
@ -84,27 +92,51 @@ VideoCallButton.propTypes = {
};
function HangupButton(props) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const onHangupClick = () => {
const call = CallHandler.getCallForRoom(props.roomId);
if (props.isConference) {
dis.dispatch({
action: props.canEndConference ? 'end_conference' : 'hangup_conference',
room_id: props.roomId,
});
return;
}
const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
if (!call) {
return;
}
const action = call.state === CallState.Ringing ? 'reject' : 'hangup';
dis.dispatch({
action: 'hangup',
action,
// hangup the call for this room, which may not be the room in props
// (e.g. conferences which will hangup the 1:1 room instead)
room_id: call.roomId,
});
};
return (<AccessibleButton className="mx_MessageComposer_button mx_MessageComposer_hangup"
let tooltip = _t("Hangup");
if (props.isConference && props.canEndConference) {
tooltip = _t("End conference");
}
const canLeaveConference = !props.isConference ? true : props.isInConference;
return (
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_hangup"
onClick={onHangupClick}
title={_t('Hangup')}
/>);
title={tooltip}
disabled={!canLeaveConference}
/>
);
}
HangupButton.propTypes = {
roomId: PropTypes.string.isRequired,
isConference: PropTypes.bool.isRequired,
canEndConference: PropTypes.bool,
isInConference: PropTypes.bool,
};
const EmojiButton = ({addEmoji}) => {
@ -225,12 +257,17 @@ export default class MessageComposer extends React.Component {
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this);
this.renderPlaceholderText = this.renderPlaceholderText.bind(this);
WidgetStore.instance.on(UPDATE_EVENT, this._onWidgetUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetUpdate);
this._dispatcherRef = null;
this.state = {
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
replyToEvent: RoomViewStore.getQuotingEvent(),
tombstone: this._getRoomTombstone(),
canSendMessages: this.props.room.maySendMessage(),
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
};
}
@ -246,6 +283,14 @@ export default class MessageComposer extends React.Component {
}
};
_onWidgetUpdate = () => {
this.setState({hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room)});
};
_onActiveWidgetUpdate = () => {
this.setState({joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room)});
};
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
@ -276,6 +321,8 @@ export default class MessageComposer extends React.Component {
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
dis.unregister(this.dispatcherRef);
}
@ -295,9 +342,9 @@ export default class MessageComposer extends React.Component {
}
_onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (this.state.isQuoting === isQuoting) return;
this.setState({ isQuoting });
const replyToEvent = RoomViewStore.getQuotingEvent();
if (this.state.replyToEvent === replyToEvent) return;
this.setState({ replyToEvent });
}
onInputStateChanged(inputState) {
@ -336,7 +383,7 @@ export default class MessageComposer extends React.Component {
}
renderPlaceholderText() {
if (this.state.isQuoting) {
if (this.state.replyToEvent) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
} else {
@ -381,16 +428,32 @@ export default class MessageComposer extends React.Component {
room={this.props.room}
placeholder={this.renderPlaceholderText()}
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.props.permalinkCreator} />,
permalinkCreator={this.props.permalinkCreator}
replyToEvent={this.state.replyToEvent}
/>,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />,
);
if (SettingsStore.getValue(UIFeature.Widgets)) {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}
if (this.state.showCallButtons) {
if (callInProgress) {
if (this.state.hasConference) {
const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
controls.push(
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} />,
<HangupButton
key="controls_hangup"
roomId={this.props.room.roomId}
isConference={true}
canEndConference={canEndConf}
isInConference={this.state.joinedConference}
/>,
);
} else if (callInProgress) {
controls.push(
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} isConference={false} />,
);
} else {
controls.push(

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
import { formatCount } from "../../../utils/FormattingUtils";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import { XOR } from "../../../@types/common";
@ -92,6 +92,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
};
public render(): React.ReactElement {
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
const {notification, forceCount, roomId, onClick, ...props} = this.props;
// Don't show a badge if we don't need to
@ -108,7 +109,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
if (!notification.hasUnreadCount) return null; // Can't render a badge
}
let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
let symbol = notification.symbol || formatCount(notification.count);
if (isEmptyBadge) symbol = "";
const classes = classNames({

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import dis from "../../../dispatcher/dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
@ -25,22 +24,23 @@ import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
import {formatFullDate} from '../../../DateUtils';
export default createReactClass({
displayName: 'PinnedEventTile',
propTypes: {
export default class PinnedEventTile extends React.Component {
static propTypes = {
mxRoom: PropTypes.object.isRequired,
mxEvent: PropTypes.object.isRequired,
onUnpinned: PropTypes.func,
},
onTileClicked: function() {
};
onTileClicked = () => {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
},
onUnpinClicked: function() {
};
onUnpinClicked = () => {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
@ -56,11 +56,13 @@ export default createReactClass({
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
},
_canUnpin: function() {
};
_canUnpin() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
},
render: function() {
}
render() {
const sender = this.props.mxEvent.getSender();
// Get the latest sender profile rather than historical
const senderProfile = this.props.mxRoom.getMember(sender);
@ -100,5 +102,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -17,46 +17,42 @@ limitations under the License.
import React from "react";
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
import PinningUtils from "../../../utils/PinningUtils";
export default createReactClass({
displayName: 'PinnedEventsPanel',
propTypes: {
export default class PinnedEventsPanel extends React.Component {
static propTypes = {
// The Room from the js-sdk we're going to show pinned events for
room: PropTypes.object.isRequired,
onCancelClick: PropTypes.func,
},
};
getInitialState: function() {
return {
loading: true,
};
},
state = {
loading: true,
};
componentDidMount: function() {
componentDidMount() {
this._updatePinnedMessages();
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
}
},
}
_onStateEvent: function(ev) {
_onStateEvent = ev => {
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
this._updatePinnedMessages();
}
},
};
_updatePinnedMessages: function() {
_updatePinnedMessages = () => {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
this.setState({ loading: false, pinned: [] });
@ -85,9 +81,9 @@ export default createReactClass({
}
this._updateReadState();
},
};
_updateReadState: function() {
_updateReadState() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents) return; // nothing to read
@ -107,9 +103,9 @@ export default createReactClass({
event_ids: readStateEvents,
});
}
},
}
_getPinnedTiles: function() {
_getPinnedTiles() {
if (this.state.pinned.length === 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}
@ -120,9 +116,9 @@ export default createReactClass({
mxEvent={context.event}
onUnpinned={this._updatePinnedMessages} />);
});
},
}
render: function() {
render() {
let tiles = <div>{ _t("Loading...") }</div>;
if (this.state && !this.state.loading) {
tiles = this._getPinnedTiles();
@ -139,5 +135,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -16,15 +16,12 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'PresenceLabel',
propTypes: {
export default class PresenceLabel extends React.Component {
static propTypes = {
// number of milliseconds ago this user was last active.
// zero = unknown
activeAgo: PropTypes.number,
@ -35,18 +32,16 @@ export default createReactClass({
// offline, online, etc
presenceState: PropTypes.string,
},
};
getDefaultProps: function() {
return {
ago: -1,
presenceState: null,
};
},
static defaultProps = {
activeAgo: -1,
presenceState: null,
};
// Return duration as a string using appropriate time units
// XXX: This would be better handled using a culture-aware library, but we don't use one yet.
getDuration: function(time) {
getDuration(time) {
if (!time) return;
const t = parseInt(time / 1000);
const s = t % 60;
@ -66,9 +61,9 @@ export default createReactClass({
return _t("%(duration)sh", {duration: h});
}
return _t("%(duration)sd", {duration: d});
},
}
getPrettyPresence: function(presence, activeAgo, currentlyActive) {
getPrettyPresence(presence, activeAgo, currentlyActive) {
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
const duration = this.getDuration(activeAgo);
if (presence === "online") return _t("Online for %(duration)s", { duration: duration });
@ -81,13 +76,13 @@ export default createReactClass({
if (presence === "offline") return _t("Offline");
return _t("Unknown");
}
},
}
render: function() {
render() {
return (
<div className="mx_PresenceLabel">
{ this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive) }
</div>
);
},
});
}
}

View file

@ -17,7 +17,6 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import '../../../VelocityBounce';
import { _t } from '../../../languageHandler';
import {formatDate} from '../../../DateUtils';
@ -33,10 +32,8 @@ try {
} catch (e) {
}
export default createReactClass({
displayName: 'ReadReceiptMarker',
propTypes: {
export default class ReadReceiptMarker extends React.Component {
static propTypes = {
// the RoomMember to show the RR for
member: PropTypes.object,
// userId to fallback the avatar to
@ -70,30 +67,27 @@ export default createReactClass({
// True to show twelve hour format, false otherwise
showTwelveHour: PropTypes.bool,
},
};
getDefaultProps: function() {
return {
leftOffset: 0,
};
},
static defaultProps = {
leftOffset: 0,
};
getInitialState: function() {
// if we are going to animate the RR, we don't show it on first render,
// and instead just add a placeholder to the DOM; once we've been
// mounted, we start an animation which moves the RR from its old
// position.
return {
constructor(props) {
super(props);
this._avatar = createRef();
this.state = {
// if we are going to animate the RR, we don't show it on first render,
// and instead just add a placeholder to the DOM; once we've been
// mounted, we start an animation which moves the RR from its old
// position.
suppressDisplay: !this.props.suppressAnimation,
};
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._avatar = createRef();
},
componentWillUnmount: function() {
componentWillUnmount() {
// before we remove the rr, store its location in the map, so that if
// it reappears, it can be animated from the right place.
const rrInfo = this.props.readReceiptInfo;
@ -112,9 +106,9 @@ export default createReactClass({
rrInfo.top = avatarNode.offsetTop;
rrInfo.left = avatarNode.offsetLeft;
rrInfo.parent = avatarNode.offsetParent;
},
}
componentDidMount: function() {
componentDidMount() {
if (!this.state.suppressDisplay) {
// we've already done our display - nothing more to do.
return;
@ -172,10 +166,9 @@ export default createReactClass({
startStyles: startStyles,
enterTransitionOpts: enterTransitionOpts,
});
},
}
render: function() {
render() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
if (this.state.suppressDisplay) {
return <div ref={this._avatar} />;
@ -222,5 +215,5 @@ export default createReactClass({
/>
</Velociraptor>
);
},
});
}
}

View file

@ -22,6 +22,7 @@ import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import PropTypes from "prop-types";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {UIFeature} from "../../../settings/UIFeature";
function cancelQuoting() {
dis.dispatch({
@ -80,11 +81,14 @@ export default class ReplyPreview extends React.Component {
onClick={cancelQuoting} />
</div>
<div className="mx_ReplyPreview_clear" />
<EventTile last={true}
tileShape="reply_preview"
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} />
<EventTile
last={true}
tileShape="reply_preview"
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
</div>
</div>;
}

View file

@ -76,7 +76,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
};
private viewRoom = (room: Room, index: number) => {
Analytics.trackEvent("Breadcrumbs", "click_node", index);
Analytics.trackEvent("Breadcrumbs", "click_node", String(index));
defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId});
};

View file

@ -19,35 +19,32 @@ import dis from '../../../dispatcher/dispatcher';
import React from 'react';
import { _t } from '../../../languageHandler';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import {roomShape} from './RoomDetailRow';
export default createReactClass({
displayName: 'RoomDetailList',
propTypes: {
export default class RoomDetailList extends React.Component {
static propTypes = {
rooms: PropTypes.arrayOf(roomShape),
className: PropTypes.string,
},
};
getRows: function() {
getRows() {
if (!this.props.rooms) return [];
const RoomDetailRow = sdk.getComponent('rooms.RoomDetailRow');
return this.props.rooms.map((room, index) => {
return <RoomDetailRow key={index} room={room} onClick={this.onDetailsClick} />;
});
},
}
onDetailsClick: function(ev, room) {
onDetailsClick = (ev, room) => {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
room_alias: room.canonicalAlias || (room.aliases || [])[0],
});
},
};
render() {
const rows = this.getRows();
@ -64,5 +61,5 @@ export default createReactClass({
return <div className={classNames("mx_RoomDetailList", this.props.className)}>
{ rooms }
</div>;
},
});
}
}

View file

@ -20,7 +20,6 @@ import { _t } from '../../../languageHandler';
import { linkifyElement } from '../../../HtmlUtils';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
export function getDisplayAliasForRoom(room) {
@ -40,47 +39,48 @@ export const roomShape = PropTypes.shape({
guestCanJoin: PropTypes.bool,
});
export default createReactClass({
propTypes: {
export default class RoomDetailRow extends React.Component {
static propTypes = {
room: roomShape,
// passes ev, room as args
onClick: PropTypes.func,
onMouseDown: PropTypes.func,
},
};
_linkifyTopic: function() {
constructor(props) {
super(props);
this._topic = createRef();
}
componentDidMount() {
this._linkifyTopic();
}
componentDidUpdate() {
this._linkifyTopic();
}
_linkifyTopic() {
if (this._topic.current) {
linkifyElement(this._topic.current);
}
},
}
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._topic = createRef();
},
componentDidMount: function() {
this._linkifyTopic();
},
componentDidUpdate: function() {
this._linkifyTopic();
},
onClick: function(ev) {
onClick = (ev) => {
ev.preventDefault();
if (this.props.onClick) {
this.props.onClick(ev, this.props.room);
}
},
};
onTopicClick: function(ev) {
onTopicClick = (ev) => {
// When clicking a link in the topic, prevent the event being propagated
// to `onClick`.
ev.stopPropagation();
},
};
render: function() {
render() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const room = this.props.room;
@ -118,5 +118,5 @@ export default createReactClass({
{ room.numJoinedMembers }
</td>
</tr>;
},
});
}
}

View file

@ -17,16 +17,12 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import RateLimitedFunc from '../../../ratelimitedfunc';
import { linkifyElement } from '../../../HtmlUtils';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
@ -35,10 +31,8 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
export default createReactClass({
displayName: 'RoomHeader',
propTypes: {
export default class RoomHeader extends React.Component {
static propTypes = {
room: PropTypes.object,
oobData: PropTypes.object,
inRoom: PropTypes.bool,
@ -48,22 +42,21 @@ export default createReactClass({
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string,
},
};
getDefaultProps: function() {
return {
editing: false,
inRoom: false,
onCancelClick: null,
};
},
static defaultProps = {
editing: false,
inRoom: false,
onCancelClick: null,
};
constructor(props) {
super(props);
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._topic = createRef();
},
}
componentDidMount: function() {
componentDidMount() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
cli.on("Room.accountData", this._onRoomAccountData);
@ -74,15 +67,15 @@ export default createReactClass({
if (this.props.room) {
this.props.room.on("Room.name", this._onRoomNameChange);
}
},
}
componentDidUpdate: function() {
componentDidUpdate() {
if (this._topic.current) {
linkifyElement(this._topic.current);
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
if (this.props.room) {
this.props.room.removeListener("Room.name", this._onRoomNameChange);
}
@ -91,41 +84,34 @@ export default createReactClass({
cli.removeListener("RoomState.events", this._onRoomStateEvents);
cli.removeListener("Room.accountData", this._onRoomAccountData);
}
},
}
_onRoomStateEvents: function(event, state) {
_onRoomStateEvents = (event, state) => {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
}
// redisplay the room name, topic, etc.
this._rateLimitedUpdate();
},
};
_onRoomAccountData: function(event, room) {
_onRoomAccountData = (event, room) => {
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
if (event.getType() !== "im.vector.room.read_pins") return;
this._rateLimitedUpdate();
},
};
_rateLimitedUpdate: new RateLimitedFunc(function() {
_rateLimitedUpdate = new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
}, 500),
}, 500);
_onRoomNameChange: function(room) {
_onRoomNameChange = (room) => {
this.forceUpdate();
},
};
onShareRoomClick: function(ev) {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
target: this.props.room,
});
},
_hasUnreadPins: function() {
_hasUnreadPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
@ -142,19 +128,18 @@ export default createReactClass({
// There's pins, and we haven't read any of them
return true;
},
}
_hasPins: function() {
_hasPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
},
}
render: function() {
render() {
let searchStatus = null;
let cancelButton = null;
let settingsButton = null;
let pinnedEventsButton = null;
if (this.props.onCancelClick) {
@ -218,14 +203,6 @@ export default createReactClass({
/>;
}
if (this.props.onSettingsClick) {
settingsButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_settingsButton"
onClick={this.props.onSettingsClick}
title={_t("Settings")} />;
}
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
@ -262,26 +239,9 @@ export default createReactClass({
title={_t("Search")} />;
}
let shareRoomButton;
if (this.props.inRoom) {
shareRoomButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_shareButton"
onClick={this.onShareRoomClick}
title={_t('Share room')} />;
}
let manageIntegsButton;
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
manageIntegsButton = <ManageIntegsButton room={this.props.room} />;
}
const rightRow =
<div className="mx_RoomHeader_buttons">
{ settingsButton }
{ pinnedEventsButton }
{ shareRoomButton }
{ manageIntegsButton }
{ forgetButton }
{ searchButton }
</div>;
@ -301,5 +261,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -45,6 +45,7 @@ import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
@ -129,7 +130,9 @@ const TAG_AESTHETICS: {
}}
/>
<IconizedContextMenuOption
label={_t("Explore public rooms")}
label={CommunityPrototypeStore.instance.getSelectedCommunityId()
? _t("Explore community rooms")
: _t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();
@ -215,7 +218,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
const lists = RoomListStore.instance.orderedLists;
let rooms: Room = [];
const rooms: Room = [];
TAG_ORDER.forEach(t => {
let listRooms = lists[t];
@ -287,7 +290,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {
return g.myMembership === 'invite';
return g.myMembership === 'invite';
}).map(g => {
const avatar = (
<GroupAvatar
@ -343,21 +346,19 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
: TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
components.push(
<RoomSublist
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}
/>
);
components.push(<RoomSublist
key={`sublist-${orderedTagId}`}
tagId={orderedTagId}
forRooms={true}
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={aesthetics.onAddRoom}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
addRoomContextMenu={aesthetics.addRoomContextMenu}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}
/>);
}
return components;

View file

@ -1,80 +0,0 @@
/*
Copyright 2016 OpenMarket 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 createReactClass from 'create-react-class';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'RoomNameEditor',
propTypes: {
room: PropTypes.object.isRequired,
},
getInitialState: function() {
return {
name: null,
};
},
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
const room = this.props.room;
const name = room.currentState.getStateEvents('m.room.name', '');
const myId = MatrixClientPeg.get().credentials.userId;
const defaultName = room.getDefaultRoomName(myId);
this.setState({
name: name ? name.getContent().name : '',
});
this._placeholderName = _t("Unnamed Room");
if (defaultName && defaultName !== 'Empty room') { // default name from JS SDK, needs no translation as we don't ever show it.
this._placeholderName += " (" + defaultName + ")";
}
},
getRoomName: function() {
return this.state.name;
},
_onValueChanged: function(value, shouldSubmit) {
this.setState({
name: value,
});
},
render: function() {
const EditableText = sdk.getComponent("elements.EditableText");
return (
<div className="mx_RoomHeader_name">
<EditableText
className="mx_RoomHeader_nametext mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={this._placeholderName}
blurToCancel={false}
initialValue={this.state.name}
onValueChanged={this._onValueChanged}
dir="auto" />
</div>
);
},
});

View file

@ -18,7 +18,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
@ -26,6 +25,8 @@ import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import IdentityAuthClient from '../../../IdentityAuthClient';
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn",
@ -44,10 +45,8 @@ const MessageCase = Object.freeze({
OtherError: "OtherError",
});
export default createReactClass({
displayName: 'RoomPreviewBar',
propTypes: {
export default class RoomPreviewBar extends React.Component {
static propTypes = {
onJoinClick: PropTypes.func,
onRejectClick: PropTypes.func,
onRejectAndIgnoreClick: PropTypes.func,
@ -84,31 +83,32 @@ export default createReactClass({
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: PropTypes.string,
},
};
getDefaultProps: function() {
return {
onJoinClick: function() {},
};
},
static defaultProps = {
onJoinClick() {},
};
getInitialState: function() {
return {
busy: false,
};
},
state = {
busy: false,
};
componentDidMount: function() {
componentDidMount() {
this._checkInvitedEmail();
},
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
}
componentDidUpdate: function(prevProps, prevState) {
componentDidUpdate(prevProps, prevState) {
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
this._checkInvitedEmail();
}
},
}
_checkInvitedEmail: async function() {
componentWillUnmount() {
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
}
async _checkInvitedEmail() {
// If this is an invite and we've been told what email address was
// invited, fetch the user's account emails and discovery bindings so we
// can check them against the email that was invited.
@ -141,7 +141,14 @@ export default createReactClass({
}
this.setState({busy: false});
}
},
}
_onCommunityUpdate = (roomId) => {
if (this.props.room && this.props.room.roomId !== roomId) {
return;
}
this.forceUpdate(); // we have nothing to update
};
_getMessageCase() {
const isGuest = MatrixClientPeg.get().isGuest();
@ -193,7 +200,7 @@ export default createReactClass({
} else {
return MessageCase.ViewingRoom;
}
},
}
_getKickOrBanInfo() {
const myMember = this._getMyMember();
@ -207,9 +214,9 @@ export default createReactClass({
kickerMember.name : myMember.events.member.getSender();
const reason = myMember.events.member.getContent().reason;
return {memberName, reason};
},
}
_joinRule: function() {
_joinRule() {
const room = this.props.room;
if (room) {
const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
@ -217,10 +224,17 @@ export default createReactClass({
return joinRules.getContent().join_rule;
}
}
},
}
_roomName: function(atStart = false) {
const name = this.props.room ? this.props.room.name : this.props.roomAlias;
_communityProfile() {
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
return {displayName: null, avatarMxc: null};
}
_roomName(atStart = false) {
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
const profile = this._communityProfile();
if (profile.displayName) name = profile.displayName;
if (name) {
return name;
} else if (atStart) {
@ -228,16 +242,16 @@ export default createReactClass({
} else {
return _t("this room");
}
},
}
_getMyMember() {
return (
this.props.room &&
this.props.room.getMember(MatrixClientPeg.get().getUserId())
);
},
}
_getInviteMember: function() {
_getInviteMember() {
const {room} = this.props;
if (!room) {
return;
@ -249,7 +263,7 @@ export default createReactClass({
}
const inviterUserId = inviteEvent.events.member.getSender();
return room.currentState.getMember(inviterUserId);
},
}
_isDMInvite() {
const myMember = this._getMyMember();
@ -259,7 +273,7 @@ export default createReactClass({
const memberEvent = myMember.events.member;
const memberContent = memberEvent.getContent();
return memberContent.membership === "invite" && memberContent.is_direct;
},
}
_makeScreenAfterLogin() {
return {
@ -272,17 +286,17 @@ export default createReactClass({
inviter_name: this.props.oobData ? this.props.oobData.inviterName : null,
}
};
},
}
onLoginClick: function() {
onLoginClick = () => {
dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
},
};
onRegisterClick: function() {
onRegisterClick = () => {
dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
},
};
render: function() {
render() {
const brand = SdkConfig.get().brand;
const Spinner = sdk.getComponent('elements.Spinner');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
@ -439,7 +453,10 @@ export default createReactClass({
}
case MessageCase.Invite: {
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
const oobData = Object.assign({}, this.props.oobData, {
avatarUrl: this._communityProfile().avatarMxc,
});
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
const inviteMember = this._getInviteMember();
let inviterElement;
@ -573,5 +590,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -1,170 +0,0 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
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 * as sdk from "../../../index";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
export default class RoomRecoveryReminder extends React.PureComponent {
static propTypes = {
// called if the user sets the option to suppress this reminder in the future
onDontAskAgainSet: PropTypes.func,
}
static defaultProps = {
onDontAskAgainSet: function() {},
}
constructor(props) {
super(props);
this.state = {
loading: true,
error: null,
backupInfo: null,
notNowClicked: false,
};
}
componentDidMount() {
this._loadBackupStatus();
}
async _loadBackupStatus() {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.setState({
loading: false,
backupInfo,
});
} catch (e) {
console.log("Unable to fetch key backup status", e);
this.setState({
loading: false,
error: e,
});
}
}
showSetupDialog = () => {
if (this.state.backupInfo) {
// A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and
// allow us to trust it (ie. upload keys to it)
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog, null, null,
/* priority = */ false, /* static = */ true,
);
} else {
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
null, null, /* priority = */ false, /* static = */ true,
);
}
}
onOnNotNowClick = () => {
this.setState({notNowClicked: true});
}
onDontAskAgainClick = () => {
// When you choose "Don't ask again" from the room reminder, we show a
// dialog to confirm the choice.
Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder",
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
{
onDontAskAgain: async () => {
await SettingsStore.setValue(
"showRoomRecoveryReminder",
null,
SettingLevel.ACCOUNT,
false,
);
this.props.onDontAskAgainSet();
},
onSetup: () => {
this.showSetupDialog();
},
},
);
}
onSetupClick = () => {
this.showSetupDialog();
}
render() {
// If there was an error loading just don't display the banner: we'll try again
// next time the user switchs to the room.
if (this.state.error || this.state.loading || this.state.notNowClicked) {
return null;
}
const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
let setupCaption;
if (this.state.backupInfo) {
setupCaption = _t("Connect this session to Key Backup");
} else {
setupCaption = _t("Start using Key Backup");
}
return (
<div className="mx_RoomRecoveryReminder">
<div className="mx_RoomRecoveryReminder_header">{_t(
"Never lose encrypted messages",
)}</div>
<div className="mx_RoomRecoveryReminder_body">
<p>{_t(
"Messages in this room are secured with end-to-end " +
"encryption. Only you and the recipient(s) have the " +
"keys to read these messages.",
)}</p>
<p>{_t(
"Securely back up your keys to avoid losing them. " +
"<a>Learn more.</a>", {},
{
// TODO: We don't have this link yet: this will prevent the translators
// having to re-translate the string when we do.
a: sub => '',
},
)}</p>
</div>
<div className="mx_RoomRecoveryReminder_buttons">
<AccessibleButton kind="primary"
onClick={this.onSetupClick}>
{setupCaption}
</AccessibleButton>
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onOnNotNowClick}>
{ _t("Not now") }
</AccessibleButton>
<AccessibleButton className="mx_RoomRecoveryReminder_secondary mx_linkButton"
onClick={this.onDontAskAgainClick}>
{ _t("Don't ask me again") }
</AccessibleButton>
</div>
</div>
);
}
}

View file

@ -517,15 +517,13 @@ export default class RoomSublist extends React.Component<IProps, IState> {
if (this.state.rooms) {
const visibleRooms = this.state.rooms.slice(0, this.numVisibleTiles);
for (const room of visibleRooms) {
tiles.push(
<RoomTile
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.tagId}
/>
);
tiles.push(<RoomTile
room={room}
key={`room-${room.roomId}`}
showMessagePreview={this.layout.showPreviews}
isMinimized={this.props.isMinimized}
tag={this.props.tagId}
/>);
}
}
@ -710,7 +708,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
// doesn't become sticky.
// The same applies to the notification badge.
return (
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
<div
className={classes}
onKeyDown={this.onHeaderKeyDown}
onFocus={onFocus}
aria-label={this.props.label}
>
<div className="mx_RoomSublist_stickable">
<Button
onFocus={onFocus}
@ -762,7 +765,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const showMoreAtMinHeight = minTiles < this.numTiles;
const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0);
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
let maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
const showMoreBtnClasses = classNames({
'mx_RoomSublist_showNButton': true,
});

View file

@ -27,11 +27,11 @@ import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import { ChevronFace, ContextMenuTooltipButton, MenuItemRadio } from "../../structures/ContextMenu";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE, } from "../../../RoomNotifs";
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import NotificationBadge from "./NotificationBadge";
import { Volume } from "../../../RoomNotifsTypes";
@ -47,8 +47,11 @@ import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber"
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
IconizedContextMenuOption,
IconizedContextMenuOptionList, IconizedContextMenuRadio
IconizedContextMenuOptionList,
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps {
room: Room;
@ -101,6 +104,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps = EchoChamber.forRoom(this.props.room);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
}
private onNotificationUpdate = () => {
@ -140,6 +144,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
defaultDispatcher.unregister(this.dispatcherRef);
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
}
private onAction = (payload: ActionPayload) => {
@ -150,6 +155,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
};
private onCommunityUpdate = (roomId: string) => {
if (roomId !== this.props.room.roomId) return;
this.forceUpdate(); // we don't have anything to actually update
};
private onRoomPreviewChanged = (room: Room) => {
if (this.props.room && room.roomId === this.props.room.roomId) {
// generatePreview() will return nothing if the user has previews disabled
@ -239,7 +249,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
removeTag,
addTag,
undefined,
0
0,
));
} else {
console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`);
@ -461,11 +471,21 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
'mx_RoomTile_minimized': this.props.isMinimized,
});
let roomProfile: IRoomProfile = {displayName: null, avatarMxc: null};
if (this.props.tag === DefaultTagID.Invite) {
roomProfile = CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
}
let name = roomProfile.displayName || this.props.room.name;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={32}
tag={this.props.tag}
displayBadge={this.props.isMinimized}
oobData={({avatarUrl: roomProfile.avatarMxc})}
/>;
let badge: React.ReactNode;
@ -482,10 +502,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
);
}
let name = this.props.room.name;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let messagePreview = null;
if (this.showMessagePreview && this.state.messagePreview) {
messagePreview = (

View file

@ -1,68 +0,0 @@
/*
Copyright 2016 OpenMarket 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 createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import { _t } from "../../../languageHandler";
export default createReactClass({
displayName: 'RoomTopicEditor',
propTypes: {
room: PropTypes.object.isRequired,
},
getInitialState: function() {
return {
topic: null,
};
},
componentDidMount: function() {
const room = this.props.room;
const topic = room.currentState.getStateEvents('m.room.topic', '');
this.setState({
topic: topic ? topic.getContent().topic : '',
});
},
getTopic: function() {
return this.state.topic;
},
_onValueChanged: function(value) {
this.setState({
topic: value,
});
},
render: function() {
const EditableText = sdk.getComponent("elements.EditableText");
return (
<EditableText
className="mx_RoomHeader_topic mx_RoomHeader_editable"
placeholderClassName="mx_RoomHeader_placeholder"
placeholder={_t("Add a topic")}
blurToCancel={false}
initialValue={this.state.topic}
onValueChanged={this._onValueChanged}
dir="auto" />
);
},
});

View file

@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018-2020 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.
@ -16,29 +16,38 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
export default createReactClass({
displayName: 'RoomUpgradeWarningBar',
propTypes: {
export default class RoomUpgradeWarningBar extends React.Component {
static propTypes = {
room: PropTypes.object.isRequired,
recommendation: PropTypes.object.isRequired,
},
};
componentDidMount: function() {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
MatrixClientPeg.get().on("RoomState.events", this._onStateEvents);
},
}
_onStateEvents: function(event, state) {
componentWillUnmount() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onStateEvents);
}
}
_onStateEvents = (event, state) => {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
}
@ -47,14 +56,14 @@ export default createReactClass({
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
this.setState({upgraded: tombstone && tombstone.getContent().replacement_room});
},
};
onUpgradeClick: function() {
onUpgradeClick = () => {
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
},
};
render: function() {
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let doUpgradeWarnings = (
@ -117,5 +126,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,35 +16,32 @@ limitations under the License.
*/
import React, {createRef} from 'react';
import createReactClass from 'create-react-class';
import AccessibleButton from "../elements/AccessibleButton";
import classNames from "classnames";
import { _t } from '../../../languageHandler';
import {Key} from "../../../Keyboard";
import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice";
export default createReactClass({
displayName: 'SearchBar',
export default class SearchBar extends React.Component {
constructor(props) {
super(props);
getInitialState: function() {
return ({
scope: 'Room',
});
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._search_term = createRef();
},
onThisRoomClick: function() {
this.state = {
scope: 'Room',
};
}
onThisRoomClick = () => {
this.setState({ scope: 'Room' }, () => this._searchIfQuery());
},
};
onAllRoomsClick: function() {
onAllRoomsClick = () => {
this.setState({ scope: 'All' }, () => this._searchIfQuery());
},
};
onSearchChange: function(e) {
onSearchChange = (e) => {
switch (e.key) {
case Key.ENTER:
this.onSearch();
@ -52,19 +50,19 @@ export default createReactClass({
this.props.onCancelClick();
break;
}
},
};
_searchIfQuery: function() {
_searchIfQuery() {
if (this._search_term.current.value) {
this.onSearch();
}
},
}
onSearch: function() {
onSearch = () => {
this.props.onSearch(this._search_term.current.value, this.state.scope);
},
};
render: function() {
render() {
const searchButtonClasses = classNames("mx_SearchBar_searchButton", {
mx_SearchBar_searching: this.props.searchInProgress,
});
@ -76,21 +74,24 @@ export default createReactClass({
});
return (
<div className="mx_SearchBar">
<div className="mx_SearchBar_buttons" role="radiogroup">
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
{_t("This Room")}
</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
{_t("All Rooms")}
</AccessibleButton>
<>
<div className="mx_SearchBar">
<div className="mx_SearchBar_buttons" role="radiogroup">
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick} aria-checked={this.state.scope === 'Room'} role="radio">
{_t("This Room")}
</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick} aria-checked={this.state.scope === 'All'} role="radio">
{_t("All Rooms")}
</AccessibleButton>
</div>
<div className="mx_SearchBar_input mx_textinput">
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
</div>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
</div>
<div className="mx_SearchBar_input mx_textinput">
<input ref={this._search_term} type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
</div>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
</div>
<DesktopBuildsNotice isRoomEncrypted={this.props.isRoomEncrypted} kind={WarningKind.Search} />
</>
);
},
});
}
}

View file

@ -17,14 +17,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as sdk from '../../../index';
import {haveTileForEvent} from "./EventTile";
import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature";
export default createReactClass({
displayName: 'SearchResult',
propTypes: {
export default class SearchResultTile extends React.Component {
static propTypes = {
// a matrix-js-sdk SearchResult containing the details of this result
searchResult: PropTypes.object.isRequired,
@ -35,9 +34,9 @@ export default createReactClass({
resultLink: PropTypes.string,
onHeightChanged: PropTypes.func,
},
};
render: function() {
render() {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventTile = sdk.getComponent('rooms.EventTile');
const result = this.props.searchResult;
@ -48,23 +47,32 @@ export default createReactClass({
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const timeline = result.context.getTimeline();
for (var j = 0; j < timeline.length; j++) {
for (let j = 0; j < timeline.length; j++) {
const ev = timeline[j];
var highlights;
let highlights;
const contextual = (j != result.context.getOurEventIndex());
if (!contextual) {
highlights = this.props.searchHighlights;
}
if (haveTileForEvent(ev)) {
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
permalinkCreator={this.props.permalinkCreator}
highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged} />);
ret.push((
<EventTile
key={`${eventId}+${j}`}
mxEvent={ev}
contextual={contextual}
highlights={highlights}
permalinkCreator={this.props.permalinkCreator}
highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
));
}
}
return (
<li data-scroll-tokens={eventId+"+"+j}>
<li data-scroll-tokens={eventId}>
{ ret }
</li>);
},
});
}
}

View file

@ -29,7 +29,6 @@ import {
} from '../../../editor/serialize';
import {CommandPartCreator} from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer";
import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
import {parseEvent} from '../../../editor/deserialize';
import {findEditableEvent} from '../../../utils/EventUtils';
@ -41,7 +40,6 @@ import {_t, _td} from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
import {isConfettiEmoji} from "../elements/Confetti";
@ -63,7 +61,7 @@ function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
}
// exported for tests
export function createMessageContent(model, permalinkCreator) {
export function createMessageContent(model, permalinkCreator, replyToEvent) {
const isEmote = containsEmote(model);
if (isEmote) {
model = stripEmoteCommand(model);
@ -72,21 +70,20 @@ export function createMessageContent(model, permalinkCreator) {
model = stripPrefix(model, "/");
}
model = unescapeMessage(model);
const repliedToEvent = RoomViewStore.getQuotingEvent();
const body = textSerialize(model);
const content = {
msgtype: isEmote ? "m.emote" : "m.text",
body: body,
};
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!repliedToEvent});
const formattedBody = htmlSerializeIfNeeded(model, {forceHTML: !!replyToEvent});
if (formattedBody) {
content.format = "org.matrix.custom.html";
content.formatted_body = formattedBody;
}
if (repliedToEvent) {
addReplyToMessageContent(content, repliedToEvent, permalinkCreator);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, permalinkCreator);
}
return content;
@ -97,21 +94,23 @@ export default class SendMessageComposer extends React.Component {
room: PropTypes.object.isRequired,
placeholder: PropTypes.string,
permalinkCreator: PropTypes.object.isRequired,
replyToEvent: PropTypes.object,
};
static contextType = MatrixClientContext;
constructor(props) {
super(props);
constructor(props, context) {
super(props, context);
this.model = null;
this._editorRef = null;
this.currentlyComposedEditorState = null;
const cli = MatrixClientPeg.get();
if (cli.isCryptoEnabled() && cli.isRoomEncrypted(this.props.room.roomId)) {
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
this._prepareToEncrypt = new RateLimitedFunc(() => {
cli.prepareToEncrypt(this.props.room);
this.context.prepareToEncrypt(this.props.room);
}, 60000);
}
window.addEventListener("beforeunload", this._saveStoredEditorState);
}
_setEditorRef = ref => {
@ -147,7 +146,7 @@ export default class SendMessageComposer extends React.Component {
if (e.shiftKey || e.metaKey) return;
const shouldSelectHistory = e.altKey && e.ctrlKey;
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !RoomViewStore.getQuotingEvent();
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent;
if (shouldSelectHistory) {
// Try select composer history
@ -189,9 +188,13 @@ export default class SendMessageComposer extends React.Component {
this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length;
return;
}
const serializedParts = this.sendHistoryManager.getItem(delta);
if (serializedParts) {
this.model.reset(serializedParts);
const {parts, replyEventId} = this.sendHistoryManager.getItem(delta);
dis.dispatch({
action: 'reply_to_event',
event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
});
if (parts) {
this.model.reset(parts);
this._editorRef.focus();
}
}
@ -301,12 +304,12 @@ export default class SendMessageComposer extends React.Component {
}
}
const replyToEvent = this.props.replyToEvent;
if (shouldSend) {
const isReply = !!RoomViewStore.getQuotingEvent();
const {roomId} = this.props.room;
const content = createMessageContent(this.model, this.props.permalinkCreator);
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
this.context.sendMessage(roomId, content);
if (isReply) {
if (replyToEvent) {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
dis.dispatch({
@ -322,7 +325,7 @@ export default class SendMessageComposer extends React.Component {
}
}
this.sendHistoryManager.save(this.model);
this.sendHistoryManager.save(this.model, replyToEvent);
// clear composer
this.model.reset([]);
this._editorRef.clearUndoHistory();
@ -332,6 +335,8 @@ export default class SendMessageComposer extends React.Component {
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
window.removeEventListener("beforeunload", this._saveStoredEditorState);
this._saveStoredEditorState();
}
// TODO: [REACT-WARNING] Move this to constructor
@ -340,11 +345,11 @@ export default class SendMessageComposer extends React.Component {
const parts = this._restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator);
this.dispatcherRef = dis.register(this.onAction);
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_composer_history_');
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, 'mx_cider_history_');
}
get _editorStateKey() {
return `cider_editor_state_${this.props.room.roomId}`;
return `mx_cider_state_${this.props.room.roomId}`;
}
_clearStoredEditorState() {
@ -354,9 +359,19 @@ export default class SendMessageComposer extends React.Component {
_restoreStoredEditorState(partCreator) {
const json = localStorage.getItem(this._editorStateKey);
if (json) {
const serializedParts = JSON.parse(json);
const parts = serializedParts.map(p => partCreator.deserializePart(p));
return parts;
try {
const {parts: serializedParts, replyEventId} = JSON.parse(json);
const parts = serializedParts.map(p => partCreator.deserializePart(p));
if (replyEventId) {
dis.dispatch({
action: 'reply_to_event',
event: this.props.room.findEventById(replyEventId),
});
}
return parts;
} catch (e) {
console.error(e);
}
}
}
@ -364,7 +379,8 @@ export default class SendMessageComposer extends React.Component {
if (this.model.isEmpty) {
this._clearStoredEditorState();
} else {
localStorage.setItem(this._editorStateKey, JSON.stringify(this.model.serializeParts()));
const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
}
}
@ -456,7 +472,6 @@ export default class SendMessageComposer extends React.Component {
room={this.props.room}
label={this.props.placeholder}
placeholder={this.props.placeholder}
onChange={this._saveStoredEditorState}
onPaste={this._onPaste}
/>
</div>

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import AccessibleButton from '../elements/AccessibleButton';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
@ -37,18 +36,16 @@ export function CancelButton(props) {
* A stripped-down room header used for things like the user settings
* and room directory.
*/
export default createReactClass({
displayName: 'SimpleRoomHeader',
propTypes: {
export default class SimpleRoomHeader extends React.Component {
static propTypes = {
title: PropTypes.string,
onCancelClick: PropTypes.func,
// `src` to a TintableSvg. Optional.
icon: PropTypes.string,
},
};
render: function() {
render() {
let cancelButton;
let icon;
if (this.props.onCancelClick) {
@ -73,5 +70,5 @@ export default createReactClass({
</div>
</div>
);
},
});
}
}

View file

@ -22,7 +22,6 @@ import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
@ -30,6 +29,7 @@ import {ContextMenu} from "../../structures/ContextMenu";
import {WidgetType} from "../../../widgets/WidgetType";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {Action} from "../../../dispatcher/actions";
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
// This should be below the dialog level (4000), but above the rest of the UI (1000-2000).
// We sit in a context menu, so this should be given to the context menu.
@ -212,9 +212,11 @@ export default class Stickerpicker extends React.Component {
_sendVisibilityToWidget(visible) {
if (!this.state.stickerpickerWidget) return;
const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
if (widgetMessaging && visible !== this._prevSentVisibility) {
widgetMessaging.sendVisibility(visible);
const messaging = WidgetMessagingStore.instance.getMessagingForId(this.state.stickerpickerWidget.id);
if (messaging && visible !== this._prevSentVisibility) {
messaging.updateVisibility(visible).catch(err => {
console.error("Error updating widget visibility: ", err);
});
this._prevSentVisibility = visible;
}
}

View file

@ -19,9 +19,7 @@ import classNames from "classnames";
import {
RovingAccessibleButton,
RovingAccessibleTooltipButton,
RovingTabIndexWrapper
} from "../../../accessibility/RovingTabIndex";
import AccessibleButton from "../../views/elements/AccessibleButton";
import NotificationBadge from "./NotificationBadge";
import { NotificationState } from "../../../stores/notifications/NotificationState";

View file

@ -18,19 +18,16 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
export default createReactClass({
displayName: 'TopUnreadMessagesBar',
propTypes: {
export default class TopUnreadMessagesBar extends React.Component {
static propTypes = {
onScrollUpClick: PropTypes.func,
onCloseClick: PropTypes.func,
},
};
render: function() {
render() {
return (
<div className="mx_TopUnreadMessagesBar">
<AccessibleButton className="mx_TopUnreadMessagesBar_scrollUp"
@ -43,5 +40,5 @@ export default createReactClass({
</AccessibleButton>
</div>
);
},
});
}
}

View file

@ -17,16 +17,13 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import * as WhoIsTyping from '../../../WhoIsTyping';
import Timer from '../../../utils/Timer';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import MemberAvatar from '../avatars/MemberAvatar';
export default createReactClass({
displayName: 'WhoIsTypingTile',
propTypes: {
export default class WhoIsTypingTile extends React.Component {
static propTypes = {
// the room this statusbar is representing.
room: PropTypes.object.isRequired,
onShown: PropTypes.func,
@ -34,32 +31,28 @@ export default createReactClass({
// Number of names to display in typing indication. E.g. set to 3, will
// result in "X, Y, Z and 100 others are typing."
whoIsTypingLimit: PropTypes.number,
},
};
getDefaultProps: function() {
return {
whoIsTypingLimit: 3,
};
},
static defaultProps = {
whoIsTypingLimit: 3,
};
getInitialState: function() {
return {
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
// a map with userid => Timer to delay
// hiding the "x is typing" message for a
// user so hiding it can coincide
// with the sent message by the other side
// resulting in less timeline jumpiness
delayedStopTypingTimers: {},
};
},
state = {
usersTyping: WhoIsTyping.usersTypingApartFromMe(this.props.room),
// a map with userid => Timer to delay
// hiding the "x is typing" message for a
// user so hiding it can coincide
// with the sent message by the other side
// resulting in less timeline jumpiness
delayedStopTypingTimers: {},
};
componentDidMount: function() {
componentDidMount() {
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
},
}
componentDidUpdate: function(_, prevState) {
componentDidUpdate(_, prevState) {
const wasVisible = this._isVisible(prevState);
const isVisible = this._isVisible(this.state);
if (this.props.onShown && !wasVisible && isVisible) {
@ -67,9 +60,9 @@ export default createReactClass({
} else if (this.props.onHidden && wasVisible && !isVisible) {
this.props.onHidden();
}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
// we may have entirely lost our client as we're logging out before clicking login on the guest bar...
const client = MatrixClientPeg.get();
if (client) {
@ -77,17 +70,17 @@ export default createReactClass({
client.removeListener("Room.timeline", this.onRoomTimeline);
}
Object.values(this.state.delayedStopTypingTimers).forEach((t) => t.abort());
},
}
_isVisible: function(state) {
_isVisible(state) {
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
},
}
isVisible: function() {
isVisible = () => {
return this._isVisible(this.state);
},
};
onRoomTimeline: function(event, room) {
onRoomTimeline = (event, room) => {
if (room && room.roomId === this.props.room.roomId) {
const userId = event.getSender();
// remove user from usersTyping
@ -96,15 +89,15 @@ export default createReactClass({
// abort timer if any
this._abortUserTimer(userId);
}
},
};
onRoomMemberTyping: function(ev, member) {
onRoomMemberTyping = (ev, member) => {
const usersTyping = WhoIsTyping.usersTypingApartFromMeAndIgnored(this.props.room);
this.setState({
delayedStopTypingTimers: this._updateDelayedStopTypingTimers(usersTyping),
usersTyping,
});
},
};
_updateDelayedStopTypingTimers(usersTyping) {
const usersThatStoppedTyping = this.state.usersTyping.filter((a) => {
@ -142,26 +135,26 @@ export default createReactClass({
}, delayedStopTypingTimers);
return delayedStopTypingTimers;
},
}
_abortUserTimer: function(userId) {
_abortUserTimer(userId) {
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
timer.abort();
this._removeUserTimer(userId);
}
},
}
_removeUserTimer: function(userId) {
_removeUserTimer(userId) {
const timer = this.state.delayedStopTypingTimers[userId];
if (timer) {
const delayedStopTypingTimers = Object.assign({}, this.state.delayedStopTypingTimers);
delete delayedStopTypingTimers[userId];
this.setState({delayedStopTypingTimers});
}
},
}
_renderTypingIndicatorAvatars: function(users, limit) {
_renderTypingIndicatorAvatars(users, limit) {
let othersCount = 0;
if (users.length > limit) {
othersCount = users.length - limit + 1;
@ -190,9 +183,9 @@ export default createReactClass({
}
return avatars;
},
}
render: function() {
render() {
let usersTyping = this.state.usersTyping;
const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers)
.map((userId) => this.props.room.getMember(userId));
@ -222,5 +215,5 @@ export default createReactClass({
</div>
</li>
);
},
});
}
}