Merge branch 'develop' into ctrl-enter-send
This commit is contained in:
commit
1346416d20
451 changed files with 25135 additions and 10894 deletions
|
@ -15,27 +15,25 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import classNames from 'classnames';
|
||||
import {Resizable} from "re-resizable";
|
||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
import Resizer from "../../../resizer/resizer";
|
||||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
|
||||
export default class AppsDrawer extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -43,12 +41,10 @@ export default class AppsDrawer extends React.Component {
|
|||
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
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showApps: true,
|
||||
hide: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -57,22 +53,27 @@ export default class AppsDrawer extends React.Component {
|
|||
this.state = {
|
||||
apps: this._getApps(),
|
||||
};
|
||||
|
||||
this._resizeContainer = null;
|
||||
this.resizer = this._createResizer();
|
||||
|
||||
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
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() {
|
||||
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);
|
||||
if (this._resizeContainer) {
|
||||
this.resizer.detach();
|
||||
}
|
||||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
|
@ -82,6 +83,95 @@ export default class AppsDrawer extends React.Component {
|
|||
this._updateApps();
|
||||
}
|
||||
|
||||
onIsResizing = (resizing) => {
|
||||
this.setState({ resizing });
|
||||
if (!resizing) {
|
||||
this._relaxResizer();
|
||||
}
|
||||
};
|
||||
|
||||
_createResizer() {
|
||||
const classNames = {
|
||||
handle: "mx_ResizeHandle",
|
||||
vertical: "mx_ResizeHandle_vertical",
|
||||
reverse: "mx_ResizeHandle_reverse",
|
||||
};
|
||||
const collapseConfig = {
|
||||
onResizeStart: () => {
|
||||
this._resizeContainer.classList.add("mx_AppsDrawer_resizing");
|
||||
},
|
||||
onResizeStop: () => {
|
||||
this._resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
// persist to localStorage
|
||||
localStorage.setItem(this._getStorageKey(), JSON.stringify([
|
||||
this.state.apps.map(app => app.id),
|
||||
...this.state.apps.slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
]));
|
||||
},
|
||||
};
|
||||
// pass a truthy container for now, we won't call attach until we update it
|
||||
const resizer = new Resizer({}, PercentageDistributor, collapseConfig);
|
||||
resizer.setClassNames(classNames);
|
||||
return resizer;
|
||||
}
|
||||
|
||||
_collectResizer = (ref) => {
|
||||
if (this._resizeContainer) {
|
||||
this.resizer.detach();
|
||||
}
|
||||
|
||||
if (ref) {
|
||||
this.resizer.container = ref;
|
||||
this.resizer.attach();
|
||||
}
|
||||
this._resizeContainer = ref;
|
||||
this._loadResizerPreferences();
|
||||
};
|
||||
|
||||
_getStorageKey = () => `mx_apps_drawer-${this.props.room.roomId}`;
|
||||
|
||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||
this._loadResizerPreferences();
|
||||
}
|
||||
}
|
||||
|
||||
_relaxResizer = () => {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
|
||||
// relax all items if they had any overconstrained flexboxes
|
||||
distributors.forEach(d => d.start());
|
||||
distributors.forEach(d => d.finish());
|
||||
};
|
||||
|
||||
_loadResizerPreferences = () => {
|
||||
try {
|
||||
const [[...lastIds], ...sizes] = JSON.parse(localStorage.getItem(this._getStorageKey()));
|
||||
// Every app was included in the last split, reuse the last sizes
|
||||
if (this.state.apps.length <= lastIds.length && this.state.apps.every((app, i) => lastIds[i] === app.id)) {
|
||||
sizes.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
distributor.size = size;
|
||||
distributor.finish();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// this is expected
|
||||
}
|
||||
|
||||
if (this.state.apps) {
|
||||
const distributors = this.resizer.getDistributors();
|
||||
distributors.forEach(d => d.item.clearSize());
|
||||
distributors.forEach(d => d.start());
|
||||
distributors.forEach(d => d.finish());
|
||||
}
|
||||
};
|
||||
|
||||
onAction = (action) => {
|
||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
|
@ -100,40 +190,14 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onRoomStateEvents = (ev, state) => {
|
||||
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||
return;
|
||||
}
|
||||
this._updateApps();
|
||||
};
|
||||
|
||||
_getApps() {
|
||||
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(),
|
||||
);
|
||||
});
|
||||
}
|
||||
_getApps = () => WidgetStore.instance.getPinnedApps(this.props.room.roomId);
|
||||
|
||||
_updateApps = () => {
|
||||
const apps = this._getApps();
|
||||
this.setState({
|
||||
apps: apps,
|
||||
apps: this._getApps(),
|
||||
});
|
||||
};
|
||||
|
||||
_canUserModify() {
|
||||
try {
|
||||
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_launchManageIntegrations() {
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll();
|
||||
|
@ -142,24 +206,9 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!this.props.showApps) return <div />;
|
||||
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
||||
|
||||
|
@ -169,9 +218,8 @@ export default class AppsDrawer extends React.Component {
|
|||
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}
|
||||
/>);
|
||||
|
@ -181,21 +229,6 @@ export default class AppsDrawer extends React.Component {
|
|||
return <div />;
|
||||
}
|
||||
|
||||
let addWidget;
|
||||
if (this.props.showApps &&
|
||||
this._canUserModify()
|
||||
) {
|
||||
addWidget = <AccessibleButton
|
||||
onClick={this.onClickAddWidget}
|
||||
className={this.state.apps.length<2 ?
|
||||
'mx_AddWidget_button mx_AddWidget_button_full_width' :
|
||||
'mx_AddWidget_button'
|
||||
}
|
||||
title={_t('Add a widget')}>
|
||||
[+] { _t('Add a widget') }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let spinner;
|
||||
if (
|
||||
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
||||
|
@ -208,10 +241,11 @@ export default class AppsDrawer extends React.Component {
|
|||
}
|
||||
|
||||
const classes = classNames({
|
||||
"mx_AppsDrawer": true,
|
||||
"mx_AppsDrawer_hidden": this.props.hide,
|
||||
"mx_AppsDrawer_fullWidth": apps.length < 2,
|
||||
"mx_AppsDrawer_minimised": !this.props.showApps,
|
||||
mx_AppsDrawer: true,
|
||||
mx_AppsDrawer_fullWidth: apps.length < 2,
|
||||
mx_AppsDrawer_resizing: this.state.resizing,
|
||||
mx_AppsDrawer_2apps: apps.length === 2,
|
||||
mx_AppsDrawer_3apps: apps.length === 3,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -221,13 +255,20 @@ export default class AppsDrawer extends React.Component {
|
|||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight ? this.props.maxHeight - 50 : undefined}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
className="mx_AppsContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{ apps }
|
||||
{ spinner }
|
||||
<div className="mx_AppsContainer" ref={this._collectResizer}>
|
||||
{ apps.map((app, i) => {
|
||||
if (i < 1) return app;
|
||||
return <React.Fragment key={app.key}>
|
||||
<ResizeHandle reverse={i > apps.length / 2} />
|
||||
{ app }
|
||||
</React.Fragment>;
|
||||
}) }
|
||||
</div>
|
||||
</PersistentVResizer>
|
||||
{ this._canUserModify() && addWidget }
|
||||
{ spinner }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -243,15 +284,13 @@ const PersistentVResizer = ({
|
|||
resizeNotifier,
|
||||
children,
|
||||
}) => {
|
||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 100);
|
||||
const [resizing, setResizing] = useState(false);
|
||||
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
|
||||
|
||||
return <Resizable
|
||||
size={{height: Math.min(height, maxHeight)}}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
||||
if (!resizing) setResizing(true);
|
||||
resizeNotifier.startResizing();
|
||||
}}
|
||||
onResize={() => {
|
||||
|
@ -259,14 +298,11 @@ const PersistentVResizer = ({
|
|||
}}
|
||||
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,
|
||||
})}
|
||||
className={className}
|
||||
enable={{bottom: true}}
|
||||
>
|
||||
{ children }
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2015, 2016, 2017, 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.
|
||||
|
@ -16,8 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room'
|
||||
import * as sdk from '../../../index';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as ObjectUtils from '../../../ObjectUtils';
|
||||
|
@ -28,38 +27,45 @@ import RateLimitedFunc from '../../../ratelimitedfunc';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import CallView from "../voip/CallView";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||
|
||||
interface IProps {
|
||||
// js-sdk room object
|
||||
room: Room,
|
||||
userId: string,
|
||||
showApps: boolean, // Render apps
|
||||
|
||||
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)
|
||||
// set to true to show the file drop target
|
||||
draggingFile: boolean,
|
||||
|
||||
// Conference Handler implementation
|
||||
conferenceHandler: PropTypes.object,
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: number,
|
||||
|
||||
// set to true to show the file drop target
|
||||
draggingFile: PropTypes.bool,
|
||||
// a callback which is called when the content of the aux panel changes
|
||||
// content in a way that is likely to make it change size.
|
||||
onResize: () => void,
|
||||
fullHeight: boolean,
|
||||
|
||||
// set to true to show the 'active conf call' banner
|
||||
displayConfCallNotification: PropTypes.bool,
|
||||
resizeNotifier: ResizeNotifier,
|
||||
}
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: PropTypes.number,
|
||||
interface Counter {
|
||||
title: string,
|
||||
value: number,
|
||||
link: string,
|
||||
severity: string,
|
||||
stateKey: string,
|
||||
}
|
||||
|
||||
// a callback which is called when the content of the aux panel changes
|
||||
// content in a way that is likely to make it change size.
|
||||
onResize: PropTypes.func,
|
||||
fullHeight: PropTypes.bool,
|
||||
};
|
||||
interface IState {
|
||||
counters: Counter[],
|
||||
}
|
||||
|
||||
export default class AuxPanel extends React.Component<IProps, IState> {
|
||||
static defaultProps = {
|
||||
showApps: true,
|
||||
hideAppsDrawer: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -111,7 +117,7 @@ export default class AuxPanel extends React.Component {
|
|||
}, 500);
|
||||
|
||||
_computeCounters() {
|
||||
let counters = [];
|
||||
const counters = [];
|
||||
|
||||
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
|
||||
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
|
||||
|
@ -119,7 +125,7 @@ export default class AuxPanel extends React.Component {
|
|||
return a.getStateKey() < b.getStateKey();
|
||||
});
|
||||
|
||||
stateEvs.forEach((ev, idx) => {
|
||||
for (const ev of stateEvs) {
|
||||
const title = ev.getContent().title;
|
||||
const value = ev.getContent().value;
|
||||
const link = ev.getContent().link;
|
||||
|
@ -130,14 +136,14 @@ export default class AuxPanel extends React.Component {
|
|||
// zero)
|
||||
if (title && value !== undefined) {
|
||||
counters.push({
|
||||
"title": title,
|
||||
"value": value,
|
||||
"link": link,
|
||||
"severity": severity,
|
||||
"stateKey": stateKey
|
||||
title,
|
||||
value,
|
||||
link,
|
||||
severity,
|
||||
stateKey,
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return counters;
|
||||
|
@ -150,8 +156,7 @@ export default class AuxPanel extends React.Component {
|
|||
if (this.props.draggingFile) {
|
||||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
<div className="mx_RoomView_fileDropTargetLabel"
|
||||
title={_t("Drop File Here")}>
|
||||
<div className="mx_RoomView_fileDropTargetLabel" title={_t("Drop File Here")}>
|
||||
<TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />
|
||||
<br />
|
||||
{ _t("Drop file here to upload") }
|
||||
|
@ -160,56 +165,28 @@ export default class AuxPanel extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
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}) }
|
||||
|
||||
{ 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}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
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;
|
||||
|
@ -218,7 +195,7 @@ export default class AuxPanel extends React.Component {
|
|||
const severity = counter.severity;
|
||||
const stateKey = counter.stateKey;
|
||||
|
||||
let span = <span>{ title }: { value }</span>
|
||||
let span = <span>{ title }: { value }</span>;
|
||||
|
||||
if (link) {
|
||||
span = (
|
||||
|
@ -243,7 +220,7 @@ export default class AuxPanel extends React.Component {
|
|||
<span
|
||||
className="m_RoomView_auxPanel_stateViews_delim"
|
||||
key={"delim" + idx}
|
||||
> ─ </span>
|
||||
> ─ </span>,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -261,7 +238,7 @@ export default class AuxPanel extends React.Component {
|
|||
"mx_RoomView_auxPanel": true,
|
||||
"mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
|
||||
});
|
||||
const style = {};
|
||||
const style: React.CSSProperties = {};
|
||||
if (!this.props.fullHeight) {
|
||||
style.maxHeight = this.props.maxHeight;
|
||||
}
|
||||
|
@ -272,7 +249,6 @@ export default class AuxPanel extends React.Component {
|
|||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ conferenceCallNotification }
|
||||
{ this.props.children }
|
||||
</AutoHideScrollbar>
|
||||
);
|
|
@ -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) {
|
||||
|
|
|
@ -33,6 +33,7 @@ import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
|||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
|
@ -186,6 +187,7 @@ export default class EditMessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
_sendEdit = () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
|
@ -194,8 +196,9 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (this._isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
this.context.sendMessage(roomId, editContent);
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
|
|
|
@ -21,6 +21,7 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from "classnames";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import * as TextForEvent from "../../../TextForEvent";
|
||||
import * as sdk from "../../../index";
|
||||
|
@ -34,6 +35,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',
|
||||
|
@ -44,6 +47,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 = {
|
||||
|
@ -110,6 +114,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];
|
||||
}
|
||||
|
||||
|
@ -147,6 +164,10 @@ export default class EventTile extends React.Component {
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -206,6 +227,9 @@ export default class EventTile extends React.Component {
|
|||
|
||||
// whether to use the irc layout
|
||||
useIRCLayout: PropTypes.bool,
|
||||
|
||||
// whether or not to show flair at all
|
||||
enableFlair: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -619,22 +643,24 @@ export default class EventTile extends React.Component {
|
|||
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 === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
|
||||
(eventType === EventType.RoomCreate) ||
|
||||
(eventType === EventType.RoomEncryption) ||
|
||||
(tileHandler === "messages.MJitsiWidgetEvent");
|
||||
let isInfoMessage = (
|
||||
!isBubbleMessage && eventType !== 'm.room.message' &&
|
||||
eventType !== 'm.sticker' && eventType !== 'm.room.create'
|
||||
!isBubbleMessage && eventType !== EventType.RoomMessage &&
|
||||
eventType !== EventType.Sticker && eventType !== EventType.RoomCreate
|
||||
);
|
||||
|
||||
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;
|
||||
|
@ -670,6 +696,7 @@ export default class EventTile extends React.Component {
|
|||
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,
|
||||
|
@ -736,10 +763,10 @@ export default class EventTile extends React.Component {
|
|||
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} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -818,6 +845,7 @@ export default class EventTile extends React.Component {
|
|||
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>
|
||||
|
@ -892,6 +920,7 @@ export default class EventTile extends React.Component {
|
|||
highlights={this.props.highlights}
|
||||
highlightLink={this.props.highlightLink}
|
||||
onHeightChanged={this.props.onHeightChanged}
|
||||
replacingEventId={this.props.replacingEventId}
|
||||
showUrlPreview={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1027,11 +1056,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}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import {Key} from '../../../Keyboard';
|
||||
|
||||
|
||||
|
@ -28,19 +27,10 @@ export default class ForwardMessage extends React.Component {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
dis.dispatch({
|
||||
action: 'panel_disable',
|
||||
middleDisabled: true,
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.dispatch({
|
||||
action: 'panel_disable',
|
||||
middleDisabled: false,
|
||||
});
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
}
|
||||
|
||||
|
|
|
@ -114,7 +114,10 @@ export default class LinkPreviewWidget extends React.Component {
|
|||
|
||||
let thumbHeight = imageMaxHeight;
|
||||
if (p["og:image:width"] && p["og:image:height"]) {
|
||||
thumbHeight = ImageUtils.thumbHeight(p["og:image:width"], p["og:image:height"], imageMaxWidth, imageMaxHeight);
|
||||
thumbHeight = ImageUtils.thumbHeight(
|
||||
p["og:image:width"], p["og:image:height"],
|
||||
imageMaxWidth, imageMaxHeight,
|
||||
);
|
||||
}
|
||||
|
||||
let img;
|
||||
|
|
|
@ -20,13 +20,13 @@ import React from 'react';
|
|||
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;
|
||||
|
@ -121,8 +121,8 @@ export default class MemberList extends React.Component {
|
|||
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()));
|
||||
}
|
||||
}
|
||||
|
@ -232,15 +232,10 @@ export default class MemberList extends React.Component {
|
|||
}
|
||||
|
||||
roomMembers() {
|
||||
const ConferenceHandler = CallHandler.getConferenceHandler();
|
||||
|
||||
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);
|
||||
|
@ -438,7 +433,13 @@ export default class MemberList extends React.Component {
|
|||
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');
|
||||
|
@ -485,25 +486,29 @@ export default class MemberList extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
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 } />
|
||||
);
|
||||
|
||||
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 = () => {
|
||||
|
|
|
@ -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.
|
||||
|
@ -22,7 +23,6 @@ import CallHandler from '../../../CallHandler';
|
|||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
|
@ -31,6 +31,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 +54,7 @@ function CallButton(props) {
|
|||
const onVoiceCallClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: "voice",
|
||||
type: PlaceCallType.Voice,
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
@ -67,7 +74,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 +91,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}) => {
|
||||
|
@ -222,15 +253,18 @@ export default class MessageComposer extends React.Component {
|
|||
super(props);
|
||||
this.onInputStateChanged = this.onInputStateChanged.bind(this);
|
||||
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
|
||||
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()),
|
||||
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,10 +280,17 @@ 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);
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
this._waitForOwnMember();
|
||||
}
|
||||
|
||||
|
@ -273,9 +314,8 @@ export default class MessageComposer extends React.Component {
|
|||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
|
||||
}
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
WidgetStore.instance.removeListener(UPDATE_EVENT, this._onWidgetUpdate);
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetUpdate);
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
||||
|
@ -294,12 +334,6 @@ export default class MessageComposer extends React.Component {
|
|||
return this.props.room.currentState.getStateEvents('m.room.tombstone', '');
|
||||
}
|
||||
|
||||
_onRoomViewStoreUpdate() {
|
||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||
if (this.state.isQuoting === isQuoting) return;
|
||||
this.setState({ isQuoting });
|
||||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
// Merge the new input state with old to support partial updates
|
||||
inputState = Object.assign({}, this.state.inputState, inputState);
|
||||
|
@ -324,6 +358,7 @@ export default class MessageComposer extends React.Component {
|
|||
event_id: createEventId,
|
||||
room_id: replacementRoomId,
|
||||
auto_join: true,
|
||||
_type: "tombstone", // instrumentation
|
||||
|
||||
// Try to join via the server that sent the event. This converts @something:example.org
|
||||
// into a server domain by splitting on colons and ignoring the first entry ("@something").
|
||||
|
@ -336,7 +371,7 @@ export default class MessageComposer extends React.Component {
|
|||
}
|
||||
|
||||
renderPlaceholderText() {
|
||||
if (this.state.isQuoting) {
|
||||
if (this.props.replyToEvent) {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
} else {
|
||||
|
@ -381,16 +416,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.props.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(
|
||||
|
|
135
src/components/views/rooms/NewRoomIntro.tsx
Normal file
135
src/components/views/rooms/NewRoomIntro.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
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, {useContext} from "react";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
const NewRoomIntro = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const {room, roomId} = useContext(RoomContext);
|
||||
|
||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
let body;
|
||||
if (dmPartner) {
|
||||
let caption;
|
||||
if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
||||
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
||||
}
|
||||
|
||||
const member = room?.getMember(dmPartner);
|
||||
const displayName = member?.rawDisplayName || dmPartner;
|
||||
body = <React.Fragment>
|
||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
|
||||
defaultDispatcher.dispatch<ViewUserPayload>({
|
||||
action: Action.ViewUser,
|
||||
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||
member: member || {userId: dmPartner},
|
||||
});
|
||||
}} />
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
|
||||
<p>{_t("This is the beginning of your direct message history with <displayName/>.", {}, {
|
||||
displayName: () => <b>{ displayName }</b>,
|
||||
})}</p>
|
||||
{ caption && <p>{ caption }</p> }
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||
const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
|
||||
const onTopicClick = () => {
|
||||
dis.dispatch({
|
||||
action: "open_room_settings",
|
||||
room_id: roomId,
|
||||
}, true);
|
||||
// focus the topic field to help the user find it as it'll gain an outline
|
||||
setImmediate(() => {
|
||||
window.document.getElementById("profileTopic").focus();
|
||||
});
|
||||
};
|
||||
|
||||
let topicText;
|
||||
if (canAddTopic && topic) {
|
||||
topicText = _t("Topic: %(topic)s (<a>edit</a>)", { topic }, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||
});
|
||||
} else if (topic) {
|
||||
topicText = _t("Topic: %(topic)s ", { topic });
|
||||
} else if (canAddTopic) {
|
||||
topicText = _t("<a>Add a topic</a> to help people know what it is about.", {}, {
|
||||
a: sub => <AccessibleButton kind="link" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||
});
|
||||
}
|
||||
|
||||
const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
const creatorName = room?.getMember(creator)?.rawDisplayName || creator;
|
||||
|
||||
let createdText;
|
||||
if (creator === cli.getUserId()) {
|
||||
createdText = _t("You created this room.");
|
||||
} else {
|
||||
createdText = _t("%(displayName)s created this room.", {
|
||||
displayName: creatorName,
|
||||
});
|
||||
}
|
||||
|
||||
const onInviteClick = () => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
};
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
body = <React.Fragment>
|
||||
<MiniAvatarUploader
|
||||
hasAvatar={!!avatarUrl}
|
||||
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
|
||||
>
|
||||
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} />
|
||||
</MiniAvatarUploader>
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
|
||||
<p>{createdText} {_t("This is the start of <roomName/>.", {}, {
|
||||
roomName: () => <b>{ room.name }</b>,
|
||||
})}</p>
|
||||
<p>{topicText}</p>
|
||||
<div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_NewRoomIntro">
|
||||
{ body }
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default NewRoomIntro;
|
|
@ -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";
|
||||
|
@ -109,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({
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
};
|
||||
|
||||
|
|
|
@ -18,14 +18,11 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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';
|
||||
|
@ -45,6 +42,8 @@ export default class RoomHeader extends React.Component {
|
|||
onLeaveClick: PropTypes.func,
|
||||
onCancelClick: PropTypes.func,
|
||||
e2eStatus: PropTypes.string,
|
||||
onAppsClick: PropTypes.func,
|
||||
appsShown: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -114,13 +113,6 @@ export default class RoomHeader extends React.Component {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onShareRoomClick = (ev) => {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
target: this.props.room,
|
||||
});
|
||||
};
|
||||
|
||||
_hasUnreadPins() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
|
@ -150,7 +142,6 @@ export default class RoomHeader extends React.Component {
|
|||
render() {
|
||||
let searchStatus = null;
|
||||
let cancelButton = null;
|
||||
let settingsButton = null;
|
||||
let pinnedEventsButton = null;
|
||||
|
||||
if (this.props.onCancelClick) {
|
||||
|
@ -214,14 +205,6 @@ export default class RoomHeader extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
@ -249,6 +232,17 @@ export default class RoomHeader extends React.Component {
|
|||
title={_t("Forget room")} />;
|
||||
}
|
||||
|
||||
let appsButton;
|
||||
if (this.props.onAppsClick) {
|
||||
appsButton =
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||
})}
|
||||
onClick={this.props.onAppsClick}
|
||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")} />;
|
||||
}
|
||||
|
||||
let searchButton;
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
searchButton =
|
||||
|
@ -258,27 +252,11 @@ export default class RoomHeader extends React.Component {
|
|||
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 }
|
||||
{ appsButton }
|
||||
{ searchButton }
|
||||
</div>;
|
||||
|
||||
|
|
|
@ -53,12 +53,12 @@ interface IProps {
|
|||
onBlur: (ev: React.FocusEvent) => void;
|
||||
onResize: () => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
collapsed: boolean;
|
||||
isMinimized: boolean;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
sublists: ITagMap;
|
||||
isNameFiltering: boolean;
|
||||
}
|
||||
|
||||
const TAG_ORDER: TagID[] = [
|
||||
|
@ -184,6 +184,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
this.state = {
|
||||
sublists: {},
|
||||
isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(),
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
|
@ -254,7 +255,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
return CustomRoomTagStore.getTags()[t];
|
||||
});
|
||||
|
||||
let doUpdate = arrayHasDiff(previousListIds, newListIds);
|
||||
const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition();
|
||||
let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds);
|
||||
if (!doUpdate) {
|
||||
// so we didn't have the visible sublists change, but did the contents of those
|
||||
// sublists change significantly enough to break the sticky headers? Probably, so
|
||||
|
@ -276,14 +278,20 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
const newSublists = objectWithOnly(newLists, newListIds);
|
||||
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
||||
|
||||
this.setState({sublists}, () => {
|
||||
this.setState({sublists, isNameFiltering}, () => {
|
||||
this.props.onResize();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onStartChat = () => {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
dis.dispatch({ action: "view_create_chat", initialText });
|
||||
};
|
||||
|
||||
private onExplore = () => {
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||
};
|
||||
|
||||
private renderCommunityInvites(): TemporaryTile[] {
|
||||
|
@ -333,6 +341,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
return p;
|
||||
}, [] as TagID[]);
|
||||
|
||||
// show a skeleton UI if the user is in no rooms and they are not filtering
|
||||
const showSkeleton = !this.state.isNameFiltering &&
|
||||
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
|
||||
|
||||
for (const orderedTagId of tagOrder) {
|
||||
const orderedRooms = this.state.sublists[orderedTagId] || [];
|
||||
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||
|
@ -357,6 +369,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
addRoomContextMenu={aesthetics.addRoomContextMenu}
|
||||
isMinimized={this.props.isMinimized}
|
||||
onResize={this.props.onResize}
|
||||
showSkeleton={showSkeleton}
|
||||
extraBadTilesThatShouldntExist={extraTiles}
|
||||
/>);
|
||||
}
|
||||
|
@ -366,13 +379,50 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
public render() {
|
||||
let explorePrompt: JSX.Element;
|
||||
if (RoomListStore.instance.getFirstNameFilterCondition()) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Can't see what you’re looking for?")}</div>
|
||||
<AccessibleButton kind="link" onClick={this.onExplore}>
|
||||
{_t("Explore all public rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
if (!this.props.isMinimized) {
|
||||
if (this.state.isNameFiltering) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Can't see what you’re looking for?")}</div>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_startChat"
|
||||
kind="link"
|
||||
onClick={this.onStartChat}
|
||||
>
|
||||
{_t("Start a new chat")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_explore"
|
||||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore all public rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||
const unfilteredLists = RoomListStore.instance.unfilteredLists
|
||||
const unfilteredRooms = unfilteredLists[DefaultTagID.Untagged] || [];
|
||||
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
|
||||
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
|
||||
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_startChat"
|
||||
kind="link"
|
||||
onClick={this.onStartChat}
|
||||
>
|
||||
{_t("Start a new chat")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_explore"
|
||||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore all public rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sublists = this.renderSublists();
|
||||
|
|
|
@ -284,7 +284,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
room_name: this.props.oobData ? this.props.oobData.room_name : null,
|
||||
room_avatar_url: this.props.oobData ? this.props.oobData.avatarUrl : null,
|
||||
inviter_name: this.props.oobData ? this.props.oobData.inviterName : null,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -337,7 +337,7 @@ export default class RoomPreviewBar extends React.Component {
|
|||
if (this.props.previewLoading) {
|
||||
footer = (
|
||||
<div>
|
||||
<Spinner w={20} h={20}/>
|
||||
<Spinner w={20} h={20} />
|
||||
{_t("Loading room preview")}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -71,6 +71,7 @@ interface IProps {
|
|||
isMinimized: boolean;
|
||||
tagId: TagID;
|
||||
onResize: () => void;
|
||||
showSkeleton?: boolean;
|
||||
|
||||
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
||||
// You should feel bad if you use this.
|
||||
|
@ -399,6 +400,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
||||
await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
|
||||
this.forceUpdate(); // because if the sublist doesn't have any changes then we will miss the list order change
|
||||
};
|
||||
|
||||
private onTagSortChanged = async (sort: SortAlgorithm) => {
|
||||
|
@ -420,7 +422,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
room = this.state.rooms && this.state.rooms[0];
|
||||
} else {
|
||||
// find the first room with a count of the same colour as the badge count
|
||||
room = this.state.rooms.find((r: Room) => {
|
||||
room = RoomListStore.instance.unfilteredLists[this.props.tagId].find((r: Room) => {
|
||||
const notifState = this.notificationState.getForRoom(r);
|
||||
return notifState.count > 0 && notifState.color === this.notificationState.color;
|
||||
});
|
||||
|
@ -876,6 +878,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
</Resizable>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else if (this.props.showSkeleton && this.state.isExpanded) {
|
||||
content = <div className="mx_RoomSublist_skeletonUI" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -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.
|
||||
|
@ -28,6 +28,11 @@ export default class RoomUpgradeWarningBar extends React.Component {
|
|||
recommendation: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
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});
|
||||
|
@ -35,6 +40,13 @@ export default class RoomUpgradeWarningBar extends React.Component {
|
|||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvents);
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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.
|
||||
|
@ -19,6 +20,7 @@ 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 class SearchBar extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -72,21 +74,24 @@ export default class SearchBar extends React.Component {
|
|||
});
|
||||
|
||||
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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import {haveTileForEvent} from "./EventTile";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
export default class SearchResultTile extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -45,22 +47,31 @@ export default class SearchResultTile extends React.Component {
|
|||
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>);
|
||||
}
|
||||
|
|
|
@ -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,10 +40,10 @@ import {_t, _td} from '../../../languageHandler';
|
|||
import ContentMessages from '../../../ContentMessages';
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||
|
@ -62,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);
|
||||
|
@ -71,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;
|
||||
|
@ -96,6 +94,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
room: PropTypes.object.isRequired,
|
||||
placeholder: PropTypes.string,
|
||||
permalinkCreator: PropTypes.object.isRequired,
|
||||
replyToEvent: PropTypes.object,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -105,12 +104,13 @@ export default class SendMessageComposer extends React.Component {
|
|||
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 => {
|
||||
|
@ -149,7 +149,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
|
||||
|
@ -191,9 +191,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();
|
||||
}
|
||||
}
|
||||
|
@ -303,12 +307,16 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
if (shouldSend) {
|
||||
const isReply = !!RoomViewStore.getQuotingEvent();
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const {roomId} = this.props.room;
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
||||
this.context.sendMessage(roomId, content);
|
||||
if (isReply) {
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
||||
// don't bother sending an empty message
|
||||
if (!content.body.trim()) return;
|
||||
|
||||
const prom = this.context.sendMessage(roomId, content);
|
||||
if (replyToEvent) {
|
||||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
dis.dispatch({
|
||||
|
@ -317,9 +325,10 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
|
||||
}
|
||||
|
||||
this.sendHistoryManager.save(this.model);
|
||||
this.sendHistoryManager.save(this.model, replyToEvent);
|
||||
// clear composer
|
||||
this.model.reset([]);
|
||||
this._editorRef.clearUndoHistory();
|
||||
|
@ -329,6 +338,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
|
||||
|
@ -337,11 +348,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() {
|
||||
|
@ -351,9 +362,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -361,7 +382,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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -453,7 +475,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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -270,13 +272,10 @@ export default class Stickerpicker extends React.Component {
|
|||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||
waitForIframeLoad={true}
|
||||
show={true}
|
||||
showMenubar={true}
|
||||
onEditClick={this._launchManageIntegrations}
|
||||
onDeleteClick={this._removeStickerpickerWidgets}
|
||||
showTitle={false}
|
||||
showMinimise={true}
|
||||
showDelete={false}
|
||||
showCancel={false}
|
||||
showPopout={false}
|
||||
onMinimiseClick={this._onHideStickersClick}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue