Merge remote-tracking branch 'upstream/develop' into feature/improved-composer

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
Šimon Brandner 2021-08-06 07:45:17 +02:00
commit 4f0c800cad
No known key found for this signature in database
GPG key ID: CC823428E9B582FB
648 changed files with 21471 additions and 11256 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

@ -179,9 +179,9 @@ export default class AuxPanel extends React.Component<IProps, IState> {
<span
className="m_RoomView_auxPanel_stateViews_span"
data-severity={severity}
key={ "x-" + stateKey }
key={"x-" + stateKey}
>
{span}
{ span }
</span>
);

View file

@ -55,6 +55,14 @@ const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.sourc
const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
const SURROUND_WITH_DOUBLE_CHARACTERS = new Map([
["(", ")"],
["[", "]"],
["{", "}"],
["<", ">"],
]);
function ctrlShortcutLabel(key: string): string {
return (IS_MAC ? "⌘" : "Ctrl") + "+" + key;
}
@ -99,6 +107,7 @@ interface IState {
showVisualBell?: boolean;
autoComplete?: AutocompleteWrapperModel;
completionIndex?: number;
surroundWith: boolean;
}
@replaceableComponent("views.rooms.BasicMessageEditor")
@ -117,12 +126,14 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private readonly emoticonSettingHandle: string;
private readonly shouldShowPillAvatarSettingHandle: string;
private readonly surroundWithHandle: string;
private readonly historyManager = new HistoryManager();
constructor(props) {
super(props);
this.state = {
showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"),
surroundWith: SettingsStore.getValue("MessageComposerInput.surroundWith"),
};
this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
@ -130,6 +141,8 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.configureEmoticonAutoReplace();
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
this.configureShouldShowPillAvatar);
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
this.surroundWithSettingChanged);
}
public componentDidUpdate(prevProps: IProps) {
@ -422,6 +435,28 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private onKeyDown = (event: React.KeyboardEvent): void => {
const model = this.props.model;
let handled = false;
if (this.state.surroundWith && document.getSelection().type != "Caret") {
// This surrounds the selected text with a character. This is
// intentionally left out of the keybinding manager as the keybinds
// here shouldn't be changeable
const selectionRange = getRangeForSelection(
this.editorRef.current,
this.props.model,
document.getSelection(),
);
// trim the range as we want it to exclude leading/trailing spaces
selectionRange.trim();
if ([...SURROUND_WITH_DOUBLE_CHARACTERS.keys(), ...SURROUND_WITH_CHARACTERS].includes(event.key)) {
this.historyManager.ensureLastChangesPushed(this.props.model);
this.modifiedFlag = true;
toggleInlineFormat(selectionRange, event.key, SURROUND_WITH_DOUBLE_CHARACTERS.get(event.key));
handled = true;
}
}
const action = getKeyBindingsManager().getMessageComposerAction(event);
switch (action) {
case MessageComposerAction.FormatBold:
@ -598,6 +633,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.setState({ showPillAvatar });
};
private surroundWithSettingChanged = () => {
const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith");
this.setState({ surroundWith });
};
componentWillUnmount() {
document.removeEventListener("selectionchange", this.onSelectionChange);
this.editorRef.current.removeEventListener("input", this.onInput, true);
@ -605,6 +645,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
SettingsStore.unwatchSetting(this.surroundWithHandle);
}
componentDidMount() {

View file

@ -129,23 +129,23 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
presenceState={this.props.presenceState} />;
}
if (this.props.subtextLabel) {
presenceLabel = <span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>;
presenceLabel = <span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>;
}
nameEl = (
<div className="mx_EntityTile_details">
<div className="mx_EntityTile_name" dir="auto">
{ name }
</div>
{presenceLabel}
{ presenceLabel }
</div>
);
} else if (this.props.subtextLabel) {
nameEl = (
<div className="mx_EntityTile_details">
<div className="mx_EntityTile_name" dir="auto">
{name}
{ name }
</div>
<span className="mx_EntityTile_subtext">{this.props.subtextLabel}</span>
<span className="mx_EntityTile_subtext">{ this.props.subtextLabel }</span>
</div>
);
} else {
@ -167,7 +167,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
const powerStatus = this.props.powerStatus;
if (powerStatus) {
const powerText = PowerLabel[powerStatus];
powerLabel = <div className="mx_EntityTile_power">{powerText}</div>;
powerLabel = <div className="mx_EntityTile_power">{ powerText }</div>;
}
let e2eIcon;

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';
@ -45,6 +44,7 @@ import EditorStateTransfer from "../../../utils/EditorStateTransfer";
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "./NotificationBadge";
import CallEventGrouper from "../../structures/CallEventGrouper";
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
import { Action } from '../../../dispatcher/actions';
import MemberAvatar from '../avatars/MemberAvatar';
@ -54,16 +54,14 @@ 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',
[EventType.Sticker]: 'messages.MessageEvent',
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
[EventType.CallInvite]: 'messages.TextualEvent',
[EventType.CallAnswer]: 'messages.TextualEvent',
[EventType.CallHangup]: 'messages.TextualEvent',
[EventType.CallReject]: 'messages.TextualEvent',
[EventType.CallInvite]: 'messages.CallEvent',
};
const stateEventTileTypes = {
@ -170,8 +168,6 @@ export function getHandlerTile(ev) {
return eventTileTypes[type];
}
const MAX_READ_AVATARS = 5;
// Our component structure for EventTiles on the timeline is:
//
// .-EventTile------------------------------------------------.
@ -192,8 +188,7 @@ export interface IReadReceiptProps {
export enum TileShape {
Notif = "notif",
FileGrid = "file_grid",
Reply = "reply",
ReplyPreview = "reply_preview",
Pinned = "pinned",
}
interface IProps {
@ -293,11 +288,17 @@ interface IProps {
// Helper to build permalinks for the room
permalinkCreator?: RoomPermalinkCreator;
// CallEventGrouper for this event
callEventGrouper?: CallEventGrouper;
// Symbol of the root node
as?: string;
// whether or not to always show timestamps
alwaysShowTimestamps?: boolean;
// whether or not to display the sender
hideSender?: boolean;
}
interface IState {
@ -321,7 +322,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>();
@ -431,7 +432,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
// TODO: [REACT-WARNING] Move into constructor
// eslint-disable-next-line camelcase
// eslint-disable-next-line
UNSAFE_componentWillMount() {
this.verifyEvent(this.props.mxEvent);
}
@ -453,7 +454,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
// eslint-disable-next-line
UNSAFE_componentWillReceiveProps(nextProps) {
// re-check the sender verification as outgoing events progress through
// the send process.
@ -657,6 +658,10 @@ export default class EventTile extends React.Component<IProps, IState> {
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
}
const MAX_READ_AVATARS = this.props.layout == Layout.Bubble
? 2
: 5;
// return early if there are no read receipts
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
// We currently must include `mx_EventTile_readAvatars` in the DOM
@ -706,9 +711,12 @@ export default class EventTile extends React.Component<IProps, IState> {
// add to the start so the most recent is on the end (ie. ends up rightmost)
avatars.unshift(
<ReadReceiptMarker key={userId} member={receipt.roomMember}
<ReadReceiptMarker
key={userId}
member={receipt.roomMember}
fallbackUserId={userId}
leftOffset={left} hidden={hidden}
leftOffset={left}
hidden={hidden}
readReceiptInfo={readReceiptInfo}
checkUnmounting={this.props.checkUnmounting}
suppressAnimation={this.suppressReadReceiptAnimation}
@ -847,35 +855,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) {
@ -902,7 +884,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_12hr: this.props.isTwelveHour,
// Note: we keep the `sending` state class for tests, not for our styles
mx_EventTile_sending: !isEditing && isSending,
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
mx_EventTile_highlight: this.props.tileShape === TileShape.Notif ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent,
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
mx_EventTile_last: this.props.last,
@ -914,6 +896,7 @@ export default class EventTile extends React.Component<IProps, IState> {
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
mx_EventTile_bad: isEncryptionFailure,
mx_EventTile_emote: msgtype === 'm.emote',
mx_EventTile_noSender: this.props.hideSender,
});
// If the tile is in the Sending state, don't speak the message.
@ -935,7 +918,7 @@ export default class EventTile extends React.Component<IProps, IState> {
let avatarSize;
let needsSenderProfile;
if (this.props.tileShape === "notif") {
if (this.props.tileShape === TileShape.Notif) {
avatarSize = 24;
needsSenderProfile = true;
} else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) {
@ -949,7 +932,7 @@ export default class EventTile extends React.Component<IProps, IState> {
} else if (this.props.layout == Layout.IRC) {
avatarSize = 14;
needsSenderProfile = true;
} else if (this.props.continuation && this.props.tileShape !== "file_grid") {
} else if (this.props.continuation && this.props.tileShape !== TileShape.FileGrid) {
// no avatar or sender profile for continuation messages
avatarSize = 0;
needsSenderProfile = false;
@ -970,16 +953,18 @@ export default class EventTile extends React.Component<IProps, IState> {
}
avatar = (
<div className="mx_EventTile_avatar">
<MemberAvatar member={member}
width={avatarSize} height={avatarSize}
<MemberAvatar
member={member}
width={avatarSize}
height={avatarSize}
viewUserOnClick={true}
/>
</div>
);
}
if (needsSenderProfile) {
if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') {
if (needsSenderProfile && this.props.hideSender !== true) {
if (!this.props.tileShape) {
sender = <SenderProfile onClick={this.onSenderProfileClick}
mxEvent={this.props.mxEvent}
enableFlair={this.props.enableFlair}
@ -998,8 +983,12 @@ export default class EventTile extends React.Component<IProps, IState> {
onFocusChange={this.onActionBarFocusChange}
/> : undefined;
const showTimestamp = this.props.mxEvent.getTs() &&
(this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused);
const showTimestamp = this.props.mxEvent.getTs()
&& (this.props.alwaysShowTimestamps
|| this.props.last
|| this.state.hover
|| this.state.actionBarFocused);
const timestamp = showTimestamp ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -1065,7 +1054,7 @@ export default class EventTile extends React.Component<IProps, IState> {
}
switch (this.props.tileShape) {
case 'notif': {
case TileShape.Notif: {
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
return React.createElement(this.props.as || "li", {
"className": classes,
@ -1093,11 +1082,12 @@ export default class EventTile extends React.Component<IProps, IState> {
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
tileShape={this.props.tileShape}
/>
</div>,
]);
}
case 'file_grid': {
case TileShape.FileGrid: {
return React.createElement(this.props.as || "li", {
"className": classes,
"aria-live": ariaLive,
@ -1128,44 +1118,6 @@ export default class EventTile extends React.Component<IProps, IState> {
]);
}
case 'reply':
case 'reply_preview': {
let thread;
if (this.props.tileShape === 'reply_preview') {
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,
@ -1176,6 +1128,8 @@ export default class EventTile extends React.Component<IProps, IState> {
this.props.alwaysShowTimestamps || this.state.hover,
);
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
return (
React.createElement(this.props.as || "li", {
@ -1185,12 +1139,16 @@ export default class EventTile extends React.Component<IProps, IState> {
"aria-live": ariaLive,
"aria-atomic": "true",
"data-scroll-tokens": scrollToken,
"data-layout": this.props.layout,
"data-self": isOwnEvent,
"data-has-reply": !!thread,
"onMouseEnter": () => this.setState({ hover: true }),
"onMouseLeave": () => this.setState({ hover: false }),
}, [
ircTimestamp,
sender,
ircPadlock,
}, <>
{ ircTimestamp }
{ sender }
{ ircPadlock }
{ avatar }
<div className="mx_EventTile_line" key="mx_EventTile_line">
{ groupTimestamp }
{ groupPadlock }
@ -1204,15 +1162,15 @@ export default class EventTile extends React.Component<IProps, IState> {
showUrlPreview={this.props.showUrlPreview}
permalinkCreator={this.props.permalinkCreator}
onHeightChanged={this.props.onHeightChanged}
callEventGrouper={this.props.callEventGrouper}
/>
{ keyRequestInfo }
{ reactionsRow }
{ actionBar }
</div>,
msgOption,
avatar,
])
{ this.props.layout === Layout.IRC && (reactionsRow) }
</div>
{ this.props.layout !== Layout.IRC && (reactionsRow) }
{ msgOption }
</>)
);
}
}
@ -1225,7 +1183,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;
@ -1235,7 +1193,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 {
@ -1315,7 +1273,7 @@ class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
className={classes}
onMouseEnter={this.onHoverStart}
onMouseLeave={this.onHoverEnd}
>{tooltip}</div>
>{ tooltip }</div>
);
}
}
@ -1379,8 +1337,8 @@ class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptSta
<div className="mx_EventTile_msgOption">
<span className="mx_EventTile_readAvatars">
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
{nonCssBadge}
{tooltip}
{ nonCssBadge }
{ tooltip }
</span>
</span>
</div>

View file

@ -84,7 +84,7 @@ export default class ExtraTile extends React.Component<IProps, IState> {
let nameContainer = (
<div className="mx_RoomTile_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
{ name }
</div>
</div>
);
@ -106,11 +106,11 @@ export default class ExtraTile extends React.Component<IProps, IState> {
title={this.props.isMinimized ? name : undefined}
>
<div className="mx_RoomTile_avatarContainer">
{this.props.avatar}
{ this.props.avatar }
</div>
{nameContainer}
{ nameContainer }
<div className="mx_RoomTile_badgeContainer">
{badge}
{ badge }
</div>
</Button>
</React.Fragment>

View file

@ -25,13 +25,14 @@ export default (props) => {
});
let badge;
if (props.numUnreadMessages) {
badge = (<div className="mx_JumpToBottomButton_badge">{props.numUnreadMessages}</div>);
badge = (<div className="mx_JumpToBottomButton_badge">{ props.numUnreadMessages }</div>);
}
return (<div className={className}>
<AccessibleButton className="mx_JumpToBottomButton_scrollDown"
<AccessibleButton
className="mx_JumpToBottomButton_scrollDown"
title={_t("Scroll to most recent messages")}
onClick={props.onScrollToBottomClick}>
</AccessibleButton>
onClick={props.onScrollToBottomClick}
/>
{ badge }
</div>);
};

View file

@ -14,43 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect } from "react";
import React, { useContext, useEffect } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { IPreviewUrlResponse } from "matrix-js-sdk/src/client";
import { useStateToggle } from "../../../hooks/useStateToggle";
import LinkPreviewWidget from "./LinkPreviewWidget";
import AccessibleButton from "../elements/AccessibleButton";
import { _t } from "../../../languageHandler";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
const INITIAL_NUM_PREVIEWS = 2;
interface IProps {
links: string[]; // the URLs to be previewed
mxEvent: MatrixEvent; // the Event associated with the preview
onCancelClick?(): void; // called when the preview's cancel ('hide') button is clicked
onHeightChanged?(): void; // called when the preview's contents has loaded
onCancelClick(): void; // called when the preview's cancel ('hide') button is clicked
onHeightChanged(): void; // called when the preview's contents has loaded
}
const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onHeightChanged }) => {
const cli = useContext(MatrixClientContext);
const [expanded, toggleExpanded] = useStateToggle();
const ts = mxEvent.getTs();
const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => {
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], []);
useEffect(() => {
onHeightChanged();
}, [onHeightChanged, expanded]);
}, [onHeightChanged, expanded, previews]);
const shownLinks = expanded ? links : links.slice(0, INITIAL_NUM_PREVIEWS);
const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS);
let toggleButton;
if (links.length > INITIAL_NUM_PREVIEWS) {
let toggleButton: JSX.Element;
if (previews.length > INITIAL_NUM_PREVIEWS) {
toggleButton = <AccessibleButton onClick={toggleExpanded}>
{ expanded
? _t("Collapse")
: _t("Show %(count)s other previews", { count: links.length - shownLinks.length }) }
: _t("Show %(count)s other previews", { count: previews.length - showPreviews.length }) }
</AccessibleButton>;
}
return <div className="mx_LinkPreviewGroup">
{ shownLinks.map((link, i) => (
<LinkPreviewWidget key={link} link={link} mxEvent={mxEvent} onHeightChanged={onHeightChanged}>
{ showPreviews.map(([link, preview], i) => (
<LinkPreviewWidget key={link} link={link} preview={preview} mxEvent={mxEvent}>
{ i === 0 ? (
<AccessibleButton
className="mx_LinkPreviewGroup_hide"

View file

@ -21,7 +21,6 @@ import { IPreviewUrlResponse } from 'matrix-js-sdk/src/client';
import { linkifyElement } from '../../../HtmlUtils';
import SettingsStore from "../../../settings/SettingsStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal";
import * as ImageUtils from "../../../ImageUtils";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -29,37 +28,15 @@ import { mediaFromMxc } from "../../../customisations/Media";
import ImageView from '../elements/ImageView';
interface IProps {
link: string; // the URL being previewed
link: string;
preview: IPreviewUrlResponse;
mxEvent: MatrixEvent; // the Event associated with the preview
onHeightChanged(): void; // called when the preview's contents has loaded
}
interface IState {
preview?: IPreviewUrlResponse;
}
@replaceableComponent("views.rooms.LinkPreviewWidget")
export default class LinkPreviewWidget extends React.Component<IProps, IState> {
private unmounted = false;
export default class LinkPreviewWidget extends React.Component<IProps> {
private readonly description = createRef<HTMLDivElement>();
constructor(props) {
super(props);
this.state = {
preview: null,
};
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((preview) => {
if (this.unmounted) {
return;
}
this.setState({ preview }, this.props.onHeightChanged);
}, (error) => {
console.error("Failed to get URL preview: " + error);
});
}
componentDidMount() {
if (this.description.current) {
linkifyElement(this.description.current);
@ -72,12 +49,8 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
}
}
componentWillUnmount() {
this.unmounted = true;
}
private onImageClick = ev => {
const p = this.state.preview;
const p = this.props.preview;
if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault();
@ -99,7 +72,7 @@ export default class LinkPreviewWidget extends React.Component<IProps, IState> {
};
render() {
const p = this.state.preview;
const p = this.props.preview;
if (!p || Object.keys(p).length === 0) {
return <div />;
}

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;
@ -92,7 +93,7 @@ export default class MemberList extends React.Component<IProps, IState> {
this.showPresence = enablePresenceByHsUrl?.[hsUrl] ?? true;
}
// eslint-disable-next-line camelcase
// eslint-disable-next-line
UNSAFE_componentWillMount() {
const cli = MatrixClientPeg.get();
this.mounted = true;
@ -305,10 +306,16 @@ export default class MemberList extends React.Component<IProps, IState> {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={onClick} />
<EntityTile
className="mx_EntityTile_ellipsis"
avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
}
name={text}
presenceState="online"
suppressOnHover={true}
onClick={onClick}
/>
);
};
@ -464,8 +471,12 @@ export default class MemberList extends React.Component<IProps, IState> {
return <MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this.showPresence} />;
} else {
// Is a 3pid invite
return <EntityTile key={m.getStateKey()} name={m.getContent().display_name} suppressOnHover={true}
onClick={() => this.onPending3pidInviteClick(m)} />;
return <EntityTile
key={m.getStateKey()}
name={m.getContent().display_name}
suppressOnHover={true}
onClick={() => this.onPending3pidInviteClick(m)}
/>;
}
});
}
@ -509,7 +520,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");
}
@ -542,14 +553,14 @@ export default class MemberList extends React.Component<IProps, IState> {
const footer = (
<SearchBox
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={ _t('Filter room members') }
onSearch={ this.onSearchQueryChanged } />
placeholder={_t('Filter room members')}
onSearch={this.onSearchQueryChanged} />
);
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

@ -35,7 +35,7 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
import { RecordingState } from "../../../voice/VoiceRecording";
import { RecordingState } from "../../../audio/VoiceRecording";
import Tooltip, { Alignment } from "../elements/Tooltip";
import ResizeNotifier from "../../../utils/ResizeNotifier";
import { E2EStatus } from '../../../utils/ShieldUtils';
@ -98,9 +98,7 @@ const EmojiButton = ({ addEmoji }) => {
isExpanded={menuDisplayed}
title={_t('Emoji picker')}
inputRef={button}
>
</ContextMenuTooltipButton>
/>
{ contextMenu }
</React.Fragment>;
@ -344,8 +342,11 @@ export default class MessageComposer extends React.Component<IProps, IState> {
private onVoiceStoreUpdate = () => {
const recording = VoiceRecordingStore.instance.activeRecording;
this.setState({ haveRecording: !!recording });
if (recording) {
// Delay saying we have a recording until it is started, as we might not yet have A/V permissions
recording.on(RecordingState.Started, () => {
this.setState({ haveRecording: !!VoiceRecordingStore.instance.activeRecording });
});
// We show a little heads up that the recording is about to automatically end soon. The 3s
// display time is completely arbitrary. Note that we don't need to deregister the listener
// because the recording instance will clean that up for us.
@ -353,6 +354,8 @@ export default class MessageComposer extends React.Component<IProps, IState> {
this.setState({ recordingTimeLeftSeconds: secondsLeft });
setTimeout(() => this.setState({ recordingTimeLeftSeconds: null }), 3000);
});
} else {
this.setState({ haveRecording: false });
}
};
@ -391,12 +394,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
}
if (SettingsStore.getValue("feature_voice_messages")) {
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={c => this.voiceRecordingButton = c}
room={this.props.room} />);
}
controls.push(<VoiceRecordComposerTile
key="controls_voice_record"
ref={c => this.voiceRecordingButton = c}
room={this.props.room} />);
if (!this.state.isComposerEmpty || this.state.haveRecording) {
controls.push(
@ -411,7 +412,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
className="mx_MessageComposer_roomReplaced_link"
onClick={this.onTombstoneClick}
>
{_t("The conversation continues here.")}
{ _t("The conversation continues here.") }
</a>
) : '';
@ -421,7 +422,7 @@ export default class MessageComposer extends React.Component<IProps, IState> {
src={require("../../../../res/img/room_replaced.svg")}
/>
<span className="mx_MessageComposer_roomReplaced_header">
{_t("This room has been replaced and is no longer active.")}
{ _t("This room has been replaced and is no longer active.") }
</span><br />
{ continuesLink }
</div>
@ -439,13 +440,14 @@ export default class MessageComposer extends React.Component<IProps, IState> {
if (secondsLeft) {
recordingTooltip = <Tooltip
label={_t("%(seconds)ss left", { seconds: secondsLeft })}
alignment={Alignment.Top} yOffset={-50}
alignment={Alignment.Top}
yOffset={-50}
/>;
}
return (
<div className="mx_MessageComposer mx_GroupLayout">
{recordingTooltip}
{ recordingTooltip }
<div className="mx_MessageComposer_wrapper">
<ReplyPreview permalinkCreator={this.props.permalinkCreator} />
<div className="mx_MessageComposer_row">

View file

@ -58,19 +58,24 @@ const NewRoomIntro = () => {
const member = room?.getMember(dmPartner);
const displayName = member?.rawDisplayName || dmPartner;
body = <React.Fragment>
<RoomAvatar room={room} width={AVATAR_SIZE} height={AVATAR_SIZE} onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || { userId: dmPartner } as User,
});
}} />
<RoomAvatar
room={room}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
onClick={() => {
defaultDispatcher.dispatch<ViewUserPayload>({
action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the receiver wants.
member: member || { userId: dmPartner } as User,
});
}}
/>
<h2>{ room.name }</h2>
<p>{_t("This is the beginning of your direct message history with <displayName/>.", {}, {
<p>{ _t("This is the beginning of your direct message history with <displayName/>.", {}, {
displayName: () => <b>{ displayName }</b>,
})}</p>
}) }</p>
{ caption && <p>{ caption }</p> }
</React.Fragment>;
} else {
@ -132,7 +137,7 @@ const NewRoomIntro = () => {
showSpaceInvite(parentSpace);
}}
>
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
{ _t("Invite to %(spaceName)s", { spaceName: parentSpace.name }) }
</AccessibleButton>
{ room.canInvite(cli.getUserId()) && <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
@ -141,7 +146,7 @@ const NewRoomIntro = () => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to just this room")}
{ _t("Invite to just this room") }
</AccessibleButton> }
</div>;
} else if (room.canInvite(cli.getUserId())) {
@ -153,7 +158,7 @@ const NewRoomIntro = () => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to this room")}
{ _t("Invite to this room") }
</AccessibleButton>
</div>;
}
@ -170,10 +175,10 @@ const NewRoomIntro = () => {
<h2>{ room.name }</h2>
<p>{createdText} {_t("This is the start of <roomName/>.", {}, {
<p>{ createdText } { _t("This is the start of <roomName/>.", {}, {
roomName: () => <b>{ room.name }</b>,
})}</p>
<p>{topicText}</p>
}) }</p>
<p>{ topicText }</p>
{ buttons }
</React.Fragment>;
}
@ -190,7 +195,7 @@ const NewRoomIntro = () => {
"Your private messages are normally encrypted, but this room isn't. "+
"Usually this is due to an unsupported device or method being used, " +
"like email invites. <a>Enable encryption in settings.</a>", {},
{ a: sub => <a onClick={openRoomSettings} href="#">{sub}</a> },
{ a: sub => <a onClick={openRoomSettings} href="#">{ sub }</a> },
);
return <div className="mx_NewRoomIntro">
@ -201,7 +206,7 @@ const NewRoomIntro = () => {
title={_t("End-to-end encryption isn't enabled")}
subtitle={sub2}
/>
)}
) }
{ body }
</div>;

View file

@ -126,14 +126,14 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
if (onClick) {
return (
<AccessibleButton {...props} className={classes} onClick={onClick}>
<span className="mx_NotificationBadge_count">{symbol}</span>
<span className="mx_NotificationBadge_count">{ symbol }</span>
</AccessibleButton>
);
}
return (
<div className={classes}>
<span className="mx_NotificationBadge_count">{symbol}</span>
<span className="mx_NotificationBadge_count">{ symbol }</span>
</div>
);
}

View file

@ -29,6 +29,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { getUserNameColorClass } from "../../../utils/FormattingUtils";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { TileShape } from "./EventTile";
interface IProps {
room: Room;
@ -84,9 +85,11 @@ export default class PinnedEventTile extends React.Component<IProps> {
<div className="mx_PinnedEventTile_message">
<MessageEvent
mxEvent={this.props.event}
// @ts-ignore - complaining that className is invalid when it's not
className="mx_PinnedEventTile_body"
maxImageHeight={150}
onHeightChanged={() => {}} // we need to give this, apparently
tileShape={TileShape.Pinned}
/>
</div>

View file

@ -145,7 +145,7 @@ export default class ReadReceiptMarker extends React.PureComponent {
if (oldInfo && oldInfo.left) {
// start at the old height and in the old h pos
startStyles.push({ top: startTopOffset+"px",
left: toPx(oldInfo.left) });
left: toPx(oldInfo.left) });
}
startStyles.push({ top: startTopOffset+'px', left: '0' });
@ -174,14 +174,14 @@ export default class ReadReceiptMarker extends React.PureComponent {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{ userName: this.props.fallbackUserId,
dateTime: dateString },
dateTime: dateString },
);
} else {
title = _t(
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
{ displayName: this.props.member.rawDisplayName,
userName: this.props.fallbackUserId,
dateTime: dateString },
userName: this.props.fallbackUserId,
dateTime: dateString },
);
}
}
@ -192,7 +192,9 @@ export default class ReadReceiptMarker extends React.PureComponent {
member={this.props.member}
fallbackUserId={this.props.fallbackUserId}
aria-hidden="true"
width={14} height={14} resizeMethod="crop"
width={14}
height={14}
resizeMethod="crop"
style={style}
title={title}
onClick={this.props.onClick}

View file

@ -1,5 +1,5 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2017 - 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.
@ -16,14 +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 ReplyTile from './ReplyTile';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { EventSubscription } from 'fbemitter';
function cancelQuoting() {
dis.dispatch({
@ -32,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">
@ -88,15 +90,12 @@ export default class ReplyPreview extends React.Component {
/>
</div>
<div className="mx_ReplyPreview_clear" />
<EventTile
alwaysShowTimestamps={true}
tileShape="reply_preview"
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,161 @@
/*
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 => {
const clickTarget = e.target as HTMLElement;
// Following a link within a reply should not dispatch the `view_room` action
// so that the browser can direct the user to the correct location
// The exception being the link wrapping the reply
if (clickTarget.tagName.toLowerCase() !== "a" || clickTarget.closest("a") === null) {
// 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

@ -105,11 +105,13 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
// NOTE: The CSSTransition timeout MUST match the timeout in our CSS!
return (
<CSSTransition
appear={true} in={this.state.doAnimation} timeout={640}
appear={true}
in={this.state.doAnimation}
timeout={640}
classNames='mx_RoomBreadcrumbs'
>
<Toolbar className='mx_RoomBreadcrumbs' aria-label={_t("Recently visited rooms")}>
{tiles.slice(this.state.skipFirst ? 1 : 0)}
{ tiles.slice(this.state.skipFirst ? 1 : 0) }
</Toolbar>
</CSSTransition>
);
@ -117,7 +119,7 @@ export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState>
return (
<div className='mx_RoomBreadcrumbs'>
<div className="mx_RoomBreadcrumbs_placeholder">
{_t("No recently visited rooms")}
{ _t("No recently visited rooms") }
</div>
</div>
);

View file

@ -1,5 +1,5 @@
/*
Copyright 2017 New Vector Ltd.
Copyright 2017-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.
@ -21,9 +21,10 @@ import { linkifyElement } from '../../../HtmlUtils';
import PropTypes from 'prop-types';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
import { getDisplayAliasForAliasSet } from '../../../Rooms';
export function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
return getDisplayAliasForAliasSet(room.canonicalAlias, room.aliases);
}
export const roomShape = PropTypes.shape({
@ -104,8 +105,12 @@ export default class RoomDetailRow extends React.Component {
return <tr key={room.roomId} onClick={this.onClick} onMouseDown={this.props.onMouseDown}>
<td className="mx_RoomDirectory_roomAvatar">
<BaseAvatar width={24} height={24} resizeMethod='crop'
name={name} idName={name}
<BaseAvatar
width={24}
height={24}
resizeMethod='crop'
name={name}
idName={name}
url={avatarUrl} />
</td>
<td className="mx_RoomDirectory_roomDescription">

View file

@ -29,6 +29,8 @@ import RoomTopic from "../elements/RoomTopic";
import RoomName from "../elements/RoomName";
import { PlaceCallType } from "../../../CallHandler";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import Modal from '../../../Modal';
import InfoDialog from "../dialogs/InfoDialog";
import { throttle } from 'lodash';
import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src';
import { E2EStatus } from '../../../utils/ShieldUtils';
@ -87,6 +89,14 @@ export default class RoomHeader extends React.Component<IProps> {
this.forceUpdate();
}, 500, { leading: true, trailing: true });
private displayInfoDialogAboutScreensharing() {
Modal.createDialog(InfoDialog, {
title: _t("Screen sharing is here!"),
description: _t("You can now share your screen by pressing the \"screen share\" " +
"button during a call. You can even do this in audio calls if both sides support it!"),
});
}
public render() {
let searchStatus = null;
@ -121,18 +131,18 @@ export default class RoomHeader extends React.Component<IProps> {
const name =
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<RoomName room={this.props.room}>
{(name) => {
{ (name) => {
const roomName = name || oobName;
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
}}
} }
</RoomName>
{ searchStatus }
</div>;
const topicElement = <RoomTopic room={this.props.room}>
{(topic, ref) => <div className="mx_RoomHeader_topic" ref={ref} title={topic} dir="auto">
{ (topic, ref) => <div className="mx_RoomHeader_topic" ref={ref} title={topic} dir="auto">
{ topic }
</div>}
</div> }
</RoomTopic>;
let roomAvatar;
@ -185,8 +195,8 @@ export default class RoomHeader extends React.Component<IProps> {
videoCallButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
onClick={(ev) => this.props.onCallPlaced(
ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
onClick={(ev) => ev.shiftKey ?
this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)}
title={_t("Video call")} />;
}

View file

@ -68,7 +68,7 @@ interface IState {
suggestedRooms: ISuggestedRoom[];
}
const TAG_ORDER: TagID[] = [
export const TAG_ORDER: TagID[] = [
DefaultTagID.Invite,
DefaultTagID.Favourite,
DefaultTagID.DM,
@ -140,7 +140,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
showCreateNewRoom(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@ -153,7 +153,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
e.preventDefault();
e.stopPropagation();
onFinished();
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
showAddExistingRooms(SpaceStore.instance.activeSpace);
}}
disabled={!canAddRooms}
tooltip={canAddRooms ? undefined
@ -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 => {
@ -428,7 +428,9 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
groupId={g.groupId}
groupName={g.name}
groupAvatarUrl={g.avatarUrl}
width={32} height={32} resizeMethod='crop'
width={32}
height={32}
resizeMethod='crop'
/>
);
const openGroup = () => {
@ -507,13 +509,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
if (!this.props.isMinimized) {
if (this.state.isNameFiltering) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{_t("Can't see what youre looking for?")}</div>
<div>{ _t("Can't see what youre looking for?") }</div>
<AccessibleButton
className="mx_RoomList_explorePrompt_startChat"
kind="link"
onClick={this.onStartChat}
>
{_t("Start a new chat")}
{ _t("Start a new chat") }
</AccessibleButton>
<AccessibleButton
className="mx_RoomList_explorePrompt_explore"
@ -532,13 +534,13 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick}
>
{_t("Invite people")}
{ _t("Invite people") }
</AccessibleButton> }
{ this.props.activeSpace.getMyMembership() === "join" && <AccessibleButton
className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore}
>
{_t("Explore rooms")}
{ _t("Explore rooms") }
</AccessibleButton> }
</div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) {
@ -549,20 +551,20 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
// show a prompt to join/create rooms if the user is in 0 rooms and no historical
if (unfilteredRooms.length < 1 && unfilteredHistorical < 1 && unfilteredFavourite < 1) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{_t("Use the + to make a new room or explore existing ones below")}</div>
<div>{ _t("Use the + to make a new room or explore existing ones below") }</div>
<AccessibleButton
className="mx_RoomList_explorePrompt_startChat"
kind="link"
onClick={this.onStartChat}
>
{_t("Start a new chat")}
{ _t("Start a new chat") }
</AccessibleButton>
<AccessibleButton
className="mx_RoomList_explorePrompt_explore"
kind="link"
onClick={this.onExplore}
>
{_t("Explore all public rooms")}
{ _t("Explore all public rooms") }
</AccessibleButton>
</div>;
}
@ -572,7 +574,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
const sublists = this.renderSublists();
return (
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={this.props.onKeyDown}>
{({ onKeyDownHandler }) => (
{ ({ onKeyDownHandler }) => (
<div
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
@ -581,10 +583,10 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
role="tree"
aria-label={_t("Rooms")}
>
{sublists}
{explorePrompt}
{ sublists }
{ explorePrompt }
</div>
)}
) }
</RovingTabIndexProvider>
);
}

View file

@ -340,7 +340,7 @@ export default class RoomPreviewBar extends React.Component {
footer = (
<div>
<Spinner w={20} h={20} />
{_t("Loading room preview")}
{ _t("Loading room preview") }
</div>
);
}
@ -465,11 +465,11 @@ export default class RoomPreviewBar extends React.Component {
if (inviteMember) {
inviterElement = <span>
<span className="mx_RoomPreviewBar_inviter">
{inviteMember.rawDisplayName}
</span> ({inviteMember.userId})
{ inviteMember.rawDisplayName }
</span> ({ inviteMember.userId })
</span>;
} else {
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>);
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{ this.props.inviterName }</span>);
}
const isDM = this._isDMInvite();
@ -536,8 +536,10 @@ export default class RoomPreviewBar extends React.Component {
"If you think you're seeing this message in error, please " +
"<issueLink>submit a bug report</issueLink>.",
{ errcode: this.props.error.errcode },
{ issueLink: label => <a href="https://github.com/vector-im/element-web/issues/new/choose"
target="_blank" rel="noreferrer noopener">{ label }</a> },
{ issueLink: label => <a
href="https://github.com/vector-im/element-web/issues/new/choose"
target="_blank"
rel="noreferrer noopener">{ label }</a> },
),
];
break;
@ -549,7 +551,7 @@ export default class RoomPreviewBar extends React.Component {
if (!Array.isArray(subTitle)) {
subTitle = [subTitle];
}
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{t}</p>);
subTitleElements = subTitle.map((t, i) => <p key={`subTitle${i}`}>{ t }</p>);
}
let titleElement;

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
};
@ -574,20 +574,20 @@ export default class RoomSublist extends React.Component<IProps, IState> {
<React.Fragment>
<hr />
<div>
<div className='mx_RoomSublist_contextMenu_title'>{_t("Appearance")}</div>
<div className='mx_RoomSublist_contextMenu_title'>{ _t("Appearance") }</div>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onUnreadFirstChanged}
checked={isUnreadFirst}
>
{_t("Show rooms with unread messages first")}
{ _t("Show rooms with unread messages first") }
</StyledMenuItemCheckbox>
<StyledMenuItemCheckbox
onClose={this.onCloseMenu}
onChange={this.onMessagePreviewChanged}
checked={this.layout.showPreviews}
>
{_t("Show previews of messages")}
{ _t("Show previews of messages") }
</StyledMenuItemCheckbox>
</div>
</React.Fragment>
@ -603,14 +603,14 @@ export default class RoomSublist extends React.Component<IProps, IState> {
>
<div className="mx_RoomSublist_contextMenu">
<div>
<div className='mx_RoomSublist_contextMenu_title'>{_t("Sort by")}</div>
<div className='mx_RoomSublist_contextMenu_title'>{ _t("Sort by") }</div>
<StyledMenuItemRadio
onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
checked={!isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{_t("Activity")}
{ _t("Activity") }
</StyledMenuItemRadio>
<StyledMenuItemRadio
onClose={this.onCloseMenu}
@ -618,10 +618,10 @@ export default class RoomSublist extends React.Component<IProps, IState> {
checked={isAlphabetical}
name={`mx_${this.props.tagId}_sortBy`}
>
{_t("A-Z")}
{ _t("A-Z") }
</StyledMenuItemRadio>
</div>
{otherSections}
{ otherSections }
</div>
</ContextMenu>
);
@ -634,7 +634,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
onFinished={this.onCloseAddRoomMenu}
compact
>
{this.props.addRoomContextMenu(this.onCloseAddRoomMenu)}
{ this.props.addRoomContextMenu(this.onCloseAddRoomMenu) }
</IconizedContextMenu>
);
}
@ -647,7 +647,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
title={_t("List options")}
isExpanded={!!this.state.contextMenuPosition}
/>
{contextMenu}
{ contextMenu }
</React.Fragment>
);
}
@ -655,7 +655,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
private renderHeader(): React.ReactElement {
return (
<RovingTabIndexWrapper inputRef={this.headerButton}>
{({ onFocus, isActive, ref }) => {
{ ({ onFocus, isActive, ref }) => {
const tabIndex = isActive ? 0 : -1;
let ariaLabel = _t("Jump to first unread room.");
@ -711,7 +711,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const badgeContainer = (
<div className="mx_RoomSublist_badgeContainer">
{badge}
{ badge }
</div>
);
@ -746,17 +746,17 @@ export default class RoomSublist extends React.Component<IProps, IState> {
title={this.props.isMinimized ? this.props.label : undefined}
>
<span className={collapseClasses} />
<span>{this.props.label}</span>
<span>{ this.props.label }</span>
</Button>
{this.renderMenu()}
{this.props.isMinimized ? null : badgeContainer}
{this.props.isMinimized ? null : addRoomButton}
{ this.renderMenu() }
{ this.props.isMinimized ? null : badgeContainer }
{ this.props.isMinimized ? null : addRoomButton }
</div>
{this.props.isMinimized ? badgeContainer : null}
{this.props.isMinimized ? addRoomButton : null}
{ this.props.isMinimized ? badgeContainer : null }
{ this.props.isMinimized ? addRoomButton : null }
</div>
);
}}
} }
</RovingTabIndexWrapper>
);
}
@ -804,7 +804,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const label = _t("Show %(count)s more", { count: numMissing });
let showMoreText = (
<span className='mx_RoomSublist_showNButtonText'>
{label}
{ label }
</span>
);
if (this.props.isMinimized) showMoreText = null;
@ -816,9 +816,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
aria-label={label}
>
<span className='mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */}
{ /* set by CSS masking */ }
</span>
{showMoreText}
{ showMoreText }
</RovingAccessibleButton>
);
} else if (this.numTiles > this.layout.defaultVisibleTiles) {
@ -826,7 +826,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
const label = _t("Show less");
let showLessText = (
<span className='mx_RoomSublist_showNButtonText'>
{label}
{ label }
</span>
);
if (this.props.isMinimized) showLessText = null;
@ -838,9 +838,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
aria-label={label}
>
<span className='mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */}
{ /* set by CSS masking */ }
</span>
{showLessText}
{ showLessText }
</RovingAccessibleButton>
);
}
@ -891,9 +891,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
enable={handles}
>
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
{visibleTiles}
{ visibleTiles }
</div>
{showNButton}
{ showNButton }
</Resizable>
</React.Fragment>
);
@ -909,8 +909,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>
{this.renderHeader()}
{content}
{ this.renderHeader() }
{ content }
</div>
);
}

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();
@ -456,7 +467,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
isExpanded={!!this.state.notificationsMenuPosition}
tabIndex={isActive ? 0 : -1}
/>
{contextMenu}
{ contextMenu }
</React.Fragment>
);
}
@ -510,13 +521,18 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
label={lowPriorityLabel}
iconClassName="mx_RoomTile_iconArrowDown"
/>
{canInvite ? (
{ canInvite ? (
<IconizedContextMenuOption
onClick={this.onInviteClick}
label={_t("Invite People")}
iconClassName="mx_RoomTile_iconInvite"
/>
) : null}
) : null }
<IconizedContextMenuOption
onClick={this.onCopyRoomClick}
label={_t("Copy Room Link")}
iconClassName="mx_RoomTile_iconCopyLink"
/>
<IconizedContextMenuOption
onClick={this.onOpenRoomSettings}
label={_t("Settings")}
@ -541,7 +557,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
title={_t("Room options")}
isExpanded={!!this.state.generalMenuPosition}
/>
{contextMenu}
{ contextMenu }
</React.Fragment>
);
}
@ -605,7 +621,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
id={messagePreviewId(this.props.room.roomId)}
title={this.state.messagePreview}
>
{this.state.messagePreview}
{ this.state.messagePreview }
</div>
);
}
@ -619,9 +635,9 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
let nameContainer = (
<div className="mx_RoomTile_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name}
{ name }
</div>
{messagePreview}
{ messagePreview }
</div>
);
if (this.props.isMinimized) nameContainer = null;
@ -659,7 +675,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
return (
<React.Fragment>
<RovingTabIndexWrapper inputRef={this.roomTileRef}>
{({ onFocus, isActive, ref }) =>
{ ({ onFocus, isActive, ref }) =>
<Button
{...props}
onFocus={onFocus}
@ -673,11 +689,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
aria-selected={this.state.selected}
aria-describedby={ariaDescribedBy}
>
{roomAvatar}
{nameContainer}
{badge}
{this.renderGeneralMenu()}
{this.renderNotificationsMenu(isActive)}
{ roomAvatar }
{ nameContainer }
{ badge }
{ this.renderGeneralMenu() }
{ this.renderNotificationsMenu(isActive) }
</Button>
}
</RovingTabIndexWrapper>

View file

@ -72,26 +72,26 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
<div>
<div className="mx_RoomUpgradeWarningBar_body">
<p>
{_t(
{ _t(
"Upgrading this room will shut down the current instance of the room and create " +
"an upgraded room with the same name.",
)}
) }
</p>
<p>
{_t(
{ _t(
"<b>Warning</b>: Upgrading a room will <i>not automatically migrate room members " +
"to the new version of the room.</i> We'll post a link to the new room in the old " +
"version of the room - room members will have to click this link to join the new room.",
{}, {
"b": (sub) => <b>{sub}</b>,
"i": (sub) => <i>{sub}</i>,
"b": (sub) => <b>{ sub }</b>,
"i": (sub) => <i>{ sub }</i>,
},
)}
) }
</p>
</div>
<p className="mx_RoomUpgradeWarningBar_upgradelink">
<AccessibleButton onClick={this.onUpgradeClick}>
{_t("Upgrade this room to the recommended room version")}
{ _t("Upgrade this room to the recommended room version") }
</AccessibleButton>
</p>
</div>
@ -101,7 +101,7 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
doUpgradeWarnings = (
<div className="mx_RoomUpgradeWarningBar_body">
<p>
{_t("This room has already been upgraded.")}
{ _t("This room has already been upgraded.") }
</p>
</div>
);
@ -111,19 +111,19 @@ export default class RoomUpgradeWarningBar extends React.PureComponent {
<div className="mx_RoomUpgradeWarningBar">
<div className="mx_RoomUpgradeWarningBar_wrapped">
<div className="mx_RoomUpgradeWarningBar_header">
{_t(
{ _t(
"This room is running room version <roomVersion />, which this homeserver has " +
"marked as <i>unstable</i>.",
{},
{
"roomVersion": () => <code>{this.props.room.getVersion()}</code>,
"i": (sub) => <i>{sub}</i>,
"roomVersion": () => <code>{ this.props.room.getVersion() }</code>,
"i": (sub) => <i>{ sub }</i>,
},
)}
) }
</div>
{doUpgradeWarnings}
{ doUpgradeWarnings }
<div className="mx_RoomUpgradeWarningBar_small">
{_t("Only room administrators will see this warning")}
{ _t("Only room administrators will see this warning") }
</div>
</div>
</div>

View file

@ -100,7 +100,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
aria-checked={this.state.scope === SearchScope.Room}
role="radio"
>
{_t("This Room")}
{ _t("This Room") }
</AccessibleButton>
<AccessibleButton
className={allRoomsClasses}
@ -108,7 +108,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
aria-checked={this.state.scope === SearchScope.All}
role="radio"
>
{_t("All Rooms")}
{ _t("All Rooms") }
</AccessibleButton>
</div>
<div className="mx_SearchBar_input mx_textinput">
@ -119,7 +119,7 @@ export default class SearchBar extends React.Component<IProps, IState> {
placeholder={_t("Search…")}
onKeyDown={this.onSearchChange}
/>
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch} />
<AccessibleButton className={searchButtonClasses} onClick={this.onSearch} />
</div>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick} />
</div>

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

@ -441,7 +441,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
}
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
UNSAFE_componentWillMount() { // eslint-disable-line
const partCreator = new CommandPartCreator(this.props.room, this.context);
const parts = this.restoreStoredEditorState(partCreator) || [];
this.model = new EditorModel(parts, partCreator);
@ -514,13 +514,11 @@ export default class SendMessageComposer extends React.Component<IProps> {
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => {
const { clipboardData } = event;
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
// in the clipboard as well as the content being copied.
if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) {
// This actually not so much for 'files' as such (at time of writing
// neither chrome nor firefox let you paste a plain file copied
// from Finder) but more images copied from a different website
// / word processor etc.
// Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap
// in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore.
// We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer
// 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,
);

View file

@ -35,13 +35,15 @@ export default class SimpleRoomHeader extends React.Component {
let icon;
if (this.props.icon) {
icon = <img
className="mx_RoomHeader_icon" src={this.props.icon}
width="25" height="25"
className="mx_RoomHeader_icon"
src={this.props.icon}
width="25"
height="25"
/>;
}
return (
<div className="mx_RoomHeader mx_RoomHeader_wrapper" >
<div className="mx_RoomHeader mx_RoomHeader_wrapper">
<div className="mx_RoomHeader_simpleHeader">
{ icon }
{ this.props.title }

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();
}
@ -403,8 +403,7 @@ export default class Stickerpicker extends React.PureComponent {
onClick={this._onHideStickersClick}
active={this.state.showStickers.toString()}
title={_t("Hide Stickers")}
>
</AccessibleButton>;
/>;
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
stickerPicker = <ContextMenu
@ -431,8 +430,7 @@ export default class Stickerpicker extends React.PureComponent {
className="mx_MessageComposer_button mx_MessageComposer_stickers"
onClick={this._onShowStickersClick}
title={_t("Show Stickers")}
>
</AccessibleTooltipButton>;
/>;
}
return <React.Fragment>
{ stickersButton }

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;
@ -123,10 +123,10 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
if (this.state.canKick && this.state.invited) {
adminTools = (
<div className="mx_MemberInfo_container">
<h3>{_t("Admin Tools")}</h3>
<h3>{ _t("Admin Tools") }</h3>
<div className="mx_MemberInfo_buttons">
<AccessibleButton className="mx_MemberInfo_field" onClick={this.onKickClick}>
{_t("Revoke invite")}
{ _t("Revoke invite") }
</AccessibleButton>
</div>
</div>
@ -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} />
@ -150,16 +150,16 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
onClick={this.onCancel}
title={_t('Close')}
/>
<h2>{this.state.displayName}</h2>
<h2>{ this.state.displayName }</h2>
</div>
<div className="mx_MemberInfo_container">
<div className="mx_MemberInfo_profile">
<div className="mx_MemberInfo_profileField">
{_t("Invited by %(sender)s", { sender: this.state.senderName })}
{ _t("Invited by %(sender)s", { sender: this.state.senderName }) }
</div>
</div>
</div>
{adminTools}
{ adminTools }
</div>
);
}

View file

@ -32,14 +32,16 @@ export default class TopUnreadMessagesBar extends React.Component {
render() {
return (
<div className="mx_TopUnreadMessagesBar">
<AccessibleButton className="mx_TopUnreadMessagesBar_scrollUp"
<AccessibleButton
className="mx_TopUnreadMessagesBar_scrollUp"
title={_t('Jump to first unread message.')}
onClick={this.props.onScrollUpClick}>
</AccessibleButton>
<AccessibleButton className="mx_TopUnreadMessagesBar_markAsRead"
onClick={this.props.onScrollUpClick}
/>
<AccessibleButton
className="mx_TopUnreadMessagesBar_markAsRead"
title={_t('Mark all as read')}
onClick={this.props.onCloseClick}>
</AccessibleButton>
onClick={this.props.onCloseClick}
/>
</div>
);
}

View file

@ -20,7 +20,7 @@ import React, { ReactNode } from "react";
import {
RecordingState,
VoiceRecording,
} from "../../../voice/VoiceRecording";
} from "../../../audio/VoiceRecording";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import classNames from "classnames";
@ -68,37 +68,49 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
}
await this.state.recorder.stop();
const upload = await this.state.recorder.upload(this.props.room.roomId);
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
"body": "Voice message",
//"msgtype": "org.matrix.msc2516.voice",
"msgtype": MsgType.Audio,
"url": upload.mxc,
"file": upload.encrypted,
"info": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
// MSC1767 + Ideals of MSC2516 as MSC3245
// https://github.com/matrix-org/matrix-doc/pull/3245
"org.matrix.msc1767.text": "Voice message",
"org.matrix.msc1767.file": {
url: upload.mxc,
file: upload.encrypted,
name: "Voice message.ogg",
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
"org.matrix.msc1767.audio": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
try {
const upload = await this.state.recorder.upload(this.props.room.roomId);
// https://github.com/matrix-org/matrix-doc/pull/3246
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)),
},
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
});
// noinspection ES6MissingAwait - we don't care if it fails, it'll get queued.
MatrixClientPeg.get().sendMessage(this.props.room.roomId, {
"body": "Voice message",
//"msgtype": "org.matrix.msc2516.voice",
"msgtype": MsgType.Audio,
"url": upload.mxc,
"file": upload.encrypted,
"info": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
// MSC1767 + Ideals of MSC2516 as MSC3245
// https://github.com/matrix-org/matrix-doc/pull/3245
"org.matrix.msc1767.text": "Voice message",
"org.matrix.msc1767.file": {
url: upload.mxc,
file: upload.encrypted,
name: "Voice message.ogg",
mimetype: this.state.recorder.contentType,
size: this.state.recorder.contentLength,
},
"org.matrix.msc1767.audio": {
duration: Math.round(this.state.recorder.durationSeconds * 1000),
// https://github.com/matrix-org/matrix-doc/pull/3246
waveform: this.state.recorder.getPlayback().thumbnailWaveform.map(v => Math.round(v * 1024)),
},
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
});
} catch (e) {
console.error("Error sending/uploading voice message:", e);
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
title: _t('Upload Failed'),
description: _t("The voice message failed to upload."),
});
return; // don't dispose the recording so the user can retry, maybe
}
await this.disposeRecording();
}
@ -124,9 +136,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
Modal.createTrackedDialog('Microphone Access Error', '', ErrorDialog, {
title: _t("Unable to access your microphone"),
description: <>
<p>{_t(
<p>{ _t(
"We were unable to access your microphone. Please check your browser settings and try again.",
)}</p>
) }</p>
</>,
});
};
@ -139,9 +151,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
Modal.createTrackedDialog('No Microphone Error', '', ErrorDialog, {
title: _t("No microphone found"),
description: <>
<p>{_t(
<p>{ _t(
"We didn't find a microphone on your device. Please check your settings and try again.",
)}</p>
) }</p>
</>,
});
return;
@ -177,7 +189,6 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
if (!this.state.recorder) return null; // no recorder means we're not recording: no waveform
if (this.state.recordingPhase !== RecordingState.Started) {
// TODO: @@ TR: Should we disable this during upload? What does a failed upload look like?
return <RecordingPlayback playback={this.state.recorder.getPlayback()} />;
}
@ -224,9 +235,9 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
}
return (<>
{deleteButton}
{this.renderWaveformArea()}
{recordingInfo}
{ deleteButton }
{ this.renderWaveformArea() }
{ recordingInfo }
</>);
}
}

View file

@ -64,8 +64,8 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
}
componentDidUpdate(_, prevState) {
const wasVisible = this._isVisible(prevState);
const isVisible = this._isVisible(this.state);
const wasVisible = WhoIsTypingTile.isVisible(prevState);
const isVisible = WhoIsTypingTile.isVisible(this.state);
if (this.props.onShown && !wasVisible && isVisible) {
this.props.onShown();
} else if (this.props.onHidden && wasVisible && !isVisible) {
@ -83,12 +83,12 @@ export default class WhoIsTypingTile extends React.Component<IProps, IState> {
Object.values(this.state.delayedStopTypingTimers).forEach((t) => (t as Timer).abort());
}
private _isVisible(state: IState): boolean {
private static isVisible(state: IState): boolean {
return state.usersTyping.length !== 0 || Object.keys(state.delayedStopTypingTimers).length !== 0;
}
public isVisible = (): boolean => {
return this._isVisible(this.state);
return WhoIsTypingTile.isVisible(this.state);
};
private onRoomTimeline = (event: MatrixEvent, room: Room): void => {