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

 Conflicts:
	src/components/structures/SpaceRoomView.tsx
	src/components/views/rooms/MemberList.tsx
	src/components/views/rooms/RoomList.tsx
This commit is contained in:
Michael Telatynski 2021-10-14 11:05:29 +01:00
commit 020eca6922
177 changed files with 6615 additions and 2121 deletions

View file

@ -28,7 +28,6 @@ import { parseEvent } from '../../../editor/deserialize';
import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts';
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
import { Action } from "../../../dispatcher/actions";
import CountlyAnalytics from "../../../CountlyAnalytics";
@ -36,7 +35,7 @@ import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindin
import { replaceableComponent } from "../../../utils/replaceableComponent";
import SendHistoryManager from '../../../SendHistoryManager';
import Modal from '../../../Modal';
import { MsgType } from 'matrix-js-sdk/src/@types/event';
import { MsgType, UNSTABLE_ELEMENT_REPLY_IN_THREAD } from 'matrix-js-sdk/src/@types/event';
import { Room } from 'matrix-js-sdk/src/models/room';
import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
@ -46,6 +45,8 @@ import { createRedactEventDialog } from '../dialogs/ConfirmRedactDialog';
import SettingsStore from "../../../settings/SettingsStore";
import { logger } from "matrix-js-sdk/src/logger";
import { withMatrixClientHOC, MatrixClientProps } from '../../../contexts/MatrixClientContext';
import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext';
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body;
@ -66,7 +67,11 @@ function getTextReplyFallback(mxEvent: MatrixEvent): string {
return "";
}
function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IContent {
function createEditContent(
model: EditorModel,
editedEvent: MatrixEvent,
renderingContext?: TimelineRenderingType,
): IContent {
const isEmote = containsEmote(model);
if (isEmote) {
model = stripEmoteCommand(model);
@ -99,41 +104,49 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`;
}
return Object.assign({
const relation = {
"m.new_content": newContent,
"m.relates_to": {
"rel_type": "m.replace",
"event_id": editedEvent.getId(),
},
}, contentBody);
};
if (renderingContext === TimelineRenderingType.Thread) {
relation['m.relates_to'][UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = true;
}
return Object.assign(relation, contentBody);
}
interface IProps {
interface IEditMessageComposerProps extends MatrixClientProps {
editState: EditorStateTransfer;
className?: string;
}
interface IState {
saveDisabled: boolean;
}
@replaceableComponent("views.rooms.EditMessageComposer")
export default class EditMessageComposer extends React.Component<IProps, IState> {
static contextType = MatrixClientContext;
context!: React.ContextType<typeof MatrixClientContext>;
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
static contextType = RoomContext;
context!: React.ContextType<typeof RoomContext>;
private readonly editorRef = createRef<BasicMessageComposer>();
private readonly dispatcherRef: string;
private model: EditorModel = null;
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
super(props);
this.context = context; // otherwise React will only set it prior to render due to type def above
const isRestored = this.createEditorModel();
const ev = this.props.editState.getEvent();
const renderingContext = this.context.timelineRenderingType;
const editContent = createEditContent(this.model, ev, renderingContext);
this.state = {
saveDisabled: !isRestored || !this.isContentModified(createEditContent(this.model, ev)["m.new_content"]),
saveDisabled: !isRestored || !this.isContentModified(editContent["m.new_content"]),
};
window.addEventListener("beforeunload", this.saveStoredEditorState);
@ -141,7 +154,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
}
private getRoom(): Room {
return this.context.getRoom(this.props.editState.getEvent().getRoomId());
return this.props.mxClient.getRoom(this.props.editState.getEvent().getRoomId());
}
private onKeyDown = (event: KeyboardEvent): void => {
@ -162,10 +175,17 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this.getRoom(), false,
this.props.editState.getEvent().getId());
const previousEvent = findEditableEvent({
events: this.events,
isForward: false,
fromEventId: this.props.editState.getEvent().getId(),
});
if (previousEvent) {
dis.dispatch({ action: 'edit_event', event: previousEvent });
dis.dispatch({
action: Action.EditEvent,
event: previousEvent,
timelineRenderingType: this.context.timelineRenderingType,
});
event.preventDefault();
}
break;
@ -174,12 +194,24 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
if (this.editorRef.current?.isModified() || !this.editorRef.current?.isCaretAtEnd()) {
return;
}
const nextEvent = findEditableEvent(this.getRoom(), true, this.props.editState.getEvent().getId());
const nextEvent = findEditableEvent({
events: this.events,
isForward: true,
fromEventId: this.props.editState.getEvent().getId(),
});
if (nextEvent) {
dis.dispatch({ action: 'edit_event', event: nextEvent });
dis.dispatch({
action: Action.EditEvent,
event: nextEvent,
timelineRenderingType: this.context.timelineRenderingType,
});
} else {
this.clearStoredEditorState();
dis.dispatch({ action: 'edit_event', event: null });
dis.dispatch({
action: Action.EditEvent,
event: null,
timelineRenderingType: this.context.timelineRenderingType,
});
dis.fire(Action.FocusSendMessageComposer);
}
event.preventDefault();
@ -189,16 +221,27 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
};
private get editorRoomKey(): string {
return `mx_edit_room_${this.getRoom().roomId}`;
return `mx_edit_room_${this.getRoom().roomId}_${this.context.timelineRenderingType}`;
}
private get editorStateKey(): string {
return `mx_edit_state_${this.props.editState.getEvent().getId()}`;
}
private get events(): MatrixEvent[] {
const liveTimelineEvents = this.context.liveTimeline.getEvents();
const pendingEvents = this.getRoom().getPendingEvents();
const isInThread = Boolean(this.props.editState.getEvent().getThread());
return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
}
private cancelEdit = (): void => {
this.clearStoredEditorState();
dis.dispatch({ action: "edit_event", event: null });
dis.dispatch({
action: Action.EditEvent,
event: null,
timelineRenderingType: this.context.timelineRenderingType,
});
dis.fire(Action.FocusSendMessageComposer);
};
@ -326,8 +369,8 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
const position = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
}
const editContent = createEditContent(this.model, editedEvent);
const renderingContext = this.context.timelineRenderingType;
const editContent = createEditContent(this.model, editedEvent, renderingContext);
const newContent = editContent["m.new_content"];
let shouldSend = true;
@ -381,7 +424,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
}
if (shouldSend) {
this.cancelPreviousPendingEdit();
const prom = this.context.sendMessage(roomId, editContent);
const prom = this.props.mxClient.sendMessage(roomId, editContent);
this.clearStoredEditorState();
dis.dispatch({ action: "message_sent" });
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
@ -389,7 +432,11 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
}
// close the event editing and focus composer
dis.dispatch({ action: "edit_event", event: null });
dis.dispatch({
action: Action.EditEvent,
event: null,
timelineRenderingType: this.context.timelineRenderingType,
});
dis.fire(Action.FocusSendMessageComposer);
};
@ -400,7 +447,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
previousEdit.status === EventStatus.QUEUED ||
previousEdit.status === EventStatus.NOT_SENT
)) {
this.context.cancelPendingEvent(previousEdit);
this.props.mxClient.cancelPendingEvent(previousEdit);
}
}
@ -428,7 +475,7 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
private createEditorModel(): boolean {
const { editState } = this.props;
const room = this.getRoom();
const partCreator = new CommandPartCreator(room, this.context);
const partCreator = new CommandPartCreator(room, this.props.mxClient);
let parts;
let isRestored = false;
@ -493,3 +540,6 @@ export default class EditMessageComposer extends React.Component<IProps, IState>
</div>);
}
}
const EditMessageComposerWithMatrixClient = withMatrixClientHOC(EditMessageComposer);
export default EditMessageComposerWithMatrixClient;

View file

@ -53,7 +53,7 @@ import SenderProfile from '../messages/SenderProfile';
import MessageTimestamp from '../messages/MessageTimestamp';
import TooltipButton from '../elements/TooltipButton';
import ReadReceiptMarker from "./ReadReceiptMarker";
import MessageActionBar, { ActionBarRenderingContext } from "../messages/MessageActionBar";
import MessageActionBar from "../messages/MessageActionBar";
import ReactionsRow from '../messages/ReactionsRow';
import { getEventDisplayInfo } from '../../../utils/EventUtils';
import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
@ -264,6 +264,8 @@ interface IProps {
// for now.
tileShape?: TileShape;
forExport?: boolean;
// show twelve hour timestamps
isTwelveHour?: boolean;
@ -340,6 +342,7 @@ export default class EventTile extends React.Component<IProps, IState> {
static defaultProps = {
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
onHeightChanged: function() {},
forExport: false,
layout: Layout.Group,
};
@ -382,7 +385,7 @@ export default class EventTile extends React.Component<IProps, IState> {
* or 'sent' receipt, for example.
* @returns {boolean}
*/
private get isEligibleForSpecialReceipt() {
private get isEligibleForSpecialReceipt(): boolean {
// First, if there are other read receipts then just short-circuit this.
if (this.props.readReceipts && this.props.readReceipts.length > 0) return false;
if (!this.props.mxEvent) return false;
@ -453,16 +456,18 @@ export default class EventTile extends React.Component<IProps, IState> {
componentDidMount() {
this.suppressReadReceiptAnimation = false;
const client = this.context;
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
if (this.props.showReactions) {
this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
}
if (!this.props.forExport) {
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
if (this.props.showReactions) {
this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
}
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
client.on("Room.receipt", this.onRoomReceipt);
this.isListeningForReceipts = true;
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
client.on("Room.receipt", this.onRoomReceipt);
this.isListeningForReceipts = true;
}
}
if (SettingsStore.getValue("feature_thread")) {
@ -698,6 +703,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
shouldHighlight() {
if (this.props.forExport) return false;
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
@ -1056,17 +1062,14 @@ export default class EventTile extends React.Component<IProps, IState> {
}
}
const renderingContext = this.props.tileShape === TileShape.Thread
? ActionBarRenderingContext.Thread
: ActionBarRenderingContext.Room;
const actionBar = !isEditing ? <MessageActionBar
const showMessageActionBar = !isEditing && !this.props.forExport;
const actionBar = showMessageActionBar ? <MessageActionBar
mxEvent={this.props.mxEvent}
reactions={this.state.reactions}
permalinkCreator={this.props.permalinkCreator}
getTile={this.getTile}
getReplyThread={this.getReplyThread}
onFocusChange={this.onActionBarFocusChange}
renderingContext={renderingContext}
isQuoteExpanded={isQuoteExpanded}
toggleThreadExpanded={() => this.setQuoteExpanded(!isQuoteExpanded)}
/> : undefined;
@ -1171,6 +1174,7 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
tileShape={this.props.tileShape}
editState={this.props.editState}
/>
</div>,
]);
@ -1204,6 +1208,8 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
tileShape={this.props.tileShape}
editState={this.props.editState}
replacingEventId={this.props.replacingEventId}
/>
{ actionBar }
</div>,
@ -1224,6 +1230,7 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={this.props.showUrlPreview}
tileShape={this.props.tileShape}
onHeightChanged={this.props.onHeightChanged}
editState={this.props.editState}
/>
</div>,
<a
@ -1247,6 +1254,7 @@ export default class EventTile extends React.Component<IProps, IState> {
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
ref={this.replyThread}
forExport={this.props.forExport}
permalinkCreator={this.props.permalinkCreator}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
@ -1280,6 +1288,7 @@ export default class EventTile extends React.Component<IProps, IState> {
{ thread }
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
forExport={this.props.forExport}
replacingEventId={this.props.replacingEventId}
editState={this.props.editState}
highlights={this.props.highlights}
@ -1305,7 +1314,7 @@ export default class EventTile extends React.Component<IProps, IState> {
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
const messageTypes = ['m.room.message', 'm.sticker'];
function isMessageEvent(ev) {
function isMessageEvent(ev: MatrixEvent): boolean {
return (messageTypes.includes(ev.getType()));
}

View file

@ -44,6 +44,8 @@ import MemberTile from "./MemberTile";
import BaseAvatar from '../avatars/BaseAvatar';
import { throttle } from 'lodash';
import SpaceStore from "../../../stores/SpaceStore";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
const getSearchQueryLSKey = (roomId: string) => `mx_MemberList_searchQuarry_${roomId}`;
@ -535,7 +537,7 @@ export default class MemberList extends React.Component<IProps, IState> {
const room = cli.getRoom(this.props.roomId);
let inviteButton;
if (room && room.getMyMembership() === 'join') {
if (room?.getMyMembership() === 'join' && shouldShowComponent(UIComponent.InviteUsers)) {
let inviteButtonText = _t("Invite to this room");
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
if (chat && chat.roomId === this.props.roomId) {

View file

@ -45,13 +45,15 @@ import { RecordingState } from "../../../audio/VoiceRecording";
import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from '../../../utils/ShieldUtils';
import SendMessageComposer from "./SendMessageComposer";
import SendMessageComposer, { SendMessageComposer as SendMessageComposerClass } from "./SendMessageComposer";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from "../../../dispatcher/actions";
import EditorModel from "../../../editor/model";
import EmojiPicker from '../emojipicker/EmojiPicker';
import MemberStatusMessageAvatar from "../avatars/MemberStatusMessageAvatar";
import UIStore, { UI_EVENTS } from '../../../stores/UIStore';
import Modal from "../../../Modal";
import InfoDialog from "../dialogs/InfoDialog";
let instanceCount = 0;
const NARROW_MODE_BREAKPOINT = 500;
@ -193,6 +195,31 @@ class UploadButton extends React.Component<IUploadButtonProps> {
}
}
// TODO: [polls] Make this component actually do something
class PollButton extends React.PureComponent {
private onCreateClick = () => {
Modal.createTrackedDialog('Polls', 'Not Yet Implemented', InfoDialog, {
// XXX: Deliberately not translated given this dialog is meant to be replaced and we don't
// want to clutter the language files with short-lived strings.
title: "Polls are currently in development",
description: "" +
"Thanks for testing polls! We haven't quite gotten a chance to write the feature yet " +
"though. Check back later for updates.",
hasCloseButton: true,
});
};
render() {
return (
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_poll"
onClick={this.onCreateClick}
title={_t('Create poll')}
/>
);
}
}
interface IProps {
room: Room;
resizeNotifier: ResizeNotifier;
@ -219,8 +246,8 @@ interface IState {
@replaceableComponent("views.rooms.MessageComposer")
export default class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef: string;
private messageComposerInput: SendMessageComposer;
private voiceRecordingButton: VoiceRecordComposerTile;
private messageComposerInput = createRef<SendMessageComposerClass>();
private voiceRecordingButton = createRef<VoiceRecordComposerTile>();
private ref: React.RefObject<HTMLDivElement> = createRef();
private instanceId: number;
@ -378,14 +405,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
}
private sendMessage = async () => {
if (this.state.haveRecording && this.voiceRecordingButton) {
if (this.state.haveRecording && this.voiceRecordingButton.current) {
// There shouldn't be any text message to send when a voice recording is active, so
// just send out the voice recording.
await this.voiceRecordingButton.send();
await this.voiceRecordingButton.current?.send();
return;
}
this.messageComposerInput.sendMessage();
this.messageComposerInput.current?.sendMessage();
};
private onChange = (model: EditorModel) => {
@ -432,6 +459,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private renderButtons(menuPosition): JSX.Element | JSX.Element[] {
const buttons: JSX.Element[] = [];
if (!this.state.haveRecording) {
if (SettingsStore.getValue("feature_polls")) {
buttons.push(
<PollButton key="polls" />,
);
}
buttons.push(
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
);
@ -460,7 +492,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
buttons.push(
<AccessibleTooltipButton
className="mx_MessageComposer_button mx_MessageComposer_voiceMessage"
onClick={() => this.voiceRecordingButton?.onRecordStartEndClick()}
onClick={() => this.voiceRecordingButton.current?.onRecordStartEndClick()}
title={_t("Send voice message")}
/>,
);
@ -521,7 +553,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
if (!this.state.tombstone && this.state.canSendMessages) {
controls.push(
<SendMessageComposer
ref={(c) => this.messageComposerInput = c}
ref={this.messageComposerInput}
key="controls_input"
room={this.props.room}
placeholder={this.renderPlaceholderText()}
@ -535,7 +567,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={c => this.voiceRecordingButton = c}
ref={this.voiceRecordingButton}
room={this.props.room} />);
} else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];

View file

@ -28,15 +28,17 @@ import AccessibleButton from "../elements/AccessibleButton";
import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader";
import RoomAvatar from "../avatars/RoomAvatar";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import dis from "../../../dispatcher/dispatcher";
import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload";
import { Action } from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher";
import SpaceStore from "../../../stores/SpaceStore";
import { showSpaceInvite } from "../../../utils/space";
import { privateShouldBeEncrypted } from "../../../createRoom";
import EventTileBubble from "../messages/EventTileBubble";
import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
@ -150,7 +152,7 @@ const NewRoomIntro = () => {
{ _t("Invite to just this room") }
</AccessibleButton> }
</div>;
} else if (room.canInvite(cli.getUserId())) {
} else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"

View file

@ -49,6 +49,8 @@ import { showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../
import { replaceableComponent } from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
interface IProps {
@ -134,32 +136,38 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
MatrixClientPeg.get().getUserId());
return <IconizedContextMenuOptionList first>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
/>
{
shouldShowComponent(UIComponent.CreateRooms)
? (<>
<IconizedContextMenuOption
label={_t("Create new room")}
iconClassName="mx_RoomList_iconPlus"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to create new rooms in this space")}
/>
<IconizedContextMenuOption
label={_t("Add existing room")}
iconClassName="mx_RoomList_iconHash"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
: _t("You do not have permissions to add rooms to this space")}
/>
</>)
: null
}
<IconizedContextMenuOption
label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconBrowse"
@ -450,8 +458,8 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
}
private renderSublists(): React.ReactElement[] {
// show a skeleton UI if the user is in no rooms and they are not filtering
const showSkeleton = !this.state.isNameFiltering &&
// show a skeleton UI if the user is in no rooms and they are not filtering and have no suggested rooms
const showSkeleton = !this.state.isNameFiltering && !this.state.suggestedRooms?.length &&
Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length);
return TAG_ORDER.reduce((tags, tagId) => {

View file

@ -35,6 +35,8 @@ import InviteReason from "../elements/InviteReason";
import { IOOBData } from "../../../stores/ThreepidInviteStore";
import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { UIFeature } from "../../../settings/UIFeature";
import SettingsStore from "../../../settings/SettingsStore";
const MemberEventHtmlReasonField = "io.element.html_reason";
@ -339,8 +341,10 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
}
case MessageCase.NotLoggedIn: {
title = _t("Join the conversation with an account");
primaryActionLabel = _t("Sign Up");
primaryActionHandler = this.onRegisterClick;
if (SettingsStore.getValue(UIFeature.Registration)) {
primaryActionLabel = _t("Sign Up");
primaryActionHandler = this.onRegisterClick;
}
secondaryActionLabel = _t("Sign In");
secondaryActionHandler = this.onLoginClick;
if (this.props.previewLoading) {

View file

@ -55,6 +55,8 @@ import { ListNotificationState } from "../../../stores/notifications/ListNotific
import IconizedContextMenu from "../context_menus/IconizedContextMenu";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
@ -675,7 +677,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
);
let addRoomButton = null;
if (!!this.props.onAddRoom) {
if (!!this.props.onAddRoom && shouldShowComponent(UIComponent.CreateRooms)) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
@ -687,6 +689,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
/>
);
} else if (this.props.addRoomContextMenu) {
// We assume that shouldShowComponent() is checked by the context menu itself.
addRoomButton = (
<ContextMenuTooltipButton
tabIndex={tabIndex}

View file

@ -29,10 +29,9 @@ import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextM
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
import { RoomNotifState } from "../../../RoomNotifs";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import NotificationBadge from "./NotificationBadge";
import { Volume } from "../../../RoomNotifsTypes";
import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions";
import { ActionPayload } from "../../../dispatcher/payloads";
@ -364,7 +363,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.setState({ generalMenuPosition: null }); // hide the menu
};
private async saveNotifState(ev: ButtonEvent, newState: Volume) {
private async saveNotifState(ev: ButtonEvent, newState: RoomNotifState) {
ev.preventDefault();
ev.stopPropagation();
if (MatrixClientPeg.get().isGuest()) return;
@ -378,10 +377,10 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
}
private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);
private onClickAlertMe = ev => this.saveNotifState(ev, ALL_MESSAGES_LOUD);
private onClickMentions = ev => this.saveNotifState(ev, MENTIONS_ONLY);
private onClickMute = ev => this.saveNotifState(ev, MUTE);
private onClickAllNotifs = ev => this.saveNotifState(ev, RoomNotifState.AllMessages);
private onClickAlertMe = ev => this.saveNotifState(ev, RoomNotifState.AllMessagesLoud);
private onClickMentions = ev => this.saveNotifState(ev, RoomNotifState.MentionsOnly);
private onClickMute = ev => this.saveNotifState(ev, RoomNotifState.Mute);
private renderNotificationsMenu(isActive: boolean): React.ReactElement {
if (MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Archived ||
@ -404,25 +403,25 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
<IconizedContextMenuOptionList first>
<IconizedContextMenuRadio
label={_t("Use default")}
active={state === ALL_MESSAGES}
active={state === RoomNotifState.AllMessages}
iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs}
/>
<IconizedContextMenuRadio
label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD}
active={state === RoomNotifState.AllMessagesLoud}
iconClassName="mx_RoomTile_iconBellDot"
onClick={this.onClickAlertMe}
/>
<IconizedContextMenuRadio
label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY}
active={state === RoomNotifState.MentionsOnly}
iconClassName="mx_RoomTile_iconBellMentions"
onClick={this.onClickMentions}
/>
<IconizedContextMenuRadio
label={_t("None")}
active={state === MUTE}
active={state === RoomNotifState.Mute}
iconClassName="mx_RoomTile_iconBellCrossed"
onClick={this.onClickMute}
/>
@ -432,14 +431,14 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
const classes = classNames("mx_RoomTile_notificationsButton", {
// Show bell icon for the default case too.
mx_RoomTile_iconBell: state === ALL_MESSAGES,
mx_RoomTile_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile_iconBellCrossed: state === MUTE,
mx_RoomTile_iconBell: state === RoomNotifState.AllMessages,
mx_RoomTile_iconBellDot: state === RoomNotifState.AllMessagesLoud,
mx_RoomTile_iconBellMentions: state === RoomNotifState.MentionsOnly,
mx_RoomTile_iconBellCrossed: state === RoomNotifState.Mute,
// Only show the icon by default if the room is overridden to muted.
// TODO: [FTUE Notifications] Probably need to detect global mute state
mx_RoomTile_notificationsButton_show: state === MUTE,
mx_RoomTile_notificationsButton_show: state === RoomNotifState.Mute,
});
return (

View file

@ -19,6 +19,7 @@ import EMOJI_REGEX from 'emojibase-regex';
import { IContent, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DebouncedFunc, throttle } from 'lodash';
import { EventType, RelationType } from "matrix-js-sdk/src/@types/event";
import { logger } from "matrix-js-sdk/src/logger";
import dis from '../../../dispatcher/dispatcher';
import EditorModel from '../../../editor/model';
@ -40,7 +41,7 @@ import { Command, CommandCategories, getCommand } from '../../../SlashCommands';
import Modal from '../../../Modal';
import { _t, _td } from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext";
import { Action } from "../../../dispatcher/actions";
import { containsEmoji } from "../../../effects/utils";
import { CHAT_EFFECTS } from '../../../effects';
@ -55,8 +56,7 @@ import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog";
import { ActionPayload } from "../../../dispatcher/payloads";
import { decorateStartSendingTime, sendRoundTripMetric } from "../../../sendTimePerformanceMetrics";
import { logger } from "matrix-js-sdk/src/logger";
import RoomContext from '../../../contexts/RoomContext';
function addReplyToMessageContent(
content: IContent,
@ -130,7 +130,7 @@ export function isQuickReaction(model: EditorModel): boolean {
return false;
}
interface IProps {
interface ISendMessageComposerProps extends MatrixClientProps {
room: Room;
placeholder?: string;
permalinkCreator: RoomPermalinkCreator;
@ -141,10 +141,8 @@ interface IProps {
}
@replaceableComponent("views.rooms.SendMessageComposer")
export default class SendMessageComposer extends React.Component<IProps> {
static contextType = MatrixClientContext;
context!: React.ContextType<typeof MatrixClientContext>;
export class SendMessageComposer extends React.Component<ISendMessageComposerProps> {
static contextType = RoomContext;
private readonly prepareToEncrypt?: DebouncedFunc<() => void>;
private readonly editorRef = createRef<BasicMessageComposer>();
private model: EditorModel = null;
@ -152,26 +150,25 @@ export default class SendMessageComposer extends React.Component<IProps> {
private dispatcherRef: string;
private sendHistoryManager: SendHistoryManager;
constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
constructor(props: ISendMessageComposerProps, context: React.ContextType<typeof RoomContext>) {
super(props);
this.context = context; // otherwise React will only set it prior to render due to type def above
if (this.context.isCryptoEnabled() && this.context.isRoomEncrypted(this.props.room.roomId)) {
if (this.props.mxClient.isCryptoEnabled() && this.props.mxClient.isRoomEncrypted(this.props.room.roomId)) {
this.prepareToEncrypt = throttle(() => {
this.context.prepareToEncrypt(this.props.room);
this.props.mxClient.prepareToEncrypt(this.props.room);
}, 60000, { leading: true, trailing: false });
}
window.addEventListener("beforeunload", this.saveStoredEditorState);
}
public componentDidUpdate(prevProps: IProps): void {
public componentDidUpdate(prevProps: ISendMessageComposerProps): void {
const replyToEventChanged = this.props.replyInThread && (this.props.replyToEvent !== prevProps.replyToEvent);
if (replyToEventChanged) {
this.model.reset([]);
}
if (this.props.replyInThread && this.props.replyToEvent && (!prevProps.replyToEvent || replyToEventChanged)) {
const partCreator = new CommandPartCreator(this.props.room, this.context);
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
const parts = this.restoreStoredEditorState(partCreator) || [];
this.model.reset(parts);
this.editorRef.current?.focus();
@ -202,13 +199,20 @@ export default class SendMessageComposer extends React.Component<IProps> {
case MessageComposerAction.EditPrevMessage:
// selection must be collapsed and caret at start
if (this.editorRef.current?.isSelectionCollapsed() && this.editorRef.current?.isCaretAtStart()) {
const editEvent = findEditableEvent(this.props.room, false);
const events =
this.context.liveTimeline.getEvents()
.concat(this.props.replyInThread ? [] : this.props.room.getPendingEvents());
const editEvent = findEditableEvent({
events,
isForward: false,
});
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
event.preventDefault();
dis.dispatch({
action: 'edit_event',
action: Action.EditEvent,
event: editEvent,
timelineRenderingType: this.context.timelineRenderingType,
});
}
}
@ -275,7 +279,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
}
private sendQuickReaction(): void {
const timeline = this.props.room.getLiveTimeline();
const timeline = this.context.liveTimeline();
const events = timeline.getEvents();
const reaction = this.model.parts[1].text;
for (let i = events.length - 1; i >= 0; i--) {
@ -448,7 +452,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
decorateStartSendingTime(content);
}
const prom = this.context.sendMessage(roomId, content);
const prom = this.props.mxClient.sendMessage(roomId, content);
if (replyToEvent) {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
@ -465,7 +469,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
});
if (SettingsStore.getValue("Performance.addSendMessageTimingMetadata")) {
prom.then(resp => {
sendRoundTripMetric(this.context, roomId, resp.event_id);
sendRoundTripMetric(this.props.mxClient, roomId, resp.event_id);
});
}
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
@ -490,7 +494,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() { // eslint-disable-line
const partCreator = new CommandPartCreator(this.props.room, this.context);
const partCreator = new CommandPartCreator(this.props.room, this.props.mxClient);
const parts = this.restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator);
this.dispatcherRef = dis.register(this.onAction);
@ -577,7 +581,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
// it puts the filename in as text/plain which we want to ignore.
if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(clipboardData.files), this.props.room.roomId, this.context,
Array.from(clipboardData.files), this.props.room.roomId, this.props.mxClient,
);
return true; // to skip internal onPaste handler
}
@ -608,3 +612,6 @@ export default class SendMessageComposer extends React.Component<IProps> {
);
}
}
const SendMessageComposerWithMatrixClient = withMatrixClientHOC(SendMessageComposer);
export default SendMessageComposerWithMatrixClient;