Merge branch 'develop' into improve-image-view
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
commit
9606297f7c
187 changed files with 7597 additions and 2588 deletions
|
@ -46,6 +46,7 @@ import {IDiff} from "../../../editor/diff";
|
|||
import AutocompleteWrapperModel from "../../../editor/autocomplete";
|
||||
import DocumentPosition from "../../../editor/position";
|
||||
import {ICompletion} from "../../../autocomplete/Autocompleter";
|
||||
import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
// matches emoticons which follow the start of a line or whitespace
|
||||
|
@ -93,6 +94,7 @@ interface IProps {
|
|||
placeholder?: string;
|
||||
label?: string;
|
||||
initialCaret?: DocumentOffset;
|
||||
disabled?: boolean;
|
||||
|
||||
onChange?();
|
||||
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
|
||||
|
@ -421,105 +423,101 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
private onKeyDown = (event: React.KeyboardEvent) => {
|
||||
const model = this.props.model;
|
||||
const modKey = IS_MAC ? event.metaKey : event.ctrlKey;
|
||||
let handled = false;
|
||||
// format bold
|
||||
if (modKey && event.key === Key.B) {
|
||||
this.onFormatAction(Formatting.Bold);
|
||||
handled = true;
|
||||
// format italics
|
||||
} else if (modKey && event.key === Key.I) {
|
||||
this.onFormatAction(Formatting.Italics);
|
||||
handled = true;
|
||||
// format quote
|
||||
} else if (modKey && event.key === Key.GREATER_THAN) {
|
||||
this.onFormatAction(Formatting.Quote);
|
||||
handled = true;
|
||||
// redo
|
||||
} else if ((!IS_MAC && modKey && event.key === Key.Y) ||
|
||||
(IS_MAC && modKey && event.shiftKey && event.key === Key.Z)) {
|
||||
if (this.historyManager.canRedo()) {
|
||||
const {parts, caret} = this.historyManager.redo();
|
||||
// pass matching inputType so historyManager doesn't push echo
|
||||
// when invoked from rerender callback.
|
||||
model.reset(parts, caret, "historyRedo");
|
||||
}
|
||||
handled = true;
|
||||
// undo
|
||||
} else if (modKey && event.key === Key.Z) {
|
||||
if (this.historyManager.canUndo()) {
|
||||
const {parts, caret} = this.historyManager.undo(this.props.model);
|
||||
// pass matching inputType so historyManager doesn't push echo
|
||||
// when invoked from rerender callback.
|
||||
model.reset(parts, caret, "historyUndo");
|
||||
}
|
||||
handled = true;
|
||||
// insert newline on Shift+Enter
|
||||
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
|
||||
this.insertText("\n");
|
||||
handled = true;
|
||||
// move selection to start of composer
|
||||
} else if (modKey && event.key === Key.HOME && !event.shiftKey) {
|
||||
setSelection(this.editorRef.current, model, {
|
||||
index: 0,
|
||||
offset: 0,
|
||||
});
|
||||
handled = true;
|
||||
// move selection to end of composer
|
||||
} else if (modKey && event.key === Key.END && !event.shiftKey) {
|
||||
setSelection(this.editorRef.current, model, {
|
||||
index: model.parts.length - 1,
|
||||
offset: model.parts[model.parts.length - 1].text.length,
|
||||
});
|
||||
handled = true;
|
||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||
} else {
|
||||
const metaOrAltPressed = event.metaKey || event.altKey;
|
||||
const modifierPressed = metaOrAltPressed || event.shiftKey;
|
||||
if (model.autoComplete && model.autoComplete.hasCompletions()) {
|
||||
const autoComplete = model.autoComplete;
|
||||
switch (event.key) {
|
||||
case Key.ARROW_UP:
|
||||
if (!modifierPressed) {
|
||||
autoComplete.onUpArrow(event);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
if (!modifierPressed) {
|
||||
autoComplete.onDownArrow(event);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.TAB:
|
||||
if (!metaOrAltPressed) {
|
||||
autoComplete.onTab(event);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
case Key.ESCAPE:
|
||||
if (!modifierPressed) {
|
||||
autoComplete.onEscape(event);
|
||||
handled = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return; // don't preventDefault on anything else
|
||||
}
|
||||
} else if (event.key === Key.TAB) {
|
||||
this.tabCompleteName(event);
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
switch (action) {
|
||||
case MessageComposerAction.FormatBold:
|
||||
this.onFormatAction(Formatting.Bold);
|
||||
handled = true;
|
||||
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
|
||||
this.formatBarRef.current.hide();
|
||||
}
|
||||
break;
|
||||
case MessageComposerAction.FormatItalics:
|
||||
this.onFormatAction(Formatting.Italics);
|
||||
handled = true;
|
||||
break;
|
||||
case MessageComposerAction.FormatQuote:
|
||||
this.onFormatAction(Formatting.Quote);
|
||||
handled = true;
|
||||
break;
|
||||
case MessageComposerAction.EditRedo:
|
||||
if (this.historyManager.canRedo()) {
|
||||
const {parts, caret} = this.historyManager.redo();
|
||||
// pass matching inputType so historyManager doesn't push echo
|
||||
// when invoked from rerender callback.
|
||||
model.reset(parts, caret, "historyRedo");
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
case MessageComposerAction.EditUndo:
|
||||
if (this.historyManager.canUndo()) {
|
||||
const {parts, caret} = this.historyManager.undo(this.props.model);
|
||||
// pass matching inputType so historyManager doesn't push echo
|
||||
// when invoked from rerender callback.
|
||||
model.reset(parts, caret, "historyUndo");
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
case MessageComposerAction.NewLine:
|
||||
this.insertText("\n");
|
||||
handled = true;
|
||||
break;
|
||||
case MessageComposerAction.MoveCursorToStart:
|
||||
setSelection(this.editorRef.current, model, {
|
||||
index: 0,
|
||||
offset: 0,
|
||||
});
|
||||
handled = true;
|
||||
break;
|
||||
case MessageComposerAction.MoveCursorToEnd:
|
||||
setSelection(this.editorRef.current, model, {
|
||||
index: model.parts.length - 1,
|
||||
offset: model.parts[model.parts.length - 1].text.length,
|
||||
});
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
|
||||
if (model.autoComplete && model.autoComplete.hasCompletions()) {
|
||||
const autoComplete = model.autoComplete;
|
||||
switch (autocompleteAction) {
|
||||
case AutocompleteAction.CompleteOrPrevSelection:
|
||||
case AutocompleteAction.PrevSelection:
|
||||
autoComplete.selectPreviousSelection();
|
||||
handled = true;
|
||||
break;
|
||||
case AutocompleteAction.CompleteOrNextSelection:
|
||||
case AutocompleteAction.NextSelection:
|
||||
autoComplete.selectNextSelection();
|
||||
handled = true;
|
||||
break;
|
||||
case AutocompleteAction.Cancel:
|
||||
autoComplete.onEscape(event);
|
||||
handled = true;
|
||||
break;
|
||||
default:
|
||||
return; // don't preventDefault on anything else
|
||||
}
|
||||
} else if (autocompleteAction === AutocompleteAction.CompleteOrPrevSelection
|
||||
|| autocompleteAction === AutocompleteAction.CompleteOrNextSelection) {
|
||||
// there is no current autocomplete window, try to open it
|
||||
this.tabCompleteName();
|
||||
handled = true;
|
||||
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
|
||||
this.formatBarRef.current.hide();
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
private async tabCompleteName(event: React.KeyboardEvent) {
|
||||
private async tabCompleteName() {
|
||||
try {
|
||||
await new Promise<void>(resolve => this.setState({showVisualBell: false}, resolve));
|
||||
const {model} = this.props;
|
||||
|
@ -542,7 +540,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
|
||||
// Don't try to do things with the autocomplete if there is none shown
|
||||
if (model.autoComplete) {
|
||||
await model.autoComplete.onTab(event);
|
||||
await model.autoComplete.startSelection();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
|
@ -672,6 +670,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
});
|
||||
const classes = classNames("mx_BasicMessageComposer_input", {
|
||||
"mx_BasicMessageComposer_input_shouldShowPillAvatar": this.state.showPillAvatar,
|
||||
|
||||
// TODO: @@ TravisR: This doesn't work properly. The composer resets in a strange way.
|
||||
"mx_BasicMessageComposer_input_disabled": this.props.disabled,
|
||||
});
|
||||
|
||||
const shortcuts = {
|
||||
|
@ -704,6 +705,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
aria-expanded={Boolean(this.state.autoComplete)}
|
||||
aria-activedescendant={completionIndex >= 0 ? generateCompletionDomId(completionIndex) : undefined}
|
||||
dir="auto"
|
||||
aria-disabled={this.props.disabled}
|
||||
/>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -27,13 +27,12 @@ import {parseEvent} from '../../../editor/deserialize';
|
|||
import {PartCreator} from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
|
@ -136,38 +135,41 @@ export default class EditMessageComposer extends React.Component {
|
|||
if (this._editorRef.isComposing(event)) {
|
||||
return;
|
||||
}
|
||||
if (event.metaKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
|
||||
const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
|
||||
: event.key === Key.ENTER;
|
||||
if (send) {
|
||||
this._sendEdit();
|
||||
event.preventDefault();
|
||||
} else if (event.key === Key.ESCAPE) {
|
||||
this._cancelEdit();
|
||||
} else if (event.key === Key.ARROW_UP) {
|
||||
if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
|
||||
if (previousEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: previousEvent});
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
switch (action) {
|
||||
case MessageComposerAction.Send:
|
||||
this._sendEdit();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case MessageComposerAction.CancelEditing:
|
||||
this._cancelEdit();
|
||||
break;
|
||||
case MessageComposerAction.EditPrevMessage: {
|
||||
if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) {
|
||||
return;
|
||||
}
|
||||
const previousEvent = findEditableEvent(this._getRoom(), false,
|
||||
this.props.editState.getEvent().getId());
|
||||
if (previousEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: previousEvent});
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (event.key === Key.ARROW_DOWN) {
|
||||
if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) {
|
||||
return;
|
||||
case MessageComposerAction.EditNextMessage: {
|
||||
if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) {
|
||||
return;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
|
||||
if (nextEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
dis.dispatch({action: 'edit_event', event: null});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
|
||||
if (nextEvent) {
|
||||
dis.dispatch({action: 'edit_event', event: nextEvent});
|
||||
} else {
|
||||
dis.dispatch({action: 'edit_event', event: null});
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ import * as sdk from "../../../index";
|
|||
import dis from '../../../dispatcher/dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {Layout, LayoutPropType} from "../../../settings/Layout";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import {formatTime} from "../../../DateUtils";
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
||||
|
@ -936,7 +936,7 @@ export default class EventTile extends React.Component {
|
|||
);
|
||||
|
||||
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
||||
const keyRequestInfo = isEncryptionFailure ?
|
||||
const keyRequestInfo = isEncryptionFailure && !isRedacted ?
|
||||
<div className="mx_EventTile_keyRequestInfo">
|
||||
<span className="mx_EventTile_keyRequestInfo_text">
|
||||
{ keyRequestInfoContent }
|
||||
|
|
|
@ -33,6 +33,7 @@ import WidgetStore from "../../../stores/WidgetStore";
|
|||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
|
@ -187,6 +188,7 @@ export default class MessageComposer extends React.Component {
|
|||
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
|
||||
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
|
||||
isComposerEmpty: true,
|
||||
haveRecording: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -325,6 +327,10 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onVoiceUpdate = (haveRecording: boolean) => {
|
||||
this.setState({haveRecording});
|
||||
};
|
||||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
|
@ -346,17 +352,32 @@ export default class MessageComposer extends React.Component {
|
|||
permalinkCreator={this.props.permalinkCreator}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
// TODO: @@ TravisR - Disabling the composer doesn't work
|
||||
disabled={this.state.haveRecording}
|
||||
/>,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
|
||||
if (!this.state.haveRecording) {
|
||||
controls.push(
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (SettingsStore.getValue(UIFeature.Widgets) &&
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton")) {
|
||||
SettingsStore.getValue("MessageComposerInput.showStickersButton") &&
|
||||
!this.state.haveRecording) {
|
||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (!this.state.isComposerEmpty) {
|
||||
if (SettingsStore.getValue("feature_voice_messages")) {
|
||||
controls.push(<VoiceRecordComposerTile
|
||||
key="controls_voice_record"
|
||||
room={this.props.room}
|
||||
onRecording={this.onVoiceUpdate} />);
|
||||
}
|
||||
|
||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
||||
controls.push(
|
||||
<SendButton key="controls_send" onClick={this.sendMessage} />,
|
||||
);
|
||||
|
|
|
@ -85,8 +85,8 @@ class FormatButton extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
as="span"
|
||||
role="button"
|
||||
element="button"
|
||||
type="button"
|
||||
onClick={this.props.onClick}
|
||||
title={this.props.label}
|
||||
tooltip={tooltip}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -28,6 +28,8 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|||
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import {showSpaceInvite} from "../../../utils/space";
|
||||
|
||||
const NewRoomIntro = () => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
@ -100,17 +102,48 @@ const NewRoomIntro = () => {
|
|||
});
|
||||
}
|
||||
|
||||
let buttons;
|
||||
if (room.canInvite(cli.getUserId())) {
|
||||
const onInviteClick = () => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
};
|
||||
let parentSpace;
|
||||
if (
|
||||
SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
|
||||
SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
|
||||
) {
|
||||
parentSpace = SpaceStore.instance.activeSpace;
|
||||
}
|
||||
|
||||
let buttons;
|
||||
if (parentSpace) {
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}>
|
||||
<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={() => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to just this room")}
|
||||
</AccessibleButton> }
|
||||
</div>;
|
||||
} else if (room.canInvite(cli.getUserId())) {
|
||||
buttons = <div className="mx_NewRoomIntro_buttons">
|
||||
<AccessibleButton
|
||||
className="mx_NewRoomIntro_inviteButton"
|
||||
kind="primary"
|
||||
onClick={() => {
|
||||
dis.dispatch({ action: "view_invite", roomId });
|
||||
}}
|
||||
>
|
||||
{_t("Invite to this room")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;
|
||||
|
|
|
@ -20,6 +20,7 @@ import React, { ReactComponentElement } from "react";
|
|||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import * as fbEmitter from "fbemitter";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
||||
|
@ -48,9 +49,8 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore";
|
||||
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
|
||||
import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
|
||||
|
@ -62,6 +62,7 @@ interface IProps {
|
|||
onResize: () => void;
|
||||
resizeNotifier: ResizeNotifier;
|
||||
isMinimized: boolean;
|
||||
activeSpace: Room;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -194,8 +195,8 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore space rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
label={_t("Explore rooms")}
|
||||
iconClassName="mx_RoomList_iconBrowse"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -424,6 +425,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
|
||||
};
|
||||
|
||||
private onSpaceInviteClick = () => {
|
||||
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
|
||||
showSpaceInvite(this.props.activeSpace, initialText);
|
||||
};
|
||||
|
||||
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
||||
return this.state.suggestedRooms.map(room => {
|
||||
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
|
||||
|
@ -569,7 +575,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
|||
kind="link"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore all public rooms")}
|
||||
{ this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (this.props.activeSpace) {
|
||||
explorePrompt = <div className="mx_RoomList_explorePrompt">
|
||||
<div>{ _t("Quick actions") }</div>
|
||||
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_spaceInvite"
|
||||
onClick={this.onSpaceInviteClick}
|
||||
>
|
||||
{_t("Invite people")}
|
||||
</AccessibleButton> }
|
||||
<AccessibleButton
|
||||
className="mx_RoomList_explorePrompt_spaceExplore"
|
||||
onClick={this.onExplore}
|
||||
>
|
||||
{_t("Explore rooms")}
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
|
||||
|
|
|
@ -51,6 +51,7 @@ import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
|||
import ExtraTile from "./ExtraTile";
|
||||
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
|
||||
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
||||
|
@ -470,18 +471,19 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
const action = getKeyBindingsManager().getRoomListAction(ev);
|
||||
switch (action) {
|
||||
case RoomListAction.CollapseSection:
|
||||
ev.stopPropagation();
|
||||
if (this.state.isExpanded) {
|
||||
// On ARROW_LEFT collapse the room sublist if it isn't already
|
||||
// Collapse the room sublist if it isn't already
|
||||
this.toggleCollapsed();
|
||||
}
|
||||
break;
|
||||
case Key.ARROW_RIGHT: {
|
||||
case RoomListAction.ExpandSection: {
|
||||
ev.stopPropagation();
|
||||
if (!this.state.isExpanded) {
|
||||
// On ARROW_RIGHT expand the room sublist if it isn't already
|
||||
// Expand the room sublist if it isn't already
|
||||
this.toggleCollapsed();
|
||||
} else if (this.sublistRef.current) {
|
||||
// otherwise focus the first room
|
||||
|
|
|
@ -333,6 +333,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
this.setState({generalMenuPosition: null}); // hide the menu
|
||||
};
|
||||
|
||||
private onInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_invite',
|
||||
roomId: this.props.room.roomId,
|
||||
});
|
||||
this.setState({generalMenuPosition: null}); // hide the menu
|
||||
};
|
||||
|
||||
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -453,6 +464,8 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
|
||||
const lowPriorityLabel = _t("Low Priority");
|
||||
|
||||
const userId = MatrixClientPeg.get().getUserId();
|
||||
const canInvite = this.props.room.canInvite(userId);
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...contextMenuBelow(this.state.generalMenuPosition)}
|
||||
onFinished={this.onCloseGeneralMenu}
|
||||
|
@ -472,7 +485,13 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
label={lowPriorityLabel}
|
||||
iconClassName="mx_RoomTile_iconArrowDown"
|
||||
/>
|
||||
|
||||
{canInvite ? (
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onInviteClick}
|
||||
label={_t("Invite People")}
|
||||
iconClassName="mx_RoomTile_iconInvite"
|
||||
/>
|
||||
) : null}
|
||||
<IconizedContextMenuOption
|
||||
onClick={this.onOpenRoomSettings}
|
||||
label={_t("Settings")}
|
||||
|
|
|
@ -33,22 +33,22 @@ import ReplyThread from "../elements/ReplyThread";
|
|||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import SendHistoryManager from "../../../SendHistoryManager";
|
||||
import {getCommand} from '../../../SlashCommands';
|
||||
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
||||
import * as sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import ContentMessages from '../../../ContentMessages';
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {containsEmoji} from "../../../effects/utils";
|
||||
import {CHAT_EFFECTS} from '../../../effects';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import EMOJI_REGEX from 'emojibase-regex';
|
||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
|
||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||
|
@ -120,6 +120,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
permalinkCreator: PropTypes.object.isRequired,
|
||||
replyToEvent: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -147,60 +148,50 @@ export default class SendMessageComposer extends React.Component {
|
|||
if (this._editorRef.isComposing(event)) {
|
||||
return;
|
||||
}
|
||||
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
|
||||
const send = ctrlEnterToSend
|
||||
? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
|
||||
: event.key === Key.ENTER && !hasModifier;
|
||||
if (send) {
|
||||
this._sendMessage();
|
||||
event.preventDefault();
|
||||
} else if (event.key === Key.ARROW_UP) {
|
||||
this.onVerticalArrow(event, true);
|
||||
} else if (event.key === Key.ARROW_DOWN) {
|
||||
this.onVerticalArrow(event, false);
|
||||
} else if (event.key === Key.ESCAPE) {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
});
|
||||
} else if (this._prepareToEncrypt) {
|
||||
// This needs to be last!
|
||||
this._prepareToEncrypt();
|
||||
const action = getKeyBindingsManager().getMessageComposerAction(event);
|
||||
switch (action) {
|
||||
case MessageComposerAction.Send:
|
||||
this._sendMessage();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case MessageComposerAction.SelectPrevSendHistory:
|
||||
case MessageComposerAction.SelectNextSendHistory: {
|
||||
// Try select composer history
|
||||
const selected = this.selectSendHistory(action === MessageComposerAction.SelectPrevSendHistory);
|
||||
if (selected) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MessageComposerAction.EditPrevMessage:
|
||||
// selection must be collapsed and caret at start
|
||||
if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
event.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: editEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MessageComposerAction.CancelEditing:
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: null,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
if (this._prepareToEncrypt) {
|
||||
// This needs to be last!
|
||||
this._prepareToEncrypt();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onVerticalArrow(e, up) {
|
||||
// arrows from an initial-caret composer navigates recent messages to edit
|
||||
// ctrl-alt-arrows navigate send history
|
||||
if (e.shiftKey || e.metaKey) return;
|
||||
|
||||
const shouldSelectHistory = e.altKey && e.ctrlKey;
|
||||
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent;
|
||||
|
||||
if (shouldSelectHistory) {
|
||||
// Try select composer history
|
||||
const selected = this.selectSendHistory(up);
|
||||
if (selected) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (shouldEditLastMessage) {
|
||||
// selection must be collapsed and caret at start
|
||||
if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
|
||||
const editEvent = findEditableEvent(this.props.room, false);
|
||||
if (editEvent) {
|
||||
// We're selecting history, so prevent the key event from doing anything else
|
||||
e.preventDefault();
|
||||
dis.dispatch({
|
||||
action: 'edit_event',
|
||||
event: editEvent,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we keep sent messages/commands in a separate history (separate from undo history)
|
||||
// so you can alt+up/down in them
|
||||
selectSendHistory(up) {
|
||||
|
@ -265,7 +256,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
const myReactionKeys = [...myReactionEvents]
|
||||
.filter(event => !event.isRedacted())
|
||||
.map(event => event.getRelation().key);
|
||||
shouldReact = !myReactionKeys.includes(reaction);
|
||||
shouldReact = !myReactionKeys.includes(reaction);
|
||||
}
|
||||
if (shouldReact) {
|
||||
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", {
|
||||
|
@ -290,15 +281,22 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
return [getCommand(this.props.room.roomId, commandText), commandText];
|
||||
const {cmd, args} = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
async _runSlashCommand(fn) {
|
||||
const cmd = fn();
|
||||
let error = cmd.error;
|
||||
if (cmd.promise) {
|
||||
async _runSlashCommand(cmd, args) {
|
||||
const result = cmd.run(this.props.room.roomId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
await cmd.promise;
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
// The command returns a modified message that we need to pass on
|
||||
messageContent = await result.promise;
|
||||
} else {
|
||||
await result.promise;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
@ -307,7 +305,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
console.error("Command failure: %s", error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
// assume the error is a server error when the command is async
|
||||
const isServerError = !!cmd.promise;
|
||||
const isServerError = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
|
@ -325,6 +323,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,13 +332,22 @@ export default class SendMessageComposer extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
let shouldSend = true;
|
||||
let content;
|
||||
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
const [cmd, commandText] = this._getSlashCommand();
|
||||
const [cmd, args, commandText] = this._getSlashCommand();
|
||||
if (cmd) {
|
||||
shouldSend = false;
|
||||
this._runSlashCommand(cmd);
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
content = await this._runSlashCommand(cmd, args);
|
||||
if (replyToEvent) {
|
||||
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
|
||||
}
|
||||
} else {
|
||||
this._runSlashCommand(cmd, args);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
|
@ -374,11 +382,12 @@ export default class SendMessageComposer extends React.Component {
|
|||
this._sendQuickReaction();
|
||||
}
|
||||
|
||||
const replyToEvent = this.props.replyToEvent;
|
||||
if (shouldSend) {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const {roomId} = this.props.room;
|
||||
const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
||||
if (!content) {
|
||||
content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
|
||||
}
|
||||
// don't bother sending an empty message
|
||||
if (!content.body.trim()) return;
|
||||
|
||||
|
@ -453,12 +462,17 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
// should save state when editor has contents or reply is open
|
||||
_shouldSaveStoredEditorState = () => {
|
||||
return !this.model.isEmpty || this.props.replyToEvent;
|
||||
}
|
||||
|
||||
_saveStoredEditorState = () => {
|
||||
if (this.model.isEmpty) {
|
||||
this._clearStoredEditorState();
|
||||
} else {
|
||||
if (this._shouldSaveStoredEditorState()) {
|
||||
const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
|
||||
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
|
||||
} else {
|
||||
this._clearStoredEditorState();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -502,7 +516,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
_insertQuotedMessage(event) {
|
||||
const {model} = this;
|
||||
const {partCreator} = model;
|
||||
const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true });
|
||||
const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true});
|
||||
// add two newlines
|
||||
quoteParts.push(partCreator.newline());
|
||||
quoteParts.push(partCreator.newline());
|
||||
|
@ -556,6 +570,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
label={this.props.placeholder}
|
||||
placeholder={this.props.placeholder}
|
||||
onPaste={this._onPaste}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {MatrixEvent} from "matrix-js-sdk";
|
||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import * as sdk from "../../../index";
|
||||
|
|
101
src/components/views/rooms/VoiceRecordComposerTile.tsx
Normal file
101
src/components/views/rooms/VoiceRecordComposerTile.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import React from "react";
|
||||
import {VoiceRecorder} from "../../../voice/VoiceRecorder";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import classNames from "classnames";
|
||||
import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import LiveRecordingClock from "../voice_messages/LiveRecordingClock";
|
||||
|
||||
interface IProps {
|
||||
room: Room;
|
||||
onRecording: (haveRecording: boolean) => void;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
recorder?: VoiceRecorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container tile for rendering the voice message recorder in the composer.
|
||||
*/
|
||||
@replaceableComponent("views.rooms.VoiceRecordComposerTile")
|
||||
export default class VoiceRecordComposerTile extends React.PureComponent<IProps, IState> {
|
||||
public constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
recorder: null, // not recording by default
|
||||
};
|
||||
}
|
||||
|
||||
private onStartStopVoiceMessage = async () => {
|
||||
// TODO: @@ TravisR: We do not want to auto-send on stop.
|
||||
if (this.state.recorder) {
|
||||
await this.state.recorder.stop();
|
||||
const mxc = await this.state.recorder.upload();
|
||||
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
|
||||
body: "Voice message",
|
||||
msgtype: "org.matrix.msc2516.voice",
|
||||
url: mxc,
|
||||
});
|
||||
this.setState({recorder: null});
|
||||
this.props.onRecording(false);
|
||||
return;
|
||||
}
|
||||
const recorder = new VoiceRecorder(MatrixClientPeg.get());
|
||||
await recorder.start();
|
||||
this.props.onRecording(true);
|
||||
this.setState({recorder});
|
||||
};
|
||||
|
||||
private renderWaveformArea() {
|
||||
if (!this.state.recorder) return null;
|
||||
|
||||
return <div className='mx_VoiceRecordComposerTile_waveformContainer'>
|
||||
<LiveRecordingClock recorder={this.state.recorder} />
|
||||
<LiveRecordingWaveform recorder={this.state.recorder} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
public render() {
|
||||
const classes = classNames({
|
||||
'mx_MessageComposer_button': !this.state.recorder,
|
||||
'mx_MessageComposer_voiceMessage': !this.state.recorder,
|
||||
'mx_VoiceRecordComposerTile_stop': !!this.state.recorder,
|
||||
});
|
||||
|
||||
let tooltip = _t("Record a voice message");
|
||||
if (!!this.state.recorder) {
|
||||
// TODO: @@ TravisR: Change to match behaviour
|
||||
tooltip = _t("Stop & send recording");
|
||||
}
|
||||
|
||||
return (<>
|
||||
{this.renderWaveformArea()}
|
||||
<AccessibleTooltipButton
|
||||
className={classes}
|
||||
onClick={this.onStartStopVoiceMessage}
|
||||
title={tooltip}
|
||||
/>
|
||||
</>);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue