You've already forked matrix-react-sdk
mirror of
https://github.com/matrix-org/matrix-react-sdk.git
synced 2025-11-08 21:42:24 +03:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17021
Conflicts: src/components/views/spaces/SpaceCreateMenu.tsx
This commit is contained in:
@@ -544,11 +544,13 @@ export default class MessagePanel extends React.Component {
|
||||
}
|
||||
if (!grouper) {
|
||||
const wantTile = this._shouldShowEvent(mxEv);
|
||||
const isGrouped = false;
|
||||
if (wantTile) {
|
||||
// make sure we unpack the array returned by _getTilesForEvent,
|
||||
// otherwise react will auto-generate keys and we will end up
|
||||
// replacing all of the DOM elements every time we paginate.
|
||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
|
||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped,
|
||||
nextEvent, nextTile));
|
||||
prevEvent = mxEv;
|
||||
}
|
||||
|
||||
@@ -564,7 +566,7 @@ export default class MessagePanel extends React.Component {
|
||||
return ret;
|
||||
}
|
||||
|
||||
_getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
|
||||
_getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
|
||||
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
|
||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
@@ -584,7 +586,7 @@ export default class MessagePanel extends React.Component {
|
||||
|
||||
// do we need a date separator since the last event?
|
||||
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
|
||||
if (wantsDateSeparator) {
|
||||
if (wantsDateSeparator && !isGrouped) {
|
||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||
ret.push(dateSeparator);
|
||||
}
|
||||
@@ -968,9 +970,9 @@ class CreationGrouper {
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
const panel = this.panel;
|
||||
const ret = [];
|
||||
const isGrouped = true;
|
||||
const createEvent = this.createEvent;
|
||||
const lastShownEvent = this.lastShownEvent;
|
||||
|
||||
@@ -984,12 +986,12 @@ class CreationGrouper {
|
||||
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
||||
if (panel._shouldShowEvent(createEvent)) {
|
||||
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
|
||||
ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
|
||||
ret.push(...panel._getTilesForEvent(createEvent, createEvent));
|
||||
}
|
||||
|
||||
for (const ejected of this.ejectedEvents) {
|
||||
ret.push(...panel._getTilesForEvent(
|
||||
createEvent, ejected, createEvent === lastShownEvent,
|
||||
createEvent, ejected, createEvent === lastShownEvent, isGrouped,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -998,7 +1000,7 @@ class CreationGrouper {
|
||||
// of EventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeparator is inserted.
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||
const ev = this.events[this.events.length - 1];
|
||||
@@ -1083,7 +1085,7 @@ class RedactionGrouper {
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
const isGrouped = true;
|
||||
const panel = this.panel;
|
||||
const ret = [];
|
||||
const lastShownEvent = this.lastShownEvent;
|
||||
@@ -1103,7 +1105,8 @@ class RedactionGrouper {
|
||||
let eventTiles = this.events.map((e, i) => {
|
||||
senders.add(e.sender);
|
||||
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
|
||||
return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile);
|
||||
return panel._getTilesForEvent(
|
||||
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
@@ -1182,7 +1185,7 @@ class MemberGrouper {
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
const isGrouped = true;
|
||||
const panel = this.panel;
|
||||
const lastShownEvent = this.lastShownEvent;
|
||||
const ret = [];
|
||||
@@ -1215,7 +1218,7 @@ class MemberGrouper {
|
||||
// of MemberEventListSummary, render each member event as if the previous
|
||||
// one was itself. This way, the timestamp of the previous event === the
|
||||
// timestamp of the current event, and no DateSeparator is inserted.
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent);
|
||||
return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
|
||||
}).reduce((a, b) => a.concat(b), []);
|
||||
|
||||
if (eventTiles.length === 0) {
|
||||
|
||||
@@ -27,8 +27,8 @@ import { Action } from "../../dispatcher/actions";
|
||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
||||
@@ -430,7 +430,9 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||
/>;
|
||||
});
|
||||
|
||||
const onNextClick = async () => {
|
||||
const onNextClick = async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (busy) return;
|
||||
setError("");
|
||||
setBusy(true);
|
||||
try {
|
||||
@@ -455,7 +457,10 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let onClick = onFinished;
|
||||
let onClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
onFinished();
|
||||
};
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (roomNames.some(name => name.trim())) {
|
||||
onClick = onNextClick;
|
||||
@@ -467,16 +472,20 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
|
||||
<div className="mx_SpaceRoomView_description">{ description }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
{ fields }
|
||||
<form onSubmit={onClick} id="mx_SpaceSetupFirstRooms">
|
||||
{ fields }
|
||||
</form>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={busy}
|
||||
onClick={onClick}
|
||||
>
|
||||
{ buttonLabel }
|
||||
</AccessibleButton>
|
||||
element="input"
|
||||
type="submit"
|
||||
form="mx_SpaceSetupFirstRooms"
|
||||
value={buttonLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
@@ -624,7 +633,9 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||
/>;
|
||||
});
|
||||
|
||||
const onNextClick = async () => {
|
||||
const onNextClick = async (ev) => {
|
||||
ev.preventDefault();
|
||||
if (busy) return;
|
||||
setError("");
|
||||
for (let i = 0; i < fieldRefs.length; i++) {
|
||||
const fieldRef = fieldRefs[i];
|
||||
@@ -658,7 +669,10 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
let onClick = onFinished;
|
||||
let onClick = (ev) => {
|
||||
ev.preventDefault();
|
||||
onFinished();
|
||||
};
|
||||
let buttonLabel = _t("Skip for now");
|
||||
if (emailAddresses.some(name => name.trim())) {
|
||||
onClick = onNextClick;
|
||||
@@ -683,7 +697,9 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||
</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
{ fields }
|
||||
<form onSubmit={onClick} id="mx_SpaceSetupPrivateInvite">
|
||||
{ fields }
|
||||
</form>
|
||||
|
||||
<div className="mx_SpaceRoomView_inviteTeammates_buttons">
|
||||
<AccessibleButton
|
||||
@@ -695,9 +711,15 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
|
||||
</div>
|
||||
|
||||
<div className="mx_SpaceRoomView_buttons">
|
||||
<AccessibleButton kind="primary" disabled={busy} onClick={onClick}>
|
||||
{ buttonLabel }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
disabled={busy}
|
||||
onClick={onClick}
|
||||
element="input"
|
||||
type="submit"
|
||||
form="mx_SpaceSetupPrivateInvite"
|
||||
value={buttonLabel}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
@@ -78,8 +78,10 @@ export default class MessageContextMenu extends React.Component {
|
||||
|
||||
// We explicitly decline to show the redact option on ACL events as it has a potential
|
||||
// to obliterate the room - https://github.com/matrix-org/synapse/issues/4042
|
||||
// Similarly for encryption events, since redacting them "breaks everything"
|
||||
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl;
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
||||
|
||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -16,20 +16,23 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import filesize from "filesize";
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||
import { getBlobSafeMimeType } from '../../../utils/blobs';
|
||||
|
||||
interface IProps {
|
||||
file: File;
|
||||
currentIndex: number;
|
||||
totalFiles?: number;
|
||||
onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void;
|
||||
}
|
||||
|
||||
@replaceableComponent("views.dialogs.UploadConfirmDialog")
|
||||
export default class UploadConfirmDialog extends React.Component {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
currentIndex: PropTypes.number,
|
||||
totalFiles: PropTypes.number,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
export default class UploadConfirmDialog extends React.Component<IProps> {
|
||||
private objectUrl: string;
|
||||
private mimeType: string;
|
||||
|
||||
static defaultProps = {
|
||||
totalFiles: 1,
|
||||
@@ -38,22 +41,28 @@ export default class UploadConfirmDialog extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._objectUrl = URL.createObjectURL(props.file);
|
||||
// Create a fresh `Blob` for previewing (even though `File` already is
|
||||
// one) so we can adjust the MIME type if needed.
|
||||
this.mimeType = getBlobSafeMimeType(props.file.type);
|
||||
const blob = new Blob([props.file], { type:
|
||||
this.mimeType,
|
||||
});
|
||||
this.objectUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._objectUrl) URL.revokeObjectURL(this._objectUrl);
|
||||
if (this.objectUrl) URL.revokeObjectURL(this.objectUrl);
|
||||
}
|
||||
|
||||
_onCancelClick = () => {
|
||||
private onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onUploadClick = () => {
|
||||
private onUploadClick = () => {
|
||||
this.props.onFinished(true);
|
||||
}
|
||||
|
||||
_onUploadAllClick = () => {
|
||||
private onUploadAllClick = () => {
|
||||
this.props.onFinished(true, true);
|
||||
}
|
||||
|
||||
@@ -75,10 +84,10 @@ export default class UploadConfirmDialog extends React.Component {
|
||||
}
|
||||
|
||||
let preview;
|
||||
if (this.props.file.type.startsWith('image/')) {
|
||||
if (this.mimeType.startsWith('image/')) {
|
||||
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
||||
<div className="mx_UploadConfirmDialog_previewInner">
|
||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
|
||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this.objectUrl} /></div>
|
||||
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
|
||||
</div>
|
||||
</div>;
|
||||
@@ -95,7 +104,7 @@ export default class UploadConfirmDialog extends React.Component {
|
||||
|
||||
let uploadAllButton;
|
||||
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
||||
uploadAllButton = <button onClick={this._onUploadAllClick}>
|
||||
uploadAllButton = <button onClick={this.onUploadAllClick}>
|
||||
{_t("Upload all")}
|
||||
</button>;
|
||||
}
|
||||
@@ -103,7 +112,7 @@ export default class UploadConfirmDialog extends React.Component {
|
||||
return (
|
||||
<BaseDialog className='mx_UploadConfirmDialog'
|
||||
fixedWidth={false}
|
||||
onFinished={this._onCancelClick}
|
||||
onFinished={this.onCancelClick}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
@@ -113,7 +122,7 @@ export default class UploadConfirmDialog extends React.Component {
|
||||
|
||||
<DialogButtons primaryButton={_t('Upload')}
|
||||
hasCancel={false}
|
||||
onPrimaryButtonClick={this._onUploadClick}
|
||||
onPrimaryButtonClick={this.onUploadClick}
|
||||
focus={true}
|
||||
>
|
||||
{uploadAllButton}
|
||||
@@ -49,6 +49,18 @@ const inPlaceOf = (elementRect) => ({
|
||||
});
|
||||
|
||||
const validServer = withValidation({
|
||||
deriveData: async ({ value }) => {
|
||||
try {
|
||||
// check if we can successfully load this server's room directory
|
||||
await MatrixClientPeg.get().publicRooms({
|
||||
limit: 1,
|
||||
server: value,
|
||||
});
|
||||
return {};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
@@ -57,21 +69,11 @@ const validServer = withValidation({
|
||||
}, {
|
||||
key: "available",
|
||||
final: true,
|
||||
test: async ({ value }) => {
|
||||
try {
|
||||
const opts = {
|
||||
limit: 1,
|
||||
server: value,
|
||||
};
|
||||
// check if we can successfully load this server's room directory
|
||||
await MatrixClientPeg.get().publicRooms(opts);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
test: async (_, { error }) => !error,
|
||||
valid: () => _t("Looks good"),
|
||||
invalid: () => _t("Can't find this server or its room list"),
|
||||
invalid: ({ error }) => error.errcode === "M_FORBIDDEN"
|
||||
? _t("You are not allowed to view this server's rooms list")
|
||||
: _t("Can't find this server or its room list"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -108,8 +108,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
window.addEventListener("resize", this.calculateZoom);
|
||||
// After the image loads for the first time we want to calculate the zoom
|
||||
this.image.current.addEventListener("load", this.calculateZoom);
|
||||
// Try to precalculate the zoom from width and height props
|
||||
this.calculateZoom();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -122,11 +120,8 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||
const image = this.image.current;
|
||||
const imageWrapper = this.imageWrapper.current;
|
||||
|
||||
const width = this.props.width || image.naturalWidth;
|
||||
const height = this.props.height || image.naturalHeight;
|
||||
|
||||
const zoomX = imageWrapper.clientWidth / width;
|
||||
const zoomY = imageWrapper.clientHeight / height;
|
||||
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
||||
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
||||
|
||||
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||
// display it in its original size
|
||||
|
||||
@@ -187,9 +187,15 @@ function DeviceItem({userId, device}: {userId: string, device: IDevice}) {
|
||||
verifyDevice(cli.getUser(userId), device);
|
||||
};
|
||||
|
||||
const deviceName = device.ambiguous ?
|
||||
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
|
||||
device.getDisplayName();
|
||||
let deviceName;
|
||||
if (!device.getDisplayName()?.trim()) {
|
||||
deviceName = device.deviceId;
|
||||
} else {
|
||||
deviceName = device.ambiguous ?
|
||||
device.getDisplayName() + " (" + device.deviceId + ")" :
|
||||
device.getDisplayName();
|
||||
}
|
||||
|
||||
let trustedLabel = null;
|
||||
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import * as sdk from '../../../index';
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import PropTypes from 'prop-types';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
import EditorModel from '../../../editor/model';
|
||||
@@ -24,16 +24,18 @@ import {getCaretOffsetAndText} from '../../../editor/dom';
|
||||
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
|
||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||
import {parseEvent} from '../../../editor/deserialize';
|
||||
import {PartCreator} from '../../../editor/parts';
|
||||
import {CommandPartCreator} from '../../../editor/parts';
|
||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||
import classNames from 'classnames';
|
||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||
import BasicMessageComposer from "./BasicMessageComposer";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
function _isReply(mxEvent) {
|
||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||
@@ -178,6 +180,22 @@ export default class EditMessageComposer extends React.Component {
|
||||
dis.fire(Action.FocusComposer);
|
||||
}
|
||||
|
||||
_isSlashCommand() {
|
||||
const parts = this.model.parts;
|
||||
const firstPart = parts[0];
|
||||
if (firstPart) {
|
||||
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
_isContentModified(newContent) {
|
||||
// if nothing has changed then bail
|
||||
const oldContent = this.props.editState.getEvent().getContent();
|
||||
@@ -190,19 +208,112 @@ export default class EditMessageComposer extends React.Component {
|
||||
return true;
|
||||
}
|
||||
|
||||
_sendEdit = () => {
|
||||
_getSlashCommand() {
|
||||
const commandText = this.model.parts.reduce((text, part) => {
|
||||
// use mxid to textify user pills in a command
|
||||
if (part.type === "user-pill") {
|
||||
return text + part.resourceId;
|
||||
}
|
||||
return text + part.text;
|
||||
}, "");
|
||||
const {cmd, args} = getCommand(commandText);
|
||||
return [cmd, args, commandText];
|
||||
}
|
||||
|
||||
async _runSlashCommand(cmd, args, roomId) {
|
||||
const result = cmd.run(roomId, args);
|
||||
let messageContent;
|
||||
let error = result.error;
|
||||
if (result.promise) {
|
||||
try {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
messageContent = await result.promise;
|
||||
} else {
|
||||
await result.promise;
|
||||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
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 = !!result.promise;
|
||||
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||
|
||||
let errText;
|
||||
if (typeof error === 'string') {
|
||||
errText = error;
|
||||
} else if (error.message) {
|
||||
errText = error.message;
|
||||
} else {
|
||||
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||
title: _t(title),
|
||||
description: errText,
|
||||
});
|
||||
} else {
|
||||
console.log("Command success.");
|
||||
if (messageContent) return messageContent;
|
||||
}
|
||||
}
|
||||
|
||||
_sendEdit = async () => {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const editedEvent = this.props.editState.getEvent();
|
||||
const editContent = createEditContent(this.model, editedEvent);
|
||||
const newContent = editContent["m.new_content"];
|
||||
let shouldSend = true;
|
||||
|
||||
// If content is modified then send an updated event into the room
|
||||
if (this._isContentModified(newContent)) {
|
||||
const roomId = editedEvent.getRoomId();
|
||||
this._cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||
const [cmd, args, commandText] = this._getSlashCommand();
|
||||
if (cmd) {
|
||||
if (cmd.category === CommandCategories.messages) {
|
||||
editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId);
|
||||
} else {
|
||||
this._runSlashCommand(cmd, args, roomId);
|
||||
shouldSend = false;
|
||||
}
|
||||
} else {
|
||||
// ask the user if their unknown command should be sent as a message
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||
title: _t("Unknown Command"),
|
||||
description: <div>
|
||||
<p>
|
||||
{ _t("Unrecognised command: %(commandText)s", {commandText}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("You can use <code>/help</code> to list available commands. " +
|
||||
"Did you mean to send this as a message?", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
<p>
|
||||
{ _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, {
|
||||
code: t => <code>{ t }</code>,
|
||||
}) }
|
||||
</p>
|
||||
</div>,
|
||||
button: _t('Send as message'),
|
||||
});
|
||||
const [sendAnyway] = await finished;
|
||||
// if !sendAnyway bail to let the user edit the composer and try again
|
||||
if (!sendAnyway) return;
|
||||
}
|
||||
}
|
||||
if (shouldSend) {
|
||||
this._cancelPreviousPendingEdit();
|
||||
const prom = this.context.sendMessage(roomId, editContent);
|
||||
dis.dispatch({action: "message_sent"});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||
}
|
||||
}
|
||||
|
||||
// close the event editing and focus composer
|
||||
@@ -240,7 +351,7 @@ export default class EditMessageComposer extends React.Component {
|
||||
_createEditorModel() {
|
||||
const {editState} = this.props;
|
||||
const room = this._getRoom();
|
||||
const partCreator = new PartCreator(room, this.context);
|
||||
const partCreator = new CommandPartCreator(room, this.context);
|
||||
let parts;
|
||||
if (editState.hasEditorState()) {
|
||||
// if restoring state from a previous editor,
|
||||
|
||||
@@ -32,17 +32,11 @@ interface IProps {
|
||||
setTopic(topic: string): void;
|
||||
}
|
||||
|
||||
const SpaceBasicSettings = ({
|
||||
export const SpaceAvatar = ({
|
||||
avatarUrl,
|
||||
avatarDisabled = false,
|
||||
setAvatar,
|
||||
name = "",
|
||||
nameDisabled = false,
|
||||
setName,
|
||||
topic = "",
|
||||
topicDisabled = false,
|
||||
setTopic,
|
||||
}: IProps) => {
|
||||
}: Pick<IProps, "avatarUrl" | "avatarDisabled" | "setAvatar">) => {
|
||||
const avatarUploadRef = useRef<HTMLInputElement>();
|
||||
const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
|
||||
|
||||
@@ -81,20 +75,34 @@ const SpaceBasicSettings = ({
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceBasicSettings_avatarContainer">
|
||||
{ avatarSection }
|
||||
<input type="file" ref={avatarUploadRef} onChange={(e) => {
|
||||
if (!e.target.files?.length) return;
|
||||
const file = e.target.files[0];
|
||||
setAvatar(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
setAvatarDataUrl(ev.target.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}} accept="image/*" />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const SpaceBasicSettings = ({
|
||||
avatarUrl,
|
||||
avatarDisabled = false,
|
||||
setAvatar,
|
||||
name = "",
|
||||
nameDisabled = false,
|
||||
setName,
|
||||
topic = "",
|
||||
topicDisabled = false,
|
||||
setTopic,
|
||||
}: IProps) => {
|
||||
return <div className="mx_SpaceBasicSettings">
|
||||
<div className="mx_SpaceBasicSettings_avatarContainer">
|
||||
{ avatarSection }
|
||||
<input type="file" ref={avatarUploadRef} onChange={(e) => {
|
||||
if (!e.target.files?.length) return;
|
||||
const file = e.target.files[0];
|
||||
setAvatar(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
setAvatarDataUrl(ev.target.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}} accept="image/*" />
|
||||
</div>
|
||||
<SpaceAvatar avatarUrl={avatarUrl} avatarDisabled={avatarDisabled} setAvatar={setAvatar} />
|
||||
|
||||
<Field
|
||||
name="spaceName"
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {useContext, useState} from "react";
|
||||
import React, {useContext, useRef, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
|
||||
import FocusLock from "react-focus-lock";
|
||||
@@ -24,12 +24,14 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
|
||||
import createRoom, {IStateEvent, Preset} from "../../../createRoom";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import SpaceBasicSettings from "./SpaceBasicSettings";
|
||||
import {SpaceAvatar} from "./SpaceBasicSettings";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {BetaPill} from "../beta/BetaCard";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
|
||||
import Field from "../elements/Field";
|
||||
import withValidation from "../elements/Validation";
|
||||
|
||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||
return (
|
||||
@@ -45,17 +47,39 @@ enum Visibility {
|
||||
Private,
|
||||
}
|
||||
|
||||
const spaceNameValidator = withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: async ({ value }) => !!value,
|
||||
invalid: () => _t("Please enter a name for the space"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const SpaceCreateMenu = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [avatar, setAvatar] = useState<File>(null);
|
||||
const [topic, setTopic] = useState<string>("");
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
const onSpaceCreateClick = async () => {
|
||||
const [name, setName] = useState("");
|
||||
const spaceNameField = useRef<Field>();
|
||||
const [avatar, setAvatar] = useState<File>(null);
|
||||
const [topic, setTopic] = useState<string>("");
|
||||
|
||||
const onSpaceCreateClick = async (e) => {
|
||||
e.preventDefault();
|
||||
if (busy) return;
|
||||
|
||||
setBusy(true);
|
||||
// require & validate the space name field
|
||||
if (!await spaceNameField.current.validate({ allowEmpty: false })) {
|
||||
spaceNameField.current.focus();
|
||||
spaceNameField.current.validate({ allowEmpty: false, focused: true });
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const initialState: IStateEvent[] = [
|
||||
{
|
||||
type: EventType.RoomHistoryVisibility,
|
||||
@@ -150,9 +174,30 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||
}
|
||||
</p>
|
||||
|
||||
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
|
||||
<form className="mx_SpaceBasicSettings" onSubmit={onSpaceCreateClick}>
|
||||
<SpaceAvatar setAvatar={setAvatar} />
|
||||
|
||||
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name || busy}>
|
||||
<Field
|
||||
name="spaceName"
|
||||
label={_t("Name")}
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={ev => setName(ev.target.value)}
|
||||
ref={spaceNameField}
|
||||
onValidate={spaceNameValidator}
|
||||
/>
|
||||
|
||||
<Field
|
||||
name="spaceTopic"
|
||||
element="textarea"
|
||||
label={_t("Description")}
|
||||
value={topic}
|
||||
onChange={ev => setTopic(ev.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={busy}>
|
||||
{ busy ? _t("Creating...") : _t("Create") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
|
||||
@@ -26,13 +26,11 @@ import {SpaceItem} from "./SpaceTreeLevel";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore, {
|
||||
HOME_SPACE,
|
||||
UPDATE_INVITED_SPACES,
|
||||
UPDATE_SELECTED_SPACE,
|
||||
UPDATE_TOP_LEVEL_SPACES,
|
||||
} from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {
|
||||
RovingAccessibleButton,
|
||||
@@ -40,13 +38,15 @@ import {
|
||||
RovingTabIndexProvider,
|
||||
} from "../../../accessibility/RovingTabIndex";
|
||||
import {Key} from "../../../Keyboard";
|
||||
import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import {NotificationState} from "../../../stores/notifications/NotificationState";
|
||||
|
||||
interface IButtonProps {
|
||||
space?: Room;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
tooltip?: string;
|
||||
notificationState?: SpaceNotificationState;
|
||||
notificationState?: NotificationState;
|
||||
isNarrow?: boolean;
|
||||
onClick(): void;
|
||||
}
|
||||
@@ -212,8 +212,8 @@ const SpacePanel = () => {
|
||||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
selected={!activeSpace}
|
||||
tooltip={_t("Home")}
|
||||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||
tooltip={_t("All rooms")}
|
||||
notificationState={RoomNotificationStateStore.instance.globalState}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
{ invites.map(s => <SpaceItem
|
||||
|
||||
Reference in New Issue
Block a user