Merge branch 'develop' into gsouquet/compact-composer-18533

This commit is contained in:
Germain Souquet 2021-09-03 09:20:38 +01:00
commit 6d80976eae
43 changed files with 683 additions and 391 deletions

View file

@ -43,11 +43,6 @@ import QuestionDialog from "../dialogs/QuestionDialog";
import { ActionPayload } from "../../../dispatcher/payloads";
import AccessibleButton from '../elements/AccessibleButton';
function eventIsReply(mxEvent: MatrixEvent): boolean {
const relatesTo = mxEvent.getContent()["m.relates_to"];
return !!(relatesTo && relatesTo["m.in_reply_to"]);
}
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body;
if (!html) {
@ -72,7 +67,7 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
if (isEmote) {
model = stripEmoteCommand(model);
}
const isReply = eventIsReply(editedEvent);
const isReply = !!editedEvent.replyEventId;
let plainPrefix = "";
let htmlPrefix = "";

View file

@ -243,6 +243,7 @@ interface IProps {
// opaque readreceipt info for each userId; used by ReadReceiptMarker
// to manage its animations. Should be an empty object when the room
// first loads
// TODO: Proper typing for RR info
readReceiptMap?: any;
// A function which is used to check if the parent panel is being

View file

@ -14,11 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import classNames from 'classnames';
export default (props) => {
interface IProps {
numUnreadMessages: number;
highlight: boolean;
onScrollToBottomClick: (e: React.MouseEvent) => void;
}
const JumpToBottomButton: React.FC<IProps> = (props) => {
const className = classNames({
'mx_JumpToBottomButton': true,
'mx_JumpToBottomButton_highlight': props.highlight,
@ -36,3 +43,5 @@ export default (props) => {
{ badge }
</div>);
};
export default JumpToBottomButton;

View file

@ -192,6 +192,7 @@ interface IProps {
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
replyToEvent?: MatrixEvent;
replyInThread?: boolean;
showReplyPreview?: boolean;
e2eStatus?: E2EStatus;
compact?: boolean;
@ -217,6 +218,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private ref: React.RefObject<HTMLDivElement> = createRef();
static defaultProps = {
replyInThread: false,
showReplyPreview: true,
compact: false,
};
@ -498,6 +500,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
room={this.props.room}
placeholder={this.renderPlaceholderText()}
permalinkCreator={this.props.permalinkCreator}
replyInThread={this.props.replyInThread}
replyToEvent={this.props.replyToEvent}
onChange={this.onChange}
disabled={this.state.haveRecording}

View file

@ -15,62 +15,75 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import React, { createRef, RefObject } from 'react';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { _t } from '../../../languageHandler';
import { formatDate } from '../../../DateUtils';
import NodeAnimator from "../../../NodeAnimator";
import * as sdk from "../../../index";
import { toPx } from "../../../utils/units";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import MemberAvatar from '../avatars/MemberAvatar';
interface IProps {
// the RoomMember to show the RR for
member?: RoomMember;
// userId to fallback the avatar to
// if the member hasn't been loaded yet
fallbackUserId: string;
// number of pixels to offset the avatar from the right of its parent;
// typically a negative value.
leftOffset?: number;
// true to hide the avatar (it will still be animated)
hidden?: boolean;
// don't animate this RR into position
suppressAnimation?: boolean;
// an opaque object for storing information about this user's RR in
// this room
// TODO: proper typing for RR info
readReceiptInfo: any;
// A function which is used to check if the parent panel is being
// unmounted, to avoid unnecessary work. Should return true if we
// are being unmounted.
checkUnmounting?: () => boolean;
// callback for clicks on this RR
onClick?: (e: React.MouseEvent) => void;
// Timestamp when the receipt was read
timestamp?: number;
// True to show twelve hour format, false otherwise
showTwelveHour?: boolean;
}
interface IState {
suppressDisplay: boolean;
startStyles?: IReadReceiptMarkerStyle[];
}
interface IReadReceiptMarkerStyle {
top: number;
left: number;
}
@replaceableComponent("views.rooms.ReadReceiptMarker")
export default class ReadReceiptMarker extends React.PureComponent {
static propTypes = {
// the RoomMember to show the RR for
member: PropTypes.object,
// userId to fallback the avatar to
// if the member hasn't been loaded yet
fallbackUserId: PropTypes.string.isRequired,
// number of pixels to offset the avatar from the right of its parent;
// typically a negative value.
leftOffset: PropTypes.number,
// true to hide the avatar (it will still be animated)
hidden: PropTypes.bool,
// don't animate this RR into position
suppressAnimation: PropTypes.bool,
// an opaque object for storing information about this user's RR in
// this room
readReceiptInfo: PropTypes.object,
// A function which is used to check if the parent panel is being
// unmounted, to avoid unnecessary work. Should return true if we
// are being unmounted.
checkUnmounting: PropTypes.func,
// callback for clicks on this RR
onClick: PropTypes.func,
// Timestamp when the receipt was read
timestamp: PropTypes.number,
// True to show twelve hour format, false otherwise
showTwelveHour: PropTypes.bool,
};
export default class ReadReceiptMarker extends React.PureComponent<IProps, IState> {
private avatar: React.RefObject<HTMLDivElement | HTMLImageElement | HTMLSpanElement> = createRef();
static defaultProps = {
leftOffset: 0,
};
constructor(props) {
constructor(props: IProps) {
super(props);
this._avatar = createRef();
this.state = {
// if we are going to animate the RR, we don't show it on first render,
// and instead just add a placeholder to the DOM; once we've been
@ -80,7 +93,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
};
}
componentWillUnmount() {
public componentWillUnmount(): void {
// before we remove the rr, store its location in the map, so that if
// it reappears, it can be animated from the right place.
const rrInfo = this.props.readReceiptInfo;
@ -95,29 +108,29 @@ export default class ReadReceiptMarker extends React.PureComponent {
return;
}
const avatarNode = this._avatar.current;
const avatarNode = this.avatar.current;
rrInfo.top = avatarNode.offsetTop;
rrInfo.left = avatarNode.offsetLeft;
rrInfo.parent = avatarNode.offsetParent;
}
componentDidMount() {
public componentDidMount(): void {
if (!this.state.suppressDisplay) {
// we've already done our display - nothing more to do.
return;
}
this._animateMarker();
this.animateMarker();
}
componentDidUpdate(prevProps) {
public componentDidUpdate(prevProps: IProps): void {
const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset;
const visibilityChanged = prevProps.hidden !== this.props.hidden;
if (differentLeftOffset || visibilityChanged) {
this._animateMarker();
this.animateMarker();
}
}
_animateMarker() {
private animateMarker(): void {
// treat new RRs as though they were off the top of the screen
let oldTop = -15;
@ -126,7 +139,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top;
}
const newElement = this._avatar.current;
const newElement = this.avatar.current;
let startTopOffset;
if (!newElement.offsetParent) {
// this seems to happen sometimes for reasons I don't understand
@ -156,10 +169,9 @@ export default class ReadReceiptMarker extends React.PureComponent {
});
}
render() {
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
public render(): JSX.Element {
if (this.state.suppressDisplay) {
return <div ref={this._avatar} />;
return <div ref={this.avatar as RefObject<HTMLDivElement>} />;
}
const style = {
@ -198,7 +210,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
style={style}
title={title}
onClick={this.props.onClick}
inputRef={this._avatar}
inputRef={this.avatar as RefObject<HTMLImageElement>}
/>
</NodeAnimator>
);

View file

@ -14,41 +14,38 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import React from 'react';
import { _t } from '../../../languageHandler';
import PropTypes from 'prop-types';
import { Room } from 'matrix-js-sdk/src';
import classNames from 'classnames';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import { roomShape } from './RoomDetailRow';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomDetailRow from "./RoomDetailRow";
interface IProps {
rooms?: Room[];
className?: string;
}
@replaceableComponent("views.rooms.RoomDetailList")
export default class RoomDetailList extends React.Component {
static propTypes = {
rooms: PropTypes.arrayOf(roomShape),
className: PropTypes.string,
};
getRows() {
export default class RoomDetailList extends React.Component<IProps> {
private getRows(): JSX.Element[] {
if (!this.props.rooms) return [];
const RoomDetailRow = sdk.getComponent('rooms.RoomDetailRow');
return this.props.rooms.map((room, index) => {
return <RoomDetailRow key={index} room={room} onClick={this.onDetailsClick} />;
});
}
onDetailsClick = (ev, room) => {
private onDetailsClick = (ev: React.MouseEvent, room: Room): void => {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
room_alias: room.canonicalAlias || (room.aliases || [])[0],
room_alias: room.getCanonicalAlias() || (room.getAltAliases() || [])[0],
});
};
render() {
public render(): JSX.Element {
const rows = this.getRows();
let rooms;
if (rows.length === 0) {

View file

@ -195,7 +195,7 @@ export default class RoomHeader extends React.Component<IProps> {
videoCallButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev) => ev.shiftKey ?
onClick={(ev: React.MouseEvent<Element>) => ev.shiftKey ?
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
title={_t("Video call")} />;
}

View file

@ -1,5 +1,5 @@
/*
Copyright 2018-2020 New Vector Ltd
Copyright 2018-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.
@ -15,41 +15,43 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import { RoomState } from 'matrix-js-sdk/src/models/room-state';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomUpgradeDialog from '../dialogs/RoomUpgradeDialog';
import AccessibleButton from '../elements/AccessibleButton';
interface IProps {
room: Room;
}
interface IState {
upgraded?: boolean;
}
@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
export default class RoomUpgradeWarningBar extends React.PureComponent {
static propTypes = {
room: PropTypes.object.isRequired,
recommendation: PropTypes.object.isRequired,
};
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
export default class RoomUpgradeWarningBar extends React.PureComponent<IProps, IState> {
public componentDidMount(): void {
const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", "");
this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room });
MatrixClientPeg.get().on("RoomState.events", this._onStateEvents);
MatrixClientPeg.get().on("RoomState.events", this.onStateEvents);
}
componentWillUnmount() {
public componentWillUnmount(): void {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onStateEvents);
cli.removeListener("RoomState.events", this.onStateEvents);
}
}
_onStateEvents = (event, state) => {
private onStateEvents = (event: MatrixEvent, state: RoomState): void => {
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
return;
}
@ -60,14 +62,11 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room });
};
onUpgradeClick = () => {
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
private onUpgradeClick = (): void => {
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room: this.props.room });
};
render() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
public render(): JSX.Element {
let doUpgradeWarnings = (
<div>
<div className="mx_RoomUpgradeWarningBar_body">

View file

@ -57,15 +57,16 @@ import { ActionPayload } from "../../../dispatcher/payloads";
function addReplyToMessageContent(
content: IContent,
repliedToEvent: MatrixEvent,
replyToEvent: MatrixEvent,
replyInThread: boolean,
permalinkCreator: RoomPermalinkCreator,
): void {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
const replyContent = ReplyThread.makeReplyMixIn(replyToEvent, replyInThread);
Object.assign(content, replyContent);
// Part of Replies fallback support - prepend the text we're sending
// with the text we're replying to
const nestedReply = ReplyThread.getNestedReplyText(repliedToEvent, permalinkCreator);
const nestedReply = ReplyThread.getNestedReplyText(replyToEvent, permalinkCreator);
if (nestedReply) {
if (content.formatted_body) {
content.formatted_body = nestedReply.html + content.formatted_body;
@ -77,8 +78,9 @@ function addReplyToMessageContent(
// exported for tests
export function createMessageContent(
model: EditorModel,
permalinkCreator: RoomPermalinkCreator,
replyToEvent: MatrixEvent,
replyInThread: boolean,
permalinkCreator: RoomPermalinkCreator,
): IContent {
const isEmote = containsEmote(model);
if (isEmote) {
@ -101,7 +103,7 @@ export function createMessageContent(
}
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, permalinkCreator);
addReplyToMessageContent(content, replyToEvent, replyInThread, permalinkCreator);
}
return content;
@ -129,6 +131,7 @@ interface IProps {
room: Room;
placeholder?: string;
permalinkCreator: RoomPermalinkCreator;
replyInThread?: boolean;
replyToEvent?: MatrixEvent;
disabled?: boolean;
onChange?(model: EditorModel): void;
@ -357,7 +360,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
if (cmd.category === CommandCategories.messages) {
content = await this.runSlashCommand(cmd, args);
if (replyToEvent) {
addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator);
addReplyToMessageContent(
content,
replyToEvent,
this.props.replyInThread,
this.props.permalinkCreator,
);
}
} else {
this.runSlashCommand(cmd, args);
@ -400,7 +408,12 @@ export default class SendMessageComposer extends React.Component<IProps> {
const startTime = CountlyAnalytics.getTimestamp();
const { roomId } = this.props.room;
if (!content) {
content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent);
content = createMessageContent(
this.model,
replyToEvent,
this.props.replyInThread,
this.props.permalinkCreator,
);
}
// don't bother sending an empty message
if (!content.body.trim()) return;

View file

@ -1,5 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2016-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.
@ -15,23 +15,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
title?: string;
// `src` to an image. Optional.
icon?: string;
}
/*
* A stripped-down room header used for things like the user settings
* and room directory.
*/
@replaceableComponent("views.rooms.SimpleRoomHeader")
export default class SimpleRoomHeader extends React.Component {
static propTypes = {
title: PropTypes.string,
// `src` to an image. Optional.
icon: PropTypes.string,
};
render() {
export default class SimpleRoomHeader extends React.PureComponent<IProps> {
public render(): JSX.Element {
let icon;
if (this.props.icon) {
icon = <img

View file

@ -1,7 +1,5 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 New Vector Ltd
Copyright 2016 - 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.
@ -17,19 +15,18 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("views.rooms.TopUnreadMessagesBar")
export default class TopUnreadMessagesBar extends React.Component {
static propTypes = {
onScrollUpClick: PropTypes.func,
onCloseClick: PropTypes.func,
};
interface IProps {
onScrollUpClick?: (e: React.MouseEvent) => void;
onCloseClick?: (e: React.MouseEvent) => void;
}
render() {
@replaceableComponent("views.rooms.TopUnreadMessagesBar")
export default class TopUnreadMessagesBar extends React.PureComponent<IProps> {
public render(): JSX.Element {
return (
<div className="mx_TopUnreadMessagesBar">
<AccessibleButton

View file

@ -179,7 +179,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
try {
// stop any noises which might be happening
await PlaybackManager.instance.playOnly(null);
await PlaybackManager.instance.pauseAllExcept(null);
const recorder = VoiceRecordingStore.instance.startRecording();
await recorder.start();