Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
|
@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { Resizable } from "re-resizable";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import AppTile from '../elements/AppTile';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import AppTile from "../elements/AppTile";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as ScalarMessaging from "../../../ScalarMessaging";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import ResizeHandle from "../elements/ResizeHandle";
|
||||
|
@ -46,7 +46,7 @@ interface IProps {
|
|||
|
||||
interface IState {
|
||||
// @ts-ignore - TS wants a string key, but we know better
|
||||
apps: {[id: Container]: IApp[]};
|
||||
apps: { [id: Container]: IApp[] };
|
||||
resizingVertical: boolean; // true when changing the height of the apps drawer
|
||||
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
|
||||
resizing: boolean;
|
||||
|
@ -117,8 +117,11 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
onResizeStop: () => {
|
||||
this.resizeContainer.classList.remove("mx_AppsDrawer_resizing");
|
||||
WidgetLayoutStore.instance.setResizerDistributions(
|
||||
this.props.room, Container.Top,
|
||||
this.topApps().slice(1).map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
this.props.room,
|
||||
Container.Top,
|
||||
this.topApps()
|
||||
.slice(1)
|
||||
.map((_, i) => this.resizer.forHandleAt(i).size),
|
||||
);
|
||||
this.setState({ resizingHorizontal: false });
|
||||
},
|
||||
|
@ -142,7 +145,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
this.loadResizerPreferences();
|
||||
};
|
||||
|
||||
private getAppsHash = (apps: IApp[]): string => apps.map(app => app.id).join("~");
|
||||
private getAppsHash = (apps: IApp[]): string => apps.map((app) => app.id).join("~");
|
||||
|
||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||
|
@ -157,13 +160,13 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
const distributors = this.resizer.getDistributors();
|
||||
|
||||
// relax all items if they had any overconstrained flexboxes
|
||||
distributors.forEach(d => d.start());
|
||||
distributors.forEach(d => d.finish());
|
||||
distributors.forEach((d) => d.start());
|
||||
distributors.forEach((d) => d.finish());
|
||||
};
|
||||
|
||||
private loadResizerPreferences = (): void => {
|
||||
const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, Container.Top);
|
||||
if (this.state.apps && (this.topApps().length - 1) === distributions.length) {
|
||||
if (this.state.apps && this.topApps().length - 1 === distributions.length) {
|
||||
distributions.forEach((size, i) => {
|
||||
const distributor = this.resizer.forHandleAt(i);
|
||||
if (distributor) {
|
||||
|
@ -173,9 +176,9 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
});
|
||||
} else 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());
|
||||
distributors.forEach((d) => d.item.clearSize());
|
||||
distributors.forEach((d) => d.start());
|
||||
distributors.forEach((d) => d.finish());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -184,7 +187,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onAction = (action: ActionPayload): void => {
|
||||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
|
||||
switch (action.action) {
|
||||
case "appsDrawer":
|
||||
// Note: these booleans are awkward because localstorage is fundamentally
|
||||
|
@ -223,17 +226,19 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
const widgetIsMaxmised: boolean = this.centerApps().length > 0;
|
||||
const appsToDisplay = widgetIsMaxmised ? this.centerApps() : this.topApps();
|
||||
const apps = appsToDisplay.map((app, index, arr) => {
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
app={app}
|
||||
fullWidth={arr.length < 2}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
pointerEvents={this.isResizing() ? 'none' : undefined}
|
||||
/>);
|
||||
return (
|
||||
<AppTile
|
||||
key={app.id}
|
||||
app={app}
|
||||
fullWidth={arr.length < 2}
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
pointerEvents={this.isResizing() ? "none" : undefined}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (apps.length === 0) {
|
||||
|
@ -242,10 +247,8 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
|
||||
let spinner;
|
||||
if (
|
||||
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
||||
this.props.room.roomId,
|
||||
WidgetUtils.getRoomWidgets(this.props.room),
|
||||
)
|
||||
apps.length === 0 &&
|
||||
WidgetEchoStore.roomHasPendingWidgets(this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room))
|
||||
) {
|
||||
spinner = <Spinner />;
|
||||
}
|
||||
|
@ -258,37 +261,43 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
|
|||
mx_AppsDrawer_2apps: apps.length === 2,
|
||||
mx_AppsDrawer_3apps: apps.length === 3,
|
||||
});
|
||||
const appContainers =
|
||||
const appContainers = (
|
||||
<div className="mx_AppsContainer" ref={this.collectResizer}>
|
||||
{ apps.map((app, i) => {
|
||||
{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>;
|
||||
return (
|
||||
<React.Fragment key={app.key}>
|
||||
<ResizeHandle reverse={i > apps.length / 2} />
|
||||
{app}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
let drawer;
|
||||
if (widgetIsMaxmised) {
|
||||
drawer = appContainers;
|
||||
} else {
|
||||
drawer = <PersistentVResizer
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight - 50}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}>
|
||||
{ appContainers }
|
||||
</PersistentVResizer>;
|
||||
drawer = (
|
||||
<PersistentVResizer
|
||||
room={this.props.room}
|
||||
minHeight={100}
|
||||
maxHeight={this.props.maxHeight - 50}
|
||||
handleClass="mx_AppsContainer_resizerHandle"
|
||||
handleWrapperClass="mx_AppsContainer_resizerHandleContainer"
|
||||
className="mx_AppsContainer_resizer"
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{appContainers}
|
||||
</PersistentVResizer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{ drawer }
|
||||
{ spinner }
|
||||
{drawer}
|
||||
{spinner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -329,33 +338,31 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
|||
defaultHeight = 280;
|
||||
}
|
||||
|
||||
return <Resizable
|
||||
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined }}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
||||
resizeNotifier.startResizing();
|
||||
}}
|
||||
onResize={() => {
|
||||
resizeNotifier.notifyTimelineHeightChanged();
|
||||
}}
|
||||
onResizeStop={(e, dir, ref, d) => {
|
||||
let newHeight = defaultHeight + d.height;
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
return (
|
||||
<Resizable
|
||||
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined }}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
||||
resizeNotifier.startResizing();
|
||||
}}
|
||||
onResize={() => {
|
||||
resizeNotifier.notifyTimelineHeightChanged();
|
||||
}}
|
||||
onResizeStop={(e, dir, ref, d) => {
|
||||
let newHeight = defaultHeight + d.height;
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
|
||||
WidgetLayoutStore.instance.setContainerHeight(
|
||||
room,
|
||||
Container.Top,
|
||||
newHeight,
|
||||
);
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
|
||||
resizeNotifier.stopResizing();
|
||||
}}
|
||||
handleWrapperClass={handleWrapperClass}
|
||||
handleClasses={{ bottom: handleClass }}
|
||||
className={className}
|
||||
enable={{ bottom: true }}
|
||||
>
|
||||
{ children }
|
||||
</Resizable>;
|
||||
resizeNotifier.stopResizing();
|
||||
}}
|
||||
handleWrapperClass={handleWrapperClass}
|
||||
handleClasses={{ bottom: handleClass }}
|
||||
className={className}
|
||||
enable={{ bottom: true }}
|
||||
>
|
||||
{children}
|
||||
</Resizable>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, KeyboardEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { createRef, KeyboardEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import { flatMap } from "lodash";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import Autocompleter, { ICompletion, ISelectionRange, IProviderCompletions } from '../../../autocomplete/Autocompleter';
|
||||
import Autocompleter, { ICompletion, ISelectionRange, IProviderCompletions } from "../../../autocomplete/Autocompleter";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
|
||||
const MAX_PROVIDER_MATCHES = 20;
|
||||
|
||||
|
@ -134,15 +134,15 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
private processQuery(query: string, selection: ISelectionRange): Promise<void> {
|
||||
return this.autocompleter.getCompletions(
|
||||
query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES,
|
||||
).then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
return;
|
||||
}
|
||||
this.processCompletions(completions);
|
||||
});
|
||||
return this.autocompleter
|
||||
.getCompletions(query, selection, this.state.forceComplete, MAX_PROVIDER_MATCHES)
|
||||
.then((completions) => {
|
||||
// Only ever process the completions for the most recent query being processed
|
||||
if (query !== this.queryRequested) {
|
||||
return;
|
||||
}
|
||||
this.processCompletions(completions);
|
||||
});
|
||||
}
|
||||
|
||||
private processCompletions(completions: IProviderCompletions[]): void {
|
||||
|
@ -154,10 +154,11 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
/* If the currently selected completion is still in the completion list,
|
||||
try to find it and jump to it. If not, select composer.
|
||||
*/
|
||||
const currentSelection = this.state.selectionOffset <= 1 ? null :
|
||||
this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||
selectionOffset = completionList.findIndex(
|
||||
(completion) => completion.completion === currentSelection);
|
||||
const currentSelection =
|
||||
this.state.selectionOffset <= 1
|
||||
? null
|
||||
: this.state.completionList[this.state.selectionOffset - 1].completion;
|
||||
selectionOffset = completionList.findIndex((completion) => completion.completion === currentSelection);
|
||||
if (selectionOffset === -1) {
|
||||
selectionOffset = 1;
|
||||
} else {
|
||||
|
@ -227,14 +228,17 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
|
||||
public forceComplete(): Promise<number> {
|
||||
return new Promise((resolve) => {
|
||||
this.setState({
|
||||
forceComplete: true,
|
||||
hide: false,
|
||||
}, () => {
|
||||
this.complete(this.props.query, this.props.selection).then(() => {
|
||||
resolve(this.countCompletions());
|
||||
});
|
||||
});
|
||||
this.setState(
|
||||
{
|
||||
forceComplete: true,
|
||||
hide: false,
|
||||
},
|
||||
() => {
|
||||
this.complete(this.props.query, this.props.selection).then(() => {
|
||||
resolve(this.countCompletions());
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -278,38 +282,40 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
|
|||
|
||||
render() {
|
||||
let position = 1;
|
||||
const renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, j) => {
|
||||
const selected = position === this.state.selectionOffset;
|
||||
const className = classNames('mx_Autocomplete_Completion', { selected });
|
||||
const componentPosition = position;
|
||||
position++;
|
||||
const renderedCompletions = this.state.completions
|
||||
.map((completionResult, i) => {
|
||||
const completions = completionResult.completions.map((completion, j) => {
|
||||
const selected = position === this.state.selectionOffset;
|
||||
const className = classNames("mx_Autocomplete_Completion", { selected });
|
||||
const componentPosition = position;
|
||||
position++;
|
||||
|
||||
const onClick = () => {
|
||||
this.onCompletionClicked(componentPosition);
|
||||
};
|
||||
const onClick = () => {
|
||||
this.onCompletionClicked(componentPosition);
|
||||
};
|
||||
|
||||
return React.cloneElement(completion.component, {
|
||||
"key": j,
|
||||
"ref": `completion${componentPosition}`,
|
||||
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
|
||||
className,
|
||||
onClick,
|
||||
"aria-selected": selected,
|
||||
return React.cloneElement(completion.component, {
|
||||
"key": j,
|
||||
"ref": `completion${componentPosition}`,
|
||||
"id": generateCompletionDomId(componentPosition - 1), // 0 index the completion IDs
|
||||
className,
|
||||
onClick,
|
||||
"aria-selected": selected,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection" role="presentation">
|
||||
<div className="mx_Autocomplete_provider_name">{ completionResult.provider.getName() }</div>
|
||||
{ completionResult.provider.renderCompletions(completions) }
|
||||
</div>
|
||||
) : null;
|
||||
}).filter((completion) => !!completion);
|
||||
return completions.length > 0 ? (
|
||||
<div key={i} className="mx_Autocomplete_ProviderSection" role="presentation">
|
||||
<div className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</div>
|
||||
{completionResult.provider.renderCompletions(completions)}
|
||||
</div>
|
||||
) : null;
|
||||
})
|
||||
.filter((completion) => !!completion);
|
||||
|
||||
return !this.state.hide && renderedCompletions.length > 0 ? (
|
||||
<div id="mx_Autocomplete" className="mx_Autocomplete" ref={this.containerRef} role="listbox">
|
||||
{ renderedCompletions }
|
||||
{renderedCompletions}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
|
|
@ -14,20 +14,20 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { lexicographicCompare } from 'matrix-js-sdk/src/utils';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { throttle } from 'lodash';
|
||||
import React from "react";
|
||||
import { lexicographicCompare } from "matrix-js-sdk/src/utils";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { throttle } from "lodash";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import AppsDrawer from "./AppsDrawer";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import LegacyCallViewForRoom from '../voip/LegacyCallViewForRoom';
|
||||
import LegacyCallViewForRoom from "../voip/LegacyCallViewForRoom";
|
||||
import { objectHasDiff } from "../../../utils/objects";
|
||||
|
||||
interface IProps {
|
||||
|
@ -86,15 +86,19 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private updateCounters = throttle(() => {
|
||||
this.setState({ counters: this.computeCounters() });
|
||||
}, 500, { leading: true, trailing: true });
|
||||
private updateCounters = throttle(
|
||||
() => {
|
||||
this.setState({ counters: this.computeCounters() });
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
private computeCounters() {
|
||||
const counters = [];
|
||||
|
||||
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
|
||||
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
|
||||
const stateEvs = this.props.room.currentState.getStateEvents("re.jki.counter");
|
||||
stateEvs.sort((a, b) => lexicographicCompare(a.getStateKey(), b.getStateKey()));
|
||||
|
||||
for (const ev of stateEvs) {
|
||||
|
@ -132,12 +136,14 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
|
||||
let appsDrawer;
|
||||
if (SettingsStore.getValue(UIFeature.Widgets)) {
|
||||
appsDrawer = <AppsDrawer
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
showApps={this.props.showApps}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>;
|
||||
appsDrawer = (
|
||||
<AppsDrawer
|
||||
room={this.props.room}
|
||||
userId={this.props.userId}
|
||||
showApps={this.props.showApps}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let stateViews = null;
|
||||
|
@ -151,12 +157,16 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
const severity = counter.severity;
|
||||
const stateKey = counter.stateKey;
|
||||
|
||||
let span = <span>{ title }: { value }</span>;
|
||||
let span = (
|
||||
<span>
|
||||
{title}: {value}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (link) {
|
||||
span = (
|
||||
<a href={link} target="_blank" rel="noreferrer noopener">
|
||||
{ span }
|
||||
{span}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
@ -167,35 +177,31 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
data-severity={severity}
|
||||
key={"x-" + stateKey}
|
||||
>
|
||||
{ span }
|
||||
{span}
|
||||
</span>
|
||||
);
|
||||
|
||||
counters.push(span);
|
||||
counters.push(
|
||||
<span
|
||||
className="m_RoomView_auxPanel_stateViews_delim"
|
||||
key={"delim" + idx}
|
||||
> ─ </span>,
|
||||
<span className="m_RoomView_auxPanel_stateViews_delim" key={"delim" + idx}>
|
||||
{" "}
|
||||
─{" "}
|
||||
</span>,
|
||||
);
|
||||
});
|
||||
|
||||
if (counters.length > 0) {
|
||||
counters.pop(); // remove last deliminator
|
||||
stateViews = (
|
||||
<div className="m_RoomView_auxPanel_stateViews">
|
||||
{ counters }
|
||||
</div>
|
||||
);
|
||||
stateViews = <div className="m_RoomView_auxPanel_stateViews">{counters}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoHideScrollbar className="mx_RoomView_auxPanel">
|
||||
{ stateViews }
|
||||
{ this.props.children }
|
||||
{ appsDrawer }
|
||||
{ callView }
|
||||
{stateViews}
|
||||
{this.props.children}
|
||||
{appsDrawer}
|
||||
{callView}
|
||||
</AutoHideScrollbar>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,23 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { createRef, ClipboardEvent } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||
import classNames from "classnames";
|
||||
import React, { createRef, ClipboardEvent } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import EMOTICON_REGEX from "emojibase-regex/emoticon";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import EditorModel from '../../../editor/model';
|
||||
import HistoryManager from '../../../editor/history';
|
||||
import { Caret, setSelection } from '../../../editor/caret';
|
||||
import { formatRange, formatRangeAsLink, replaceRangeAndMoveCaret, toggleInlineFormat }
|
||||
from '../../../editor/operations';
|
||||
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
|
||||
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
||||
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
|
||||
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
|
||||
import { renderModel } from '../../../editor/render';
|
||||
import EditorModel from "../../../editor/model";
|
||||
import HistoryManager from "../../../editor/history";
|
||||
import { Caret, setSelection } from "../../../editor/caret";
|
||||
import {
|
||||
formatRange,
|
||||
formatRangeAsLink,
|
||||
replaceRangeAndMoveCaret,
|
||||
toggleInlineFormat,
|
||||
} from "../../../editor/operations";
|
||||
import { getCaretOffsetAndText, getRangeForSelection } from "../../../editor/dom";
|
||||
import Autocomplete, { generateCompletionDomId } from "../rooms/Autocomplete";
|
||||
import { getAutoCompleteCreator, Part, Type } from "../../../editor/parts";
|
||||
import { parseEvent, parsePlainTextMessage } from "../../../editor/deserialize";
|
||||
import { renderModel } from "../../../editor/render";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { IS_MAC, Key } from "../../../Keyboard";
|
||||
import { EMOTICON_TO_EMOJI } from "../../../emoji";
|
||||
|
@ -40,19 +44,19 @@ import MessageComposerFormatBar, { Formatting } from "./MessageComposerFormatBar
|
|||
import DocumentOffset from "../../../editor/offset";
|
||||
import { IDiff } from "../../../editor/diff";
|
||||
import AutocompleteWrapperModel from "../../../editor/autocomplete";
|
||||
import DocumentPosition from '../../../editor/position';
|
||||
import DocumentPosition from "../../../editor/position";
|
||||
import { ICompletion } from "../../../autocomplete/Autocompleter";
|
||||
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
|
||||
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { ALTERNATE_KEY_NAME, KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { linkify } from '../../../linkify-matrix';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
import { linkify } from "../../../linkify-matrix";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
// matches emoticons which follow the start of a line or whitespace
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
|
||||
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
|
||||
const REGEX_EMOTICON_WHITESPACE = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")\\s|:^$");
|
||||
export const REGEX_EMOTICON = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")$");
|
||||
|
||||
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
|
||||
const SURROUND_WITH_CHARACTERS = ['"', "_", "`", "'", "*", "~", "$"];
|
||||
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
|
||||
["(", ")"],
|
||||
["[", "]"],
|
||||
|
@ -61,10 +65,13 @@ const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
|
|||
]);
|
||||
|
||||
function ctrlShortcutLabel(key: string, needsShift = false, needsAlt = false): string {
|
||||
return (IS_MAC ? "⌘" : _t(ALTERNATE_KEY_NAME[Key.CONTROL])) +
|
||||
(needsShift ? ("+" + _t(ALTERNATE_KEY_NAME[Key.SHIFT])) : "") +
|
||||
(needsAlt ? ("+" + _t(ALTERNATE_KEY_NAME[Key.ALT])) : "") +
|
||||
"+" + key;
|
||||
return (
|
||||
(IS_MAC ? "⌘" : _t(ALTERNATE_KEY_NAME[Key.CONTROL])) +
|
||||
(needsShift ? "+" + _t(ALTERNATE_KEY_NAME[Key.SHIFT]) : "") +
|
||||
(needsAlt ? "+" + _t(ALTERNATE_KEY_NAME[Key.ALT]) : "") +
|
||||
"+" +
|
||||
key
|
||||
);
|
||||
}
|
||||
|
||||
function cloneSelection(selection: Selection): Partial<Selection> {
|
||||
|
@ -80,13 +87,15 @@ function cloneSelection(selection: Selection): Partial<Selection> {
|
|||
}
|
||||
|
||||
function selectionEquals(a: Partial<Selection>, b: Selection): boolean {
|
||||
return a.anchorNode === b.anchorNode &&
|
||||
return (
|
||||
a.anchorNode === b.anchorNode &&
|
||||
a.anchorOffset === b.anchorOffset &&
|
||||
a.focusNode === b.focusNode &&
|
||||
a.focusOffset === b.focusOffset &&
|
||||
a.isCollapsed === b.isCollapsed &&
|
||||
a.rangeCount === b.rangeCount &&
|
||||
a.type === b.type;
|
||||
a.type === b.type
|
||||
);
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
|
@ -140,15 +149,27 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
showVisualBell: false,
|
||||
};
|
||||
|
||||
this.useMarkdownHandle = SettingsStore.watchSetting('MessageComposerInput.useMarkdown', null,
|
||||
this.configureUseMarkdown);
|
||||
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
|
||||
this.configureEmoticonAutoReplace);
|
||||
this.useMarkdownHandle = SettingsStore.watchSetting(
|
||||
"MessageComposerInput.useMarkdown",
|
||||
null,
|
||||
this.configureUseMarkdown,
|
||||
);
|
||||
this.emoticonSettingHandle = SettingsStore.watchSetting(
|
||||
"MessageComposerInput.autoReplaceEmoji",
|
||||
null,
|
||||
this.configureEmoticonAutoReplace,
|
||||
);
|
||||
this.configureEmoticonAutoReplace();
|
||||
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
|
||||
this.configureShouldShowPillAvatar);
|
||||
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
|
||||
this.surroundWithSettingChanged);
|
||||
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting(
|
||||
"Pill.shouldShowPillAvatar",
|
||||
null,
|
||||
this.configureShouldShowPillAvatar,
|
||||
);
|
||||
this.surroundWithHandle = SettingsStore.watchSetting(
|
||||
"MessageComposerInput.surroundWith",
|
||||
null,
|
||||
this.surroundWithSettingChanged,
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IProps) {
|
||||
|
@ -208,7 +229,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||
renderModel(this.editorRef.current, this.props.model);
|
||||
if (selection) { // set the caret/selection
|
||||
if (selection) {
|
||||
// set the caret/selection
|
||||
try {
|
||||
setSelection(this.editorRef.current, this.props.model, selection);
|
||||
} catch (err) {
|
||||
|
@ -246,11 +268,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
isTyping = false;
|
||||
}
|
||||
}
|
||||
SdkContextClass.instance.typingStore.setSelfTyping(
|
||||
this.props.room.roomId,
|
||||
this.props.threadId,
|
||||
isTyping,
|
||||
);
|
||||
SdkContextClass.instance.typingStore.setSelfTyping(this.props.room.roomId, this.props.threadId, isTyping);
|
||||
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange();
|
||||
|
@ -259,7 +277,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private showPlaceholder(): void {
|
||||
// escape single quotes
|
||||
const placeholder = this.props.placeholder.replace(/'/g, '\\\'');
|
||||
const placeholder = this.props.placeholder.replace(/'/g, "\\'");
|
||||
this.editorRef.current.style.setProperty("--placeholder", `'${placeholder}'`);
|
||||
this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty");
|
||||
}
|
||||
|
@ -287,7 +305,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
// however, doing this async seems to break things in Safari for some reason, so browser sniff.
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isSafari = ua.includes('safari/') && !ua.includes('chrome/');
|
||||
const isSafari = ua.includes("safari/") && !ua.includes("chrome/");
|
||||
|
||||
if (isSafari) {
|
||||
this.onInput({ inputType: "insertCompositionText" });
|
||||
|
@ -311,7 +329,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
if (text) {
|
||||
const { model } = this.props;
|
||||
const range = getRangeForSelection(this.editorRef.current, model, selection);
|
||||
const selectedParts = range.parts.map(p => p.serialize());
|
||||
const selectedParts = range.parts.map((p) => p.serialize());
|
||||
event.clipboardData.setData("application/x-element-composer", JSON.stringify(selectedParts));
|
||||
event.clipboardData.setData("text/plain", text); // so plain copy/paste works
|
||||
if (type === "cut") {
|
||||
|
@ -346,7 +364,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
let parts: Part[];
|
||||
if (partsText) {
|
||||
const serializedTextParts = JSON.parse(partsText);
|
||||
parts = serializedTextParts.map(p => partCreator.deserializePart(p));
|
||||
parts = serializedTextParts.map((p) => partCreator.deserializePart(p));
|
||||
} else {
|
||||
parts = parsePlainTextMessage(plainText, partCreator, { shouldEscape: false });
|
||||
}
|
||||
|
@ -593,16 +611,16 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private async tabCompleteName(): Promise<void> {
|
||||
try {
|
||||
await new Promise<void>(resolve => this.setState({ showVisualBell: false }, resolve));
|
||||
await new Promise<void>((resolve) => this.setState({ showVisualBell: false }, resolve));
|
||||
const { model } = this.props;
|
||||
const caret = this.getCaret();
|
||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
const range = model.startRange(position);
|
||||
range.expandBackwardsWhile((index, offset, part) => {
|
||||
return part.text[offset] !== " " && part.text[offset] !== "+" && (
|
||||
part.type === Type.Plain ||
|
||||
part.type === Type.PillCandidate ||
|
||||
part.type === Type.Command
|
||||
return (
|
||||
part.text[offset] !== " " &&
|
||||
part.text[offset] !== "+" &&
|
||||
(part.type === Type.Plain || part.type === Type.PillCandidate || part.type === Type.Command)
|
||||
);
|
||||
});
|
||||
const { partCreator } = model;
|
||||
|
@ -664,7 +682,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
};
|
||||
|
||||
private transform = (documentPosition: DocumentPosition): void => {
|
||||
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
|
||||
const shouldReplace = SettingsStore.getValue("MessageComposerInput.autoReplaceEmoji");
|
||||
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
|
||||
};
|
||||
|
||||
|
@ -685,10 +703,12 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
const partCreator = model.partCreator;
|
||||
// TODO: does this allow us to get rid of EditorStateTransfer?
|
||||
// not really, but we could not serialize the parts, and just change the autoCompleter
|
||||
partCreator.setAutoCompleteCreator(getAutoCompleteCreator(
|
||||
() => this.autocompleteRef.current,
|
||||
query => new Promise(resolve => this.setState({ query }, resolve)),
|
||||
));
|
||||
partCreator.setAutoCompleteCreator(
|
||||
getAutoCompleteCreator(
|
||||
() => this.autocompleteRef.current,
|
||||
(query) => new Promise((resolve) => this.setState({ query }, resolve)),
|
||||
),
|
||||
);
|
||||
// initial render of model
|
||||
this.updateEditorState(this.getInitialCaretPosition());
|
||||
// attach input listener by hand so React doesn't proxy the events,
|
||||
|
@ -731,23 +751,25 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
if (this.state.autoComplete) {
|
||||
const query = this.state.query;
|
||||
const queryLen = query.length;
|
||||
autoComplete = (<div className="mx_BasicMessageComposer_AutoCompleteWrapper">
|
||||
<Autocomplete
|
||||
ref={this.autocompleteRef}
|
||||
query={query}
|
||||
onConfirm={this.onAutoCompleteConfirm}
|
||||
onSelectionChange={this.onAutoCompleteSelectionChange}
|
||||
selection={{ beginning: true, end: queryLen, start: queryLen }}
|
||||
room={this.props.room}
|
||||
/>
|
||||
</div>);
|
||||
autoComplete = (
|
||||
<div className="mx_BasicMessageComposer_AutoCompleteWrapper">
|
||||
<Autocomplete
|
||||
ref={this.autocompleteRef}
|
||||
query={query}
|
||||
onConfirm={this.onAutoCompleteConfirm}
|
||||
onSelectionChange={this.onAutoCompleteSelectionChange}
|
||||
selection={{ beginning: true, end: queryLen, start: queryLen }}
|
||||
room={this.props.room}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const wrapperClasses = classNames("mx_BasicMessageComposer", {
|
||||
"mx_BasicMessageComposer_input_error": this.state.showVisualBell,
|
||||
mx_BasicMessageComposer_input_error: this.state.showVisualBell,
|
||||
});
|
||||
const classes = classNames("mx_BasicMessageComposer_input", {
|
||||
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
|
||||
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
|
||||
mx_BasicMessageComposer_input_shouldShowPillAvatar: this.state.showPillAvatar,
|
||||
mx_BasicMessageComposer_input_disabled: this.props.disabled,
|
||||
});
|
||||
|
||||
const shortcuts = {
|
||||
|
@ -765,33 +787,39 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
activeDescendant = generateCompletionDomId(completionIndex);
|
||||
}
|
||||
|
||||
return (<div className={wrapperClasses}>
|
||||
{ autoComplete }
|
||||
<MessageComposerFormatBar ref={this.formatBarRef} onAction={this.onFormatAction} shortcuts={shortcuts} />
|
||||
<div
|
||||
className={classes}
|
||||
contentEditable={this.props.disabled ? null : true}
|
||||
tabIndex={0}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onCopy={this.onCopy}
|
||||
onCut={this.onCut}
|
||||
onPaste={this.onPaste}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.editorRef}
|
||||
aria-label={this.props.label}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={hasAutocomplete ? true : undefined}
|
||||
aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined}
|
||||
aria-activedescendant={activeDescendant}
|
||||
dir="auto"
|
||||
aria-disabled={this.props.disabled}
|
||||
data-testid="basicmessagecomposer"
|
||||
/>
|
||||
</div>);
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
{autoComplete}
|
||||
<MessageComposerFormatBar
|
||||
ref={this.formatBarRef}
|
||||
onAction={this.onFormatAction}
|
||||
shortcuts={shortcuts}
|
||||
/>
|
||||
<div
|
||||
className={classes}
|
||||
contentEditable={this.props.disabled ? null : true}
|
||||
tabIndex={0}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onCopy={this.onCopy}
|
||||
onCut={this.onCut}
|
||||
onPaste={this.onPaste}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.editorRef}
|
||||
aria-label={this.props.label}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={hasAutocomplete ? true : undefined}
|
||||
aria-owns={hasAutocomplete ? "mx_Autocomplete" : undefined}
|
||||
aria-activedescendant={activeDescendant}
|
||||
dir="auto"
|
||||
aria-disabled={this.props.disabled}
|
||||
data-testid="basicmessagecomposer"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
|
@ -803,8 +831,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
const { model } = this.props;
|
||||
const { partCreator } = model;
|
||||
const member = this.props.room.getMember(userId);
|
||||
const displayName = member ?
|
||||
member.rawDisplayName : userId;
|
||||
const displayName = member ? member.rawDisplayName : userId;
|
||||
const caret = this.getCaret();
|
||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
// Insert suffix only if the caret is at the start of the composer
|
||||
|
|
|
@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps, useContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, useContext } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { MenuItem } from "../../structures/ContextMenu";
|
||||
import { OverflowMenuContext } from './MessageComposerButtons';
|
||||
import { IconizedContextMenuOption } from '../context_menus/IconizedContextMenu';
|
||||
import { OverflowMenuContext } from "./MessageComposerButtons";
|
||||
import { IconizedContextMenuOption } from "../context_menus/IconizedContextMenu";
|
||||
|
||||
interface ICollapsibleButtonProps extends ComponentProps<typeof MenuItem> {
|
||||
title: string;
|
||||
|
@ -30,18 +30,12 @@ interface ICollapsibleButtonProps extends ComponentProps<typeof MenuItem> {
|
|||
export const CollapsibleButton = ({ title, children, className, iconClassName, ...props }: ICollapsibleButtonProps) => {
|
||||
const inOverflowMenu = !!useContext(OverflowMenuContext);
|
||||
if (inOverflowMenu) {
|
||||
return <IconizedContextMenuOption
|
||||
{...props}
|
||||
iconClassName={iconClassName}
|
||||
label={title}
|
||||
/>;
|
||||
return <IconizedContextMenuOption {...props} iconClassName={iconClassName} label={title} />;
|
||||
}
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
{...props}
|
||||
title={title}
|
||||
className={classNames(className, iconClassName)}
|
||||
>
|
||||
{ children }
|
||||
</AccessibleTooltipButton>;
|
||||
return (
|
||||
<AccessibleTooltipButton {...props} title={title} className={classNames(className, iconClassName)}>
|
||||
{children}
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
@ -65,13 +65,16 @@ const E2EIcon: React.FC<IProps> = ({
|
|||
}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const classes = classNames({
|
||||
mx_E2EIcon: true,
|
||||
mx_E2EIcon_bordered: bordered,
|
||||
mx_E2EIcon_warning: status === E2EState.Warning,
|
||||
mx_E2EIcon_normal: status === E2EState.Normal,
|
||||
mx_E2EIcon_verified: status === E2EState.Verified,
|
||||
}, className);
|
||||
const classes = classNames(
|
||||
{
|
||||
mx_E2EIcon: true,
|
||||
mx_E2EIcon_bordered: bordered,
|
||||
mx_E2EIcon_warning: status === E2EState.Warning,
|
||||
mx_E2EIcon_normal: status === E2EState.Normal,
|
||||
mx_E2EIcon_verified: status === E2EState.Verified,
|
||||
},
|
||||
className,
|
||||
);
|
||||
|
||||
let e2eTitle;
|
||||
if (isUser) {
|
||||
|
@ -102,14 +105,16 @@ const E2EIcon: React.FC<IProps> = ({
|
|||
className={classes}
|
||||
style={style}
|
||||
>
|
||||
{ tip }
|
||||
{tip}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
|
||||
{ tip }
|
||||
</div>;
|
||||
return (
|
||||
<div onMouseOver={onMouseOver} onMouseLeave={onMouseLeave} className={classes} style={style}>
|
||||
{tip}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default E2EIcon;
|
||||
|
|
|
@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, KeyboardEvent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { EventStatus, IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import React, { createRef, KeyboardEvent } from "react";
|
||||
import classNames from "classnames";
|
||||
import { EventStatus, IContent, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
import { getCaretOffsetAndText } from '../../../editor/dom';
|
||||
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
|
||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||
import { parseEvent } from '../../../editor/deserialize';
|
||||
import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import EditorModel from "../../../editor/model";
|
||||
import { getCaretOffsetAndText } from "../../../editor/dom";
|
||||
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from "../../../editor/serialize";
|
||||
import { findEditableEvent } from "../../../utils/EventUtils";
|
||||
import { parseEvent } from "../../../editor/deserialize";
|
||||
import { CommandPartCreator, Part, PartCreator } from "../../../editor/parts";
|
||||
import EditorStateTransfer from "../../../utils/EditorStateTransfer";
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import { CommandCategories } from '../../../SlashCommands';
|
||||
import { CommandCategories } from "../../../SlashCommands";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
|
||||
import SendHistoryManager from '../../../SendHistoryManager';
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
|
@ -60,17 +60,14 @@ function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
|||
|
||||
function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const body = mxEvent.getContent().body;
|
||||
const lines = body.split("\n").map(l => l.trim());
|
||||
const lines = body.split("\n").map((l) => l.trim());
|
||||
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
|
||||
return `${lines[0]}\n\n`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function createEditContent(
|
||||
model: EditorModel,
|
||||
editedEvent: MatrixEvent,
|
||||
): IContent {
|
||||
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
|
||||
const isEmote = containsEmote(model);
|
||||
if (isEmote) {
|
||||
model = stripEmoteCommand(model);
|
||||
|
@ -87,8 +84,8 @@ function createEditContent(
|
|||
const body = textSerialize(model);
|
||||
|
||||
const newContent: IContent = {
|
||||
"msgtype": isEmote ? MsgType.Emote : MsgType.Text,
|
||||
"body": body,
|
||||
msgtype: isEmote ? MsgType.Emote : MsgType.Text,
|
||||
body: body,
|
||||
};
|
||||
const contentBody: IContent = {
|
||||
msgtype: newContent.msgtype,
|
||||
|
@ -109,8 +106,8 @@ function createEditContent(
|
|||
const relation = {
|
||||
"m.new_content": newContent,
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": editedEvent.getId(),
|
||||
rel_type: "m.replace",
|
||||
event_id: editedEvent.getId(),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -256,7 +253,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
if (json) {
|
||||
try {
|
||||
const { parts: serializedParts } = JSON.parse(json);
|
||||
const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
const parts: Part[] = serializedParts.map((p) => partCreator.deserializePart(p));
|
||||
return parts;
|
||||
} catch (e) {
|
||||
logger.error("Error parsing editing state: ", e);
|
||||
|
@ -280,9 +277,12 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
private isContentModified(newContent: IContent): boolean {
|
||||
// if nothing has changed then bail
|
||||
const oldContent = this.props.editState.getEvent().getContent();
|
||||
if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
|
||||
if (
|
||||
oldContent["msgtype"] === newContent["msgtype"] &&
|
||||
oldContent["body"] === newContent["body"] &&
|
||||
oldContent["format"] === newContent["format"] &&
|
||||
oldContent["formatted_body"] === newContent["formatted_body"]) {
|
||||
oldContent["formatted_body"] === newContent["formatted_body"]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -301,7 +301,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
});
|
||||
|
||||
// Replace emoticon at the end of the message
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
if (SettingsStore.getValue("MessageComposerInput.autoReplaceEmoji")) {
|
||||
const caret = this.editorRef.current?.getCaret();
|
||||
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
|
||||
|
@ -311,7 +311,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
|
||||
let shouldSend = true;
|
||||
|
||||
if (newContent?.body === '') {
|
||||
if (newContent?.body === "") {
|
||||
this.cancelPreviousPendingEdit();
|
||||
createRedactEventDialog({
|
||||
mxEvent: editedEvent,
|
||||
|
@ -339,7 +339,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
} else {
|
||||
shouldSend = false;
|
||||
}
|
||||
} else if (!await shouldSendAnyway(commandText)) {
|
||||
} else if (!(await shouldSendAnyway(commandText))) {
|
||||
// if !sendAnyway bail to let the user edit the composer and try again
|
||||
return;
|
||||
}
|
||||
|
@ -361,10 +361,10 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
private cancelPreviousPendingEdit(): void {
|
||||
const originalEvent = this.props.editState.getEvent();
|
||||
const previousEdit = originalEvent.replacingEvent();
|
||||
if (previousEdit && (
|
||||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
if (
|
||||
previousEdit &&
|
||||
(previousEdit.status === EventStatus.QUEUED || previousEdit.status === EventStatus.NOT_SENT)
|
||||
) {
|
||||
this.props.mxClient.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
@ -400,13 +400,15 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore serialized parts from the state
|
||||
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
parts = editState.getSerializedParts().map((p) => partCreator.deserializePart(p));
|
||||
} else {
|
||||
// otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||
const restoredParts = this.restoreStoredEditorState(partCreator);
|
||||
parts = restoredParts || parseEvent(editState.getEvent(), partCreator, {
|
||||
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
||||
});
|
||||
parts =
|
||||
restoredParts ||
|
||||
parseEvent(editState.getEvent(), partCreator, {
|
||||
shouldEscape: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
|
||||
});
|
||||
isRestored = !!restoredParts;
|
||||
}
|
||||
this.model = new EditorModel(parts, partCreator);
|
||||
|
@ -445,25 +447,27 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
|
|||
};
|
||||
|
||||
render() {
|
||||
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this.onKeyDown}>
|
||||
<BasicMessageComposer
|
||||
ref={this.editorRef}
|
||||
model={this.model}
|
||||
room={this.getRoom()}
|
||||
threadId={this.props.editState?.getEvent()?.getThread()?.id}
|
||||
initialCaret={this.props.editState.getCaret()}
|
||||
label={_t("Edit message")}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<div className="mx_EditMessageComposer_buttons">
|
||||
<AccessibleButton kind="secondary" onClick={this.cancelEdit}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this.sendEdit} disabled={this.state.saveDisabled}>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
return (
|
||||
<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this.onKeyDown}>
|
||||
<BasicMessageComposer
|
||||
ref={this.editorRef}
|
||||
model={this.model}
|
||||
room={this.getRoom()}
|
||||
threadId={this.props.editState?.getEvent()?.getThread()?.id}
|
||||
initialCaret={this.props.editState.getCaret()}
|
||||
label={_t("Edit message")}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<div className="mx_EditMessageComposer_buttons">
|
||||
<AccessibleButton kind="secondary" onClick={this.cancelEdit}>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={this.sendEdit} disabled={this.state.saveDisabled}>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,41 +35,39 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP
|
|||
|
||||
let contextMenu: React.ReactElement | null = null;
|
||||
if (menuDisplayed && button.current) {
|
||||
const position = (
|
||||
menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect())
|
||||
);
|
||||
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
|
||||
|
||||
contextMenu = <ContextMenu
|
||||
{...position}
|
||||
onFinished={() => {
|
||||
closeMenu();
|
||||
overflowMenuCloser?.();
|
||||
}}
|
||||
managed={false}
|
||||
>
|
||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||
</ContextMenu>;
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
{...position}
|
||||
onFinished={() => {
|
||||
closeMenu();
|
||||
overflowMenuCloser?.();
|
||||
}}
|
||||
managed={false}
|
||||
>
|
||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
const computedClassName = classNames(
|
||||
"mx_EmojiButton",
|
||||
className,
|
||||
{
|
||||
"mx_EmojiButton_highlight": menuDisplayed,
|
||||
},
|
||||
);
|
||||
const computedClassName = classNames("mx_EmojiButton", className, {
|
||||
mx_EmojiButton_highlight: menuDisplayed,
|
||||
});
|
||||
|
||||
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
||||
// the header buttons and the right panel buttons
|
||||
return <>
|
||||
<CollapsibleButton
|
||||
className={computedClassName}
|
||||
iconClassName="mx_EmojiButton_icon"
|
||||
onClick={openMenu}
|
||||
title={_t("Emoji")}
|
||||
inputRef={button}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<CollapsibleButton
|
||||
className={computedClassName}
|
||||
iconClassName="mx_EmojiButton_icon"
|
||||
onClick={openMenu}
|
||||
title={_t("Emoji")}
|
||||
inputRef={button}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</>;
|
||||
{contextMenu}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,13 +16,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import E2EIcon, { E2EState } from './E2EIcon';
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import E2EIcon, { E2EState } from "./E2EIcon";
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import PresenceLabel from "./PresenceLabel";
|
||||
|
||||
export enum PowerStatus {
|
||||
|
@ -36,28 +36,28 @@ const PowerLabel: Record<PowerStatus, string> = {
|
|||
};
|
||||
|
||||
const PRESENCE_CLASS = {
|
||||
"offline": "mx_EntityTile_offline",
|
||||
"online": "mx_EntityTile_online",
|
||||
"unavailable": "mx_EntityTile_unavailable",
|
||||
offline: "mx_EntityTile_offline",
|
||||
online: "mx_EntityTile_online",
|
||||
unavailable: "mx_EntityTile_unavailable",
|
||||
};
|
||||
|
||||
function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean): string {
|
||||
if (showPresence === false) {
|
||||
return 'mx_EntityTile_online_beenactive';
|
||||
return "mx_EntityTile_online_beenactive";
|
||||
}
|
||||
|
||||
// offline is split into two categories depending on whether we have
|
||||
// a last_active_ago for them.
|
||||
if (presenceState === 'offline') {
|
||||
if (presenceState === "offline") {
|
||||
if (lastActiveAgo) {
|
||||
return PRESENCE_CLASS['offline'] + '_beenactive';
|
||||
return PRESENCE_CLASS["offline"] + "_beenactive";
|
||||
} else {
|
||||
return PRESENCE_CLASS['offline'] + '_neveractive';
|
||||
return PRESENCE_CLASS["offline"] + "_neveractive";
|
||||
}
|
||||
} else if (presenceState) {
|
||||
return PRESENCE_CLASS[presenceState];
|
||||
} else {
|
||||
return PRESENCE_CLASS['offline'] + '_neveractive';
|
||||
return PRESENCE_CLASS["offline"] + "_neveractive";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,13 +105,15 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
render() {
|
||||
const mainClassNames = {
|
||||
"mx_EntityTile": true,
|
||||
"mx_EntityTile_noHover": this.props.suppressOnHover,
|
||||
mx_EntityTile: true,
|
||||
mx_EntityTile_noHover: this.props.suppressOnHover,
|
||||
};
|
||||
if (this.props.className) mainClassNames[this.props.className] = true;
|
||||
|
||||
const presenceClass = presenceClassForMember(
|
||||
this.props.presenceState, this.props.presenceLastActiveAgo, this.props.showPresence,
|
||||
this.props.presenceState,
|
||||
this.props.presenceLastActiveAgo,
|
||||
this.props.showPresence,
|
||||
);
|
||||
mainClassNames[presenceClass] = true;
|
||||
|
||||
|
@ -119,39 +121,38 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
|||
const name = this.props.nameJSX || this.props.name;
|
||||
|
||||
if (!this.props.suppressOnHover) {
|
||||
const activeAgo = this.props.presenceLastActiveAgo ?
|
||||
(Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1;
|
||||
const activeAgo = this.props.presenceLastActiveAgo
|
||||
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
|
||||
: -1;
|
||||
|
||||
let presenceLabel = null;
|
||||
if (this.props.showPresence) {
|
||||
presenceLabel = <PresenceLabel activeAgo={activeAgo}
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState} />;
|
||||
presenceLabel = (
|
||||
<PresenceLabel
|
||||
activeAgo={activeAgo}
|
||||
currentlyActive={this.props.presenceCurrentlyActive}
|
||||
presenceState={this.props.presenceState}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (this.props.subtextLabel) {
|
||||
presenceLabel = <span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>;
|
||||
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
|
||||
}
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<div className="mx_EntityTile_name">
|
||||
{ name }
|
||||
</div>
|
||||
{ presenceLabel }
|
||||
<div className="mx_EntityTile_name">{name}</div>
|
||||
{presenceLabel}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.subtextLabel) {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_details">
|
||||
<div className="mx_EntityTile_name">
|
||||
{ name }
|
||||
</div>
|
||||
<span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>
|
||||
<div className="mx_EntityTile_name">{name}</div>
|
||||
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
nameEl = (
|
||||
<div className="mx_EntityTile_name">{ name }</div>
|
||||
);
|
||||
nameEl = <div className="mx_EntityTile_name">{name}</div>;
|
||||
}
|
||||
|
||||
let inviteButton;
|
||||
|
@ -167,7 +168,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
|||
const powerStatus = this.props.powerStatus;
|
||||
if (powerStatus) {
|
||||
const powerText = _t(PowerLabel[powerStatus]);
|
||||
powerLabel = <div className="mx_EntityTile_power">{ powerText }</div>;
|
||||
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
|
||||
}
|
||||
|
||||
let e2eIcon;
|
||||
|
@ -176,8 +177,9 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
|||
e2eIcon = <E2EIcon status={e2eStatus} isUser={true} bordered={true} />;
|
||||
}
|
||||
|
||||
const av = this.props.avatarJsx ||
|
||||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
|
||||
const av = this.props.avatarJsx || (
|
||||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />
|
||||
);
|
||||
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
|
@ -188,12 +190,12 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
|
|||
onClick={this.props.onClick}
|
||||
>
|
||||
<div className="mx_EntityTile_avatar">
|
||||
{ av }
|
||||
{ e2eIcon }
|
||||
{av}
|
||||
{e2eIcon}
|
||||
</div>
|
||||
{ nameEl }
|
||||
{ powerLabel }
|
||||
{ inviteButton }
|
||||
{nameEl}
|
||||
{powerLabel}
|
||||
{inviteButton}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,10 +17,7 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import {
|
||||
RovingAccessibleButton,
|
||||
RovingAccessibleTooltipButton,
|
||||
} from "../../../accessibility/RovingTabIndex";
|
||||
import { RovingAccessibleButton, RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
import { NotificationState } from "../../../stores/notifications/NotificationState";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
|
@ -58,35 +55,30 @@ export default class ExtraTile extends React.Component<IProps, IState> {
|
|||
public render(): React.ReactElement {
|
||||
// XXX: We copy classes because it's easier
|
||||
const classes = classNames({
|
||||
'mx_ExtraTile': true,
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.props.isSelected,
|
||||
'mx_RoomTile_minimized': this.props.isMinimized,
|
||||
mx_ExtraTile: true,
|
||||
mx_RoomTile: true,
|
||||
mx_RoomTile_selected: this.props.isSelected,
|
||||
mx_RoomTile_minimized: this.props.isMinimized,
|
||||
});
|
||||
|
||||
let badge;
|
||||
if (this.props.notificationState) {
|
||||
badge = (
|
||||
<NotificationBadge
|
||||
notification={this.props.notificationState}
|
||||
forceCount={false}
|
||||
/>
|
||||
);
|
||||
badge = <NotificationBadge notification={this.props.notificationState} forceCount={false} />;
|
||||
}
|
||||
|
||||
let name = this.props.displayName;
|
||||
if (typeof name !== 'string') name = '';
|
||||
if (typeof name !== "string") name = "";
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
const nameClasses = classNames({
|
||||
"mx_RoomTile_title": true,
|
||||
"mx_RoomTile_titleHasUnreadEvents": this.props.notificationState?.isUnread,
|
||||
mx_RoomTile_title: true,
|
||||
mx_RoomTile_titleHasUnreadEvents: this.props.notificationState?.isUnread,
|
||||
});
|
||||
|
||||
let nameContainer = (
|
||||
<div className="mx_RoomTile_titleContainer">
|
||||
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||
{ name }
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -107,15 +99,11 @@ export default class ExtraTile extends React.Component<IProps, IState> {
|
|||
role="treeitem"
|
||||
title={this.props.isMinimized ? name : undefined}
|
||||
>
|
||||
<div className="mx_RoomTile_avatarContainer">
|
||||
{ this.props.avatar }
|
||||
</div>
|
||||
<div className="mx_RoomTile_avatarContainer">{this.props.avatar}</div>
|
||||
<div className="mx_RoomTile_details">
|
||||
<div className="mx_RoomTile_primaryDetails">
|
||||
{ nameContainer }
|
||||
<div className="mx_RoomTile_badgeContainer">
|
||||
{ badge }
|
||||
</div>
|
||||
{nameContainer}
|
||||
<div className="mx_RoomTile_badgeContainer">{badge}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
|
|
@ -37,11 +37,9 @@ const HistoryTile = () => {
|
|||
subtitle = _t("Encrypted messages before this point are unavailable.");
|
||||
}
|
||||
|
||||
return <EventTileBubble
|
||||
className="mx_HistoryTile"
|
||||
title={_t("You can't see earlier messages")}
|
||||
subtitle={subtitle}
|
||||
/>;
|
||||
return (
|
||||
<EventTileBubble className="mx_HistoryTile" title={_t("You can't see earlier messages")} subtitle={subtitle} />
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryTile;
|
||||
|
|
|
@ -15,10 +15,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
numUnreadMessages?: number;
|
||||
|
@ -28,21 +28,23 @@ interface IProps {
|
|||
|
||||
const JumpToBottomButton: React.FC<IProps> = (props) => {
|
||||
const className = classNames({
|
||||
'mx_JumpToBottomButton': true,
|
||||
'mx_JumpToBottomButton_highlight': props.highlight,
|
||||
mx_JumpToBottomButton: true,
|
||||
mx_JumpToBottomButton_highlight: props.highlight,
|
||||
});
|
||||
let badge;
|
||||
if (props.numUnreadMessages) {
|
||||
badge = (<div className="mx_JumpToBottomButton_badge">{ props.numUnreadMessages }</div>);
|
||||
badge = <div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>;
|
||||
}
|
||||
return (<div className={className}>
|
||||
<AccessibleButton
|
||||
className="mx_JumpToBottomButton_scrollDown"
|
||||
title={_t("Scroll to most recent messages")}
|
||||
onClick={props.onScrollToBottomClick}
|
||||
/>
|
||||
{ badge }
|
||||
</div>);
|
||||
return (
|
||||
<div className={className}>
|
||||
<AccessibleButton
|
||||
className="mx_JumpToBottomButton_scrollDown"
|
||||
title={_t("Scroll to most recent messages")}
|
||||
onClick={props.onScrollToBottomClick}
|
||||
/>
|
||||
{badge}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JumpToBottomButton;
|
||||
|
|
|
@ -40,9 +40,13 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
|
|||
const [expanded, toggleExpanded] = useStateToggle();
|
||||
|
||||
const ts = mxEvent.getTs();
|
||||
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
|
||||
return fetchPreviews(cli, links, ts);
|
||||
}, [links, ts], []);
|
||||
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(
|
||||
async () => {
|
||||
return fetchPreviews(cli, links, ts);
|
||||
},
|
||||
[links, ts],
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onHeightChanged();
|
||||
|
@ -52,50 +56,55 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
|
|||
|
||||
let toggleButton: JSX.Element;
|
||||
if (previews.length > INITIAL_NUM_PREVIEWS) {
|
||||
toggleButton = <AccessibleButton onClick={toggleExpanded}>
|
||||
{ expanded
|
||||
? _t("Collapse")
|
||||
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length }) }
|
||||
</AccessibleButton>;
|
||||
toggleButton = (
|
||||
<AccessibleButton onClick={toggleExpanded}>
|
||||
{expanded
|
||||
? _t("Collapse")
|
||||
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length })}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_LinkPreviewGroup">
|
||||
{ showPreviews.map(([link, preview], i) => (
|
||||
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
|
||||
{ i === 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_LinkPreviewGroup_hide"
|
||||
onClick={onCancelClick}
|
||||
aria-label={_t("Close preview")}
|
||||
>
|
||||
<img
|
||||
className="mx_filterFlipColor"
|
||||
alt=""
|
||||
role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg").default}
|
||||
width="18"
|
||||
height="18"
|
||||
/>
|
||||
</AccessibleButton>
|
||||
): undefined }
|
||||
</LinkPreviewWidget>
|
||||
)) }
|
||||
{ toggleButton }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_LinkPreviewGroup">
|
||||
{showPreviews.map(([link, preview], i) => (
|
||||
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
|
||||
{i === 0 ? (
|
||||
<AccessibleButton
|
||||
className="mx_LinkPreviewGroup_hide"
|
||||
onClick={onCancelClick}
|
||||
aria-label={_t("Close preview")}
|
||||
>
|
||||
<img
|
||||
className="mx_filterFlipColor"
|
||||
alt=""
|
||||
role="presentation"
|
||||
src={require("../../../../res/img/cancel.svg").default}
|
||||
width="18"
|
||||
height="18"
|
||||
/>
|
||||
</AccessibleButton>
|
||||
) : undefined}
|
||||
</LinkPreviewWidget>
|
||||
))}
|
||||
{toggleButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const fetchPreviews = (cli: MatrixClient, links: string[], ts: number):
|
||||
Promise<[string, IPreviewUrlResponse][]> => {
|
||||
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
|
||||
try {
|
||||
const preview = await cli.getUrlPreview(link, ts);
|
||||
if (preview && Object.keys(preview).length > 0) {
|
||||
return [link, preview];
|
||||
const fetchPreviews = (cli: MatrixClient, links: string[], ts: number): Promise<[string, IPreviewUrlResponse][]> => {
|
||||
return Promise.all<[string, IPreviewUrlResponse] | void>(
|
||||
links.map(async (link) => {
|
||||
try {
|
||||
const preview = await cli.getUrlPreview(link, ts);
|
||||
if (preview && Object.keys(preview).length > 0) {
|
||||
return [link, preview];
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to get URL preview: " + error);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to get URL preview: " + error);
|
||||
}
|
||||
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
|
||||
}),
|
||||
).then((a) => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
|
||||
};
|
||||
|
||||
export default LinkPreviewGroup;
|
||||
|
|
|
@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ComponentProps, createRef } from 'react';
|
||||
import { decode } from 'html-entities';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
|
||||
import React, { ComponentProps, createRef } from "react";
|
||||
import { decode } from "html-entities";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { linkifyElement } from '../../../HtmlUtils';
|
||||
import { linkifyElement } from "../../../HtmlUtils";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import Modal from "../../../Modal";
|
||||
import * as ImageUtils from "../../../ImageUtils";
|
||||
import { mediaFromMxc } from "../../../customisations/Media";
|
||||
import ImageView from '../elements/ImageView';
|
||||
import LinkWithTooltip from '../elements/LinkWithTooltip';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import ImageView from "../elements/ImageView";
|
||||
import LinkWithTooltip from "../elements/LinkWithTooltip";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
|
||||
interface IProps {
|
||||
link: string;
|
||||
|
@ -50,7 +50,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
}
|
||||
}
|
||||
|
||||
private onImageClick = ev => {
|
||||
private onImageClick = (ev) => {
|
||||
const p = this.props.preview;
|
||||
if (ev.button != 0 || ev.metaKey) return;
|
||||
ev.preventDefault();
|
||||
|
@ -98,28 +98,32 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
const imageMaxHeight = 100;
|
||||
if (image && image.startsWith("mxc://")) {
|
||||
// We deliberately don't want a square here, so use the source HTTP thumbnail function
|
||||
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, 'scale');
|
||||
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, "scale");
|
||||
}
|
||||
|
||||
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,
|
||||
p["og:image:width"],
|
||||
p["og:image:height"],
|
||||
imageMaxWidth,
|
||||
imageMaxHeight,
|
||||
);
|
||||
}
|
||||
|
||||
let img;
|
||||
if (image) {
|
||||
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||
<img
|
||||
ref={this.image}
|
||||
style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }}
|
||||
src={image}
|
||||
onClick={this.onImageClick}
|
||||
alt=""
|
||||
/>
|
||||
</div>;
|
||||
img = (
|
||||
<div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
|
||||
<img
|
||||
ref={this.image}
|
||||
style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }}
|
||||
src={image}
|
||||
onClick={this.onImageClick}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// The description includes &-encoded HTML entities, we decode those as React treats the thing as an
|
||||
|
@ -127,30 +131,36 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
|
|||
const description = decode(p["og:description"] || "");
|
||||
|
||||
const title = p["og:title"]?.trim() ?? "";
|
||||
const anchor = <a href={this.props.link} target="_blank" rel="noreferrer noopener">{ title }</a>;
|
||||
const anchor = (
|
||||
<a href={this.props.link} target="_blank" rel="noreferrer noopener">
|
||||
{title}
|
||||
</a>
|
||||
);
|
||||
const needsTooltip = PlatformPeg.get()?.needsUrlTooltips() && this.props.link !== title;
|
||||
|
||||
return (
|
||||
<div className="mx_LinkPreviewWidget">
|
||||
<div className="mx_LinkPreviewWidget_wrapImageCaption">
|
||||
{ img }
|
||||
{img}
|
||||
<div className="mx_LinkPreviewWidget_caption">
|
||||
<div className="mx_LinkPreviewWidget_title">
|
||||
{ needsTooltip ? <LinkWithTooltip
|
||||
tooltip={new URL(this.props.link, window.location.href).toString()}
|
||||
>
|
||||
{ anchor }
|
||||
</LinkWithTooltip> : anchor }
|
||||
{ p["og:site_name"] && <span className="mx_LinkPreviewWidget_siteName">
|
||||
{ (" - " + p["og:site_name"]) }
|
||||
</span> }
|
||||
{needsTooltip ? (
|
||||
<LinkWithTooltip tooltip={new URL(this.props.link, window.location.href).toString()}>
|
||||
{anchor}
|
||||
</LinkWithTooltip>
|
||||
) : (
|
||||
anchor
|
||||
)}
|
||||
{p["og:site_name"] && (
|
||||
<span className="mx_LinkPreviewWidget_siteName">{" - " + p["og:site_name"]}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mx_LinkPreviewWidget_description" ref={this.description}>
|
||||
{ description }
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ this.props.children }
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -40,21 +40,23 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
|
|||
<span className="mx_LiveContentSummary">
|
||||
<span
|
||||
className={classNames("mx_LiveContentSummary_text", {
|
||||
"mx_LiveContentSummary_text_video": type === LiveContentType.Video,
|
||||
"mx_LiveContentSummary_text_active": active,
|
||||
mx_LiveContentSummary_text_video: type === LiveContentType.Video,
|
||||
mx_LiveContentSummary_text_active: active,
|
||||
})}
|
||||
>
|
||||
{ text }
|
||||
{text}
|
||||
</span>
|
||||
{ participantCount > 0 && <>
|
||||
{ " • " }
|
||||
<span
|
||||
className="mx_LiveContentSummary_participants"
|
||||
aria-label={_t("%(count)s participants", { count: participantCount })}
|
||||
>
|
||||
{ participantCount }
|
||||
</span>
|
||||
</> }
|
||||
{participantCount > 0 && (
|
||||
<>
|
||||
{" • "}
|
||||
<span
|
||||
className="mx_LiveContentSummary_participants"
|
||||
aria-label={_t("%(count)s participants", { count: participantCount })}
|
||||
>
|
||||
{participantCount}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
@ -62,10 +64,11 @@ interface LiveContentSummaryWithCallProps {
|
|||
call: Call;
|
||||
}
|
||||
|
||||
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) =>
|
||||
export const LiveContentSummaryWithCall: FC<LiveContentSummaryWithCallProps> = ({ call }) => (
|
||||
<LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={_t("Video")}
|
||||
active={false}
|
||||
participantCount={useParticipantCount(call)}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -17,35 +17,35 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomMember, RoomMemberEvent } from 'matrix-js-sdk/src/models/room-member';
|
||||
import { RoomState, RoomStateEvent } from 'matrix-js-sdk/src/models/room-state';
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { User, UserEvent } from "matrix-js-sdk/src/models/user";
|
||||
import { throttle } from 'lodash';
|
||||
import { throttle } from "lodash";
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import TruncatedList from '../elements/TruncatedList';
|
||||
import TruncatedList from "../elements/TruncatedList";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import EntityTile from "./EntityTile";
|
||||
import MemberTile from "./MemberTile";
|
||||
import BaseAvatar from '../avatars/BaseAvatar';
|
||||
import BaseAvatar from "../avatars/BaseAvatar";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
import PosthogTrackers from "../../../PosthogTrackers";
|
||||
import { SDKContext } from '../../../contexts/SDKContext';
|
||||
import { SDKContext } from "../../../contexts/SDKContext";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -123,10 +123,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
|
||||
return (
|
||||
room?.canInvite(cli.getUserId()) ||
|
||||
(room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public)
|
||||
);
|
||||
return room?.canInvite(cli.getUserId()) || (room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public);
|
||||
}
|
||||
|
||||
private getMembersState(invitedMembers: Array<RoomMember>, joinedMembers: Array<RoomMember>): IState {
|
||||
|
@ -190,9 +187,13 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite });
|
||||
};
|
||||
|
||||
private updateList = throttle(() => {
|
||||
this.updateListNow(false);
|
||||
}, 500, { leading: true, trailing: true });
|
||||
private updateList = throttle(
|
||||
() => {
|
||||
this.updateListNow(false);
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
private async updateListNow(showLoadingSpinner: boolean): Promise<void> {
|
||||
if (!this.mounted) {
|
||||
|
@ -202,7 +203,8 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
this.setState({ loading: true });
|
||||
}
|
||||
const { joined, invited } = await this.context.memberListStore.loadMemberList(
|
||||
this.props.roomId, this.props.searchQuery,
|
||||
this.props.roomId,
|
||||
this.props.searchQuery,
|
||||
);
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
|
@ -229,7 +231,12 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
<EntityTile
|
||||
className="mx_EntityTile_ellipsis"
|
||||
avatarJsx={
|
||||
<BaseAvatar url={require("../../../../res/img/ellipsis.svg").default} name="..." width={36} height={36} />
|
||||
<BaseAvatar
|
||||
url={require("../../../../res/img/ellipsis.svg").default}
|
||||
name="..."
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
}
|
||||
name={text}
|
||||
presenceState="online"
|
||||
|
@ -289,7 +296,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
|
||||
private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => {
|
||||
dis.dispatch({
|
||||
action: 'view_3pid_invite',
|
||||
action: "view_3pid_invite",
|
||||
event: inviteEvent,
|
||||
});
|
||||
};
|
||||
|
@ -302,7 +309,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
|
||||
if (room) {
|
||||
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function(e) {
|
||||
return room.currentState.getStateEvents("m.room.third_party_invite").filter(function (e) {
|
||||
if (!isValid3pidInvite(e)) return false;
|
||||
|
||||
// discard all invites which have a m.room.member event since we've
|
||||
|
@ -321,12 +328,14 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
|
||||
} else {
|
||||
// Is a 3pid invite
|
||||
return <EntityTile
|
||||
key={m.getStateKey()}
|
||||
name={m.getContent().display_name}
|
||||
suppressOnHover={true}
|
||||
onClick={() => this.onPending3pidInviteClick(m)}
|
||||
/>;
|
||||
return (
|
||||
<EntityTile
|
||||
key={m.getStateKey()}
|
||||
name={m.getContent().display_name}
|
||||
suppressOnHover={true}
|
||||
onClick={() => this.onPending3pidInviteClick(m)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -352,19 +361,18 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<Spinner />
|
||||
</BaseCard>;
|
||||
return (
|
||||
<BaseCard className="mx_MemberList" onClose={this.props.onClose}>
|
||||
<Spinner />
|
||||
</BaseCard>
|
||||
);
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(this.props.roomId);
|
||||
let inviteButton;
|
||||
|
||||
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
if (room?.getMyMembership() === "join" && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
let inviteButtonText = _t("Invite to this room");
|
||||
if (room.isSpaceRoom()) {
|
||||
inviteButtonText = _t("Invite to this space");
|
||||
|
@ -376,7 +384,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
onClick={this.onInviteButtonClick}
|
||||
disabled={!this.state.canInvite}
|
||||
>
|
||||
<span>{ inviteButtonText }</span>
|
||||
<span>{inviteButtonText}</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -384,7 +392,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
let invitedHeader;
|
||||
let invitedSection;
|
||||
if (this.getChildCountInvited() > 0) {
|
||||
invitedHeader = <h2>{ _t("Invited") }</h2>;
|
||||
invitedHeader = <h2>{_t("Invited")}</h2>;
|
||||
invitedSection = (
|
||||
<TruncatedList
|
||||
className="mx_MemberList_section mx_MemberList_invited"
|
||||
|
@ -399,7 +407,7 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
const footer = (
|
||||
<SearchBox
|
||||
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
|
||||
placeholder={_t('Filter room members')}
|
||||
placeholder={_t("Filter room members")}
|
||||
onSearch={this.onSearchQueryChanged}
|
||||
initialValue={this.props.searchQuery}
|
||||
/>
|
||||
|
@ -407,45 +415,52 @@ export default class MemberList extends React.Component<IProps, IState> {
|
|||
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
</div>;
|
||||
scopeHeader = (
|
||||
<div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
header={<React.Fragment>
|
||||
{ scopeHeader }
|
||||
{ inviteButton }
|
||||
</React.Fragment>}
|
||||
footer={footer}
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<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>;
|
||||
return (
|
||||
<BaseCard
|
||||
className="mx_MemberList"
|
||||
header={
|
||||
<React.Fragment>
|
||||
{scopeHeader}
|
||||
{inviteButton}
|
||||
</React.Fragment>
|
||||
}
|
||||
footer={footer}
|
||||
onClose={this.props.onClose}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
private onInviteButtonClick = (ev: ButtonEvent): void => {
|
||||
PosthogTrackers.trackInteraction("WebRightPanelMemberListInviteButton", ev);
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({ action: 'require_registration' });
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
|
||||
// open the room inviter
|
||||
dis.dispatch({
|
||||
action: 'view_invite',
|
||||
action: "view_invite",
|
||||
roomId: this.props.roomId,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -15,23 +15,23 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { DeviceInfo } from "matrix-js-sdk/src/crypto/deviceinfo";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
||||
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EntityTile, { PowerStatus } from "./EntityTile";
|
||||
import MemberAvatar from "./../avatars/MemberAvatar";
|
||||
import DisambiguatedProfile from "../messages/DisambiguatedProfile";
|
||||
import UserIdentifierCustomisations from '../../../customisations/UserIdentifier';
|
||||
import UserIdentifierCustomisations from "../../../customisations/UserIdentifier";
|
||||
|
||||
interface IProps {
|
||||
member: RoomMember;
|
||||
|
@ -127,7 +127,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const devices = cli.getStoredDevicesForUser(userId);
|
||||
const anyDeviceUnverified = devices.some(device => {
|
||||
const anyDeviceUnverified = devices.some((device) => {
|
||||
const { deviceId } = device;
|
||||
// For your own devices, we use the stricter check of cross-signing
|
||||
// verification to encourage everyone to trust their own devices via
|
||||
|
@ -152,14 +152,11 @@ export default class MemberTile extends React.Component<IProps, IState> {
|
|||
if (
|
||||
nextProps.member.user &&
|
||||
(this.userLastModifiedTime === undefined ||
|
||||
this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
|
||||
this.userLastModifiedTime < nextProps.member.user.getLastModifiedTime())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
nextState.isRoomEncrypted !== this.state.isRoomEncrypted ||
|
||||
nextState.e2eStatus !== this.state.e2eStatus
|
||||
) {
|
||||
if (nextState.isRoomEncrypted !== this.state.isRoomEncrypted || nextState.e2eStatus !== this.state.e2eStatus) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -179,9 +176,9 @@ export default class MemberTile extends React.Component<IProps, IState> {
|
|||
|
||||
private getPowerLabel(): string {
|
||||
return _t("%(userName)s (power %(powerLevelNumber)s)", {
|
||||
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(
|
||||
this.props.member.userId, { roomId: this.props.member.roomId },
|
||||
),
|
||||
userName: UserIdentifierCustomisations.getDisplayUserIdentifier(this.props.member.userId, {
|
||||
roomId: this.props.member.roomId,
|
||||
}),
|
||||
powerLevelNumber: this.props.member.powerLevel,
|
||||
}).trim();
|
||||
}
|
||||
|
@ -191,9 +188,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
|
|||
const name = this.getDisplayName();
|
||||
const presenceState = member.user ? member.user.presence : null;
|
||||
|
||||
const av = (
|
||||
<MemberAvatar member={member} width={36} height={36} aria-hidden="true" />
|
||||
);
|
||||
const av = <MemberAvatar member={member} width={36} height={36} aria-hidden="true" />;
|
||||
|
||||
if (member.user) {
|
||||
this.userLastModifiedTime = member.user.getLastModifiedTime();
|
||||
|
@ -221,12 +216,7 @@ export default class MemberTile extends React.Component<IProps, IState> {
|
|||
e2eStatus = this.state.e2eStatus;
|
||||
}
|
||||
|
||||
const nameJSX = (
|
||||
<DisambiguatedProfile
|
||||
member={member}
|
||||
fallbackName={name || ""}
|
||||
/>
|
||||
);
|
||||
const nameJSX = <DisambiguatedProfile member={member} fallbackName={name || ""} />;
|
||||
|
||||
return (
|
||||
<EntityTile
|
||||
|
|
|
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { createRef, ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
import { IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import Stickerpicker from './Stickerpicker';
|
||||
import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import Stickerpicker from "./Stickerpicker";
|
||||
import { makeRoomPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import E2EIcon from "./E2EIcon";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { aboveLeftOf } from "../../structures/ContextMenu";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
@ -40,26 +40,26 @@ import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
|||
import { RecordingState } from "../../../audio/VoiceRecording";
|
||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
|
||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import EditorModel from "../../../editor/model";
|
||||
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { SettingUpdatedPayload } from "../../../dispatcher/payloads/SettingUpdatedPayload";
|
||||
import MessageComposerButtons from './MessageComposerButtons';
|
||||
import { ButtonEvent } from '../elements/AccessibleButton';
|
||||
import MessageComposerButtons from "./MessageComposerButtons";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||
import { Features } from '../../../settings/Settings';
|
||||
import { VoiceMessageRecording } from '../../../audio/VoiceMessageRecording';
|
||||
import { VoiceBroadcastRecordingsStore } from '../../../voice-broadcast';
|
||||
import { SendWysiwygComposer, sendMessage } from './wysiwyg_composer/';
|
||||
import { MatrixClientProps, withMatrixClientHOC } from '../../../contexts/MatrixClientContext';
|
||||
import { htmlToPlainText } from '../../../utils/room/htmlToPlaintext';
|
||||
import { setUpVoiceBroadcastPreRecording } from '../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording';
|
||||
import { SdkContextClass } from '../../../contexts/SDKContext';
|
||||
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||
import { Features } from "../../../settings/Settings";
|
||||
import { VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
||||
import { VoiceBroadcastRecordingsStore } from "../../../voice-broadcast";
|
||||
import { SendWysiwygComposer, sendMessage } from "./wysiwyg_composer/";
|
||||
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
||||
import { htmlToPlainText } from "../../../utils/room/htmlToPlaintext";
|
||||
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
|
||||
let instanceCount = 0;
|
||||
|
||||
|
@ -73,7 +73,7 @@ function SendButton(props: ISendButtonProps) {
|
|||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_sendMessage"
|
||||
onClick={props.onClick}
|
||||
title={props.title ?? _t('Send message')}
|
||||
title={props.title ?? _t("Send message")}
|
||||
data-testid="sendmessagebtn"
|
||||
/>
|
||||
);
|
||||
|
@ -129,7 +129,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
this.state = {
|
||||
isComposerEmpty: true,
|
||||
composerContent: '',
|
||||
composerContent: "",
|
||||
haveRecording: false,
|
||||
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
|
||||
isMenuOpen: false,
|
||||
|
@ -139,7 +139,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
|
||||
isWysiwygLabEnabled: SettingsStore.getValue<boolean>("feature_wysiwyg_composer"),
|
||||
isRichTextEnabled: true,
|
||||
initialComposerContent: '',
|
||||
initialComposerContent: "",
|
||||
};
|
||||
|
||||
this.instanceId = instanceCount++;
|
||||
|
@ -268,15 +268,15 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
private onTombstoneClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const replacementRoomId = this.context.tombstone.getContent()['replacement_room'];
|
||||
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"];
|
||||
const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId);
|
||||
let createEventId = null;
|
||||
if (replacementRoom) {
|
||||
const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, '');
|
||||
const createEvent = replacementRoom.currentState.getStateEvents(EventType.RoomCreate, "");
|
||||
if (createEvent && createEvent.getId()) createEventId = createEvent.getId();
|
||||
}
|
||||
|
||||
const viaServers = [this.context.tombstone.getSender().split(':').slice(1).join(':')];
|
||||
const viaServers = [this.context.tombstone.getSender().split(":").slice(1).join(":")];
|
||||
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -296,19 +296,19 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
if (this.props.replyToEvent) {
|
||||
const replyingToThread = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name;
|
||||
if (replyingToThread && this.props.e2eStatus) {
|
||||
return _t('Reply to encrypted thread…');
|
||||
return _t("Reply to encrypted thread…");
|
||||
} else if (replyingToThread) {
|
||||
return _t('Reply to thread…');
|
||||
return _t("Reply to thread…");
|
||||
} else if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted reply…');
|
||||
return _t("Send an encrypted reply…");
|
||||
} else {
|
||||
return _t('Send a reply…');
|
||||
return _t("Send a reply…");
|
||||
}
|
||||
} else {
|
||||
if (this.props.e2eStatus) {
|
||||
return _t('Send an encrypted message…');
|
||||
return _t("Send an encrypted message…");
|
||||
} else {
|
||||
return _t('Send a message…');
|
||||
return _t("Send a message…");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -334,11 +334,15 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
if (this.state.isWysiwygLabEnabled) {
|
||||
const { permalinkCreator, relation, replyToEvent } = this.props;
|
||||
sendMessage(this.state.composerContent,
|
||||
this.state.isRichTextEnabled,
|
||||
{ mxClient: this.props.mxClient, roomContext: this.context, permalinkCreator, relation, replyToEvent });
|
||||
sendMessage(this.state.composerContent, this.state.isRichTextEnabled, {
|
||||
mxClient: this.props.mxClient,
|
||||
roomContext: this.context,
|
||||
permalinkCreator,
|
||||
relation,
|
||||
replyToEvent,
|
||||
});
|
||||
dis.dispatch({ action: Action.ClearAndFocusSendMessageComposer });
|
||||
this.setState({ composerContent: '', initialComposerContent: '' });
|
||||
this.setState({ composerContent: "", initialComposerContent: "" });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -356,12 +360,12 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onRichTextToggle = () => {
|
||||
this.setState(state => ({
|
||||
this.setState((state) => ({
|
||||
isRichTextEnabled: !state.isRichTextEnabled,
|
||||
initialComposerContent: !state.isRichTextEnabled ?
|
||||
state.composerContent :
|
||||
// TODO when available use rust model plain text
|
||||
htmlToPlainText(state.composerContent),
|
||||
initialComposerContent: !state.isRichTextEnabled
|
||||
? state.composerContent
|
||||
: // TODO when available use rust model plain text
|
||||
htmlToPlainText(state.composerContent),
|
||||
}));
|
||||
};
|
||||
|
||||
|
@ -432,15 +436,17 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
contentRect.x,
|
||||
contentRect.y + heightToRemove,
|
||||
contentRect.width,
|
||||
contentRect.height - heightToRemove);
|
||||
contentRect.height - heightToRemove,
|
||||
);
|
||||
return aboveLeftOf(fixedRect);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
|
||||
const e2eIcon = hasE2EIcon &&
|
||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />;
|
||||
const e2eIcon = hasE2EIcon && (
|
||||
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" />
|
||||
);
|
||||
|
||||
const controls: ReactNode[] = [];
|
||||
const menuPosition = this.getMenuPosition();
|
||||
|
@ -449,8 +455,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
let composer: ReactNode;
|
||||
if (canSendMessages) {
|
||||
if (this.state.isWysiwygLabEnabled && menuPosition) {
|
||||
composer =
|
||||
<SendWysiwygComposer key="controls_input"
|
||||
composer = (
|
||||
<SendWysiwygComposer
|
||||
key="controls_input"
|
||||
disabled={this.state.haveRecording}
|
||||
onChange={this.onWysiwygChange}
|
||||
onSend={this.sendMessage}
|
||||
|
@ -459,9 +466,10 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
e2eStatus={this.props.e2eStatus}
|
||||
menuPosition={menuPosition}
|
||||
placeholder={this.renderPlaceholderText()}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
composer =
|
||||
composer = (
|
||||
<SendMessageComposer
|
||||
ref={this.messageComposerInput}
|
||||
key="controls_input"
|
||||
|
@ -473,43 +481,54 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
onChange={this.onChange}
|
||||
disabled={this.state.haveRecording}
|
||||
toggleStickerPickerOpen={this.toggleStickerPickerOpen}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent} />);
|
||||
controls.push(
|
||||
<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
ref={this.voiceRecordingButton}
|
||||
room={this.props.room}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
relation={this.props.relation}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
/>,
|
||||
);
|
||||
} else if (this.context.tombstone) {
|
||||
const replacementRoomId = this.context.tombstone.getContent()['replacement_room'];
|
||||
const replacementRoomId = this.context.tombstone.getContent()["replacement_room"];
|
||||
|
||||
const continuesLink = replacementRoomId ? (
|
||||
<a href={makeRoomPermalink(replacementRoomId)}
|
||||
<a
|
||||
href={makeRoomPermalink(replacementRoomId)}
|
||||
className="mx_MessageComposer_roomReplaced_link"
|
||||
onClick={this.onTombstoneClick}
|
||||
>
|
||||
{ _t("The conversation continues here.") }
|
||||
{_t("The conversation continues here.")}
|
||||
</a>
|
||||
) : '';
|
||||
) : (
|
||||
""
|
||||
);
|
||||
|
||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img className="mx_MessageComposer_roomReplaced_icon"
|
||||
src={require("../../../../res/img/room_replaced.svg").default}
|
||||
/>
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
{ _t("This room has been replaced and is no longer active.") }
|
||||
</span><br />
|
||||
{ continuesLink }
|
||||
</div>
|
||||
</div>);
|
||||
controls.push(
|
||||
<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
|
||||
<div className="mx_MessageComposer_replaced_valign">
|
||||
<img
|
||||
className="mx_MessageComposer_roomReplaced_icon"
|
||||
src={require("../../../../res/img/room_replaced.svg").default}
|
||||
/>
|
||||
<span className="mx_MessageComposer_roomReplaced_header">
|
||||
{_t("This room has been replaced and is no longer active.")}
|
||||
</span>
|
||||
<br />
|
||||
{continuesLink}
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<div key="controls_error" className="mx_MessageComposer_noperm_error">
|
||||
{ _t('You do not have permission to post to this room') }
|
||||
{_t("You do not have permission to post to this room")}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
@ -517,15 +536,13 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
let recordingTooltip;
|
||||
if (this.state.recordingTimeLeftSeconds) {
|
||||
const secondsLeft = Math.round(this.state.recordingTimeLeftSeconds);
|
||||
recordingTooltip = <Tooltip
|
||||
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
|
||||
alignment={Alignment.Top}
|
||||
/>;
|
||||
recordingTooltip = (
|
||||
<Tooltip label={_t("%(seconds)ss left", { seconds: secondsLeft })} alignment={Alignment.Top} />
|
||||
);
|
||||
}
|
||||
|
||||
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
|
||||
? this.props.relation.event_id
|
||||
: null;
|
||||
const threadId =
|
||||
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
|
||||
|
||||
controls.push(
|
||||
<Stickerpicker
|
||||
|
@ -549,55 +566,58 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className={classes} ref={this.ref}>
|
||||
{ recordingTooltip }
|
||||
{recordingTooltip}
|
||||
<div className="mx_MessageComposer_wrapper">
|
||||
<ReplyPreview
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator} />
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>
|
||||
<div className="mx_MessageComposer_row">
|
||||
{ e2eIcon }
|
||||
{ composer }
|
||||
{e2eIcon}
|
||||
{composer}
|
||||
<div className="mx_MessageComposer_actions">
|
||||
{ controls }
|
||||
{ canSendMessages && <MessageComposerButtons
|
||||
addEmoji={this.addEmoji}
|
||||
haveRecording={this.state.haveRecording}
|
||||
isMenuOpen={this.state.isMenuOpen}
|
||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||
menuPosition={menuPosition}
|
||||
relation={this.props.relation}
|
||||
onRecordStartEndClick={() => {
|
||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||
if (this.context.narrow) {
|
||||
{controls}
|
||||
{canSendMessages && (
|
||||
<MessageComposerButtons
|
||||
addEmoji={this.addEmoji}
|
||||
haveRecording={this.state.haveRecording}
|
||||
isMenuOpen={this.state.isMenuOpen}
|
||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||
menuPosition={menuPosition}
|
||||
relation={this.props.relation}
|
||||
onRecordStartEndClick={() => {
|
||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||
if (this.context.narrow) {
|
||||
this.toggleButtonMenu();
|
||||
}
|
||||
}}
|
||||
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||
showLocationButton={!window.electron}
|
||||
showPollsButton={this.state.showPollsButton}
|
||||
showStickersButton={this.showStickersButton}
|
||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||
onComposerModeClick={this.onRichTextToggle}
|
||||
toggleButtonMenu={this.toggleButtonMenu}
|
||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
||||
onStartVoiceBroadcastClick={() => {
|
||||
setUpVoiceBroadcastPreRecording(
|
||||
this.props.room,
|
||||
MatrixClientPeg.get(),
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastRecordingsStore.instance(),
|
||||
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
||||
);
|
||||
this.toggleButtonMenu();
|
||||
}
|
||||
}}
|
||||
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||
showLocationButton={!window.electron}
|
||||
showPollsButton={this.state.showPollsButton}
|
||||
showStickersButton={this.showStickersButton}
|
||||
isRichTextEnabled={this.state.isRichTextEnabled}
|
||||
onComposerModeClick={this.onRichTextToggle}
|
||||
toggleButtonMenu={this.toggleButtonMenu}
|
||||
showVoiceBroadcastButton={this.state.showVoiceBroadcastButton}
|
||||
onStartVoiceBroadcastClick={() => {
|
||||
setUpVoiceBroadcastPreRecording(
|
||||
this.props.room,
|
||||
MatrixClientPeg.get(),
|
||||
SdkContextClass.instance.voiceBroadcastPlaybacksStore,
|
||||
VoiceBroadcastRecordingsStore.instance(),
|
||||
SdkContextClass.instance.voiceBroadcastPreRecordingStore,
|
||||
);
|
||||
this.toggleButtonMenu();
|
||||
}}
|
||||
/> }
|
||||
{ showSendButton && (
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showSendButton && (
|
||||
<SendButton
|
||||
key="controls_send"
|
||||
onClick={this.sendMessage}
|
||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||
/>
|
||||
) }
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,32 +14,32 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
import { IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||
import { M_POLL_START } from "matrix-events-sdk";
|
||||
import React, { createContext, MouseEventHandler, ReactElement, useContext, useRef } from 'react';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||
import React, { createContext, MouseEventHandler, ReactElement, useContext, useRef } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { CollapsibleButton } from './CollapsibleButton';
|
||||
import { AboveLeftOf } from '../../structures/ContextMenu';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { CollapsibleButton } from "./CollapsibleButton";
|
||||
import { AboveLeftOf } from "../../structures/ContextMenu";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import LocationButton from '../location/LocationButton';
|
||||
import LocationButton from "../location/LocationButton";
|
||||
import Modal from "../../../Modal";
|
||||
import PollCreateDialog from "../elements/PollCreateDialog";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||
import RoomContext from '../../../contexts/RoomContext';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import ContentMessages from "../../../ContentMessages";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import { useDispatcher } from "../../../hooks/useDispatcher";
|
||||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu';
|
||||
import { EmojiButton } from './EmojiButton';
|
||||
import { useSettingValue } from '../../../hooks/useSettings';
|
||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import { EmojiButton } from "./EmojiButton";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
|
||||
interface IProps {
|
||||
addEmoji: (emoji: string) => boolean;
|
||||
|
@ -67,7 +67,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
const matrixClient: MatrixClient = useContext(MatrixClientContext);
|
||||
const { room, roomId, narrow } = useContext(RoomContext);
|
||||
|
||||
const isWysiwygLabEnabled = useSettingValue<boolean>('feature_wysiwyg_composer');
|
||||
const isWysiwygLabEnabled = useSettingValue<boolean>("feature_wysiwyg_composer");
|
||||
|
||||
if (props.haveRecording) {
|
||||
return null;
|
||||
|
@ -77,9 +77,15 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
let moreButtons: ReactElement[];
|
||||
if (narrow) {
|
||||
mainButtons = [
|
||||
isWysiwygLabEnabled ?
|
||||
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
|
||||
emojiButton(props),
|
||||
isWysiwygLabEnabled ? (
|
||||
<ComposerModeButton
|
||||
key="composerModeButton"
|
||||
isRichTextEnabled={props.isRichTextEnabled}
|
||||
onClick={props.onComposerModeClick}
|
||||
/>
|
||||
) : (
|
||||
emojiButton(props)
|
||||
),
|
||||
];
|
||||
moreButtons = [
|
||||
uploadButton(), // props passed via UploadButtonContext
|
||||
|
@ -91,9 +97,15 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
];
|
||||
} else {
|
||||
mainButtons = [
|
||||
isWysiwygLabEnabled ?
|
||||
<ComposerModeButton key="composerModeButton" isRichTextEnabled={props.isRichTextEnabled} onClick={props.onComposerModeClick} /> :
|
||||
emojiButton(props),
|
||||
isWysiwygLabEnabled ? (
|
||||
<ComposerModeButton
|
||||
key="composerModeButton"
|
||||
isRichTextEnabled={props.isRichTextEnabled}
|
||||
onClick={props.onComposerModeClick}
|
||||
/>
|
||||
) : (
|
||||
emojiButton(props)
|
||||
),
|
||||
uploadButton(), // props passed via UploadButtonContext
|
||||
];
|
||||
moreButtons = [
|
||||
|
@ -114,37 +126,41 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
mx_MessageComposer_closeButtonMenu: props.isMenuOpen,
|
||||
});
|
||||
|
||||
return <UploadButtonContextProvider roomId={roomId} relation={props.relation}>
|
||||
{ mainButtons }
|
||||
{ moreButtons.length > 0 && <AccessibleTooltipButton
|
||||
className={moreOptionsClasses}
|
||||
onClick={props.toggleButtonMenu}
|
||||
title={_t("More options")}
|
||||
/> }
|
||||
{ props.isMenuOpen && (
|
||||
<IconizedContextMenu
|
||||
onFinished={props.toggleButtonMenu}
|
||||
{...props.menuPosition}
|
||||
wrapperClassName="mx_MessageComposer_Menu"
|
||||
compact={true}
|
||||
>
|
||||
<OverflowMenuContext.Provider value={props.toggleButtonMenu}>
|
||||
<IconizedContextMenuOptionList>
|
||||
{ moreButtons }
|
||||
</IconizedContextMenuOptionList>
|
||||
</OverflowMenuContext.Provider>
|
||||
</IconizedContextMenu>
|
||||
) }
|
||||
</UploadButtonContextProvider>;
|
||||
return (
|
||||
<UploadButtonContextProvider roomId={roomId} relation={props.relation}>
|
||||
{mainButtons}
|
||||
{moreButtons.length > 0 && (
|
||||
<AccessibleTooltipButton
|
||||
className={moreOptionsClasses}
|
||||
onClick={props.toggleButtonMenu}
|
||||
title={_t("More options")}
|
||||
/>
|
||||
)}
|
||||
{props.isMenuOpen && (
|
||||
<IconizedContextMenu
|
||||
onFinished={props.toggleButtonMenu}
|
||||
{...props.menuPosition}
|
||||
wrapperClassName="mx_MessageComposer_Menu"
|
||||
compact={true}
|
||||
>
|
||||
<OverflowMenuContext.Provider value={props.toggleButtonMenu}>
|
||||
<IconizedContextMenuOptionList>{moreButtons}</IconizedContextMenuOptionList>
|
||||
</OverflowMenuContext.Provider>
|
||||
</IconizedContextMenu>
|
||||
)}
|
||||
</UploadButtonContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function emojiButton(props: IProps): ReactElement {
|
||||
return <EmojiButton
|
||||
key="emoji_button"
|
||||
addEmoji={props.addEmoji}
|
||||
menuPosition={props.menuPosition}
|
||||
className="mx_MessageComposer_button"
|
||||
/>;
|
||||
return (
|
||||
<EmojiButton
|
||||
key="emoji_button"
|
||||
addEmoji={props.addEmoji}
|
||||
menuPosition={props.menuPosition}
|
||||
className="mx_MessageComposer_button"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function uploadButton(): ReactElement {
|
||||
|
@ -167,13 +183,13 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
|
|||
|
||||
const onUploadClick = () => {
|
||||
if (cli.isGuest()) {
|
||||
dis.dispatch({ action: 'require_registration' });
|
||||
dis.dispatch({ action: "require_registration" });
|
||||
return;
|
||||
}
|
||||
uploadInput.current?.click();
|
||||
};
|
||||
|
||||
useDispatcher(dis, payload => {
|
||||
useDispatcher(dis, (payload) => {
|
||||
if (roomContext.timelineRenderingType === payload.context && payload.action === "upload_file") {
|
||||
onUploadClick();
|
||||
}
|
||||
|
@ -195,22 +211,24 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel
|
|||
// not keeping any state, so reset the value of the form control
|
||||
// to empty.
|
||||
// NB. we need to set 'value': the 'files' property is immutable.
|
||||
ev.target.value = '';
|
||||
ev.target.value = "";
|
||||
};
|
||||
|
||||
const uploadInputStyle = { display: 'none' };
|
||||
return <UploadButtonContext.Provider value={onUploadClick}>
|
||||
{ children }
|
||||
const uploadInputStyle = { display: "none" };
|
||||
return (
|
||||
<UploadButtonContext.Provider value={onUploadClick}>
|
||||
{children}
|
||||
|
||||
<input
|
||||
ref={uploadInput}
|
||||
type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onClick={chromeFileInputFix}
|
||||
onChange={onUploadFileInputChange}
|
||||
/>
|
||||
</UploadButtonContext.Provider>;
|
||||
<input
|
||||
ref={uploadInput}
|
||||
type="file"
|
||||
style={uploadInputStyle}
|
||||
multiple
|
||||
onClick={chromeFileInputFix}
|
||||
onChange={onUploadFileInputChange}
|
||||
/>
|
||||
</UploadButtonContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Must be rendered within an UploadButtonContextProvider
|
||||
|
@ -223,55 +241,51 @@ const UploadButton = () => {
|
|||
overflowMenuCloser?.(); // close overflow menu
|
||||
};
|
||||
|
||||
return <CollapsibleButton
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_upload"
|
||||
onClick={onClick}
|
||||
title={_t('Attachment')}
|
||||
/>;
|
||||
return (
|
||||
<CollapsibleButton
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_upload"
|
||||
onClick={onClick}
|
||||
title={_t("Attachment")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function showStickersButton(props: IProps): ReactElement | null {
|
||||
return (
|
||||
props.showStickersButton
|
||||
? <CollapsibleButton
|
||||
id='stickersButton'
|
||||
key="controls_stickers"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_stickers"
|
||||
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
|
||||
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
|
||||
/>
|
||||
: null
|
||||
);
|
||||
return props.showStickersButton ? (
|
||||
<CollapsibleButton
|
||||
id="stickersButton"
|
||||
key="controls_stickers"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_stickers"
|
||||
onClick={() => props.setStickerPickerOpen(!props.isStickerPickerOpen)}
|
||||
title={props.isStickerPickerOpen ? _t("Hide stickers") : _t("Sticker")}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const startVoiceBroadcastButton: React.FC<IProps> = (props: IProps): ReactElement | null => {
|
||||
return (
|
||||
props.showVoiceBroadcastButton
|
||||
? <CollapsibleButton
|
||||
key="start_voice_broadcast"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_voiceBroadcast"
|
||||
onClick={props.onStartVoiceBroadcastClick}
|
||||
title={_t("Voice broadcast")}
|
||||
/>
|
||||
: null
|
||||
);
|
||||
return props.showVoiceBroadcastButton ? (
|
||||
<CollapsibleButton
|
||||
key="start_voice_broadcast"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_voiceBroadcast"
|
||||
onClick={props.onStartVoiceBroadcastClick}
|
||||
title={_t("Voice broadcast")}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
function voiceRecordingButton(props: IProps, narrow: boolean): ReactElement | null {
|
||||
// XXX: recording UI does not work well in narrow mode, so hide for now
|
||||
return (
|
||||
narrow
|
||||
? null
|
||||
: <CollapsibleButton
|
||||
key="voice_message_send"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_voiceMessage"
|
||||
onClick={props.onRecordStartEndClick}
|
||||
title={_t("Voice Message")}
|
||||
/>
|
||||
return narrow ? null : (
|
||||
<CollapsibleButton
|
||||
key="voice_message_send"
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName="mx_MessageComposer_voiceMessage"
|
||||
onClick={props.onRecordStartEndClick}
|
||||
title={_t("Voice Message")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -295,19 +309,13 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
|
|||
MatrixClientPeg.get().getUserId()!,
|
||||
);
|
||||
if (!canSend) {
|
||||
Modal.createDialog(
|
||||
ErrorDialog,
|
||||
{
|
||||
title: _t("Permission Required"),
|
||||
description: _t(
|
||||
"You do not have permission to start polls in this room.",
|
||||
),
|
||||
},
|
||||
);
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Permission Required"),
|
||||
description: _t("You do not have permission to start polls in this room."),
|
||||
});
|
||||
} else {
|
||||
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
|
||||
? this.props.relation.event_id
|
||||
: null;
|
||||
const threadId =
|
||||
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
|
||||
|
||||
Modal.createDialog(
|
||||
PollCreateDialog,
|
||||
|
@ -315,9 +323,9 @@ class PollButton extends React.PureComponent<IPollButtonProps> {
|
|||
room: this.props.room,
|
||||
threadId,
|
||||
},
|
||||
'mx_CompoundDialog',
|
||||
"mx_CompoundDialog",
|
||||
false, // isPriorityModal
|
||||
true, // isStaticModal
|
||||
true, // isStaticModal
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -345,17 +353,15 @@ function showLocationButton(
|
|||
): ReactElement | null {
|
||||
const sender = room.getMember(matrixClient.getUserId()!);
|
||||
|
||||
return (
|
||||
props.showLocationButton && sender
|
||||
? <LocationButton
|
||||
key="location"
|
||||
roomId={roomId}
|
||||
relation={props.relation}
|
||||
sender={sender}
|
||||
menuPosition={props.menuPosition}
|
||||
/>
|
||||
: null
|
||||
);
|
||||
return props.showLocationButton && sender ? (
|
||||
<LocationButton
|
||||
key="location"
|
||||
roomId={roomId}
|
||||
relation={props.relation}
|
||||
sender={sender}
|
||||
menuPosition={props.menuPosition}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
interface WysiwygToggleButtonProps {
|
||||
|
@ -366,15 +372,17 @@ interface WysiwygToggleButtonProps {
|
|||
function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps) {
|
||||
const title = isRichTextEnabled ? _t("Hide formatting") : _t("Show formatting");
|
||||
|
||||
return <CollapsibleButton
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName={classNames({
|
||||
"mx_MessageComposer_plain_text": isRichTextEnabled,
|
||||
"mx_MessageComposer_rich_text": !isRichTextEnabled,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
/>;
|
||||
return (
|
||||
<CollapsibleButton
|
||||
className="mx_MessageComposer_button"
|
||||
iconClassName={classNames({
|
||||
mx_MessageComposer_plain_text: isRichTextEnabled,
|
||||
mx_MessageComposer_rich_text: !isRichTextEnabled,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessageComposerButtons;
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
export enum Formatting {
|
||||
|
@ -48,16 +48,53 @@ export default class MessageComposerFormatBar extends React.PureComponent<IProps
|
|||
|
||||
render() {
|
||||
const classes = classNames("mx_MessageComposerFormatBar", {
|
||||
"mx_MessageComposerFormatBar_shown": this.state.visible,
|
||||
mx_MessageComposerFormatBar_shown: this.state.visible,
|
||||
});
|
||||
return (<div className={classes} ref={this.formatBarRef}>
|
||||
<FormatButton label={_t("Bold")} onClick={() => this.props.onAction(Formatting.Bold)} icon="Bold" shortcut={this.props.shortcuts.bold} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Italics")} onClick={() => this.props.onAction(Formatting.Italics)} icon="Italic" shortcut={this.props.shortcuts.italics} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Strikethrough")} onClick={() => this.props.onAction(Formatting.Strikethrough)} icon="Strikethrough" visible={this.state.visible} />
|
||||
<FormatButton label={_t("Code block")} onClick={() => this.props.onAction(Formatting.Code)} icon="Code" shortcut={this.props.shortcuts.code} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Quote")} onClick={() => this.props.onAction(Formatting.Quote)} icon="Quote" shortcut={this.props.shortcuts.quote} visible={this.state.visible} />
|
||||
<FormatButton label={_t("Insert link")} onClick={() => this.props.onAction(Formatting.InsertLink)} icon="InsertLink" shortcut={this.props.shortcuts.insert_link} visible={this.state.visible} />
|
||||
</div>);
|
||||
return (
|
||||
<div className={classes} ref={this.formatBarRef}>
|
||||
<FormatButton
|
||||
label={_t("Bold")}
|
||||
onClick={() => this.props.onAction(Formatting.Bold)}
|
||||
icon="Bold"
|
||||
shortcut={this.props.shortcuts.bold}
|
||||
visible={this.state.visible}
|
||||
/>
|
||||
<FormatButton
|
||||
label={_t("Italics")}
|
||||
onClick={() => this.props.onAction(Formatting.Italics)}
|
||||
icon="Italic"
|
||||
shortcut={this.props.shortcuts.italics}
|
||||
visible={this.state.visible}
|
||||
/>
|
||||
<FormatButton
|
||||
label={_t("Strikethrough")}
|
||||
onClick={() => this.props.onAction(Formatting.Strikethrough)}
|
||||
icon="Strikethrough"
|
||||
visible={this.state.visible}
|
||||
/>
|
||||
<FormatButton
|
||||
label={_t("Code block")}
|
||||
onClick={() => this.props.onAction(Formatting.Code)}
|
||||
icon="Code"
|
||||
shortcut={this.props.shortcuts.code}
|
||||
visible={this.state.visible}
|
||||
/>
|
||||
<FormatButton
|
||||
label={_t("Quote")}
|
||||
onClick={() => this.props.onAction(Formatting.Quote)}
|
||||
icon="Quote"
|
||||
shortcut={this.props.shortcuts.quote}
|
||||
visible={this.state.visible}
|
||||
/>
|
||||
<FormatButton
|
||||
label={_t("Insert link")}
|
||||
onClick={() => this.props.onAction(Formatting.InsertLink)}
|
||||
icon="InsertLink"
|
||||
shortcut={this.props.shortcuts.insert_link}
|
||||
visible={this.state.visible}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public showAt(selectionRect: DOMRect): void {
|
||||
|
@ -91,18 +128,14 @@ class FormatButton extends React.PureComponent<IFormatButtonProps> {
|
|||
const className = `mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIcon${this.props.icon}`;
|
||||
let shortcut;
|
||||
if (this.props.shortcut) {
|
||||
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">
|
||||
{ this.props.shortcut }
|
||||
</div>;
|
||||
shortcut = <div className="mx_MessageComposerFormatBar_tooltipShortcut">{this.props.shortcut}</div>;
|
||||
}
|
||||
const tooltip = <div>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ this.props.label }
|
||||
const tooltip = (
|
||||
<div>
|
||||
<div className="mx_Tooltip_title">{this.props.label}</div>
|
||||
<div className="mx_Tooltip_sub">{shortcut}</div>
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ shortcut }
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
|
||||
// element="button" and type="button" are necessary for the buttons to work on WebKit,
|
||||
// otherwise the text is deselected before onClick can ever be called
|
||||
|
@ -113,7 +146,8 @@ class FormatButton extends React.PureComponent<IFormatButtonProps> {
|
|||
onClick={this.props.onClick}
|
||||
title={this.props.label}
|
||||
tooltip={tooltip}
|
||||
className={className} />
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,9 +51,7 @@ const NewRoomIntro = () => {
|
|||
const { room, roomId } = useContext(RoomContext);
|
||||
|
||||
const isLocalRoom = room instanceof LocalRoom;
|
||||
const dmPartner = isLocalRoom
|
||||
? room.targets[0]?.userId
|
||||
: DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
const dmPartner = isLocalRoom ? room.targets[0]?.userId : DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
|
||||
let body: JSX.Element;
|
||||
if (dmPartner) {
|
||||
|
@ -62,43 +60,54 @@ const NewRoomIntro = () => {
|
|||
|
||||
if (isLocalRoom) {
|
||||
introMessage = _t("Send your first message to invite <displayName/> to chat");
|
||||
} else if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
||||
} else 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 = room?.name || 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 } as User,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
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 } as User),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
<h2>{room.name}</h2>
|
||||
|
||||
<p>{ _t(introMessage, {}, {
|
||||
displayName: () => <b>{ displayName }</b>,
|
||||
}) }</p>
|
||||
{ caption && <p>{ caption }</p> }
|
||||
</React.Fragment>;
|
||||
<p>
|
||||
{_t(
|
||||
introMessage,
|
||||
{},
|
||||
{
|
||||
displayName: () => <b>{displayName}</b>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
{caption && <p>{caption}</p>}
|
||||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
const inRoom = room && room.getMyMembership() === "join";
|
||||
const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic;
|
||||
const canAddTopic = inRoom && room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId());
|
||||
|
||||
const onTopicClick = () => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "open_room_settings",
|
||||
room_id: roomId,
|
||||
}, true);
|
||||
defaultDispatcher.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();
|
||||
|
@ -107,15 +116,31 @@ const NewRoomIntro = () => {
|
|||
|
||||
let topicText;
|
||||
if (canAddTopic && topic) {
|
||||
topicText = _t("Topic: %(topic)s (<a>edit</a>)", { topic }, {
|
||||
a: sub => <AccessibleButton kind="link_inline" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||
});
|
||||
topicText = _t(
|
||||
"Topic: %(topic)s (<a>edit</a>)",
|
||||
{ topic },
|
||||
{
|
||||
a: (sub) => (
|
||||
<AccessibleButton kind="link_inline" 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_inline" onClick={onTopicClick}>{ sub }</AccessibleButton>,
|
||||
});
|
||||
topicText = _t(
|
||||
"<a>Add a topic</a> to help people know what it is about.",
|
||||
{},
|
||||
{
|
||||
a: (sub) => (
|
||||
<AccessibleButton kind="link_inline" onClick={onTopicClick}>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender();
|
||||
|
@ -140,38 +165,44 @@ const NewRoomIntro = () => {
|
|||
|
||||
let buttons;
|
||||
if (parentSpace && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
showSpaceInvite(parentSpace);
|
||||
}}
|
||||
>
|
||||
{ _t("Invite to %(spaceName)s", { spaceName: parentSpace.name }) }
|
||||
</AccessibleButton>
|
||||
{ room.canInvite(cli.getUserId()) && <AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary_outline"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{ _t("Invite to just this room") }
|
||||
</AccessibleButton> }
|
||||
</div>;
|
||||
buttons = (
|
||||
<div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
showSpaceInvite(parentSpace);
|
||||
}}
|
||||
>
|
||||
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
|
||||
</AccessibleButton>
|
||||
{room.canInvite(cli.getUserId()) && (
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary_outline"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to just this room")}
|
||||
</AccessibleButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{ _t("Invite to this room") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
buttons = (
|
||||
<div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
|
@ -180,26 +211,37 @@ const NewRoomIntro = () => {
|
|||
);
|
||||
|
||||
if (!avatarUrl) {
|
||||
avatar = <MiniAvatarUploader
|
||||
hasAvatar={false}
|
||||
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||
setAvatarUrl={url => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')}
|
||||
>
|
||||
{ avatar }
|
||||
</MiniAvatarUploader>;
|
||||
avatar = (
|
||||
<MiniAvatarUploader
|
||||
hasAvatar={false}
|
||||
noAvatarLabel={_t("Add a photo, so people can easily spot your room.")}
|
||||
setAvatarUrl={(url) => cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, "")}
|
||||
>
|
||||
{avatar}
|
||||
</MiniAvatarUploader>
|
||||
);
|
||||
}
|
||||
|
||||
body = <React.Fragment>
|
||||
{ avatar }
|
||||
body = (
|
||||
<React.Fragment>
|
||||
{avatar}
|
||||
|
||||
<h2>{ room.name }</h2>
|
||||
<h2>{room.name}</h2>
|
||||
|
||||
<p>{ createdText } { _t("This is the start of <roomName/>.", {}, {
|
||||
roomName: () => <b>{ room.name }</b>,
|
||||
}) }</p>
|
||||
<p>{ topicText }</p>
|
||||
{ buttons }
|
||||
</React.Fragment>;
|
||||
<p>
|
||||
{createdText}{" "}
|
||||
{_t(
|
||||
"This is the start of <roomName/>.",
|
||||
{},
|
||||
{
|
||||
roomName: () => <b>{room.name}</b>,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>{topicText}</p>
|
||||
{buttons}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
function openRoomSettings(event) {
|
||||
|
@ -211,33 +253,40 @@ const NewRoomIntro = () => {
|
|||
}
|
||||
|
||||
const subText = _t(
|
||||
"Your private messages are normally encrypted, but this room isn't. "+
|
||||
"Usually this is due to an unsupported device or method being used, " +
|
||||
"like email invites.",
|
||||
"Your private messages are normally encrypted, but this room isn't. " +
|
||||
"Usually this is due to an unsupported device or method being used, " +
|
||||
"like email invites.",
|
||||
);
|
||||
|
||||
let subButton;
|
||||
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get()) && !isLocalRoom) {
|
||||
subButton = (
|
||||
<AccessibleButton kind='link_inline' onClick={openRoomSettings}>{ _t("Enable encryption in settings.") }</AccessibleButton>
|
||||
<AccessibleButton kind="link_inline" onClick={openRoomSettings}>
|
||||
{_t("Enable encryption in settings.")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
const subtitle = (
|
||||
<span> { subText } { subButton } </span>
|
||||
<span>
|
||||
{" "}
|
||||
{subText} {subButton}{" "}
|
||||
</span>
|
||||
);
|
||||
|
||||
return <li className="mx_NewRoomIntro">
|
||||
{ !hasExpectedEncryptionSettings(cli, room) && (
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||
title={_t("End-to-end encryption isn't enabled")}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
) }
|
||||
return (
|
||||
<li className="mx_NewRoomIntro">
|
||||
{!hasExpectedEncryptionSettings(cli, room) && (
|
||||
<EventTileBubble
|
||||
className="mx_cryptoEvent mx_cryptoEvent_icon_warning"
|
||||
title={_t("End-to-end encryption isn't enabled")}
|
||||
subtitle={subtitle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ body }
|
||||
</li>;
|
||||
{body}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewRoomIntro;
|
||||
|
|
|
@ -65,7 +65,8 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
};
|
||||
|
||||
this.countWatcherRef = SettingsStore.watchSetting(
|
||||
"Notifications.alwaysShowBadgeCounts", this.roomId,
|
||||
"Notifications.alwaysShowBadgeCounts",
|
||||
this.roomId,
|
||||
this.countPreferenceChanged,
|
||||
);
|
||||
}
|
||||
|
@ -125,16 +126,18 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
|
|||
tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />;
|
||||
}
|
||||
|
||||
return <StatelessNotificationBadge
|
||||
label={label}
|
||||
symbol={notification.symbol}
|
||||
count={notification.count}
|
||||
color={notification.color}
|
||||
onClick={onClick}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{ tooltip }
|
||||
</StatelessNotificationBadge>;
|
||||
return (
|
||||
<StatelessNotificationBadge
|
||||
label={label}
|
||||
symbol={notification.symbol}
|
||||
count={notification.count}
|
||||
color={notification.color}
|
||||
onClick={onClick}
|
||||
onMouseOver={this.onMouseOver}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
>
|
||||
{tooltip}
|
||||
</StatelessNotificationBadge>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,11 +33,7 @@ interface Props {
|
|||
label?: string;
|
||||
}
|
||||
|
||||
export function StatelessNotificationBadge({
|
||||
symbol,
|
||||
count,
|
||||
color,
|
||||
...props }: Props) {
|
||||
export function StatelessNotificationBadge({ symbol, count, color, ...props }: Props) {
|
||||
const hideBold = useSettingValue("feature_hidebold");
|
||||
|
||||
// Don't show a badge if we don't need to
|
||||
|
@ -54,12 +50,12 @@ export function StatelessNotificationBadge({
|
|||
}
|
||||
|
||||
const classes = classNames({
|
||||
'mx_NotificationBadge': true,
|
||||
'mx_NotificationBadge_visible': isEmptyBadge ? true : hasUnreadCount,
|
||||
'mx_NotificationBadge_highlighted': color >= NotificationColor.Red,
|
||||
'mx_NotificationBadge_dot': isEmptyBadge,
|
||||
'mx_NotificationBadge_2char': symbol?.length > 0 && symbol?.length < 3,
|
||||
'mx_NotificationBadge_3char': symbol?.length > 2,
|
||||
mx_NotificationBadge: true,
|
||||
mx_NotificationBadge_visible: isEmptyBadge ? true : hasUnreadCount,
|
||||
mx_NotificationBadge_highlighted: color >= NotificationColor.Red,
|
||||
mx_NotificationBadge_dot: isEmptyBadge,
|
||||
mx_NotificationBadge_2char: symbol?.length > 0 && symbol?.length < 3,
|
||||
mx_NotificationBadge_3char: symbol?.length > 2,
|
||||
});
|
||||
|
||||
if (props.onClick) {
|
||||
|
@ -72,15 +68,15 @@ export function StatelessNotificationBadge({
|
|||
onMouseOver={props.onMouseOver}
|
||||
onMouseLeave={props.onMouseLeave}
|
||||
>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
{ props.children }
|
||||
<span className="mx_NotificationBadge_count">{symbol}</span>
|
||||
{props.children}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<span className="mx_NotificationBadge_count">{ symbol }</span>
|
||||
<span className="mx_NotificationBadge_count">{symbol}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,9 +28,5 @@ interface Props {
|
|||
export function UnreadNotificationBadge({ room, threadId }: Props) {
|
||||
const { symbol, count, color } = useUnreadNotifications(room, threadId);
|
||||
|
||||
return <StatelessNotificationBadge
|
||||
symbol={symbol}
|
||||
count={count}
|
||||
color={color}
|
||||
/>;
|
||||
return <StatelessNotificationBadge symbol={symbol} count={count} color={color} />;
|
||||
}
|
||||
|
|
|
@ -27,8 +27,8 @@ import { Action } from "../../../dispatcher/actions";
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MessageEvent from "../messages/MessageEvent";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { formatDate } from '../../../DateUtils';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { formatDate } from "../../../DateUtils";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
@ -78,8 +78,8 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
|||
|
||||
try {
|
||||
await Promise.all(
|
||||
[M_POLL_RESPONSE.name, M_POLL_RESPONSE.altName, M_POLL_END.name, M_POLL_END.altName]
|
||||
.map(async eventType => {
|
||||
[M_POLL_RESPONSE.name, M_POLL_RESPONSE.altName, M_POLL_END.name, M_POLL_END.altName].map(
|
||||
async (eventType) => {
|
||||
const relations = new Relations(RelationType.Reference, eventType, room);
|
||||
relations.setTargetEvent(this.props.event);
|
||||
|
||||
|
@ -91,12 +91,17 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
|||
let nextBatch: string | undefined;
|
||||
do {
|
||||
const page = await this.context.relations(
|
||||
roomId, eventId, RelationType.Reference, eventType, { from: nextBatch },
|
||||
roomId,
|
||||
eventId,
|
||||
RelationType.Reference,
|
||||
eventType,
|
||||
{ from: nextBatch },
|
||||
);
|
||||
nextBatch = page.nextBatch;
|
||||
page.events.forEach(event => relations.addEvent(event));
|
||||
page.events.forEach((event) => relations.addEvent(event));
|
||||
} while (nextBatch);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(`Error fetching responses to pinned poll ${eventId} in room ${roomId}`);
|
||||
|
@ -119,43 +124,45 @@ export default class PinnedEventTile extends React.Component<IProps> {
|
|||
);
|
||||
}
|
||||
|
||||
return <div className="mx_PinnedEventTile">
|
||||
<MemberAvatar
|
||||
className="mx_PinnedEventTile_senderAvatar"
|
||||
member={this.props.event.sender}
|
||||
width={AVATAR_SIZE}
|
||||
height={AVATAR_SIZE}
|
||||
fallbackUserId={sender}
|
||||
/>
|
||||
|
||||
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
|
||||
{ this.props.event.sender?.name || sender }
|
||||
</span>
|
||||
|
||||
{ unpinButton }
|
||||
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent
|
||||
mxEvent={this.props.event}
|
||||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
// @ts-ignore - complaining that className is invalid when it's not
|
||||
className="mx_PinnedEventTile_body"
|
||||
maxImageHeight={150}
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
replacingEventId={this.props.event.replacingEventId()}
|
||||
return (
|
||||
<div className="mx_PinnedEventTile">
|
||||
<MemberAvatar
|
||||
className="mx_PinnedEventTile_senderAvatar"
|
||||
member={this.props.event.sender}
|
||||
width={AVATAR_SIZE}
|
||||
height={AVATAR_SIZE}
|
||||
fallbackUserId={sender}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx_PinnedEventTile_footer">
|
||||
<span className="mx_MessageTimestamp mx_PinnedEventTile_timestamp">
|
||||
{ formatDate(new Date(this.props.event.getTs())) }
|
||||
<span className={"mx_PinnedEventTile_sender " + getUserNameColorClass(sender)}>
|
||||
{this.props.event.sender?.name || sender}
|
||||
</span>
|
||||
|
||||
<AccessibleButton onClick={this.onTileClicked} kind="link">
|
||||
{ _t("View message") }
|
||||
</AccessibleButton>
|
||||
{unpinButton}
|
||||
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent
|
||||
mxEvent={this.props.event}
|
||||
getRelationsForEvent={this.getRelationsForEvent}
|
||||
// @ts-ignore - complaining that className is invalid when it's not
|
||||
className="mx_PinnedEventTile_body"
|
||||
maxImageHeight={150}
|
||||
onHeightChanged={() => {}} // we need to give this, apparently
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
replacingEventId={this.props.event.replacingEventId()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx_PinnedEventTile_footer">
|
||||
<span className="mx_MessageTimestamp mx_PinnedEventTile_timestamp">
|
||||
{formatDate(new Date(this.props.event.getTs()))}
|
||||
</span>
|
||||
|
||||
<AccessibleButton onClick={this.onTileClicked} kind="link">
|
||||
{_t("View message")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
|
||||
|
||||
|
@ -85,7 +85,7 @@ export default class PresenceLabel extends React.Component<IProps> {
|
|||
render() {
|
||||
return (
|
||||
<div className="mx_PresenceLabel">
|
||||
{ this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive) }
|
||||
{this.getPrettyPresence(this.props.presenceState, this.props.activeAgo, this.props.currentlyActive)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -83,30 +83,27 @@ export function readReceiptTooltip(members: string[], hasMore: boolean): string
|
|||
}
|
||||
}
|
||||
|
||||
export function ReadReceiptGroup(
|
||||
{ readReceipts, readReceiptMap, checkUnmounting, suppressAnimation, isTwelveHour }: Props,
|
||||
) {
|
||||
export function ReadReceiptGroup({
|
||||
readReceipts,
|
||||
readReceiptMap,
|
||||
checkUnmounting,
|
||||
suppressAnimation,
|
||||
isTwelveHour,
|
||||
}: Props) {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
// If we are above MAX_READ_AVATARS, we’ll have to remove a few to have space for the +n count.
|
||||
const hasMore = readReceipts.length > MAX_READ_AVATARS;
|
||||
const maxAvatars = hasMore
|
||||
? MAX_READ_AVATARS_PLUS_N
|
||||
: MAX_READ_AVATARS;
|
||||
const maxAvatars = hasMore ? MAX_READ_AVATARS_PLUS_N : MAX_READ_AVATARS;
|
||||
|
||||
const tooltipMembers: string[] = readReceipts.slice(0, maxAvatars)
|
||||
.map(it => it.roomMember?.name ?? it.userId);
|
||||
const tooltipMembers: string[] = readReceipts.slice(0, maxAvatars).map((it) => it.roomMember?.name ?? it.userId);
|
||||
const tooltipText = readReceiptTooltip(tooltipMembers, hasMore);
|
||||
|
||||
const [{ showTooltip, hideTooltip }, tooltip] = useTooltip({
|
||||
label: (
|
||||
<>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ _t("Seen by %(count)s people", { count: readReceipts.length }) }
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ tooltipText }
|
||||
</div>
|
||||
<div className="mx_Tooltip_title">{_t("Seen by %(count)s people", { count: readReceipts.length })}</div>
|
||||
<div className="mx_Tooltip_sub">{tooltipText}</div>
|
||||
</>
|
||||
),
|
||||
alignment: Alignment.TopRight,
|
||||
|
@ -132,42 +129,44 @@ export function ReadReceiptGroup(
|
|||
);
|
||||
}
|
||||
|
||||
const avatars = readReceipts.map((receipt, index) => {
|
||||
const { hidden, position } = determineAvatarPosition(index, maxAvatars);
|
||||
const avatars = readReceipts
|
||||
.map((receipt, index) => {
|
||||
const { hidden, position } = determineAvatarPosition(index, maxAvatars);
|
||||
|
||||
const userId = receipt.userId;
|
||||
let readReceiptInfo: IReadReceiptInfo;
|
||||
const userId = receipt.userId;
|
||||
let readReceiptInfo: IReadReceiptInfo;
|
||||
|
||||
if (readReceiptMap) {
|
||||
readReceiptInfo = readReceiptMap[userId];
|
||||
if (!readReceiptInfo) {
|
||||
readReceiptInfo = {};
|
||||
readReceiptMap[userId] = readReceiptInfo;
|
||||
if (readReceiptMap) {
|
||||
readReceiptInfo = readReceiptMap[userId];
|
||||
if (!readReceiptInfo) {
|
||||
readReceiptInfo = {};
|
||||
readReceiptMap[userId] = readReceiptInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ReadReceiptMarker
|
||||
key={userId}
|
||||
member={receipt.roomMember}
|
||||
fallbackUserId={userId}
|
||||
offset={position * READ_AVATAR_OFFSET}
|
||||
hidden={hidden}
|
||||
readReceiptInfo={readReceiptInfo}
|
||||
checkUnmounting={checkUnmounting}
|
||||
suppressAnimation={suppressAnimation}
|
||||
timestamp={receipt.ts}
|
||||
showTwelveHour={isTwelveHour}
|
||||
/>
|
||||
);
|
||||
}).reverse();
|
||||
return (
|
||||
<ReadReceiptMarker
|
||||
key={userId}
|
||||
member={receipt.roomMember}
|
||||
fallbackUserId={userId}
|
||||
offset={position * READ_AVATAR_OFFSET}
|
||||
hidden={hidden}
|
||||
readReceiptInfo={readReceiptInfo}
|
||||
checkUnmounting={checkUnmounting}
|
||||
suppressAnimation={suppressAnimation}
|
||||
timestamp={receipt.ts}
|
||||
showTwelveHour={isTwelveHour}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.reverse();
|
||||
|
||||
let remText: JSX.Element;
|
||||
const remainder = readReceipts.length - maxAvatars;
|
||||
if (remainder > 0) {
|
||||
remText = (
|
||||
<span className="mx_ReadReceiptGroup_remainder" aria-live="off">
|
||||
+{ remainder }
|
||||
+{remainder}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
@ -176,22 +175,19 @@ export function ReadReceiptGroup(
|
|||
if (menuDisplayed) {
|
||||
const buttonRect = button.current.getBoundingClientRect();
|
||||
contextMenu = (
|
||||
<ContextMenu
|
||||
menuClassName="mx_ReadReceiptGroup_popup"
|
||||
onFinished={closeMenu}
|
||||
{...aboveLeftOf(buttonRect)}>
|
||||
<ContextMenu menuClassName="mx_ReadReceiptGroup_popup" onFinished={closeMenu} {...aboveLeftOf(buttonRect)}>
|
||||
<AutoHideScrollbar>
|
||||
<SectionHeader className="mx_ReadReceiptGroup_title">
|
||||
{ _t("Seen by %(count)s people", { count: readReceipts.length }) }
|
||||
{_t("Seen by %(count)s people", { count: readReceipts.length })}
|
||||
</SectionHeader>
|
||||
{ readReceipts.map(receipt => (
|
||||
{readReceipts.map((receipt) => (
|
||||
<ReadReceiptPerson
|
||||
key={receipt.userId}
|
||||
{...receipt}
|
||||
isTwelveHour={isTwelveHour}
|
||||
onAfterClick={closeMenu}
|
||||
/>
|
||||
)) }
|
||||
))}
|
||||
</AutoHideScrollbar>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
@ -211,19 +207,21 @@ export function ReadReceiptGroup(
|
|||
onFocus={showTooltip}
|
||||
onBlur={hideTooltip}
|
||||
>
|
||||
{ remText }
|
||||
{remText}
|
||||
<span
|
||||
className="mx_ReadReceiptGroup_container"
|
||||
style={{
|
||||
width: Math.min(maxAvatars, readReceipts.length) * READ_AVATAR_OFFSET +
|
||||
READ_AVATAR_SIZE - READ_AVATAR_OFFSET,
|
||||
width:
|
||||
Math.min(maxAvatars, readReceipts.length) * READ_AVATAR_OFFSET +
|
||||
READ_AVATAR_SIZE -
|
||||
READ_AVATAR_OFFSET,
|
||||
}}
|
||||
>
|
||||
{ avatars }
|
||||
{avatars}
|
||||
</span>
|
||||
</AccessibleButton>
|
||||
{ tooltip }
|
||||
{ contextMenu }
|
||||
{tooltip}
|
||||
{contextMenu}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -240,12 +238,8 @@ function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick
|
|||
tooltipClassName: "mx_ReadReceiptGroup_person--tooltip",
|
||||
label: (
|
||||
<>
|
||||
<div className="mx_Tooltip_title">
|
||||
{ roomMember?.rawDisplayName ?? userId }
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{ userId }
|
||||
</div>
|
||||
<div className="mx_Tooltip_title">{roomMember?.rawDisplayName ?? userId}</div>
|
||||
<div className="mx_Tooltip_sub">{userId}</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
@ -260,7 +254,7 @@ function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick
|
|||
// The ViewUser action leads to the RightPanelStore, and RightPanelStoreIPanelState defines the
|
||||
// member property of IRightPanelCardState as `RoomMember | User`, so we’re fine for now, but we
|
||||
// should definitely clean this up later
|
||||
member: roomMember ?? { userId } as User,
|
||||
member: roomMember ?? ({ userId } as User),
|
||||
push: false,
|
||||
});
|
||||
onAfterClick?.();
|
||||
|
@ -282,12 +276,10 @@ function ReadReceiptPerson({ userId, roomMember, ts, isTwelveHour, onAfterClick
|
|||
hideTitle
|
||||
/>
|
||||
<div className="mx_ReadReceiptGroup_name">
|
||||
<p>{ roomMember?.name ?? userId }</p>
|
||||
<p className="mx_ReadReceiptGroup_secondary">
|
||||
{ formatDate(new Date(ts), isTwelveHour) }
|
||||
</p>
|
||||
<p>{roomMember?.name ?? userId}</p>
|
||||
<p className="mx_ReadReceiptGroup_secondary">{formatDate(new Date(ts), isTwelveHour)}</p>
|
||||
</div>
|
||||
{ tooltip }
|
||||
{tooltip}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
@ -301,14 +293,8 @@ function SectionHeader({ className, children }: PropsWithChildren<ISectionHeader
|
|||
const [onFocus] = useRovingTabIndex(ref);
|
||||
|
||||
return (
|
||||
<h3
|
||||
className={className}
|
||||
role="menuitem"
|
||||
onFocus={onFocus}
|
||||
tabIndex={-1}
|
||||
ref={ref}
|
||||
>
|
||||
{ children }
|
||||
<h3 className={className} role="menuitem" onFocus={onFocus} tabIndex={-1} ref={ref}>
|
||||
{children}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, RefObject } from 'react';
|
||||
import React, { createRef, RefObject } from "react";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import NodeAnimator from "../../../NodeAnimator";
|
||||
import { toPx } from "../../../utils/units";
|
||||
import { LegacyMemberAvatar as MemberAvatar } from '../avatars/MemberAvatar';
|
||||
import { LegacyMemberAvatar as MemberAvatar } from "../avatars/MemberAvatar";
|
||||
import { READ_AVATAR_SIZE } from "./ReadReceiptGroup";
|
||||
|
||||
export interface IReadReceiptInfo {
|
||||
|
@ -174,10 +174,10 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
|
|||
|
||||
const newPosition = this.readReceiptPosition(newInfo);
|
||||
const oldPosition = oldInfo
|
||||
// start at the old height and in the old h pos
|
||||
? this.readReceiptPosition(oldInfo)
|
||||
// treat new RRs as though they were off the top of the screen
|
||||
: -READ_AVATAR_SIZE;
|
||||
? // start at the old height and in the old h pos
|
||||
this.readReceiptPosition(oldInfo)
|
||||
: // treat new RRs as though they were off the top of the screen
|
||||
-READ_AVATAR_SIZE;
|
||||
|
||||
const startStyles = [];
|
||||
if (oldInfo?.right) {
|
||||
|
@ -204,7 +204,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
|
|||
|
||||
const style = {
|
||||
right: toPx(this.props.offset),
|
||||
top: '0px',
|
||||
top: "0px",
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -33,45 +33,52 @@ const RecentlyViewedButton = () => {
|
|||
const tooltipRef = useRef<InteractiveTooltip>();
|
||||
const crumbs = useEventEmitterState(BreadcrumbsStore.instance, UPDATE_EVENT, () => BreadcrumbsStore.instance.rooms);
|
||||
|
||||
const content = <div className="mx_RecentlyViewedButton_ContextMenu">
|
||||
<h4>{ _t("Recently viewed") }</h4>
|
||||
<div>
|
||||
{ crumbs.map(crumb => {
|
||||
return <MenuItem
|
||||
key={crumb.roomId}
|
||||
onClick={(ev) => {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: crumb.roomId,
|
||||
metricsTrigger: "WebVerticalBreadcrumbs",
|
||||
metricsViaKeyboard: ev.type !== "click",
|
||||
});
|
||||
tooltipRef.current?.hideTooltip();
|
||||
}}
|
||||
>
|
||||
{ crumb.isSpaceRoom()
|
||||
? <RoomAvatar room={crumb} width={24} height={24} />
|
||||
: <DecoratedRoomAvatar room={crumb} avatarSize={24} tooltipProps={{ tabIndex: -1 }} />
|
||||
}
|
||||
<span className="mx_RecentlyViewedButton_entry_label">
|
||||
<div>{ crumb.name }</div>
|
||||
<RoomContextDetails className="mx_RecentlyViewedButton_entry_spaces" room={crumb} />
|
||||
</span>
|
||||
</MenuItem>;
|
||||
}) }
|
||||
const content = (
|
||||
<div className="mx_RecentlyViewedButton_ContextMenu">
|
||||
<h4>{_t("Recently viewed")}</h4>
|
||||
<div>
|
||||
{crumbs.map((crumb) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={crumb.roomId}
|
||||
onClick={(ev) => {
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: crumb.roomId,
|
||||
metricsTrigger: "WebVerticalBreadcrumbs",
|
||||
metricsViaKeyboard: ev.type !== "click",
|
||||
});
|
||||
tooltipRef.current?.hideTooltip();
|
||||
}}
|
||||
>
|
||||
{crumb.isSpaceRoom() ? (
|
||||
<RoomAvatar room={crumb} width={24} height={24} />
|
||||
) : (
|
||||
<DecoratedRoomAvatar room={crumb} avatarSize={24} tooltipProps={{ tabIndex: -1 }} />
|
||||
)}
|
||||
<span className="mx_RecentlyViewedButton_entry_label">
|
||||
<div>{crumb.name}</div>
|
||||
<RoomContextDetails className="mx_RecentlyViewedButton_entry_spaces" room={crumb} />
|
||||
</span>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
|
||||
return <InteractiveTooltip content={content} direction={Direction.Right} ref={tooltipRef}>
|
||||
{ ({ ref, onMouseOver }) => (
|
||||
<span
|
||||
className="mx_LeftPanel_recentsButton"
|
||||
title={_t("Recently viewed")}
|
||||
ref={ref}
|
||||
onMouseOver={onMouseOver}
|
||||
/>
|
||||
) }
|
||||
</InteractiveTooltip>;
|
||||
return (
|
||||
<InteractiveTooltip content={content} direction={Direction.Right} ref={tooltipRef}>
|
||||
{({ ref, onMouseOver }) => (
|
||||
<span
|
||||
className="mx_LeftPanel_recentsButton"
|
||||
title={_t("Recently viewed")}
|
||||
ref={ref}
|
||||
onMouseOver={onMouseOver}
|
||||
/>
|
||||
)}
|
||||
</InteractiveTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentlyViewedButton;
|
||||
|
|
|
@ -14,19 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import ReplyTile from './ReplyTile';
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
import ReplyTile from "./ReplyTile";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
function cancelQuoting(context: TimelineRenderingType) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
action: "reply_to_event",
|
||||
event: null,
|
||||
context,
|
||||
});
|
||||
|
@ -43,20 +43,19 @@ export default class ReplyPreview extends React.Component<IProps> {
|
|||
public render(): JSX.Element | null {
|
||||
if (!this.props.replyToEvent) return null;
|
||||
|
||||
return <div className="mx_ReplyPreview">
|
||||
<div className="mx_ReplyPreview_section">
|
||||
<div className="mx_ReplyPreview_header">
|
||||
<span>{ _t('Replying') }</span>
|
||||
<AccessibleButton
|
||||
className="mx_ReplyPreview_header_cancel"
|
||||
onClick={() => cancelQuoting(this.context.timelineRenderingType)}
|
||||
/>
|
||||
return (
|
||||
<div className="mx_ReplyPreview">
|
||||
<div className="mx_ReplyPreview_section">
|
||||
<div className="mx_ReplyPreview_header">
|
||||
<span>{_t("Replying")}</span>
|
||||
<AccessibleButton
|
||||
className="mx_ReplyPreview_header_cancel"
|
||||
onClick={() => cancelQuoting(this.context.timelineRenderingType)}
|
||||
/>
|
||||
</div>
|
||||
<ReplyTile mxEvent={this.props.replyToEvent} permalinkCreator={this.props.permalinkCreator} />
|
||||
</div>
|
||||
<ReplyTile
|
||||
mxEvent={this.props.replyToEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event';
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import SenderProfile from "../messages/SenderProfile";
|
||||
import MImageReplyBody from "../messages/MImageReplyBody";
|
||||
import { isVoiceMessage } from '../../../utils/EventUtils';
|
||||
import { isVoiceMessage } from "../../../utils/EventUtils";
|
||||
import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils";
|
||||
import MFileBody from "../messages/MFileBody";
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import MVoiceMessageBody from "../messages/MVoiceMessageBody";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { renderReplyTile } from "../../../events/EventTileFactory";
|
||||
|
@ -109,17 +109,20 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
const msgType = mxEvent.getContent().msgtype;
|
||||
const evType = mxEvent.getType();
|
||||
|
||||
const {
|
||||
hasRenderer, isInfoMessage, isSeeingThroughMessageHiddenForModeration,
|
||||
} = getEventDisplayInfo(mxEvent, false /* Replies are never hidden, so this should be fine */);
|
||||
const { hasRenderer, isInfoMessage, isSeeingThroughMessageHiddenForModeration } = getEventDisplayInfo(
|
||||
mxEvent,
|
||||
false /* Replies are never hidden, so this should be fine */,
|
||||
);
|
||||
// This shouldn't happen: the caller should check we support this type
|
||||
// before trying to instantiate us
|
||||
if (!hasRenderer) {
|
||||
const { mxEvent } = this.props;
|
||||
logger.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
|
||||
return <div className="mx_ReplyTile mx_ReplyTile_info mx_MNoticeBody">
|
||||
{ _t('This event could not be displayed') }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_ReplyTile mx_ReplyTile_info mx_MNoticeBody">
|
||||
{_t("This event could not be displayed")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const classes = classNames("mx_ReplyTile", {
|
||||
|
@ -139,15 +142,8 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
if (!hasOwnSender) {
|
||||
sender = (
|
||||
<div className="mx_ReplyTile_sender">
|
||||
<MemberAvatar
|
||||
member={mxEvent.sender}
|
||||
fallbackUserId={mxEvent.getSender()}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
<SenderProfile
|
||||
mxEvent={mxEvent}
|
||||
/>
|
||||
<MemberAvatar member={mxEvent.sender} fallbackUserId={mxEvent.getSender()} width={16} height={16} />
|
||||
<SenderProfile mxEvent={mxEvent} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -166,24 +162,27 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||
return (
|
||||
<div className={classes}>
|
||||
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
|
||||
{ sender }
|
||||
{ renderReplyTile({
|
||||
...this.props,
|
||||
{sender}
|
||||
{renderReplyTile(
|
||||
{
|
||||
...this.props,
|
||||
|
||||
// overrides
|
||||
ref: null,
|
||||
showUrlPreview: false,
|
||||
overrideBodyTypes: msgtypeOverrides,
|
||||
overrideEventTypes: evOverrides,
|
||||
maxImageHeight: 96,
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
// overrides
|
||||
ref: null,
|
||||
showUrlPreview: false,
|
||||
overrideBodyTypes: msgtypeOverrides,
|
||||
overrideEventTypes: evOverrides,
|
||||
maxImageHeight: 96,
|
||||
isSeeingThroughMessageHiddenForModeration,
|
||||
|
||||
// appease TS
|
||||
highlights: this.props.highlights,
|
||||
highlightLink: this.props.highlightLink,
|
||||
onHeightChanged: this.props.onHeightChanged,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
}, false /* showHiddenEvents shouldn't be relevant */) }
|
||||
// appease TS
|
||||
highlights: this.props.highlights,
|
||||
highlightLink: this.props.highlightLink,
|
||||
onHeightChanged: this.props.onHeightChanged,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
},
|
||||
false /* showHiddenEvents shouldn't be relevant */,
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -30,8 +30,7 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
}
|
||||
interface IProps {}
|
||||
|
||||
interface IState {
|
||||
// Both of these control the animation for the breadcrumbs. For details on the
|
||||
|
@ -44,7 +43,7 @@ interface IState {
|
|||
skipFirst: boolean;
|
||||
}
|
||||
|
||||
const RoomBreadcrumbTile = ({ room, onClick }: { room: Room, onClick: (ev: ButtonEvent) => void }) => {
|
||||
const RoomBreadcrumbTile = ({ room, onClick }: { room: Room; onClick: (ev: ButtonEvent) => void }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||
|
||||
return (
|
||||
|
@ -123,23 +122,16 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
|
|||
if (tiles.length > 0) {
|
||||
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
|
||||
return (
|
||||
<CSSTransition
|
||||
appear={true}
|
||||
in={this.state.doAnimation}
|
||||
timeout={640}
|
||||
classNames='mx_RoomBreadcrumbs'
|
||||
>
|
||||
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
|
||||
{ tiles.slice(this.state.skipFirst ? 1 : 0) }
|
||||
<CSSTransition appear={true} in={this.state.doAnimation} timeout={640} classNames="mx_RoomBreadcrumbs">
|
||||
<Toolbar className="mx_RoomBreadcrumbs" aria-label={_t("Recently visited rooms")}>
|
||||
{tiles.slice(this.state.skipFirst ? 1 : 0)}
|
||||
</Toolbar>
|
||||
</CSSTransition>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='mx_RoomBreadcrumbs'>
|
||||
<div className="mx_RoomBreadcrumbs_placeholder">
|
||||
{ _t("No recently visited rooms") }
|
||||
</div>
|
||||
<div className="mx_RoomBreadcrumbs">
|
||||
<div className="mx_RoomBreadcrumbs_placeholder">{_t("No recently visited rooms")}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,10 +27,14 @@ type Props<T extends keyof ReactHTML> = HTMLAttributes<T> & {
|
|||
export function RoomContextDetails<T extends keyof ReactHTML>({ room, component, ...other }: Props<T>) {
|
||||
const contextDetails = roomContextDetails(room);
|
||||
if (contextDetails) {
|
||||
return React.createElement(component ?? "div", {
|
||||
...other,
|
||||
"aria-label": contextDetails.ariaLabel,
|
||||
}, [contextDetails.details]);
|
||||
return React.createElement(
|
||||
component ?? "div",
|
||||
{
|
||||
...other,
|
||||
"aria-label": contextDetails.ariaLabel,
|
||||
},
|
||||
[contextDetails.details],
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -15,38 +15,38 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useMemo, useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import React, { FC, useState, useMemo, useCallback } from "react";
|
||||
import classNames from "classnames";
|
||||
import { throttle } from "lodash";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { ISearchResults } from 'matrix-js-sdk/src/@types/search';
|
||||
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
||||
|
||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserTab";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import RoomHeaderButtons from "../right_panel/RoomHeaderButtons";
|
||||
import E2EIcon from "./E2EIcon";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||
import { IOOBData } from '../../../stores/ThreepidInviteStore';
|
||||
import { SearchScope } from './SearchBar';
|
||||
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import { SearchScope } from "./SearchBar";
|
||||
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import RoomContextMenu from "../context_menus/RoomContextMenu";
|
||||
import { contextMenuBelow } from './RoomTile';
|
||||
import { RoomNotificationStateStore } from '../../../stores/notifications/RoomNotificationStateStore';
|
||||
import { RightPanelPhases } from '../../../stores/right-panel/RightPanelStorePhases';
|
||||
import { NotificationStateEvents } from '../../../stores/notifications/NotificationState';
|
||||
import { contextMenuBelow } from "./RoomTile";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import RoomContext from "../../../contexts/RoomContext";
|
||||
import RoomLiveShareWarning from '../beacon/RoomLiveShareWarning';
|
||||
import RoomLiveShareWarning from "../beacon/RoomLiveShareWarning";
|
||||
import { BetaPill } from "../beta/BetaCard";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
@ -68,10 +68,10 @@ import IconizedContextMenu, {
|
|||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { GroupCallDuration } from "../voip/CallDuration";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import RoomCallBanner from '../beacon/RoomCallBanner';
|
||||
import RoomCallBanner from "../beacon/RoomCallBanner";
|
||||
|
||||
class DisabledWithReason {
|
||||
constructor(public readonly reason: string) { }
|
||||
constructor(public readonly reason: string) {}
|
||||
}
|
||||
|
||||
interface VoiceCallButtonProps {
|
||||
|
@ -93,7 +93,8 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
tooltip: behavior.reason,
|
||||
disabled: true,
|
||||
};
|
||||
} else { // behavior === "legacy_or_jitsi"
|
||||
} else {
|
||||
// behavior === "legacy_or_jitsi"
|
||||
return {
|
||||
onClick: async (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
|
@ -106,14 +107,16 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
}
|
||||
}, [behavior, room, setBusy]);
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Voice call")}
|
||||
tooltip={tooltip ?? _t("Voice call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>;
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Voice call")}
|
||||
tooltip={tooltip ?? _t("Voice call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface VideoCallButtonProps {
|
||||
|
@ -171,7 +174,8 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
},
|
||||
disabled: false,
|
||||
};
|
||||
} else { // behavior === "jitsi_or_element"
|
||||
} else {
|
||||
// behavior === "jitsi_or_element"
|
||||
return {
|
||||
onClick: async (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
|
@ -182,42 +186,55 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
}
|
||||
}, [behavior, startLegacyCall, startElementCall, openMenu]);
|
||||
|
||||
const onJitsiClick = useCallback(async (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
await startLegacyCall();
|
||||
}, [closeMenu, startLegacyCall]);
|
||||
const onJitsiClick = useCallback(
|
||||
async (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
await startLegacyCall();
|
||||
},
|
||||
[closeMenu, startLegacyCall],
|
||||
);
|
||||
|
||||
const onElementClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
startElementCall();
|
||||
}, [closeMenu, startElementCall]);
|
||||
const onElementClick = useCallback(
|
||||
(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
startElementCall();
|
||||
},
|
||||
[closeMenu, startElementCall],
|
||||
);
|
||||
|
||||
let menu: JSX.Element | null = null;
|
||||
if (menuOpen) {
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
menu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||
<IconizedContextMenuOption label={_t("Video call (%(brand)s)", { brand })} onClick={onElementClick} />
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
menu = (
|
||||
<IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Video call (%(brand)s)", { brand })}
|
||||
onClick={onElementClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Video call")}
|
||||
tooltip={tooltip ?? _t("Video call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
{ menu }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Video call")}
|
||||
tooltip={tooltip ?? _t("Video call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
{menu}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface CallButtonsProps {
|
||||
|
@ -243,24 +260,29 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
|||
);
|
||||
|
||||
const widgets = useWidgets(room);
|
||||
const hasJitsiWidget = useMemo(() => widgets.some(widget => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
||||
const hasJitsiWidget = useMemo(() => widgets.some((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
||||
|
||||
const hasGroupCall = useCall(room.roomId) !== null;
|
||||
|
||||
const [functionalMembers, mayEditWidgets, mayCreateElementCalls] = useTypedEventEmitterState(
|
||||
room,
|
||||
RoomStateEvent.Update,
|
||||
useCallback(() => [
|
||||
getJoinedNonFunctionalMembers(room),
|
||||
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
||||
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
|
||||
], [room]),
|
||||
useCallback(
|
||||
() => [
|
||||
getJoinedNonFunctionalMembers(room),
|
||||
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
||||
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
|
||||
],
|
||||
[room],
|
||||
),
|
||||
);
|
||||
|
||||
const makeVoiceCallButton = (behavior: VoiceCallButtonProps["behavior"]): JSX.Element =>
|
||||
<VoiceCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />;
|
||||
const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element =>
|
||||
<VideoCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />;
|
||||
const makeVoiceCallButton = (behavior: VoiceCallButtonProps["behavior"]): JSX.Element => (
|
||||
<VoiceCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
|
||||
);
|
||||
const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element => (
|
||||
<VideoCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
|
||||
);
|
||||
|
||||
if (isVideoRoom || !showButtons) {
|
||||
return null;
|
||||
|
@ -276,54 +298,72 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
|||
);
|
||||
}
|
||||
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
|
||||
return <>
|
||||
{ makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call"))) }
|
||||
{ makeVideoCallButton(new DisabledWithReason(_t("Ongoing call"))) }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length <= 1) {
|
||||
return <>
|
||||
{ makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
|
||||
{ makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length === 2) {
|
||||
return <>
|
||||
{ makeVoiceCallButton("legacy_or_jitsi") }
|
||||
{ makeVideoCallButton("legacy_or_jitsi") }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton("legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else if (mayEditWidgets) {
|
||||
return <>
|
||||
{ makeVoiceCallButton("legacy_or_jitsi") }
|
||||
{ makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi") }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const videoCallBehavior = mayCreateElementCalls
|
||||
? "element"
|
||||
: new DisabledWithReason(_t("You do not have permission to start video calls"));
|
||||
return <>
|
||||
{ makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls"))) }
|
||||
{ makeVideoCallButton(videoCallBehavior) }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||
{makeVideoCallButton(videoCallBehavior)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else if (hasLegacyCall || hasJitsiWidget) {
|
||||
return <>
|
||||
{ makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call"))) }
|
||||
{ makeVideoCallButton(new DisabledWithReason(_t("Ongoing call"))) }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length <= 1) {
|
||||
return <>
|
||||
{ makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
|
||||
{ makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call"))) }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length === 2 || mayEditWidgets) {
|
||||
return <>
|
||||
{ makeVoiceCallButton("legacy_or_jitsi") }
|
||||
{ makeVideoCallButton("legacy_or_jitsi") }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton("legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return <>
|
||||
{ makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls"))) }
|
||||
{ makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls"))) }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls")))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -335,62 +375,75 @@ const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
|
|||
const layout = useLayout(call);
|
||||
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||
|
||||
const onClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
openMenu();
|
||||
}, [openMenu]);
|
||||
const onClick = useCallback(
|
||||
(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
openMenu();
|
||||
},
|
||||
[openMenu],
|
||||
);
|
||||
|
||||
const onFreedomClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Tile);
|
||||
}, [closeMenu, call]);
|
||||
const onFreedomClick = useCallback(
|
||||
(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Tile);
|
||||
},
|
||||
[closeMenu, call],
|
||||
);
|
||||
|
||||
const onSpotlightClick = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Spotlight);
|
||||
}, [closeMenu, call]);
|
||||
const onSpotlightClick = useCallback(
|
||||
(ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
closeMenu();
|
||||
call.setLayout(Layout.Spotlight);
|
||||
},
|
||||
[closeMenu, call],
|
||||
);
|
||||
|
||||
let menu: JSX.Element | null = null;
|
||||
if (menuOpen) {
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
menu = <IconizedContextMenu
|
||||
className="mx_RoomHeader_layoutMenu"
|
||||
{...aboveLeftOf(buttonRect)}
|
||||
onFinished={closeMenu}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_freedomIcon"
|
||||
label={_t("Freedom")}
|
||||
active={layout === Layout.Tile}
|
||||
onClick={onFreedomClick}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_spotlightIcon"
|
||||
label={_t("Spotlight")}
|
||||
active={layout === Layout.Spotlight}
|
||||
onClick={onSpotlightClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
menu = (
|
||||
<IconizedContextMenu
|
||||
className="mx_RoomHeader_layoutMenu"
|
||||
{...aboveLeftOf(buttonRect)}
|
||||
onFinished={closeMenu}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_freedomIcon"
|
||||
label={_t("Freedom")}
|
||||
active={layout === Layout.Tile}
|
||||
onClick={onFreedomClick}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_spotlightIcon"
|
||||
label={_t("Spotlight")}
|
||||
active={layout === Layout.Spotlight}
|
||||
onClick={onSpotlightClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className={classNames("mx_RoomHeader_button", {
|
||||
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
||||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={_t("Change layout")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
{ menu }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className={classNames("mx_RoomHeader_button", {
|
||||
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
||||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={_t("Change layout")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
{menu}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ISearchInfo {
|
||||
|
@ -479,9 +532,13 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
};
|
||||
|
||||
private rateLimitedUpdate = throttle(() => {
|
||||
this.forceUpdate();
|
||||
}, 500, { leading: true, trailing: true });
|
||||
private rateLimitedUpdate = throttle(
|
||||
() => {
|
||||
this.forceUpdate();
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
private onContextMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
|
@ -516,76 +573,90 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onForgetClick) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="forget"
|
||||
/>);
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="forget"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onAppsClick) {
|
||||
startButtons.push(<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")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="apps"
|
||||
/>);
|
||||
startButtons.push(
|
||||
<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")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="apps"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="search"
|
||||
/>);
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="search"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
|
||||
startButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
|
||||
onClick={this.props.onInviteClick}
|
||||
title={_t("Invite")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="invite"
|
||||
/>);
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
|
||||
onClick={this.props.onInviteClick}
|
||||
title={_t("Invite")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="invite"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
const endButtons: JSX.Element[] = [];
|
||||
|
||||
if (this.props.viewingCall && !isVideoRoom) {
|
||||
if (this.props.activeCall === null) {
|
||||
endButtons.push(<AccessibleButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("Close call")}
|
||||
key="close"
|
||||
/>);
|
||||
endButtons.push(
|
||||
<AccessibleButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("Close call")}
|
||||
key="close"
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
endButtons.push(<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("View chat timeline")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="minimise"
|
||||
/>);
|
||||
endButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("View chat timeline")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="minimise"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
{ startButtons }
|
||||
<RoomHeaderButtons
|
||||
room={this.props.room}
|
||||
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
{ endButtons }
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
{startButtons}
|
||||
<RoomHeaderButtons
|
||||
room={this.props.room}
|
||||
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
|
||||
/>
|
||||
{endButtons}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderName(oobName: string) {
|
||||
|
@ -605,22 +676,26 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === this.client.credentials.userId) {
|
||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||
const nameEvent = this.props.room.currentState.getStateEvents("m.room.name", "");
|
||||
if (!nameEvent || !nameEvent.getContent().name) {
|
||||
settingsHint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const roomName = <RoomName room={this.props.room}>
|
||||
{ (name) => {
|
||||
const roomName = name || oobName;
|
||||
return <div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
|
||||
{ roomName }
|
||||
</div>;
|
||||
} }
|
||||
</RoomName>;
|
||||
const textClasses = classNames("mx_RoomHeader_nametext", { mx_RoomHeader_settingsHint: settingsHint });
|
||||
const roomName = (
|
||||
<RoomName room={this.props.room}>
|
||||
{(name) => {
|
||||
const roomName = name || oobName;
|
||||
return (
|
||||
<div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
|
||||
{roomName}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</RoomName>
|
||||
);
|
||||
|
||||
if (this.props.enableRoomOptionsMenu) {
|
||||
return (
|
||||
|
@ -631,16 +706,14 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
title={_t("Room options")}
|
||||
alignment={Alignment.Bottom}
|
||||
>
|
||||
{ roomName }
|
||||
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
||||
{ contextMenu }
|
||||
{roomName}
|
||||
{this.props.room && <div className="mx_RoomHeader_chevron" />}
|
||||
{contextMenu}
|
||||
</ContextMenuTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">
|
||||
{ roomName }
|
||||
</div>;
|
||||
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">{roomName}</div>;
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -648,27 +721,25 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
roomAvatar = (
|
||||
<DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const icon = this.props.viewingCall
|
||||
? <div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
|
||||
: this.props.e2eStatus
|
||||
? <E2EIcon
|
||||
className="mx_RoomHeader_icon"
|
||||
status={this.props.e2eStatus}
|
||||
tooltipAlignment={Alignment.Bottom}
|
||||
/>
|
||||
// If we're expecting an E2EE status to come in, but it hasn't
|
||||
// yet been loaded, insert a blank div to reserve space
|
||||
: this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled()
|
||||
? <div className="mx_RoomHeader_icon" />
|
||||
: null;
|
||||
const icon = this.props.viewingCall ? (
|
||||
<div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
|
||||
) : this.props.e2eStatus ? (
|
||||
<E2EIcon className="mx_RoomHeader_icon" status={this.props.e2eStatus} tooltipAlignment={Alignment.Bottom} />
|
||||
) : // If we're expecting an E2EE status to come in, but it hasn't
|
||||
// yet been loaded, insert a blank div to reserve space
|
||||
this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled() ? (
|
||||
<div className="mx_RoomHeader_icon" />
|
||||
) : null;
|
||||
|
||||
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
|
||||
|
||||
|
@ -679,17 +750,17 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
className="mx_RoomHeader_wrapper"
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ icon }
|
||||
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
|
||||
{icon}
|
||||
<div className="mx_RoomHeader_name mx_RoomHeader_name--textonly mx_RoomHeader_name--small">
|
||||
{ _t("Video call") }
|
||||
{_t("Video call")}
|
||||
</div>
|
||||
{ this.props.activeCall instanceof ElementCall && (
|
||||
{this.props.activeCall instanceof ElementCall && (
|
||||
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
|
||||
) }
|
||||
{ /* Empty topic element to fill out space */ }
|
||||
)}
|
||||
{/* Empty topic element to fill out space */}
|
||||
<div className="mx_RoomHeader_topic" />
|
||||
{ buttons }
|
||||
{buttons}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
@ -700,9 +771,12 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
// don't display the search count until the search completes and
|
||||
// gives us a valid (possibly zero) searchCount.
|
||||
if (typeof this.props.searchInfo?.count === "number") {
|
||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.count }) }
|
||||
</div>;
|
||||
searchStatus = (
|
||||
<div className="mx_RoomHeader_searchStatus">
|
||||
|
||||
{_t("(~%(count)s results)", { count: this.props.searchInfo.count })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let oobName = _t("Join Room");
|
||||
|
@ -712,15 +786,13 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
|
||||
const name = this.renderName(oobName);
|
||||
|
||||
const topicElement = <RoomTopic
|
||||
room={this.props.room}
|
||||
className="mx_RoomHeader_topic"
|
||||
/>;
|
||||
const topicElement = <RoomTopic room={this.props.room} className="mx_RoomHeader_topic" />;
|
||||
|
||||
const viewLabs = () => defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
const viewLabs = () =>
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
const betaPill = isVideoRoom ? (
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
||||
) : null;
|
||||
|
@ -731,15 +803,15 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
className="mx_RoomHeader_wrapper"
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
{ icon }
|
||||
{ name }
|
||||
{ searchStatus }
|
||||
{ topicElement }
|
||||
{ betaPill }
|
||||
{ buttons }
|
||||
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
|
||||
{icon}
|
||||
{name}
|
||||
{searchStatus}
|
||||
{topicElement}
|
||||
{betaPill}
|
||||
{buttons}
|
||||
</div>
|
||||
{ !isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} /> }
|
||||
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
|
||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||
</header>
|
||||
);
|
||||
|
|
|
@ -41,7 +41,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
|
|||
return null;
|
||||
}
|
||||
}, [room]);
|
||||
const joinRule = useRoomState(room, state => state.getJoinRule());
|
||||
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||
const membership = useMyRoomMembership(room);
|
||||
const memberCount = useRoomMemberCount(room);
|
||||
|
||||
|
@ -64,27 +64,31 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
|
|||
let members: JSX.Element;
|
||||
if (membership === "invite" && summary) {
|
||||
// Don't trust local state and instead use the summary API
|
||||
members = <span className="mx_RoomInfoLine_members">
|
||||
{ _t("%(count)s members", { count: summary.num_joined_members }) }
|
||||
</span>;
|
||||
} else if (memberCount && summary !== undefined) { // summary is not still loading
|
||||
const viewMembers = () => RightPanelStore.instance.setCard({
|
||||
phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList,
|
||||
});
|
||||
members = (
|
||||
<span className="mx_RoomInfoLine_members">
|
||||
{_t("%(count)s members", { count: summary.num_joined_members })}
|
||||
</span>
|
||||
);
|
||||
} else if (memberCount && summary !== undefined) {
|
||||
// summary is not still loading
|
||||
const viewMembers = () =>
|
||||
RightPanelStore.instance.setCard({
|
||||
phase: room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList,
|
||||
});
|
||||
|
||||
members = <AccessibleButton
|
||||
kind="link"
|
||||
className="mx_RoomInfoLine_members"
|
||||
onClick={viewMembers}
|
||||
>
|
||||
{ _t("%(count)s members", { count: memberCount }) }
|
||||
</AccessibleButton>;
|
||||
members = (
|
||||
<AccessibleButton kind="link" className="mx_RoomInfoLine_members" onClick={viewMembers}>
|
||||
{_t("%(count)s members", { count: memberCount })}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={`mx_RoomInfoLine ${iconClass}`}>
|
||||
{ roomType }
|
||||
{ members }
|
||||
</div>;
|
||||
return (
|
||||
<div className={`mx_RoomInfoLine ${iconClass}`}>
|
||||
{roomType}
|
||||
{members}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomInfoLine;
|
||||
|
|
|
@ -92,10 +92,7 @@ export const TAG_ORDER: TagID[] = [
|
|||
DefaultTagID.Suggested,
|
||||
DefaultTagID.Archived,
|
||||
];
|
||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [
|
||||
DefaultTagID.DM,
|
||||
DefaultTagID.Untagged,
|
||||
];
|
||||
const ALWAYS_VISIBLE_TAGS: TagID[] = [DefaultTagID.DM, DefaultTagID.Untagged];
|
||||
|
||||
interface ITagAesthetics {
|
||||
sectionLabel: string;
|
||||
|
@ -133,62 +130,78 @@ const DmAuxButton = ({ tabIndex, dispatcher = defaultDispatcher }: IAuxButtonPro
|
|||
if (menuDisplayed) {
|
||||
const canInvite = shouldShowSpaceInvite(activeSpace);
|
||||
|
||||
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ showCreateRooms && <IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomList_iconStartChat"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
|
||||
}}
|
||||
/> }
|
||||
{ showInviteUsers && <IconizedContextMenuOption
|
||||
label={_t("Invite to space")}
|
||||
iconClassName="mx_RoomList_iconInvite"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
showSpaceInvite(activeSpace);
|
||||
}}
|
||||
disabled={!canInvite}
|
||||
tooltip={canInvite ? undefined
|
||||
: _t("You do not have permissions to invite people to this space")}
|
||||
/> }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
contextMenu = (
|
||||
<IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{showCreateRooms && (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomList_iconStartChat"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
||||
PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomsSublistPlusMenuCreateChatItem",
|
||||
e,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showInviteUsers && (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Invite to space")}
|
||||
iconClassName="mx_RoomList_iconInvite"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
showSpaceInvite(activeSpace);
|
||||
}}
|
||||
disabled={!canInvite}
|
||||
tooltip={
|
||||
canInvite
|
||||
? undefined
|
||||
: _t("You do not have permissions to invite people to this space")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<ContextMenuTooltipButton
|
||||
return (
|
||||
<>
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={openMenu}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_t("Add people")}
|
||||
title={_t("Add people")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={handle}
|
||||
/>
|
||||
|
||||
{contextMenu}
|
||||
</>
|
||||
);
|
||||
} else if (!activeSpace && showCreateRooms) {
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={openMenu}
|
||||
onClick={(e) => {
|
||||
dispatcher.dispatch({ action: "view_create_chat" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
|
||||
}}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_t("Add people")}
|
||||
title={_t("Add people")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={handle}
|
||||
aria-label={_t("Start chat")}
|
||||
title={_t("Start chat")}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</>;
|
||||
} else if (!activeSpace && showCreateRooms) {
|
||||
return <AccessibleTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={(e) => {
|
||||
dispatcher.dispatch({ action: 'view_create_chat' });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
|
||||
}}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_t("Start chat")}
|
||||
title={_t("Start chat")}
|
||||
/>;
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -206,28 +219,30 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
|
||||
let contextMenuContent: JSX.Element | null = null;
|
||||
if (menuDisplayed && activeSpace) {
|
||||
const canAddRooms = activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
|
||||
MatrixClientPeg.get().getUserId());
|
||||
const canAddRooms = activeSpace.currentState.maySendStateEvent(
|
||||
EventType.SpaceChild,
|
||||
MatrixClientPeg.get().getUserId(),
|
||||
);
|
||||
|
||||
contextMenuContent = <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: activeSpace.roomId,
|
||||
metricsTrigger: undefined, // other
|
||||
});
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
|
||||
}}
|
||||
/>
|
||||
{
|
||||
showCreateRoom
|
||||
? (<>
|
||||
contextMenuContent = (
|
||||
<IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: activeSpace.roomId,
|
||||
metricsTrigger: undefined, // other
|
||||
});
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
|
||||
}}
|
||||
/>
|
||||
{showCreateRoom ? (
|
||||
<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconNewRoom"
|
||||
|
@ -239,10 +254,13 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
tooltip={
|
||||
canAddRooms
|
||||
? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")
|
||||
}
|
||||
/>
|
||||
{ videoRoomsEnabled && (
|
||||
{videoRoomsEnabled && (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
|
@ -256,12 +274,15 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
tooltip={
|
||||
canAddRooms
|
||||
? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")
|
||||
}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
) }
|
||||
)}
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconAddExistingRoom"
|
||||
|
@ -272,80 +293,91 @@ const UntaggedAuxButton = ({ tabIndex }: IAuxButtonProps) => {
|
|||
showAddExistingRooms(activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to add rooms to this space")}
|
||||
tooltip={
|
||||
canAddRooms ? undefined : _t("You do not have permissions to add rooms to this space")
|
||||
}
|
||||
/>
|
||||
</>)
|
||||
: null
|
||||
}
|
||||
</IconizedContextMenuOptionList>;
|
||||
</>
|
||||
) : null}
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
} else if (menuDisplayed) {
|
||||
contextMenuContent = <IconizedContextMenuOptionList first>
|
||||
{ showCreateRoom && <>
|
||||
contextMenuContent = (
|
||||
<IconizedContextMenuOptionList first>
|
||||
{showCreateRoom && (
|
||||
<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
}}
|
||||
/>
|
||||
{videoRoomsEnabled && (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: elementCallVideoRoomsEnabled
|
||||
? RoomType.UnstableCall
|
||||
: RoomType.ElementVideo,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomList_iconNewRoom"
|
||||
label={_t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomList_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
) }
|
||||
</> }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore public rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
closeMenu();
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
</IconizedContextMenuOptionList>
|
||||
);
|
||||
}
|
||||
|
||||
let contextMenu: JSX.Element | null = null;
|
||||
if (menuDisplayed) {
|
||||
contextMenu = <IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
|
||||
{ contextMenuContent }
|
||||
</IconizedContextMenu>;
|
||||
contextMenu = (
|
||||
<IconizedContextMenu {...auxButtonContextMenuPosition(handle)} onFinished={closeMenu} compact>
|
||||
{contextMenuContent}
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={openMenu}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_t("Add room")}
|
||||
title={_t("Add room")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={handle}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<ContextMenuTooltipButton
|
||||
tabIndex={tabIndex}
|
||||
onClick={openMenu}
|
||||
className="mx_RoomSublist_auxButton"
|
||||
tooltipClassName="mx_RoomSublist_addRoomTooltip"
|
||||
aria-label={_t("Add room")}
|
||||
title={_t("Add room")}
|
||||
isExpanded={menuDisplayed}
|
||||
inputRef={handle}
|
||||
/>
|
||||
|
||||
{ contextMenu }
|
||||
</>;
|
||||
{contextMenu}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const TAG_AESTHETICS: ITagAestheticsMap = {
|
||||
|
@ -424,10 +456,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||
this.favouriteMessageWatcher =
|
||||
SettingsStore.watchSetting("feature_favourite_messages", null, (...[,,, value]) => {
|
||||
this.favouriteMessageWatcher = SettingsStore.watchSetting(
|
||||
"feature_favourite_messages",
|
||||
null,
|
||||
(...[, , , value]) => {
|
||||
this.setState({ feature_favourite_messages: value });
|
||||
});
|
||||
},
|
||||
);
|
||||
this.updateLists(); // trigger the first update
|
||||
}
|
||||
|
||||
|
@ -467,12 +502,12 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
private getRoomDelta = (roomId: string, delta: number, unread = false) => {
|
||||
const lists = RoomListStore.instance.orderedLists;
|
||||
const rooms: Room[] = [];
|
||||
TAG_ORDER.forEach(t => {
|
||||
TAG_ORDER.forEach((t) => {
|
||||
let listRooms = lists[t];
|
||||
|
||||
if (unread) {
|
||||
// filter to only notification rooms (and our current active room so we can index properly)
|
||||
listRooms = listRooms.filter(r => {
|
||||
listRooms = listRooms.filter((r) => {
|
||||
const state = RoomNotificationStateStore.instance.getRoomState(r);
|
||||
return state.room.roomId === roomId || state.isUnread;
|
||||
});
|
||||
|
@ -481,7 +516,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
rooms.push(...listRooms);
|
||||
});
|
||||
|
||||
const currentIndex = rooms.findIndex(r => r.roomId === roomId);
|
||||
const currentIndex = rooms.findIndex((r) => r.roomId === roomId);
|
||||
// use slice to account for looping around the start
|
||||
const [room] = rooms.slice((currentIndex + delta) % rooms.length);
|
||||
return room;
|
||||
|
@ -525,7 +560,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
return this.state.suggestedRooms.map(room => {
|
||||
return this.state.suggestedRooms.map((room) => {
|
||||
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Empty room");
|
||||
const avatar = (
|
||||
<RoomAvatar
|
||||
|
@ -573,7 +608,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
width={32}
|
||||
height={32}
|
||||
resizeMethod="crop"
|
||||
/>);
|
||||
/>
|
||||
);
|
||||
|
||||
return [
|
||||
<ExtraTile
|
||||
|
@ -589,45 +625,44 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private renderSublists(): React.ReactElement[] {
|
||||
// show a skeleton UI if the user is in no rooms and they are not filtering and have no suggested rooms
|
||||
const showSkeleton = !this.state.suggestedRooms?.length &&
|
||||
Object.values(RoomListStore.instance.orderedLists).every(list => !list?.length);
|
||||
const showSkeleton =
|
||||
!this.state.suggestedRooms?.length &&
|
||||
Object.values(RoomListStore.instance.orderedLists).every((list) => !list?.length);
|
||||
|
||||
return TAG_ORDER
|
||||
.map(orderedTagId => {
|
||||
let extraTiles = null;
|
||||
if (orderedTagId === DefaultTagID.Suggested) {
|
||||
extraTiles = this.renderSuggestedRooms();
|
||||
} else if (this.state.feature_favourite_messages && orderedTagId === DefaultTagID.SavedItems) {
|
||||
extraTiles = this.renderFavoriteMessagesList();
|
||||
}
|
||||
return TAG_ORDER.map((orderedTagId) => {
|
||||
let extraTiles = null;
|
||||
if (orderedTagId === DefaultTagID.Suggested) {
|
||||
extraTiles = this.renderSuggestedRooms();
|
||||
} else if (this.state.feature_favourite_messages && orderedTagId === DefaultTagID.SavedItems) {
|
||||
extraTiles = this.renderFavoriteMessagesList();
|
||||
}
|
||||
|
||||
const aesthetics = TAG_AESTHETICS[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
const aesthetics = TAG_AESTHETICS[orderedTagId];
|
||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||
|
||||
let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId);
|
||||
if (
|
||||
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) ||
|
||||
(this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) ||
|
||||
(this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM) ||
|
||||
(
|
||||
!isMetaSpace(this.props.activeSpace) &&
|
||||
orderedTagId === DefaultTagID.DM &&
|
||||
!SettingsStore.getValue("Spaces.showPeopleInSpace", this.props.activeSpace)
|
||||
)
|
||||
) {
|
||||
alwaysVisible = false;
|
||||
}
|
||||
let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId);
|
||||
if (
|
||||
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) ||
|
||||
(this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) ||
|
||||
(this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM) ||
|
||||
(!isMetaSpace(this.props.activeSpace) &&
|
||||
orderedTagId === DefaultTagID.DM &&
|
||||
!SettingsStore.getValue("Spaces.showPeopleInSpace", this.props.activeSpace))
|
||||
) {
|
||||
alwaysVisible = false;
|
||||
}
|
||||
|
||||
let forceExpanded = false;
|
||||
if (
|
||||
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId === DefaultTagID.Favourite) ||
|
||||
(this.props.activeSpace === MetaSpace.People && orderedTagId === DefaultTagID.DM)
|
||||
) {
|
||||
forceExpanded = true;
|
||||
}
|
||||
// The cost of mounting/unmounting this component offsets the cost
|
||||
// of keeping it in the DOM and hiding it when it is not required
|
||||
return <RoomSublist
|
||||
let forceExpanded = false;
|
||||
if (
|
||||
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId === DefaultTagID.Favourite) ||
|
||||
(this.props.activeSpace === MetaSpace.People && orderedTagId === DefaultTagID.DM)
|
||||
) {
|
||||
forceExpanded = true;
|
||||
}
|
||||
// The cost of mounting/unmounting this component offsets the cost
|
||||
// of keeping it in the DOM and hiding it when it is not required
|
||||
return (
|
||||
<RoomSublist
|
||||
key={`sublist-${orderedTagId}`}
|
||||
tagId={orderedTagId}
|
||||
forRooms={true}
|
||||
|
@ -641,22 +676,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
alwaysVisible={alwaysVisible}
|
||||
onListCollapse={this.props.onListCollapse}
|
||||
forceExpanded={forceExpanded}
|
||||
/>;
|
||||
});
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
// focus the first focusable element in this aria treeview widget
|
||||
const treeItems = this.treeRef.current?.querySelectorAll<HTMLElement>('[role="treeitem"]');
|
||||
if (!treeItems) return;
|
||||
[...treeItems].find(e => e.offsetParent !== null)?.focus();
|
||||
[...treeItems].find((e) => e.offsetParent !== null)?.focus();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const sublists = this.renderSublists();
|
||||
return (
|
||||
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.props.onKeyDown}>
|
||||
{ ({ onKeyDownHandler }) => (
|
||||
{({ onKeyDownHandler }) => (
|
||||
<div
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
|
@ -666,9 +702,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
aria-label={_t("Rooms")}
|
||||
ref={this.treeRef}
|
||||
>
|
||||
{ sublists }
|
||||
{sublists}
|
||||
</div>
|
||||
) }
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ const usePendingActions = (): Map<PendingActionType, Set<string>> => {
|
|||
}
|
||||
};
|
||||
|
||||
useDispatcher(defaultDispatcher, payload => {
|
||||
useDispatcher(defaultDispatcher, (payload) => {
|
||||
switch (payload.action) {
|
||||
case Action.JoinRoom:
|
||||
addAction(PendingActionType.JoinRoom, payload.roomId);
|
||||
|
@ -103,9 +103,7 @@ const usePendingActions = (): Map<PendingActionType, Set<string>> => {
|
|||
break;
|
||||
}
|
||||
});
|
||||
useTypedEventEmitter(cli, ClientEvent.Room, (room: Room) =>
|
||||
removeAction(PendingActionType.JoinRoom, room.roomId),
|
||||
);
|
||||
useTypedEventEmitter(cli, ClientEvent.Room, (room: Room) => removeAction(PendingActionType.JoinRoom, room.roomId));
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
@ -151,8 +149,10 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
const canCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
|
||||
const canCreateSpaces = shouldShowComponent(UIComponent.CreateSpaces);
|
||||
|
||||
const hasPermissionToAddSpaceChild =
|
||||
activeSpace?.currentState?.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||
const hasPermissionToAddSpaceChild = activeSpace?.currentState?.maySendStateEvent(
|
||||
EventType.SpaceChild,
|
||||
cli.getUserId(),
|
||||
);
|
||||
const canAddSubRooms = hasPermissionToAddSpaceChild && canCreateRooms;
|
||||
const canAddSubSpaces = hasPermissionToAddSpaceChild && canCreateSpaces;
|
||||
|
||||
|
@ -170,159 +170,170 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
ContextMenuComponent = HomeButtonContextMenu;
|
||||
}
|
||||
|
||||
contextMenu = <ContextMenuComponent
|
||||
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
|
||||
space={activeSpace}
|
||||
onFinished={closeMainMenu}
|
||||
hideHeader={true}
|
||||
/>;
|
||||
contextMenu = (
|
||||
<ContextMenuComponent
|
||||
{...contextMenuBelow(mainMenuHandle.current.getBoundingClientRect())}
|
||||
space={activeSpace}
|
||||
onFinished={closeMainMenu}
|
||||
hideHeader={true}
|
||||
/>
|
||||
);
|
||||
} else if (plusMenuDisplayed && activeSpace) {
|
||||
let inviteOption: JSX.Element;
|
||||
if (shouldShowSpaceInvite(activeSpace)) {
|
||||
inviteOption = <IconizedContextMenuOption
|
||||
label={_t("Invite")}
|
||||
iconClassName="mx_RoomListHeader_iconInvite"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showSpaceInvite(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>;
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Invite")}
|
||||
iconClassName="mx_RoomListHeader_iconInvite"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showSpaceInvite(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let newRoomOptions: JSX.Element;
|
||||
if (activeSpace?.currentState.maySendStateEvent(EventType.RoomAvatar, cli.getUserId())) {
|
||||
newRoomOptions = <>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
label={_t("New room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(activeSpace);
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && (
|
||||
newRoomOptions = (
|
||||
<>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
label={_t("New room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(
|
||||
activeSpace,
|
||||
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
|
||||
);
|
||||
showCreateNewRoom(activeSpace);
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
) }
|
||||
</>;
|
||||
/>
|
||||
{videoRoomsEnabled && (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
label={_t("New video room")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewRoom(
|
||||
activeSpace,
|
||||
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
|
||||
);
|
||||
closePlusMenu();
|
||||
}}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
|
||||
onFinished={closePlusMenu}
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
{ newRoomOptions }
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomListHeader_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: activeSpace.roomId,
|
||||
metricsTrigger: undefined, // other
|
||||
});
|
||||
closePlusMenu();
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e);
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomListHeader_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showAddExistingRooms(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
disabled={!canAddSubRooms}
|
||||
tooltip={!canAddSubRooms && _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
{ canCreateSpaces && <IconizedContextMenuOption
|
||||
label={_t("Add space")}
|
||||
iconClassName="mx_RoomListHeader_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewSubspace(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
disabled={!canAddSubSpaces}
|
||||
tooltip={!canAddSubSpaces && _t("You do not have permissions to add spaces to this space")}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
}
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
contextMenu = (
|
||||
<IconizedContextMenu
|
||||
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
|
||||
onFinished={closePlusMenu}
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{inviteOption}
|
||||
{newRoomOptions}
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomListHeader_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: activeSpace.roomId,
|
||||
metricsTrigger: undefined, // other
|
||||
});
|
||||
closePlusMenu();
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuExploreRoomsItem", e);
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomListHeader_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showAddExistingRooms(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
disabled={!canAddSubRooms}
|
||||
tooltip={!canAddSubRooms && _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
{canCreateSpaces && (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add space")}
|
||||
iconClassName="mx_RoomListHeader_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showCreateNewSubspace(activeSpace);
|
||||
closePlusMenu();
|
||||
}}
|
||||
disabled={!canAddSubSpaces}
|
||||
tooltip={!canAddSubSpaces && _t("You do not have permissions to add spaces to this space")}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
)}
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
} else if (plusMenuDisplayed) {
|
||||
let newRoomOpts: JSX.Element;
|
||||
let joinRoomOpt: JSX.Element;
|
||||
|
||||
if (canCreateRooms) {
|
||||
newRoomOpts = <>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomListHeader_iconStartChat"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
{ videoRoomsEnabled && (
|
||||
newRoomOpts = (
|
||||
<>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
label={_t("Start new chat")}
|
||||
iconClassName="mx_RoomListHeader_iconStartChat"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
|
||||
});
|
||||
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
) }
|
||||
</>;
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({ action: "view_create_room" });
|
||||
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
|
||||
closePlusMenu();
|
||||
}}
|
||||
/>
|
||||
{videoRoomsEnabled && (
|
||||
<IconizedContextMenuOption
|
||||
label={_t("New video room")}
|
||||
iconClassName="mx_RoomListHeader_iconNewVideoRoom"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_create_room",
|
||||
type: elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
|
||||
});
|
||||
closePlusMenu();
|
||||
}}
|
||||
>
|
||||
<BetaPill />
|
||||
</IconizedContextMenuOption>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (canExploreRooms) {
|
||||
joinRoomOpt = (
|
||||
|
@ -340,16 +351,18 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
|
||||
onFinished={closePlusMenu}
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ newRoomOpts }
|
||||
{ joinRoomOpt }
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
contextMenu = (
|
||||
<IconizedContextMenu
|
||||
{...contextMenuBelow(plusMenuHandle.current.getBoundingClientRect())}
|
||||
onFinished={closePlusMenu}
|
||||
compact
|
||||
>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{newRoomOpts}
|
||||
{joinRoomOpt}
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
let title: string;
|
||||
|
@ -371,36 +384,46 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
|
|||
})
|
||||
.join("\n");
|
||||
|
||||
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{ title }</div>;
|
||||
let contextMenuButton: JSX.Element = <div className="mx_RoomListHeader_contextLessTitle">{title}</div>;
|
||||
if (canShowMainMenu) {
|
||||
contextMenuButton = <ContextMenuTooltipButton
|
||||
inputRef={mainMenuHandle}
|
||||
onClick={openMainMenu}
|
||||
isExpanded={mainMenuDisplayed}
|
||||
className="mx_RoomListHeader_contextMenuButton"
|
||||
title={activeSpace
|
||||
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
|
||||
: _t("Home options")}
|
||||
>
|
||||
{ title }
|
||||
</ContextMenuTooltipButton>;
|
||||
contextMenuButton = (
|
||||
<ContextMenuTooltipButton
|
||||
inputRef={mainMenuHandle}
|
||||
onClick={openMainMenu}
|
||||
isExpanded={mainMenuDisplayed}
|
||||
className="mx_RoomListHeader_contextMenuButton"
|
||||
title={
|
||||
activeSpace
|
||||
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
|
||||
: _t("Home options")
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</ContextMenuTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_RoomListHeader">
|
||||
{ contextMenuButton }
|
||||
{ pendingActionSummary ?
|
||||
<TooltipTarget label={pendingActionSummary}><InlineSpinner /></TooltipTarget> :
|
||||
null }
|
||||
{ canShowPlusMenu && <ContextMenuTooltipButton
|
||||
inputRef={plusMenuHandle}
|
||||
onClick={openPlusMenu}
|
||||
isExpanded={plusMenuDisplayed}
|
||||
className="mx_RoomListHeader_plusButton"
|
||||
title={_t("Add")}
|
||||
/> }
|
||||
return (
|
||||
<div className="mx_RoomListHeader">
|
||||
{contextMenuButton}
|
||||
{pendingActionSummary ? (
|
||||
<TooltipTarget label={pendingActionSummary}>
|
||||
<InlineSpinner />
|
||||
</TooltipTarget>
|
||||
) : null}
|
||||
{canShowPlusMenu && (
|
||||
<ContextMenuTooltipButton
|
||||
inputRef={plusMenuHandle}
|
||||
onClick={openPlusMenu}
|
||||
isExpanded={plusMenuDisplayed}
|
||||
className="mx_RoomListHeader_plusButton"
|
||||
title={_t("Add")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{ contextMenu }
|
||||
</div>;
|
||||
{contextMenu}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomListHeader;
|
||||
|
|
|
@ -20,17 +20,14 @@ import { MatrixError } from "matrix-js-sdk/src/http-api";
|
|||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
RoomPreviewOpts,
|
||||
RoomViewLifecycle,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import classNames from "classnames";
|
||||
import { RoomPreviewOpts, RoomViewLifecycle } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||
import InviteReason from "../elements/InviteReason";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
@ -135,7 +132,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
// Gather the account 3PIDs
|
||||
const account3pids = await MatrixClientPeg.get().getThreePids();
|
||||
this.setState({
|
||||
accountEmails: account3pids.threepids.filter(b => b.medium === 'email').map(b => b.address),
|
||||
accountEmails: account3pids.threepids.filter((b) => b.medium === "email").map((b) => b.address),
|
||||
});
|
||||
// If we have an IS connected, use that to lookup the email and
|
||||
// check the bound MXID.
|
||||
|
@ -146,7 +143,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
const authClient = new IdentityAuthClient();
|
||||
const identityAccessToken = await authClient.getAccessToken();
|
||||
const result = await MatrixClientPeg.get().lookupThreePid(
|
||||
'email',
|
||||
"email",
|
||||
this.props.invitedEmail,
|
||||
identityAccessToken,
|
||||
);
|
||||
|
@ -187,10 +184,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
if (this.props.invitedEmail) {
|
||||
if (this.state.threePidFetchError) {
|
||||
return MessageCase.OtherThreePIDError;
|
||||
} else if (
|
||||
this.state.accountEmails &&
|
||||
!this.state.accountEmails.includes(this.props.invitedEmail)
|
||||
) {
|
||||
} else if (this.state.accountEmails && !this.state.accountEmails.includes(this.props.invitedEmail)) {
|
||||
return MessageCase.InvitedEmailNotFoundInAccount;
|
||||
} else if (!MatrixClientPeg.get().getIdentityServerUrl()) {
|
||||
return MessageCase.InvitedEmailNoIdentityServer;
|
||||
|
@ -200,7 +194,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
return MessageCase.Invite;
|
||||
} else if (this.props.error) {
|
||||
if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
|
||||
if ((this.props.error as MatrixError).errcode == "M_NOT_FOUND") {
|
||||
return MessageCase.RoomNotFound;
|
||||
} else {
|
||||
return MessageCase.OtherError;
|
||||
|
@ -210,23 +204,21 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private getKickOrBanInfo(): { memberName?: string, reason?: string } {
|
||||
private getKickOrBanInfo(): { memberName?: string; reason?: string } {
|
||||
const myMember = this.getMyMember();
|
||||
if (!myMember) {
|
||||
return {};
|
||||
}
|
||||
const kickerMember = this.props.room.currentState.getMember(
|
||||
myMember.events.member.getSender(),
|
||||
);
|
||||
const memberName = kickerMember ?
|
||||
kickerMember.name : myMember.events.member.getSender();
|
||||
const kickerMember = this.props.room.currentState.getMember(myMember.events.member.getSender());
|
||||
const memberName = kickerMember ? kickerMember.name : myMember.events.member.getSender();
|
||||
const reason = myMember.events.member.getContent().reason;
|
||||
return { memberName, reason };
|
||||
}
|
||||
|
||||
private joinRule(): JoinRule {
|
||||
return this.props.room?.currentState
|
||||
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
|
||||
.getStateEvents(EventType.RoomJoinRules, "")
|
||||
?.getContent<IJoinRuleEventContent>().join_rule;
|
||||
}
|
||||
|
||||
private getMyMember(): RoomMember {
|
||||
|
@ -257,9 +249,9 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||
}
|
||||
|
||||
private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
|
||||
private makeScreenAfterLogin(): { screen: string; params: Record<string, any> } {
|
||||
return {
|
||||
screen: 'room',
|
||||
screen: "room",
|
||||
params: {
|
||||
email: this.props.invitedEmail,
|
||||
signurl: this.props.signUrl,
|
||||
|
@ -271,11 +263,11 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onLoginClick = () => {
|
||||
dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
dis.dispatch({ action: "start_login", screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
private onRegisterClick = () => {
|
||||
dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
dis.dispatch({ action: "start_registration", screenAfterLogin: this.makeScreenAfterLogin() });
|
||||
};
|
||||
|
||||
render() {
|
||||
|
@ -319,8 +311,11 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
case MessageCase.NotLoggedIn: {
|
||||
const opts: RoomPreviewOpts = { canJoin: false };
|
||||
if (this.props.room?.roomId) {
|
||||
ModuleRunner.instance
|
||||
.invoke(RoomViewLifecycle.PreviewRoomNotLoggedIn, opts, this.props.room.roomId);
|
||||
ModuleRunner.instance.invoke(
|
||||
RoomViewLifecycle.PreviewRoomNotLoggedIn,
|
||||
opts,
|
||||
this.props.room.roomId,
|
||||
);
|
||||
}
|
||||
if (opts.canJoin) {
|
||||
title = _t("Join the room to participate");
|
||||
|
@ -341,7 +336,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
footer = (
|
||||
<div>
|
||||
<Spinner w={20} h={20} />
|
||||
{ _t("Loading preview") }
|
||||
{_t("Loading preview")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -350,8 +345,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
case MessageCase.Kicked: {
|
||||
const { memberName, reason } = this.getKickOrBanInfo();
|
||||
if (roomName) {
|
||||
title = _t("You were removed from %(roomName)s by %(memberName)s",
|
||||
{ memberName, roomName });
|
||||
title = _t("You were removed from %(roomName)s by %(memberName)s", { memberName, roomName });
|
||||
} else {
|
||||
title = _t("You were removed by %(memberName)s", { memberName });
|
||||
}
|
||||
|
@ -398,15 +392,12 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
const joinRule = this.joinRule();
|
||||
const errCodeMessage = _t(
|
||||
"An error (%(errcode)s) was returned while trying to validate your " +
|
||||
"invite. You could try to pass this information on to the person who invited you.",
|
||||
"invite. You could try to pass this information on to the person who invited you.",
|
||||
{ errcode: this.state.threePidFetchError.errcode || _t("unknown error code") },
|
||||
);
|
||||
switch (joinRule) {
|
||||
case "invite":
|
||||
subTitle = [
|
||||
_t("You can only join it with a working invite."),
|
||||
errCodeMessage,
|
||||
];
|
||||
subTitle = [_t("You can only join it with a working invite."), errCodeMessage];
|
||||
primaryActionLabel = _t("Try to join anyway");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
|
@ -427,22 +418,20 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
if (roomName) {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s which is not " +
|
||||
"associated with your account",
|
||||
"associated with your account",
|
||||
{
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
title = _t(
|
||||
"This invite was sent to %(email)s which is not associated with your account",
|
||||
{ email: this.props.invitedEmail },
|
||||
);
|
||||
title = _t("This invite was sent to %(email)s which is not associated with your account", {
|
||||
email: this.props.invitedEmail,
|
||||
});
|
||||
}
|
||||
|
||||
subTitle = _t(
|
||||
"Link this email with your account in Settings to receive invites " +
|
||||
"directly in %(brand)s.",
|
||||
"Link this email with your account in Settings to receive invites " + "directly in %(brand)s.",
|
||||
{ brand },
|
||||
);
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
|
@ -451,42 +440,32 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
}
|
||||
case MessageCase.InvitedEmailNoIdentityServer: {
|
||||
if (roomName) {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
title = _t("This invite to %(roomName)s was sent to %(email)s", {
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
});
|
||||
} else {
|
||||
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
|
||||
}
|
||||
|
||||
subTitle = _t(
|
||||
"Use an identity server in Settings to receive invites directly in %(brand)s.",
|
||||
{ brand },
|
||||
);
|
||||
subTitle = _t("Use an identity server in Settings to receive invites directly in %(brand)s.", {
|
||||
brand,
|
||||
});
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailMismatch: {
|
||||
if (roomName) {
|
||||
title = _t(
|
||||
"This invite to %(roomName)s was sent to %(email)s",
|
||||
{
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
},
|
||||
);
|
||||
title = _t("This invite to %(roomName)s was sent to %(email)s", {
|
||||
roomName,
|
||||
email: this.props.invitedEmail,
|
||||
});
|
||||
} else {
|
||||
title = _t("This invite was sent to %(email)s", { email: this.props.invitedEmail });
|
||||
}
|
||||
|
||||
subTitle = _t(
|
||||
"Share this email in Settings to receive invites directly in %(brand)s.",
|
||||
{ brand },
|
||||
);
|
||||
subTitle = _t("Share this email in Settings to receive invites directly in %(brand)s.", { brand });
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
break;
|
||||
|
@ -497,29 +476,24 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
const inviteMember = this.getInviteMember();
|
||||
let inviterElement;
|
||||
if (inviteMember) {
|
||||
inviterElement = <span>
|
||||
<span className="mx_RoomPreviewBar_inviter">
|
||||
{ inviteMember.rawDisplayName }
|
||||
</span> ({ inviteMember.userId })
|
||||
</span>;
|
||||
inviterElement = (
|
||||
<span>
|
||||
<span className="mx_RoomPreviewBar_inviter">{inviteMember.rawDisplayName}</span> (
|
||||
{inviteMember.userId})
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
|
||||
inviterElement = <span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>;
|
||||
}
|
||||
|
||||
const isDM = this.isDMInvite();
|
||||
if (isDM) {
|
||||
title = _t("Do you want to chat with %(user)s?", { user: inviteMember.name });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> wants to chat", {}, { userName: () => inviterElement }),
|
||||
];
|
||||
subTitle = [avatar, _t("<userName/> wants to chat", {}, { userName: () => inviterElement })];
|
||||
primaryActionLabel = _t("Start chatting");
|
||||
} else {
|
||||
title = _t("Do you want to join %(roomName)s?", { roomName });
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
|
||||
];
|
||||
subTitle = [avatar, _t("<userName/> invited you", {}, { userName: () => inviterElement })];
|
||||
primaryActionLabel = _t("Accept");
|
||||
}
|
||||
|
||||
|
@ -527,10 +501,12 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.getContent();
|
||||
|
||||
if (memberEventContent.reason) {
|
||||
reasonElement = <InviteReason
|
||||
reason={memberEventContent.reason}
|
||||
htmlReason={memberEventContent[MemberEventHtmlReasonField]}
|
||||
/>;
|
||||
reasonElement = (
|
||||
<InviteReason
|
||||
reason={memberEventContent.reason}
|
||||
htmlReason={memberEventContent[MemberEventHtmlReasonField]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
|
@ -540,7 +516,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
if (this.props.onRejectAndIgnoreClick) {
|
||||
extraComponents.push(
|
||||
<AccessibleButton kind="secondary" onClick={this.props.onRejectAndIgnoreClick} key="ignore">
|
||||
{ _t("Reject & Ignore user") }
|
||||
{_t("Reject & Ignore user")}
|
||||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
|
@ -577,13 +553,20 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
_t("Try again later, or ask a room or space admin to check if you have access."),
|
||||
_t(
|
||||
"%(errcode)s was returned while trying to access the room or space. " +
|
||||
"If you think you're seeing this message in error, please " +
|
||||
"<issueLink>submit a bug report</issueLink>.",
|
||||
"If you think you're seeing this message in error, please " +
|
||||
"<issueLink>submit a bug report</issueLink>.",
|
||||
{ errcode: this.props.error.errcode },
|
||||
{ issueLink: label => <a
|
||||
href="https://github.com/vector-im/element-web/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{ label }</a> },
|
||||
{
|
||||
issueLink: (label) => (
|
||||
<a
|
||||
href="https://github.com/vector-im/element-web/issues/new/choose"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
),
|
||||
},
|
||||
),
|
||||
];
|
||||
break;
|
||||
|
@ -595,21 +578,26 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
if (!Array.isArray(subTitle)) {
|
||||
subTitle = [subTitle];
|
||||
}
|
||||
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{ t }</p>);
|
||||
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{t}</p>);
|
||||
}
|
||||
|
||||
let titleElement;
|
||||
if (showSpinner) {
|
||||
titleElement = <h3 className="mx_RoomPreviewBar_spinnerTitle"><Spinner />{ title }</h3>;
|
||||
titleElement = (
|
||||
<h3 className="mx_RoomPreviewBar_spinnerTitle">
|
||||
<Spinner />
|
||||
{title}
|
||||
</h3>
|
||||
);
|
||||
} else {
|
||||
titleElement = <h3>{ title }</h3>;
|
||||
titleElement = <h3>{title}</h3>;
|
||||
}
|
||||
|
||||
let primaryButton;
|
||||
if (primaryActionHandler) {
|
||||
primaryButton = (
|
||||
<AccessibleButton kind="primary" onClick={primaryActionHandler}>
|
||||
{ primaryActionLabel }
|
||||
{primaryActionLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -618,7 +606,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
if (secondaryActionHandler) {
|
||||
secondaryButton = (
|
||||
<AccessibleButton kind="secondary" onClick={secondaryActionHandler}>
|
||||
{ secondaryActionLabel }
|
||||
{secondaryActionLabel}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -626,36 +614,34 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
|
|||
const isPanel = this.props.canPreview;
|
||||
|
||||
const classes = classNames("mx_RoomPreviewBar", "dark-panel", `mx_RoomPreviewBar_${messageCase}`, {
|
||||
"mx_RoomPreviewBar_panel": isPanel,
|
||||
"mx_RoomPreviewBar_dialog": !isPanel,
|
||||
mx_RoomPreviewBar_panel: isPanel,
|
||||
mx_RoomPreviewBar_dialog: !isPanel,
|
||||
});
|
||||
|
||||
// ensure correct tab order for both views
|
||||
const actions = isPanel
|
||||
? <>
|
||||
{ secondaryButton }
|
||||
{ extraComponents }
|
||||
{ primaryButton }
|
||||
const actions = isPanel ? (
|
||||
<>
|
||||
{secondaryButton}
|
||||
{extraComponents}
|
||||
{primaryButton}
|
||||
</>
|
||||
: <>
|
||||
{ primaryButton }
|
||||
{ extraComponents }
|
||||
{ secondaryButton }
|
||||
</>;
|
||||
) : (
|
||||
<>
|
||||
{primaryButton}
|
||||
{extraComponents}
|
||||
{secondaryButton}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_RoomPreviewBar_message">
|
||||
{ titleElement }
|
||||
{ subTitleElements }
|
||||
</div>
|
||||
{ reasonElement }
|
||||
<div className="mx_RoomPreviewBar_actions">
|
||||
{ actions }
|
||||
</div>
|
||||
<div className="mx_RoomPreviewBar_footer">
|
||||
{ footer }
|
||||
{titleElement}
|
||||
{subTitleElements}
|
||||
</div>
|
||||
{reasonElement}
|
||||
<div className="mx_RoomPreviewBar_actions">{actions}</div>
|
||||
<div className="mx_RoomPreviewBar_footer">{footer}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
|
||||
const isVideoRoom = room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom());
|
||||
const myMembership = useMyRoomMembership(room);
|
||||
useDispatcher(defaultDispatcher, payload => {
|
||||
useDispatcher(defaultDispatcher, (payload) => {
|
||||
if (payload.action === Action.JoinRoomError && payload.roomId === room.roomId) {
|
||||
setBusy(false); // stop the spinner, join failed
|
||||
}
|
||||
|
@ -62,14 +62,15 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const joinRule = useRoomState(room, state => state.getJoinRule());
|
||||
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
|
||||
&& joinRule !== JoinRule.Public;
|
||||
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||
const cannotJoin =
|
||||
getEffectiveMembership(myMembership) === EffectiveMembership.Leave && joinRule !== JoinRule.Public;
|
||||
|
||||
const viewLabs = () => defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
const viewLabs = () =>
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
|
||||
let inviterSection: JSX.Element | null = null;
|
||||
let joinButtons: JSX.Element;
|
||||
|
@ -84,7 +85,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
});
|
||||
}}
|
||||
>
|
||||
{ _t("Leave") }
|
||||
{_t("Leave")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else if (myMembership === "invite") {
|
||||
|
@ -93,41 +94,47 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
if (inviteSender) {
|
||||
const inviter = room.getMember(inviteSender);
|
||||
|
||||
inviterSection = <div className="mx_RoomPreviewCard_inviter">
|
||||
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
|
||||
<div>
|
||||
<div className="mx_RoomPreviewCard_inviter_name">
|
||||
{ _t("<inviter/> invites you", {}, {
|
||||
inviter: () => <b>{ inviter?.name || inviteSender }</b>,
|
||||
}) }
|
||||
inviterSection = (
|
||||
<div className="mx_RoomPreviewCard_inviter">
|
||||
<MemberAvatar member={inviter} fallbackUserId={inviteSender} width={32} height={32} />
|
||||
<div>
|
||||
<div className="mx_RoomPreviewCard_inviter_name">
|
||||
{_t(
|
||||
"<inviter/> invites you",
|
||||
{},
|
||||
{
|
||||
inviter: () => <b>{inviter?.name || inviteSender}</b>,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
{inviter ? <div className="mx_RoomPreviewCard_inviter_mxid">{inviteSender}</div> : null}
|
||||
</div>
|
||||
{ inviter ? <div className="mx_RoomPreviewCard_inviter_mxid">
|
||||
{ inviteSender }
|
||||
</div> : null }
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
}
|
||||
|
||||
joinButtons = <>
|
||||
<AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
onRejectButtonClicked();
|
||||
}}
|
||||
>
|
||||
{ _t("Reject") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
onJoinButtonClicked();
|
||||
}}
|
||||
>
|
||||
{ _t("Accept") }
|
||||
</AccessibleButton>
|
||||
</>;
|
||||
joinButtons = (
|
||||
<>
|
||||
<AccessibleButton
|
||||
kind="secondary"
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
onRejectButtonClicked();
|
||||
}}
|
||||
>
|
||||
{_t("Reject")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
setBusy(true);
|
||||
onJoinButtonClicked();
|
||||
}}
|
||||
>
|
||||
{_t("Accept")}
|
||||
</AccessibleButton>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
joinButtons = (
|
||||
<AccessibleButton
|
||||
|
@ -141,7 +148,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
}}
|
||||
disabled={cannotJoin}
|
||||
>
|
||||
{ _t("Join") }
|
||||
{_t("Join")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -152,11 +159,13 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
|
||||
let avatarRow: JSX.Element;
|
||||
if (isVideoRoom) {
|
||||
avatarRow = <>
|
||||
<RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />
|
||||
<div className="mx_RoomPreviewCard_video" />
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
||||
</>;
|
||||
avatarRow = (
|
||||
<>
|
||||
<RoomAvatar room={room} height={50} width={50} viewAvatarOnClick />
|
||||
<div className="mx_RoomPreviewCard_video" />
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
||||
</>
|
||||
);
|
||||
} else if (room.isSpaceRoom()) {
|
||||
avatarRow = <RoomAvatar room={room} height={80} width={80} viewAvatarOnClick />;
|
||||
} else {
|
||||
|
@ -169,33 +178,32 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
|
|||
roomName: room.name,
|
||||
});
|
||||
} else if (isVideoRoom && !videoRoomsEnabled) {
|
||||
notice = myMembership === "join"
|
||||
? _t("To view, please enable video rooms in Labs first")
|
||||
: _t("To join, please enable video rooms in Labs first");
|
||||
notice =
|
||||
myMembership === "join"
|
||||
? _t("To view, please enable video rooms in Labs first")
|
||||
: _t("To join, please enable video rooms in Labs first");
|
||||
|
||||
joinButtons = <AccessibleButton kind="primary" onClick={viewLabs}>
|
||||
{ _t("Show Labs settings") }
|
||||
</AccessibleButton>;
|
||||
joinButtons = (
|
||||
<AccessibleButton kind="primary" onClick={viewLabs}>
|
||||
{_t("Show Labs settings")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_RoomPreviewCard">
|
||||
{ inviterSection }
|
||||
<div className="mx_RoomPreviewCard_avatar">
|
||||
{ avatarRow }
|
||||
return (
|
||||
<div className="mx_RoomPreviewCard">
|
||||
{inviterSection}
|
||||
<div className="mx_RoomPreviewCard_avatar">{avatarRow}</div>
|
||||
<h1 className="mx_RoomPreviewCard_name">
|
||||
<RoomName room={room} />
|
||||
</h1>
|
||||
<RoomInfoLine room={room} />
|
||||
<RoomTopic room={room} className="mx_RoomPreviewCard_topic" />
|
||||
{room.getJoinRule() === "public" && <RoomFacePile room={room} />}
|
||||
{notice ? <div className="mx_RoomPreviewCard_notice">{notice}</div> : null}
|
||||
<div className="mx_RoomPreviewCard_joinButtons">{joinButtons}</div>
|
||||
</div>
|
||||
<h1 className="mx_RoomPreviewCard_name">
|
||||
<RoomName room={room} />
|
||||
</h1>
|
||||
<RoomInfoLine room={room} />
|
||||
<RoomTopic room={room} className="mx_RoomPreviewCard_topic" />
|
||||
{ room.getJoinRule() === "public" && <RoomFacePile room={room} /> }
|
||||
{ notice ? <div className="mx_RoomPreviewCard_notice">
|
||||
{ notice }
|
||||
</div> : null }
|
||||
<div className="mx_RoomPreviewCard_joinButtons">
|
||||
{ joinButtons }
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
};
|
||||
|
||||
export default RoomPreviewCard;
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
import { Dispatcher } from "flux";
|
||||
import { Enable, Resizable } from "re-resizable";
|
||||
import { Direction } from "re-resizable/lib/resizer";
|
||||
|
@ -198,8 +198,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// Do the same check used on props for state, without the rooms we're going to no-op
|
||||
const prevStateNoRooms = objectExcluding(this.state, ['rooms']);
|
||||
const nextStateNoRooms = objectExcluding(nextState, ['rooms']);
|
||||
const prevStateNoRooms = objectExcluding(this.state, ["rooms"]);
|
||||
const nextStateNoRooms = objectExcluding(nextState, ["rooms"]);
|
||||
if (objectHasDiff(prevStateNoRooms, nextStateNoRooms)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -343,9 +343,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||
const count = RoomListStore.instance.getCount(this.props.tagId);
|
||||
await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, {
|
||||
ranges: [
|
||||
[0, count],
|
||||
],
|
||||
ranges: [[0, count]],
|
||||
});
|
||||
}
|
||||
// read number of visible tiles before we mutate it
|
||||
|
@ -448,12 +446,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const listScrollTop = Math.round(list.scrollTop);
|
||||
const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
|
||||
const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
|
||||
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
|
||||
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
|
||||
const isStickyTop = possibleSticky.classList.contains("mx_RoomSublist_headerContainer_stickyTop");
|
||||
const isStickyBottom = possibleSticky.classList.contains("mx_RoomSublist_headerContainer_stickyBottom");
|
||||
|
||||
if ((isStickyBottom && !isAtBottom) || (isStickyTop && !isAtTop)) {
|
||||
// is sticky - jump to list
|
||||
sublist.scrollIntoView({ behavior: 'smooth' });
|
||||
sublist.scrollIntoView({ behavior: "smooth" });
|
||||
} else {
|
||||
// on screen - toggle collapse
|
||||
const isExpanded = this.state.isExpanded;
|
||||
|
@ -461,7 +459,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
// if the bottom list is collapsed then scroll it in so it doesn't expand off screen
|
||||
if (!isExpanded && isStickyBottom) {
|
||||
setImmediate(() => {
|
||||
sublist.scrollIntoView({ behavior: 'smooth' });
|
||||
sublist.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -532,13 +530,15 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
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}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -569,9 +569,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
|
||||
isAlphabetical = slidingList.sort[0] === "by_name";
|
||||
isUnreadFirst = (
|
||||
slidingList.sort[0] === "by_notification_level"
|
||||
);
|
||||
isUnreadFirst = slidingList.sort[0] === "by_notification_level";
|
||||
}
|
||||
|
||||
// Invites don't get some nonsense options, so only add them if we have to.
|
||||
|
@ -581,20 +579,20 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
<React.Fragment>
|
||||
<hr />
|
||||
<div>
|
||||
<div className='mx_RoomSublist_contextMenu_title'>{ _t("Appearance") }</div>
|
||||
<div className="mx_RoomSublist_contextMenu_title">{_t("Appearance")}</div>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onUnreadFirstChanged}
|
||||
checked={isUnreadFirst}
|
||||
>
|
||||
{ _t("Show rooms with unread messages first") }
|
||||
{_t("Show rooms with unread messages first")}
|
||||
</StyledMenuItemCheckbox>
|
||||
<StyledMenuItemCheckbox
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={this.onMessagePreviewChanged}
|
||||
checked={this.layout.showPreviews}
|
||||
>
|
||||
{ _t("Show previews of messages") }
|
||||
{_t("Show previews of messages")}
|
||||
</StyledMenuItemCheckbox>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
|
@ -610,14 +608,14 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
>
|
||||
<div className="mx_RoomSublist_contextMenu">
|
||||
<div>
|
||||
<div className='mx_RoomSublist_contextMenu_title'>{ _t("Sort by") }</div>
|
||||
<div className="mx_RoomSublist_contextMenu_title">{_t("Sort by")}</div>
|
||||
<StyledMenuItemRadio
|
||||
onClose={this.onCloseMenu}
|
||||
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
||||
checked={!isAlphabetical}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{ _t("Activity") }
|
||||
{_t("Activity")}
|
||||
</StyledMenuItemRadio>
|
||||
<StyledMenuItemRadio
|
||||
onClose={this.onCloseMenu}
|
||||
|
@ -625,10 +623,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
checked={isAlphabetical}
|
||||
name={`mx_${this.props.tagId}_sortBy`}
|
||||
>
|
||||
{ _t("A-Z") }
|
||||
{_t("A-Z")}
|
||||
</StyledMenuItemRadio>
|
||||
</div>
|
||||
{ otherSections }
|
||||
{otherSections}
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
@ -642,7 +640,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
title={_t("List options")}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
/>
|
||||
{ contextMenu }
|
||||
{contextMenu}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -650,7 +648,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private renderHeader(): React.ReactElement {
|
||||
return (
|
||||
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
||||
{ ({ onFocus, isActive, ref }) => {
|
||||
{({ onFocus, isActive, ref }) => {
|
||||
const tabIndex = isActive ? 0 : -1;
|
||||
|
||||
let ariaLabel = _t("Jump to first unread room.");
|
||||
|
@ -676,20 +674,16 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
const collapseClasses = classNames({
|
||||
'mx_RoomSublist_collapseBtn': true,
|
||||
'mx_RoomSublist_collapseBtn_collapsed': !this.state.isExpanded && !this.props.forceExpanded,
|
||||
mx_RoomSublist_collapseBtn: true,
|
||||
mx_RoomSublist_collapseBtn_collapsed: !this.state.isExpanded && !this.props.forceExpanded,
|
||||
});
|
||||
|
||||
const classes = classNames({
|
||||
'mx_RoomSublist_headerContainer': true,
|
||||
'mx_RoomSublist_headerContainer_withAux': !!addRoomButton,
|
||||
mx_RoomSublist_headerContainer: true,
|
||||
mx_RoomSublist_headerContainer_withAux: !!addRoomButton,
|
||||
});
|
||||
|
||||
const badgeContainer = (
|
||||
<div className="mx_RoomSublist_badgeContainer">
|
||||
{ badge }
|
||||
</div>
|
||||
);
|
||||
const badgeContainer = <div className="mx_RoomSublist_badgeContainer">{badge}</div>;
|
||||
|
||||
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||
if (this.props.isMinimized) {
|
||||
|
@ -723,18 +717,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
title={this.props.isMinimized ? this.props.label : undefined}
|
||||
>
|
||||
<span className={collapseClasses} />
|
||||
<span>{ this.props.label }</span>
|
||||
<span>{this.props.label}</span>
|
||||
</Button>
|
||||
{ this.renderMenu() }
|
||||
{ this.props.isMinimized ? null : badgeContainer }
|
||||
{ this.props.isMinimized ? null : addRoomButton }
|
||||
{this.renderMenu()}
|
||||
{this.props.isMinimized ? null : badgeContainer}
|
||||
{this.props.isMinimized ? null : addRoomButton}
|
||||
</div>
|
||||
</div>
|
||||
{ this.props.isMinimized ? badgeContainer : null }
|
||||
{ this.props.isMinimized ? addRoomButton : null }
|
||||
{this.props.isMinimized ? badgeContainer : null}
|
||||
{this.props.isMinimized ? addRoomButton : null}
|
||||
</div>
|
||||
);
|
||||
} }
|
||||
}}
|
||||
</RovingTabIndexWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -749,21 +743,23 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const visibleTiles = this.renderVisibleTiles();
|
||||
const hidden = !this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true;
|
||||
const classes = classNames({
|
||||
'mx_RoomSublist': true,
|
||||
'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
|
||||
'mx_RoomSublist_minimized': this.props.isMinimized,
|
||||
'mx_RoomSublist_hidden': hidden,
|
||||
mx_RoomSublist: true,
|
||||
mx_RoomSublist_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||
mx_RoomSublist_minimized: this.props.isMinimized,
|
||||
mx_RoomSublist_hidden: hidden,
|
||||
});
|
||||
|
||||
let content = null;
|
||||
if (this.state.roomsLoading) {
|
||||
content = <div className="mx_RoomSublist_skeletonUI" />;
|
||||
} else if (visibleTiles.length > 0 && this.props.forceExpanded) {
|
||||
content = <div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{ visibleTiles }
|
||||
content = (
|
||||
<div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{visibleTiles}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
);
|
||||
} else if (visibleTiles.length > 0) {
|
||||
const layout = this.layout; // to shorten calls
|
||||
|
||||
|
@ -773,18 +769,17 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
|
||||
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
|
||||
const showMoreBtnClasses = classNames({
|
||||
'mx_RoomSublist_showNButton': true,
|
||||
mx_RoomSublist_showNButton: true,
|
||||
});
|
||||
|
||||
// If we're hiding rooms, show a 'show more' button to the user. This button
|
||||
// floats above the resize handle, if we have one present. If the user has all
|
||||
// tiles visible, it becomes 'show less'.
|
||||
let showNButton = null;
|
||||
const hasMoreSlidingSync = (
|
||||
this.slidingSyncMode && (RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length)
|
||||
);
|
||||
const hasMoreSlidingSync =
|
||||
this.slidingSyncMode && RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length;
|
||||
|
||||
if ((maxTilesPx > this.state.height) || hasMoreSlidingSync) {
|
||||
if (maxTilesPx > this.state.height || hasMoreSlidingSync) {
|
||||
// the height of all the tiles is greater than the section height: we need a 'show more' button
|
||||
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
|
||||
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
|
||||
|
@ -793,11 +788,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
|
||||
}
|
||||
const label = _t("Show %(count)s more", { count: numMissing });
|
||||
let showMoreText = (
|
||||
<span className='mx_RoomSublist_showNButtonText'>
|
||||
{ label }
|
||||
</span>
|
||||
);
|
||||
let showMoreText = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
|
||||
if (this.props.isMinimized) showMoreText = null;
|
||||
showNButton = (
|
||||
<RovingAccessibleButton
|
||||
|
@ -806,20 +797,16 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
className={showMoreBtnClasses}
|
||||
aria-label={label}
|
||||
>
|
||||
<span className='mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron'>
|
||||
{ /* set by CSS masking */ }
|
||||
<span className="mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron">
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
{ showMoreText }
|
||||
{showMoreText}
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
} else if (this.numTiles > this.layout.defaultVisibleTiles) {
|
||||
// we have all tiles visible - add a button to show less
|
||||
const label = _t("Show less");
|
||||
let showLessText = (
|
||||
<span className='mx_RoomSublist_showNButtonText'>
|
||||
{ label }
|
||||
</span>
|
||||
);
|
||||
let showLessText = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
|
||||
if (this.props.isMinimized) showLessText = null;
|
||||
showNButton = (
|
||||
<RovingAccessibleButton
|
||||
|
@ -828,10 +815,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
className={showMoreBtnClasses}
|
||||
aria-label={label}
|
||||
>
|
||||
<span className='mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron'>
|
||||
{ /* set by CSS masking */ }
|
||||
<span className="mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron">
|
||||
{/* set by CSS masking */}
|
||||
</span>
|
||||
{ showLessText }
|
||||
{showLessText}
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
}
|
||||
|
@ -863,8 +850,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
// only mathematically 7 possible).
|
||||
|
||||
const handleWrapperClasses = classNames({
|
||||
'mx_RoomSublist_resizerHandles': true,
|
||||
'mx_RoomSublist_resizerHandles_showNButton': !!showNButton,
|
||||
mx_RoomSublist_resizerHandles: true,
|
||||
mx_RoomSublist_resizerHandles_showNButton: !!showNButton,
|
||||
});
|
||||
|
||||
content = (
|
||||
|
@ -882,9 +869,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
enable={handles}
|
||||
>
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{ visibleTiles }
|
||||
{visibleTiles}
|
||||
</div>
|
||||
{ showNButton }
|
||||
{showNButton}
|
||||
</Resizable>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
@ -901,8 +888,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
aria-label={this.props.label}
|
||||
onKeyDown={this.onKeyDown}
|
||||
>
|
||||
{ this.renderHeader() }
|
||||
{ content }
|
||||
{this.renderHeader()}
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import classNames from "classnames";
|
|||
import type { Call } from "../../../models/Call";
|
||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||
import defaultDispatcher from '../../../dispatcher/dispatcher';
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
|
||||
|
@ -181,7 +181,8 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === Action.ViewRoom &&
|
||||
if (
|
||||
payload.action === Action.ViewRoom &&
|
||||
payload.room_id === this.props.room.roomId &&
|
||||
payload.show_room_tile
|
||||
) {
|
||||
|
@ -223,8 +224,9 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
ev.stopPropagation();
|
||||
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array<string | undefined>)
|
||||
.includes(action);
|
||||
const clearSearch = ([KeyBindingAction.Enter, KeyBindingAction.Space] as Array<string | undefined>).includes(
|
||||
action,
|
||||
);
|
||||
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -279,8 +281,11 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
};
|
||||
|
||||
private renderNotificationsMenu(isActive: boolean): React.ReactElement | null {
|
||||
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
|
||||
!this.showContextMenu || this.props.isMinimized
|
||||
if (
|
||||
MatrixClientPeg.get().isGuest() ||
|
||||
this.props.tag === DefaultTagID.Archived ||
|
||||
!this.showContextMenu ||
|
||||
this.props.isMinimized
|
||||
) {
|
||||
// the menu makes no sense in these cases so do not show one
|
||||
return null;
|
||||
|
@ -309,13 +314,13 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
isExpanded={!!this.state.notificationsMenuPosition}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
/>
|
||||
{ this.state.notificationsMenuPosition && (
|
||||
{this.state.notificationsMenuPosition && (
|
||||
<RoomNotificationContextMenu
|
||||
{...contextMenuBelow(this.state.notificationsMenuPosition)}
|
||||
onFinished={this.onCloseNotificationsMenu}
|
||||
room={this.props.room}
|
||||
/>
|
||||
) }
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -330,39 +335,39 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
title={_t("Room options")}
|
||||
isExpanded={!!this.state.generalMenuPosition}
|
||||
/>
|
||||
{ this.state.generalMenuPosition && (
|
||||
{this.state.generalMenuPosition && (
|
||||
<RoomGeneralContextMenu
|
||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
||||
onFinished={this.onCloseGeneralMenu}
|
||||
room={this.props.room}
|
||||
onPostFavoriteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuFavouriteToggle", ev,
|
||||
)}
|
||||
onPostInviteClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuInviteItem", ev,
|
||||
)}
|
||||
onPostSettingsClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuSettingsItem", ev,
|
||||
)}
|
||||
onPostLeaveClick={(ev: ButtonEvent) => PosthogTrackers.trackInteraction(
|
||||
"WebRoomListRoomTileContextMenuLeaveItem", ev,
|
||||
)}
|
||||
onPostFavoriteClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuFavouriteToggle", ev)
|
||||
}
|
||||
onPostInviteClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuInviteItem", ev)
|
||||
}
|
||||
onPostSettingsClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuSettingsItem", ev)
|
||||
}
|
||||
onPostLeaveClick={(ev: ButtonEvent) =>
|
||||
PosthogTrackers.trackInteraction("WebRoomListRoomTileContextMenuLeaveItem", ev)
|
||||
}
|
||||
/>
|
||||
) }
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement {
|
||||
const classes = classNames({
|
||||
'mx_RoomTile': true,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
'mx_RoomTile_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
|
||||
'mx_RoomTile_minimized': this.props.isMinimized,
|
||||
mx_RoomTile: true,
|
||||
mx_RoomTile_selected: this.state.selected,
|
||||
mx_RoomTile_hasMenuOpen: !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
|
||||
mx_RoomTile_minimized: this.props.isMinimized,
|
||||
});
|
||||
|
||||
let name = this.props.room.name;
|
||||
if (typeof name !== 'string') name = '';
|
||||
if (typeof name !== "string") name = "";
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let badge: React.ReactNode;
|
||||
|
@ -395,25 +400,23 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
id={messagePreviewId(this.props.room.roomId)}
|
||||
title={this.state.messagePreview}
|
||||
>
|
||||
{ this.state.messagePreview }
|
||||
{this.state.messagePreview}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const titleClasses = classNames({
|
||||
"mx_RoomTile_title": true,
|
||||
"mx_RoomTile_titleWithSubtitle": !!subtitle,
|
||||
"mx_RoomTile_titleHasUnreadEvents": this.notificationState.isUnread,
|
||||
mx_RoomTile_title: true,
|
||||
mx_RoomTile_titleWithSubtitle: !!subtitle,
|
||||
mx_RoomTile_titleHasUnreadEvents: this.notificationState.isUnread,
|
||||
});
|
||||
|
||||
const titleContainer = this.props.isMinimized ? null : (
|
||||
<div className="mx_RoomTile_titleContainer">
|
||||
<div title={name} className={titleClasses} tabIndex={-1}>
|
||||
<span dir="auto">
|
||||
{ name }
|
||||
</span>
|
||||
<span dir="auto">{name}</span>
|
||||
</div>
|
||||
{ subtitle }
|
||||
{subtitle}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -422,13 +425,17 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
if (this.props.tag === DefaultTagID.Invite) {
|
||||
// append nothing
|
||||
} else if (this.notificationState.hasMentions) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
|
||||
count: this.notificationState.count,
|
||||
});
|
||||
ariaLabel +=
|
||||
" " +
|
||||
_t("%(count)s unread messages including mentions.", {
|
||||
count: this.notificationState.count,
|
||||
});
|
||||
} else if (this.notificationState.hasUnreadCount) {
|
||||
ariaLabel += " " + _t("%(count)s unread messages.", {
|
||||
count: this.notificationState.count,
|
||||
});
|
||||
ariaLabel +=
|
||||
" " +
|
||||
_t("%(count)s unread messages.", {
|
||||
count: this.notificationState.count,
|
||||
});
|
||||
} else if (this.notificationState.isUnread) {
|
||||
ariaLabel += " " + _t("Unread messages.");
|
||||
}
|
||||
|
@ -450,7 +457,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
|
||||
{ ({ onFocus, isActive, ref }) =>
|
||||
{({ onFocus, isActive, ref }) => (
|
||||
<Button
|
||||
{...props}
|
||||
onFocus={onFocus}
|
||||
|
@ -470,12 +477,12 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
|
|||
displayBadge={this.props.isMinimized}
|
||||
tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
|
||||
/>
|
||||
{ titleContainer }
|
||||
{ badge }
|
||||
{ this.renderGeneralMenu() }
|
||||
{ this.renderNotificationsMenu(isActive) }
|
||||
{titleContainer}
|
||||
{badge}
|
||||
{this.renderGeneralMenu()}
|
||||
{this.renderNotificationsMenu(isActive)}
|
||||
</Button>
|
||||
}
|
||||
)}
|
||||
</RovingTabIndexWrapper>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -46,10 +46,12 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
|
|||
break;
|
||||
}
|
||||
|
||||
return <LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={text}
|
||||
active={active}
|
||||
participantCount={useParticipantCount(call)}
|
||||
/>;
|
||||
return (
|
||||
<LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={text}
|
||||
active={active}
|
||||
participantCount={useParticipantCount(call)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { RoomStateEvent } from 'matrix-js-sdk/src/models/room-state';
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
|
||||
import Modal from '../../../Modal';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import RoomUpgradeDialog from '../dialogs/RoomUpgradeDialog';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import RoomUpgradeDialog from "../dialogs/RoomUpgradeDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
|
||||
interface IProps {
|
||||
|
@ -74,26 +74,27 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
|
|||
<div>
|
||||
<div className="mx_RoomUpgradeWarningBar_body">
|
||||
<p>
|
||||
{ _t(
|
||||
{_t(
|
||||
"Upgrading this room will shut down the current instance of the room and create " +
|
||||
"an upgraded room with the same name.",
|
||||
) }
|
||||
"an upgraded room with the same name.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
{_t(
|
||||
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
|
||||
"to the new version of the room.</i> We'll post a link to the new room in the old " +
|
||||
"version of the room - room members will have to click this link to join the new room.",
|
||||
{}, {
|
||||
"b": (sub) => <b>{ sub }</b>,
|
||||
"i": (sub) => <i>{ sub }</i>,
|
||||
"to the new version of the room.</i> We'll post a link to the new room in the old " +
|
||||
"version of the room - room members will have to click this link to join the new room.",
|
||||
{},
|
||||
{
|
||||
b: (sub) => <b>{sub}</b>,
|
||||
i: (sub) => <i>{sub}</i>,
|
||||
},
|
||||
) }
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mx_RoomUpgradeWarningBar_upgradelink">
|
||||
<AccessibleButton onClick={this.onUpgradeClick}>
|
||||
{ _t("Upgrade this room to the recommended room version") }
|
||||
{_t("Upgrade this room to the recommended room version")}
|
||||
</AccessibleButton>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -102,9 +103,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
|
|||
if (this.state.upgraded) {
|
||||
doUpgradeWarnings = (
|
||||
<div className="mx_RoomUpgradeWarningBar_body">
|
||||
<p>
|
||||
{ _t("This room has already been upgraded.") }
|
||||
</p>
|
||||
<p>{_t("This room has already been upgraded.")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -113,19 +112,19 @@ export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, I
|
|||
<div className="mx_RoomUpgradeWarningBar">
|
||||
<div className="mx_RoomUpgradeWarningBar_wrapped">
|
||||
<div className="mx_RoomUpgradeWarningBar_header">
|
||||
{ _t(
|
||||
{_t(
|
||||
"This room is running room version <roomVersion />, which this homeserver has " +
|
||||
"marked as <i>unstable</i>.",
|
||||
"marked as <i>unstable</i>.",
|
||||
{},
|
||||
{
|
||||
"roomVersion": () => <code>{ this.props.room.getVersion() }</code>,
|
||||
"i": (sub) => <i>{ sub }</i>,
|
||||
roomVersion: () => <code>{this.props.room.getVersion()}</code>,
|
||||
i: (sub) => <i>{sub}</i>,
|
||||
},
|
||||
) }
|
||||
)}
|
||||
</div>
|
||||
{ doUpgradeWarnings }
|
||||
{doUpgradeWarnings}
|
||||
<div className="mx_RoomUpgradeWarningBar_small">
|
||||
{ _t("Only room administrators will see this warning") }
|
||||
{_t("Only room administrators will see this warning")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { createRef, RefObject } from 'react';
|
||||
import React, { createRef, RefObject } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { PosthogScreenTracker } from '../../../PosthogTrackers';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { PosthogScreenTracker } from "../../../PosthogTrackers";
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import SearchWarning, { WarningKind } from "../elements/SearchWarning";
|
||||
|
@ -104,7 +104,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
|
|||
aria-checked={this.state.scope === SearchScope.Room}
|
||||
role="radio"
|
||||
>
|
||||
{ _t("This Room") }
|
||||
{_t("This Room")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className={allRoomsClasses}
|
||||
|
@ -112,7 +112,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
|
|||
aria-checked={this.state.scope === SearchScope.All}
|
||||
role="radio"
|
||||
>
|
||||
{ _t("All Rooms") }
|
||||
{_t("All Rooms")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_SearchBar_input mx_textinput">
|
||||
|
|
|
@ -21,7 +21,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import DateSeparator from "../messages/DateSeparator";
|
||||
import EventTile from "./EventTile";
|
||||
import { shouldFormContinuation } from "../../structures/MessagePanel";
|
||||
|
@ -73,7 +73,7 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
for (let j = 0; j < timeline.length; j++) {
|
||||
const mxEv = timeline[j];
|
||||
let highlights;
|
||||
const contextual = (j != result.context.getOurEventIndex());
|
||||
const contextual = j != result.context.getOurEventIndex();
|
||||
if (!contextual) {
|
||||
highlights = this.props.searchHighlights;
|
||||
}
|
||||
|
@ -82,7 +82,8 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
// do we need a date separator since the last event?
|
||||
const prevEv = timeline[j - 1];
|
||||
// is this a continuation of the previous message?
|
||||
const continuation = prevEv &&
|
||||
const continuation =
|
||||
prevEv &&
|
||||
!wantsDateSeparator(prevEv.getDate(), mxEv.getDate()) &&
|
||||
shouldFormContinuation(
|
||||
prevEv,
|
||||
|
@ -96,7 +97,7 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
const nextEv = timeline[j + 1];
|
||||
if (nextEv) {
|
||||
const willWantDateSeparator = wantsDateSeparator(mxEv.getDate(), nextEv.getDate());
|
||||
lastInSection = (
|
||||
lastInSection =
|
||||
willWantDateSeparator ||
|
||||
mxEv.getSender() !== nextEv.getSender() ||
|
||||
!shouldFormContinuation(
|
||||
|
@ -105,8 +106,7 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
this.context?.showHiddenEvents,
|
||||
threadsEnabled,
|
||||
TimelineRenderingType.Search,
|
||||
)
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
ret.push(
|
||||
|
@ -129,8 +129,10 @@ export default class SearchResultTile extends React.Component<IProps> {
|
|||
}
|
||||
}
|
||||
|
||||
return <li data-scroll-tokens={eventId}>
|
||||
<ol>{ ret }</ol>
|
||||
</li>;
|
||||
return (
|
||||
<li data-scroll-tokens={eventId}>
|
||||
<ol>{ret}</ol>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,18 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ClipboardEvent, createRef, KeyboardEvent } from 'react';
|
||||
import EMOJI_REGEX from 'emojibase-regex';
|
||||
import { IContent, MatrixEvent, IEventRelation } from 'matrix-js-sdk/src/models/event';
|
||||
import { DebouncedFunc, throttle } from 'lodash';
|
||||
import React, { ClipboardEvent, createRef, KeyboardEvent } from "react";
|
||||
import EMOJI_REGEX from "emojibase-regex";
|
||||
import { IContent, MatrixEvent, IEventRelation } from "matrix-js-sdk/src/models/event";
|
||||
import { DebouncedFunc, throttle } from "lodash";
|
||||
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
||||
import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread';
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import EditorModel from "../../../editor/model";
|
||||
import {
|
||||
containsEmote,
|
||||
htmlSerializeIfNeeded,
|
||||
|
@ -34,37 +34,37 @@ import {
|
|||
stripPrefix,
|
||||
textSerialize,
|
||||
unescapeMessage,
|
||||
} from '../../../editor/serialize';
|
||||
} from "../../../editor/serialize";
|
||||
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
|
||||
import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts';
|
||||
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||
import { CommandPartCreator, Part, PartCreator, SerializedPart } from "../../../editor/parts";
|
||||
import { findEditableEvent } from "../../../utils/EventUtils";
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import { CommandCategories } from '../../../SlashCommands';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import { CommandCategories } from "../../../SlashCommands";
|
||||
import ContentMessages from "../../../ContentMessages";
|
||||
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { containsEmoji } from "../../../effects/utils";
|
||||
import { CHAT_EFFECTS } from '../../../effects';
|
||||
import { CHAT_EFFECTS } from "../../../effects";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import { getKeyBindingsManager } from '../../../KeyBindingsManager';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
|
||||
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import DocumentPosition from "../../../editor/position";
|
||||
import { ComposerType } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } from "../../../editor/commands";
|
||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||
import { PosthogAnalytics } from "../../../PosthogAnalytics";
|
||||
import { addReplyToMessageContent } from '../../../utils/Reply';
|
||||
import { doMaybeLocalRoomAction } from '../../../utils/local-room';
|
||||
import { addReplyToMessageContent } from "../../../utils/Reply";
|
||||
import { doMaybeLocalRoomAction } from "../../../utils/local-room";
|
||||
|
||||
// Merges favouring the given relation
|
||||
export function attachRelation(content: IContent, relation?: IEventRelation): void {
|
||||
if (relation) {
|
||||
content['m.relates_to'] = {
|
||||
...(content['m.relates_to'] || {}),
|
||||
content["m.relates_to"] = {
|
||||
...(content["m.relates_to"] || {}),
|
||||
...relation,
|
||||
};
|
||||
}
|
||||
|
@ -124,8 +124,7 @@ export function isQuickReaction(model: EditorModel): boolean {
|
|||
const hasShortcut = text.startsWith("+") || text.startsWith("+ ");
|
||||
const emojiMatch = text.match(EMOJI_REGEX);
|
||||
if (hasShortcut && emojiMatch && emojiMatch.length == 1) {
|
||||
return emojiMatch[0] === text.substring(1) ||
|
||||
emojiMatch[0] === text.substring(2);
|
||||
return emojiMatch[0] === text.substring(1) || emojiMatch[0] === text.substring(2);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -163,9 +162,13 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
this.context = context; // otherwise React will only set it prior to render due to type def above
|
||||
|
||||
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
|
||||
this.prepareToEncrypt = throttle(() => {
|
||||
this.props.mxClient.prepareToEncrypt(this.props.room);
|
||||
}, 60000, { leading: true, trailing: false });
|
||||
this.prepareToEncrypt = throttle(
|
||||
() => {
|
||||
this.props.mxClient.prepareToEncrypt(this.props.room);
|
||||
},
|
||||
60000,
|
||||
{ leading: true, trailing: false },
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener("beforeunload", this.saveStoredEditorState);
|
||||
|
@ -174,14 +177,14 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
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_history_');
|
||||
this.sendHistoryManager = new SendHistoryManager(this.props.room.roomId, "mx_cider_history_");
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
|
||||
const replyingToThread = this.props.relation?.key === THREAD_RELATION_TYPE.name;
|
||||
const differentEventTarget = this.props.relation?.event_id !== prevProps.relation?.event_id;
|
||||
|
||||
const threadChanged = replyingToThread && (differentEventTarget);
|
||||
const threadChanged = replyingToThread && differentEventTarget;
|
||||
if (threadChanged) {
|
||||
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
|
||||
const parts = this.restoreStoredEditorState(partCreator) || [];
|
||||
|
@ -223,9 +226,9 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
case KeyBindingAction.EditPrevMessage:
|
||||
// selection must be collapsed and caret at start
|
||||
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
|
||||
const events =
|
||||
this.context.liveTimeline.getEvents()
|
||||
.concat(replyingToThread ? [] : this.props.room.getPendingEvents());
|
||||
const events = this.context.liveTimeline
|
||||
.getEvents()
|
||||
.concat(replyingToThread ? [] : this.props.room.getPendingEvents());
|
||||
const editEvent = findEditableEvent({
|
||||
events,
|
||||
isForward: false,
|
||||
|
@ -243,7 +246,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
break;
|
||||
case KeyBindingAction.CancelReplyOrEdit:
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
action: "reply_to_event",
|
||||
event: null,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
|
@ -275,7 +278,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
}
|
||||
const { parts, replyEventId } = this.sendHistoryManager.getItem(delta);
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
action: "reply_to_event",
|
||||
event: replyEventId ? this.props.room.findEventById(replyEventId) : null,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
|
@ -295,23 +298,26 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
let shouldReact = true;
|
||||
const lastMessage = events[i];
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const messageReactions = this.props.room.relations
|
||||
.getChildEventsForEvent(lastMessage.getId(), RelationType.Annotation, EventType.Reaction);
|
||||
const messageReactions = this.props.room.relations.getChildEventsForEvent(
|
||||
lastMessage.getId(),
|
||||
RelationType.Annotation,
|
||||
EventType.Reaction,
|
||||
);
|
||||
|
||||
// if we have already sent this reaction, don't redact but don't re-send
|
||||
if (messageReactions) {
|
||||
const myReactionEvents = messageReactions.getAnnotationsBySender()[userId] || [];
|
||||
const myReactionKeys = [...myReactionEvents]
|
||||
.filter(event => !event.isRedacted())
|
||||
.map(event => event.getRelation().key);
|
||||
.filter((event) => !event.isRedacted())
|
||||
.map((event) => event.getRelation().key);
|
||||
shouldReact = !myReactionKeys.includes(reaction);
|
||||
}
|
||||
if (shouldReact) {
|
||||
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), EventType.Reaction, {
|
||||
"m.relates_to": {
|
||||
"rel_type": RelationType.Annotation,
|
||||
"event_id": lastMessage.getId(),
|
||||
"key": reaction,
|
||||
rel_type: RelationType.Annotation,
|
||||
event_id: lastMessage.getId(),
|
||||
key: reaction,
|
||||
},
|
||||
});
|
||||
dis.dispatch({ action: "message_sent" });
|
||||
|
@ -341,7 +347,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
PosthogAnalytics.instance.trackEvent<ComposerEvent>(posthogEvent);
|
||||
|
||||
// Replace emoticon at the end of the message
|
||||
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
|
||||
if (SettingsStore.getValue("MessageComposerInput.autoReplaceEmoji")) {
|
||||
const indexOfLastPart = model.parts.length - 1;
|
||||
const positionInLastPart = model.parts[indexOfLastPart].text.length;
|
||||
this.editorRef.current?.replaceEmoticon(
|
||||
|
@ -357,9 +363,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
if (!containsEmote(model) && isSlashCommand(this.model)) {
|
||||
const [cmd, args, commandText] = getSlashCommand(this.model);
|
||||
if (cmd) {
|
||||
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
|
||||
? this.props.relation?.event_id
|
||||
: null;
|
||||
const threadId =
|
||||
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation?.event_id : null;
|
||||
|
||||
let commandSuccessful: boolean;
|
||||
[content, commandSuccessful] = await runSlashCommand(cmd, args, this.props.room.roomId, threadId);
|
||||
|
@ -379,7 +384,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
} else {
|
||||
shouldSend = false;
|
||||
}
|
||||
} else if (!await shouldSendAnyway(commandText)) {
|
||||
} else if (!(await shouldSendAnyway(commandText))) {
|
||||
// if !sendAnyway bail to let the user edit the composer and try again
|
||||
return;
|
||||
}
|
||||
|
@ -408,9 +413,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
|
||||
? this.props.relation.event_id
|
||||
: null;
|
||||
const threadId =
|
||||
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
|
||||
|
||||
const prom = doMaybeLocalRoomAction(
|
||||
roomId,
|
||||
|
@ -421,7 +425,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
action: "reply_to_event",
|
||||
event: null,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
|
@ -438,7 +442,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
}
|
||||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
prom.then((resp) => {
|
||||
sendRoundTripMetric(this.props.mxClient, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
|
@ -486,10 +490,10 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
if (json) {
|
||||
try {
|
||||
const { parts: serializedParts, replyEventId } = JSON.parse(json);
|
||||
const parts: Part[] = serializedParts.map(p => partCreator.deserializePart(p));
|
||||
const parts: Part[] = serializedParts.map((p) => partCreator.deserializePart(p));
|
||||
if (replyEventId) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
action: "reply_to_event",
|
||||
event: this.props.room.findEventById(replyEventId),
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
|
@ -521,7 +525,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
if (this.props.disabled) return;
|
||||
|
||||
switch (payload.action) {
|
||||
case 'reply_to_event':
|
||||
case "reply_to_event":
|
||||
case Action.FocusSendMessageComposer:
|
||||
if ((payload.context ?? TimelineRenderingType.Room) === this.context.timelineRenderingType) {
|
||||
this.editorRef.current?.focus();
|
||||
|
@ -569,9 +573,8 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
};
|
||||
|
||||
render() {
|
||||
const threadId = this.props.relation?.rel_type === THREAD_RELATION_TYPE.name
|
||||
? this.props.relation.event_id
|
||||
: null;
|
||||
const threadId =
|
||||
this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null;
|
||||
return (
|
||||
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this.onKeyDown}>
|
||||
<BasicMessageComposer
|
||||
|
|
|
@ -14,27 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
|
||||
import React from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils, { IWidgetEvent } from '../../../utils/WidgetUtils';
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import WidgetUtils, { IWidgetEvent } from "../../../utils/WidgetUtils";
|
||||
import PersistedElement from "../elements/PersistedElement";
|
||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||
import ContextMenu, { ChevronFace } from "../../structures/ContextMenu";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
import ScalarAuthClient from "../../../ScalarAuthClient";
|
||||
import GenericElementContextMenu from "../context_menus/GenericElementContextMenu";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
import RightPanelStore from '../../../stores/right-panel/RightPanelStore';
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
// 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.
|
||||
|
@ -87,12 +87,15 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
// TODO: Pick the right manager for the widget
|
||||
if (IntegrationManagers.sharedInstance().hasManager()) {
|
||||
this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient();
|
||||
return this.scalarClient.connect().then(() => {
|
||||
this.forceUpdate();
|
||||
return this.scalarClient;
|
||||
}).catch((e) => {
|
||||
this.imError(_td("Failed to connect to integration manager"), e);
|
||||
});
|
||||
return this.scalarClient
|
||||
.connect()
|
||||
.then(() => {
|
||||
this.forceUpdate();
|
||||
return this.scalarClient;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.imError(_td("Failed to connect to integration manager"), e);
|
||||
});
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().openNoManagerDialog();
|
||||
}
|
||||
|
@ -100,32 +103,37 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private removeStickerpickerWidgets = async (): Promise<void> => {
|
||||
const scalarClient = await this.acquireScalarClient();
|
||||
logger.log('Removing Stickerpicker widgets');
|
||||
logger.log("Removing Stickerpicker widgets");
|
||||
if (this.state.widgetId) {
|
||||
if (scalarClient) {
|
||||
scalarClient.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId).then(() => {
|
||||
logger.log('Assets disabled');
|
||||
}).catch(() => {
|
||||
logger.error('Failed to disable assets');
|
||||
});
|
||||
scalarClient
|
||||
.disableWidgetAssets(WidgetType.STICKERPICKER, this.state.widgetId)
|
||||
.then(() => {
|
||||
logger.log("Assets disabled");
|
||||
})
|
||||
.catch(() => {
|
||||
logger.error("Failed to disable assets");
|
||||
});
|
||||
} else {
|
||||
logger.error("Cannot disable assets: no scalar client");
|
||||
}
|
||||
} else {
|
||||
logger.warn('No widget ID specified, not disabling assets');
|
||||
logger.warn("No widget ID specified, not disabling assets");
|
||||
}
|
||||
|
||||
this.props.setStickerPickerOpen(false);
|
||||
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
logger.error('Failed to remove sticker picker widget', e);
|
||||
});
|
||||
WidgetUtils.removeStickerpickerWidgets()
|
||||
.then(() => {
|
||||
this.forceUpdate();
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error("Failed to remove sticker picker widget", e);
|
||||
});
|
||||
};
|
||||
|
||||
public componentDidMount(): void {
|
||||
// Close the sticker picker when the window resizes
|
||||
window.addEventListener('resize', this.onResize);
|
||||
window.addEventListener("resize", this.onResize);
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
||||
|
@ -141,7 +149,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
const client = MatrixClientPeg.get();
|
||||
if (client) client.removeListener(RoomEvent.AccountData, this.updateWidget);
|
||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
@ -211,10 +219,9 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
|
||||
private defaultStickerpickerContent(): JSX.Element {
|
||||
return (
|
||||
<AccessibleButton onClick={this.launchManageIntegrations}
|
||||
className='mx_Stickers_contentPlaceholder'>
|
||||
<p>{ _t("You don't currently have any stickerpacks enabled") }</p>
|
||||
<p className='mx_Stickers_addLink'>{ _t("Add some now") }</p>
|
||||
<AccessibleButton onClick={this.launchManageIntegrations} className="mx_Stickers_contentPlaceholder">
|
||||
<p>{_t("You don't currently have any stickerpacks enabled")}</p>
|
||||
<p className="mx_Stickers_addLink">{_t("Add some now")}</p>
|
||||
<img src={require("../../../../res/img/stickerpack-placeholder.png")} alt="" />
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -223,7 +230,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
private errorStickerpickerContent(): JSX.Element {
|
||||
return (
|
||||
<div style={{ textAlign: "center" }} className="error">
|
||||
<p> { this.state.imError } </p>
|
||||
<p> {this.state.imError} </p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -234,7 +241,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
WidgetUtils.calcWidgetUid(this.state.stickerpickerWidget.id, null),
|
||||
);
|
||||
if (messaging && visible !== this.prevSentVisibility) {
|
||||
messaging.updateVisibility(visible).catch(err => {
|
||||
messaging.updateVisibility(visible).catch((err) => {
|
||||
logger.error("Error updating widget visibility: ", err);
|
||||
});
|
||||
this.prevSentVisibility = visible;
|
||||
|
@ -277,12 +284,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
stickersContent = (
|
||||
<div className='mx_Stickers_content_container'>
|
||||
<div className="mx_Stickers_content_container">
|
||||
<div
|
||||
id='stickersContent'
|
||||
className='mx_Stickers_content'
|
||||
id="stickersContent"
|
||||
className="mx_Stickers_content"
|
||||
style={{
|
||||
border: 'none',
|
||||
border: "none",
|
||||
height: this.popoverHeight,
|
||||
width: this.popoverWidth,
|
||||
}}
|
||||
|
@ -339,28 +346,28 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
|||
*/
|
||||
private launchManageIntegrations = (): void => {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
this.props.room,
|
||||
`type_${WidgetType.STICKERPICKER.preferred}`,
|
||||
this.state.widgetId,
|
||||
);
|
||||
IntegrationManagers.sharedInstance()
|
||||
.getPrimaryManager()
|
||||
.open(this.props.room, `type_${WidgetType.STICKERPICKER.preferred}`, this.state.widgetId);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (!this.props.isStickerPickerOpen) return null;
|
||||
|
||||
return <ContextMenu
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
menuWidth={this.popoverWidth}
|
||||
menuHeight={this.popoverHeight}
|
||||
onFinished={this.onFinished}
|
||||
menuPaddingTop={0}
|
||||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
{...this.props.menuPosition}
|
||||
>
|
||||
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
|
||||
</ContextMenu>;
|
||||
return (
|
||||
<ContextMenu
|
||||
chevronFace={ChevronFace.Bottom}
|
||||
menuWidth={this.popoverWidth}
|
||||
menuHeight={this.popoverHeight}
|
||||
onFinished={this.onFinished}
|
||||
menuPaddingTop={0}
|
||||
menuPaddingLeft={0}
|
||||
menuPaddingRight={0}
|
||||
zIndex={STICKERPICKER_Z_INDEX}
|
||||
{...this.props.menuPosition}
|
||||
>
|
||||
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
@ -28,8 +28,8 @@ import Modal from "../../../Modal";
|
|||
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import ErrorDialog from '../dialogs/ErrorDialog';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
event: MatrixEvent;
|
||||
|
@ -55,7 +55,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
|
||||
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
|
||||
if (typeof(kickLevel) !== 'number') kickLevel = 50;
|
||||
if (typeof kickLevel !== "number") kickLevel = 50;
|
||||
|
||||
const sender = this.room.getMember(this.props.event.getSender());
|
||||
|
||||
|
@ -86,7 +86,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
const isInvited = isValid3pidInvite(ev);
|
||||
|
||||
const newState = { invited: isInvited };
|
||||
if (newDisplayName) newState['displayName'] = newDisplayName;
|
||||
if (newDisplayName) newState["displayName"] = newDisplayName;
|
||||
this.setState(newState);
|
||||
}
|
||||
};
|
||||
|
@ -99,7 +99,8 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
};
|
||||
|
||||
onKickClick = () => {
|
||||
MatrixClientPeg.get().sendStateEvent(this.state.roomId, "m.room.third_party_invite", {}, this.state.stateKey)
|
||||
MatrixClientPeg.get()
|
||||
.sendStateEvent(this.state.roomId, "m.room.third_party_invite", {}, this.state.stateKey)
|
||||
.catch((err) => {
|
||||
logger.error(err);
|
||||
|
||||
|
@ -110,7 +111,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
title: _t("Failed to revoke invite"),
|
||||
description: _t(
|
||||
"Could not revoke the invite. The server may be experiencing a temporary problem or " +
|
||||
"you do not have sufficient permissions to revoke the invite.",
|
||||
"you do not have sufficient permissions to revoke the invite.",
|
||||
),
|
||||
});
|
||||
});
|
||||
|
@ -124,10 +125,10 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
if (this.state.canKick && this.state.invited) {
|
||||
adminTools = (
|
||||
<div className="mx_MemberInfo_container">
|
||||
<h3>{ _t("Admin Tools") }</h3>
|
||||
<h3>{_t("Admin Tools")}</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
<AccessibleButton className="mx_MemberInfo_field" onClick={this.onKickClick}>
|
||||
{ _t("Revoke invite") }
|
||||
{_t("Revoke invite")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -136,31 +137,30 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
|||
|
||||
let scopeHeader;
|
||||
if (this.room.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
<RoomName room={this.room} />
|
||||
</div>;
|
||||
scopeHeader = (
|
||||
<div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
<RoomName room={this.room} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// We shamelessly rip off the MemberInfo styles here.
|
||||
return (
|
||||
<div className="mx_MemberInfo" role="tabpanel">
|
||||
{ scopeHeader }
|
||||
{scopeHeader}
|
||||
<div className="mx_MemberInfo_name">
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"
|
||||
onClick={this.onCancel}
|
||||
title={_t('Close')}
|
||||
/>
|
||||
<h2>{ this.state.displayName }</h2>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel} title={_t("Close")} />
|
||||
<h2>{this.state.displayName}</h2>
|
||||
</div>
|
||||
<div className="mx_MemberInfo_container">
|
||||
<div className="mx_MemberInfo_profile">
|
||||
<div className="mx_MemberInfo_profileField">
|
||||
{ _t("Invited by %(sender)s", { sender: this.state.senderName }) }
|
||||
{_t("Invited by %(sender)s", { sender: this.state.senderName })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ adminTools }
|
||||
{adminTools}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -62,9 +62,7 @@ const ThreadSummary = ({ mxEvent, thread, ...props }: IProps) => {
|
|||
}}
|
||||
aria-label={_t("Open thread")}
|
||||
>
|
||||
<span className="mx_ThreadSummary_replies_amount">
|
||||
{ countSection }
|
||||
</span>
|
||||
<span className="mx_ThreadSummary_replies_amount">{countSection}</span>
|
||||
<ThreadMessagePreview thread={thread} showDisplayname={!roomContext.narrow} />
|
||||
<div className="mx_ThreadSummary_chevron" />
|
||||
</AccessibleButton>
|
||||
|
@ -99,23 +97,23 @@ export const ThreadMessagePreview = ({ thread, showDisplayname = false }: IPrevi
|
|||
return null;
|
||||
}
|
||||
|
||||
return <>
|
||||
<MemberAvatar
|
||||
member={lastReply.sender}
|
||||
fallbackUserId={lastReply.getSender()}
|
||||
width={24}
|
||||
height={24}
|
||||
className="mx_ThreadSummary_avatar"
|
||||
/>
|
||||
{ showDisplayname && <div className="mx_ThreadSummary_sender">
|
||||
{ lastReply.sender?.name ?? lastReply.getSender() }
|
||||
</div> }
|
||||
<div className="mx_ThreadSummary_content" title={preview}>
|
||||
<span className="mx_ThreadSummary_message-preview">
|
||||
{ preview }
|
||||
</span>
|
||||
</div>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<MemberAvatar
|
||||
member={lastReply.sender}
|
||||
fallbackUserId={lastReply.getSender()}
|
||||
width={24}
|
||||
height={24}
|
||||
className="mx_ThreadSummary_avatar"
|
||||
/>
|
||||
{showDisplayname && (
|
||||
<div className="mx_ThreadSummary_sender">{lastReply.sender?.name ?? lastReply.getSender()}</div>
|
||||
)}
|
||||
<div className="mx_ThreadSummary_content" title={preview}>
|
||||
<span className="mx_ThreadSummary_message-preview">{preview}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThreadSummary;
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
|
||||
interface IProps {
|
||||
onScrollUpClick?: (e: React.MouseEvent) => void;
|
||||
|
@ -30,12 +30,12 @@ export default class TopUnreadMessagesBar extends React.PureComponent<IProps> {
|
|||
<div className="mx_TopUnreadMessagesBar">
|
||||
<AccessibleButton
|
||||
className="mx_TopUnreadMessagesBar_scrollUp"
|
||||
title={_t('Jump to first unread message.')}
|
||||
title={_t("Jump to first unread message.")}
|
||||
onClick={this.props.onScrollUpClick}
|
||||
/>
|
||||
<AccessibleButton
|
||||
className="mx_TopUnreadMessagesBar_markAsRead"
|
||||
title={_t('Mark all as read')}
|
||||
title={_t("Mark all as read")}
|
||||
onClick={this.props.onCloseClick}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -128,7 +128,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
Math.round(this.state.recorder.durationSeconds * 1000),
|
||||
this.state.recorder.contentLength,
|
||||
upload.encrypted,
|
||||
this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
|
||||
this.state.recorder.getPlayback().thumbnailWaveform.map((v) => Math.round(v * 1024)),
|
||||
);
|
||||
|
||||
attachRelation(content, relation);
|
||||
|
@ -140,15 +140,14 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
defaultDispatcher.dispatch({
|
||||
action: 'reply_to_event',
|
||||
action: "reply_to_event",
|
||||
event: null,
|
||||
context: this.context.timelineRenderingType,
|
||||
});
|
||||
}
|
||||
|
||||
doMaybeLocalRoomAction(
|
||||
this.props.room.roomId,
|
||||
(actualRoomId: string) => MatrixClientPeg.get().sendMessage(actualRoomId, content),
|
||||
doMaybeLocalRoomAction(this.props.room.roomId, (actualRoomId: string) =>
|
||||
MatrixClientPeg.get().sendMessage(actualRoomId, content),
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Error sending voice message:", e);
|
||||
|
@ -181,11 +180,15 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
const accessError = () => {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("Unable to access your microphone"),
|
||||
description: <>
|
||||
<p>{ _t(
|
||||
"We were unable to access your microphone. Please check your browser settings and try again.",
|
||||
) }</p>
|
||||
</>,
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
"We were unable to access your microphone. Please check your browser settings and try again.",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -196,11 +199,15 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
if (!devices?.[MediaDeviceKindEnum.AudioInput]?.length) {
|
||||
Modal.createDialog(ErrorDialog, {
|
||||
title: _t("No microphone found"),
|
||||
description: <>
|
||||
<p>{ _t(
|
||||
"We didn't find a microphone on your device. Please check your settings and try again.",
|
||||
) }</p>
|
||||
</>,
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
{_t(
|
||||
"We didn't find a microphone on your device. Please check your settings and try again.",
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -247,17 +254,16 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
|
||||
|
||||
if (this.state.recordingPhase !== RecordingState.Started) {
|
||||
return <RecordingPlayback
|
||||
playback={this.state.recorder.getPlayback()}
|
||||
layout={PlaybackLayout.Composer}
|
||||
/>;
|
||||
return <RecordingPlayback playback={this.state.recorder.getPlayback()} layout={PlaybackLayout.Composer} />;
|
||||
}
|
||||
|
||||
// only other UI is the recording-in-progress UI
|
||||
return <div className="mx_MediaBody mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
|
||||
<LiveRecordingClock recorder={this.state.recorder} />
|
||||
<LiveRecordingWaveform recorder={this.state.recorder} />
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_MediaBody mx_VoiceMessagePrimaryContainer mx_VoiceRecordComposerTile_recording">
|
||||
<LiveRecordingClock recorder={this.state.recorder} />
|
||||
<LiveRecordingWaveform recorder={this.state.recorder} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
|
@ -271,46 +277,56 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
|||
tooltip = _t("Stop recording");
|
||||
}
|
||||
|
||||
stopBtn = <AccessibleTooltipButton
|
||||
className="mx_VoiceRecordComposerTile_stop"
|
||||
onClick={this.onRecordStartEndClick}
|
||||
title={tooltip}
|
||||
/>;
|
||||
stopBtn = (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_VoiceRecordComposerTile_stop"
|
||||
onClick={this.onRecordStartEndClick}
|
||||
title={tooltip}
|
||||
/>
|
||||
);
|
||||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||
stopBtn = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.recorder && this.state.recordingPhase !== RecordingState.Uploading) {
|
||||
deleteButton = <AccessibleTooltipButton
|
||||
className='mx_VoiceRecordComposerTile_delete'
|
||||
title={_t("Delete")}
|
||||
onClick={this.onCancel}
|
||||
/>;
|
||||
deleteButton = (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_VoiceRecordComposerTile_delete"
|
||||
title={_t("Delete")}
|
||||
onClick={this.onCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let uploadIndicator;
|
||||
if (this.state.recordingPhase === RecordingState.Uploading) {
|
||||
uploadIndicator = <span className='mx_VoiceRecordComposerTile_uploadingState'>
|
||||
<InlineSpinner w={16} h={16} />
|
||||
</span>;
|
||||
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
|
||||
uploadIndicator = <span className='mx_VoiceRecordComposerTile_failedState'>
|
||||
<span className='mx_VoiceRecordComposerTile_uploadState_badge'>
|
||||
{ /* Need to stick the badge in a span to ensure it doesn't create a block component */ }
|
||||
<NotificationBadge
|
||||
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||
/>
|
||||
uploadIndicator = (
|
||||
<span className="mx_VoiceRecordComposerTile_uploadingState">
|
||||
<InlineSpinner w={16} h={16} />
|
||||
</span>
|
||||
<span className='text-warning'>{ _t("Failed to send") }</span>
|
||||
</span>;
|
||||
);
|
||||
} else if (this.state.didUploadFail && this.state.recordingPhase === RecordingState.Ended) {
|
||||
uploadIndicator = (
|
||||
<span className="mx_VoiceRecordComposerTile_failedState">
|
||||
<span className="mx_VoiceRecordComposerTile_uploadState_badge">
|
||||
{/* Need to stick the badge in a span to ensure it doesn't create a block component */}
|
||||
<NotificationBadge
|
||||
notification={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-warning">{_t("Failed to send")}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (<>
|
||||
{ uploadIndicator }
|
||||
{ deleteButton }
|
||||
{ stopBtn }
|
||||
{ this.renderWaveformArea() }
|
||||
</>);
|
||||
return (
|
||||
<>
|
||||
{uploadIndicator}
|
||||
{deleteButton}
|
||||
{stopBtn}
|
||||
{this.renderWaveformArea()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,16 +15,16 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/models/room-member";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { compare } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import * as WhoIsTyping from '../../../WhoIsTyping';
|
||||
import Timer from '../../../utils/Timer';
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import MemberAvatar from '../avatars/MemberAvatar';
|
||||
import * as WhoIsTyping from "../../../WhoIsTyping";
|
||||
import Timer from "../../../utils/Timer";
|
||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
|
||||
interface IProps {
|
||||
// the room this statusbar is representing.
|
||||
|
@ -139,7 +139,9 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
|||
timer.start();
|
||||
timer.finished().then(
|
||||
() => this.removeUserTimer(m.userId), // on elapsed
|
||||
() => {/* aborted */},
|
||||
() => {
|
||||
/* aborted */
|
||||
},
|
||||
);
|
||||
}
|
||||
return delayedStopTypingTimers;
|
||||
|
@ -189,7 +191,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
|||
if (othersCount > 0) {
|
||||
avatars.push(
|
||||
<span className="mx_WhoIsTypingTile_remainingAvatarPlaceholder" key="others">
|
||||
+{ othersCount }
|
||||
+{othersCount}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
@ -199,8 +201,9 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
|||
|
||||
render() {
|
||||
let usersTyping = this.state.usersTyping;
|
||||
const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers)
|
||||
.map((userId) => this.props.room.getMember(userId));
|
||||
const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers).map((userId) =>
|
||||
this.props.room.getMember(userId),
|
||||
);
|
||||
// append the users that have been reported not typing anymore
|
||||
// but have a timeout timer running so they can disappear
|
||||
// when a message comes in
|
||||
|
@ -209,10 +212,7 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
|||
// moved to delayedStopTypingTimers
|
||||
usersTyping.sort((a, b) => compare(a.name, b.name));
|
||||
|
||||
const typingString = WhoIsTyping.whoIsTypingString(
|
||||
usersTyping,
|
||||
this.props.whoIsTypingLimit,
|
||||
);
|
||||
const typingString = WhoIsTyping.whoIsTypingString(usersTyping, this.props.whoIsTypingLimit);
|
||||
if (!typingString) {
|
||||
return null;
|
||||
}
|
||||
|
@ -220,11 +220,9 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
|
|||
return (
|
||||
<li className="mx_WhoIsTypingTile" aria-atomic="true">
|
||||
<div className="mx_WhoIsTypingTile_avatars">
|
||||
{ this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||
</div>
|
||||
<div className="mx_WhoIsTypingTile_label">
|
||||
{ typingString }
|
||||
{this.renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit)}
|
||||
</div>
|
||||
<div className="mx_WhoIsTypingTile_label">{typingString}</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,26 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { forwardRef, RefObject } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, { forwardRef, RefObject } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import EditorStateTransfer from '../../../../utils/EditorStateTransfer';
|
||||
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||
import { EditionButtons } from './components/EditionButtons';
|
||||
import { useWysiwygEditActionHandler } from './hooks/useWysiwygEditActionHandler';
|
||||
import { useEditing } from './hooks/useEditing';
|
||||
import { useInitialContent } from './hooks/useInitialContent';
|
||||
import EditorStateTransfer from "../../../../utils/EditorStateTransfer";
|
||||
import { WysiwygComposer } from "./components/WysiwygComposer";
|
||||
import { EditionButtons } from "./components/EditionButtons";
|
||||
import { useWysiwygEditActionHandler } from "./hooks/useWysiwygEditActionHandler";
|
||||
import { useEditing } from "./hooks/useEditing";
|
||||
import { useInitialContent } from "./hooks/useInitialContent";
|
||||
|
||||
interface ContentProps {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||
function Content({ disabled }: ContentProps, forwardRef: RefObject<HTMLElement>) {
|
||||
useWysiwygEditActionHandler(disabled, forwardRef);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
|
||||
{ disabled }: ContentProps,
|
||||
forwardRef: RefObject<HTMLElement>,
|
||||
) {
|
||||
useWysiwygEditActionHandler(disabled, forwardRef);
|
||||
return null;
|
||||
});
|
||||
|
||||
interface EditWysiwygComposerProps {
|
||||
disabled?: boolean;
|
||||
|
@ -48,17 +49,26 @@ export function EditWysiwygComposer({ editorStateTransfer, className, ...props }
|
|||
|
||||
const { editMessage, endEditing, onChange, isSaveDisabled } = useEditing(editorStateTransfer, initialContent);
|
||||
|
||||
return isReady && <WysiwygComposer
|
||||
className={classNames("mx_EditWysiwygComposer", className)}
|
||||
initialContent={initialContent}
|
||||
onChange={onChange}
|
||||
onSend={editMessage}
|
||||
{...props}>
|
||||
{ (ref) => (
|
||||
<>
|
||||
<Content disabled={props.disabled} ref={ref} />
|
||||
<EditionButtons onCancelClick={endEditing} onSaveClick={editMessage} isSaveDisabled={isSaveDisabled} />
|
||||
</>)
|
||||
}
|
||||
</WysiwygComposer>;
|
||||
return (
|
||||
isReady && (
|
||||
<WysiwygComposer
|
||||
className={classNames("mx_EditWysiwygComposer", className)}
|
||||
initialContent={initialContent}
|
||||
onChange={onChange}
|
||||
onSend={editMessage}
|
||||
{...props}
|
||||
>
|
||||
{(ref) => (
|
||||
<>
|
||||
<Content disabled={props.disabled} ref={ref} />
|
||||
<EditionButtons
|
||||
onCancelClick={endEditing}
|
||||
onSaveClick={editMessage}
|
||||
isSaveDisabled={isSaveDisabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</WysiwygComposer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,31 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ForwardedRef, forwardRef, MutableRefObject } from 'react';
|
||||
import React, { ForwardedRef, forwardRef, MutableRefObject } from "react";
|
||||
|
||||
import { useWysiwygSendActionHandler } from './hooks/useWysiwygSendActionHandler';
|
||||
import { WysiwygComposer } from './components/WysiwygComposer';
|
||||
import { PlainTextComposer } from './components/PlainTextComposer';
|
||||
import { ComposerFunctions } from './types';
|
||||
import { E2EStatus } from '../../../../utils/ShieldUtils';
|
||||
import E2EIcon from '../E2EIcon';
|
||||
import { AboveLeftOf } from '../../../structures/ContextMenu';
|
||||
import { Emoji } from './components/Emoji';
|
||||
import { useWysiwygSendActionHandler } from "./hooks/useWysiwygSendActionHandler";
|
||||
import { WysiwygComposer } from "./components/WysiwygComposer";
|
||||
import { PlainTextComposer } from "./components/PlainTextComposer";
|
||||
import { ComposerFunctions } from "./types";
|
||||
import { E2EStatus } from "../../../../utils/ShieldUtils";
|
||||
import E2EIcon from "../E2EIcon";
|
||||
import { AboveLeftOf } from "../../../structures/ContextMenu";
|
||||
import { Emoji } from "./components/Emoji";
|
||||
|
||||
interface ContentProps {
|
||||
disabled?: boolean;
|
||||
composerFunctions: ComposerFunctions;
|
||||
}
|
||||
|
||||
const Content = forwardRef<HTMLElement, ContentProps>(
|
||||
function Content(
|
||||
{ disabled = false, composerFunctions }: ContentProps,
|
||||
forwardRef: ForwardedRef<HTMLElement>,
|
||||
) {
|
||||
useWysiwygSendActionHandler(disabled, forwardRef as MutableRefObject<HTMLElement>, composerFunctions);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
const Content = forwardRef<HTMLElement, ContentProps>(function Content(
|
||||
{ disabled = false, composerFunctions }: ContentProps,
|
||||
forwardRef: ForwardedRef<HTMLElement>,
|
||||
) {
|
||||
useWysiwygSendActionHandler(disabled, forwardRef as MutableRefObject<HTMLElement>, composerFunctions);
|
||||
return null;
|
||||
});
|
||||
|
||||
interface SendWysiwygComposerProps {
|
||||
initialContent?: string;
|
||||
|
@ -51,19 +49,26 @@ interface SendWysiwygComposerProps {
|
|||
menuPosition: AboveLeftOf;
|
||||
}
|
||||
|
||||
export function SendWysiwygComposer(
|
||||
{ isRichTextEnabled, e2eStatus, menuPosition, ...props }: SendWysiwygComposerProps) {
|
||||
export function SendWysiwygComposer({
|
||||
isRichTextEnabled,
|
||||
e2eStatus,
|
||||
menuPosition,
|
||||
...props
|
||||
}: SendWysiwygComposerProps) {
|
||||
const Composer = isRichTextEnabled ? WysiwygComposer : PlainTextComposer;
|
||||
|
||||
return <Composer
|
||||
className="mx_SendWysiwygComposer"
|
||||
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
|
||||
rightComponent={(selectPreviousSelection) =>
|
||||
<Emoji menuPosition={menuPosition} selectPreviousSelection={selectPreviousSelection} />}
|
||||
{...props}
|
||||
>
|
||||
{ (ref, composerFunctions) => (
|
||||
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||
) }
|
||||
</Composer>;
|
||||
return (
|
||||
<Composer
|
||||
className="mx_SendWysiwygComposer"
|
||||
leftComponent={e2eStatus && <E2EIcon status={e2eStatus} />}
|
||||
rightComponent={(selectPreviousSelection) => (
|
||||
<Emoji menuPosition={menuPosition} selectPreviousSelection={selectPreviousSelection} />
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{(ref, composerFunctions) => (
|
||||
<Content disabled={props.disabled} ref={ref} composerFunctions={composerFunctions} />
|
||||
)}
|
||||
</Composer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { MouseEventHandler } from 'react';
|
||||
import React, { MouseEventHandler } from "react";
|
||||
|
||||
import { _t } from '../../../../../languageHandler';
|
||||
import AccessibleButton from '../../../elements/AccessibleButton';
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
|
||||
interface EditionButtonsProps {
|
||||
onCancelClick: MouseEventHandler<HTMLButtonElement>;
|
||||
|
@ -26,12 +26,14 @@ interface EditionButtonsProps {
|
|||
}
|
||||
|
||||
export function EditionButtons({ onCancelClick, onSaveClick, isSaveDisabled = false }: EditionButtonsProps) {
|
||||
return <div className="mx_EditWysiwygComposer_buttons">
|
||||
<AccessibleButton kind="secondary" onClick={onCancelClick}>
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={onSaveClick} disabled={isSaveDisabled}>
|
||||
{ _t("Save") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_EditWysiwygComposer_buttons">
|
||||
<AccessibleButton kind="secondary" onClick={onCancelClick}>
|
||||
{_t("Cancel")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton kind="primary" onClick={onSaveClick} disabled={isSaveDisabled}>
|
||||
{_t("Save")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import React, { CSSProperties, forwardRef, memo, MutableRefObject, ReactNode } from "react";
|
||||
|
||||
import { useIsExpanded } from '../hooks/useIsExpanded';
|
||||
import { useSelection } from '../hooks/useSelection';
|
||||
import { useIsExpanded } from "../hooks/useIsExpanded";
|
||||
import { useSelection } from "../hooks/useSelection";
|
||||
|
||||
const HEIGHT_BREAKING_POINT = 20;
|
||||
|
||||
|
@ -30,40 +30,41 @@ interface EditorProps {
|
|||
}
|
||||
|
||||
export const Editor = memo(
|
||||
forwardRef<HTMLDivElement, EditorProps>(
|
||||
function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref,
|
||||
) {
|
||||
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
||||
const { onFocus, onBlur, selectPreviousSelection, onInput } = useSelection();
|
||||
forwardRef<HTMLDivElement, EditorProps>(function Editor(
|
||||
{ disabled, placeholder, leftComponent, rightComponent }: EditorProps,
|
||||
ref,
|
||||
) {
|
||||
const isExpanded = useIsExpanded(ref as MutableRefObject<HTMLDivElement | null>, HEIGHT_BREAKING_POINT);
|
||||
const { onFocus, onBlur, selectPreviousSelection, onInput } = useSelection();
|
||||
|
||||
return <div
|
||||
return (
|
||||
<div
|
||||
data-testid="WysiwygComposerEditor"
|
||||
className="mx_WysiwygComposer_Editor"
|
||||
data-is-expanded={isExpanded}
|
||||
>
|
||||
{ leftComponent }
|
||||
{leftComponent}
|
||||
<div className="mx_WysiwygComposer_Editor_container">
|
||||
<div className={classNames("mx_WysiwygComposer_Editor_content",
|
||||
{
|
||||
"mx_WysiwygComposer_Editor_content_placeholder": Boolean(placeholder),
|
||||
},
|
||||
)}
|
||||
style={{ "--placeholder": `"${placeholder}"` } as CSSProperties}
|
||||
ref={ref}
|
||||
contentEditable={!disabled}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
dir="auto"
|
||||
aria-disabled={disabled}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onInput={onInput}
|
||||
<div
|
||||
className={classNames("mx_WysiwygComposer_Editor_content", {
|
||||
mx_WysiwygComposer_Editor_content_placeholder: Boolean(placeholder),
|
||||
})}
|
||||
style={{ "--placeholder": `"${placeholder}"` } as CSSProperties}
|
||||
ref={ref}
|
||||
contentEditable={!disabled}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-autocomplete="list"
|
||||
aria-haspopup="listbox"
|
||||
dir="auto"
|
||||
aria-disabled={disabled}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onInput={onInput}
|
||||
/>
|
||||
</div>
|
||||
{ rightComponent?.(selectPreviousSelection) }
|
||||
</div>;
|
||||
},
|
||||
),
|
||||
{rightComponent?.(selectPreviousSelection)}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { AboveLeftOf } from "../../../../structures/ContextMenu";
|
||||
import { EmojiButton } from "../../EmojiButton";
|
||||
import dis from '../../../../../dispatcher/dispatcher';
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { ComposerInsertPayload } from "../../../../../dispatcher/payloads/ComposerInsertPayload";
|
||||
import { Action } from "../../../../../dispatcher/actions";
|
||||
import { useRoomContext } from "../../../../../contexts/RoomContext";
|
||||
|
@ -31,15 +31,18 @@ interface EmojiProps {
|
|||
export function Emoji({ selectPreviousSelection, menuPosition }: EmojiProps) {
|
||||
const roomContext = useRoomContext();
|
||||
|
||||
return <EmojiButton menuPosition={menuPosition}
|
||||
addEmoji={(emoji) => {
|
||||
selectPreviousSelection();
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
timelineRenderingType: roomContext.timelineRenderingType,
|
||||
});
|
||||
return true;
|
||||
}}
|
||||
/>;
|
||||
return (
|
||||
<EmojiButton
|
||||
menuPosition={menuPosition}
|
||||
addEmoji={(emoji) => {
|
||||
selectPreviousSelection();
|
||||
dis.dispatch<ComposerInsertPayload>({
|
||||
action: Action.ComposerInsert,
|
||||
text: emoji,
|
||||
timelineRenderingType: roomContext.timelineRenderingType,
|
||||
});
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@ import React, { MouseEventHandler, ReactNode } from "react";
|
|||
import { FormattingFunctions, AllActionStates } from "@matrix-org/matrix-wysiwyg";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { Icon as BoldIcon } from '../../../../../../res/img/element-icons/room/composer/bold.svg';
|
||||
import { Icon as ItalicIcon } from '../../../../../../res/img/element-icons/room/composer/italic.svg';
|
||||
import { Icon as UnderlineIcon } from '../../../../../../res/img/element-icons/room/composer/underline.svg';
|
||||
import { Icon as StrikeThroughIcon } from '../../../../../../res/img/element-icons/room/composer/strikethrough.svg';
|
||||
import { Icon as InlineCodeIcon } from '../../../../../../res/img/element-icons/room/composer/inline_code.svg';
|
||||
import { Icon as BoldIcon } from "../../../../../../res/img/element-icons/room/composer/bold.svg";
|
||||
import { Icon as ItalicIcon } from "../../../../../../res/img/element-icons/room/composer/italic.svg";
|
||||
import { Icon as UnderlineIcon } from "../../../../../../res/img/element-icons/room/composer/underline.svg";
|
||||
import { Icon as StrikeThroughIcon } from "../../../../../../res/img/element-icons/room/composer/strikethrough.svg";
|
||||
import { Icon as InlineCodeIcon } from "../../../../../../res/img/element-icons/room/composer/inline_code.svg";
|
||||
import AccessibleTooltipButton from "../../../elements/AccessibleTooltipButton";
|
||||
import { Alignment } from "../../../elements/Tooltip";
|
||||
import { KeyboardShortcut } from "../../../settings/KeyboardShortcut";
|
||||
|
@ -36,10 +36,14 @@ interface TooltipProps {
|
|||
}
|
||||
|
||||
function Tooltip({ label, keyCombo }: TooltipProps) {
|
||||
return <div className="mx_FormattingButtons_Tooltip">
|
||||
{ label }
|
||||
{ keyCombo && <KeyboardShortcut value={keyCombo} className="mx_FormattingButtons_Tooltip_KeyboardShortcut" /> }
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_FormattingButtons_Tooltip">
|
||||
{label}
|
||||
{keyCombo && (
|
||||
<KeyboardShortcut value={keyCombo} className="mx_FormattingButtons_Tooltip_KeyboardShortcut" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ButtonProps extends TooltipProps {
|
||||
|
@ -49,20 +53,21 @@ interface ButtonProps extends TooltipProps {
|
|||
}
|
||||
|
||||
function Button({ label, keyCombo, onClick, isActive, icon }: ButtonProps) {
|
||||
return <AccessibleTooltipButton
|
||||
element="button"
|
||||
onClick={onClick as (e: ButtonEvent) => void}
|
||||
title={label}
|
||||
className={
|
||||
classNames('mx_FormattingButtons_Button', {
|
||||
'mx_FormattingButtons_active': isActive,
|
||||
'mx_FormattingButtons_Button_hover': !isActive,
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
element="button"
|
||||
onClick={onClick as (e: ButtonEvent) => void}
|
||||
title={label}
|
||||
className={classNames("mx_FormattingButtons_Button", {
|
||||
mx_FormattingButtons_active: isActive,
|
||||
mx_FormattingButtons_Button_hover: !isActive,
|
||||
})}
|
||||
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
{ icon }
|
||||
</AccessibleTooltipButton>;
|
||||
tooltip={keyCombo && <Tooltip label={label} keyCombo={keyCombo} />}
|
||||
alignment={Alignment.Top}
|
||||
>
|
||||
{icon}
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormattingButtonsProps {
|
||||
|
@ -71,11 +76,42 @@ interface FormattingButtonsProps {
|
|||
}
|
||||
|
||||
export function FormattingButtons({ composer, actionStates }: FormattingButtonsProps) {
|
||||
return <div className="mx_FormattingButtons">
|
||||
<Button isActive={actionStates.bold === 'reversed'} label={_td("Bold")} keyCombo={{ ctrlOrCmdKey: true, key: 'b' }} onClick={() => composer.bold()} icon={<BoldIcon className="mx_FormattingButtons_Icon" />} />
|
||||
<Button isActive={actionStates.italic === 'reversed'} label={_td('Italic')} keyCombo={{ ctrlOrCmdKey: true, key: 'i' }} onClick={() => composer.italic()} icon={<ItalicIcon className="mx_FormattingButtons_Icon" />} />
|
||||
<Button isActive={actionStates.underline === 'reversed'} label={_td('Underline')} keyCombo={{ ctrlOrCmdKey: true, key: 'u' }} onClick={() => composer.underline()} icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />} />
|
||||
<Button isActive={actionStates.strikeThrough === 'reversed'} label={_td('Strikethrough')} onClick={() => composer.strikeThrough()} icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />} />
|
||||
<Button isActive={actionStates.inlineCode === 'reversed'} label={_td('Code')} keyCombo={{ ctrlOrCmdKey: true, key: 'e' }} onClick={() => composer.inlineCode()} icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />} />
|
||||
</div>;
|
||||
return (
|
||||
<div className="mx_FormattingButtons">
|
||||
<Button
|
||||
isActive={actionStates.bold === "reversed"}
|
||||
label={_td("Bold")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "b" }}
|
||||
onClick={() => composer.bold()}
|
||||
icon={<BoldIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.italic === "reversed"}
|
||||
label={_td("Italic")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "i" }}
|
||||
onClick={() => composer.italic()}
|
||||
icon={<ItalicIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.underline === "reversed"}
|
||||
label={_td("Underline")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "u" }}
|
||||
onClick={() => composer.underline()}
|
||||
icon={<UnderlineIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.strikeThrough === "reversed"}
|
||||
label={_td("Strikethrough")}
|
||||
onClick={() => composer.strikeThrough()}
|
||||
icon={<StrikeThroughIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
<Button
|
||||
isActive={actionStates.inlineCode === "reversed"}
|
||||
label={_td("Code")}
|
||||
keyCombo={{ ctrlOrCmdKey: true, key: "e" }}
|
||||
onClick={() => composer.inlineCode()}
|
||||
icon={<InlineCodeIcon className="mx_FormattingButtons_Icon" />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, { MutableRefObject, ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import React, { MutableRefObject, ReactNode } from "react";
|
||||
|
||||
import { useComposerFunctions } from '../hooks/useComposerFunctions';
|
||||
import { useIsFocused } from '../hooks/useIsFocused';
|
||||
import { usePlainTextInitialization } from '../hooks/usePlainTextInitialization';
|
||||
import { usePlainTextListeners } from '../hooks/usePlainTextListeners';
|
||||
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||
import { ComposerFunctions } from '../types';
|
||||
import { useComposerFunctions } from "../hooks/useComposerFunctions";
|
||||
import { useIsFocused } from "../hooks/useIsFocused";
|
||||
import { usePlainTextInitialization } from "../hooks/usePlainTextInitialization";
|
||||
import { usePlainTextListeners } from "../hooks/usePlainTextListeners";
|
||||
import { useSetCursorPosition } from "../hooks/useSetCursorPosition";
|
||||
import { ComposerFunctions } from "../types";
|
||||
import { Editor } from "./Editor";
|
||||
|
||||
interface PlainTextComposerProps {
|
||||
|
@ -33,13 +33,8 @@ interface PlainTextComposerProps {
|
|||
initialContent?: string;
|
||||
className?: string;
|
||||
leftComponent?: ReactNode;
|
||||
rightComponent?: (
|
||||
selectPreviousSelection: () => void
|
||||
) => ReactNode;
|
||||
children?: (
|
||||
ref: MutableRefObject<HTMLDivElement | null>,
|
||||
composerFunctions: ComposerFunctions,
|
||||
) => ReactNode;
|
||||
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
|
||||
children?: (ref: MutableRefObject<HTMLDivElement | null>, composerFunctions: ComposerFunctions) => ReactNode;
|
||||
}
|
||||
|
||||
export function PlainTextComposer({
|
||||
|
@ -52,26 +47,36 @@ export function PlainTextComposer({
|
|||
initialContent,
|
||||
leftComponent,
|
||||
rightComponent,
|
||||
}: PlainTextComposerProps,
|
||||
) {
|
||||
const { ref, onInput, onPaste, onKeyDown, content, setContent } =
|
||||
usePlainTextListeners(initialContent, onChange, onSend);
|
||||
}: PlainTextComposerProps) {
|
||||
const { ref, onInput, onPaste, onKeyDown, content, setContent } = usePlainTextListeners(
|
||||
initialContent,
|
||||
onChange,
|
||||
onSend,
|
||||
);
|
||||
const composerFunctions = useComposerFunctions(ref, setContent);
|
||||
usePlainTextInitialization(initialContent, ref);
|
||||
useSetCursorPosition(disabled, ref);
|
||||
const { isFocused, onFocus } = useIsFocused();
|
||||
const computedPlaceholder = !content && placeholder || undefined;
|
||||
const computedPlaceholder = (!content && placeholder) || undefined;
|
||||
|
||||
return <div
|
||||
data-testid="PlainTextComposer"
|
||||
className={classNames(className, { [`${className}-focused`]: isFocused })}
|
||||
onFocus={onFocus}
|
||||
onBlur={onFocus}
|
||||
onInput={onInput}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<Editor ref={ref} disabled={disabled} leftComponent={leftComponent} rightComponent={rightComponent} placeholder={computedPlaceholder} />
|
||||
{ children?.(ref, composerFunctions) }
|
||||
</div>;
|
||||
return (
|
||||
<div
|
||||
data-testid="PlainTextComposer"
|
||||
className={classNames(className, { [`${className}-focused`]: isFocused })}
|
||||
onFocus={onFocus}
|
||||
onBlur={onFocus}
|
||||
onInput={onInput}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<Editor
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
leftComponent={leftComponent}
|
||||
rightComponent={rightComponent}
|
||||
placeholder={computedPlaceholder}
|
||||
/>
|
||||
{children?.(ref, composerFunctions)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { memo, MutableRefObject, ReactNode, useEffect } from 'react';
|
||||
import React, { memo, MutableRefObject, ReactNode, useEffect } from "react";
|
||||
import { useWysiwyg, FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
|
||||
import { FormattingButtons } from './FormattingButtons';
|
||||
import { Editor } from './Editor';
|
||||
import { useInputEventProcessor } from '../hooks/useInputEventProcessor';
|
||||
import { useSetCursorPosition } from '../hooks/useSetCursorPosition';
|
||||
import { useIsFocused } from '../hooks/useIsFocused';
|
||||
import { FormattingButtons } from "./FormattingButtons";
|
||||
import { Editor } from "./Editor";
|
||||
import { useInputEventProcessor } from "../hooks/useInputEventProcessor";
|
||||
import { useSetCursorPosition } from "../hooks/useSetCursorPosition";
|
||||
import { useIsFocused } from "../hooks/useIsFocused";
|
||||
|
||||
interface WysiwygComposerProps {
|
||||
disabled?: boolean;
|
||||
|
@ -32,32 +32,24 @@ interface WysiwygComposerProps {
|
|||
initialContent?: string;
|
||||
className?: string;
|
||||
leftComponent?: ReactNode;
|
||||
rightComponent?: (
|
||||
selectPreviousSelection: () => void
|
||||
) => ReactNode;
|
||||
children?: (
|
||||
ref: MutableRefObject<HTMLDivElement | null>,
|
||||
wysiwyg: FormattingFunctions,
|
||||
) => ReactNode;
|
||||
rightComponent?: (selectPreviousSelection: () => void) => ReactNode;
|
||||
children?: (ref: MutableRefObject<HTMLDivElement | null>, wysiwyg: FormattingFunctions) => ReactNode;
|
||||
}
|
||||
|
||||
export const WysiwygComposer = memo(function WysiwygComposer(
|
||||
{
|
||||
disabled = false,
|
||||
onChange,
|
||||
onSend,
|
||||
placeholder,
|
||||
initialContent,
|
||||
className,
|
||||
leftComponent,
|
||||
rightComponent,
|
||||
children,
|
||||
}: WysiwygComposerProps,
|
||||
) {
|
||||
export const WysiwygComposer = memo(function WysiwygComposer({
|
||||
disabled = false,
|
||||
onChange,
|
||||
onSend,
|
||||
placeholder,
|
||||
initialContent,
|
||||
className,
|
||||
leftComponent,
|
||||
rightComponent,
|
||||
children,
|
||||
}: WysiwygComposerProps) {
|
||||
const inputEventProcessor = useInputEventProcessor(onSend);
|
||||
|
||||
const { ref, isWysiwygReady, content, actionStates, wysiwyg } =
|
||||
useWysiwyg({ initialContent, inputEventProcessor });
|
||||
const { ref, isWysiwygReady, content, actionStates, wysiwyg } = useWysiwyg({ initialContent, inputEventProcessor });
|
||||
|
||||
useEffect(() => {
|
||||
if (!disabled && content !== null) {
|
||||
|
@ -69,13 +61,24 @@ export const WysiwygComposer = memo(function WysiwygComposer(
|
|||
useSetCursorPosition(!isReady, ref);
|
||||
|
||||
const { isFocused, onFocus } = useIsFocused();
|
||||
const computedPlaceholder = !content && placeholder || undefined;
|
||||
const computedPlaceholder = (!content && placeholder) || undefined;
|
||||
|
||||
return (
|
||||
<div data-testid="WysiwygComposer" className={classNames(className, { [`${className}-focused`]: isFocused })} onFocus={onFocus} onBlur={onFocus}>
|
||||
<div
|
||||
data-testid="WysiwygComposer"
|
||||
className={classNames(className, { [`${className}-focused`]: isFocused })}
|
||||
onFocus={onFocus}
|
||||
onBlur={onFocus}
|
||||
>
|
||||
<FormattingButtons composer={wysiwyg} actionStates={actionStates} />
|
||||
<Editor ref={ref} disabled={!isReady} leftComponent={leftComponent} rightComponent={rightComponent} placeholder={computedPlaceholder} />
|
||||
{ children?.(ref, wysiwyg) }
|
||||
<Editor
|
||||
ref={ref}
|
||||
disabled={!isReady}
|
||||
leftComponent={leftComponent}
|
||||
rightComponent={rightComponent}
|
||||
placeholder={computedPlaceholder}
|
||||
/>
|
||||
{children?.(ref, wysiwyg)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -19,27 +19,30 @@ import { RefObject, useMemo } from "react";
|
|||
import { setSelection } from "../utils/selection";
|
||||
|
||||
export function useComposerFunctions(ref: RefObject<HTMLDivElement>, setContent: (content: string) => void) {
|
||||
return useMemo(() => ({
|
||||
clear: () => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = '';
|
||||
}
|
||||
},
|
||||
insertText: (text: string) => {
|
||||
const selection = document.getSelection();
|
||||
return useMemo(
|
||||
() => ({
|
||||
clear: () => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = "";
|
||||
}
|
||||
},
|
||||
insertText: (text: string) => {
|
||||
const selection = document.getSelection();
|
||||
|
||||
if (ref.current && selection) {
|
||||
const content = ref.current.innerHTML;
|
||||
const { anchorOffset, focusOffset } = selection;
|
||||
ref.current.innerHTML = `${content.slice(0, anchorOffset)}${text}${content.slice(focusOffset)}`;
|
||||
setSelection({
|
||||
anchorNode: ref.current.firstChild,
|
||||
anchorOffset: anchorOffset + text.length,
|
||||
focusNode: ref.current.firstChild,
|
||||
focusOffset: focusOffset + text.length,
|
||||
});
|
||||
setContent(ref.current.innerHTML);
|
||||
}
|
||||
},
|
||||
}), [ref, setContent]);
|
||||
if (ref.current && selection) {
|
||||
const content = ref.current.innerHTML;
|
||||
const { anchorOffset, focusOffset } = selection;
|
||||
ref.current.innerHTML = `${content.slice(0, anchorOffset)}${text}${content.slice(focusOffset)}`;
|
||||
setSelection({
|
||||
anchorNode: ref.current.firstChild,
|
||||
anchorOffset: anchorOffset + text.length,
|
||||
focusNode: ref.current.firstChild,
|
||||
focusOffset: focusOffset + text.length,
|
||||
});
|
||||
setContent(ref.current.innerHTML);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[ref, setContent],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,14 +28,17 @@ export function useEditing(editorStateTransfer: EditorStateTransfer, initialCont
|
|||
|
||||
const [isSaveDisabled, setIsSaveDisabled] = useState(true);
|
||||
const [content, setContent] = useState(initialContent);
|
||||
const onChange = useCallback((_content: string) => {
|
||||
setContent(_content);
|
||||
setIsSaveDisabled(_isSaveDisabled => _isSaveDisabled && _content === initialContent);
|
||||
}, [initialContent]);
|
||||
const onChange = useCallback(
|
||||
(_content: string) => {
|
||||
setContent(_content);
|
||||
setIsSaveDisabled((_isSaveDisabled) => _isSaveDisabled && _content === initialContent);
|
||||
},
|
||||
[initialContent],
|
||||
);
|
||||
|
||||
const editMessageMemoized = useCallback(() =>
|
||||
content !== undefined && editMessage(content, { roomContext, mxClient, editorStateTransfer }),
|
||||
[content, roomContext, mxClient, editorStateTransfer],
|
||||
const editMessageMemoized = useCallback(
|
||||
() => content !== undefined && editMessage(content, { roomContext, mxClient, editorStateTransfer }),
|
||||
[content, roomContext, mxClient, editorStateTransfer],
|
||||
);
|
||||
|
||||
const endEditingMemoized = useCallback(() => endEditing(roomContext), [roomContext]);
|
||||
|
|
|
@ -25,7 +25,12 @@ import SettingsStore from "../../../../../settings/SettingsStore";
|
|||
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||
|
||||
function getFormattedContent(editorStateTransfer: EditorStateTransfer): string {
|
||||
return editorStateTransfer.getEvent().getContent().formatted_body?.replace(/<mx-reply>.*<\/mx-reply>/, '') || '';
|
||||
return (
|
||||
editorStateTransfer
|
||||
.getEvent()
|
||||
.getContent()
|
||||
.formatted_body?.replace(/<mx-reply>.*<\/mx-reply>/, "") || ""
|
||||
);
|
||||
}
|
||||
|
||||
function parseEditorStateTransfer(
|
||||
|
@ -39,13 +44,13 @@ function parseEditorStateTransfer(
|
|||
if (editorStateTransfer.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
// restore serialized parts from the state
|
||||
parts = editorStateTransfer.getSerializedParts().map(p => partCreator.deserializePart(p));
|
||||
parts = editorStateTransfer.getSerializedParts().map((p) => partCreator.deserializePart(p));
|
||||
} else {
|
||||
// otherwise, either restore serialized parts from localStorage or parse the body of the event
|
||||
// TODO local storage
|
||||
// const restoredParts = this.restoreStoredEditorState(partCreator);
|
||||
|
||||
if (editorStateTransfer.getEvent().getContent().format === 'org.matrix.custom.html') {
|
||||
if (editorStateTransfer.getEvent().getContent().format === "org.matrix.custom.html") {
|
||||
return getFormattedContent(editorStateTransfer);
|
||||
}
|
||||
|
||||
|
@ -54,7 +59,7 @@ function parseEditorStateTransfer(
|
|||
});
|
||||
}
|
||||
|
||||
return parts.reduce((content, part) => content + part.text, '');
|
||||
return parts.reduce((content, part) => content + part.text, "");
|
||||
// Todo local storage
|
||||
// this.saveStoredEditorState();
|
||||
}
|
||||
|
|
|
@ -21,20 +21,19 @@ import { useSettingValue } from "../../../../../hooks/useSettings";
|
|||
|
||||
export function useInputEventProcessor(onSend: () => void) {
|
||||
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||
return useCallback((event: WysiwygInputEvent) => {
|
||||
if (event instanceof ClipboardEvent) {
|
||||
return useCallback(
|
||||
(event: WysiwygInputEvent) => {
|
||||
if (event instanceof ClipboardEvent) {
|
||||
return event;
|
||||
}
|
||||
|
||||
if ((event.inputType === "insertParagraph" && !isCtrlEnter) || event.inputType === "sendMessage") {
|
||||
onSend();
|
||||
return null;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
if (
|
||||
(event.inputType === 'insertParagraph' && !isCtrlEnter) ||
|
||||
event.inputType === 'sendMessage'
|
||||
) {
|
||||
onSend();
|
||||
return null;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
, [isCtrlEnter, onSend]);
|
||||
},
|
||||
[isCtrlEnter, onSend],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export function useIsExpanded(ref: MutableRefObject<HTMLElement | null>, breakin
|
|||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const editor = ref.current;
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
requestAnimationFrame(() => {
|
||||
const height = entries[0]?.contentBoxSize?.[0].blockSize;
|
||||
setIsExpanded(height >= breakingPoint);
|
||||
|
|
|
@ -21,16 +21,19 @@ export function useIsFocused() {
|
|||
const timeoutIDRef = useRef<number>();
|
||||
|
||||
useEffect(() => () => clearTimeout(timeoutIDRef.current), [timeoutIDRef]);
|
||||
const onFocus = useCallback((event: FocusEvent<HTMLElement>) => {
|
||||
clearTimeout(timeoutIDRef.current);
|
||||
if (event.type === 'focus') {
|
||||
setIsFocused(true);
|
||||
} else {
|
||||
// To avoid a blink when we switch mode between plain text and rich text mode
|
||||
// We delay the unfocused action
|
||||
timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100);
|
||||
}
|
||||
}, [setIsFocused, timeoutIDRef]);
|
||||
const onFocus = useCallback(
|
||||
(event: FocusEvent<HTMLElement>) => {
|
||||
clearTimeout(timeoutIDRef.current);
|
||||
if (event.type === "focus") {
|
||||
setIsFocused(true);
|
||||
} else {
|
||||
// To avoid a blink when we switch mode between plain text and rich text mode
|
||||
// We delay the unfocused action
|
||||
timeoutIDRef.current = window.setTimeout(() => setIsFocused(false), 100);
|
||||
}
|
||||
},
|
||||
[setIsFocused, timeoutIDRef],
|
||||
);
|
||||
|
||||
return { isFocused, onFocus };
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
import { RefObject, useEffect } from "react";
|
||||
|
||||
export function usePlainTextInitialization(initialContent = '', ref: RefObject<HTMLElement>) {
|
||||
export function usePlainTextInitialization(initialContent = "", ref: RefObject<HTMLElement>) {
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.innerText = initialContent;
|
||||
|
|
|
@ -29,32 +29,41 @@ export function usePlainTextListeners(
|
|||
) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [content, setContent] = useState<string | undefined>(initialContent);
|
||||
const send = useCallback((() => {
|
||||
const send = useCallback(() => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = '';
|
||||
ref.current.innerHTML = "";
|
||||
}
|
||||
onSend?.();
|
||||
}), [ref, onSend]);
|
||||
}, [ref, onSend]);
|
||||
|
||||
const setText = useCallback((text: string) => {
|
||||
setContent(text);
|
||||
onChange?.(text);
|
||||
}, [onChange]);
|
||||
const setText = useCallback(
|
||||
(text: string) => {
|
||||
setContent(text);
|
||||
onChange?.(text);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const onInput = useCallback((event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||
if (isDivElement(event.target)) {
|
||||
setText(event.target.innerHTML);
|
||||
}
|
||||
}, [setText]);
|
||||
const onInput = useCallback(
|
||||
(event: SyntheticEvent<HTMLDivElement, InputEvent | ClipboardEvent>) => {
|
||||
if (isDivElement(event.target)) {
|
||||
setText(event.target.innerHTML);
|
||||
}
|
||||
},
|
||||
[setText],
|
||||
);
|
||||
|
||||
const isCtrlEnter = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
|
||||
const onKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
send();
|
||||
}
|
||||
}, [isCtrlEnter, send]);
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" && !event.shiftKey && (!isCtrlEnter || (isCtrlEnter && event.ctrlKey))) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
send();
|
||||
}
|
||||
},
|
||||
[isCtrlEnter, send],
|
||||
);
|
||||
|
||||
return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText };
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { MutableRefObject, useCallback, useEffect, useRef } from "react";
|
|||
import useFocus from "../../../../../hooks/useFocus";
|
||||
import { setSelection } from "../utils/selection";
|
||||
|
||||
type SubSelection = Pick<Selection, 'anchorNode' | 'anchorOffset' | 'focusNode' | 'focusOffset'>;
|
||||
type SubSelection = Pick<Selection, "anchorNode" | "anchorOffset" | "focusNode" | "focusOffset">;
|
||||
|
||||
function setSelectionRef(selectionRef: MutableRefObject<SubSelection>) {
|
||||
const selection = document.getSelection();
|
||||
|
@ -49,10 +49,10 @@ export function useSelection() {
|
|||
}
|
||||
|
||||
if (isFocused) {
|
||||
document.addEventListener('selectionchange', onSelectionChange);
|
||||
document.addEventListener("selectionchange", onSelectionChange);
|
||||
}
|
||||
|
||||
return () => document.removeEventListener('selectionchange', onSelectionChange);
|
||||
return () => document.removeEventListener("selectionchange", onSelectionChange);
|
||||
}, [isFocused]);
|
||||
|
||||
const onInput = useCallback(() => {
|
||||
|
|
|
@ -23,26 +23,26 @@ import { TimelineRenderingType, useRoomContext } from "../../../../../contexts/R
|
|||
import { useDispatcher } from "../../../../../hooks/useDispatcher";
|
||||
import { focusComposer } from "./utils";
|
||||
|
||||
export function useWysiwygEditActionHandler(
|
||||
disabled: boolean,
|
||||
composerElement: RefObject<HTMLElement>,
|
||||
) {
|
||||
export function useWysiwygEditActionHandler(disabled: boolean, composerElement: RefObject<HTMLElement>) {
|
||||
const roomContext = useRoomContext();
|
||||
const timeoutId = useRef<number | null>(null);
|
||||
|
||||
const handler = useCallback((payload: ActionPayload) => {
|
||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||
// to the cursor being in the composer
|
||||
if (disabled || !composerElement.current) return;
|
||||
const handler = useCallback(
|
||||
(payload: ActionPayload) => {
|
||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||
// to the cursor being in the composer
|
||||
if (disabled || !composerElement.current) return;
|
||||
|
||||
const context = payload.context ?? TimelineRenderingType.Room;
|
||||
const context = payload.context ?? TimelineRenderingType.Room;
|
||||
|
||||
switch (payload.action) {
|
||||
case Action.FocusEditMessageComposer:
|
||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||
break;
|
||||
}
|
||||
}, [disabled, composerElement, timeoutId, roomContext]);
|
||||
switch (payload.action) {
|
||||
case Action.FocusEditMessageComposer:
|
||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[disabled, composerElement, timeoutId, roomContext],
|
||||
);
|
||||
|
||||
useDispatcher(defaultDispatcher, handler);
|
||||
}
|
||||
|
|
|
@ -33,36 +33,39 @@ export function useWysiwygSendActionHandler(
|
|||
const roomContext = useRoomContext();
|
||||
const timeoutId = useRef<number | null>(null);
|
||||
|
||||
const handler = useCallback((payload: ActionPayload) => {
|
||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||
// to the cursor being in the composer
|
||||
if (disabled || !composerElement?.current) return;
|
||||
const handler = useCallback(
|
||||
(payload: ActionPayload) => {
|
||||
// don't let the user into the composer if it is disabled - all of these branches lead
|
||||
// to the cursor being in the composer
|
||||
if (disabled || !composerElement?.current) return;
|
||||
|
||||
const context = payload.context ?? TimelineRenderingType.Room;
|
||||
const context = payload.context ?? TimelineRenderingType.Room;
|
||||
|
||||
switch (payload.action) {
|
||||
case "reply_to_event":
|
||||
case Action.FocusSendMessageComposer:
|
||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||
break;
|
||||
case Action.ClearAndFocusSendMessageComposer:
|
||||
composerFunctions.clear();
|
||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||
break;
|
||||
case Action.ComposerInsert:
|
||||
if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break;
|
||||
if (payload.composerType !== ComposerType.Send) break;
|
||||
switch (payload.action) {
|
||||
case "reply_to_event":
|
||||
case Action.FocusSendMessageComposer:
|
||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||
break;
|
||||
case Action.ClearAndFocusSendMessageComposer:
|
||||
composerFunctions.clear();
|
||||
focusComposer(composerElement, context, roomContext, timeoutId);
|
||||
break;
|
||||
case Action.ComposerInsert:
|
||||
if (payload.timelineRenderingType !== roomContext.timelineRenderingType) break;
|
||||
if (payload.composerType !== ComposerType.Send) break;
|
||||
|
||||
if (payload.userId) {
|
||||
// TODO insert mention - see SendMessageComposer
|
||||
} else if (payload.event) {
|
||||
// TODO insert quote message - see SendMessageComposer
|
||||
} else if (payload.text) {
|
||||
composerFunctions.insertText(payload.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, [disabled, composerElement, composerFunctions, timeoutId, roomContext]);
|
||||
if (payload.userId) {
|
||||
// TODO insert mention - see SendMessageComposer
|
||||
} else if (payload.event) {
|
||||
// TODO insert quote message - see SendMessageComposer
|
||||
} else if (payload.text) {
|
||||
composerFunctions.insertText(payload.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
[disabled, composerElement, composerFunctions, timeoutId, roomContext],
|
||||
);
|
||||
|
||||
useDispatcher(defaultDispatcher, handler);
|
||||
}
|
||||
|
|
|
@ -37,10 +37,7 @@ export function focusComposer(
|
|||
if (timeoutId.current) {
|
||||
clearTimeout(timeoutId.current);
|
||||
}
|
||||
timeoutId.current = window.setTimeout(
|
||||
() => composerElement.current?.focus(),
|
||||
200,
|
||||
);
|
||||
timeoutId.current = window.setTimeout(() => composerElement.current?.focus(), 200);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export { SendWysiwygComposer } from './SendWysiwygComposer';
|
||||
export { EditWysiwygComposer } from './EditWysiwygComposer';
|
||||
export { sendMessage } from './utils/message';
|
||||
export { SendWysiwygComposer } from "./SendWysiwygComposer";
|
||||
export { EditWysiwygComposer } from "./EditWysiwygComposer";
|
||||
export { sendMessage } from "./utils/message";
|
||||
|
|
|
@ -25,8 +25,8 @@ import { htmlToPlainText } from "../../../../../utils/room/htmlToPlaintext";
|
|||
// Merges favouring the given relation
|
||||
function attachRelation(content: IContent, relation?: IEventRelation): void {
|
||||
if (relation) {
|
||||
content['m.relates_to'] = {
|
||||
...(content['m.relates_to'] || {}),
|
||||
content["m.relates_to"] = {
|
||||
...(content["m.relates_to"] || {}),
|
||||
...relation,
|
||||
};
|
||||
}
|
||||
|
@ -44,10 +44,10 @@ function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
|
|||
|
||||
function getTextReplyFallback(mxEvent: MatrixEvent): string {
|
||||
const body = mxEvent.getContent().body;
|
||||
if (typeof body !== 'string') {
|
||||
if (typeof body !== "string") {
|
||||
return "";
|
||||
}
|
||||
const lines = body.split("\n").map(l => l.trim());
|
||||
const lines = body.split("\n").map((l) => l.trim());
|
||||
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
|
||||
return `${lines[0]}\n\n`;
|
||||
}
|
||||
|
@ -65,8 +65,13 @@ interface CreateMessageContentParams {
|
|||
export function createMessageContent(
|
||||
message: string,
|
||||
isHTML: boolean,
|
||||
{ relation, replyToEvent, permalinkCreator, includeReplyLegacyFallback = true, editedEvent }:
|
||||
CreateMessageContentParams,
|
||||
{
|
||||
relation,
|
||||
replyToEvent,
|
||||
permalinkCreator,
|
||||
includeReplyLegacyFallback = true,
|
||||
editedEvent,
|
||||
}: CreateMessageContentParams,
|
||||
): IContent {
|
||||
// TODO emote ?
|
||||
|
||||
|
@ -86,9 +91,9 @@ export function createMessageContent(
|
|||
// const body = textSerialize(model);
|
||||
|
||||
// TODO remove this ugly hack for replace br tag
|
||||
const body = isHTML && htmlToPlainText(message) || message.replace(/<br>/g, '\n');
|
||||
const bodyPrefix = isReplyAndEditing && getTextReplyFallback(editedEvent) || '';
|
||||
const formattedBodyPrefix = isReplyAndEditing && getHtmlReplyFallback(editedEvent) || '';
|
||||
const body = (isHTML && htmlToPlainText(message)) || message.replace(/<br>/g, "\n");
|
||||
const bodyPrefix = (isReplyAndEditing && getTextReplyFallback(editedEvent)) || "";
|
||||
const formattedBodyPrefix = (isReplyAndEditing && getHtmlReplyFallback(editedEvent)) || "";
|
||||
|
||||
const content: IContent = {
|
||||
// TODO emote
|
||||
|
@ -100,12 +105,11 @@ export function createMessageContent(
|
|||
// TODO markdown support
|
||||
|
||||
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
|
||||
const formattedBody =
|
||||
isHTML ?
|
||||
message :
|
||||
isMarkdownEnabled ?
|
||||
htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply }) :
|
||||
null;
|
||||
const formattedBody = isHTML
|
||||
? message
|
||||
: isMarkdownEnabled
|
||||
? htmlSerializeFromMdIfNeeded(message, { forceHTML: isReply })
|
||||
: null;
|
||||
|
||||
if (formattedBody) {
|
||||
content.format = "org.matrix.custom.html";
|
||||
|
@ -113,20 +117,18 @@ export function createMessageContent(
|
|||
}
|
||||
|
||||
if (isEditing) {
|
||||
content['m.new_content'] = {
|
||||
"msgtype": content.msgtype,
|
||||
"body": body,
|
||||
content["m.new_content"] = {
|
||||
msgtype: content.msgtype,
|
||||
body: body,
|
||||
};
|
||||
|
||||
if (formattedBody) {
|
||||
content['m.new_content'].format = "org.matrix.custom.html";
|
||||
content['m.new_content']['formatted_body'] = formattedBody;
|
||||
content["m.new_content"].format = "org.matrix.custom.html";
|
||||
content["m.new_content"]["formatted_body"] = formattedBody;
|
||||
}
|
||||
}
|
||||
|
||||
const newRelation = isEditing ?
|
||||
{ ...relation, 'rel_type': 'm.replace', 'event_id': editedEvent.getId() }
|
||||
: relation;
|
||||
const newRelation = isEditing ? { ...relation, rel_type: "m.replace", event_id: editedEvent.getId() } : relation;
|
||||
|
||||
attachRelation(content, newRelation);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import { EventStatus, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { IRoomState } from "../../../../structures/RoomView";
|
||||
import dis from '../../../../../dispatcher/dispatcher';
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../../../dispatcher/actions";
|
||||
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||
|
||||
|
@ -41,10 +41,7 @@ export function endEditing(roomContext: IRoomState) {
|
|||
export function cancelPreviousPendingEdit(mxClient: MatrixClient, editorStateTransfer: EditorStateTransfer) {
|
||||
const originalEvent = editorStateTransfer.getEvent();
|
||||
const previousEdit = originalEvent.replacingEvent();
|
||||
if (previousEdit && (
|
||||
previousEdit.status === EventStatus.QUEUED ||
|
||||
previousEdit.status === EventStatus.NOT_SENT
|
||||
)) {
|
||||
if (previousEdit && (previousEdit.status === EventStatus.QUEUED || previousEdit.status === EventStatus.NOT_SENT)) {
|
||||
mxClient.cancelPendingEvent(previousEdit);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,12 @@ import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
|||
export function isContentModified(newContent: IContent, editorStateTransfer: EditorStateTransfer): boolean {
|
||||
// if nothing has changed then bail
|
||||
const oldContent = editorStateTransfer.getEvent().getContent();
|
||||
if (oldContent["msgtype"] === newContent["msgtype"] && oldContent["body"] === newContent["body"] &&
|
||||
if (
|
||||
oldContent["msgtype"] === newContent["msgtype"] &&
|
||||
oldContent["body"] === newContent["body"] &&
|
||||
oldContent["format"] === newContent["format"] &&
|
||||
oldContent["formatted_body"] === newContent["formatted_body"]) {
|
||||
oldContent["formatted_body"] === newContent["formatted_body"]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer";
|
||||
import { IContent, IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||
import { IContent, IEventRelation, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { ISendEventResponse, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
|
||||
|
@ -27,7 +27,7 @@ import { doMaybeLocalRoomAction } from "../../../../../utils/local-room";
|
|||
import { CHAT_EFFECTS } from "../../../../../effects";
|
||||
import { containsEmoji } from "../../../../../effects/utils";
|
||||
import { IRoomState } from "../../../../structures/RoomView";
|
||||
import dis from '../../../../../dispatcher/dispatcher';
|
||||
import dis from "../../../../../dispatcher/dispatcher";
|
||||
import { createRedactEventDialog } from "../../../dialogs/ConfirmRedactDialog";
|
||||
import { endEditing, cancelPreviousPendingEdit } from "./editing";
|
||||
import EditorStateTransfer from "../../../../../utils/EditorStateTransfer";
|
||||
|
@ -43,11 +43,7 @@ interface SendMessageParams {
|
|||
includeReplyLegacyFallback?: boolean;
|
||||
}
|
||||
|
||||
export function sendMessage(
|
||||
message: string,
|
||||
isHTML: boolean,
|
||||
{ roomContext, mxClient, ...params }: SendMessageParams,
|
||||
) {
|
||||
export function sendMessage(message: string, isHTML: boolean, { roomContext, mxClient, ...params }: SendMessageParams) {
|
||||
const { relation, replyToEvent } = params;
|
||||
const { room } = roomContext;
|
||||
const { roomId } = room;
|
||||
|
@ -76,11 +72,7 @@ export function sendMessage(
|
|||
// TODO quick reaction
|
||||
|
||||
if (!content) {
|
||||
content = createMessageContent(
|
||||
message,
|
||||
isHTML,
|
||||
params,
|
||||
);
|
||||
content = createMessageContent(message, isHTML, params);
|
||||
}
|
||||
|
||||
// don't bother sending an empty message
|
||||
|
@ -92,9 +84,7 @@ export function sendMessage(
|
|||
decorateStartSendingTime(content);
|
||||
}
|
||||
|
||||
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name
|
||||
? relation.event_id
|
||||
: null;
|
||||
const threadId = relation?.rel_type === THREAD_RELATION_TYPE.name ? relation.event_id : null;
|
||||
|
||||
const prom = doMaybeLocalRoomAction(
|
||||
roomId,
|
||||
|
@ -106,7 +96,7 @@ export function sendMessage(
|
|||
// Clear reply_to_event as we put the message into the queue
|
||||
// if the send fails, retry will handle resending.
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
action: "reply_to_event",
|
||||
event: null,
|
||||
context: roomContext.timelineRenderingType,
|
||||
});
|
||||
|
@ -124,7 +114,7 @@ export function sendMessage(
|
|||
}
|
||||
});
|
||||
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
|
||||
prom.then(resp => {
|
||||
prom.then((resp) => {
|
||||
sendRoundTripMetric(mxClient, roomId, resp.event_id);
|
||||
});
|
||||
}
|
||||
|
@ -149,10 +139,7 @@ interface EditMessageParams {
|
|||
editorStateTransfer: EditorStateTransfer;
|
||||
}
|
||||
|
||||
export function editMessage(
|
||||
html: string,
|
||||
{ roomContext, mxClient, editorStateTransfer }: EditMessageParams,
|
||||
) {
|
||||
export function editMessage(html: string, { roomContext, mxClient, editorStateTransfer }: EditMessageParams) {
|
||||
const editedEvent = editorStateTransfer.getEvent();
|
||||
|
||||
PosthogAnalytics.instance.trackEvent<ComposerEvent>({
|
||||
|
@ -174,7 +161,7 @@ export function editMessage(
|
|||
|
||||
const shouldSend = true;
|
||||
|
||||
if (newContent?.body === '') {
|
||||
if (newContent?.body === "") {
|
||||
cancelPreviousPendingEdit(mxClient, editorStateTransfer);
|
||||
createRedactEventDialog({
|
||||
mxEvent: editedEvent,
|
||||
|
|
|
@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
export function setSelection(selection:
|
||||
Pick<Selection, 'anchorNode' | 'anchorOffset' | 'focusNode' | 'focusOffset'>,
|
||||
) {
|
||||
export function setSelection(selection: Pick<Selection, "anchorNode" | "anchorOffset" | "focusNode" | "focusOffset">) {
|
||||
if (selection.anchorNode && selection.focusNode) {
|
||||
const range = new Range();
|
||||
range.setStart(selection.anchorNode, selection.anchorOffset);
|
||||
|
@ -26,4 +24,3 @@ export function setSelection(selection:
|
|||
document.getSelection()?.addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue