Merge pull request #6753 from matrix-org/revert-6752-revert-6682-gsouquet/compact-composer-18533
This commit is contained in:
commit
d475b7f1ea
10 changed files with 273 additions and 127 deletions
|
@ -733,4 +733,8 @@ $hover-select-border: 4px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_sendMessage {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,11 +186,14 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_button {
|
.mx_MessageComposer_button {
|
||||||
|
--size: 26px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 26px;
|
height: var(--size);
|
||||||
width: 26px;
|
line-height: var(--size);
|
||||||
|
width: auto;
|
||||||
|
padding-left: calc(var(--size) + 5px);
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -207,8 +210,22 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&::after {
|
||||||
background: rgba($accent-color, 0.1);
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 0;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.mx_MessageComposer_closeButtonMenu {
|
||||||
|
&::after {
|
||||||
|
background: rgba($accent-color, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
|
@ -237,10 +254,18 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
|
mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_buttonMenu::before {
|
||||||
|
mask-image: url('$(res)/img/image-view/more.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_closeButtonMenu::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_sendMessage {
|
.mx_MessageComposer_sendMessage {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 6px;
|
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
@ -349,10 +374,19 @@ limitations under the License.
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
|
|
||||||
.mx_MessageComposer_wrapper {
|
.mx_MessageComposer_wrapper {
|
||||||
padding: 0;
|
padding: 0 0 0 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MessageComposer_button:last-child {
|
.mx_MessageComposer_button:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_e2eIcon {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageComposer_Menu .mx_CallContextMenu_item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
|
||||||
|
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room?: Room; // if showing panels for a given room, this is set
|
room?: Room; // if showing panels for a given room, this is set
|
||||||
|
@ -60,6 +61,7 @@ interface IProps {
|
||||||
user?: User; // used if we know the user ahead of opening the panel
|
user?: User; // used if we know the user ahead of opening the panel
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
e2eStatus?: E2EStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -319,7 +321,8 @@ export default class RightPanel extends React.Component<IProps, IState> {
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
onClose={this.onClose}
|
onClose={this.onClose}
|
||||||
mxEvent={this.state.event}
|
mxEvent={this.state.event}
|
||||||
permalinkCreator={this.props.permalinkCreator} />;
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
e2eStatus={this.props.e2eStatus} />;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case RightPanelPhases.ThreadPanel:
|
case RightPanelPhases.ThreadPanel:
|
||||||
|
|
|
@ -2063,7 +2063,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
? <RightPanel
|
? <RightPanel
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} />
|
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
|
||||||
|
e2eStatus={this.state.e2eStatus} />
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const timelineClasses = classNames("mx_RoomView_timeline", {
|
const timelineClasses = classNames("mx_RoomView_timeline", {
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { ActionPayload } from '../../dispatcher/payloads';
|
||||||
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
|
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
|
||||||
import { Action } from '../../dispatcher/actions';
|
import { Action } from '../../dispatcher/actions';
|
||||||
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../MatrixClientPeg';
|
||||||
|
import { E2EStatus } from '../../utils/ShieldUtils';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -40,6 +41,7 @@ interface IProps {
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
|
e2eStatus?: E2EStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -144,6 +146,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
|
||||||
replyToEvent={this.state?.thread?.replyToEvent}
|
replyToEvent={this.state?.thread?.replyToEvent}
|
||||||
showReplyPreview={false}
|
showReplyPreview={false}
|
||||||
permalinkCreator={this.props.permalinkCreator}
|
permalinkCreator={this.props.permalinkCreator}
|
||||||
|
e2eStatus={this.props.e2eStatus}
|
||||||
compact={true}
|
compact={true}
|
||||||
/>
|
/>
|
||||||
</BaseCard>
|
</BaseCard>
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
interface ITooltipProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||||
title: string;
|
title: string;
|
||||||
tooltip?: React.ReactNode;
|
tooltip?: React.ReactNode;
|
||||||
|
label?: React.ReactNode;
|
||||||
tooltipClassName?: string;
|
tooltipClassName?: string;
|
||||||
forceHide?: boolean;
|
forceHide?: boolean;
|
||||||
yOffset?: number;
|
yOffset?: number;
|
||||||
|
@ -84,7 +85,8 @@ export default class AccessibleTooltipButton extends React.PureComponent<IToolti
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
>
|
>
|
||||||
{ children }
|
{ children }
|
||||||
{ tip }
|
{ this.props.label }
|
||||||
|
{ (tooltip || title) && tip }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
|
@ -27,7 +27,12 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
import {
|
||||||
|
aboveLeftOf,
|
||||||
|
ContextMenu,
|
||||||
|
useContextMenu,
|
||||||
|
MenuItem,
|
||||||
|
} from "../../structures/ContextMenu";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import { UIFeature } from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
|
@ -45,6 +50,10 @@ import { Action } from "../../../dispatcher/actions";
|
||||||
import EditorModel from "../../../editor/model";
|
import EditorModel from "../../../editor/model";
|
||||||
import EmojiPicker from '../emojipicker/EmojiPicker';
|
import EmojiPicker from '../emojipicker/EmojiPicker';
|
||||||
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
|
||||||
|
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
|
||||||
|
|
||||||
|
let instanceCount = 0;
|
||||||
|
const NARROW_MODE_BREAKPOINT = 500;
|
||||||
|
|
||||||
interface IComposerAvatarProps {
|
interface IComposerAvatarProps {
|
||||||
me: object;
|
me: object;
|
||||||
|
@ -71,13 +80,19 @@ function SendButton(props: ISendButtonProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiButton = ({ addEmoji }) => {
|
interface IEmojiButtonProps {
|
||||||
|
addEmoji: (unicode: string) => boolean;
|
||||||
|
menuPosition: any; // TODO: Types
|
||||||
|
narrowMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiButton: React.FC<IEmojiButtonProps> = ({ addEmoji, menuPosition, narrowMode }) => {
|
||||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
const buttonRect = button.current.getBoundingClientRect();
|
const position = menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect());
|
||||||
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
|
contextMenu = <ContextMenu {...position} onFinished={closeMenu} managed={false}>
|
||||||
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
|
||||||
</ContextMenu>;
|
</ContextMenu>;
|
||||||
}
|
}
|
||||||
|
@ -93,12 +108,11 @@ const EmojiButton = ({ addEmoji }) => {
|
||||||
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
// TODO: replace ContextMenuTooltipButton with a unified representation of
|
||||||
// the header buttons and the right panel buttons
|
// the header buttons and the right panel buttons
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<ContextMenuTooltipButton
|
<AccessibleTooltipButton
|
||||||
className={className}
|
className={className}
|
||||||
onClick={openMenu}
|
onClick={openMenu}
|
||||||
isExpanded={menuDisplayed}
|
title={!narrowMode && _t('Emoji picker')}
|
||||||
title={_t('Emoji picker')}
|
label={narrowMode && _t("Add emoji")}
|
||||||
inputRef={button}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
|
@ -196,6 +210,9 @@ interface IState {
|
||||||
haveRecording: boolean;
|
haveRecording: boolean;
|
||||||
recordingTimeLeftSeconds?: number;
|
recordingTimeLeftSeconds?: number;
|
||||||
me?: RoomMember;
|
me?: RoomMember;
|
||||||
|
narrowMode?: boolean;
|
||||||
|
isMenuOpen: boolean;
|
||||||
|
showStickers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.MessageComposer")
|
@replaceableComponent("views.rooms.MessageComposer")
|
||||||
|
@ -203,6 +220,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private messageComposerInput: SendMessageComposer;
|
private messageComposerInput: SendMessageComposer;
|
||||||
private voiceRecordingButton: VoiceRecordComposerTile;
|
private voiceRecordingButton: VoiceRecordComposerTile;
|
||||||
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
|
private instanceId: number;
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
replyInThread: false,
|
replyInThread: false,
|
||||||
|
@ -220,15 +239,32 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
isComposerEmpty: true,
|
isComposerEmpty: true,
|
||||||
haveRecording: false,
|
haveRecording: false,
|
||||||
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
|
recordingTimeLeftSeconds: null, // when set to a number, shows a toast
|
||||||
|
isMenuOpen: false,
|
||||||
|
showStickers: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.instanceId = instanceCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
|
||||||
this.waitForOwnMember();
|
this.waitForOwnMember();
|
||||||
|
UIStore.instance.trackElementDimensions(`MessageComposer${this.instanceId}`, this.ref.current);
|
||||||
|
UIStore.instance.on(`MessageComposer${this.instanceId}`, this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onResize = (type: UI_EVENTS, entry: ResizeObserverEntry) => {
|
||||||
|
if (type === UI_EVENTS.Resize) {
|
||||||
|
const narrowMode = entry.contentRect.width <= NARROW_MODE_BREAKPOINT;
|
||||||
|
this.setState({
|
||||||
|
narrowMode,
|
||||||
|
isMenuOpen: !narrowMode ? false : this.state.isMenuOpen,
|
||||||
|
showStickers: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onAction = (payload: ActionPayload) => {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload.action === 'reply_to_event') {
|
if (payload.action === 'reply_to_event') {
|
||||||
// add a timeout for the reply preview to be rendered, so
|
// add a timeout for the reply preview to be rendered, so
|
||||||
|
@ -263,6 +299,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
|
VoiceRecordingStore.instance.off(UPDATE_EVENT, this.onVoiceStoreUpdate);
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
|
||||||
|
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomStateEvents = (ev, state) => {
|
private onRoomStateEvents = (ev, state) => {
|
||||||
|
@ -312,7 +350,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
private renderPlaceholderText = () => {
|
private renderPlaceholderText = () => {
|
||||||
if (this.props.replyToEvent) {
|
if (this.props.replyToEvent) {
|
||||||
if (this.props.e2eStatus) {
|
if (this.props.replyInThread && this.props.e2eStatus) {
|
||||||
|
return _t('Reply to encrypted thread…');
|
||||||
|
} else if (this.props.replyInThread) {
|
||||||
|
return _t('Reply to thread…');
|
||||||
|
} else if (this.props.e2eStatus) {
|
||||||
return _t('Send an encrypted reply…');
|
return _t('Send an encrypted reply…');
|
||||||
} else {
|
} else {
|
||||||
return _t('Send a reply…');
|
return _t('Send a reply…');
|
||||||
|
@ -326,11 +368,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private addEmoji(emoji: string) {
|
private addEmoji(emoji: string): boolean {
|
||||||
dis.dispatch<ComposerInsertPayload>({
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
action: Action.ComposerInsert,
|
action: Action.ComposerInsert,
|
||||||
text: emoji,
|
text: emoji,
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessage = async () => {
|
private sendMessage = async () => {
|
||||||
|
@ -369,6 +412,97 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private shouldShowStickerPicker = (): boolean => {
|
||||||
|
return SettingsStore.getValue(UIFeature.Widgets)
|
||||||
|
&& SettingsStore.getValue("MessageComposerInput.showStickersButton")
|
||||||
|
&& !this.state.haveRecording;
|
||||||
|
};
|
||||||
|
|
||||||
|
private showStickers = (showStickers: boolean) => {
|
||||||
|
this.setState({ showStickers });
|
||||||
|
};
|
||||||
|
|
||||||
|
private toggleButtonMenu = (): void => {
|
||||||
|
this.setState({
|
||||||
|
isMenuOpen: !this.state.isMenuOpen,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
|
||||||
|
const buttons: JSX.Element[] = [];
|
||||||
|
if (!this.state.haveRecording) {
|
||||||
|
buttons.push(
|
||||||
|
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||||
|
);
|
||||||
|
buttons.push(
|
||||||
|
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} menuPosition={menuPosition} narrowMode={this.state.narrowMode} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.shouldShowStickerPicker()) {
|
||||||
|
let title;
|
||||||
|
if (!this.state.narrowMode) {
|
||||||
|
title = this.state.showStickers ? _t("Hide Stickers") : _t("Show Stickers");
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
id='stickersButton'
|
||||||
|
key="controls_stickers"
|
||||||
|
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
||||||
|
onClick={() => this.showStickers(!this.state.showStickers)}
|
||||||
|
title={title}
|
||||||
|
label={this.state.narrowMode && _t("Send a sticker")}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.state.haveRecording && !this.state.narrowMode) {
|
||||||
|
buttons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
|
||||||
|
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
|
||||||
|
title={_t("Send voice message")}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.narrowMode) {
|
||||||
|
return buttons;
|
||||||
|
} else {
|
||||||
|
const classnames = classNames({
|
||||||
|
mx_MessageComposer_button: true,
|
||||||
|
mx_MessageComposer_buttonMenu: true,
|
||||||
|
mx_MessageComposer_closeButtonMenu: this.state.isMenuOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <>
|
||||||
|
{ buttons[0] }
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={classnames}
|
||||||
|
onClick={this.toggleButtonMenu}
|
||||||
|
title={_t("More options")}
|
||||||
|
tooltip={false}
|
||||||
|
/>
|
||||||
|
{ this.state.isMenuOpen && (
|
||||||
|
<ContextMenu
|
||||||
|
onFinished={this.toggleButtonMenu}
|
||||||
|
{...menuPosition}
|
||||||
|
menuPaddingRight={10}
|
||||||
|
menuPaddingTop={5}
|
||||||
|
menuPaddingBottom={5}
|
||||||
|
menuWidth={150}
|
||||||
|
wrapperClassName="mx_MessageComposer_Menu"
|
||||||
|
>
|
||||||
|
{ buttons.slice(1).map((button, index) => (
|
||||||
|
<MenuItem className="mx_CallContextMenu_item" key={index} onClick={this.toggleButtonMenu}>
|
||||||
|
{ button }
|
||||||
|
</MenuItem>
|
||||||
|
)) }
|
||||||
|
</ContextMenu>
|
||||||
|
) }
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const controls = [
|
const controls = [
|
||||||
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||||
|
@ -377,6 +511,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
null,
|
null,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let menuPosition;
|
||||||
|
if (this.ref.current) {
|
||||||
|
const contentRect = this.ref.current.getBoundingClientRect();
|
||||||
|
menuPosition = aboveLeftOf(contentRect);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||||
controls.push(
|
controls.push(
|
||||||
<SendMessageComposer
|
<SendMessageComposer
|
||||||
|
@ -392,33 +532,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
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") &&
|
|
||||||
!this.state.haveRecording) {
|
|
||||||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
controls.push(<VoiceRecordComposerTile
|
controls.push(<VoiceRecordComposerTile
|
||||||
key="controls_voice_record"
|
key="controls_voice_record"
|
||||||
ref={c => this.voiceRecordingButton = c}
|
ref={c => this.voiceRecordingButton = c}
|
||||||
room={this.props.room} />);
|
room={this.props.room} />);
|
||||||
|
|
||||||
if (!this.state.isComposerEmpty || this.state.haveRecording) {
|
|
||||||
controls.push(
|
|
||||||
<SendButton
|
|
||||||
key="controls_send"
|
|
||||||
onClick={this.sendMessage}
|
|
||||||
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.state.tombstone) {
|
} else if (this.state.tombstone) {
|
||||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||||
|
|
||||||
|
@ -459,6 +576,15 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
yOffset={-50}
|
yOffset={-50}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
controls.push(
|
||||||
|
<Stickerpicker
|
||||||
|
room={this.props.room}
|
||||||
|
showStickers={this.state.showStickers}
|
||||||
|
setShowStickers={this.showStickers}
|
||||||
|
menuPosition={menuPosition} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
const showSendButton = !this.state.isComposerEmpty || this.state.haveRecording;
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"mx_MessageComposer": true,
|
"mx_MessageComposer": true,
|
||||||
|
@ -467,7 +593,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes} ref={this.ref}>
|
||||||
{ recordingTooltip }
|
{ recordingTooltip }
|
||||||
<div className="mx_MessageComposer_wrapper">
|
<div className="mx_MessageComposer_wrapper">
|
||||||
{ this.props.showReplyPreview && (
|
{ this.props.showReplyPreview && (
|
||||||
|
@ -475,6 +601,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
) }
|
) }
|
||||||
<div className="mx_MessageComposer_row">
|
<div className="mx_MessageComposer_row">
|
||||||
{ controls }
|
{ controls }
|
||||||
|
{ this.renderButtons(menuPosition) }
|
||||||
|
{ showSendButton && (
|
||||||
|
<SendButton
|
||||||
|
key="controls_send"
|
||||||
|
onClick={this.sendMessage}
|
||||||
|
title={this.state.haveRecording ? _t("Send voice message") : undefined}
|
||||||
|
/>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import AppTile from '../elements/AppTile';
|
import AppTile from '../elements/AppTile';
|
||||||
|
@ -27,7 +26,6 @@ import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
|
import { ChevronFace, ContextMenu } from "../../structures/ContextMenu";
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
@ -44,10 +42,12 @@ const PERSISTED_ELEMENT_KEY = "stickerPicker";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
showStickers: boolean;
|
||||||
|
menuPosition?: any;
|
||||||
|
setShowStickers: (showStickers: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
showStickers: boolean;
|
|
||||||
imError: string;
|
imError: string;
|
||||||
stickerpickerX: number;
|
stickerpickerX: number;
|
||||||
stickerpickerY: number;
|
stickerpickerY: number;
|
||||||
|
@ -72,7 +72,6 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showStickers: false,
|
|
||||||
imError: null,
|
imError: null,
|
||||||
stickerpickerX: null,
|
stickerpickerX: null,
|
||||||
stickerpickerY: null,
|
stickerpickerY: null,
|
||||||
|
@ -114,7 +113,7 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
console.warn('No widget ID specified, not disabling assets');
|
console.warn('No widget ID specified, not disabling assets');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ showStickers: false });
|
this.props.setShowStickers(false);
|
||||||
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
@ -146,15 +145,15 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
this.sendVisibilityToWidget(this.state.showStickers);
|
this.sendVisibilityToWidget(this.props.showStickers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private imError(errorMsg: string, e: Error): void {
|
private imError(errorMsg: string, e: Error): void {
|
||||||
console.error(errorMsg, e);
|
console.error(errorMsg, e);
|
||||||
this.setState({
|
this.setState({
|
||||||
showStickers: false,
|
|
||||||
imError: _t(errorMsg),
|
imError: _t(errorMsg),
|
||||||
});
|
});
|
||||||
|
this.props.setShowStickers(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateWidget = (): void => {
|
private updateWidget = (): void => {
|
||||||
|
@ -194,12 +193,12 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
break;
|
break;
|
||||||
case "stickerpicker_close":
|
case "stickerpicker_close":
|
||||||
this.setState({ showStickers: false });
|
this.props.setShowStickers(false);
|
||||||
break;
|
break;
|
||||||
case Action.AfterRightPanelPhaseChange:
|
case Action.AfterRightPanelPhaseChange:
|
||||||
case "show_left_panel":
|
case "show_left_panel":
|
||||||
case "hide_left_panel":
|
case "hide_left_panel":
|
||||||
this.setState({ showStickers: false });
|
this.props.setShowStickers(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -338,8 +337,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||||
|
|
||||||
|
this.props.setShowStickers(true);
|
||||||
this.setState({
|
this.setState({
|
||||||
showStickers: true,
|
|
||||||
stickerpickerX: x,
|
stickerpickerX: x,
|
||||||
stickerpickerY: y,
|
stickerpickerY: y,
|
||||||
stickerpickerChevronOffset,
|
stickerpickerChevronOffset,
|
||||||
|
@ -351,8 +350,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
* @param {Event} ev Event that triggered the function call
|
* @param {Event} ev Event that triggered the function call
|
||||||
*/
|
*/
|
||||||
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
private onHideStickersClick = (ev: React.MouseEvent): void => {
|
||||||
if (this.state.showStickers) {
|
if (this.props.showStickers) {
|
||||||
this.setState({ showStickers: false });
|
this.props.setShowStickers(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -360,8 +359,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
* Called when the window is resized
|
* Called when the window is resized
|
||||||
*/
|
*/
|
||||||
private onResize = (): void => {
|
private onResize = (): void => {
|
||||||
if (this.state.showStickers) {
|
if (this.props.showStickers) {
|
||||||
this.setState({ showStickers: false });
|
this.props.setShowStickers(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -369,8 +368,8 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
* The stickers picker was hidden
|
* The stickers picker was hidden
|
||||||
*/
|
*/
|
||||||
private onFinished = (): void => {
|
private onFinished = (): void => {
|
||||||
if (this.state.showStickers) {
|
if (this.props.showStickers) {
|
||||||
this.setState({ showStickers: false });
|
this.props.setShowStickers(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -395,54 +394,23 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
let stickerPicker;
|
if (!this.props.showStickers) return null;
|
||||||
let stickersButton;
|
|
||||||
const className = classNames(
|
|
||||||
"mx_MessageComposer_button",
|
|
||||||
"mx_MessageComposer_stickers",
|
|
||||||
"mx_Stickers_hideStickers",
|
|
||||||
"mx_MessageComposer_button_highlight",
|
|
||||||
);
|
|
||||||
if (this.state.showStickers) {
|
|
||||||
// Show hide-stickers button
|
|
||||||
stickersButton =
|
|
||||||
<AccessibleButton
|
|
||||||
id='stickersButton'
|
|
||||||
key="controls_hide_stickers"
|
|
||||||
className={className}
|
|
||||||
onClick={this.onHideStickersClick}
|
|
||||||
title={_t("Hide Stickers")}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
stickerPicker = <ContextMenu
|
return <ContextMenu
|
||||||
chevronOffset={this.state.stickerpickerChevronOffset}
|
chevronOffset={this.state.stickerpickerChevronOffset}
|
||||||
chevronFace={ChevronFace.Bottom}
|
chevronFace={ChevronFace.Bottom}
|
||||||
left={this.state.stickerpickerX}
|
left={this.state.stickerpickerX}
|
||||||
top={this.state.stickerpickerY}
|
top={this.state.stickerpickerY}
|
||||||
menuWidth={this.popoverWidth}
|
menuWidth={this.popoverWidth}
|
||||||
menuHeight={this.popoverHeight}
|
menuHeight={this.popoverHeight}
|
||||||
onFinished={this.onFinished}
|
onFinished={this.onFinished}
|
||||||
menuPaddingTop={0}
|
menuPaddingTop={0}
|
||||||
menuPaddingLeft={0}
|
menuPaddingLeft={0}
|
||||||
menuPaddingRight={0}
|
menuPaddingRight={0}
|
||||||
zIndex={STICKERPICKER_Z_INDEX}
|
zIndex={STICKERPICKER_Z_INDEX}
|
||||||
>
|
{...this.props.menuPosition}
|
||||||
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
|
>
|
||||||
</ContextMenu>;
|
<GenericElementContextMenu element={this.getStickerpickerContent()} onResize={this.onFinished} />
|
||||||
} else {
|
</ContextMenu>;
|
||||||
// Show show-stickers button
|
|
||||||
stickersButton =
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
id='stickersButton'
|
|
||||||
key="controls_show_stickers"
|
|
||||||
className="mx_MessageComposer_button mx_MessageComposer_stickers"
|
|
||||||
onClick={this.onShowStickersClick}
|
|
||||||
title={_t("Show Stickers")}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
return <React.Fragment>
|
|
||||||
{ stickersButton }
|
|
||||||
{ stickerPicker }
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import React, { ReactNode } from "react";
|
||||||
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
import { IUpload, RecordingState, VoiceRecording } from "../../../audio/VoiceRecording";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import classNames from "classnames";
|
|
||||||
import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
|
import LiveRecordingWaveform from "../audio_messages/LiveRecordingWaveform";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
|
import LiveRecordingClock from "../audio_messages/LiveRecordingClock";
|
||||||
|
@ -137,7 +136,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
await this.disposeRecording();
|
await this.disposeRecording();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRecordStartEndClick = async () => {
|
public onRecordStartEndClick = async () => {
|
||||||
if (this.state.recorder) {
|
if (this.state.recorder) {
|
||||||
await this.state.recorder.stop();
|
await this.state.recorder.stop();
|
||||||
return;
|
return;
|
||||||
|
@ -215,27 +214,23 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): ReactNode {
|
public render(): ReactNode {
|
||||||
let stopOrRecordBtn;
|
if (!this.state.recordingPhase) return null;
|
||||||
let deleteButton;
|
|
||||||
if (!this.state.recordingPhase || this.state.recordingPhase === RecordingState.Started) {
|
|
||||||
const classes = classNames({
|
|
||||||
'mx_MessageComposer_button': !this.state.recorder,
|
|
||||||
'mx_MessageComposer_voiceMessage': !this.state.recorder,
|
|
||||||
'mx_VoiceRecordComposerTile_stop': this.state.recorder?.isRecording,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
let stopBtn;
|
||||||
|
let deleteButton;
|
||||||
|
if (this.state.recordingPhase === RecordingState.Started) {
|
||||||
let tooltip = _t("Send voice message");
|
let tooltip = _t("Send voice message");
|
||||||
if (!!this.state.recorder) {
|
if (!!this.state.recorder) {
|
||||||
tooltip = _t("Stop recording");
|
tooltip = _t("Stop recording");
|
||||||
}
|
}
|
||||||
|
|
||||||
stopOrRecordBtn = <AccessibleTooltipButton
|
stopBtn = <AccessibleTooltipButton
|
||||||
className={classes}
|
className="mx_VoiceRecordComposerTile_stop"
|
||||||
onClick={this.onRecordStartEndClick}
|
onClick={this.onRecordStartEndClick}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
/>;
|
/>;
|
||||||
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
if (this.state.recorder && !this.state.recorder?.isRecording) {
|
||||||
stopOrRecordBtn = null;
|
stopBtn = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,13 +259,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The record button (mic icon) is meant to be on the right edge, but we also want the
|
|
||||||
// stop button to be left of the waveform area. Luckily, none of the surrounding UI is
|
|
||||||
// rendered when we're not recording, so the record button ends up in the correct spot.
|
|
||||||
return (<>
|
return (<>
|
||||||
{ uploadIndicator }
|
{ uploadIndicator }
|
||||||
{ deleteButton }
|
{ deleteButton }
|
||||||
{ stopOrRecordBtn }
|
{ stopBtn }
|
||||||
{ this.renderWaveformArea() }
|
{ this.renderWaveformArea() }
|
||||||
</>);
|
</>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1559,12 +1559,19 @@
|
||||||
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
"%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)",
|
||||||
"Send message": "Send message",
|
"Send message": "Send message",
|
||||||
"Emoji picker": "Emoji picker",
|
"Emoji picker": "Emoji picker",
|
||||||
|
"Add emoji": "Add emoji",
|
||||||
"Upload file": "Upload file",
|
"Upload file": "Upload file",
|
||||||
|
"Reply to encrypted thread…": "Reply to encrypted thread…",
|
||||||
|
"Reply to thread…": "Reply to thread…",
|
||||||
"Send an encrypted reply…": "Send an encrypted reply…",
|
"Send an encrypted reply…": "Send an encrypted reply…",
|
||||||
"Send a reply…": "Send a reply…",
|
"Send a reply…": "Send a reply…",
|
||||||
"Send an encrypted message…": "Send an encrypted message…",
|
"Send an encrypted message…": "Send an encrypted message…",
|
||||||
"Send a message…": "Send a message…",
|
"Send a message…": "Send a message…",
|
||||||
|
"Hide Stickers": "Hide Stickers",
|
||||||
|
"Show Stickers": "Show Stickers",
|
||||||
|
"Send a sticker": "Send a sticker",
|
||||||
"Send voice message": "Send voice message",
|
"Send voice message": "Send voice message",
|
||||||
|
"More options": "More options",
|
||||||
"The conversation continues here.": "The conversation continues here.",
|
"The conversation continues here.": "The conversation continues here.",
|
||||||
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
"This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.",
|
||||||
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
"You do not have permission to post to this room": "You do not have permission to post to this room",
|
||||||
|
@ -1726,8 +1733,6 @@
|
||||||
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
||||||
"Add some now": "Add some now",
|
"Add some now": "Add some now",
|
||||||
"Stickerpack": "Stickerpack",
|
"Stickerpack": "Stickerpack",
|
||||||
"Hide Stickers": "Hide Stickers",
|
|
||||||
"Show Stickers": "Show Stickers",
|
|
||||||
"Failed to revoke invite": "Failed to revoke invite",
|
"Failed to revoke invite": "Failed to revoke invite",
|
||||||
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.",
|
"Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.",
|
||||||
"Admin Tools": "Admin Tools",
|
"Admin Tools": "Admin Tools",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue