Merge branch 'develop' into travis/msc2873

This commit is contained in:
Travis Ralston 2021-05-14 14:22:53 -06:00
commit 89933d779c
41 changed files with 913 additions and 663 deletions

View file

@ -20,7 +20,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { User } from "matrix-js-sdk/src/models/user";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { TagID } from '../../../stores/room-list/models';
import RoomAvatar from "./RoomAvatar";
import NotificationBadge from '../rooms/NotificationBadge';
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
@ -35,7 +34,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
room: Room;
avatarSize: number;
tag: TagID;
displayBadge?: boolean;
forceCount?: boolean;
oobData?: object;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext, useMemo, useState} from "react";
import React, {ReactNode, useContext, useMemo, useState} from "react";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client";
@ -37,6 +37,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@ -46,7 +47,10 @@ interface IProps extends IDialogProps {
const Entry = ({ room, checked, onChange }) => {
return <label className="mx_AddExistingToSpace_entry">
<RoomAvatar room={room} height={32} width={32} />
{ room?.isSpaceRoom()
? <RoomAvatar room={room} height={32} width={32} />
: <DecoratedRoomAvatar room={room} avatarSize={32} />
}
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
<StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null}
@ -58,14 +62,23 @@ const Entry = ({ room, checked, onChange }) => {
interface IAddExistingToSpaceProps {
space: Room;
selected: Set<Room>;
onChange(checked: boolean, room: Room): void;
footerPrompt?: ReactNode;
emptySelectionButton?: ReactNode;
onFinished(added: boolean): void;
}
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space, selected, onChange }) => {
export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
space,
footerPrompt,
emptySelectionButton,
onFinished,
}) => {
const cli = useContext(MatrixClientContext);
const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
@ -93,120 +106,6 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
return arr;
}, [[], [], []]);
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selected.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selected.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
const addRooms = async () => {
setError(null);
setProgress(0);
@ -269,20 +168,145 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div>
</span>;
} else {
let button = emptySelectionButton;
if (!button || selectedToAdd.size > 0) {
button = <AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
{ _t("Add") }
</AccessibleButton>;
}
footer = <>
<span>
<div>{ _t("Want to add a new room instead?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
{ footerPrompt }
</span>
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
{ _t("Add") }
</AccessibleButton>
{ button }
</>;
}
const onChange = !busy && !error ? (checked, room) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
} : null;
return <div className="mx_AddExistingToSpace">
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery}
autoComplete={true}
autoFocus={true}
/>
<AutoHideScrollbar className="mx_AddExistingToSpace_content" id="mx_AddExistingToSpace">
{ rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3>
{ rooms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpace_section mx_AddExistingToSpace_section_spaces">
<h3>{ _t("Spaces") }</h3>
<div className="mx_AddExistingToSpace_section_experimental">
<div>{ _t("Feeling experimental?") }</div>
<div>{ _t("You can add existing spaces to a space.") }</div>
</div>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={onChange ? (checked) => {
onChange(checked, space);
} : null}
/>;
}) }
</div>
) : null }
{ dms.length > 0 ? (
<div className="mx_AddExistingToSpace_section">
<h3>{ _t("Direct Messages") }</h3>
{ dms.map(room => {
return <Entry
key={room.roomId}
room={room}
checked={selectedToAdd.has(room)}
onChange={onChange ? (checked) => {
onChange(checked, room);
} : null}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length + dms.length < 1 ? <span className="mx_AddExistingToSpace_noResults">
{ _t("No results") }
</span> : undefined }
</AutoHideScrollbar>
<div className="mx_AddExistingToSpace_footer">
{ footer }
</div>
</div>;
};
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
const [selectedSpace, setSelectedSpace] = useState(space);
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
let spaceOptionSection;
if (existingSubspaces.length > 0) {
const options = [space, ...existingSubspaces].map((space) => {
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
});
return <div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
});
spaceOptionSection = (
<Dropdown
id="mx_SpaceSelectDropdown"
onOptionChange={(key: string) => {
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
}}
value={selectedSpace.roomId}
label={_t("Space selection")}
>
{ options }
</Dropdown>
);
} else {
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
</div>;
}
const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} />
<div>
<h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection }
</div>
</React.Fragment>;
return <BaseDialog
title={title}
className="mx_AddExistingToSpaceDialog"
@ -293,21 +317,16 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
<MatrixClientContext.Provider value={cli}>
<AddExistingToSpace
space={space}
selected={selectedToAdd}
onChange={!busy && !error ? (checked, room) => {
if (checked) {
selectedToAdd.add(room);
} else {
selectedToAdd.delete(room);
}
setSelectedToAdd(new Set(selectedToAdd));
} : null}
onFinished={onFinished}
footerPrompt={<>
<div>{ _t("Want to add a new room instead?") }</div>
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
{ _t("Create a new room") }
</AccessibleButton>
</>}
/>
</MatrixClientContext.Provider>
<div className="mx_AddExistingToSpaceDialog_footer">
{ footer }
</div>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>;
};

View file

@ -0,0 +1,73 @@
/*
Copyright 2019, 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { User } from "matrix-js-sdk/src/models/user";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import E2EIcon from "../rooms/E2EIcon";
import AccessibleButton from "../elements/AccessibleButton";
import BaseDialog from "./BaseDialog";
import { IDialogProps } from "./IDialogProps";
import { IDevice } from "../right_panel/UserInfo";
interface IProps extends IDialogProps {
user: User;
device: IDevice;
}
const UntrustedDeviceDialog: React.FC<IProps> = ({device, user, onFinished}) => {
let askToVerifyText;
let newSessionText;
if (MatrixClientPeg.get().getUserId() === user.userId) {
newSessionText = _t("You signed in to a new session without verifying it:");
askToVerifyText = _t("Verify your other session using one of the options below.");
} else {
newSessionText = _t("%(name)s (%(userId)s) signed in to a new session without verifying it:",
{name: user.displayName, userId: user.userId});
askToVerifyText = _t("Ask this user to verify their session, or manually verify it below.");
}
return <BaseDialog
onFinished={onFinished}
className="mx_UntrustedDeviceDialog"
title={<>
<E2EIcon status="warning" size={24} hideTooltip={true} />
{ _t("Not Trusted")}
</>}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
<p>{newSessionText}</p>
<p>{device.getDisplayName()} ({device.deviceId})</p>
<p>{askToVerifyText}</p>
</div>
<div className='mx_Dialog_buttons'>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("legacy")}>
{ _t("Manually Verify by Text") }
</AccessibleButton>
<AccessibleButton element="button" kind="secondary" onClick={() => onFinished("sas")}>
{ _t("Interactively verify by Emoji") }
</AccessibleButton>
<AccessibleButton kind="primary" onClick={() => onFinished(false)}>
{ _t("Done") }
</AccessibleButton>
</div>
</BaseDialog>;
};
export default UntrustedDeviceDialog;

View file

@ -207,6 +207,7 @@ export default class ImageView extends React.Component<IProps, IState> {
a.href = this.props.src;
a.download = this.props.name;
a.target = "_blank";
a.rel = "noreferrer noopener";
a.click();
};
@ -442,16 +443,16 @@ export default class ImageView extends React.Component<IProps, IState> {
<div className="mx_ImageView_panel">
{info}
<div className="mx_ImageView_toolbar">
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCCW"
title={_t("Rotate Left")}
onClick={ this.onRotateCounterClockwiseClick }>
</AccessibleTooltipButton>
<AccessibleTooltipButton
className="mx_ImageView_button mx_ImageView_button_rotateCW"
title={_t("Rotate Right")}
onClick={this.onRotateClockwiseClick}>
</AccessibleTooltipButton>
{zoomOutButton}
{zoomInButton}
<AccessibleTooltipButton

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,29 +14,68 @@ 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 classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Relations } from "matrix-js-sdk/src/models/relations";
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { isContentActionable } from '../../../utils/EventUtils';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import { aboveLeftOf, ContextMenu, useContextMenu } from "../../structures/ContextMenu";
import ReactionPicker from "../emojipicker/ReactionPicker";
import ReactionsRowButton from "./ReactionsRowButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
// The maximum number of reactions to initially show on a message.
const MAX_ITEMS_WHEN_LIMITED = 8;
@replaceableComponent("views.messages.ReactionsRow")
export default class ReactionsRow extends React.PureComponent {
static propTypes = {
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions: PropTypes.object,
const ReactButton = ({ mxEvent, reactions }: IProps) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
let contextMenu;
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}>
<ReactionPicker mxEvent={mxEvent} reactions={reactions} onFinished={closeMenu} />
</ContextMenu>;
}
constructor(props) {
super(props);
return <React.Fragment>
<ContextMenuTooltipButton
className={classNames("mx_ReactionsRow_addReactionButton", {
mx_ReactionsRow_addReactionButton_active: menuDisplayed,
})}
title={_t("Add reaction")}
onClick={openMenu}
isExpanded={menuDisplayed}
inputRef={button}
/>
{ contextMenu }
</React.Fragment>;
};
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The Relations model from the JS SDK for reactions to `mxEvent`
reactions?: Relations;
}
interface IState {
myReactions: MatrixEvent[];
showAll: boolean;
}
@replaceableComponent("views.messages.ReactionsRow")
export default class ReactionsRow extends React.PureComponent<IProps, IState> {
static contextType = MatrixClientContext;
constructor(props, context) {
super(props, context);
if (props.reactions) {
props.reactions.on("Relations.add", this.onReactionsChange);
@ -92,7 +131,7 @@ export default class ReactionsRow extends React.PureComponent {
if (!reactions) {
return null;
}
const userId = MatrixClientPeg.get().getUserId();
const userId = this.context.getUserId();
const myReactions = reactions.getAnnotationsBySender()[userId];
if (!myReactions) {
return null;
@ -114,7 +153,6 @@ export default class ReactionsRow extends React.PureComponent {
return null;
}
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
let items = reactions.getSortedAnnotationsByKey().map(([content, events]) => {
const count = events.size;
if (!count) {
@ -151,13 +189,21 @@ export default class ReactionsRow extends React.PureComponent {
</a>;
}
const cli = this.context;
let addReactionButton;
if (cli.getRoom(mxEvent.getRoomId()).currentState.maySendEvent(EventType.Reaction, cli.getUserId())) {
addReactionButton = <ReactButton mxEvent={mxEvent} reactions={reactions} />;
}
return <div
className="mx_ReactionsRow"
role="toolbar"
aria-label={_t("Reactions")}
>
{items}
{showAllButton}
{ items }
{ showAllButton }
{ addReactionButton }
</div>;
}
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,49 +14,54 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import dis from "../../../dispatcher/dispatcher";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip";
import AccessibleButton from "../elements/AccessibleButton";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The reaction content / key / emoji
content: string;
// The count of votes for this key
count: number;
// A Set of Matrix reaction events for this key
reactionEvents: Set<MatrixEvent>;
// A possible Matrix event if the current user has voted for this type
myReactionEvent?: MatrixEvent;
}
interface IState {
tooltipRendered: boolean;
tooltipVisible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButton")
export default class ReactionsRowButton extends React.PureComponent {
static propTypes = {
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The reaction content / key / emoji
content: PropTypes.string.isRequired,
// The count of votes for this key
count: PropTypes.number.isRequired,
// A Set of Martix reaction events for this key
reactionEvents: PropTypes.object.isRequired,
// A possible Matrix event if the current user has voted for this type
myReactionEvent: PropTypes.object,
}
export default class ReactionsRowButton extends React.PureComponent<IProps, IState> {
static contextType = MatrixClientContext;
constructor(props) {
super(props);
state = {
tooltipRendered: false,
tooltipVisible: false,
};
this.state = {
tooltipVisible: false,
};
}
onClick = (ev) => {
onClick = () => {
const { mxEvent, myReactionEvent, content } = this.props;
if (myReactionEvent) {
MatrixClientPeg.get().redactEvent(
this.context.redactEvent(
mxEvent.getRoomId(),
myReactionEvent.getId(),
);
} else {
MatrixClientPeg.get().sendEvent(mxEvent.getRoomId(), "m.reaction", {
this.context.sendEvent(mxEvent.getRoomId(), "m.reaction", {
"m.relates_to": {
"rel_type": "m.annotation",
"event_id": mxEvent.getId(),
@ -83,8 +88,6 @@ export default class ReactionsRowButton extends React.PureComponent {
}
render() {
const ReactionsRowButtonTooltip =
sdk.getComponent('messages.ReactionsRowButtonTooltip');
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;
const classes = classNames({
@ -102,7 +105,7 @@ export default class ReactionsRowButton extends React.PureComponent {
/>;
}
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
const room = this.context.getRoom(mxEvent.getRoomId());
let label;
if (room) {
const senders = [];
@ -130,7 +133,6 @@ export default class ReactionsRowButton extends React.PureComponent {
);
}
const isPeeking = room.getMyMembership() !== "join";
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return <AccessibleButton
className={classes}
aria-label={label}

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,33 +14,34 @@ 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 { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
import { unicodeToShortcode } from '../../../HtmlUtils';
import { _t } from '../../../languageHandler';
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Tooltip from "../elements/Tooltip";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
interface IProps {
// The event we're displaying reactions for
mxEvent: MatrixEvent;
// The reaction content / key / emoji
content: string;
// A Set of Matrix reaction events for this key
reactionEvents: Set<MatrixEvent>;
visible: boolean;
}
@replaceableComponent("views.messages.ReactionsRowButtonTooltip")
export default class ReactionsRowButtonTooltip extends React.PureComponent {
static propTypes = {
// The event we're displaying reactions for
mxEvent: PropTypes.object.isRequired,
// The reaction content / key / emoji
content: PropTypes.string.isRequired,
// A Set of Martix reaction events for this key
reactionEvents: PropTypes.object.isRequired,
visible: PropTypes.bool.isRequired,
}
export default class ReactionsRowButtonTooltip extends React.PureComponent<IProps> {
static contextType = MatrixClientContext;
render() {
const Tooltip = sdk.getComponent('elements.Tooltip');
const { content, reactionEvents, mxEvent, visible } = this.props;
const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId());
const room = this.context.getRoom(mxEvent.getRoomId());
let tooltipLabel;
if (room) {
const senders = [];

View file

@ -67,7 +67,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import {mediaFromMxc} from "../../../customisations/Media";
interface IDevice {
export interface IDevice {
deviceId: string;
ambiguous?: boolean;
getDisplayName(): string;

View file

@ -35,6 +35,7 @@ import {Action} from "../../../dispatcher/actions";
import CountlyAnalytics from "../../../CountlyAnalytics";
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import SendHistoryManager from '../../../SendHistoryManager';
import Modal from '../../../Modal';
function _isReply(mxEvent) {
@ -122,6 +123,7 @@ export default class EditMessageComposer extends React.Component {
saveDisabled: true,
};
this._createEditorModel();
window.addEventListener("beforeunload", this._saveStoredEditorState);
}
_setEditorRef = ref => {
@ -175,11 +177,55 @@ export default class EditMessageComposer extends React.Component {
}
}
get _editorRoomKey() {
return `mx_edit_room_${this._getRoom().roomId}`;
}
get _editorStateKey() {
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
}
_cancelEdit = () => {
this._clearStoredEditorState();
dis.dispatch({action: "edit_event", event: null});
dis.fire(Action.FocusComposer);
}
get _shouldSaveStoredEditorState() {
return localStorage.getItem(this._editorRoomKey) !== null;
}
_restoreStoredEditorState(partCreator) {
const json = localStorage.getItem(this._editorStateKey);
if (json) {
try {
const {parts: serializedParts} = JSON.parse(json);
const parts = serializedParts.map(p => partCreator.deserializePart(p));
return parts;
} catch (e) {
console.error("Error parsing editing state: ", e);
}
}
}
_clearStoredEditorState() {
localStorage.removeItem(this._editorRoomKey);
localStorage.removeItem(this._editorStateKey);
}
_clearPreviousEdit() {
if (localStorage.getItem(this._editorRoomKey)) {
localStorage.removeItem(`mx_edit_state_${localStorage.getItem(this._editorRoomKey)}`);
}
}
_saveStoredEditorState() {
const item = SendHistoryManager.createItem(this.model);
this._clearPreviousEdit();
localStorage.setItem(this._editorRoomKey, this.props.editState.getEvent().getId());
localStorage.setItem(this._editorStateKey, JSON.stringify(item));
}
_isSlashCommand() {
const parts = this.model.parts;
const firstPart = parts[0];
@ -266,6 +312,7 @@ export default class EditMessageComposer extends React.Component {
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
@ -311,6 +358,7 @@ export default class EditMessageComposer extends React.Component {
if (shouldSend) {
this._cancelPreviousPendingEdit();
const prom = this.context.sendMessage(roomId, editContent);
this._clearStoredEditorState();
dis.dispatch({action: "message_sent"});
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
}
@ -346,6 +394,10 @@ export default class EditMessageComposer extends React.Component {
// then when mounting the editor again with the same editor state,
// it will set the cursor at the end.
this.props.editState.setEditorState(caret, parts);
window.removeEventListener("beforeunload", this._saveStoredEditorState);
if (this._shouldSaveStoredEditorState) {
this._saveStoredEditorState();
}
}
_createEditorModel() {
@ -358,10 +410,11 @@ export default class EditMessageComposer extends React.Component {
// restore serialized parts from the state
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
} else {
// otherwise, parse the body of the event
parts = parseEvent(editState.getEvent(), partCreator);
//otherwise, either restore serialized parts from localStorage or parse the body of the event
parts = this._restoreStoredEditorState(partCreator) || parseEvent(editState.getEvent(), partCreator);
}
this.model = new EditorModel(parts, partCreator);
this._saveStoredEditorState();
}
_getInitialCaretPosition() {

View file

@ -23,11 +23,9 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition } from "react-transition-group";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Toolbar from "../../../accessibility/Toolbar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
}
@ -84,8 +82,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
public render(): React.ReactElement {
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
const roomTags = RoomListStore.instance.getTagsForRoom(r);
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return (
<RovingAccessibleTooltipButton
className="mx_RoomBreadcrumbs_crumb"
@ -98,7 +94,6 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
<DecoratedRoomAvatar
room={r}
avatarSize={32}
tag={roomTag}
displayBadge={true}
forceCount={true}
/>

View file

@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
import E2EIcon from './E2EIcon';
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {DefaultTagID} from "../../../stores/room-list/models";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
@ -177,7 +176,6 @@ export default class RoomHeader extends React.Component {
roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={32}
tag={DefaultTagID.Untagged} // to apply room publicity badging
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>;

View file

@ -576,7 +576,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={32}
tag={this.props.tag}
displayBadge={this.props.isMinimized}
oobData={({avatarUrl: roomProfile.avatarMxc})}
/>;

View file

@ -232,7 +232,7 @@ export default class EventIndexPanel extends React.Component<{}, IState> {
<p>
{this.state.enabling
? <InlineSpinner />
: _t("Message search initilisation failed")
: _t("Message search initialisation failed")
}
</p>
{EventIndexPeg.error && (

View file

@ -23,6 +23,7 @@ import {copyPlaintext} from "../../../utils/strings";
import {sleep} from "../../../utils/promise";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import {showRoomInviteDialog} from "../../../RoomInvite";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
interface IProps {
space: Room;
@ -50,7 +51,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
<h3>{ _t("Share invite link") }</h3>
<span>{ copiedText }</span>
</AccessibleButton>
<AccessibleButton
{ space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
className="mx_SpacePublicShare_inviteButton"
onClick={() => {
showRoomInviteDialog(space.roomId);
@ -59,7 +60,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
>
<h3>{ _t("Invite people") }</h3>
<span>{ _t("Invite with email or username") }</span>
</AccessibleButton>
</AccessibleButton> : null }
</div>;
};

View file

@ -209,7 +209,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const userId = this.context.getUserId();
let inviteOption;
if (this.props.space.canInvite(userId)) {
if (this.props.space.getJoinRule() === "public" || this.props.space.canInvite(userId)) {
inviteOption = (
<IconizedContextMenuOption
className="mx_SpacePanel_contextMenu_inviteButton"

View file

@ -57,8 +57,8 @@ export default class PlaybackWaveform extends React.PureComponent<IProps, IState
};
private onTimeUpdate = (time: number[]) => {
// Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar.
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1));
// Track percentages to a general precision to avoid over-waking the component.
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
this.setState({progress});
};