Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/18760

 Conflicts:
	src/components/structures/SpaceRoomView.tsx
This commit is contained in:
Michael Telatynski 2021-09-02 12:10:20 +01:00
commit daba23d1bf
49 changed files with 892 additions and 194 deletions

View file

@ -1016,6 +1016,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setStateForNewView({
view: Views.LOGGED_IN,
justRegistered,
currentRoomId: null,
});
this.setPage(PageTypes.HomePage);
this.notifyNewScreen('home');

View file

@ -173,6 +173,8 @@ interface IProps {
onUnfillRequest?(backwards: boolean, scrollToken: string): void;
getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations;
hideThreadedMessages?: boolean;
}
interface IState {
@ -265,6 +267,9 @@ export default class MessagePanel extends React.Component<IProps, IState> {
componentDidMount() {
this.calculateRoomMembersCount();
this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
if (SettingsStore.getValue("feature_thread")) {
this.props.room?.getThreads().forEach(thread => thread.fetchReplyChain());
}
this.isMounted = true;
}
@ -443,6 +448,12 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true;
if (mxEv.replyEventId
&& this.props.hideThreadedMessages
&& SettingsStore.getValue("feature_thread")) {
return false;
}
return !shouldHideEvent(mxEv, this.context);
}

View file

@ -45,17 +45,21 @@ import GroupRoomInfo from "../views/groups/GroupRoomInfo";
import UserInfo from "../views/right_panel/UserInfo";
import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
import FilePanel from "./FilePanel";
import ThreadView from "./ThreadView";
import ThreadPanel from "./ThreadPanel";
import NotificationPanel from "./NotificationPanel";
import ResizeNotifier from "../../utils/ResizeNotifier";
import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
import { throttle } from 'lodash';
import SpaceStore from "../../stores/SpaceStore";
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
interface IProps {
room?: Room; // if showing panels for a given room, this is set
groupId?: string; // if showing panels for a given group, this is set
user?: User; // used if we know the user ahead of opening the panel
resizeNotifier: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator;
}
interface IState {
@ -309,6 +313,22 @@ export default class RightPanel extends React.Component<IProps, IState> {
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
break;
case RightPanelPhases.ThreadView:
panel = <ThreadView
room={this.props.room}
resizeNotifier={this.props.resizeNotifier}
onClose={this.onClose}
mxEvent={this.state.event}
permalinkCreator={this.props.permalinkCreator} />;
break;
case RightPanelPhases.ThreadPanel:
panel = <ThreadPanel
roomId={roomId}
resizeNotifier={this.props.resizeNotifier}
onClose={this.onClose} />;
break;
case RightPanelPhases.RoomSummary:
panel = <RoomSummaryCard room={this.props.room} onClose={this.onClose} />;
break;

View file

@ -2052,7 +2052,10 @@ export default class RoomView extends React.Component<IProps, IState> {
const showRightPanel = this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel
? <RightPanel room={this.state.room} resizeNotifier={this.props.resizeNotifier} />
? <RightPanel
room={this.state.room}
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} />
: null;
const timelineClasses = classNames("mx_RoomView_timeline", {

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { RefObject, useContext, useRef, useState } from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventSubscription } from "fbemitter";
@ -505,11 +505,12 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
setError("");
setBusy(true);
try {
const isPublic = space.getJoinRule() === JoinRule.Public;
const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean);
await Promise.all(filteredRoomNames.map(name => {
return createRoom({
createOpts: {
preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat,
preset: isPublic ? Preset.PublicChat : Preset.PrivateChat,
name,
},
spinner: false,
@ -517,6 +518,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
andView: false,
inlineErrors: true,
parentSpace: space,
joinRule: !isPublic ? JoinRule.Restricted : undefined,
suggested: true,
});
}));

View file

@ -0,0 +1,93 @@
/*
Copyright 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 { MatrixEvent, Room } from 'matrix-js-sdk/src';
import { Thread } from 'matrix-js-sdk/src/models/thread';
import BaseCard from "../views/right_panel/BaseCard";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { MatrixClientPeg } from '../../MatrixClientPeg';
import ResizeNotifier from '../../utils/ResizeNotifier';
import EventTile from '../views/rooms/EventTile';
interface IProps {
roomId: string;
onClose: () => void;
resizeNotifier: ResizeNotifier;
}
interface IState {
threads?: Thread[];
}
@replaceableComponent("structures.ThreadView")
export default class ThreadPanel extends React.Component<IProps, IState> {
private room: Room;
constructor(props: IProps) {
super(props);
this.room = MatrixClientPeg.get().getRoom(this.props.roomId);
}
public componentDidMount(): void {
this.room.on("Thread.update", this.onThreadEventReceived);
this.room.on("Thread.ready", this.onThreadEventReceived);
}
public componentWillUnmount(): void {
this.room.removeListener("Thread.update", this.onThreadEventReceived);
this.room.removeListener("Thread.ready", this.onThreadEventReceived);
}
private onThreadEventReceived = () => this.updateThreads();
private updateThreads = (callback?: () => void): void => {
this.setState({
threads: this.room.getThreads(),
}, callback);
};
private renderEventTile(event: MatrixEvent): JSX.Element {
return <EventTile
key={event.getId()}
mxEvent={event}
enableFlair={false}
showReadReceipts={false}
as="div"
/>;
}
public render(): JSX.Element {
return (
<BaseCard
className="mx_ThreadPanel"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
{
this.state?.threads.map((thread: Thread) => {
if (thread.ready) {
return this.renderEventTile(thread.rootEvent);
}
})
}
</BaseCard>
);
}
}

View file

@ -0,0 +1,148 @@
/*
Copyright 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 { MatrixEvent, Room } from 'matrix-js-sdk/src';
import { Thread } from 'matrix-js-sdk/src/models/thread';
import BaseCard from "../views/right_panel/BaseCard";
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from '../../utils/ResizeNotifier';
import { TileShape } from '../views/rooms/EventTile';
import MessageComposer from '../views/rooms/MessageComposer';
import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks';
import { Layout } from '../../settings/Layout';
import TimelinePanel from './TimelinePanel';
import dis from "../../dispatcher/dispatcher";
import { ActionPayload } from '../../dispatcher/payloads';
import { SetRightPanelPhasePayload } from '../../dispatcher/payloads/SetRightPanelPhasePayload';
import { Action } from '../../dispatcher/actions';
interface IProps {
room: Room;
onClose: () => void;
resizeNotifier: ResizeNotifier;
mxEvent: MatrixEvent;
permalinkCreator?: RoomPermalinkCreator;
}
interface IState {
replyToEvent?: MatrixEvent;
thread?: Thread;
}
@replaceableComponent("structures.ThreadView")
export default class ThreadView extends React.Component<IProps, IState> {
private dispatcherRef: string;
constructor(props: IProps) {
super(props);
this.state = {};
}
public componentDidMount(): void {
this.setupThread(this.props.mxEvent);
this.dispatcherRef = dis.register(this.onAction);
}
public componentWillUnmount(): void {
this.teardownThread();
dis.unregister(this.dispatcherRef);
}
public componentDidUpdate(prevProps) {
if (prevProps.mxEvent !== this.props.mxEvent) {
this.teardownThread();
this.setupThread(this.props.mxEvent);
}
if (prevProps.room !== this.props.room) {
dis.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomSummary,
});
}
}
private onAction = (payload: ActionPayload): void => {
if (payload.phase == RightPanelPhases.ThreadView && payload.event) {
if (payload.event !== this.props.mxEvent) {
this.teardownThread();
this.setupThread(payload.event);
}
}
};
private setupThread = (mxEv: MatrixEvent) => {
const thread = mxEv.getThread();
if (thread) {
thread.on("Thread.update", this.updateThread);
thread.once("Thread.ready", this.updateThread);
this.updateThread(thread);
}
};
private teardownThread = () => {
if (this.state.thread) {
this.state.thread.removeListener("Thread.update", this.updateThread);
this.state.thread.removeListener("Thread.ready", this.updateThread);
}
};
private updateThread = (thread?: Thread) => {
if (thread) {
this.setState({ thread });
} else {
this.forceUpdate();
}
};
public render(): JSX.Element {
return (
<BaseCard
className="mx_ThreadView"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
withoutScrollContainer={true}
>
{ this.state.thread && (
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state?.thread?.timelineSet}
showUrlPreview={false}
tileShape={TileShape.Notif}
empty={<div>empty</div>}
alwaysShowTimestamps={true}
layout={Layout.Group}
hideThreadedMessages={false}
/>
) }
<MessageComposer
room={this.props.room}
resizeNotifier={this.props.resizeNotifier}
replyInThread={true}
replyToEvent={this.state?.thread?.replyToEvent}
showReplyPreview={false}
permalinkCreator={this.props.permalinkCreator}
compact={true}
/>
</BaseCard>
);
}
}

View file

@ -126,6 +126,8 @@ interface IProps {
// callback which is called when we wish to paginate the timeline window.
onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise<boolean>;
hideThreadedMessages?: boolean;
}
interface IState {
@ -214,6 +216,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
timelineCap: Number.MAX_VALUE,
className: 'mx_RoomView_messagePanel',
sendReadReceiptOnLoad: true,
hideThreadedMessages: true,
};
private lastRRSentEventId: string = undefined;
@ -1511,6 +1514,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
showReactions={this.props.showReactions}
layout={this.props.layout}
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
hideThreadedMessages={this.props.hideThreadedMessages}
/>
);
}

View file

@ -79,7 +79,7 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
}
try {
await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace });
await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace, joinRule });
onFinished(true);
} catch (e) {

View file

@ -1,5 +1,5 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 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.
@ -20,6 +20,7 @@ import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import {
Capability,
isTimelineCapability,
Widget,
WidgetEventCapability,
WidgetKind,
@ -30,6 +31,7 @@ import DialogButtons from "../elements/DialogButtons";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { CapabilityText } from "../../../widgets/CapabilityText";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { lexicographicCompare } from "matrix-js-sdk/src/utils";
interface IProps extends IDialogProps {
requestedCapabilities: Set<Capability>;
@ -91,7 +93,20 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
}
public render() {
const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
// We specifically order the timeline capabilities down to the bottom. The capability text
// generation cares strongly about this.
const orderedCapabilities = Object.entries(this.state.booleanStates).sort(([capA], [capB]) => {
const isTimelineA = isTimelineCapability(capA);
const isTimelineB = isTimelineCapability(capB);
if (!isTimelineA && !isTimelineB) return lexicographicCompare(capA, capB);
if (isTimelineA && !isTimelineB) return 1;
if (!isTimelineA && isTimelineB) return -1;
if (isTimelineA && isTimelineB) return lexicographicCompare(capA, capB);
return 0;
});
const checkboxRows = orderedCapabilities.map(([cap, isChecked], i) => {
const text = CapabilityText.for(cap, this.props.widgetKind);
const byline = text.byline
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{ text.byline }</span>

View file

@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/Layout";
import { UIFeature } from "../../../settings/UIFeature";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Spinner from './Spinner';
interface IProps {
/**
@ -45,7 +46,7 @@ interface IProps {
/**
* The ID of the displayed user
*/
userId: string;
userId?: string;
/**
* The display name of the displayed user
@ -118,13 +119,16 @@ export default class EventTilePreview extends React.Component<IProps, IState> {
}
public render() {
const event = this.fakeEvent(this.state);
const className = classnames(this.props.className, {
"mx_IRCLayout": this.props.layout == Layout.IRC,
"mx_GroupLayout": this.props.layout == Layout.Group,
"mx_EventTilePreview_loader": !this.props.userId,
});
if (!this.props.userId) return <div className={className}><Spinner /></div>;
const event = this.fakeEvent(this.state);
return <div className={className}>
<EventTile
mxEvent={event}

View file

@ -419,6 +419,7 @@ export default class ImageView extends React.Component<IProps, IState> {
const avatar = (
<MemberAvatar
member={mxEvent.sender}
fallbackUserId={mxEvent.getSender()}
width={32}
height={32}
viewUserOnClick={true}

View file

@ -16,11 +16,13 @@ limitations under the License.
import classNames from "classnames";
import React from "react";
import { sanitizedHtmlNode } from "../../../HtmlUtils";
import { _t } from "../../../languageHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
reason: string;
htmlReason?: string;
}
interface IState {
@ -51,7 +53,7 @@ export default class InviteReason extends React.PureComponent<IProps, IState> {
});
return <div className={classes}>
<div className="mx_InviteReason_reason">{ this.props.reason }</div>
<div className="mx_InviteReason_reason">{ this.props.htmlReason ? sanitizedHtmlNode(this.props.htmlReason) : this.props.reason }</div>
<div className="mx_InviteReason_view"
onClick={this.onViewClick}
>

View file

@ -19,6 +19,7 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import dis from '../../../dispatcher/dispatcher';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { UNSTABLE_ELEMENT_REPLY_IN_THREAD } from "matrix-js-sdk/src/@types/event";
import { makeUserPermalink, RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import { Layout } from "../../../settings/Layout";
@ -206,15 +207,28 @@ export default class ReplyThread extends React.Component<IProps, IState> {
return { body, html };
}
public static makeReplyMixIn(ev: MatrixEvent) {
public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) {
if (!ev) return {};
return {
const replyMixin = {
'm.relates_to': {
'm.in_reply_to': {
'event_id': ev.getId(),
},
},
};
/**
* @experimental
* Rendering hint for threads, only attached if true to make
* sure that Element does not start sending that property for all events
*/
if (replyInThread) {
const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to'];
inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread;
}
return replyMixin;
}
public static makeThread(

View file

@ -173,16 +173,16 @@ class EmojiPicker extends React.Component<IProps, IState> {
};
private onChangeFilter = (filter: string) => {
filter = filter.toLowerCase(); // filter is case insensitive stored lower-case
const lcFilter = filter.toLowerCase().trim(); // filter is case insensitive
for (const cat of this.categories) {
let emojis;
// If the new filter string includes the old filter string, we don't have to re-filter the whole dataset.
if (filter.includes(this.state.filter)) {
if (lcFilter.includes(this.state.filter)) {
emojis = this.memoizedDataByCategory[cat.id];
} else {
emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id];
}
emojis = emojis.filter(emoji => this.emojiMatchesFilter(emoji, filter));
emojis = emojis.filter(emoji => this.emojiMatchesFilter(emoji, lcFilter));
this.memoizedDataByCategory[cat.id] = emojis;
cat.enabled = emojis.length > 0;
// The setState below doesn't re-render the header and we already have the refs for updateVisibility, so...
@ -194,9 +194,12 @@ class EmojiPicker extends React.Component<IProps, IState> {
setTimeout(this.updateVisibility, 0);
};
private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean =>
[emoji.annotation, ...emoji.shortcodes, emoji.emoticon, ...emoji.unicode.split(ZERO_WIDTH_JOINER)]
.some(x => x?.includes(filter));
private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => {
return emoji.annotation.toLowerCase().includes(filter) ||
emoji.emoticon?.toLowerCase().includes(filter) ||
emoji.shortcodes.some(x => x.toLowerCase().includes(filter)) ||
emoji.unicode.split(ZERO_WIDTH_JOINER).includes(filter);
};
private onEnterFilter = () => {
const btn = this.bodyRef.current.querySelector<HTMLButtonElement>(".mx_EmojiPicker_item");

View file

@ -23,6 +23,8 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
import { Action } from '../../../dispatcher/actions';
import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu';
import { isContentActionable, canEditContent } from '../../../utils/EventUtils';
import RoomContext from "../../../contexts/RoomContext";
@ -34,6 +36,7 @@ import Resend from "../../../Resend";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
import DownloadActionButton from "./DownloadActionButton";
import SettingsStore from '../../../settings/SettingsStore';
const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
@ -170,6 +173,17 @@ export default class MessageActionBar extends React.PureComponent {
});
};
onThreadClick = () => {
dis.dispatch({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.ThreadView,
allowClose: false,
refireParams: {
event: this.props.mxEvent,
},
});
}
onEditClick = (ev) => {
dis.dispatch({
action: 'edit_event',
@ -254,12 +268,22 @@ export default class MessageActionBar extends React.PureComponent {
// The only catch is we do the reply button first so that we can make sure the react
// button is the very first button without having to do length checks for `splice()`.
if (this.context.canReply) {
toolbarOpts.splice(0, 0, <RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
key="reply"
/>);
toolbarOpts.splice(0, 0, <>
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
title={_t("Reply")}
onClick={this.onReplyClick}
key="reply"
/>
{ SettingsStore.getValue("feature_thread") && (
<RovingAccessibleTooltipButton
className="mx_MessageActionBar_maskButton mx_MessageActionBar_threadButton"
title={_t("Thread")}
onClick={this.onThreadClick}
key="thread"
/>
) }
</>);
}
if (this.context.canReact) {
toolbarOpts.splice(0, 0, <ReactButton

View file

@ -220,6 +220,13 @@ const onRoomFilesClick = () => {
});
};
const onRoomThreadsClick = () => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.ThreadPanel,
});
};
const onRoomSettingsClick = () => {
defaultDispatcher.dispatch({ action: "open_room_settings" });
};
@ -273,6 +280,11 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
{ _t("Show files") }
</Button>
{ SettingsStore.getValue("feature_thread") && (
<Button className="mx_RoomSummaryCard_icon_threads" onClick={onRoomThreadsClick}>
{ _t("Show threads") }
</Button>
) }
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
{ _t("Share room") }
</Button>

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

@ -21,6 +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 ReplyThread from "../elements/ReplyThread";
import { _t } from '../../../languageHandler';
@ -55,6 +56,8 @@ import ReadReceiptMarker from "./ReadReceiptMarker";
import MessageActionBar from "../messages/MessageActionBar";
import ReactionsRow from '../messages/ReactionsRow';
import { getEventDisplayInfo } from '../../../utils/EventUtils';
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
import SettingsStore from "../../../settings/SettingsStore";
const eventTileTypes = {
[EventType.RoomMessage]: 'messages.MessageEvent',
@ -299,6 +302,9 @@ interface IProps {
// whether or not to display the sender
hideSender?: boolean;
// whether or not to display thread info
showThreadInfo?: boolean;
}
interface IState {
@ -315,6 +321,8 @@ interface IState {
reactions: Relations;
hover: boolean;
thread?: Thread;
}
@replaceableComponent("views.rooms.EventTile")
@ -351,6 +359,8 @@ export default class EventTile extends React.Component<IProps, IState> {
reactions: this.getReactions(),
hover: false,
thread: this.props.mxEvent?.getThread(),
};
// don't do RR animations until we are mounted
@ -451,8 +461,20 @@ export default class EventTile extends React.Component<IProps, IState> {
client.on("Room.receipt", this.onRoomReceipt);
this.isListeningForReceipts = true;
}
if (SettingsStore.getValue("feature_thread")) {
this.props.mxEvent.once("Thread.ready", this.updateThread);
this.props.mxEvent.on("Thread.update", this.updateThread);
}
}
private updateThread = (thread) => {
this.setState({
thread,
});
this.forceUpdate();
};
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line
UNSAFE_componentWillReceiveProps(nextProps) {
@ -463,7 +485,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(nextProps, nextState, nextContext) {
if (objectHasDiff(this.state, nextState)) {
return true;
}
@ -491,6 +513,43 @@ export default class EventTile extends React.Component<IProps, IState> {
}
}
private renderThreadInfo(): React.ReactNode {
if (!SettingsStore.getValue("feature_thread")) {
return null;
}
const thread = this.state.thread;
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
if (!thread || this.props.showThreadInfo === false) {
return null;
}
const avatars = Array.from(thread.participants).map((mxId: string) => {
const member = room.getMember(mxId);
return <MemberAvatar key={member.userId} member={member} width={14} height={14} />;
});
return (
<div
className="mx_ThreadInfo"
onClick={() => {
dis.dispatch({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.ThreadView,
refireParams: {
event: this.props.mxEvent,
},
});
}}
>
<span className="mx_EventListSummary_avatars">
{ avatars }
</span>
{ thread.length - 1 } { thread.length === 2 ? 'reply' : 'replies' }
</div>
);
}
private onRoomReceipt = (ev, room) => {
// ignore events for other rooms
const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
@ -1180,6 +1239,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ keyRequestInfo }
{ actionBar }
{ this.props.layout === Layout.IRC && (reactionsRow) }
{ this.renderThreadInfo() }
</div>
{ this.props.layout !== Layout.IRC && (reactionsRow) }
{ msgOption }

View file

@ -183,7 +183,10 @@ interface IProps {
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
replyToEvent?: MatrixEvent;
replyInThread?: boolean;
showReplyPreview?: boolean;
e2eStatus?: E2EStatus;
compact?: boolean;
}
interface IState {
@ -201,6 +204,12 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private messageComposerInput: SendMessageComposer;
private voiceRecordingButton: VoiceRecordComposerTile;
static defaultProps = {
replyInThread: false,
showReplyPreview: true,
compact: false,
};
constructor(props) {
super(props);
VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate);
@ -362,7 +371,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
render() {
const controls = [
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
this.state.me && !this.props.compact ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
this.props.e2eStatus ?
<E2EIcon key="e2eIcon" status={this.props.e2eStatus} className="mx_MessageComposer_e2eIcon" /> :
null,
@ -376,6 +385,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}
@ -450,11 +460,19 @@ export default class MessageComposer extends React.Component<IProps, IState> {
/>;
}
const classes = classNames({
"mx_MessageComposer": true,
"mx_GroupLayout": true,
"mx_MessageComposer--compact": this.props.compact,
});
return (
<div className="mx_MessageComposer mx_GroupLayout">
<div className={classes}>
{ recordingTooltip }
<div className="mx_MessageComposer_wrapper">
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
{ this.props.showReplyPreview && (
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
) }
<div className="mx_MessageComposer_row">
{ controls }
</div>

View file

@ -28,6 +28,8 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import InviteReason from "../elements/InviteReason";
const MemberEventHtmlReasonField = "io.element.html_reason";
const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn",
Joining: "Joining",
@ -492,9 +494,13 @@ export default class RoomPreviewBar extends React.Component {
}
const myUserId = MatrixClientPeg.get().getUserId();
const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason;
if (reason) {
reasonElement = <InviteReason reason={reason} />;
const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.getContent();
if (memberEventContent.reason) {
reasonElement = <InviteReason
reason={memberEventContent.reason}
htmlReason={memberEventContent[MemberEventHtmlReasonField]}
/>;
}
primaryActionHandler = this.props.onJoinClick;

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

@ -26,7 +26,7 @@ import { Layout } from "../../../settings/Layout";
import { SettingLevel } from "../../../settings/SettingLevel";
interface IProps {
userId: string;
userId?: string;
displayName: string;
avatarUrl: string;
messagePreviewText: string;

View file

@ -67,7 +67,7 @@ interface IState extends IThemeState {
showAdvanced: boolean;
layout: Layout;
// User profile data for the message preview
userId: string;
userId?: string;
displayName: string;
avatarUrl: string;
}
@ -92,8 +92,8 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
systemFont: SettingsStore.getValue("systemFont"),
showAdvanced: false,
layout: SettingsStore.getValue("layout"),
userId: "@erim:fink.fink",
displayName: "Erimayas Fink",
userId: null,
displayName: null,
avatarUrl: null,
};
}

View file

@ -54,8 +54,8 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
{ space.canInvite(MatrixClientPeg.get()?.getUserId()) ? <AccessibleButton
className="mx_SpacePublicShare_inviteButton"
onClick={() => {
showRoomInviteDialog(space.roomId);
if (onFinished) onFinished();
showRoomInviteDialog(space.roomId);
}}
>
<h3>{ _t("Invite people") }</h3>