Merge branch 'develop' into gsouquet/message-bubbles-4635

This commit is contained in:
Germain Souquet 2021-07-20 10:16:21 +02:00
commit 06c9a060b4
171 changed files with 3649 additions and 2694 deletions

View file

@ -55,7 +55,7 @@ interface IState {
export default class Autocomplete extends React.PureComponent<IProps, IState> {
autocompleter: Autocompleter;
queryRequested: string;
debounceCompletionsRequest: NodeJS.Timeout;
debounceCompletionsRequest: number;
private containerRef = createRef<HTMLDivElement>();
constructor(props) {

View file

@ -27,7 +27,6 @@ import { _t } from '../../../languageHandler';
import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/Layout";
import { formatTime } from "../../../DateUtils";
import { MatrixClientPeg } from '../../../MatrixClientPeg';
@ -54,6 +53,7 @@ import TooltipButton from '../elements/TooltipButton';
import ReadReceiptMarker from "./ReadReceiptMarker";
import MessageActionBar from "../messages/MessageActionBar";
import ReactionsRow from '../messages/ReactionsRow';
import { getEventDisplayInfo } from '../../../utils/EventUtils';
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
@ -190,8 +190,6 @@ export interface IReadReceiptProps {
export enum TileShape {
Notif = "notif",
FileGrid = "file_grid",
Reply = "reply",
ReplyPreview = "reply_preview",
Pinned = "pinned",
}
@ -323,7 +321,7 @@ export default class EventTile extends React.Component<IProps, IState> {
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
private tile = React.createRef();
private replyThread = React.createRef();
private replyThread = React.createRef<ReplyThread>();
public readonly ref = createRef<HTMLElement>();
@ -853,35 +851,9 @@ export default class EventTile extends React.Component<IProps, IState> {
};
render() {
//console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
const msgtype = this.props.mxEvent.getContent().msgtype;
const { tileHandler, isBubbleMessage, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent);
const content = this.props.mxEvent.getContent();
const msgtype = content.msgtype;
const eventType = this.props.mxEvent.getType();
let tileHandler = getHandlerTile(this.props.mxEvent);
// Info messages are basically information about commands processed on a room
let isBubbleMessage = eventType.startsWith("m.key.verification") ||
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
(eventType === EventType.RoomCreate) ||
(eventType === EventType.RoomEncryption) ||
(tileHandler === "messages.MJitsiWidgetEvent");
let isInfoMessage = (
!isBubbleMessage && eventType !== EventType.RoomMessage &&
eventType !== EventType.Sticker && eventType !== EventType.RoomCreate
);
// If we're showing hidden events in the timeline, we should use the
// source tile when there's no regular tile for an event and also for
// replace relations (which otherwise would display as a confusing
// duplicate of the thing they are replacing).
if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) {
tileHandler = "messages.ViewSourceEvent";
isBubbleMessage = false;
// Reuse info message avatar and sender profile styling
isInfoMessage = true;
}
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!tileHandler) {
@ -985,11 +957,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
if (needsSenderProfile && this.props.hideSender !== true) {
if (
!this.props.tileShape
|| this.props.tileShape === TileShape.Reply
|| this.props.tileShape === TileShape.ReplyPreview
) {
if (!this.props.tileShape) {
sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent}
enableFlair={this.props.enableFlair}
@ -1143,44 +1111,6 @@ export default class EventTile extends React.Component<IProps, IState> {
]);
}
case TileShape.Reply:
case TileShape.ReplyPreview: {
let thread;
if (this.props.tileShape === TileShape.ReplyPreview) {
thread = ReplyThread.makeThread(
this.props.mxEvent,
this.props.onHeightChanged,
this.props.permalinkCreator,
this.replyThread,
null,
this.props.alwaysShowTimestamps || this.state.hover,
);
}
return React.createElement(this.props.as || "li", {
"className": classes,
"aria-live": ariaLive,
"aria-atomic": true,
"data-scroll-tokens": scrollToken,
}, [
ircTimestamp,
avatar,
sender,
ircPadlock,
<div className="mx_EventTile_reply" key="mx_EventTile_reply">
{ groupTimestamp }
{ groupPadlock }
{ thread }
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onHeightChanged={this.props.onHeightChanged}
replacingEventId={this.props.replacingEventId}
showUrlPreview={false}
/>
</div>,
]);
}
default: {
const thread = ReplyThread.makeThread(
this.props.mxEvent,
@ -1207,10 +1137,10 @@ export default class EventTile extends React.Component<IProps, IState> {
"data-has-reply": !!thread,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [
ircTimestamp,
sender,
ircPadlock,
}, <>
{ ircTimestamp }
{ sender }
{ ircPadlock }
<div className="mx_EventTile_line" key="mx_EventTile_line">
{ groupTimestamp }
{ groupPadlock }
@ -1245,7 +1175,7 @@ function isMessageEvent(ev) {
return (messageTypes.includes(ev.getType()));
}
export function haveTileForEvent(e) {
export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) {
// Only messages have a tile (black-rectangle) if redacted
if (e.isRedacted() && !isMessageEvent(e)) return false;
@ -1255,7 +1185,7 @@ export function haveTileForEvent(e) {
const handler = getHandlerTile(e);
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
return hasText(e);
return hasText(e, showHiddenEvents);
} else if (handler === 'messages.RoomCreate') {
return Boolean(e.getContent()['predecessor']);
} else {

View file

@ -40,10 +40,12 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(link => {
return cli.getUrlPreview(link, ts).then(preview => [link, preview], error => {
return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => {
try {
return [link, await cli.getUrlPreview(link, ts)];
} catch (error) {
console.error("Failed to get URL preview: " + error);
});
}
})).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>;
}, [links, ts], []);

View file

@ -43,6 +43,7 @@ import EntityTile from "./EntityTile";
import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
import { throttle } from 'lodash';
import SpaceStore from "../../../stores/SpaceStore";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@ -509,7 +510,7 @@ export default class MemberList extends React.Component<IProps, IState> {
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat && chat.roomId === this.props.roomId) {
inviteButtonText = _t("Invite to this community");
} else if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
} else if (SpaceStore.spacesEnabled && room.isSpaceRoom()) {
inviteButtonText = _t("Invite to this space");
}
@ -549,7 +550,7 @@ export default class MemberList extends React.Component<IProps, IState> {
let previousPhase = RightPanelPhases.RoomSummary;
// We have no previousPhase for when viewing a MemberList from a Space
let scopeHeader;
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) {
previousPhase = undefined;
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={room} height={32} width={32} />

View file

@ -16,15 +16,13 @@ limitations under the License.
import React from 'react';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import PropTypes from "prop-types";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { TileShape } from "./EventTile";
import ReplyTile from './ReplyTile';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventSubscription } from 'fbemitter';
function cancelQuoting() {
dis.dispatch({
@ -33,47 +31,50 @@ function cancelQuoting() {
});
}
interface IProps {
permalinkCreator: RoomPermalinkCreator;
}
interface IState {
event: MatrixEvent;
}
@replaceableComponent("views.rooms.ReplyPreview")
export default class ReplyPreview extends React.Component {
static propTypes = {
permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
};
export default class ReplyPreview extends React.Component<IProps, IState> {
private unmounted = false;
private readonly roomStoreToken: EventSubscription;
constructor(props) {
super(props);
this.unmounted = false;
this.state = {
event: RoomViewStore.getQuotingEvent(),
};
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
}
componentWillUnmount() {
this.unmounted = true;
// Remove RoomStore listener
if (this._roomStoreToken) {
this._roomStoreToken.remove();
if (this.roomStoreToken) {
this.roomStoreToken.remove();
}
}
_onRoomViewStoreUpdate() {
private onRoomViewStoreUpdate = (): void => {
if (this.unmounted) return;
const event = RoomViewStore.getQuotingEvent();
if (this.state.event !== event) {
this.setState({ event });
}
}
};
render() {
if (!this.state.event) return null;
const EventTile = sdk.getComponent('rooms.EventTile');
return <div className="mx_ReplyPreview">
<div className="mx_ReplyPreview_section">
<div className="mx_ReplyPreview_header mx_ReplyPreview_title">
@ -89,15 +90,12 @@ export default class ReplyPreview extends React.Component {
/>
</div>
<div className="mx_ReplyPreview_clear" />
<EventTile
alwaysShowTimestamps={true}
tileShape={TileShape.ReplyPreview}
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
as="div"
/>
<div className="mx_ReplyPreview_tile">
<ReplyTile
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator}
/>
</div>
</div>
</div>;
}

View file

@ -0,0 +1,155 @@
/*
Copyright 2020-2021 Tulir Asokan <tulir@maunium.net>
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 classNames from 'classnames';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import SenderProfile from "../messages/SenderProfile";
import MImageReplyBody from "../messages/MImageReplyBody";
import * as sdk from '../../../index';
import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event';
import { replaceableComponent } from '../../../utils/replaceableComponent';
import { getEventDisplayInfo } from '../../../utils/EventUtils';
import MFileBody from "../messages/MFileBody";
interface IProps {
mxEvent: MatrixEvent;
permalinkCreator?: RoomPermalinkCreator;
highlights?: string[];
highlightLink?: string;
onHeightChanged?(): void;
}
@replaceableComponent("views.rooms.ReplyTile")
export default class ReplyTile extends React.PureComponent<IProps> {
static defaultProps = {
onHeightChanged: () => {},
};
componentDidMount() {
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
this.props.mxEvent.on("Event.beforeRedaction", this.onEventRequiresUpdate);
this.props.mxEvent.on("Event.replaced", this.onEventRequiresUpdate);
}
componentWillUnmount() {
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
this.props.mxEvent.removeListener("Event.beforeRedaction", this.onEventRequiresUpdate);
this.props.mxEvent.removeListener("Event.replaced", this.onEventRequiresUpdate);
}
private onDecrypted = (): void => {
this.forceUpdate();
if (this.props.onHeightChanged) {
this.props.onHeightChanged();
}
};
private onEventRequiresUpdate = (): void => {
// Force update when necessary - redactions and edits
this.forceUpdate();
};
private onClick = (e: React.MouseEvent): void => {
// This allows the permalink to be opened in a new tab/window or copied as
// matrix.to, but also for it to enable routing within Riot when clicked.
e.preventDefault();
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
};
render() {
const mxEvent = this.props.mxEvent;
const msgType = mxEvent.getContent().msgtype;
const evType = mxEvent.getType() as EventType;
const { tileHandler, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!tileHandler) {
const { mxEvent } = this.props;
console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
return <div className="mx_ReplyTile mx_ReplyTile_info mx_MNoticeBody">
{ _t('This event could not be displayed') }
</div>;
}
const EventTileType = sdk.getComponent(tileHandler);
const classes = classNames("mx_ReplyTile", {
mx_ReplyTile_info: isInfoMessage && !this.props.mxEvent.isRedacted(),
mx_ReplyTile_audio: msgType === MsgType.Audio,
mx_ReplyTile_video: msgType === MsgType.Video,
});
let permalink = "#";
if (this.props.permalinkCreator) {
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
}
let sender;
const needsSenderProfile = (
!isInfoMessage &&
msgType !== MsgType.Image &&
tileHandler !== EventType.RoomCreate &&
evType !== EventType.Sticker
);
if (needsSenderProfile) {
sender = <SenderProfile
mxEvent={this.props.mxEvent}
enableFlair={false}
/>;
}
const msgtypeOverrides = {
[MsgType.Image]: MImageReplyBody,
// Override audio and video body with file body. We also hide the download/decrypt button using CSS
[MsgType.Audio]: MFileBody,
[MsgType.Video]: MFileBody,
};
const evOverrides = {
// Use MImageReplyBody so that the sticker isn't taking up a lot of space
[EventType.Sticker]: MImageReplyBody,
};
return (
<div className={classes}>
<a href={permalink} onClick={this.onClick}>
{ sender }
<EventTileType
ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onHeightChanged={this.props.onHeightChanged}
showUrlPreview={false}
overrideBodyTypes={msgtypeOverrides}
overrideEventTypes={evOverrides}
replacingEventId={this.props.mxEvent.replacingEventId()}
maxImageHeight={96} />
</a>
</div>
);
}
}

View file

@ -417,7 +417,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}
private renderCommunityInvites(): ReactComponentElement<typeof ExtraTile>[] {
if (SettingsStore.getValue("feature_spaces")) return [];
if (SpaceStore.spacesEnabled) return [];
// TODO: Put community invites in a more sensible place (not in the room list)
// See https://github.com/vector-im/element-web/issues/14456
return MatrixClientPeg.get().getGroups().filter(g => {

View file

@ -408,10 +408,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
this.setState({ addRoomContextMenuPosition: null });
};
private onUnreadFirstChanged = async () => {
private onUnreadFirstChanged = () => {
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
this.forceUpdate(); // because if the sublist doesn't have any changes then we will miss the list order change
};

View file

@ -358,6 +358,17 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.setState({ generalMenuPosition: null }); // hide the menu
};
private onCopyRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
dis.dispatch({
action: 'copy_room',
room_id: this.props.room.roomId,
});
this.setState({ generalMenuPosition: null }); // hide the menu
};
private onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
@ -408,7 +419,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
>
<IconizedContextMenuOptionList first>
<IconizedContextMenuRadio
label={_t("Use default")}
label={_t("Global")}
active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs}
@ -517,6 +528,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
iconClassName="mx_RoomTile_iconInvite"
/>
) : null}
<IconizedContextMenuOption
onClick={this.onCopyRoomClick}
label={_t("Copy Room Link")}
iconClassName="mx_RoomTile_iconCopyLink"
/>
<IconizedContextMenuOption
onClick={this.onOpenRoomSettings}
label={_t("Settings")}

View file

@ -15,14 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import React from "react";
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
import EventTile, { haveTileForEvent } from "./EventTile";
import DateSeparator from '../messages/DateSeparator';
import RoomContext from "../../../contexts/RoomContext";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import DateSeparator from "../messages/DateSeparator";
import EventTile, { haveTileForEvent } from "./EventTile";
interface IProps {
// a matrix-js-sdk SearchResult containing the details of this result
@ -37,6 +38,8 @@ interface IProps {
@replaceableComponent("views.rooms.SearchResultTile")
export default class SearchResultTile extends React.Component<IProps> {
static contextType = RoomContext;
public render() {
const result = this.props.searchResult;
const mxEv = result.context.getEvent();
@ -44,7 +47,10 @@ export default class SearchResultTile extends React.Component<IProps> {
const ts1 = mxEv.getTs();
const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
const layout = SettingsStore.getValue("layout");
const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
const enableFlair = SettingsStore.getValue(UIFeature.Flair);
const timeline = result.context.getTimeline();
for (let j = 0; j < timeline.length; j++) {
@ -54,26 +60,25 @@ export default class SearchResultTile extends React.Component<IProps> {
if (!contextual) {
highlights = this.props.searchHighlights;
}
if (haveTileForEvent(ev)) {
ret.push((
if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) {
ret.push(
<EventTile
key={`${eventId}+${j}`}
mxEvent={ev}
layout={layout}
contextual={contextual}
highlights={highlights}
permalinkCreator={this.props.permalinkCreator}
highlightLink={this.props.resultLink}
onHeightChanged={this.props.onHeightChanged}
isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
isTwelveHour={isTwelveHour}
alwaysShowTimestamps={alwaysShowTimestamps}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
/>
));
enableFlair={enableFlair}
/>,
);
}
}
return (
<li data-scroll-tokens={eventId}>
{ ret }
</li>);
return <li data-scroll-tokens={eventId}>{ ret }</li>;
}
}

View file

@ -224,7 +224,7 @@ export default class Stickerpicker extends React.PureComponent {
}
_getStickerpickerContent() {
// Handle Integration Manager errors
// Handle integration manager errors
if (this.state._imError) {
return this._errorStickerpickerContent();
}

View file

@ -25,9 +25,9 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import RoomAvatar from "../avatars/RoomAvatar";
import RoomName from "../elements/RoomName";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import ErrorDialog from '../dialogs/ErrorDialog';
import AccessibleButton from '../elements/AccessibleButton';
import SpaceStore from "../../../stores/SpaceStore";
interface IProps {
event: MatrixEvent;
@ -134,7 +134,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
}
let scopeHeader;
if (SettingsStore.getValue("feature_spaces") && this.room.isSpaceRoom()) {
if (SpaceStore.spacesEnabled && this.room.isSpaceRoom()) {
scopeHeader = <div className="mx_RightPanel_scopeHeader">
<RoomAvatar room={this.room} height={32} width={32} />
<RoomName room={this.room} />