Merge remote-tracking branch 'upstream/develop' into task/dialogs-ts

This commit is contained in:
Šimon Brandner 2021-09-17 18:25:14 +02:00
commit 31e1831f02
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
71 changed files with 1742 additions and 1476 deletions

View file

@ -15,43 +15,48 @@ limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { _t } from "../../../languageHandler";
import MemberAvatar from '../avatars/MemberAvatar';
import classNames from 'classnames';
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore";
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
interface IProps {
member: RoomMember;
width?: number;
height?: number;
resizeMethod?: ResizeMethod;
}
interface IState {
hasStatus: boolean;
menuDisplayed: boolean;
}
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
export default class MemberStatusMessageAvatar extends React.Component {
static propTypes = {
member: PropTypes.object.isRequired,
width: PropTypes.number,
height: PropTypes.number,
resizeMethod: PropTypes.string,
};
static defaultProps = {
export default class MemberStatusMessageAvatar extends React.Component<IProps, IState> {
public static defaultProps: Partial<IProps> = {
width: 40,
height: 40,
resizeMethod: 'crop',
};
private button = createRef<HTMLDivElement>();
constructor(props) {
constructor(props: IProps) {
super(props);
this.state = {
hasStatus: this.hasStatus,
menuDisplayed: false,
};
this._button = createRef();
}
componentDidMount() {
public componentDidMount(): void {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}
@ -62,44 +67,44 @@ export default class MemberStatusMessageAvatar extends React.Component {
if (!user) {
return;
}
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
}
componentWillUnmount() {
public componentWillUnmount(): void {
const { user } = this.props.member;
if (!user) {
return;
}
user.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
this.onStatusMessageCommitted,
);
}
get hasStatus() {
private get hasStatus(): boolean {
const { user } = this.props.member;
if (!user) {
return false;
}
return !!user._unstable_statusMessage;
return !!user.unstable_statusMessage;
}
_onStatusMessageCommitted = () => {
private onStatusMessageCommitted = (): void => {
// The `User` object has observed a status message change.
this.setState({
hasStatus: this.hasStatus,
});
};
openMenu = () => {
private openMenu = (): void => {
this.setState({ menuDisplayed: true });
};
closeMenu = () => {
private closeMenu = (): void => {
this.setState({ menuDisplayed: false });
};
render() {
public render(): JSX.Element {
const avatar = <MemberAvatar
member={this.props.member}
width={this.props.width}
@ -118,7 +123,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._button.current.getBoundingClientRect();
const elementRect = this.button.current.getBoundingClientRect();
const chevronWidth = 16; // See .mx_ContextualMenu_chevron_bottom
const chevronMargin = 1; // Add some spacing away from target
@ -126,13 +131,13 @@ export default class MemberStatusMessageAvatar extends React.Component {
contextMenu = (
<ContextMenu
chevronOffset={(elementRect.width - chevronWidth) / 2}
chevronFace="bottom"
chevronFace={ChevronFace.Bottom}
left={elementRect.left + window.pageXOffset}
top={elementRect.top + window.pageYOffset - chevronMargin}
menuWidth={226}
onFinished={this.closeMenu}
>
<StatusMessageContextMenu user={this.props.member.user} onFinished={this.closeMenu} />
<StatusMessageContextMenu user={this.props.member.user} />
</ContextMenu>
);
}
@ -140,7 +145,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
return <React.Fragment>
<ContextMenuButton
className={classes}
inputRef={this._button}
inputRef={this.button}
onClick={this.openMenu}
isExpanded={this.state.menuDisplayed}
label={_t("User Status")}

View file

@ -15,45 +15,41 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent";
/*
interface IProps {
element: React.ReactNode;
// Function to be called when the parent window is resized
// This can be used to reposition or close the menu on resize and
// ensure that it is not displayed in a stale position.
onResize?: () => void;
}
/**
* This component can be used to display generic HTML content in a contextual
* menu.
*/
@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component {
static propTypes = {
element: PropTypes.element.isRequired,
// Function to be called when the parent window is resized
// This can be used to reposition or close the menu on resize and
// ensure that it is not displayed in a stale position.
onResize: PropTypes.func,
};
constructor(props) {
export default class GenericElementContextMenu extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
this.resize = this.resize.bind(this);
}
componentDidMount() {
this.resize = this.resize.bind(this);
public componentDidMount(): void {
window.addEventListener("resize", this.resize);
}
componentWillUnmount() {
public componentWillUnmount(): void {
window.removeEventListener("resize", this.resize);
}
resize() {
private resize = (): void => {
if (this.props.onResize) {
this.props.onResize();
}
}
};
render() {
public render(): JSX.Element {
return <div>{ this.props.element }</div>;
}
}

View file

@ -15,16 +15,15 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component {
static propTypes = {
message: PropTypes.string.isRequired,
};
interface IProps {
message: string;
}
render() {
@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component<IProps> {
public render(): JSX.Element {
return <div>{ this.props.message }</div>;
}
}

View file

@ -14,53 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import React, { ChangeEvent } from 'react';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import AccessibleButton from '../elements/AccessibleButton';
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { User } from "matrix-js-sdk/src/models/user";
import Spinner from "../elements/Spinner";
interface IProps {
// js-sdk User object. Not required because it might not exist.
user?: User;
}
interface IState {
message: string;
waiting: boolean;
}
@replaceableComponent("views.context_menus.StatusMessageContextMenu")
export default class StatusMessageContextMenu extends React.Component {
static propTypes = {
// js-sdk User object. Not required because it might not exist.
user: PropTypes.object,
};
constructor(props) {
export default class StatusMessageContextMenu extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
message: this.comittedStatusMessage,
waiting: false,
};
}
componentDidMount() {
public componentDidMount(): void {
const { user } = this.props;
if (!user) {
return;
}
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
user.on("User._unstable_statusMessage", this.onStatusMessageCommitted);
}
componentWillUnmount() {
public componentWillUnmount(): void {
const { user } = this.props;
if (!user) {
return;
}
user.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
this.onStatusMessageCommitted,
);
}
get comittedStatusMessage() {
return this.props.user ? this.props.user._unstable_statusMessage : "";
get comittedStatusMessage(): string {
return this.props.user ? this.props.user.unstable_statusMessage : "";
}
_onStatusMessageCommitted = () => {
private onStatusMessageCommitted = (): void => {
// The `User` object has observed a status message change.
this.setState({
message: this.comittedStatusMessage,
@ -68,14 +74,14 @@ export default class StatusMessageContextMenu extends React.Component {
});
};
_onClearClick = (e) => {
private onClearClick = (): void=> {
MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({
waiting: true,
});
};
_onSubmit = (e) => {
private onSubmit = (e: ButtonEvent): void => {
e.preventDefault();
MatrixClientPeg.get()._unstable_setStatusMessage(this.state.message);
this.setState({
@ -83,27 +89,25 @@ export default class StatusMessageContextMenu extends React.Component {
});
};
_onStatusChange = (e) => {
private onStatusChange = (e: ChangeEvent): void => {
// The input field's value was changed.
this.setState({
message: e.target.value,
message: (e.target as HTMLInputElement).value,
});
};
render() {
const Spinner = sdk.getComponent('views.elements.Spinner');
public render(): JSX.Element {
let actionButton;
if (this.comittedStatusMessage) {
if (this.state.message === this.comittedStatusMessage) {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_clear"
onClick={this._onClearClick}
onClick={this.onClearClick}
>
<span>{ _t("Clear status") }</span>
</AccessibleButton>;
} else {
actionButton = <AccessibleButton className="mx_StatusMessageContextMenu_submit"
onClick={this._onSubmit}
onClick={this.onSubmit}
>
<span>{ _t("Update status") }</span>
</AccessibleButton>;
@ -112,7 +116,7 @@ export default class StatusMessageContextMenu extends React.Component {
actionButton = <AccessibleButton
className="mx_StatusMessageContextMenu_submit"
disabled={!this.state.message}
onClick={this._onSubmit}
onClick={this.onSubmit}
>
<span>{ _t("Set status") }</span>
</AccessibleButton>;
@ -120,13 +124,13 @@ export default class StatusMessageContextMenu extends React.Component {
let spinner = null;
if (this.state.waiting) {
spinner = <Spinner w="24" h="24" />;
spinner = <Spinner w={24} h={24} />;
}
const form = <form
className="mx_StatusMessageContextMenu_form"
autoComplete="off"
onSubmit={this._onSubmit}
onSubmit={this.onSubmit}
>
<input
type="text"
@ -134,9 +138,9 @@ export default class StatusMessageContextMenu extends React.Component {
key="message"
placeholder={_t("Set a new status...")}
autoFocus={true}
maxLength="60"
maxLength={60}
value={this.state.message}
onChange={this._onStatusChange}
onChange={this.onStatusChange}
/>
<div className="mx_StatusMessageContextMenu_actionContainer">
{ actionButton }

View file

@ -258,7 +258,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content">

View file

@ -243,7 +243,6 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search for rooms or people")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ForwardList_content">

View file

@ -57,7 +57,6 @@ const SpaceChildPicker = ({ filterPlaceholder, rooms, selected, onChange }) => {
className="mx_textinput_icon mx_textinput_search"
placeholder={filterPlaceholder}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_LeaveSpaceDialog_content">

View file

@ -126,7 +126,6 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
className="mx_textinput_icon mx_textinput_search"
placeholder={_t("Search spaces")}
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_ManageRestrictedJoinRuleDialog_content">

View file

@ -135,7 +135,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
const desc = formatCommaSeparatedList(descs);
return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc });
return _t('%(nameList)s %(transitionList)s', { nameList, transitionList: desc });
});
if (!summaries) {

View file

@ -106,31 +106,20 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
}
const room = this.context.getRoom(mxEvent.getRoomId());
let label;
let label: string;
if (room) {
const senders = [];
for (const reactionEvent of reactionEvents) {
const member = room.getMember(reactionEvent.getSender());
const name = member ? member.name : reactionEvent.getSender();
senders.push(name);
senders.push(member?.name || reactionEvent.getSender());
}
const reactors = formatCommaSeparatedList(senders, 6);
if (content) {
label = _t("%(reactors)s reacted with %(content)s", { reactors, content });
} else {
label = reactors;
}
label = _t(
"<reactors/><reactedWith> reacted with %(content)s</reactedWith>",
{
content,
},
{
reactors: () => {
return formatCommaSeparatedList(senders, 6);
},
reactedWith: (sub) => {
if (!content) {
return null;
}
return sub;
},
},
);
}
const isPeeking = room.getMyMembership() !== "join";
return <AccessibleButton

View file

@ -429,7 +429,7 @@ const UserOptionsSection: React.FC<{
if (!isMe) {
directMessageButton = (
<AccessibleButton onClick={() => { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field">
{ _t('Direct message') }
{ _t("Message") }
</AccessibleButton>
);
}

View file

@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
import { replaceableComponent } from "../../../utils/replaceableComponent";
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
@ -161,7 +162,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}
}
private replaceEmoticon = (caretPosition: DocumentPosition): number => {
public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
const { model } = this.props;
const range = model.startRange(caretPosition);
// expand range max 8 characters backwards from caretPosition,
@ -170,9 +171,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
range.expandBackwardsWhile((index, offset) => {
const part = model.parts[index];
n -= 1;
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
});
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
const emoticonMatch = regex.exec(range.text);
if (emoticonMatch) {
const query = emoticonMatch[1].replace("-", "");
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
@ -180,18 +181,23 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
if (data) {
const { partCreator } = model;
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
const moveStart = emoticonMatch[0][0] === " " ? 1 : 0;
const moveEnd = emoticonMatch[0].length - emoticonMatch.length - moveStart;
// we need the range to only comprise of the emoticon
// because we'll replace the whole range with an emoji,
// so move the start forward to the start of the emoticon.
// Take + 1 because index is reported without the possible preceding space.
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
range.moveStartForwards(emoticonMatch.index + moveStart);
// and move end backwards so that we don't replace the trailing space/newline
range.moveEndBackwards(moveEnd);
// this returns the amount of added/removed characters during the replace
// so the caret position can be adjusted.
return range.replace([partCreator.plain(data.unicode + " ")]);
return range.replace([partCreator.plain(data.unicode)]);
}
}
};
}
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
renderModel(this.editorRef.current, this.props.model);
@ -607,8 +613,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
};
private configureEmoticonAutoReplace = (): void => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
this.props.model.setTransformCallback(this.transform);
};
private configureShouldShowPillAvatar = (): void => {
@ -621,6 +626,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.setState({ surroundWith });
};
private transform = (documentPosition: DocumentPosition): void => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
};
componentWillUnmount() {
document.removeEventListener("selectionchange", this.onSelectionChange);
this.editorRef.current.removeEventListener("input", this.onInput, true);

View file

@ -21,7 +21,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { Thread } from 'matrix-js-sdk/src/models/thread';
import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import ReplyThread from "../elements/ReplyThread";
import { _t } from '../../../languageHandler';
@ -464,8 +464,8 @@ export default class EventTile extends React.Component<IProps, IState> {
}
if (SettingsStore.getValue("feature_thread")) {
this.props.mxEvent.once("Thread.ready", this.updateThread);
this.props.mxEvent.on("Thread.update", this.updateThread);
this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread);
this.props.mxEvent.on(ThreadEvent.Update, this.updateThread);
}
}
@ -1192,14 +1192,19 @@ export default class EventTile extends React.Component<IProps, IState> {
}
default: {
const thread = ReplyThread.makeThread(
this.props.mxEvent,
this.props.onHeightChanged,
this.props.permalinkCreator,
this.replyThread,
this.props.layout,
this.props.alwaysShowTimestamps || this.state.hover,
);
let thread;
// When the "showHiddenEventsInTimeline" lab is enabled,
// avoid showing replies for hidden events (events without tiles)
if (haveTileForEvent(this.props.mxEvent)) {
thread = ReplyThread.makeThread(
this.props.mxEvent,
this.props.onHeightChanged,
this.props.permalinkCreator,
this.replyThread,
this.props.layout,
this.props.alwaysShowTimestamps || this.state.hover,
);
}
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();

View file

@ -57,7 +57,7 @@ let instanceCount = 0;
const NARROW_MODE_BREAKPOINT = 500;
interface IComposerAvatarProps {
me: object;
me: RoomMember;
}
function ComposerAvatar(props: IComposerAvatarProps) {

View file

@ -547,7 +547,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const unfilteredHistorical = unfilteredLists[DefaultTagID.Archived] || [];
const unfilteredFavourite = unfilteredLists[DefaultTagID.Favourite] || [];
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
if (unfilteredRooms.length < 1 && unfilteredHistorical.length < 1 && unfilteredFavourite.length < 1) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
<AccessibleButton

View file

@ -14,8 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import React from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { IJoinRuleEventContent, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import * as sdk from '../../../index';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
@ -27,91 +32,102 @@ import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import InviteReason from "../elements/InviteReason";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
const MemberEventHtmlReasonField = "io.element.html_reason";
const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn",
Joining: "Joining",
Loading: "Loading",
Rejecting: "Rejecting",
Kicked: "Kicked",
Banned: "Banned",
OtherThreePIDError: "OtherThreePIDError",
InvitedEmailNotFoundInAccount: "InvitedEmailNotFoundInAccount",
InvitedEmailNoIdentityServer: "InvitedEmailNoIdentityServer",
InvitedEmailMismatch: "InvitedEmailMismatch",
Invite: "Invite",
ViewingRoom: "ViewingRoom",
RoomNotFound: "RoomNotFound",
OtherError: "OtherError",
});
enum MessageCase {
NotLoggedIn = "NotLoggedIn",
Joining = "Joining",
Loading = "Loading",
Rejecting = "Rejecting",
Kicked = "Kicked",
Banned = "Banned",
OtherThreePIDError = "OtherThreePIDError",
InvitedEmailNotFoundInAccount = "InvitedEmailNotFoundInAccount",
InvitedEmailNoIdentityServer = "InvitedEmailNoIdentityServer",
InvitedEmailMismatch = "InvitedEmailMismatch",
Invite = "Invite",
ViewingRoom = "ViewingRoom",
RoomNotFound = "RoomNotFound",
OtherError = "OtherError",
}
interface IProps {
// if inviterName is specified, the preview bar will shown an invite to the room.
// You should also specify onRejectClick if specifying inviterName
inviterName?: string;
// If invited by 3rd party invite, the email address the invite was sent to
invitedEmail?: string;
// For third party invites, information passed about the room out-of-band
oobData?: IOOBData;
// For third party invites, a URL for a 3pid invite signing service
signUrl?: string;
// A standard client/server API error object. If supplied, indicates that the
// caller was unable to fetch details about the room for the given reason.
error?: MatrixError;
canPreview?: boolean;
previewLoading?: boolean;
room?: Room;
loading?: boolean;
joining?: boolean;
rejecting?: boolean;
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias?: string;
onJoinClick?(): void;
onRejectClick?(): void;
onRejectAndIgnoreClick?(): void;
onForgetClick?(): void;
}
interface IState {
busy: boolean;
accountEmails?: string[];
invitedEmailMxid?: string;
threePidFetchError?: MatrixError;
}
@replaceableComponent("views.rooms.RoomPreviewBar")
export default class RoomPreviewBar extends React.Component {
static propTypes = {
onJoinClick: PropTypes.func,
onRejectClick: PropTypes.func,
onRejectAndIgnoreClick: PropTypes.func,
onForgetClick: PropTypes.func,
// if inviterName is specified, the preview bar will shown an invite to the room.
// You should also specify onRejectClick if specifiying inviterName
inviterName: PropTypes.string,
// If invited by 3rd party invite, the email address the invite was sent to
invitedEmail: PropTypes.string,
// For third party invites, information passed about the room out-of-band
oobData: PropTypes.object,
// For third party invites, a URL for a 3pid invite signing service
signUrl: PropTypes.string,
// A standard client/server API error object. If supplied, indicates that the
// caller was unable to fetch details about the room for the given reason.
error: PropTypes.object,
canPreview: PropTypes.bool,
previewLoading: PropTypes.bool,
room: PropTypes.object,
// When a spinner is present, a spinnerState can be specified to indicate the
// purpose of the spinner.
spinner: PropTypes.bool,
spinnerState: PropTypes.oneOf(["joining"]),
loading: PropTypes.bool,
joining: PropTypes.bool,
rejecting: PropTypes.bool,
// The alias that was used to access this room, if appropriate
// If given, this will be how the room is referred to (eg.
// in error messages).
roomAlias: PropTypes.string,
};
export default class RoomPreviewBar extends React.Component<IProps, IState> {
static defaultProps = {
onJoinClick() {},
};
state = {
busy: false,
};
constructor(props) {
super(props);
this.state = {
busy: false,
};
}
componentDidMount() {
this._checkInvitedEmail();
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
this.checkInvitedEmail();
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
}
componentDidUpdate(prevProps, prevState) {
if (this.props.invitedEmail !== prevProps.invitedEmail || this.props.inviterName !== prevProps.inviterName) {
this._checkInvitedEmail();
this.checkInvitedEmail();
}
}
componentWillUnmount() {
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
}
async _checkInvitedEmail() {
private async checkInvitedEmail() {
// If this is an invite and we've been told what email address was
// invited, fetch the user's account emails and discovery bindings so we
// can check them against the email that was invited.
@ -121,8 +137,7 @@ export default class RoomPreviewBar extends React.Component {
// Gather the account 3PIDs
const account3pids = await MatrixClientPeg.get().getThreePids();
this.setState({
accountEmails: account3pids.threepids
.filter(b => b.medium === 'email').map(b => b.address),
accountEmails: account3pids.threepids.filter(b => b.medium === 'email').map(b => b.address),
});
// If we have an IS connected, use that to lookup the email and
// check the bound MXID.
@ -146,21 +161,21 @@ export default class RoomPreviewBar extends React.Component {
}
}
_onCommunityUpdate = (roomId) => {
private onCommunityUpdate = (roomId: string): void => {
if (this.props.room && this.props.room.roomId !== roomId) {
return;
}
this.forceUpdate(); // we have nothing to update
};
_getMessageCase() {
private getMessageCase(): MessageCase {
const isGuest = MatrixClientPeg.get().isGuest();
if (isGuest) {
return MessageCase.NotLoggedIn;
}
const myMember = this._getMyMember();
const myMember = this.getMyMember();
if (myMember) {
if (myMember.isKicked()) {
@ -195,7 +210,7 @@ export default class RoomPreviewBar extends React.Component {
}
return MessageCase.Invite;
} else if (this.props.error) {
if (this.props.error.errcode == 'M_NOT_FOUND') {
if ((this.props.error as MatrixError).errcode == 'M_NOT_FOUND') {
return MessageCase.RoomNotFound;
} else {
return MessageCase.OtherError;
@ -205,8 +220,8 @@ export default class RoomPreviewBar extends React.Component {
}
}
_getKickOrBanInfo() {
const myMember = this._getMyMember();
private getKickOrBanInfo(): { memberName?: string, reason?: string } {
const myMember = this.getMyMember();
if (!myMember) {
return {};
}
@ -219,24 +234,19 @@ export default class RoomPreviewBar extends React.Component {
return { memberName, reason };
}
_joinRule() {
const room = this.props.room;
if (room) {
const joinRules = room.currentState.getStateEvents('m.room.join_rules', '');
if (joinRules) {
return joinRules.getContent().join_rule;
}
}
private joinRule(): JoinRule {
return this.props.room?.currentState
.getStateEvents(EventType.RoomJoinRules, "")?.getContent<IJoinRuleEventContent>().join_rule;
}
_communityProfile() {
private communityProfile(): { displayName?: string, avatarMxc?: string } {
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
return { displayName: null, avatarMxc: null };
}
_roomName(atStart = false) {
private roomName(atStart = false): string {
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
const profile = this._communityProfile();
const profile = this.communityProfile();
if (profile.displayName) name = profile.displayName;
if (name) {
return name;
@ -247,14 +257,11 @@ export default class RoomPreviewBar extends React.Component {
}
}
_getMyMember() {
return (
this.props.room &&
this.props.room.getMember(MatrixClientPeg.get().getUserId())
);
private getMyMember(): RoomMember {
return this.props.room?.getMember(MatrixClientPeg.get().getUserId());
}
_getInviteMember() {
private getInviteMember(): RoomMember {
const { room } = this.props;
if (!room) {
return;
@ -268,8 +275,8 @@ export default class RoomPreviewBar extends React.Component {
return room.currentState.getMember(inviterUserId);
}
_isDMInvite() {
const myMember = this._getMyMember();
private isDMInvite(): boolean {
const myMember = this.getMyMember();
if (!myMember) {
return false;
}
@ -278,7 +285,7 @@ export default class RoomPreviewBar extends React.Component {
return memberContent.membership === "invite" && memberContent.is_direct;
}
_makeScreenAfterLogin() {
private makeScreenAfterLogin(): { screen: string, params: Record<string, any> } {
return {
screen: 'room',
params: {
@ -291,18 +298,16 @@ export default class RoomPreviewBar extends React.Component {
};
}
onLoginClick = () => {
dis.dispatch({ action: 'start_login', screenAfterLogin: this._makeScreenAfterLogin() });
private onLoginClick = () => {
dis.dispatch({ action: 'start_login', screenAfterLogin: this.makeScreenAfterLogin() });
};
onRegisterClick = () => {
dis.dispatch({ action: 'start_registration', screenAfterLogin: this._makeScreenAfterLogin() });
private onRegisterClick = () => {
dis.dispatch({ action: 'start_registration', screenAfterLogin: this.makeScreenAfterLogin() });
};
render() {
const brand = SdkConfig.get().brand;
const Spinner = sdk.getComponent('elements.Spinner');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let showSpinner = false;
let title;
@ -315,10 +320,10 @@ export default class RoomPreviewBar extends React.Component {
let footer;
const extraComponents = [];
const messageCase = this._getMessageCase();
const messageCase = this.getMessageCase();
switch (messageCase) {
case MessageCase.Joining: {
title = _t("Joining room …");
title = this.props.oobData.roomType === RoomType.Space ? _t("Joining space …") : _t("Joining room …");
showSpinner = true;
break;
}
@ -349,12 +354,12 @@ export default class RoomPreviewBar extends React.Component {
break;
}
case MessageCase.Kicked: {
const { memberName, reason } = this._getKickOrBanInfo();
const { memberName, reason } = this.getKickOrBanInfo();
title = _t("You were kicked from %(roomName)s by %(memberName)s",
{ memberName, roomName: this._roomName() });
{ memberName, roomName: this.roomName() });
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
if (this._joinRule() === "invite") {
if (this.joinRule() === "invite") {
primaryActionLabel = _t("Forget this room");
primaryActionHandler = this.props.onForgetClick;
} else {
@ -366,9 +371,9 @@ export default class RoomPreviewBar extends React.Component {
break;
}
case MessageCase.Banned: {
const { memberName, reason } = this._getKickOrBanInfo();
const { memberName, reason } = this.getKickOrBanInfo();
title = _t("You were banned from %(roomName)s by %(memberName)s",
{ memberName, roomName: this._roomName() });
{ memberName, roomName: this.roomName() });
subTitle = reason ? _t("Reason: %(reason)s", { reason }) : null;
primaryActionLabel = _t("Forget this room");
primaryActionHandler = this.props.onForgetClick;
@ -376,8 +381,8 @@ export default class RoomPreviewBar extends React.Component {
}
case MessageCase.OtherThreePIDError: {
title = _t("Something went wrong with your invite to %(roomName)s",
{ roomName: this._roomName() });
const joinRule = this._joinRule();
{ roomName: this.roomName() });
const joinRule = this.joinRule();
const errCodeMessage = _t(
"An error (%(errcode)s) was returned while trying to validate your " +
"invite. You could try to pass this information on to a room admin.",
@ -410,7 +415,7 @@ export default class RoomPreviewBar extends React.Component {
"This invite to %(roomName)s was sent to %(email)s which is not " +
"associated with your account",
{
roomName: this._roomName(),
roomName: this.roomName(),
email: this.props.invitedEmail,
},
);
@ -427,7 +432,7 @@ export default class RoomPreviewBar extends React.Component {
title = _t(
"This invite to %(roomName)s was sent to %(email)s",
{
roomName: this._roomName(),
roomName: this.roomName(),
email: this.props.invitedEmail,
},
);
@ -443,7 +448,7 @@ export default class RoomPreviewBar extends React.Component {
title = _t(
"This invite to %(roomName)s was sent to %(email)s",
{
roomName: this._roomName(),
roomName: this.roomName(),
email: this.props.invitedEmail,
},
);
@ -458,11 +463,11 @@ export default class RoomPreviewBar extends React.Component {
case MessageCase.Invite: {
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
const oobData = Object.assign({}, this.props.oobData, {
avatarUrl: this._communityProfile().avatarMxc,
avatarUrl: this.communityProfile().avatarMxc,
});
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
const inviteMember = this._getInviteMember();
const inviteMember = this.getInviteMember();
let inviterElement;
if (inviteMember) {
inviterElement = <span>
@ -474,7 +479,7 @@ export default class RoomPreviewBar extends React.Component {
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
}
const isDM = this._isDMInvite();
const isDM = this.isDMInvite();
if (isDM) {
title = _t("Do you want to chat with %(user)s?",
{ user: inviteMember.name });
@ -485,7 +490,7 @@ export default class RoomPreviewBar extends React.Component {
primaryActionLabel = _t("Start chatting");
} else {
title = _t("Do you want to join %(roomName)s?",
{ roomName: this._roomName() });
{ roomName: this.roomName() });
subTitle = [
avatar,
_t("<userName/> invited you", {}, { userName: () => inviterElement }),
@ -519,22 +524,22 @@ export default class RoomPreviewBar extends React.Component {
case MessageCase.ViewingRoom: {
if (this.props.canPreview) {
title = _t("You're previewing %(roomName)s. Want to join it?",
{ roomName: this._roomName() });
{ roomName: this.roomName() });
} else {
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
{ roomName: this._roomName(true) });
{ roomName: this.roomName(true) });
}
primaryActionLabel = _t("Join the discussion");
primaryActionHandler = this.props.onJoinClick;
break;
}
case MessageCase.RoomNotFound: {
title = _t("%(roomName)s does not exist.", { roomName: this._roomName(true) });
title = _t("%(roomName)s does not exist.", { roomName: this.roomName(true) });
subTitle = _t("This room doesn't exist. Are you sure you're at the right place?");
break;
}
case MessageCase.OtherError: {
title = _t("%(roomName)s is not accessible at this time.", { roomName: this._roomName(true) });
title = _t("%(roomName)s is not accessible at this time.", { roomName: this.roomName(true) });
subTitle = [
_t("Try again later, or ask a room admin to check if you have access."),
_t(

View file

@ -31,8 +31,8 @@ import {
textSerialize,
unescapeMessage,
} from '../../../editor/serialize';
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer";
import ReplyThread from "../elements/ReplyThread";
import { findEditableEvent } from '../../../utils/EventUtils';
import SendHistoryManager from "../../../SendHistoryManager";
@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
}
public async sendMessage(): Promise<void> {
if (this.model.isEmpty) {
const model = this.model;
if (model.isEmpty) {
return;
}
// Replace emoticon at the end of the message
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
const caret = this.editorRef.current?.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
}
const replyToEvent = this.props.replyToEvent;
let shouldSend = true;
let content;
if (!containsEmote(this.model) && this.isSlashCommand()) {
if (!containsEmote(model) && this.isSlashCommand()) {
const [cmd, args, commandText] = this.getSlashCommand();
if (cmd) {
if (cmd.category === CommandCategories.messages) {
@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
}
}
if (isQuickReaction(this.model)) {
if (isQuickReaction(model)) {
shouldSend = false;
this.sendQuickReaction();
}
@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
const { roomId } = this.props.room;
if (!content) {
content = createMessageContent(
this.model,
model,
replyToEvent,
this.props.replyInThread,
this.props.permalinkCreator,
@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
}
this.sendHistoryManager.save(this.model, replyToEvent);
this.sendHistoryManager.save(model, replyToEvent);
// clear composer
this.model.reset([]);
model.reset([]);
this.editorRef.current?.clearUndoHistory();
this.editorRef.current?.focus();
this.clearStoredEditorState();

View file

@ -28,6 +28,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog";
import RoomUpgradeWarningDialog from "../dialogs/RoomUpgradeWarningDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
import { upgradeRoom } from "../../../utils/RoomUpgrade";
import { arrayHasDiff } from "../../../utils/arrays";
import { useLocalEcho } from "../../../hooks/useLocalEcho";
@ -207,27 +208,50 @@ const JoinRuleSettings = ({ room, promptUpgrade, onError, beforeChange, closeSet
} else if (preferredRestrictionVersion) {
// Block this action on a room upgrade otherwise it'd make their room unjoinable
const targetVersion = preferredRestrictionVersion;
Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
const modal = Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, {
roomId: room.roomId,
targetVersion,
description: _t("This upgrade will allow members of selected spaces " +
"access to this room without an invite."),
onFinished: async (resp) => {
if (!resp?.continue) return;
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
closeSettingsFn();
// switch to the new room in the background
dis.dispatch({
action: "view_room",
room_id: roomId,
});
// open new settings on this tab
dis.dispatch({
action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB,
});
},
});
const [resp] = await modal.finished;
if (!resp?.continue) return;
const userId = cli.getUserId();
const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId))
.some(roomId => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId));
if (unableToUpdateSomeParents) {
const modal = Modal.createTrackedDialog<[boolean]>('Parent relink warning', '', QuestionDialog, {
title: _t("Before you upgrade"),
description: (
<div>{ _t("This room is in some spaces youre not an admin of. " +
"In those spaces, the old room will still be shown, " +
"but people will be prompted to join the new one.") }</div>
),
hasCancelButton: true,
button: _t("Upgrade anyway"),
danger: true,
});
const [shouldUpgrade] = await modal.finished;
if (!shouldUpgrade) return;
}
const roomId = await upgradeRoom(room, targetVersion, resp.invite, true, true, true);
closeSettingsFn();
// switch to the new room in the background
dis.dispatch({
action: "view_room",
room_id: roomId,
});
// open new settings on this tab
dis.dispatch({
action: "open_room_settings",
initial_tab_id: ROOM_SECURITY_TAB,
});
return;
}

View file

@ -28,7 +28,6 @@ import { replaceableComponent } from "../../../../../utils/replaceableComponent"
import SettingsFlag from '../../../elements/SettingsFlag';
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
import AccessibleButton from "../../../elements/AccessibleButton";
import SpaceStore from "../../../../../stores/SpaceStore";
import GroupAvatar from "../../../avatars/GroupAvatar";
import dis from "../../../../../dispatcher/dispatcher";
import GroupActions from "../../../../../actions/GroupActions";
@ -145,7 +144,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
];
static COMMUNITIES_SETTINGS = [
// TODO: part of delabsing move the toggle here - https://github.com/vector-im/element-web/issues/18088
"showCommunitiesInsteadOfSpaces",
];
static KEYBINDINGS_SETTINGS = [
@ -286,9 +285,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
};
private renderGroup(settingIds: string[]): React.ReactNodeArray {
return settingIds.filter(SettingsStore.isEnabled).map(i => {
return <SettingsFlag key={i} name={i} level={SettingLevel.ACCOUNT} />;
private renderGroup(
settingIds: string[],
level = SettingLevel.ACCOUNT,
includeDisabled = false,
): React.ReactNodeArray {
if (!includeDisabled) {
settingIds = settingIds.filter(SettingsStore.isEnabled);
}
return settingIds.map(i => {
return <SettingsFlag key={i} name={i} level={level} />;
});
}
@ -334,10 +341,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
{ this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS) }
</div>
{ SpaceStore.spacesEnabled && <div className="mx_SettingsTab_section">
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Spaces") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS) }
</div> }
{ this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT, true) }
</div>
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("Communities") }</span>
@ -349,7 +356,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
<p>{ _t("If a community isn't shown you may not have permission to convert it.") }</p>
<CommunityMigrator onFinished={this.props.closeSettingsFn} />
</details>
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS) }
{ this.renderGroup(PreferencesUserSettingsTab.COMMUNITIES_SETTINGS, SettingLevel.DEVICE) }
</div>
<div className="mx_SettingsTab_section">

View file

@ -117,9 +117,7 @@ export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
"Your feedback will help inform the next versions."),
rageshakeLabel: "spaces-feedback",
rageshakeData: Object.fromEntries([
"feature_spaces.all_rooms",
"feature_spaces.space_member_dms",
"feature_spaces.space_dm_badges",
"Spaces.allRoomsInHome",
].map(k => [k, SettingsStore.getValue(k)])),
});
}}
@ -301,13 +299,13 @@ const SpaceCreateMenu = ({ onFinished }) => {
/>
<p>
{ _t("You can also create a Space from a <a>community</a>.", {}, {
{ _t("You can also make Spaces from <a>communities</a>.", {}, {
a: sub => <AccessibleButton kind="link" onClick={onCreateSpaceFromCommunityClick}>
{ sub }
</AccessibleButton>,
}) }
<br />
{ _t("To join an existing space you'll need an invite.") }
{ _t("To join a space you'll need an invite.") }
</p>
<SpaceFeedbackPrompt onClick={onFinished} />

View file

@ -151,12 +151,19 @@ const CreateSpaceButton = ({
}
const onNewClick = menuDisplayed ? closeMenu : () => {
// persist that the user has interacted with this, use it to dismiss the beta dot
localStorage.setItem("mx_seenSpaces", "1");
if (!isPanelCollapsed) setPanelCollapsed(true);
openMenu();
};
let betaDot: JSX.Element;
if (!localStorage.getItem("mx_seenSpaces") && !SpaceStore.instance.spacePanelSpaces.length) {
betaDot = <div className="mx_BetaDot" />;
}
return <li
className={classNames("mx_SpaceItem", {
className={classNames("mx_SpaceItem mx_SpaceItem_new", {
"collapsed": isPanelCollapsed,
})}
role="treeitem"
@ -169,6 +176,7 @@ const CreateSpaceButton = ({
onClick={onNewClick}
isNarrow={isPanelCollapsed}
/>
{ betaDot }
{ contextMenu }
</li>;

View file

@ -277,9 +277,13 @@ export default class CallView extends React.Component<IProps, IState> {
if (this.state.screensharing) {
isScreensharing = await this.props.call.setScreensharingEnabled(false);
} else {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
if (window.electron?.getDesktopCapturerSources) {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
isScreensharing = await this.props.call.setScreensharingEnabled(true, source);
} else {
isScreensharing = await this.props.call.setScreensharingEnabled(true);
}
}
this.setState({