From e23c41a775319c11d8dae66642750814e38cd4d5 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Wed, 10 Aug 2022 15:03:58 -0400 Subject: [PATCH 001/176] emotes show up in messages --- res/css/views/rooms/_EventTile.pcss | 4 +- src/HtmlUtils.tsx | 24 +- src/components/structures/MessagePanel.tsx | 114 +- .../views/dialogs/RoomSettingsDialog.tsx | 18 + src/components/views/messages/TextualBody.tsx | 27 +- .../views/room_settings/RoomEmoteSettings.tsx | 388 +++++ .../settings/tabs/room/EmoteSettingsTab.tsx | 75 + src/matrix-react-sdk - Shortcut.lnk | Bin 0 -> 1077 bytes .../room-list/previews/MessageEventPreview.ts | 2 +- src/utils/exportUtils/MessagePanel.tsx | 1343 +++++++++++++++++ 10 files changed, 1948 insertions(+), 47 deletions(-) create mode 100644 src/components/views/room_settings/RoomEmoteSettings.tsx create mode 100644 src/components/views/settings/tabs/room/EmoteSettingsTab.tsx create mode 100644 src/matrix-react-sdk - Shortcut.lnk create mode 100644 src/utils/exportUtils/MessagePanel.tsx diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 35cd87b1364..ccc88a0cf83 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -646,7 +646,9 @@ $left-gutter: 64px; font-size: inherit !important; } } - +.mx_Emote{ + height: 30px; +} .mx_EventTile_e2eIcon { position: relative; width: 14px; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 15fc2e075ef..3767e6840aa 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -396,6 +396,7 @@ interface IOpts { returnString?: boolean; forComposerQuote?: boolean; ref?: React.Ref; + emotes?: Dictionary; } export interface IOptsReturnNode extends IOpts { @@ -468,8 +469,8 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (opts.forComposerQuote) { sanitizeParams = composerSanitizeHtmlParams; } - let strippedBody: string; + let safeBody: string; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext try { @@ -486,11 +487,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody); strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody; bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody : plainBody); - const highlighter = safeHighlights?.length ? new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink) : null; - if (isFormattedBody) { if (highlighter) { // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying @@ -498,8 +497,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. + sanitizeParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join(''); + return highlighter.applyHighlights(safeText, safeHighlights).join('').replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; } @@ -524,7 +524,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // @ts-ignore - `e` can be an Element, not just a Node displayMode: e.name == 'div', output: "htmlAndMathml", - }); + }) }); safeBody = phtml.html(); } @@ -538,11 +538,12 @@ export function bodyToHtml(content: IContent, highlights: Optional, op delete sanitizeParams.textFilter; } - const contentBody = safeBody ?? strippedBody; + let contentBody = safeBody ?? strippedBody; + contentBody=contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); if (opts.returnString) { return contentBody; } - + let emojiBody = false; if (!opts.disableBigEmoji && bodyHasEmoji) { let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : ''; @@ -574,12 +575,17 @@ export function bodyToHtml(content: IContent, highlights: Optional, op 'mx_EventTile_bigEmoji': emojiBody, 'markdown-body': isHtmlMessage && !emojiBody, }); - + let tmp=strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + if(tmp!=strippedBody){ + safeBody=tmp; + } + + let emojiBodyElements: JSX.Element[]; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } - + return safeBody ? ; } interface IReadReceiptForUser { @@ -272,6 +277,7 @@ export default class MessagePanel extends React.Component { ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), hideSender: this.shouldHideSender(), + emotes: {}, }; // Cache these settings on mount since Settings is expensive to query, @@ -282,11 +288,20 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + + let emotesEvent=this.props.room.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let finalEmotes = {}; + for (let key in rawEmotes) { + this.state.emotes[":"+key+":"] = ""; + } + } componentDidMount() { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); + //this.props.room?.currentState.on(RoomStateEvent.Update, this.getEmotes); this.isMounted = true; } @@ -608,7 +623,6 @@ export default class MessagePanel extends React.Component { // we also need to figure out which is the last event we show which isn't // a local echo, to manage the read-marker. let lastShownEvent; - let lastShownNonLocalEchoIndex = -1; for (i = this.props.events.length-1; i >= 0; i--) { const mxEv = this.props.events[i]; @@ -768,37 +782,77 @@ export default class MessagePanel extends React.Component { isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); + // use txnId as key if available so that we don't remount during sending + if(mxEv.getType()==="m.room.message"){ + let messageText = mxEv.getContent().body; + //console.log(messageText); + let editedMessageText = messageText.replace(/:[\w+-]+:/, m => this.state.emotes[m] ? this.state.emotes[m] : m); + let m=[{body:messageText, + mimetype:"text/plain", + }, + { + body:editedMessageText, + mimetype:"text/html", + } + ]; + // if(mxEv.clearEvent){ + // console.log("clearevent",mxEv.getRoomId()); + // mxEv.clearEvent.content={ + // "format":"org.matrix.custom.html", + // "formatted_body":editedMessageText, + // "body":messageText, + // "msgtype":"m.text", + // "org.matrix.msc1767.message":m + // } + // } + // else{ + // console.log("no clearevent",mxEv); + // mxEv.content={ + // "format":"org.matrix.custom.html", + // "formatted_body":editedMessageText, + // "body":messageText, + // "msgtype":"m.text", + // "org.matrix.msc1767.message":m + // } + // } + + //mxEv.getContent().formatted_body = messageText; + //mxEv.clearEvent.content["org.matrix.msc1767.text"] = ""; + //mxEv.getContent().formatted_body = (hi); + //mxEv.getContent().format = "org.matrix.custom.html"; + + } ret.push( , + key={mxEv.getTxnId() || eventId} + as="li" + ref={this.collectEventTile.bind(this, eventId)} + alwaysShowTimestamps={this.props.alwaysShowTimestamps} + mxEvent={mxEv} + continuation={continuation} + isRedacted={mxEv.isRedacted()} + replacingEventId={mxEv.replacingEventId()} + editState={isEditing && this.props.editState} + onHeightChanged={this.onHeightChanged} + readReceipts={readReceipts} + readReceiptMap={this.readReceiptMap} + showUrlPreview={this.props.showUrlPreview} + checkUnmounting={this.isUnmounting} + eventSendStatus={mxEv.getAssociatedStatus()} + isTwelveHour={this.props.isTwelveHour} + permalinkCreator={this.props.permalinkCreator} + last={last} + lastInSection={lastInSection} + lastSuccessful={isLastSuccessful} + isSelectedEvent={highlight} + getRelationsForEvent={this.props.getRelationsForEvent} + showReactions={this.props.showReactions} + layout={this.props.layout} + showReadReceipts={this.props.showReadReceipts} + callEventGrouper={callEventGrouper} + hideSender={this.state.hideSender} + /> ); return ret; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index ce8d24cd3fd..23c7abe3fcd 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -26,6 +26,10 @@ import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab"; import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab"; import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; +<<<<<<< HEAD +======= +import EmoteSettingsTab from "../settings/tabs/room/EmoteSettingsTab"; +>>>>>>> ed46baa7ab (emotes show up in messages) import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; @@ -38,6 +42,10 @@ export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; +<<<<<<< HEAD +======= +export const ROOM_EMOTES_TAB = "ROOM_EMOTES_TAB"; +>>>>>>> ed46baa7ab (emotes show up in messages) export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; interface IProps { @@ -120,7 +128,17 @@ export default class RoomSettingsDialog extends React.Component this.props.onFinished(true)} />, "RoomSettingsNotifications", )); +<<<<<<< HEAD +======= + tabs.push(new Tab( + ROOM_NOTIFICATIONS_TAB, + _td("Emotes"), + "mx_RoomSettingsDialog_emotesIcon", + , + "RoomSettingsNotifications", + )); +>>>>>>> ed46baa7ab (emotes show up in messages) if (SettingsStore.getValue("feature_bridge_state")) { tabs.push(new Tab( ROOM_BRIDGES_TAB, diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 23ba901acdf..bf0f4d459b3 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -48,7 +48,8 @@ import RoomContext from "../../../contexts/RoomContext"; import AccessibleButton from '../elements/AccessibleButton'; import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from '../../../utils/Reply'; - +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { mediaFromMxc } from "../../../customisations/Media"; const MAX_HIGHLIGHT_LENGTH = 4096; interface IState { @@ -568,16 +569,23 @@ export default class TextualBody extends React.Component { const content = mxEvent.getContent(); let isNotice = false; let isEmote = false; - // only strip reply if this is the original replying event, edits thereafter do not have the fallback const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent); let body: ReactNode; + const client = MatrixClientPeg.get(); + const room = client.getRoom(mxEvent.getRoomId()); + let emotesEvent=room.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let finalEmotes = {}; + for (let key in rawEmotes) { + finalEmotes[":"+key+":"] = ""; + } if (SettingsStore.isEnabled("feature_extensible_events")) { const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; if (extev?.isEquivalentTo(M_MESSAGE)) { isEmote = isEventLike(extev.wireFormat, LegacyMsgType.Emote); isNotice = isEventLike(extev.wireFormat, LegacyMsgType.Notice); - body = HtmlUtils.bodyToHtml({ + body = (HtmlUtils.bodyToHtml({ body: extev.text, format: extev.html ? "org.matrix.custom.html" : undefined, formatted_body: extev.html, @@ -589,21 +597,25 @@ export default class TextualBody extends React.Component { stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, - }); + emotes: finalEmotes, + })); } } if (!body) { isEmote = content.msgtype === MsgType.Emote; isNotice = content.msgtype === MsgType.Notice; - body = HtmlUtils.bodyToHtml(content, this.props.highlights, { + body = (HtmlUtils.bodyToHtml(content, this.props.highlights, { disableBigEmoji: isEmote || !SettingsStore.getValue('TextualBody.enableBigEmoji'), // Part of Replies fallback support stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, - }); + })as any).replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m); + } + //console.log(body); + //body.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (this.props.replacingEventId) { body = <> { body } @@ -640,6 +652,8 @@ export default class TextualBody extends React.Component { />; } + //console.log(body.props.children); + //.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (isEmote) { return (
{
); } + //console.log(body) return (
{ body } diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx new file mode 100644 index 00000000000..60f5edbc268 --- /dev/null +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -0,0 +1,388 @@ +/* +Copyright 2019 New Vector Ltd + +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, { createRef } from 'react'; +import classNames from "classnames"; + +import { _t } from "../../../languageHandler"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import Field from "../elements/Field"; +import { mediaFromMxc } from "../../../customisations/Media"; +import AccessibleButton from "../elements/AccessibleButton"; +import AvatarSetting from "../settings/AvatarSetting"; +import { htmlSerializeFromMdIfNeeded } from '../../../editor/serialize'; +import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; +import { string } from 'prop-types'; + +interface IProps { + roomId: string; +} + +interface IState { + emotes: Dictionary; + EmoteFieldsTouched: Record; + newEmoteFileAdded: boolean, + newEmoteCodeAdded: boolean, + newEmoteCode: string, + newEmoteFile: File, + canAddEmote: boolean; + deleted: boolean; + deletedItems: Dictionary; + value: Dictionary; +} + +// TODO: Merge with EmoteSettings? +export default class RoomEmoteSettings extends React.Component { + private emoteUpload = createRef(); + private emoteCodeUpload = createRef(); + private emoteUploadImage = createRef(); + constructor(props: IProps) { + super(props); + + const client = MatrixClientPeg.get(); + const room = client.getRoom(props.roomId); + if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); + + let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + console.log(room.currentState); + let emotes: Dictionary; + emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let value = {}; + for (let emote in emotes) { + value[emote] = emote; + } + //TODO: Decrypt the shortcodes and emotes if they are encrypted + if (emotes) { + console.log(room.roomId); + console.log(room.name); + console.log(emotes); + } + //if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); + //emotes={} + this.state = { + emotes: emotes, + EmoteFieldsTouched: {}, + newEmoteFileAdded: false, + newEmoteCodeAdded: false, + newEmoteCode: "", + newEmoteFile: null, + deleted: false, + deletedItems: {}, + canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), + value: value, + }; + } + + private uploadEmoteClick = (): void => { + this.emoteUpload.current.click(); + }; + + + private isSaveEnabled = () => { + return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || this.state.deleted; + }; + + private cancelEmoteChanges = async (e: React.MouseEvent): Promise => { + e.stopPropagation(); + e.preventDefault(); + let value = {}; + if (this.state.deleted) { + for (let key in this.state.deletedItems) { + this.state.emotes[key] = this.state.deletedItems[key]; + value[key] = key; + } + } + document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { + value[(field as HTMLInputElement).id] = (field as HTMLInputElement).id; + }) + if (!this.isSaveEnabled()) return; + this.setState({ + EmoteFieldsTouched: {}, + newEmoteFileAdded: false, + newEmoteCodeAdded: false, + deleted: false, + deletedItems: {}, + value: value, + }); + + this.emoteUpload.current.value = ""; + this.emoteCodeUpload.current.value = ""; + + }; + private deleteEmote = (e: React.MouseEvent): Promise => { + e.stopPropagation(); + e.preventDefault(); + let cleanemotes = {} + let deletedItems = this.state.deletedItems; + let value = {} + //console.log(e.currentTarget.getAttribute("name")); + let id = e.currentTarget.getAttribute("id") + for (let emote in this.state.emotes) { + if (emote != id) { + cleanemotes[emote] = this.state.emotes[emote] + value[emote] = emote + } + else { + deletedItems[emote] = this.state.emotes[emote] + } + } + + this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }) + // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { + // field.setAttribute("value",(field as HTMLInputElement).id); + // field.setAttribute("defaultValue",(field as HTMLInputElement).id); + // }) + // for(let DOMid in this.state.emotes){ + // document.getElementById(DOMid).setAttribute("value",DOMid); + // } + return; + + } + private saveEmote = async (e: React.FormEvent): Promise => { + e.stopPropagation(); + e.preventDefault(); + + if (!this.isSaveEnabled()) return; + const client = MatrixClientPeg.get(); + const newState: Partial = {}; + const emotesMxcs = {}; + let value = {}; + // TODO: What do we do about errors? + + if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { + //const emotes = await client.uploadContent(this.state.emotes); + //TODO: Encrypt the shortcode and the image data before uploading + if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { + const newEmote = await client.uploadContent(this.state.newEmoteFile); + emotesMxcs[this.state.newEmoteCode] = newEmote; + value[this.state.newEmoteCode] = this.state.newEmoteCode; + } + if (this.state.emotes) { + for (let shortcode in this.state.emotes) { + if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) && (shortcode === this.state.newEmoteCode)) { + continue; + } + if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { + emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; + value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; + + // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { + // if((field as HTMLInputElement).name===shortcode){ + // (field as HTMLInputElement).name= this.state.EmoteFieldsTouched[shortcode]; + // } + // }) + } + + else { + emotesMxcs[shortcode] = this.state.emotes[shortcode]; + value[shortcode] = shortcode; + } + + }; + } + //console.log(emotesMxcs); + newState.value = value; + await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); + this.emoteUpload.current.value = ""; + this.emoteCodeUpload.current.value = ""; + newState.newEmoteFileAdded = false; + newState.newEmoteCodeAdded = false; + newState.EmoteFieldsTouched = {}; + newState.emotes = emotesMxcs; + newState.deleted = false; + newState.deletedItems = {}; + + /*newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); + newState.originalAvatarUrl = newState.avatarUrl; + newState.avatarFile = null;*/ + } /*else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { + await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, ''); + }*/ + this.setState(newState as IState); + }; + + + private onEmoteChange = (e: React.ChangeEvent): void => { + const id = e.target.getAttribute("id"); + //e.target.setAttribute("value", e.target.value); + //const newEmotes = { ...this.state.emotes, [value]: value }; + //let newState=this.state.emotes; + let b = this.state.value + b[id] = e.target.value; + this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); + } + + private onEmoteFileAdd = (e: React.ChangeEvent): void => { + if (!e.target.files || !e.target.files.length) { + this.setState({ + newEmoteFileAdded: false, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); + return; + } + + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = (ev) => { + this.setState({ + newEmoteFileAdded: true, + newEmoteFile: file, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); + this.emoteUploadImage.current.src = URL.createObjectURL(file); + }; + reader.readAsDataURL(file); + }; + private onEmoteCodeAdd = (e: React.ChangeEvent): void => { + if (e.target.value.length > 0) { + this.setState({ + newEmoteCodeAdded: true, + newEmoteCode: e.target.value, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); + } + else { + this.setState({ + newEmoteCodeAdded: false, + }); + } + } + + public render(): JSX.Element { + let emoteSettingsButtons; + if ( + this.state.canAddEmote + ) { + emoteSettingsButtons = ( +
+ + {_t("Cancel")} + + + {_t("Save")} + +
+ ); + } + + let existingEmotes = []; + if (this.state.emotes) { + for (let emotecode in this.state.emotes) { + existingEmotes.push( +
  • + + +
    + + {_t("Delete")} + +
    +
  • + ) + } + + + } + + + let emoteUploadButton; + if (this.state.canAddEmote) { + emoteUploadButton = ( +
    + + {_t("Upload Emote")} + +
    + ); + } + + + return ( + +
    + +
  • + + { + this.state.newEmoteFileAdded ? + : null + } + + {emoteUploadButton} +
  • + { + existingEmotes + } + {emoteSettingsButtons} +
    + ); + } +} diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx new file mode 100644 index 00000000000..864fff3171e --- /dev/null +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -0,0 +1,75 @@ +/* +Copyright 2019 New Vector Ltd + +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, { ContextType } from 'react'; + +import { _t } from "../../../../../languageHandler"; +import RoomEmoteSettings from "../../../room_settings/RoomEmoteSettings"; +import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; +import dis from "../../../../../dispatcher/dispatcher"; +import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings"; +import AliasSettings from "../../../room_settings/AliasSettings"; +import PosthogTrackers from "../../../../../PosthogTrackers"; + +interface IProps { + roomId: string; +} + +interface IState { + isRoomPublished: boolean; +} + +export default class EmoteRoomSettingsTab extends React.Component { + public static contextType = MatrixClientContext; + context: ContextType; + + constructor(props: IProps, context: ContextType) { + super(props, context); + + this.state = { + isRoomPublished: false, // loaded async + }; + } + + + public render(): JSX.Element { + const client = this.context; + const room = client.getRoom(this.props.roomId); + + /*const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this + const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client); + const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');*/ + + const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ? + : + null; + + + return ( +
    + +
    {_t("Emotes")}
    +
    + +
    + +
    + ); + } +} diff --git a/src/matrix-react-sdk - Shortcut.lnk b/src/matrix-react-sdk - Shortcut.lnk new file mode 100644 index 0000000000000000000000000000000000000000..cb57c92d572caad0631ea61e6b0fc0396a6dc407 GIT binary patch literal 1077 zcmb7CT}V@57=F$Yn&rh-CM7yYFfqo)41*d7!N%rAOi3j#&V^$;v(wy9=bVXn5e7x9 z>>^VlqR6nMG9sd)i)7ecS3@b*D2SjNL&Teqg6jDu6Z)a$dw9P0J@5I>^S0cKbj*MD6K&)qFZkK?DubF&3$Vr#MI$+P|2AKXG@dzCkj&p-F79C! zYuiDas8)+4S~s{YPzIHRk~E^qkZxpRF?69CRpcmdahHR?dIf_}kRXyU5Tzf2&M1Qz zAHW9fmuiWyp{c_zO$kQ~mmZA+M>kb3fiT%Ms2GNeUg3L$85gTYLAsKN$LSjgp``FG zLf8NywAn&5)cp7&rU9Qv%fBfj|Jl0++H!A1*5 z6KYb`4J~Y>G``P|TEF##9KNg1PwseKbevy!H}L+-u4U^)Y%RM`JeRBV{S%K-P5!gw zYVGdoUiQ2w`j8FItmSu3>#@Bm^HBm#gp>I9Q0!=d4_?UVrDzI8)ES2;X_{k8GWH`y zQ8OE3)v-^|mu!TqXu`AVc(xL(x7=Pe^^e{7JbQroZDNOLLTFrhQYg_@m4B*X5MUSL zLfLtFQjElf5)ulVB1GjoCn7EFlMB`-Z; CONTINUATION_MAX_INTERVAL) return false; + + // As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa + if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false; + + // Some events should appear as continuations from previous events of different types. + if (mxEvent.getType() !== prevEvent.getType() && + (!continuedTypes.includes(mxEvent.getType() as EventType) || + !continuedTypes.includes(prevEvent.getType() as EventType))) return false; + + // Check if the sender is the same and hasn't changed their displayname/avatar between these events + if (mxEvent.sender.userId !== prevEvent.sender.userId || + mxEvent.sender.name !== prevEvent.sender.name || + mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; + + // Thread summaries in the main timeline should break up a continuation on both sides + if (threadsEnabled && + (hasThreadSummary(mxEvent) || hasThreadSummary(prevEvent)) && + timelineRenderingType !== TimelineRenderingType.Thread + ) { + return false; + } + + // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile + if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false; + + return true; +} + +interface IProps { + // the list of MatrixEvents to display + events: MatrixEvent[]; + + // true to give the component a 'display: none' style. + hidden?: boolean; + + // true to show a spinner at the top of the timeline to indicate + // back-pagination in progress + backPaginating?: boolean; + + // true to show a spinner at the end of the timeline to indicate + // forward-pagination in progress + forwardPaginating?: boolean; + + // ID of an event to highlight. If undefined, no event will be highlighted. + highlightedEventId?: string; + + // The room these events are all in together, if any. + // (The notification panel won't have a room here, for example.) + room?: Room; + + // Should we show URL Previews + showUrlPreview?: boolean; + + // event after which we should show a read marker + readMarkerEventId?: string; + + // whether the read marker should be visible + readMarkerVisible?: boolean; + + // the userid of our user. This is used to suppress the read marker + // for pending messages. + ourUserId?: string; + + // whether the timeline can visually go back any further + canBackPaginate?: boolean; + + // whether to show read receipts + showReadReceipts?: boolean; + + // true if updates to the event list should cause the scroll panel to + // scroll down when we are at the bottom of the window. See ScrollPanel + // for more details. + stickyBottom?: boolean; + + // className for the panel + className: string; + + // show twelve hour timestamps + isTwelveHour?: boolean; + + // show timestamps always + alwaysShowTimestamps?: boolean; + + // whether to show reactions for an event + showReactions?: boolean; + + // which layout to use + layout?: Layout; + + resizeNotifier: ResizeNotifier; + permalinkCreator?: RoomPermalinkCreator; + editState?: EditorStateTransfer; + + // callback which is called when the panel is scrolled. + onScroll?(event: Event): void; + + // callback which is called when more content is needed. + onFillRequest?(backwards: boolean): Promise; + + // helper function to access relations for an event + onUnfillRequest?(backwards: boolean, scrollToken: string): void; + + getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations; + + hideThreadedMessages?: boolean; + disableGrouping?: boolean; + + callEventGroupers: Map; +} + +interface IState { + ghostReadMarkers: string[]; + showTypingNotifications: boolean; + hideSender: boolean; +} + +interface IReadReceiptForUser { + lastShownEventId: string; + receipt: IReadReceiptProps; +} + +/* (almost) stateless UI component which builds the event tiles in the room timeline. + */ +export default class MessagePanel extends React.Component { + static contextType = RoomContext; + public context!: React.ContextType; + + static defaultProps = { + disableGrouping: false, + }; + + // opaque readreceipt info for each userId; used by ReadReceiptMarker + // to manage its animations + private readonly readReceiptMap: { [userId: string]: IReadReceiptInfo } = {}; + + // Track read receipts by event ID. For each _shown_ event ID, we store + // the list of read receipts to display: + // [ + // { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // ] + // This is recomputed on each render. It's only stored on the component + // for ease of passing the data around since it's computed in one pass + // over all events. + private readReceiptsByEvent: Record = {}; + + // Track read receipts by user ID. For each user ID we've ever shown a + // a read receipt for, we store an object: + // { + // lastShownEventId: string, + // receipt: { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // } + // so that we can always keep receipts displayed by reverting back to + // the last shown event for that user ID when needed. This may feel like + // it duplicates the receipt storage in the room, but at this layer, we + // are tracking _shown_ event IDs, which the JS SDK knows nothing about. + // This is recomputed on each render, using the data from the previous + // render as our fallback for any user IDs we can't match a receipt to a + // displayed event in the current render cycle. + private readReceiptsByUserId: Record = {}; + + private readonly _showHiddenEvents: boolean; + private readonly threadsEnabled: boolean; + private isMounted = false; + + private readMarkerNode = createRef(); + private whoIsTyping = createRef(); + private scrollPanel = createRef(); + + private readonly showTypingNotificationsWatcherRef: string; + private eventTiles: Record = {}; + + // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. + public grouperKeyMap = new WeakMap(); + + constructor(props, context) { + super(props, context); + + this.state = { + // previous positions the read marker has been in, so we can + // display 'ghost' read markers that are animating away + ghostReadMarkers: [], + showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), + hideSender: this.shouldHideSender(), + }; + + // Cache these settings on mount since Settings is expensive to query, + // and we check this in a hot code path. This is also cached in our + // RoomContext, however we still need a fallback for roomless MessagePanels. + this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); + this.threadsEnabled = SettingsStore.getValue("feature_thread"); + + this.showTypingNotificationsWatcherRef = + SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + } + + componentDidMount() { + this.calculateRoomMembersCount(); + this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); + this.isMounted = true; + } + + componentWillUnmount() { + this.isMounted = false; + this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); + SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.layout !== this.props.layout) { + this.calculateRoomMembersCount(); + } + + if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { + const ghostReadMarkers = this.state.ghostReadMarkers; + ghostReadMarkers.push(prevProps.readMarkerEventId); + this.setState({ + ghostReadMarkers, + }); + } + + const pendingEditItem = this.pendingEditItem; + if (!this.props.editState && this.props.room && pendingEditItem) { + const event = this.props.room.findEventById(pendingEditItem); + defaultDispatcher.dispatch({ + action: Action.EditEvent, + event: !event?.isRedacted() ? event : null, + timelineRenderingType: this.context.timelineRenderingType, + }); + } + } + + private shouldHideSender(): boolean { + return this.props.room?.getInvitedAndJoinedMemberCount() <= 2 && this.props.layout === Layout.Bubble; + } + + private calculateRoomMembersCount = (): void => { + this.setState({ + hideSender: this.shouldHideSender(), + }); + }; + + private onShowTypingNotificationsChange = (): void => { + this.setState({ + showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), + }); + }; + + /* get the DOM node representing the given event */ + public getNodeForEventId(eventId: string): HTMLElement { + if (!this.eventTiles) { + return undefined; + } + + return this.eventTiles[eventId]?.ref?.current; + } + + public getTileForEventId(eventId: string): UnwrappedEventTile { + if (!this.eventTiles) { + return undefined; + } + return this.eventTiles[eventId]; + } + + /* return true if the content is fully scrolled down right now; else false. + */ + public isAtBottom(): boolean { + return this.scrollPanel.current?.isAtBottom(); + } + + /* get the current scroll state. See ScrollPanel.getScrollState for + * details. + * + * returns null if we are not mounted. + */ + public getScrollState(): IScrollState { + return this.scrollPanel.current?.getScrollState() ?? null; + } + + // returns one of: + // + // null: there is no read marker + // -1: read marker is above the window + // 0: read marker is within the window + // +1: read marker is below the window + public getReadMarkerPosition(): number { + const readMarker = this.readMarkerNode.current; + const messageWrapper = this.scrollPanel.current; + + if (!readMarker || !messageWrapper) { + return null; + } + + const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect(); + const readMarkerRect = readMarker.getBoundingClientRect(); + + // the read-marker pretends to have zero height when it is actually + // two pixels high; +2 here to account for that. + if (readMarkerRect.bottom + 2 < wrapperRect.top) { + return -1; + } else if (readMarkerRect.top < wrapperRect.bottom) { + return 0; + } else { + return 1; + } + } + + /* jump to the top of the content. + */ + public scrollToTop(): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToTop(); + } + } + + /* jump to the bottom of the content. + */ + public scrollToBottom(): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToBottom(); + } + } + + /** + * Page up/down. + * + * @param {number} mult: -1 to page up, +1 to page down + */ + public scrollRelative(mult: number): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollRelative(mult); + } + } + + /** + * Scroll up/down in response to a scroll key + * + * @param {KeyboardEvent} ev: the keyboard event to handle + */ + public handleScrollKey(ev: KeyboardEvent): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.handleScrollKey(ev); + } + } + + /* jump to the given event id. + * + * offsetBase gives the reference point for the pixelOffset. 0 means the + * top of the container, 1 means the bottom, and fractional values mean + * somewhere in the middle. If omitted, it defaults to 0. + * + * pixelOffset gives the number of pixels *above* the offsetBase that the + * node (specifically, the bottom of it) will be positioned. If omitted, it + * defaults to 0. + */ + public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); + } + } + + public scrollToEventIfNeeded(eventId: string): void { + const node = this.getNodeForEventId(eventId); + if (node) { + node.scrollIntoView({ + block: "nearest", + behavior: "instant", + }); + } + } + + private isUnmounting = (): boolean => { + return !this.isMounted; + }; + + public get showHiddenEvents(): boolean { + return this.context?.showHiddenEvents ?? this._showHiddenEvents; + } + + // TODO: Implement granular (per-room) hide options + public shouldShowEvent(mxEv: MatrixEvent, forceHideEvents = false): boolean { + if (this.props.hideThreadedMessages && this.threadsEnabled && this.props.room) { + const { shouldLiveInRoom } = this.props.room.eventShouldLiveIn(mxEv, this.props.events); + if (!shouldLiveInRoom) { + return false; + } + } + + if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) { + return false; // ignored = no show (only happens if the ignore happens after an event was received) + } + + if (this.showHiddenEvents && !forceHideEvents) { + return true; + } + + if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) { + return false; // no tile = no show + } + + // Always show highlighted event + if (this.props.highlightedEventId === mxEv.getId()) return true; + + return !shouldHideEvent(mxEv, this.context); + } + + public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode { + const visible = !isLastEvent && this.props.readMarkerVisible; + + if (this.props.readMarkerEventId === eventId) { + let hr; + // if the read marker comes at the end of the timeline (except + // for local echoes, which are excluded from RMs, because they + // don't have useful event ids), we don't want to show it, but + // we still want to create the
  • for it so that the + // algorithms which depend on its position on the screen aren't + // confused. + if (visible) { + hr =
    ; + } + + return ( +
  • + { hr } +
  • + ); + } else if (this.state.ghostReadMarkers.includes(eventId)) { + // We render 'ghost' read markers in the DOM while they + // transition away. This allows the actual read marker + // to be in the right place straight away without having + // to wait for the transition to finish. + // There are probably much simpler ways to do this transition, + // possibly using react-transition-group which handles keeping + // elements in the DOM whilst they transition out, although our + // case is a little more complex because only some of the items + // transition (ie. the read markers do but the event tiles do not) + // and TransitionGroup requires that all its children are Transitions. + const hr =
    ; + + // give it a key which depends on the event id. That will ensure that + // we get a new DOM node (restarting the animation) when the ghost + // moves to a different event. + return ( +
  • + { hr } +
  • + ); + } + + return null; + } + + private collectGhostReadMarker = (node: HTMLElement): void => { + if (node) { + // now the element has appeared, change the style which will trigger the CSS transition + requestAnimationFrame(() => { + node.style.width = '10%'; + node.style.opacity = '0'; + }); + } + }; + + private onGhostTransitionEnd = (ev: TransitionEvent): void => { + // we can now clean up the ghost element + const finishedEventId = (ev.target as HTMLElement).dataset.eventid; + this.setState({ + ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId), + }); + }; + + private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } { + const nextEvent = i < arr.length - 1 + ? arr[i + 1] + : null; + + // The next event with tile is used to to determine the 'last successful' flag + // when rendering the tile. The shouldShowEvent function is pretty quick at what + // it does, so this should have no significant cost even when a room is used for + // not-chat purposes. + const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e)); + + return { nextEvent, nextTile }; + } + + private get pendingEditItem(): string | undefined { + if (!this.props.room) { + return undefined; + } + + try { + return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType)); + } catch (err) { + logger.error(err); + return undefined; + } + } + + private getEventTiles(): ReactNode[] { + let i; + + // first figure out which is the last event in the list which we're + // actually going to show; this allows us to behave slightly + // differently for the last event in the list. (eg show timestamp) + // + // we also need to figure out which is the last event we show which isn't + // a local echo, to manage the read-marker. + let lastShownEvent; + + let lastShownNonLocalEchoIndex = -1; + for (i = this.props.events.length-1; i >= 0; i--) { + const mxEv = this.props.events[i]; + if (!this.shouldShowEvent(mxEv)) { + continue; + } + + if (lastShownEvent === undefined) { + lastShownEvent = mxEv; + } + + if (mxEv.status) { + // this is a local echo + continue; + } + + lastShownNonLocalEchoIndex = i; + break; + } + + const ret = []; + + let prevEvent = null; // the last event we showed + + // Note: the EventTile might still render a "sent/sending receipt" independent of + // this information. When not providing read receipt information, the tile is likely + // to assume that sent receipts are to be shown more often. + this.readReceiptsByEvent = {}; + if (this.props.showReadReceipts) { + this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(); + } + + let grouper: BaseGrouper = null; + + for (i = 0; i < this.props.events.length; i++) { + const mxEv = this.props.events[i]; + const eventId = mxEv.getId(); + const last = (mxEv === lastShownEvent); + const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i); + + if (grouper) { + if (grouper.shouldGroup(mxEv)) { + grouper.add(mxEv); + continue; + } else { + // not part of group, so get the group tiles, close the + // group, and continue like a normal event + ret.push(...grouper.getTiles()); + prevEvent = grouper.getNewPrevEvent(); + grouper = null; + } + } + + for (const Grouper of groupers) { + if (Grouper.canStartGroup(this, mxEv) && !this.props.disableGrouping) { + grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); + break; // break on first grouper + } + } + + if (!grouper) { + if (this.shouldShowEvent(mxEv)) { + // make sure we unpack the array returned by getTilesForEvent, + // otherwise React will auto-generate keys, and we will end up + // replacing all the DOM elements every time we paginate. + ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, false, nextEvent, nextTile)); + prevEvent = mxEv; + } + + const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); + if (readMarker) ret.push(readMarker); + } + } + + if (grouper) { + ret.push(...grouper.getTiles()); + } + + return ret; + } + + public getTilesForEvent( + prevEvent: MatrixEvent, + mxEv: MatrixEvent, + last = false, + isGrouped = false, + nextEvent?: MatrixEvent, + nextEventWithTile?: MatrixEvent, + ): ReactNode[] { + const ret = []; + + const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId(); + // local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators. + let ts1 = mxEv.getTs(); + let eventDate = mxEv.getDate(); + if (mxEv.status) { + eventDate = new Date(); + ts1 = eventDate.getTime(); + } + + // do we need a date separator since the last event? + const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); + if (wantsDateSeparator && !isGrouped && this.props.room) { + const dateSeparator = ( +
  • + +
  • + ); + ret.push(dateSeparator); + } + + let lastInSection = true; + if (nextEventWithTile) { + const nextEv = nextEventWithTile; + const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date()); + lastInSection = willWantDateSeparator || + mxEv.getSender() !== nextEv.getSender() || + getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage || + !shouldFormContinuation( + mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, + ); + } + + // is this a continuation of the previous message? + const continuation = !wantsDateSeparator && + shouldFormContinuation( + prevEvent, mxEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, + ); + + const eventId = mxEv.getId(); + const highlight = (eventId === this.props.highlightedEventId); + + const readReceipts = this.readReceiptsByEvent[eventId]; + + let isLastSuccessful = false; + const isSentState = s => !s || s === 'sent'; + const isSent = isSentState(mxEv.getAssociatedStatus()); + const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); + if (!hasNextEvent && isSent) { + isLastSuccessful = true; + } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { + isLastSuccessful = true; + } + + // This is a bit nuanced, but if our next event is hidden but a future event is not + // hidden then we're not the last successful. + if ( + nextEventWithTile && + nextEventWithTile !== nextEvent && + isSentState(nextEventWithTile.getAssociatedStatus()) + ) { + isLastSuccessful = false; + } + + // We only want to consider "last successful" if the event is sent by us, otherwise of course + // it's successful: we received it. + isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); + + const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); + // use txnId as key if available so that we don't remount during sending + ret.push( + , + ); + + return ret; + } + + public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean { + if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { + return false; + } + if (prevEvent == null) { + // first event in the panel: depends if we could back-paginate from + // here. + return !this.props.canBackPaginate; + } + return wantsDateSeparator(prevEvent.getDate(), nextEventDate); + } + + // Get a list of read receipts that should be shown next to this event + // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. + private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] { + const myUserId = MatrixClientPeg.get().credentials.userId; + + // get list of read receipts, sorted most recent first + const { room } = this.props; + if (!room) { + return null; + } + const receipts: IReadReceiptProps[] = []; + room.getReceiptsForEvent(event).forEach((r) => { + if ( + !r.userId || + !isSupportedReceiptType(r.type) || + r.userId === myUserId + ) { + return; // ignore non-read receipts and receipts from self. + } + if (MatrixClientPeg.get().isUserIgnored(r.userId)) { + return; // ignore ignored users + } + const member = room.getMember(r.userId); + receipts.push({ + userId: r.userId, + roomMember: member, + ts: r.data ? r.data.ts : 0, + }); + }); + return receipts; + } + + // Get an object that maps from event ID to a list of read receipts that + // should be shown next to that event. If a hidden event has read receipts, + // they are folded into the receipts of the last shown event. + private getReadReceiptsByShownEvent(): Record { + const receiptsByEvent = {}; + const receiptsByUserId = {}; + + let lastShownEventId; + for (const event of this.props.events) { + if (this.shouldShowEvent(event)) { + lastShownEventId = event.getId(); + } + if (!lastShownEventId) { + continue; + } + + const existingReceipts = receiptsByEvent[lastShownEventId] || []; + const newReceipts = this.getReadReceiptsForEvent(event); + receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); + + // Record these receipts along with their last shown event ID for + // each associated user ID. + for (const receipt of newReceipts) { + receiptsByUserId[receipt.userId] = { + lastShownEventId, + receipt, + }; + } + } + + // It's possible in some cases (for example, when a read receipt + // advances before we have paginated in the new event that it's marking + // received) that we can temporarily not have a matching event for + // someone which had one in the last. By looking through our previous + // mapping of receipts by user ID, we can cover recover any receipts + // that would have been lost by using the same event ID from last time. + for (const userId in this.readReceiptsByUserId) { + if (receiptsByUserId[userId]) { + continue; + } + const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId]; + const existingReceipts = receiptsByEvent[lastShownEventId] || []; + receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); + receiptsByUserId[userId] = { lastShownEventId, receipt }; + } + this.readReceiptsByUserId = receiptsByUserId; + + // After grouping receipts by shown events, do another pass to sort each + // receipt list. + for (const eventId in receiptsByEvent) { + receiptsByEvent[eventId].sort((r1, r2) => { + return r2.ts - r1.ts; + }); + } + + return receiptsByEvent; + } + + private collectEventTile = (eventId: string, node: UnwrappedEventTile): void => { + this.eventTiles[eventId] = node; + }; + + // once dynamic content in the events load, make the scrollPanel check the + // scroll offsets. + public onHeightChanged = (): void => { + const scrollPanel = this.scrollPanel.current; + if (scrollPanel) { + scrollPanel.checkScroll(); + } + }; + + private onTypingShown = (): void => { + const scrollPanel = this.scrollPanel.current; + // this will make the timeline grow, so checkScroll + scrollPanel.checkScroll(); + if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { + scrollPanel.preventShrinking(); + } + }; + + private onTypingHidden = (): void => { + const scrollPanel = this.scrollPanel.current; + if (scrollPanel) { + // as hiding the typing notifications doesn't + // update the scrollPanel, we tell it to apply + // the shrinking prevention once the typing notifs are hidden + scrollPanel.updatePreventShrinking(); + // order is important here as checkScroll will scroll down to + // reveal added padding to balance the notifs disappearing. + scrollPanel.checkScroll(); + } + }; + + public updateTimelineMinHeight(): void { + const scrollPanel = this.scrollPanel.current; + + if (scrollPanel) { + const isAtBottom = scrollPanel.isAtBottom(); + const whoIsTyping = this.whoIsTyping.current; + const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); + // when messages get added to the timeline, + // but somebody else is still typing, + // update the min-height, so once the last + // person stops typing, no jumping occurs + if (isAtBottom && isTypingVisible) { + scrollPanel.preventShrinking(); + } + } + } + + public onTimelineReset(): void { + const scrollPanel = this.scrollPanel.current; + if (scrollPanel) { + scrollPanel.clearPreventShrinking(); + } + } + + render() { + let topSpinner; + let bottomSpinner; + if (this.props.backPaginating) { + topSpinner =
  • ; + } + if (this.props.forwardPaginating) { + bottomSpinner =
  • ; + } + + const style = this.props.hidden ? { display: 'none' } : {}; + + let whoIsTyping; + if (this.props.room && + this.state.showTypingNotifications && + this.context.timelineRenderingType === TimelineRenderingType.Room + ) { + whoIsTyping = ( + ); + } + + let ircResizer = null; + if (this.props.layout == Layout.IRC) { + ircResizer = ; + } + + const classes = classNames(this.props.className, { + "mx_MessagePanel_narrow": this.context.narrow, + }); + + return ( + + + { topSpinner } + { this.getEventTiles() } + { whoIsTyping } + { bottomSpinner } + + + ); + } +} + +abstract class BaseGrouper { + static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true; + + public events: MatrixEvent[] = []; + // events that we include in the group but then eject out and place above the group. + public ejectedEvents: MatrixEvent[] = []; + public readMarker: ReactNode; + + constructor( + public readonly panel: MessagePanel, + public readonly event: MatrixEvent, + public readonly prevEvent: MatrixEvent, + public readonly lastShownEvent: MatrixEvent, + public readonly nextEvent?: MatrixEvent, + public readonly nextEventTile?: MatrixEvent, + ) { + this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent); + } + + public abstract shouldGroup(ev: MatrixEvent): boolean; + public abstract add(ev: MatrixEvent): void; + public abstract getTiles(): ReactNode[]; + public abstract getNewPrevEvent(): MatrixEvent; +} + +/* Grouper classes determine when events can be grouped together in a summary. + * Groupers should have the following methods: + * - canStartGroup (static): determines if a new group should be started with the + * given event + * - shouldGroup: determines if the given event should be added to an existing group + * - add: adds an event to an existing group (should only be called if shouldGroup + * return true) + * - getTiles: returns the tiles that represent the group + * - getNewPrevEvent: returns the event that should be used as the new prevEvent + * when determining things such as whether a date separator is necessary + */ + +// Wrap initial room creation events into a GenericEventListSummary +// Grouping only events sent by the same user that sent the `m.room.create` and only until +// the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event +class CreationGrouper extends BaseGrouper { + static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { + return ev.getType() === EventType.RoomCreate; + }; + + public shouldGroup(ev: MatrixEvent): boolean { + const panel = this.panel; + const createEvent = this.event; + if (!panel.shouldShowEvent(ev)) { + return true; + } + if (panel.wantsDateSeparator(this.event, ev.getDate())) { + return false; + } + if (ev.getType() === EventType.RoomMember + && (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) { + return false; + } + // beacons are not part of room creation configuration + // should be shown in timeline + if (M_BEACON_INFO.matches(ev.getType())) { + return false; + } + if (ev.isState() && ev.getSender() === createEvent.getSender()) { + return true; + } + + return false; + } + + public add(ev: MatrixEvent): void { + const panel = this.panel; + this.readMarker = this.readMarker || panel.readMarkerForEvent( + ev.getId(), + ev === this.lastShownEvent, + ); + if (!panel.shouldShowEvent(ev)) { + return; + } + if (ev.getType() === EventType.RoomEncryption) { + this.ejectedEvents.push(ev); + } else { + this.events.push(ev); + } + } + + public getTiles(): ReactNode[] { + // If we don't have any events to group, don't even try to group them. The logic + // below assumes that we have a group of events to deal with, but we might not if + // the events we were supposed to group were redacted. + if (!this.events || !this.events.length) return []; + + const panel = this.panel; + const ret: ReactNode[] = []; + const isGrouped = true; + const createEvent = this.event; + const lastShownEvent = this.lastShownEvent; + + if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) { + const ts = createEvent.getTs(); + ret.push( +
  • , + ); + } + + // If this m.room.create event should be shown (room upgrade) then show it before the summary + if (panel.shouldShowEvent(createEvent)) { + // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered + ret.push(...panel.getTilesForEvent(createEvent, createEvent)); + } + + for (const ejected of this.ejectedEvents) { + ret.push(...panel.getTilesForEvent( + createEvent, ejected, createEvent === lastShownEvent, isGrouped, + )); + } + + const eventTiles = this.events.map((e) => { + // In order to prevent DateSeparators from appearing in the expanded form + // of GenericEventListSummary, render each member event as if the previous + // one was itself. This way, the timestamp of the previous event === the + // timestamp of the current event, and no DateSeparator is inserted. + return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); + }).reduce((a, b) => a.concat(b), []); + // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one + const ev = this.events[this.events.length - 1]; + + let summaryText: string; + const roomId = ev.getRoomId(); + const creator = ev.sender ? ev.sender.name : ev.getSender(); + if (DMRoomMap.shared().getUserIdForRoomId(roomId)) { + summaryText = _t("%(creator)s created this DM.", { creator }); + } else { + summaryText = _t("%(creator)s created and configured the room.", { creator }); + } + + ret.push(); + + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + public getNewPrevEvent(): MatrixEvent { + return this.event; + } +} + +// Wrap consecutive grouped events in a ListSummary +class MainGrouper extends BaseGrouper { + static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { + if (!panel.shouldShowEvent(ev)) return false; + + if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { + return true; + } + + if (ev.isRedacted()) { + return true; + } + + if (panel.showHiddenEvents && !panel.shouldShowEvent(ev, true)) { + return true; + } + + return false; + }; + + constructor( + public readonly panel: MessagePanel, + public readonly event: MatrixEvent, + public readonly prevEvent: MatrixEvent, + public readonly lastShownEvent: MatrixEvent, + nextEvent: MatrixEvent, + nextEventTile: MatrixEvent, + ) { + super(panel, event, prevEvent, lastShownEvent, nextEvent, nextEventTile); + this.events = [event]; + } + + public shouldGroup(ev: MatrixEvent): boolean { + if (!this.panel.shouldShowEvent(ev)) { + // absorb hidden events so that they do not break up streams of messages & redaction events being grouped + return true; + } + if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { + return false; + } + if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { + return true; + } + if (ev.isRedacted()) { + return true; + } + if (this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev, true)) { + return true; + } + return false; + } + + public add(ev: MatrixEvent): void { + if (ev.getType() === EventType.RoomMember) { + // We can ignore any events that don't actually have a message to display + if (!hasText(ev, this.panel.showHiddenEvents)) return; + } + this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent); + if (!this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev)) { + // absorb hidden events to not split the summary + return; + } + this.events.push(ev); + } + + private generateKey(): string { + return "eventlistsummary-" + this.events[0].getId(); + } + + public getTiles(): ReactNode[] { + // If we don't have any events to group, don't even try to group them. The logic + // below assumes that we have a group of events to deal with, but we might not if + // the events we were supposed to group were redacted. + if (!this.events?.length) return []; + + const isGrouped = true; + const panel = this.panel; + const lastShownEvent = this.lastShownEvent; + const ret: ReactNode[] = []; + + if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { + const ts = this.events[0].getTs(); + ret.push( +
  • , + ); + } + + // Ensure that the key of the EventListSummary does not change with new events in either direction. + // This will prevent it from being re-created unnecessarily, and instead will allow new props to be provided. + // In turn, the shouldComponentUpdate method on ELS can be used to prevent unnecessary renderings. + const keyEvent = this.events.find(e => this.panel.grouperKeyMap.get(e)); + const key = keyEvent ? this.panel.grouperKeyMap.get(keyEvent) : this.generateKey(); + if (!keyEvent) { + // Populate the weak map with the key. + // Note that we only set the key on the specific event it refers to, since this group might get + // split up in the future by other intervening events. If we were to set the key on all events + // currently in the group, we would risk later giving the same key to multiple groups. + this.panel.grouperKeyMap.set(this.events[0], key); + } + + let highlightInSummary = false; + let eventTiles = this.events.map((e, i) => { + if (e.getId() === panel.props.highlightedEventId) { + highlightInSummary = true; + } + return panel.getTilesForEvent( + i === 0 ? this.prevEvent : this.events[i - 1], + e, + e === lastShownEvent, + isGrouped, + this.nextEvent, + this.nextEventTile, + ); + }).reduce((a, b) => a.concat(b), []); + + if (eventTiles.length === 0) { + eventTiles = null; + } + + // If a membership event is the start of visible history, tell the user + // why they can't see earlier messages + if (!this.panel.props.canBackPaginate && !this.prevEvent) { + ret.push(); + } + + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + public getNewPrevEvent(): MatrixEvent { + return this.events[this.events.length - 1]; + } +} + +// all the grouper classes that we use, ordered by priority +const groupers = [CreationGrouper, MainGrouper]; From 4db0a7e912e776e5ee8986d36c92923a895f8f6c Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Wed, 10 Aug 2022 15:17:57 -0400 Subject: [PATCH 002/176] emotes show up in messages --- src/components/structures/MessagePanel.tsx | 50 +--------------------- 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index c17f168f15a..54c930fe15f 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -18,7 +18,7 @@ import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'rea import ReactDOM from 'react-dom'; import classNames from 'classnames'; import { Room } from 'matrix-js-sdk/src/models/room'; -import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; +import { EventType} from 'matrix-js-sdk/src/@types/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { Relations } from "matrix-js-sdk/src/models/relations"; import { logger } from 'matrix-js-sdk/src/logger'; @@ -58,9 +58,6 @@ import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; import { haveRendererForEvent } from "../../events/EventTileFactory"; import { editorRoomKey } from "../../Editing"; import { hasThreadSummary } from "../../utils/EventUtils"; -import { mediaFromMxc } from "../../customisations/Media"; -import { mockStateEventImplementation } from '../../../test/test-utils'; - const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; @@ -289,12 +286,6 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - let emotesEvent=this.props.room.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let finalEmotes = {}; - for (let key in rawEmotes) { - this.state.emotes[":"+key+":"] = ""; - } } @@ -784,45 +775,6 @@ export default class MessagePanel extends React.Component { const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); // use txnId as key if available so that we don't remount during sending - if(mxEv.getType()==="m.room.message"){ - let messageText = mxEv.getContent().body; - //console.log(messageText); - let editedMessageText = messageText.replace(/:[\w+-]+:/, m => this.state.emotes[m] ? this.state.emotes[m] : m); - let m=[{body:messageText, - mimetype:"text/plain", - }, - { - body:editedMessageText, - mimetype:"text/html", - } - ]; - // if(mxEv.clearEvent){ - // console.log("clearevent",mxEv.getRoomId()); - // mxEv.clearEvent.content={ - // "format":"org.matrix.custom.html", - // "formatted_body":editedMessageText, - // "body":messageText, - // "msgtype":"m.text", - // "org.matrix.msc1767.message":m - // } - // } - // else{ - // console.log("no clearevent",mxEv); - // mxEv.content={ - // "format":"org.matrix.custom.html", - // "formatted_body":editedMessageText, - // "body":messageText, - // "msgtype":"m.text", - // "org.matrix.msc1767.message":m - // } - // } - - //mxEv.getContent().formatted_body = messageText; - //mxEv.clearEvent.content["org.matrix.msc1767.text"] = ""; - //mxEv.getContent().formatted_body = (hi); - //mxEv.getContent().format = "org.matrix.custom.html"; - - } ret.push( Date: Wed, 10 Aug 2022 21:22:20 -0400 Subject: [PATCH 003/176] added tooltips --- src/components/views/messages/TextualBody.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index bf0f4d459b3..c02f438de56 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -574,12 +574,12 @@ export default class TextualBody extends React.Component { let body: ReactNode; const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); - let emotesEvent=room.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let finalEmotes = {}; - for (let key in rawEmotes) { - finalEmotes[":"+key+":"] = ""; - } + let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let finalEmotes = {}; + for (let key in rawEmotes) { + finalEmotes[":" + key + ":"] = ""; + } if (SettingsStore.isEnabled("feature_extensible_events")) { const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; if (extev?.isEquivalentTo(M_MESSAGE)) { @@ -604,14 +604,14 @@ export default class TextualBody extends React.Component { if (!body) { isEmote = content.msgtype === MsgType.Emote; isNotice = content.msgtype === MsgType.Notice; - body = (HtmlUtils.bodyToHtml(content, this.props.highlights, { + body = HtmlUtils.bodyToHtml(content, this.props.highlights, { disableBigEmoji: isEmote || !SettingsStore.getValue('TextualBody.enableBigEmoji'), // Part of Replies fallback support stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, - })as any).replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m); + }); } //console.log(body); From d1b606856d173e7f4d3fa3f2ef5aa501b9423162 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Thu, 11 Aug 2022 09:51:50 -0400 Subject: [PATCH 004/176] removed extra logging --- .../views/room_settings/RoomEmoteSettings.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 60f5edbc268..7d9615cf2e5 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -57,7 +57,7 @@ export default class RoomEmoteSettings extends React.Component { if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - console.log(room.currentState); + //console.log(room.currentState); let emotes: Dictionary; emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let value = {}; @@ -65,11 +65,11 @@ export default class RoomEmoteSettings extends React.Component { value[emote] = emote; } //TODO: Decrypt the shortcodes and emotes if they are encrypted - if (emotes) { - console.log(room.roomId); - console.log(room.name); - console.log(emotes); - } + // if (emotes) { + // console.log(room.roomId); + // console.log(room.name); + // console.log(emotes); + // } //if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); //emotes={} this.state = { From 047f39e335fc2ece1e133627d01e83522ec6a907 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Fri, 12 Aug 2022 16:31:31 -0400 Subject: [PATCH 005/176] edits to changelog --- CHANGELOG.md | 6 +++ src/components/views/messages/TextualBody.tsx | 5 +-- .../views/room_settings/RoomEmoteSettings.tsx | 40 ++----------------- 3 files changed, 11 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09cbd0b5162..9a39978ff74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD Changes in [3.53.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.53.0) (2022-08-31) ===================================================================================================== @@ -75,6 +76,11 @@ Changes in [3.52.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/ * Use default styling on nested numbered lists due to MD being sensitive ([\#9110](https://github.com/matrix-org/matrix-react-sdk/pull/9110)). Fixes vector-im/element-web#22935. * Fix replying using chat effect commands ([\#9101](https://github.com/matrix-org/matrix-react-sdk/pull/9101)). Fixes vector-im/element-web#22824. * The first message in a DM can no longer be a sticker. This has been changed to avoid issues with the integration manager. ([\#9180](https://github.com/matrix-org/matrix-react-sdk/pull/9180)). +======= +Added ability to upload room-specific custom emotes in Room Settings. These show up in the room's messages when the shortcode is in the message. +The file fixes were a local issue in which I had to copy the correct version of the files from another folder. Not a part of the emote feature. +Currently emotes are not encrypted and do not show up in autocomplete or the right side emoji panel. I think this could be a start for fully implementing custom emotes. +>>>>>>> 8a422d88e6 (edits to changelog) Changes in [3.51.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.51.0) (2022-08-02) ===================================================================================================== diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index c02f438de56..3ac7adf3da6 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -574,6 +574,7 @@ export default class TextualBody extends React.Component { let body: ReactNode; const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); + //TODO: Decrypt emotes if encryption is added let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let finalEmotes = {}; @@ -614,8 +615,6 @@ export default class TextualBody extends React.Component { }); } - //console.log(body); - //body.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (this.props.replacingEventId) { body = <> { body } @@ -652,8 +651,6 @@ export default class TextualBody extends React.Component { />; } - //console.log(body.props.children); - //.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (isEmote) { return (
    { const client = MatrixClientPeg.get(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); - + //TODO: Decrypt the shortcodes and emotes if they are encrypted let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - //console.log(room.currentState); let emotes: Dictionary; emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let value = {}; for (let emote in emotes) { value[emote] = emote; } - //TODO: Decrypt the shortcodes and emotes if they are encrypted - // if (emotes) { - // console.log(room.roomId); - // console.log(room.name); - // console.log(emotes); - // } - //if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); - //emotes={} + this.state = { emotes: emotes, EmoteFieldsTouched: {}, @@ -128,7 +120,6 @@ export default class RoomEmoteSettings extends React.Component { let cleanemotes = {} let deletedItems = this.state.deletedItems; let value = {} - //console.log(e.currentTarget.getAttribute("name")); let id = e.currentTarget.getAttribute("id") for (let emote in this.state.emotes) { if (emote != id) { @@ -141,13 +132,6 @@ export default class RoomEmoteSettings extends React.Component { } this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }) - // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { - // field.setAttribute("value",(field as HTMLInputElement).id); - // field.setAttribute("defaultValue",(field as HTMLInputElement).id); - // }) - // for(let DOMid in this.state.emotes){ - // document.getElementById(DOMid).setAttribute("value",DOMid); - // } return; } @@ -163,7 +147,6 @@ export default class RoomEmoteSettings extends React.Component { // TODO: What do we do about errors? if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { - //const emotes = await client.uploadContent(this.state.emotes); //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { const newEmote = await client.uploadContent(this.state.newEmoteFile); @@ -178,12 +161,6 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; - - // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { - // if((field as HTMLInputElement).name===shortcode){ - // (field as HTMLInputElement).name= this.state.EmoteFieldsTouched[shortcode]; - // } - // }) } else { @@ -193,7 +170,6 @@ export default class RoomEmoteSettings extends React.Component { }; } - //console.log(emotesMxcs); newState.value = value; await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); this.emoteUpload.current.value = ""; @@ -205,21 +181,13 @@ export default class RoomEmoteSettings extends React.Component { newState.deleted = false; newState.deletedItems = {}; - /*newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); - newState.originalAvatarUrl = newState.avatarUrl; - newState.avatarFile = null;*/ - } /*else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { - await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, ''); - }*/ + } this.setState(newState as IState); }; private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); - //e.target.setAttribute("value", e.target.value); - //const newEmotes = { ...this.state.emotes, [value]: value }; - //let newState=this.state.emotes; let b = this.state.value b[id] = e.target.value; this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); @@ -306,7 +274,7 @@ export default class RoomEmoteSettings extends React.Component { />
    Date: Sat, 13 Aug 2022 00:12:45 -0400 Subject: [PATCH 006/176] fixed bug with User Appearance Settings Tab caused by changes in TextualBody.tsx --- src/components/views/messages/TextualBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 3ac7adf3da6..78a30e99271 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -575,7 +575,7 @@ export default class TextualBody extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); //TODO: Decrypt emotes if encryption is added - let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let finalEmotes = {}; for (let key in rawEmotes) { From 87aed984cab0d8a066391b2af2b1918461894cde Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Sat, 13 Aug 2022 12:50:37 -0400 Subject: [PATCH 007/176] added autocomplete custom emote functionality --- src/autocomplete/Components.tsx | 4 ++-- src/autocomplete/EmojiProvider.tsx | 28 ++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 079e407c5e2..db217cac1b7 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -46,7 +46,7 @@ export const TextualCompletion = forwardRef((props aria-selected={ariaSelectedAttribute} ref={ref} > - { title } + {title} { subtitle } { description }
    @@ -75,7 +75,7 @@ export const PillCompletion = forwardRef((props, ref) ref={ref} > { children } - { title } + {} { subtitle } { description }
    diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 4a2c37988ae..eba94a6a047 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -32,6 +32,7 @@ import SettingsStore from "../settings/SettingsStore"; import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji'; import { TimelineRenderingType } from '../contexts/RoomContext'; import * as recent from '../emojipicker/recent'; +import { mediaFromMxc } from "../customisations/Media"; const LIMIT = 20; @@ -75,9 +76,15 @@ export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - + emotes:Dictionary; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); + let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + this.emotes = {}; + for (let key in rawEmotes) { + this.emotes[key] = ""; + } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], @@ -102,7 +109,20 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } - + let emojisAndEmotes=[...SORTED_EMOJI]; + for(let key in this.emotes){ + emojisAndEmotes.push({ + emoji:{label:key, + shortcodes:[this.emotes[key]], + hexcode:"", + unicode:":"+key+":", + + }, + _orderBy:0 + }) + } + this.matcher.setObjects(emojisAndEmotes); + this.nameMatcher.setObjects(emojisAndEmotes); let completions = []; const { command, range } = this.getCurrentCommand(query, selection); @@ -146,8 +166,8 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.map(c => ({ completion: c.emoji.unicode, component: ( - - { c.emoji.unicode } + + { this.emotes[c.emoji.shortcodes[0]]? this.emotes[c.emoji.shortcodes[0]]:c.emoji.unicode } ), range, From 48af2e33456f3022c4efda284488a1c98a37453b Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 17:08:12 -0400 Subject: [PATCH 008/176] Removing merge conflict blocks and fixing some formatting --- CHANGELOG.md | 3 --- src/HtmlUtils.tsx | 4 ++-- src/components/views/dialogs/RoomSettingsDialog.tsx | 9 --------- src/components/views/room_settings/RoomEmoteSettings.tsx | 4 ++-- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a39978ff74..10666fc797a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD Changes in [3.53.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.53.0) (2022-08-31) ===================================================================================================== @@ -76,11 +75,9 @@ Changes in [3.52.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/ * Use default styling on nested numbered lists due to MD being sensitive ([\#9110](https://github.com/matrix-org/matrix-react-sdk/pull/9110)). Fixes vector-im/element-web#22935. * Fix replying using chat effect commands ([\#9101](https://github.com/matrix-org/matrix-react-sdk/pull/9101)). Fixes vector-im/element-web#22824. * The first message in a DM can no longer be a sticker. This has been changed to avoid issues with the integration manager. ([\#9180](https://github.com/matrix-org/matrix-react-sdk/pull/9180)). -======= Added ability to upload room-specific custom emotes in Room Settings. These show up in the room's messages when the shortcode is in the message. The file fixes were a local issue in which I had to copy the correct version of the files from another folder. Not a part of the emote feature. Currently emotes are not encrypted and do not show up in autocomplete or the right side emoji panel. I think this could be a start for fully implementing custom emotes. ->>>>>>> 8a422d88e6 (edits to changelog) Changes in [3.51.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.51.0) (2022-08-02) ===================================================================================================== diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 3767e6840aa..0ca878c9ccf 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -539,11 +539,11 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } let contentBody = safeBody ?? strippedBody; - contentBody=contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + contentBody = contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); if (opts.returnString) { return contentBody; } - + let emojiBody = false; if (!opts.disableBigEmoji && bodyHasEmoji) { let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : ''; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 23c7abe3fcd..467c75120e2 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -26,10 +26,7 @@ import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab"; import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab"; import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; -<<<<<<< HEAD -======= import EmoteSettingsTab from "../settings/tabs/room/EmoteSettingsTab"; ->>>>>>> ed46baa7ab (emotes show up in messages) import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; @@ -42,10 +39,7 @@ export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; -<<<<<<< HEAD -======= export const ROOM_EMOTES_TAB = "ROOM_EMOTES_TAB"; ->>>>>>> ed46baa7ab (emotes show up in messages) export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; interface IProps { @@ -128,9 +122,7 @@ export default class RoomSettingsDialog extends React.Component this.props.onFinished(true)} />, "RoomSettingsNotifications", )); -<<<<<<< HEAD -======= tabs.push(new Tab( ROOM_NOTIFICATIONS_TAB, _td("Emotes"), @@ -138,7 +130,6 @@ export default class RoomSettingsDialog extends React.Component , "RoomSettingsNotifications", )); ->>>>>>> ed46baa7ab (emotes show up in messages) if (SettingsStore.getValue("feature_bridge_state")) { tabs.push(new Tab( ROOM_BRIDGES_TAB, diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index eecc98a94ef..2c977f26c18 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -63,7 +63,7 @@ export default class RoomEmoteSettings extends React.Component { for (let emote in emotes) { value[emote] = emote; } - + this.state = { emotes: emotes, EmoteFieldsTouched: {}, @@ -181,7 +181,7 @@ export default class RoomEmoteSettings extends React.Component { newState.deleted = false; newState.deletedItems = {}; - } + } this.setState(newState as IState); }; From 99cf97b1a244f333d0f3e48b2b34a0e3fbf0fb38 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 17:36:43 -0400 Subject: [PATCH 009/176] some lint fixes --- res/css/views/rooms/_EventTile.pcss | 2 +- .../views/room_settings/RoomEmoteSettings.tsx | 75 +++++++++---------- .../settings/tabs/room/EmoteSettingsTab.tsx | 17 ----- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index ccc88a0cf83..2c934364b8e 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -646,7 +646,7 @@ $left-gutter: 64px; font-size: inherit !important; } } -.mx_Emote{ +.mx_Emote { height: 30px; } .mx_EventTile_e2eIcon { diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 2c977f26c18..5385ee471bc 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -15,17 +15,13 @@ limitations under the License. */ import React, { createRef } from 'react'; -import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Field from "../elements/Field"; import { mediaFromMxc } from "../../../customisations/Media"; import AccessibleButton from "../elements/AccessibleButton"; -import AvatarSetting from "../settings/AvatarSetting"; -import { htmlSerializeFromMdIfNeeded } from '../../../editor/serialize'; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; -import { string } from 'prop-types'; interface IProps { roomId: string; @@ -34,10 +30,10 @@ interface IProps { interface IState { emotes: Dictionary; EmoteFieldsTouched: Record; - newEmoteFileAdded: boolean, - newEmoteCodeAdded: boolean, - newEmoteCode: string, - newEmoteFile: File, + newEmoteFileAdded: boolean; + newEmoteCodeAdded: boolean; + newEmoteCode: string; + newEmoteFile: File; canAddEmote: boolean; deleted: boolean; deletedItems: Dictionary; @@ -56,10 +52,10 @@ export default class RoomEmoteSettings extends React.Component { const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); //TODO: Decrypt the shortcodes and emotes if they are encrypted - let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - let emotes: Dictionary; - emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let value = {}; + const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + + const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const value = {}; for (let emote in emotes) { value[emote] = emote; } @@ -84,22 +80,24 @@ export default class RoomEmoteSettings extends React.Component { private isSaveEnabled = () => { - return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || this.state.deleted; + return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || + (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || + (this.state.deleted); }; private cancelEmoteChanges = async (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); - let value = {}; + const value = {}; if (this.state.deleted) { - for (let key in this.state.deletedItems) { + for (const key in this.state.deletedItems) { this.state.emotes[key] = this.state.deletedItems[key]; value[key] = key; } } document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { value[(field as HTMLInputElement).id] = (field as HTMLInputElement).id; - }) + }); if (!this.isSaveEnabled()) return; this.setState({ EmoteFieldsTouched: {}, @@ -117,17 +115,17 @@ export default class RoomEmoteSettings extends React.Component { private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); - let cleanemotes = {} - let deletedItems = this.state.deletedItems; - let value = {} - let id = e.currentTarget.getAttribute("id") - for (let emote in this.state.emotes) { + const cleanemotes = {}; + const deletedItems = this.state.deletedItems; + const value = {}; + const id = e.currentTarget.getAttribute("id"); + for (const emote in this.state.emotes) { if (emote != id) { - cleanemotes[emote] = this.state.emotes[emote] - value[emote] = emote + cleanemotes[emote] = this.state.emotes[emote]; + value[emote] = emote; } else { - deletedItems[emote] = this.state.emotes[emote] + deletedItems[emote] = this.state.emotes[emote]; } } @@ -154,7 +152,7 @@ export default class RoomEmoteSettings extends React.Component { value[this.state.newEmoteCode] = this.state.newEmoteCode; } if (this.state.emotes) { - for (let shortcode in this.state.emotes) { + for (const shortcode in this.state.emotes) { if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) && (shortcode === this.state.newEmoteCode)) { continue; } @@ -257,8 +255,7 @@ export default class RoomEmoteSettings extends React.Component { {_t("Save")}
    - ); - } + )} let existingEmotes = []; if (this.state.emotes) { @@ -273,7 +270,8 @@ export default class RoomEmoteSettings extends React.Component { className="mx_EmoteSettings_existingEmoteCode" /> -
    @@ -288,13 +286,10 @@ export default class RoomEmoteSettings extends React.Component {
    - ) + ); } - - } - let emoteUploadButton; if (this.state.canAddEmote) { emoteUploadButton = ( @@ -313,22 +308,22 @@ export default class RoomEmoteSettings extends React.Component { return (
  • { this.state.newEmoteFileAdded ? : null } - {emoteUploadButton} + { emoteUploadButton }
  • { existingEmotes } - {emoteSettingsButtons} + { emoteSettingsButtons }
    ); } diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx index 864fff3171e..d412ffedb01 100644 --- a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -18,14 +18,7 @@ import React, { ContextType } from 'react'; import { _t } from "../../../../../languageHandler"; import RoomEmoteSettings from "../../../room_settings/RoomEmoteSettings"; -import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; -import dis from "../../../../../dispatcher/dispatcher"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; -import SettingsStore from "../../../../../settings/SettingsStore"; -import { UIFeature } from "../../../../../settings/UIFeature"; -import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings"; -import AliasSettings from "../../../room_settings/AliasSettings"; -import PosthogTrackers from "../../../../../PosthogTrackers"; interface IProps { roomId: string; @@ -47,20 +40,10 @@ export default class EmoteRoomSettingsTab extends React.Component : - null; - - return (
    From 9555dc2b3d4dc211cfde767cac581fceed0d410e Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:02:16 -0400 Subject: [PATCH 010/176] more lint fixes --- src/HtmlUtils.tsx | 14 ++-- src/autocomplete/Components.tsx | 4 +- src/autocomplete/EmojiProvider.tsx | 27 ++++---- src/components/structures/MessagePanel.tsx | 65 +++++++++---------- src/components/views/messages/TextualBody.tsx | 11 ++-- .../views/room_settings/RoomEmoteSettings.tsx | 36 +++++----- .../settings/tabs/room/EmoteSettingsTab.tsx | 5 +- .../room-list/previews/MessageEventPreview.ts | 2 +- 8 files changed, 77 insertions(+), 87 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 0ca878c9ccf..685d791830e 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -470,7 +470,6 @@ export function bodyToHtml(content: IContent, highlights: Optional, op sanitizeParams = composerSanitizeHtmlParams; } let strippedBody: string; - let safeBody: string; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext try { @@ -497,9 +496,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join('').replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + return highlighter.applyHighlights(safeText, safeHighlights).join('') + .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; } @@ -524,7 +523,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // @ts-ignore - `e` can be an Element, not just a Node displayMode: e.name == 'div', output: "htmlAndMathml", - }) + }); }); safeBody = phtml.html(); } @@ -575,17 +574,16 @@ export function bodyToHtml(content: IContent, highlights: Optional, op 'mx_EventTile_bigEmoji': emojiBody, 'markdown-body': isHtmlMessage && !emojiBody, }); - let tmp=strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); - if(tmp!=strippedBody){ + const tmp=strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + if (tmp!=strippedBody) { safeBody=tmp; } - let emojiBodyElements: JSX.Element[]; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } - + return safeBody ? ((props aria-selected={ariaSelectedAttribute} ref={ref} > - {title} + { title } { subtitle } { description }
    @@ -75,7 +75,7 @@ export const PillCompletion = forwardRef((props, ref) ref={ref} > { children } - {} + { } { subtitle } { description } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index eba94a6a047..055dd7f816a 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -76,14 +76,15 @@ export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - emotes:Dictionary; + emotes: Dictionary; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); - let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; this.emotes = {}; - for (let key in rawEmotes) { - this.emotes[key] = ""; + for (const key in rawEmotes) { + this.emotes[key] = ""; } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], @@ -109,17 +110,17 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } - let emojisAndEmotes=[...SORTED_EMOJI]; - for(let key in this.emotes){ + const emojisAndEmotes=[...SORTED_EMOJI]; + for(const key in this.emotes) { emojisAndEmotes.push({ - emoji:{label:key, - shortcodes:[this.emotes[key]], - hexcode:"", - unicode:":"+key+":", + emoji: {label: key, + shortcodes: [this.emotes[key]], + hexcode: "", + unicode: ":"+key+":", }, - _orderBy:0 - }) + _orderBy: 0, + }); } this.matcher.setObjects(emojisAndEmotes); this.nameMatcher.setObjects(emojisAndEmotes); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 54c930fe15f..3e3589912e7 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -18,7 +18,7 @@ import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'rea import ReactDOM from 'react-dom'; import classNames from 'classnames'; import { Room } from 'matrix-js-sdk/src/models/room'; -import { EventType} from 'matrix-js-sdk/src/@types/event'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { Relations } from "matrix-js-sdk/src/models/relations"; import { logger } from 'matrix-js-sdk/src/logger'; @@ -26,7 +26,6 @@ import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon'; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; - import shouldHideEvent from '../../shouldHideEvent'; import { wantsDateSeparator } from '../../DateUtils'; import { MatrixClientPeg } from '../../MatrixClientPeg'; @@ -196,7 +195,7 @@ interface IState { ghostReadMarkers: string[]; showTypingNotifications: boolean; hideSender: boolean; - emotes:Dictionary; + emotes: Dictionary; } interface IReadReceiptForUser { @@ -285,8 +284,6 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - - } componentDidMount() { @@ -773,38 +770,38 @@ export default class MessagePanel extends React.Component { isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); - + // use txnId as key if available so that we don't remount during sending ret.push( + key={mxEv.getTxnId() || eventId} + as="li" + ref={this.collectEventTile.bind(this, eventId)} + alwaysShowTimestamps={this.props.alwaysShowTimestamps} + mxEvent={mxEv} + continuation={continuation} + isRedacted={mxEv.isRedacted()} + replacingEventId={mxEv.replacingEventId()} + editState={isEditing && this.props.editState} + onHeightChanged={this.onHeightChanged} + readReceipts={readReceipts} + readReceiptMap={this.readReceiptMap} + showUrlPreview={this.props.showUrlPreview} + checkUnmounting={this.isUnmounting} + eventSendStatus={mxEv.getAssociatedStatus()} + isTwelveHour={this.props.isTwelveHour} + permalinkCreator={this.props.permalinkCreator} + last={last} + lastInSection={lastInSection} + lastSuccessful={isLastSuccessful} + isSelectedEvent={highlight} + getRelationsForEvent={this.props.getRelationsForEvent} + showReactions={this.props.showReactions} + layout={this.props.layout} + showReadReceipts={this.props.showReadReceipts} + callEventGrouper={callEventGrouper} + hideSender={this.state.hideSender} + />, ); return ret; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 78a30e99271..fb56f1a59b8 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -575,11 +575,12 @@ export default class TextualBody extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); //TODO: Decrypt emotes if encryption is added - let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let finalEmotes = {}; - for (let key in rawEmotes) { - finalEmotes[":" + key + ":"] = ""; + const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const finalEmotes = {}; + for (const key in rawEmotes) { + finalEmotes[":" + key + ":"] = ""; } if (SettingsStore.isEnabled("feature_extensible_events")) { const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 5385ee471bc..5642e60caed 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -18,7 +18,6 @@ import React, { createRef } from 'react'; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import Field from "../elements/Field"; import { mediaFromMxc } from "../../../customisations/Media"; import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; @@ -53,10 +52,9 @@ export default class RoomEmoteSettings extends React.Component { if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); //TODO: Decrypt the shortcodes and emotes if they are encrypted const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const value = {}; - for (let emote in emotes) { + for (const emote in emotes) { value[emote] = emote; } @@ -78,10 +76,9 @@ export default class RoomEmoteSettings extends React.Component { this.emoteUpload.current.click(); }; - private isSaveEnabled = () => { return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || - (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || + (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || (this.state.deleted); }; @@ -141,7 +138,7 @@ export default class RoomEmoteSettings extends React.Component { const client = MatrixClientPeg.get(); const newState: Partial = {}; const emotesMxcs = {}; - let value = {}; + const value = {}; // TODO: What do we do about errors? if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { @@ -181,12 +178,11 @@ export default class RoomEmoteSettings extends React.Component { } this.setState(newState as IState); - }; - + } private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); - let b = this.state.value + const b = this.state.value; b[id] = e.target.value; this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); } @@ -241,18 +237,18 @@ export default class RoomEmoteSettings extends React.Component { emoteSettingsButtons = (
    - {_t("Cancel")} + { _t("Cancel") } - {_t("Save")} + { _t("Save") }
    )} @@ -264,9 +260,9 @@ export default class RoomEmoteSettings extends React.Component {
  • @@ -282,7 +278,7 @@ export default class RoomEmoteSettings extends React.Component { aria-label="Close" id={emotecode} > - {_t("Delete")} + { _t("Delete") }
  • @@ -295,7 +291,7 @@ export default class RoomEmoteSettings extends React.Component { emoteUploadButton = (
    {_t("Upload Emote")} diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx index d412ffedb01..d8aba88defb 100644 --- a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -41,13 +41,10 @@ export default class EmoteRoomSettingsTab extends React.Component -
    {_t("Emotes")}
    +
    { _t("Emotes") }
    diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index 825d806a8ed..72b513be5f2 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -61,7 +61,7 @@ export class MessageEventPreview implements IPreview { // run it through DOMParser to fixup encoded html entities body = new DOMParser().parseFromString(sanitised, "text/html").documentElement.textContent; } - + body = sanitizeForTranslation(body); if (msgtype === MsgType.Emote) { From 776d40c19b948b354eb0b35f96ec57f5aebb1b5e Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:16:25 -0400 Subject: [PATCH 011/176] even more lint fixes --- src/HtmlUtils.tsx | 6 +-- src/autocomplete/EmojiProvider.tsx | 4 +- src/components/views/messages/TextualBody.tsx | 3 +- .../views/room_settings/RoomEmoteSettings.tsx | 43 +++++++++---------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 685d791830e..6ca79728ead 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -496,9 +496,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function(safeText) { + sanitizeParams.textFilter = function (safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join('') - .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; } @@ -578,7 +578,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (tmp!=strippedBody) { safeBody=tmp; } - + let emojiBodyElements: JSX.Element[]; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 055dd7f816a..f96cce8dfc7 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -111,9 +111,9 @@ export default class EmojiProvider extends AutocompleteProvider { return []; // don't give any suggestions if the user doesn't want them } const emojisAndEmotes=[...SORTED_EMOJI]; - for(const key in this.emotes) { + for (const key in this.emotes) { emojisAndEmotes.push({ - emoji: {label: key, + emoji: { label: key, shortcodes: [this.emotes[key]], hexcode: "", unicode: ":"+key+":", diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index fb56f1a59b8..32dc2ac9f60 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -579,7 +579,7 @@ export default class TextualBody extends React.Component { const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const finalEmotes = {}; for (const key in rawEmotes) { - finalEmotes[":" + key + ":"] = ""; } if (SettingsStore.isEnabled("feature_extensible_events")) { @@ -614,7 +614,6 @@ export default class TextualBody extends React.Component { ref: this.contentRef, returnString: false, }); - } if (this.props.replacingEventId) { body = <> diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 5642e60caed..bd1423fbff4 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -126,9 +126,8 @@ export default class RoomEmoteSettings extends React.Component { } } - this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }) + this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }); return; - } private saveEmote = async (e: React.FormEvent): Promise => { e.stopPropagation(); @@ -150,7 +149,8 @@ export default class RoomEmoteSettings extends React.Component { } if (this.state.emotes) { for (const shortcode in this.state.emotes) { - if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) && (shortcode === this.state.newEmoteCode)) { + if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) + && (shortcode === this.state.newEmoteCode)) { continue; } if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { @@ -163,7 +163,7 @@ export default class RoomEmoteSettings extends React.Component { value[shortcode] = shortcode; } - }; + } } newState.value = value; await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); @@ -175,17 +175,16 @@ export default class RoomEmoteSettings extends React.Component { newState.emotes = emotesMxcs; newState.deleted = false; newState.deletedItems = {}; - } this.setState(newState as IState); - } + }; private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); const b = this.state.value; b[id] = e.target.value; this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); - } + }; private onEmoteFileAdd = (e: React.ChangeEvent): void => { if (!e.target.files || !e.target.files.length) { @@ -227,7 +226,7 @@ export default class RoomEmoteSettings extends React.Component { newEmoteCodeAdded: false, }); } - } + }; public render(): JSX.Element { let emoteSettingsButtons; @@ -237,21 +236,21 @@ export default class RoomEmoteSettings extends React.Component { emoteSettingsButtons = (
    { _t("Cancel") } { _t("Save") }
    - )} + )}; let existingEmotes = []; if (this.state.emotes) { @@ -260,13 +259,13 @@ export default class RoomEmoteSettings extends React.Component {
  • - @@ -304,7 +303,7 @@ export default class RoomEmoteSettings extends React.Component { return (
    { type="file" ref={ this.emoteUpload } className="mx_EmoteSettings_emoteUpload" - onClick={ chromeFileInputFix } - onChange={ this.onEmoteFileAdd } + onClick={chromeFileInputFix} + onChange={this.onEmoteFileAdd} accept="image/*" />
  • { this.state.newEmoteFileAdded ? : null } From 1d18c70d67eb9e74f9044045bfe1c3cefe648b88 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:44:24 -0400 Subject: [PATCH 012/176] lint fixes 4 --- src/HtmlUtils.tsx | 2 +- .../views/room_settings/RoomEmoteSettings.tsx | 31 ++++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 6ca79728ead..b44ebab2121 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -496,7 +496,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function (safeText) { + sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join('') .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index bd1423fbff4..e3c8334dea7 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -107,7 +107,6 @@ export default class RoomEmoteSettings extends React.Component { this.emoteUpload.current.value = ""; this.emoteCodeUpload.current.value = ""; - }; private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); @@ -120,15 +119,14 @@ export default class RoomEmoteSettings extends React.Component { if (emote != id) { cleanemotes[emote] = this.state.emotes[emote]; value[emote] = emote; - } - else { + } else { deletedItems[emote] = this.state.emotes[emote]; } } this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }); return; - } + }; private saveEmote = async (e: React.FormEvent): Promise => { e.stopPropagation(); e.preventDefault(); @@ -156,13 +154,10 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; - } - - else { + } else { emotesMxcs[shortcode] = this.state.emotes[shortcode]; value[shortcode] = shortcode; } - } } newState.value = value; @@ -220,8 +215,7 @@ export default class RoomEmoteSettings extends React.Component { ...this.state.EmoteFieldsTouched, }, }); - } - else { + } else { this.setState({ newEmoteCodeAdded: false, }); @@ -250,11 +244,12 @@ export default class RoomEmoteSettings extends React.Component { { _t("Save") }
  • - )}; + ); + } - let existingEmotes = []; + const existingEmotes = []; if (this.state.emotes) { - for (let emotecode in this.state.emotes) { + for (const emotecode in this.state.emotes) { existingEmotes.push(
  • { /> + src={ + mediaFromMxc(this.state.emotes[emotecode]).srcHttp + } />
    { aria-label="Close" id={emotecode} > - { _t("Delete") } + {_t("Delete")}
  • @@ -310,7 +305,7 @@ export default class RoomEmoteSettings extends React.Component { > Date: Fri, 2 Sep 2022 18:48:37 -0400 Subject: [PATCH 013/176] lint fixes 5 --- src/components/views/room_settings/RoomEmoteSettings.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index e3c8334dea7..77c8a4e24c0 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -272,10 +272,10 @@ export default class RoomEmoteSettings extends React.Component { aria-label="Close" id={emotecode} > - {_t("Delete")} + { _t("Delete") } - + , ); } } @@ -285,16 +285,15 @@ export default class RoomEmoteSettings extends React.Component { emoteUploadButton = (
    - {_t("Upload Emote")} + { _t("Upload Emote") }
    ); } - return ( Date: Fri, 2 Sep 2022 19:05:49 -0400 Subject: [PATCH 014/176] removed extraneous file that got accidentally copied over --- src/utils/exportUtils/MessagePanel.tsx | 1343 ------------------------ 1 file changed, 1343 deletions(-) delete mode 100644 src/utils/exportUtils/MessagePanel.tsx diff --git a/src/utils/exportUtils/MessagePanel.tsx b/src/utils/exportUtils/MessagePanel.tsx deleted file mode 100644 index 71f698c9f83..00000000000 --- a/src/utils/exportUtils/MessagePanel.tsx +++ /dev/null @@ -1,1343 +0,0 @@ -/* -Copyright 2016 - 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react'; -import ReactDOM from 'react-dom'; -import classNames from 'classnames'; -import { Room } from 'matrix-js-sdk/src/models/room'; -import { EventType } from 'matrix-js-sdk/src/@types/event'; -import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { Relations } from "matrix-js-sdk/src/models/relations"; -import { logger } from 'matrix-js-sdk/src/logger'; -import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon'; -import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; - -import shouldHideEvent from '../../shouldHideEvent'; -import { wantsDateSeparator } from '../../DateUtils'; -import { MatrixClientPeg } from '../../MatrixClientPeg'; -import SettingsStore from '../../settings/SettingsStore'; -import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; -import { Layout } from "../../settings/enums/Layout"; -import { _t } from "../../languageHandler"; -import EventTile, { UnwrappedEventTile, IReadReceiptProps } from "../views/rooms/EventTile"; -import { hasText } from "../../TextForEvent"; -import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; -import DMRoomMap from "../../utils/DMRoomMap"; -import NewRoomIntro from "../views/rooms/NewRoomIntro"; -import HistoryTile from "../views/rooms/HistoryTile"; -import defaultDispatcher from '../../dispatcher/dispatcher'; -import CallEventGrouper from "./CallEventGrouper"; -import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile'; -import ScrollPanel, { IScrollState } from "./ScrollPanel"; -import GenericEventListSummary from '../views/elements/GenericEventListSummary'; -import EventListSummary from '../views/elements/EventListSummary'; -import DateSeparator from '../views/messages/DateSeparator'; -import ErrorBoundary from '../views/elements/ErrorBoundary'; -import ResizeNotifier from "../../utils/ResizeNotifier"; -import Spinner from "../views/elements/Spinner"; -import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -import EditorStateTransfer from "../../utils/EditorStateTransfer"; -import { Action } from '../../dispatcher/actions'; -import { getEventDisplayInfo } from "../../utils/EventRenderingUtils"; -import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; -import { haveRendererForEvent } from "../../events/EventTileFactory"; -import { editorRoomKey } from "../../Editing"; -import { hasThreadSummary } from "../../utils/EventUtils"; - -const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes -const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; -const groupedStateEvents = [ - EventType.RoomMember, - EventType.RoomThirdPartyInvite, - EventType.RoomServerAcl, - EventType.RoomPinnedEvents, -]; - -// check if there is a previous event and it has the same sender as this event -// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL -export function shouldFormContinuation( - prevEvent: MatrixEvent, - mxEvent: MatrixEvent, - showHiddenEvents: boolean, - threadsEnabled: boolean, - timelineRenderingType?: TimelineRenderingType, -): boolean { - if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false; - // sanity check inputs - if (!prevEvent?.sender || !mxEvent.sender) return false; - // check if within the max continuation period - if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; - - // As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa - if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false; - - // Some events should appear as continuations from previous events of different types. - if (mxEvent.getType() !== prevEvent.getType() && - (!continuedTypes.includes(mxEvent.getType() as EventType) || - !continuedTypes.includes(prevEvent.getType() as EventType))) return false; - - // Check if the sender is the same and hasn't changed their displayname/avatar between these events - if (mxEvent.sender.userId !== prevEvent.sender.userId || - mxEvent.sender.name !== prevEvent.sender.name || - mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; - - // Thread summaries in the main timeline should break up a continuation on both sides - if (threadsEnabled && - (hasThreadSummary(mxEvent) || hasThreadSummary(prevEvent)) && - timelineRenderingType !== TimelineRenderingType.Thread - ) { - return false; - } - - // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile - if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false; - - return true; -} - -interface IProps { - // the list of MatrixEvents to display - events: MatrixEvent[]; - - // true to give the component a 'display: none' style. - hidden?: boolean; - - // true to show a spinner at the top of the timeline to indicate - // back-pagination in progress - backPaginating?: boolean; - - // true to show a spinner at the end of the timeline to indicate - // forward-pagination in progress - forwardPaginating?: boolean; - - // ID of an event to highlight. If undefined, no event will be highlighted. - highlightedEventId?: string; - - // The room these events are all in together, if any. - // (The notification panel won't have a room here, for example.) - room?: Room; - - // Should we show URL Previews - showUrlPreview?: boolean; - - // event after which we should show a read marker - readMarkerEventId?: string; - - // whether the read marker should be visible - readMarkerVisible?: boolean; - - // the userid of our user. This is used to suppress the read marker - // for pending messages. - ourUserId?: string; - - // whether the timeline can visually go back any further - canBackPaginate?: boolean; - - // whether to show read receipts - showReadReceipts?: boolean; - - // true if updates to the event list should cause the scroll panel to - // scroll down when we are at the bottom of the window. See ScrollPanel - // for more details. - stickyBottom?: boolean; - - // className for the panel - className: string; - - // show twelve hour timestamps - isTwelveHour?: boolean; - - // show timestamps always - alwaysShowTimestamps?: boolean; - - // whether to show reactions for an event - showReactions?: boolean; - - // which layout to use - layout?: Layout; - - resizeNotifier: ResizeNotifier; - permalinkCreator?: RoomPermalinkCreator; - editState?: EditorStateTransfer; - - // callback which is called when the panel is scrolled. - onScroll?(event: Event): void; - - // callback which is called when more content is needed. - onFillRequest?(backwards: boolean): Promise; - - // helper function to access relations for an event - onUnfillRequest?(backwards: boolean, scrollToken: string): void; - - getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations; - - hideThreadedMessages?: boolean; - disableGrouping?: boolean; - - callEventGroupers: Map; -} - -interface IState { - ghostReadMarkers: string[]; - showTypingNotifications: boolean; - hideSender: boolean; -} - -interface IReadReceiptForUser { - lastShownEventId: string; - receipt: IReadReceiptProps; -} - -/* (almost) stateless UI component which builds the event tiles in the room timeline. - */ -export default class MessagePanel extends React.Component { - static contextType = RoomContext; - public context!: React.ContextType; - - static defaultProps = { - disableGrouping: false, - }; - - // opaque readreceipt info for each userId; used by ReadReceiptMarker - // to manage its animations - private readonly readReceiptMap: { [userId: string]: IReadReceiptInfo } = {}; - - // Track read receipts by event ID. For each _shown_ event ID, we store - // the list of read receipts to display: - // [ - // { - // userId: string, - // member: RoomMember, - // ts: number, - // }, - // ] - // This is recomputed on each render. It's only stored on the component - // for ease of passing the data around since it's computed in one pass - // over all events. - private readReceiptsByEvent: Record = {}; - - // Track read receipts by user ID. For each user ID we've ever shown a - // a read receipt for, we store an object: - // { - // lastShownEventId: string, - // receipt: { - // userId: string, - // member: RoomMember, - // ts: number, - // }, - // } - // so that we can always keep receipts displayed by reverting back to - // the last shown event for that user ID when needed. This may feel like - // it duplicates the receipt storage in the room, but at this layer, we - // are tracking _shown_ event IDs, which the JS SDK knows nothing about. - // This is recomputed on each render, using the data from the previous - // render as our fallback for any user IDs we can't match a receipt to a - // displayed event in the current render cycle. - private readReceiptsByUserId: Record = {}; - - private readonly _showHiddenEvents: boolean; - private readonly threadsEnabled: boolean; - private isMounted = false; - - private readMarkerNode = createRef(); - private whoIsTyping = createRef(); - private scrollPanel = createRef(); - - private readonly showTypingNotificationsWatcherRef: string; - private eventTiles: Record = {}; - - // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. - public grouperKeyMap = new WeakMap(); - - constructor(props, context) { - super(props, context); - - this.state = { - // previous positions the read marker has been in, so we can - // display 'ghost' read markers that are animating away - ghostReadMarkers: [], - showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - hideSender: this.shouldHideSender(), - }; - - // Cache these settings on mount since Settings is expensive to query, - // and we check this in a hot code path. This is also cached in our - // RoomContext, however we still need a fallback for roomless MessagePanels. - this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); - this.threadsEnabled = SettingsStore.getValue("feature_thread"); - - this.showTypingNotificationsWatcherRef = - SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - } - - componentDidMount() { - this.calculateRoomMembersCount(); - this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); - this.isMounted = true; - } - - componentWillUnmount() { - this.isMounted = false; - this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); - SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.layout !== this.props.layout) { - this.calculateRoomMembersCount(); - } - - if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { - const ghostReadMarkers = this.state.ghostReadMarkers; - ghostReadMarkers.push(prevProps.readMarkerEventId); - this.setState({ - ghostReadMarkers, - }); - } - - const pendingEditItem = this.pendingEditItem; - if (!this.props.editState && this.props.room && pendingEditItem) { - const event = this.props.room.findEventById(pendingEditItem); - defaultDispatcher.dispatch({ - action: Action.EditEvent, - event: !event?.isRedacted() ? event : null, - timelineRenderingType: this.context.timelineRenderingType, - }); - } - } - - private shouldHideSender(): boolean { - return this.props.room?.getInvitedAndJoinedMemberCount() <= 2 && this.props.layout === Layout.Bubble; - } - - private calculateRoomMembersCount = (): void => { - this.setState({ - hideSender: this.shouldHideSender(), - }); - }; - - private onShowTypingNotificationsChange = (): void => { - this.setState({ - showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - }); - }; - - /* get the DOM node representing the given event */ - public getNodeForEventId(eventId: string): HTMLElement { - if (!this.eventTiles) { - return undefined; - } - - return this.eventTiles[eventId]?.ref?.current; - } - - public getTileForEventId(eventId: string): UnwrappedEventTile { - if (!this.eventTiles) { - return undefined; - } - return this.eventTiles[eventId]; - } - - /* return true if the content is fully scrolled down right now; else false. - */ - public isAtBottom(): boolean { - return this.scrollPanel.current?.isAtBottom(); - } - - /* get the current scroll state. See ScrollPanel.getScrollState for - * details. - * - * returns null if we are not mounted. - */ - public getScrollState(): IScrollState { - return this.scrollPanel.current?.getScrollState() ?? null; - } - - // returns one of: - // - // null: there is no read marker - // -1: read marker is above the window - // 0: read marker is within the window - // +1: read marker is below the window - public getReadMarkerPosition(): number { - const readMarker = this.readMarkerNode.current; - const messageWrapper = this.scrollPanel.current; - - if (!readMarker || !messageWrapper) { - return null; - } - - const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect(); - const readMarkerRect = readMarker.getBoundingClientRect(); - - // the read-marker pretends to have zero height when it is actually - // two pixels high; +2 here to account for that. - if (readMarkerRect.bottom + 2 < wrapperRect.top) { - return -1; - } else if (readMarkerRect.top < wrapperRect.bottom) { - return 0; - } else { - return 1; - } - } - - /* jump to the top of the content. - */ - public scrollToTop(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToTop(); - } - } - - /* jump to the bottom of the content. - */ - public scrollToBottom(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToBottom(); - } - } - - /** - * Page up/down. - * - * @param {number} mult: -1 to page up, +1 to page down - */ - public scrollRelative(mult: number): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollRelative(mult); - } - } - - /** - * Scroll up/down in response to a scroll key - * - * @param {KeyboardEvent} ev: the keyboard event to handle - */ - public handleScrollKey(ev: KeyboardEvent): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.handleScrollKey(ev); - } - } - - /* jump to the given event id. - * - * offsetBase gives the reference point for the pixelOffset. 0 means the - * top of the container, 1 means the bottom, and fractional values mean - * somewhere in the middle. If omitted, it defaults to 0. - * - * pixelOffset gives the number of pixels *above* the offsetBase that the - * node (specifically, the bottom of it) will be positioned. If omitted, it - * defaults to 0. - */ - public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); - } - } - - public scrollToEventIfNeeded(eventId: string): void { - const node = this.getNodeForEventId(eventId); - if (node) { - node.scrollIntoView({ - block: "nearest", - behavior: "instant", - }); - } - } - - private isUnmounting = (): boolean => { - return !this.isMounted; - }; - - public get showHiddenEvents(): boolean { - return this.context?.showHiddenEvents ?? this._showHiddenEvents; - } - - // TODO: Implement granular (per-room) hide options - public shouldShowEvent(mxEv: MatrixEvent, forceHideEvents = false): boolean { - if (this.props.hideThreadedMessages && this.threadsEnabled && this.props.room) { - const { shouldLiveInRoom } = this.props.room.eventShouldLiveIn(mxEv, this.props.events); - if (!shouldLiveInRoom) { - return false; - } - } - - if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) { - return false; // ignored = no show (only happens if the ignore happens after an event was received) - } - - if (this.showHiddenEvents && !forceHideEvents) { - return true; - } - - if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) { - return false; // no tile = no show - } - - // Always show highlighted event - if (this.props.highlightedEventId === mxEv.getId()) return true; - - return !shouldHideEvent(mxEv, this.context); - } - - public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode { - const visible = !isLastEvent && this.props.readMarkerVisible; - - if (this.props.readMarkerEventId === eventId) { - let hr; - // if the read marker comes at the end of the timeline (except - // for local echoes, which are excluded from RMs, because they - // don't have useful event ids), we don't want to show it, but - // we still want to create the
  • for it so that the - // algorithms which depend on its position on the screen aren't - // confused. - if (visible) { - hr =
    ; - } - - return ( -
  • - { hr } -
  • - ); - } else if (this.state.ghostReadMarkers.includes(eventId)) { - // We render 'ghost' read markers in the DOM while they - // transition away. This allows the actual read marker - // to be in the right place straight away without having - // to wait for the transition to finish. - // There are probably much simpler ways to do this transition, - // possibly using react-transition-group which handles keeping - // elements in the DOM whilst they transition out, although our - // case is a little more complex because only some of the items - // transition (ie. the read markers do but the event tiles do not) - // and TransitionGroup requires that all its children are Transitions. - const hr =
    ; - - // give it a key which depends on the event id. That will ensure that - // we get a new DOM node (restarting the animation) when the ghost - // moves to a different event. - return ( -
  • - { hr } -
  • - ); - } - - return null; - } - - private collectGhostReadMarker = (node: HTMLElement): void => { - if (node) { - // now the element has appeared, change the style which will trigger the CSS transition - requestAnimationFrame(() => { - node.style.width = '10%'; - node.style.opacity = '0'; - }); - } - }; - - private onGhostTransitionEnd = (ev: TransitionEvent): void => { - // we can now clean up the ghost element - const finishedEventId = (ev.target as HTMLElement).dataset.eventid; - this.setState({ - ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId), - }); - }; - - private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } { - const nextEvent = i < arr.length - 1 - ? arr[i + 1] - : null; - - // The next event with tile is used to to determine the 'last successful' flag - // when rendering the tile. The shouldShowEvent function is pretty quick at what - // it does, so this should have no significant cost even when a room is used for - // not-chat purposes. - const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e)); - - return { nextEvent, nextTile }; - } - - private get pendingEditItem(): string | undefined { - if (!this.props.room) { - return undefined; - } - - try { - return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType)); - } catch (err) { - logger.error(err); - return undefined; - } - } - - private getEventTiles(): ReactNode[] { - let i; - - // first figure out which is the last event in the list which we're - // actually going to show; this allows us to behave slightly - // differently for the last event in the list. (eg show timestamp) - // - // we also need to figure out which is the last event we show which isn't - // a local echo, to manage the read-marker. - let lastShownEvent; - - let lastShownNonLocalEchoIndex = -1; - for (i = this.props.events.length-1; i >= 0; i--) { - const mxEv = this.props.events[i]; - if (!this.shouldShowEvent(mxEv)) { - continue; - } - - if (lastShownEvent === undefined) { - lastShownEvent = mxEv; - } - - if (mxEv.status) { - // this is a local echo - continue; - } - - lastShownNonLocalEchoIndex = i; - break; - } - - const ret = []; - - let prevEvent = null; // the last event we showed - - // Note: the EventTile might still render a "sent/sending receipt" independent of - // this information. When not providing read receipt information, the tile is likely - // to assume that sent receipts are to be shown more often. - this.readReceiptsByEvent = {}; - if (this.props.showReadReceipts) { - this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(); - } - - let grouper: BaseGrouper = null; - - for (i = 0; i < this.props.events.length; i++) { - const mxEv = this.props.events[i]; - const eventId = mxEv.getId(); - const last = (mxEv === lastShownEvent); - const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i); - - if (grouper) { - if (grouper.shouldGroup(mxEv)) { - grouper.add(mxEv); - continue; - } else { - // not part of group, so get the group tiles, close the - // group, and continue like a normal event - ret.push(...grouper.getTiles()); - prevEvent = grouper.getNewPrevEvent(); - grouper = null; - } - } - - for (const Grouper of groupers) { - if (Grouper.canStartGroup(this, mxEv) && !this.props.disableGrouping) { - grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); - break; // break on first grouper - } - } - - if (!grouper) { - if (this.shouldShowEvent(mxEv)) { - // make sure we unpack the array returned by getTilesForEvent, - // otherwise React will auto-generate keys, and we will end up - // replacing all the DOM elements every time we paginate. - ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, false, nextEvent, nextTile)); - prevEvent = mxEv; - } - - const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); - if (readMarker) ret.push(readMarker); - } - } - - if (grouper) { - ret.push(...grouper.getTiles()); - } - - return ret; - } - - public getTilesForEvent( - prevEvent: MatrixEvent, - mxEv: MatrixEvent, - last = false, - isGrouped = false, - nextEvent?: MatrixEvent, - nextEventWithTile?: MatrixEvent, - ): ReactNode[] { - const ret = []; - - const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId(); - // local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators. - let ts1 = mxEv.getTs(); - let eventDate = mxEv.getDate(); - if (mxEv.status) { - eventDate = new Date(); - ts1 = eventDate.getTime(); - } - - // do we need a date separator since the last event? - const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); - if (wantsDateSeparator && !isGrouped && this.props.room) { - const dateSeparator = ( -
  • - -
  • - ); - ret.push(dateSeparator); - } - - let lastInSection = true; - if (nextEventWithTile) { - const nextEv = nextEventWithTile; - const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date()); - lastInSection = willWantDateSeparator || - mxEv.getSender() !== nextEv.getSender() || - getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage || - !shouldFormContinuation( - mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, - ); - } - - // is this a continuation of the previous message? - const continuation = !wantsDateSeparator && - shouldFormContinuation( - prevEvent, mxEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, - ); - - const eventId = mxEv.getId(); - const highlight = (eventId === this.props.highlightedEventId); - - const readReceipts = this.readReceiptsByEvent[eventId]; - - let isLastSuccessful = false; - const isSentState = s => !s || s === 'sent'; - const isSent = isSentState(mxEv.getAssociatedStatus()); - const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); - if (!hasNextEvent && isSent) { - isLastSuccessful = true; - } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { - isLastSuccessful = true; - } - - // This is a bit nuanced, but if our next event is hidden but a future event is not - // hidden then we're not the last successful. - if ( - nextEventWithTile && - nextEventWithTile !== nextEvent && - isSentState(nextEventWithTile.getAssociatedStatus()) - ) { - isLastSuccessful = false; - } - - // We only want to consider "last successful" if the event is sent by us, otherwise of course - // it's successful: we received it. - isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); - - const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); - // use txnId as key if available so that we don't remount during sending - ret.push( - , - ); - - return ret; - } - - public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean { - if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { - return false; - } - if (prevEvent == null) { - // first event in the panel: depends if we could back-paginate from - // here. - return !this.props.canBackPaginate; - } - return wantsDateSeparator(prevEvent.getDate(), nextEventDate); - } - - // Get a list of read receipts that should be shown next to this event - // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. - private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] { - const myUserId = MatrixClientPeg.get().credentials.userId; - - // get list of read receipts, sorted most recent first - const { room } = this.props; - if (!room) { - return null; - } - const receipts: IReadReceiptProps[] = []; - room.getReceiptsForEvent(event).forEach((r) => { - if ( - !r.userId || - !isSupportedReceiptType(r.type) || - r.userId === myUserId - ) { - return; // ignore non-read receipts and receipts from self. - } - if (MatrixClientPeg.get().isUserIgnored(r.userId)) { - return; // ignore ignored users - } - const member = room.getMember(r.userId); - receipts.push({ - userId: r.userId, - roomMember: member, - ts: r.data ? r.data.ts : 0, - }); - }); - return receipts; - } - - // Get an object that maps from event ID to a list of read receipts that - // should be shown next to that event. If a hidden event has read receipts, - // they are folded into the receipts of the last shown event. - private getReadReceiptsByShownEvent(): Record { - const receiptsByEvent = {}; - const receiptsByUserId = {}; - - let lastShownEventId; - for (const event of this.props.events) { - if (this.shouldShowEvent(event)) { - lastShownEventId = event.getId(); - } - if (!lastShownEventId) { - continue; - } - - const existingReceipts = receiptsByEvent[lastShownEventId] || []; - const newReceipts = this.getReadReceiptsForEvent(event); - receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); - - // Record these receipts along with their last shown event ID for - // each associated user ID. - for (const receipt of newReceipts) { - receiptsByUserId[receipt.userId] = { - lastShownEventId, - receipt, - }; - } - } - - // It's possible in some cases (for example, when a read receipt - // advances before we have paginated in the new event that it's marking - // received) that we can temporarily not have a matching event for - // someone which had one in the last. By looking through our previous - // mapping of receipts by user ID, we can cover recover any receipts - // that would have been lost by using the same event ID from last time. - for (const userId in this.readReceiptsByUserId) { - if (receiptsByUserId[userId]) { - continue; - } - const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId]; - const existingReceipts = receiptsByEvent[lastShownEventId] || []; - receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); - receiptsByUserId[userId] = { lastShownEventId, receipt }; - } - this.readReceiptsByUserId = receiptsByUserId; - - // After grouping receipts by shown events, do another pass to sort each - // receipt list. - for (const eventId in receiptsByEvent) { - receiptsByEvent[eventId].sort((r1, r2) => { - return r2.ts - r1.ts; - }); - } - - return receiptsByEvent; - } - - private collectEventTile = (eventId: string, node: UnwrappedEventTile): void => { - this.eventTiles[eventId] = node; - }; - - // once dynamic content in the events load, make the scrollPanel check the - // scroll offsets. - public onHeightChanged = (): void => { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - scrollPanel.checkScroll(); - } - }; - - private onTypingShown = (): void => { - const scrollPanel = this.scrollPanel.current; - // this will make the timeline grow, so checkScroll - scrollPanel.checkScroll(); - if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { - scrollPanel.preventShrinking(); - } - }; - - private onTypingHidden = (): void => { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - // as hiding the typing notifications doesn't - // update the scrollPanel, we tell it to apply - // the shrinking prevention once the typing notifs are hidden - scrollPanel.updatePreventShrinking(); - // order is important here as checkScroll will scroll down to - // reveal added padding to balance the notifs disappearing. - scrollPanel.checkScroll(); - } - }; - - public updateTimelineMinHeight(): void { - const scrollPanel = this.scrollPanel.current; - - if (scrollPanel) { - const isAtBottom = scrollPanel.isAtBottom(); - const whoIsTyping = this.whoIsTyping.current; - const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); - // when messages get added to the timeline, - // but somebody else is still typing, - // update the min-height, so once the last - // person stops typing, no jumping occurs - if (isAtBottom && isTypingVisible) { - scrollPanel.preventShrinking(); - } - } - } - - public onTimelineReset(): void { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - scrollPanel.clearPreventShrinking(); - } - } - - render() { - let topSpinner; - let bottomSpinner; - if (this.props.backPaginating) { - topSpinner =
  • ; - } - if (this.props.forwardPaginating) { - bottomSpinner =
  • ; - } - - const style = this.props.hidden ? { display: 'none' } : {}; - - let whoIsTyping; - if (this.props.room && - this.state.showTypingNotifications && - this.context.timelineRenderingType === TimelineRenderingType.Room - ) { - whoIsTyping = ( - ); - } - - let ircResizer = null; - if (this.props.layout == Layout.IRC) { - ircResizer = ; - } - - const classes = classNames(this.props.className, { - "mx_MessagePanel_narrow": this.context.narrow, - }); - - return ( - - - { topSpinner } - { this.getEventTiles() } - { whoIsTyping } - { bottomSpinner } - - - ); - } -} - -abstract class BaseGrouper { - static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true; - - public events: MatrixEvent[] = []; - // events that we include in the group but then eject out and place above the group. - public ejectedEvents: MatrixEvent[] = []; - public readMarker: ReactNode; - - constructor( - public readonly panel: MessagePanel, - public readonly event: MatrixEvent, - public readonly prevEvent: MatrixEvent, - public readonly lastShownEvent: MatrixEvent, - public readonly nextEvent?: MatrixEvent, - public readonly nextEventTile?: MatrixEvent, - ) { - this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent); - } - - public abstract shouldGroup(ev: MatrixEvent): boolean; - public abstract add(ev: MatrixEvent): void; - public abstract getTiles(): ReactNode[]; - public abstract getNewPrevEvent(): MatrixEvent; -} - -/* Grouper classes determine when events can be grouped together in a summary. - * Groupers should have the following methods: - * - canStartGroup (static): determines if a new group should be started with the - * given event - * - shouldGroup: determines if the given event should be added to an existing group - * - add: adds an event to an existing group (should only be called if shouldGroup - * return true) - * - getTiles: returns the tiles that represent the group - * - getNewPrevEvent: returns the event that should be used as the new prevEvent - * when determining things such as whether a date separator is necessary - */ - -// Wrap initial room creation events into a GenericEventListSummary -// Grouping only events sent by the same user that sent the `m.room.create` and only until -// the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event -class CreationGrouper extends BaseGrouper { - static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { - return ev.getType() === EventType.RoomCreate; - }; - - public shouldGroup(ev: MatrixEvent): boolean { - const panel = this.panel; - const createEvent = this.event; - if (!panel.shouldShowEvent(ev)) { - return true; - } - if (panel.wantsDateSeparator(this.event, ev.getDate())) { - return false; - } - if (ev.getType() === EventType.RoomMember - && (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) { - return false; - } - // beacons are not part of room creation configuration - // should be shown in timeline - if (M_BEACON_INFO.matches(ev.getType())) { - return false; - } - if (ev.isState() && ev.getSender() === createEvent.getSender()) { - return true; - } - - return false; - } - - public add(ev: MatrixEvent): void { - const panel = this.panel; - this.readMarker = this.readMarker || panel.readMarkerForEvent( - ev.getId(), - ev === this.lastShownEvent, - ); - if (!panel.shouldShowEvent(ev)) { - return; - } - if (ev.getType() === EventType.RoomEncryption) { - this.ejectedEvents.push(ev); - } else { - this.events.push(ev); - } - } - - public getTiles(): ReactNode[] { - // If we don't have any events to group, don't even try to group them. The logic - // below assumes that we have a group of events to deal with, but we might not if - // the events we were supposed to group were redacted. - if (!this.events || !this.events.length) return []; - - const panel = this.panel; - const ret: ReactNode[] = []; - const isGrouped = true; - const createEvent = this.event; - const lastShownEvent = this.lastShownEvent; - - if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) { - const ts = createEvent.getTs(); - ret.push( -
  • , - ); - } - - // If this m.room.create event should be shown (room upgrade) then show it before the summary - if (panel.shouldShowEvent(createEvent)) { - // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered - ret.push(...panel.getTilesForEvent(createEvent, createEvent)); - } - - for (const ejected of this.ejectedEvents) { - ret.push(...panel.getTilesForEvent( - createEvent, ejected, createEvent === lastShownEvent, isGrouped, - )); - } - - const eventTiles = this.events.map((e) => { - // In order to prevent DateSeparators from appearing in the expanded form - // of GenericEventListSummary, render each member event as if the previous - // one was itself. This way, the timestamp of the previous event === the - // timestamp of the current event, and no DateSeparator is inserted. - return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); - }).reduce((a, b) => a.concat(b), []); - // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one - const ev = this.events[this.events.length - 1]; - - let summaryText: string; - const roomId = ev.getRoomId(); - const creator = ev.sender ? ev.sender.name : ev.getSender(); - if (DMRoomMap.shared().getUserIdForRoomId(roomId)) { - summaryText = _t("%(creator)s created this DM.", { creator }); - } else { - summaryText = _t("%(creator)s created and configured the room.", { creator }); - } - - ret.push(); - - ret.push( - - { eventTiles } - , - ); - - if (this.readMarker) { - ret.push(this.readMarker); - } - - return ret; - } - - public getNewPrevEvent(): MatrixEvent { - return this.event; - } -} - -// Wrap consecutive grouped events in a ListSummary -class MainGrouper extends BaseGrouper { - static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { - if (!panel.shouldShowEvent(ev)) return false; - - if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { - return true; - } - - if (ev.isRedacted()) { - return true; - } - - if (panel.showHiddenEvents && !panel.shouldShowEvent(ev, true)) { - return true; - } - - return false; - }; - - constructor( - public readonly panel: MessagePanel, - public readonly event: MatrixEvent, - public readonly prevEvent: MatrixEvent, - public readonly lastShownEvent: MatrixEvent, - nextEvent: MatrixEvent, - nextEventTile: MatrixEvent, - ) { - super(panel, event, prevEvent, lastShownEvent, nextEvent, nextEventTile); - this.events = [event]; - } - - public shouldGroup(ev: MatrixEvent): boolean { - if (!this.panel.shouldShowEvent(ev)) { - // absorb hidden events so that they do not break up streams of messages & redaction events being grouped - return true; - } - if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { - return false; - } - if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { - return true; - } - if (ev.isRedacted()) { - return true; - } - if (this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev, true)) { - return true; - } - return false; - } - - public add(ev: MatrixEvent): void { - if (ev.getType() === EventType.RoomMember) { - // We can ignore any events that don't actually have a message to display - if (!hasText(ev, this.panel.showHiddenEvents)) return; - } - this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent); - if (!this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev)) { - // absorb hidden events to not split the summary - return; - } - this.events.push(ev); - } - - private generateKey(): string { - return "eventlistsummary-" + this.events[0].getId(); - } - - public getTiles(): ReactNode[] { - // If we don't have any events to group, don't even try to group them. The logic - // below assumes that we have a group of events to deal with, but we might not if - // the events we were supposed to group were redacted. - if (!this.events?.length) return []; - - const isGrouped = true; - const panel = this.panel; - const lastShownEvent = this.lastShownEvent; - const ret: ReactNode[] = []; - - if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { - const ts = this.events[0].getTs(); - ret.push( -
  • , - ); - } - - // Ensure that the key of the EventListSummary does not change with new events in either direction. - // This will prevent it from being re-created unnecessarily, and instead will allow new props to be provided. - // In turn, the shouldComponentUpdate method on ELS can be used to prevent unnecessary renderings. - const keyEvent = this.events.find(e => this.panel.grouperKeyMap.get(e)); - const key = keyEvent ? this.panel.grouperKeyMap.get(keyEvent) : this.generateKey(); - if (!keyEvent) { - // Populate the weak map with the key. - // Note that we only set the key on the specific event it refers to, since this group might get - // split up in the future by other intervening events. If we were to set the key on all events - // currently in the group, we would risk later giving the same key to multiple groups. - this.panel.grouperKeyMap.set(this.events[0], key); - } - - let highlightInSummary = false; - let eventTiles = this.events.map((e, i) => { - if (e.getId() === panel.props.highlightedEventId) { - highlightInSummary = true; - } - return panel.getTilesForEvent( - i === 0 ? this.prevEvent : this.events[i - 1], - e, - e === lastShownEvent, - isGrouped, - this.nextEvent, - this.nextEventTile, - ); - }).reduce((a, b) => a.concat(b), []); - - if (eventTiles.length === 0) { - eventTiles = null; - } - - // If a membership event is the start of visible history, tell the user - // why they can't see earlier messages - if (!this.panel.props.canBackPaginate && !this.prevEvent) { - ret.push(); - } - - ret.push( - - { eventTiles } - , - ); - - if (this.readMarker) { - ret.push(this.readMarker); - } - - return ret; - } - - public getNewPrevEvent(): MatrixEvent { - return this.events[this.events.length - 1]; - } -} - -// all the grouper classes that we use, ordered by priority -const groupers = [CreationGrouper, MainGrouper]; From 9d2b15f136ffc2f22829f4e29e40471710a4adef Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 19:30:35 -0400 Subject: [PATCH 015/176] fixed issue in autocomplete where title of normal emote was notsurrounded in colons --- src/autocomplete/EmojiProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index f96cce8dfc7..a6a857493ac 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -167,7 +167,7 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.map(c => ({ completion: c.emoji.unicode, component: ( - + { this.emotes[c.emoji.shortcodes[0]]? this.emotes[c.emoji.shortcodes[0]]:c.emoji.unicode } ), From 147fd66f31bfda62fd0324efafa44b2c3e2d6804 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 20:17:02 -0400 Subject: [PATCH 016/176] changed i18n files to include emote phrases --- src/i18n/strings/en_EN.json | 218 +++++++++++++++--------------------- src/i18n/strings/en_US.json | 3 + 2 files changed, 91 insertions(+), 130 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 070fb79b4db..52c75a311df 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -16,6 +16,40 @@ "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", + "Call Failed": "Call Failed", + "User Busy": "User Busy", + "The user you called is busy.": "The user you called is busy.", + "The call could not be established": "The call could not be established", + "Answered Elsewhere": "Answered Elsewhere", + "The call was answered on another device.": "The call was answered on another device.", + "Call failed due to misconfigured server": "Call failed due to misconfigured server", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", + "Try using turn.matrix.org": "Try using turn.matrix.org", + "OK": "OK", + "Unable to access microphone": "Unable to access microphone", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", + "Unable to access webcam / microphone": "Unable to access webcam / microphone", + "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", + "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", + "Permission is granted to use the webcam": "Permission is granted to use the webcam", + "No other application is using the webcam": "No other application is using the webcam", + "Already in call": "Already in call", + "You're already in a call with this person.": "You're already in a call with this person.", + "Calls are unsupported": "Calls are unsupported", + "You cannot place calls in this browser.": "You cannot place calls in this browser.", + "Connectivity to the server has been lost": "Connectivity to the server has been lost", + "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", + "Too Many Calls": "Too Many Calls", + "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", + "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", + "Unable to transfer call": "Unable to transfer call", + "Transfer Failed": "Transfer Failed", + "Failed to transfer call": "Failed to transfer call", + "Permission Required": "Permission Required", + "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "Upload Failed": "Upload Failed", @@ -58,53 +92,11 @@ "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Trust": "Trust", - "Call Failed": "Call Failed", - "User Busy": "User Busy", - "The user you called is busy.": "The user you called is busy.", - "The call could not be established": "The call could not be established", - "Answered Elsewhere": "Answered Elsewhere", - "The call was answered on another device.": "The call was answered on another device.", - "Call failed due to misconfigured server": "Call failed due to misconfigured server", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", - "Try using turn.matrix.org": "Try using turn.matrix.org", - "OK": "OK", - "Unable to access microphone": "Unable to access microphone", - "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", - "Unable to access webcam / microphone": "Unable to access webcam / microphone", - "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", - "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", - "Permission is granted to use the webcam": "Permission is granted to use the webcam", - "No other application is using the webcam": "No other application is using the webcam", - "Already in call": "Already in call", - "You're already in a call with this person.": "You're already in a call with this person.", - "Calls are unsupported": "Calls are unsupported", - "You cannot place calls in this browser.": "You cannot place calls in this browser.", - "Connectivity to the server has been lost": "Connectivity to the server has been lost", - "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", - "Too Many Calls": "Too Many Calls", - "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", - "You cannot place a call with yourself.": "You cannot place a call with yourself.", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", - "Unable to transfer call": "Unable to transfer call", - "Transfer Failed": "Transfer Failed", - "Failed to transfer call": "Failed to transfer call", - "Permission Required": "Permission Required", - "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "We couldn't log you in": "We couldn't log you in", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", "Try again": "Try again", "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.", "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.", - "Empty room": "Empty room", - "%(user1)s and %(user2)s": "%(user1)s and %(user2)s", - "%(user)s and %(count)s others|other": "%(user)s and %(count)s others", - "%(user)s and %(count)s others|one": "%(user)s and 1 other", - "Inviting %(user1)s and %(user2)s": "Inviting %(user1)s and %(user2)s", - "Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others", - "Inviting %(user)s and %(count)s others|one": "Inviting %(user)s and 1 other", - "Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)", "%(name)s is requesting verification": "%(name)s is requesting verification", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again", @@ -907,9 +899,10 @@ "Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)", "Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)", "Send read receipts": "Send read receipts", + "Right-click message context menu": "Right-click message context menu", + "Location sharing - pin drop": "Location sharing - pin drop", "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", "Favourite Messages (under active development)": "Favourite Messages (under active development)", - "Use new session manager (under active development)": "Use new session manager (under active development)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", @@ -958,7 +951,6 @@ "Order rooms by name": "Order rooms by name", "Show rooms with unread notifications first": "Show rooms with unread notifications first", "Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list", - "Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list", "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", @@ -1010,8 +1002,8 @@ "Get stuff done by finding your teammates": "Get stuff done by finding your teammates", "Find people": "Find people", "Find and invite your community members": "Find and invite your community members", - "Download %(brand)s": "Download %(brand)s", - "Don’t miss a thing by taking %(brand)s with you": "Don’t miss a thing by taking %(brand)s with you", + "Download Element": "Download Element", + "Don’t miss a thing by taking Element with you": "Don’t miss a thing by taking Element with you", "Download apps": "Download apps", "Set up your profile": "Set up your profile", "Make sure people know it’s really you": "Make sure people know it’s really you", @@ -1039,18 +1031,6 @@ "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", - "%(count)s people joined|other": "%(count)s people joined", - "%(count)s people joined|one": "%(count)s person joined", - "Audio devices": "Audio devices", - "Audio input %(n)s": "Audio input %(n)s", - "Mute microphone": "Mute microphone", - "Unmute microphone": "Unmute microphone", - "Video devices": "Video devices", - "Video input %(n)s": "Video input %(n)s", - "Turn off camera": "Turn off camera", - "Turn on camera": "Turn on camera", - "Join": "Join", - "Dial": "Dial", "You are presenting": "You are presenting", "%(sharerName)s is presenting": "%(sharerName)s is presenting", "Your camera is turned off": "Your camera is turned off", @@ -1061,6 +1041,16 @@ "You held the call Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", "Connecting": "Connecting", + "Dial": "Dial", + "%(count)s people joined|other": "%(count)s people joined", + "%(count)s people joined|one": "%(count)s person joined", + "Audio devices": "Audio devices", + "Mute microphone": "Mute microphone", + "Unmute microphone": "Unmute microphone", + "Video devices": "Video devices", + "Turn off camera": "Turn off camera", + "Turn on camera": "Turn on camera", + "Join": "Join", "Dialpad": "Dialpad", "Mute the microphone": "Mute the microphone", "Unmute the microphone": "Unmute the microphone", @@ -1157,9 +1147,8 @@ "Anchor": "Anchor", "Headphones": "Headphones", "Folder": "Folder", - "Welcome": "Welcome", - "How are you finding %(brand)s so far?": "How are you finding %(brand)s so far?", - "We’d appreciate any feedback on how you’re finding %(brand)s.": "We’d appreciate any feedback on how you’re finding %(brand)s.", + "How are you finding Element so far?": "How are you finding Element so far?", + "We’d appreciate any feedback on how you’re finding Element.": "We’d appreciate any feedback on how you’re finding Element.", "Feedback": "Feedback", "Secure messaging for friends and family": "Secure messaging for friends and family", "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.", @@ -1295,6 +1284,15 @@ "Session key:": "Session key:", "Your homeserver does not support device management.": "Your homeserver does not support device management.", "Unable to load device list": "Unable to load device list", + "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", + "Confirm signing out these devices|other": "Confirm signing out these devices", + "Confirm signing out these devices|one": "Confirm signing out this device", + "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", + "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", + "Sign out devices|other": "Sign out devices", + "Sign out devices|one": "Sign out device", + "Authentication": "Authentication", "Deselect all": "Deselect all", "Select all": "Select all", "Verified devices": "Verified devices", @@ -1305,6 +1303,7 @@ "You aren't signed into any other devices.": "You aren't signed into any other devices.", "This device": "This device", "Failed to set display name": "Failed to set display name", + "Last seen %(date)s at %(ip)s": "Last seen %(date)s at %(ip)s", "Sign Out": "Sign Out", "Display Name": "Display Name", "Rename": "Rename", @@ -1564,9 +1563,6 @@ "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", - "Sessions": "Sessions", - "Other sessions": "Other sessions", - "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", @@ -1657,7 +1653,7 @@ "Select the roles required to change various parts of the space": "Select the roles required to change various parts of the space", "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", "Are you sure you want to add encryption to this public room?": "Are you sure you want to add encryption to this public room?", - "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", + "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", "To avoid these issues, create a new encrypted room for the conversation you plan to have.": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", "Enable encryption?": "Enable encryption?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", @@ -1695,51 +1691,6 @@ "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", - "Current session": "Current session", - "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", - "Confirm signing out these devices|other": "Confirm signing out these devices", - "Confirm signing out these devices|one": "Confirm signing out this device", - "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", - "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", - "Sign out devices|other": "Sign out devices", - "Sign out devices|one": "Sign out device", - "Authentication": "Authentication", - "Session ID": "Session ID", - "Last activity": "Last activity", - "Device": "Device", - "IP address": "IP address", - "Session details": "Session details", - "Toggle device details": "Toggle device details", - "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", - "Verified": "Verified", - "Unverified": "Unverified", - "Unknown device type": "Unknown device type", - "Verified session": "Verified session", - "This session is ready for secure messaging.": "This session is ready for secure messaging.", - "Unverified session": "Unverified session", - "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", - "Verified sessions": "Verified sessions", - "For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.", - "Unverified sessions": "Unverified sessions", - "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", - "Inactive sessions": "Inactive sessions", - "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", - "No verified sessions found.": "No verified sessions found.", - "No unverified sessions found.": "No unverified sessions found.", - "No inactive sessions found.": "No inactive sessions found.", - "No sessions found.": "No sessions found.", - "Show all": "Show all", - "All": "All", - "Ready for secure messaging": "Ready for secure messaging", - "Not ready for secure messaging": "Not ready for secure messaging", - "Inactive": "Inactive", - "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer", - "Filter devices": "Filter devices", - "Show": "Show", - "Security recommendations": "Security recommendations", - "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", - "View all": "View all", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", @@ -1805,6 +1756,8 @@ "%(seconds)ss left": "%(seconds)ss left", "Send voice message": "Send voice message", "Emoji": "Emoji", + "Emote": "Emote", + "Emotes": "Emotes", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", @@ -1894,6 +1847,7 @@ "System Alerts": "System Alerts", "Historical": "Historical", "Suggested Rooms": "Suggested Rooms", + "Empty room": "Empty room", "Add space": "Add space", "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", @@ -1974,11 +1928,6 @@ "%(count)s unread messages.|other": "%(count)s unread messages.", "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", - "Video": "Video", - "Joining…": "Joining…", - "Joined": "Joined", - "%(count)s participants|other": "%(count)s participants", - "%(count)s participants|one": "1 participant", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", "This room is running room version , which this homeserver has marked as unstable.": "This room is running room version , which this homeserver has marked as unstable.", @@ -2000,6 +1949,11 @@ "Open thread": "Open thread", "Jump to first unread message.": "Jump to first unread message.", "Mark all as read": "Mark all as read", + "Video": "Video", + "Joining…": "Joining…", + "Joined": "Joined", + "%(count)s participants|other": "%(count)s participants", + "%(count)s participants|one": "1 participant", "Unable to access your microphone": "Unable to access your microphone", "We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.", "No microphone found": "No microphone found", @@ -2157,6 +2111,17 @@ "%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.", "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", + "Call declined": "Call declined", + "Call back": "Call back", + "No answer": "No answer", + "Could not connect media": "Could not connect media", + "Connection failed": "Connection failed", + "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", + "An unknown error occurred": "An unknown error occurred", + "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", + "Retry": "Retry", + "Missed call": "Missed call", + "The call is in an unknown state!": "The call is in an unknown state!", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -2187,17 +2152,6 @@ "Message pending moderation": "Message pending moderation", "Pick a date to jump to": "Pick a date to jump to", "Go": "Go", - "Call declined": "Call declined", - "Call back": "Call back", - "No answer": "No answer", - "Could not connect media": "Could not connect media", - "Connection failed": "Connection failed", - "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", - "An unknown error occurred": "An unknown error occurred", - "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", - "Retry": "Retry", - "Missed call": "Missed call", - "The call is in an unknown state!": "The call is in an unknown state!", "Error processing audio message": "Error processing audio message", "View live location": "View live location", "React": "React", @@ -2258,6 +2212,7 @@ "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", + "Show all": "Show all", "Reactions": "Reactions", "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s", "reacted with %(shortName)s": "reacted with %(shortName)s", @@ -2504,6 +2459,7 @@ "We don't record or profile any account data": "We don't record or profile any account data", "We don't share information with third parties": "We don't share information with third parties", "You can turn this off anytime in settings": "You can turn this off anytime in settings", + "Download %(brand)s": "Download %(brand)s", "Download %(brand)s Desktop": "Download %(brand)s Desktop", "iOS": "iOS", "Download on the App Store": "Download on the App Store", @@ -2722,6 +2678,7 @@ "a key signature": "a key signature", "%(brand)s encountered an error during upload of:": "%(brand)s encountered an error during upload of:", "Upload completed": "Upload completed", + "Upload Emote": "Upload Emote", "Cancelled signature upload": "Cancelled signature upload", "Unable to upload": "Unable to upload", "Signature upload success": "Signature upload success", @@ -2762,6 +2719,7 @@ "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", + "Session ID": "Session ID", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", "Verify session": "Verify session", @@ -2876,8 +2834,8 @@ "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", "Not Trusted": "Not Trusted", - "Manually verify by text": "Manually verify by text", - "Interactively verify by emoji": "Interactively verify by emoji", + "Manually Verify by Text": "Manually Verify by Text", + "Interactively verify by Emoji": "Interactively verify by Emoji", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload all": "Upload all", @@ -3020,11 +2978,11 @@ "Observe only": "Observe only", "No verification requests found": "No verification requests found", "There was an error finding this widget.": "There was an error finding this widget.", + "Resume": "Resume", + "Hold": "Hold", "Input devices": "Input devices", "Output devices": "Output devices", "Cameras": "Cameras", - "Resume": "Resume", - "Hold": "Hold", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Open in OpenStreetMap": "Open in OpenStreetMap", "Forward": "Forward", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 5f68a61b47c..1510a42b4b8 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -50,6 +50,8 @@ "Email": "Email", "Email address": "Email address", "Emoji": "Emoji", + "Emote": "Emote", + "Emotes": "Emotes", "Error": "Error", "Error decrypting attachment": "Error decrypting attachment", "Export": "Export", @@ -180,6 +182,7 @@ "Unmute": "Unmute", "Upload avatar": "Upload avatar", "Upload Failed": "Upload Failed", + "Upload Emote": "Upload Emote", "Usage": "Usage", "Users": "Users", "Verification Pending": "Verification Pending", From 92d3bae0574a40a509f6c3352e8053b0c5ab8aeb Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 21:24:40 -0400 Subject: [PATCH 017/176] added some missing css --- res/css/_components.pcss | 1 + .../views/dialogs/_RoomSettingsDialog.pcss | 3 + res/css/views/settings/_EmoteSettings.pcss | 77 +++++++++++++++++++ .../views/dialogs/RoomSettingsDialog.tsx | 4 +- 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 res/css/views/settings/_EmoteSettings.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 640184c8cab..c9134ff37ee 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -305,6 +305,7 @@ @import "./views/settings/_Notifications.pcss"; @import "./views/settings/_PhoneNumbers.pcss"; @import "./views/settings/_ProfileSettings.pcss"; +@import "./views/settings/_EmoteSettings.pcss"; @import "./views/settings/_SecureBackupPanel.pcss"; @import "./views/settings/_SetIdServer.pcss"; @import "./views/settings/_SetIntegrationManager.pcss"; diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index a242a99596b..5fadab8083a 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -41,6 +41,9 @@ limitations under the License. .mx_RoomSettingsDialog_warningIcon::before { mask-image: url('$(res)/img/element-icons/room/settings/advanced.svg'); } +.mx_RoomSettingsDialog_emotesIcon::before { + mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); +} .mx_RoomSettingsDialog .mx_Dialog_title { -ms-text-overflow: ellipsis; diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss new file mode 100644 index 00000000000..21c65311cec --- /dev/null +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -0,0 +1,77 @@ +/* +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EmoteSettings { + margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end); + border-bottom: 1px solid $quinary-content; + + .mx_EmoteSettings_emoteUpload { + display: none; + } + .mx_EmoteSettings_addEmoteField { + display: flex; + width: 100%; + } + .mx_EmoteSettings_emoteField { + } + .mx_EmoteSettings_uploadButton { + margin-left:auto; + align-self: center; + } + .mx_EmoteSettings_uploadedEmoteImage{ + height: 30px; + width: var(--emote-image-width)*30/var(--emote-image-height); + margin-left:30px; + align-self: center; + } + .mx_EmoteSettings_Emote { + display: flex; + + .mx_EmoteSettings_Emote_controls { + flex-grow: 1; + margin-inline-end: 54px; + + .mx_Field:first-child { + margin-top: 0; + } + + .mx_EmoteSettings_Emote_controls_topic { + & > textarea { + font-family: inherit; + resize: vertical; + } + + &.mx_EmoteSettings_Emote_controls_topic--room textarea { + min-height: 4em; + } + } + + .mx_EmoteSettings_Emote_controls_userId { + margin-inline-end: $spacing-20; + } + } + } + + .mx_EmoteSettings_buttons { + margin-top: 10px; /* 18px is already accounted for by the

    above the buttons */ + //margin-bottom: $spacing-28; + + > .mx_AccessibleButton_kind_link { + font-size: $font-14px; + margin-inline-end: 10px; /* TODO: Use a spacing variable */ + } + } +} diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 467c75120e2..ed50d97b21e 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -124,11 +124,11 @@ export default class RoomSettingsDialog extends React.Component )); tabs.push(new Tab( - ROOM_NOTIFICATIONS_TAB, + ROOM_EMOTES_TAB, _td("Emotes"), "mx_RoomSettingsDialog_emotesIcon", , - "RoomSettingsNotifications", + "RoomSettingsEmotes", )); if (SettingsStore.getValue("feature_bridge_state")) { tabs.push(new Tab( From a2146be98602e88d7e61cc6ad3a7b5933c3907ed Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:13:09 -0400 Subject: [PATCH 018/176] added some more missing changes --- res/css/views/settings/_EmoteSettings.pcss | 8 ++++---- src/autocomplete/EmojiProvider.tsx | 8 ++++---- src/components/views/room_settings/RoomEmoteSettings.tsx | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index 21c65311cec..6f4222fc764 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -28,13 +28,13 @@ limitations under the License. .mx_EmoteSettings_emoteField { } .mx_EmoteSettings_uploadButton { - margin-left:auto; + margin-left: auto; align-self: center; } - .mx_EmoteSettings_uploadedEmoteImage{ + .mx_EmoteSettings_uploadedEmoteImage { height: 30px; - width: var(--emote-image-width)*30/var(--emote-image-height); - margin-left:30px; + width: var(--emote-image-width) *30/var(--emote-image-height); + margin-left: 30px; align-self: center; } .mx_EmoteSettings_Emote { diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index a6a857493ac..e0859d8d953 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -115,8 +115,8 @@ export default class EmojiProvider extends AutocompleteProvider { emojisAndEmotes.push({ emoji: { label: key, shortcodes: [this.emotes[key]], - hexcode: "", - unicode: ":"+key+":", + hexcode: key, + unicode: this.emotes[key], }, _orderBy: 0, @@ -167,8 +167,8 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.map(c => ({ completion: c.emoji.unicode, component: ( - - { this.emotes[c.emoji.shortcodes[0]]? this.emotes[c.emoji.shortcodes[0]]:c.emoji.unicode } + + { this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode } ), range, diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 77c8a4e24c0..bc8ad73e888 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -310,6 +310,7 @@ export default class RoomEmoteSettings extends React.Component { onChange={this.onEmoteFileAdd} accept="image/*" /> + { emoteSettingsButtons }

  • { { existingEmotes } - { emoteSettingsButtons } + ); } From 7da8d5f098ddc09befe0b617e768241612d808b1 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:27:26 -0400 Subject: [PATCH 019/176] lint fixes 6 --- res/css/views/settings/_EmoteSettings.pcss | 2 -- src/components/views/dialogs/RoomSettingsDialog.tsx | 2 +- src/components/views/room_settings/RoomEmoteSettings.tsx | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index 6f4222fc764..4dee60d6fbc 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -25,8 +25,6 @@ limitations under the License. display: flex; width: 100%; } - .mx_EmoteSettings_emoteField { - } .mx_EmoteSettings_uploadButton { margin-left: auto; align-self: center; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index ed50d97b21e..4d99d6f9089 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -128,7 +128,7 @@ export default class RoomSettingsDialog extends React.Component _td("Emotes"), "mx_RoomSettingsDialog_emotesIcon", , - "RoomSettingsEmotes", + "RoomSettingsNotifications", )); if (SettingsStore.getValue("feature_bridge_state")) { tabs.push(new Tab( diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index bc8ad73e888..4608df1397c 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -334,7 +334,6 @@ export default class RoomEmoteSettings extends React.Component { { existingEmotes } - ); } From 495774a3e187514f23dd0693797c0c4d1bd2bc89 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 20:44:52 -0400 Subject: [PATCH 020/176] i18n fixes/changes --- src/i18n/strings/en_EN.json | 3 --- src/i18n/strings/en_US.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 52c75a311df..07b563820da 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1756,8 +1756,6 @@ "%(seconds)ss left": "%(seconds)ss left", "Send voice message": "Send voice message", "Emoji": "Emoji", - "Emote": "Emote", - "Emotes": "Emotes", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", @@ -2678,7 +2676,6 @@ "a key signature": "a key signature", "%(brand)s encountered an error during upload of:": "%(brand)s encountered an error during upload of:", "Upload completed": "Upload completed", - "Upload Emote": "Upload Emote", "Cancelled signature upload": "Cancelled signature upload", "Unable to upload": "Unable to upload", "Signature upload success": "Signature upload success", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 1510a42b4b8..5f68a61b47c 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -50,8 +50,6 @@ "Email": "Email", "Email address": "Email address", "Emoji": "Emoji", - "Emote": "Emote", - "Emotes": "Emotes", "Error": "Error", "Error decrypting attachment": "Error decrypting attachment", "Export": "Export", @@ -182,7 +180,6 @@ "Unmute": "Unmute", "Upload avatar": "Upload avatar", "Upload Failed": "Upload Failed", - "Upload Emote": "Upload Emote", "Usage": "Usage", "Users": "Users", "Verification Pending": "Verification Pending", From 12d7bdaec8b6a97669c2e5c20ee812ed22633644 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 20:53:35 -0400 Subject: [PATCH 021/176] i18n changes retry --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 07b563820da..dbc0843cf69 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1756,6 +1756,8 @@ "%(seconds)ss left": "%(seconds)ss left", "Send voice message": "Send voice message", "Emoji": "Emoji", + "Emotes": "Emotes", + "Upload Emote": "Upload Emote", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", From 5c04be4c327e02cf9a04c48afe10c5da5c9414b8 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:35:22 -0400 Subject: [PATCH 022/176] i18n changes retry 3 --- src/i18n/strings/en_EN.json | 125 ++++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 41 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dbc0843cf69..3d1b5241cad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -97,6 +97,12 @@ "Try again": "Try again", "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.", "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.", + "Empty room": "Empty room", + "%(user1)s and %(user2)s": "%(user1)s and %(user2)s", + "%(user)s and %(count)s others|other": "%(user)s and %(count)s others", + "Inviting %(user1)s and %(user2)s": "Inviting %(user1)s and %(user2)s", + "Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others", + "Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)", "%(name)s is requesting verification": "%(name)s is requesting verification", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again", @@ -767,6 +773,8 @@ "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", + "%(creator)s created this DM.": "%(creator)s created this DM.", + "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", "Starting export process...": "Starting export process...", "Fetching events...": "Fetching events...", @@ -899,10 +907,9 @@ "Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)", "Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)", "Send read receipts": "Send read receipts", - "Right-click message context menu": "Right-click message context menu", - "Location sharing - pin drop": "Location sharing - pin drop", "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", "Favourite Messages (under active development)": "Favourite Messages (under active development)", + "Use new session manager (under active development)": "Use new session manager (under active development)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", @@ -951,6 +958,7 @@ "Order rooms by name": "Order rooms by name", "Show rooms with unread notifications first": "Show rooms with unread notifications first", "Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list", + "Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list", "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)", "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", @@ -1002,8 +1010,8 @@ "Get stuff done by finding your teammates": "Get stuff done by finding your teammates", "Find people": "Find people", "Find and invite your community members": "Find and invite your community members", - "Download Element": "Download Element", - "Don’t miss a thing by taking Element with you": "Don’t miss a thing by taking Element with you", + "Download %(brand)s": "Download %(brand)s", + "Don’t miss a thing by taking %(brand)s with you": "Don’t miss a thing by taking %(brand)s with you", "Download apps": "Download apps", "Set up your profile": "Set up your profile", "Make sure people know it’s really you": "Make sure people know it’s really you", @@ -1031,6 +1039,17 @@ "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", + "%(count)s people joined|other": "%(count)s people joined", + "%(count)s people joined|one": "%(count)s person joined", + "Audio devices": "Audio devices", + "Audio input %(n)s": "Audio input %(n)s", + "Mute microphone": "Mute microphone", + "Unmute microphone": "Unmute microphone", + "Video devices": "Video devices", + "Video input %(n)s": "Video input %(n)s", + "Turn off camera": "Turn off camera", + "Turn on camera": "Turn on camera", + "Join": "Join", "You are presenting": "You are presenting", "%(sharerName)s is presenting": "%(sharerName)s is presenting", "Your camera is turned off": "Your camera is turned off", @@ -1042,15 +1061,6 @@ "%(peerName)s held the call": "%(peerName)s held the call", "Connecting": "Connecting", "Dial": "Dial", - "%(count)s people joined|other": "%(count)s people joined", - "%(count)s people joined|one": "%(count)s person joined", - "Audio devices": "Audio devices", - "Mute microphone": "Mute microphone", - "Unmute microphone": "Unmute microphone", - "Video devices": "Video devices", - "Turn off camera": "Turn off camera", - "Turn on camera": "Turn on camera", - "Join": "Join", "Dialpad": "Dialpad", "Mute the microphone": "Mute the microphone", "Unmute the microphone": "Unmute the microphone", @@ -1147,8 +1157,9 @@ "Anchor": "Anchor", "Headphones": "Headphones", "Folder": "Folder", - "How are you finding Element so far?": "How are you finding Element so far?", - "We’d appreciate any feedback on how you’re finding Element.": "We’d appreciate any feedback on how you’re finding Element.", + "Welcome": "Welcome", + "How are you finding %(brand)s so far?": "How are you finding %(brand)s so far?", + "We’d appreciate any feedback on how you’re finding %(brand)s.": "We’d appreciate any feedback on how you’re finding %(brand)s.", "Feedback": "Feedback", "Secure messaging for friends and family": "Secure messaging for friends and family", "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.", @@ -1284,15 +1295,6 @@ "Session key:": "Session key:", "Your homeserver does not support device management.": "Your homeserver does not support device management.", "Unable to load device list": "Unable to load device list", - "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", - "Confirm signing out these devices|other": "Confirm signing out these devices", - "Confirm signing out these devices|one": "Confirm signing out this device", - "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", - "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", - "Sign out devices|other": "Sign out devices", - "Sign out devices|one": "Sign out device", - "Authentication": "Authentication", "Deselect all": "Deselect all", "Select all": "Select all", "Verified devices": "Verified devices", @@ -1303,7 +1305,6 @@ "You aren't signed into any other devices.": "You aren't signed into any other devices.", "This device": "This device", "Failed to set display name": "Failed to set display name", - "Last seen %(date)s at %(ip)s": "Last seen %(date)s at %(ip)s", "Sign Out": "Sign Out", "Display Name": "Display Name", "Rename": "Rename", @@ -1563,6 +1564,9 @@ "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", + "Sessions": "Sessions", + "Other sessions": "Other sessions", + "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.": "Spaces are ways to group rooms and people. Alongside the spaces you're in, you can use some pre-built ones too.", @@ -1597,6 +1601,7 @@ "This room is bridging messages to the following platforms. Learn more.": "This room is bridging messages to the following platforms. Learn more.", "This room isn't bridging messages to any platforms. Learn more.": "This room isn't bridging messages to any platforms. Learn more.", "Bridges": "Bridges", + "Emotes": "Emotes", "Room Addresses": "Room Addresses", "Uploaded sound": "Uploaded sound", "Get notifications as set up in your settings": "Get notifications as set up in your settings", @@ -1653,7 +1658,7 @@ "Select the roles required to change various parts of the space": "Select the roles required to change various parts of the space", "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", "Are you sure you want to add encryption to this public room?": "Are you sure you want to add encryption to this public room?", - "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", + "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", "To avoid these issues, create a new encrypted room for the conversation you plan to have.": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", "Enable encryption?": "Enable encryption?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", @@ -1691,6 +1696,51 @@ "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", + "Current session": "Current session", + "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", + "Confirm signing out these devices|other": "Confirm signing out these devices", + "Confirm signing out these devices|one": "Confirm signing out this device", + "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", + "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", + "Sign out devices|other": "Sign out devices", + "Sign out devices|one": "Sign out device", + "Authentication": "Authentication", + "Session ID": "Session ID", + "Last activity": "Last activity", + "Device": "Device", + "IP address": "IP address", + "Session details": "Session details", + "Toggle device details": "Toggle device details", + "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", + "Verified": "Verified", + "Unverified": "Unverified", + "Unknown device type": "Unknown device type", + "Verified session": "Verified session", + "This session is ready for secure messaging.": "This session is ready for secure messaging.", + "Unverified session": "Unverified session", + "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", + "Verified sessions": "Verified sessions", + "For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.", + "Unverified sessions": "Unverified sessions", + "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", + "Inactive sessions": "Inactive sessions", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", + "No verified sessions found.": "No verified sessions found.", + "No unverified sessions found.": "No unverified sessions found.", + "No inactive sessions found.": "No inactive sessions found.", + "No sessions found.": "No sessions found.", + "Show all": "Show all", + "All": "All", + "Ready for secure messaging": "Ready for secure messaging", + "Not ready for secure messaging": "Not ready for secure messaging", + "Inactive": "Inactive", + "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer", + "Filter devices": "Filter devices", + "Show": "Show", + "Security recommendations": "Security recommendations", + "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", + "View all": "View all", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", @@ -1756,8 +1806,6 @@ "%(seconds)ss left": "%(seconds)ss left", "Send voice message": "Send voice message", "Emoji": "Emoji", - "Emotes": "Emotes", - "Upload Emote": "Upload Emote", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", @@ -1847,7 +1895,6 @@ "System Alerts": "System Alerts", "Historical": "Historical", "Suggested Rooms": "Suggested Rooms", - "Empty room": "Empty room", "Add space": "Add space", "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", @@ -1928,6 +1975,11 @@ "%(count)s unread messages.|other": "%(count)s unread messages.", "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", + "Video": "Video", + "Joining…": "Joining…", + "Joined": "Joined", + "%(count)s participants|other": "%(count)s participants", + "%(count)s participants|one": "1 participant", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", "This room is running room version , which this homeserver has marked as unstable.": "This room is running room version , which this homeserver has marked as unstable.", @@ -1949,11 +2001,6 @@ "Open thread": "Open thread", "Jump to first unread message.": "Jump to first unread message.", "Mark all as read": "Mark all as read", - "Video": "Video", - "Joining…": "Joining…", - "Joined": "Joined", - "%(count)s participants|other": "%(count)s participants", - "%(count)s participants|one": "1 participant", "Unable to access your microphone": "Unable to access your microphone", "We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.", "No microphone found": "No microphone found", @@ -1983,6 +2030,7 @@ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", "Show more": "Show more", + "Upload Emote": "Upload Emote", "Room Name": "Room Name", "Room Topic": "Room Topic", "Room avatar": "Room avatar", @@ -2212,7 +2260,6 @@ "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", - "Show all": "Show all", "Reactions": "Reactions", "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s", "reacted with %(shortName)s": "reacted with %(shortName)s", @@ -2459,7 +2506,6 @@ "We don't record or profile any account data": "We don't record or profile any account data", "We don't share information with third parties": "We don't share information with third parties", "You can turn this off anytime in settings": "You can turn this off anytime in settings", - "Download %(brand)s": "Download %(brand)s", "Download %(brand)s Desktop": "Download %(brand)s Desktop", "iOS": "iOS", "Download on the App Store": "Download on the App Store", @@ -2718,7 +2764,6 @@ "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", - "Session ID": "Session ID", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", "Verify session": "Verify session", @@ -2833,8 +2878,8 @@ "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", "Not Trusted": "Not Trusted", - "Manually Verify by Text": "Manually Verify by Text", - "Interactively verify by Emoji": "Interactively verify by Emoji", + "Manually verify by text": "Manually verify by text", + "Interactively verify by emoji": "Interactively verify by emoji", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload all": "Upload all", @@ -3133,8 +3178,6 @@ "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Verification requested": "Verification requested", "Logout": "Logout", - "%(creator)s created this DM.": "%(creator)s created this DM.", - "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "You're all caught up": "You're all caught up", "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", From d7b0a4497df7af90b576037cdb83877f922f16af Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:44:15 -0400 Subject: [PATCH 023/176] i18n changes retry 4 --- src/i18n/strings/en_EN.json | 95 ++----------------------------------- 1 file changed, 3 insertions(+), 92 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d1b5241cad..af06a837d85 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -516,9 +516,7 @@ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s added the alternative addresses %(addresses)s for this room.", - "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s added alternative address %(addresses)s for this room.", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s removed the alternative addresses %(addresses)s for this room.", - "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s removed alternative address %(addresses)s for this room.", "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.", "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.", "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.", @@ -568,7 +566,6 @@ "Dark": "Dark", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", - "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", "Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running", "Remain on your screen while running": "Remain on your screen while running", @@ -651,7 +648,6 @@ "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", "Attachment": "Attachment", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", - "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "a few seconds ago": "a few seconds ago", "about a minute ago": "about a minute ago", @@ -670,11 +666,7 @@ "%(space1Name)s and %(space2Name)s": "%(space1Name)s and %(space2Name)s", "In spaces %(space1Name)s and %(space2Name)s.": "In spaces %(space1Name)s and %(space2Name)s.", "%(spaceName)s and %(count)s others|other": "%(spaceName)s and %(count)s others", - "%(spaceName)s and %(count)s others|zero": "%(spaceName)s", - "%(spaceName)s and %(count)s others|one": "%(spaceName)s and %(count)s other", "In %(spaceName)s and %(count)s other spaces.|other": "In %(spaceName)s and %(count)s other spaces.", - "In %(spaceName)s and %(count)s other spaces.|zero": "In space %(spaceName)s.", - "In %(spaceName)s and %(count)s other spaces.|one": "In %(spaceName)s and %(count)s other space.", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "Unexpected server error trying to leave the room": "Unexpected server error trying to leave the room", "Can't leave Server Notices room": "Can't leave Server Notices room", @@ -750,9 +742,7 @@ "Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?", "Generating a ZIP": "Generating a ZIP", "Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s", - "Fetched %(count)s events out of %(total)s|one": "Fetched %(count)s event out of %(total)s", "Fetched %(count)s events so far|other": "Fetched %(count)s events so far", - "Fetched %(count)s events so far|one": "Fetched %(count)s event so far", "HTML": "HTML", "JSON": "JSON", "Plain Text": "Plain Text", @@ -768,11 +758,9 @@ "Processing event %(number)s out of %(total)s": "Processing event %(number)s out of %(total)s", "Starting export...": "Starting export...", "Fetched %(count)s events in %(seconds)ss|other": "Fetched %(count)s events in %(seconds)ss", - "Fetched %(count)s events in %(seconds)ss|one": "Fetched %(count)s event in %(seconds)ss", "Creating HTML...": "Creating HTML...", "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", - "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", @@ -1040,7 +1028,6 @@ "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", "%(count)s people joined|other": "%(count)s people joined", - "%(count)s people joined|one": "%(count)s person joined", "Audio devices": "Audio devices", "Audio input %(n)s": "Audio input %(n)s", "Mute microphone": "Mute microphone", @@ -1171,7 +1158,6 @@ "Find your people": "Find your people", "Welcome to %(brand)s": "Welcome to %(brand)s", "Only %(count)s steps to go|other": "Only %(count)s steps to go", - "Only %(count)s steps to go|one": "Only %(count)s step to go", "You did it!": "You did it!", "Complete these to get the most out of %(brand)s": "Complete these to get the most out of %(brand)s", "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", @@ -1301,7 +1287,6 @@ "Unverified devices": "Unverified devices", "Devices without encryption support": "Devices without encryption support", "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", - "Sign out %(count)s selected devices|one": "Sign out %(count)s selected device", "You aren't signed into any other devices.": "You aren't signed into any other devices.", "This device": "This device", "Failed to set display name": "Failed to set display name", @@ -1310,7 +1295,6 @@ "Rename": "Rename", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", - "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", "Manage": "Manage", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", @@ -1331,9 +1315,7 @@ "Anyone can find and join.": "Anyone can find and join.", "Upgrade required": "Upgrade required", "& %(count)s more|other": "& %(count)s more", - "& %(count)s more|one": "& %(count)s more", "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", - "Currently, %(count)s spaces have access|one": "Currently, a space has access", "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", "Spaces with access": "Spaces with access", "Anyone in can find and join. You can select other spaces too.": "Anyone in can find and join. You can select other spaces too.", @@ -1344,9 +1326,7 @@ "Upgrading room": "Upgrading room", "Loading new room": "Loading new room", "Sending invites... (%(progress)s out of %(count)s)|other": "Sending invites... (%(progress)s out of %(count)s)", - "Sending invites... (%(progress)s out of %(count)s)|one": "Sending invite...", "Updating spaces... (%(progress)s out of %(count)s)|other": "Updating spaces... (%(progress)s out of %(count)s)", - "Updating spaces... (%(progress)s out of %(count)s)|one": "Updating space...", "Message layout": "Message layout", "IRC (Experimental)": "IRC (Experimental)", "Modern": "Modern", @@ -1698,13 +1678,9 @@ "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Current session": "Current session", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", "Confirm signing out these devices|other": "Confirm signing out these devices", - "Confirm signing out these devices|one": "Confirm signing out this device", "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", - "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", "Sign out devices|other": "Sign out devices", - "Sign out devices|one": "Sign out device", "Authentication": "Authentication", "Session ID": "Session ID", "Last activity": "Last activity", @@ -1784,10 +1760,8 @@ "You can't see earlier messages": "You can't see earlier messages", "Scroll to most recent messages": "Scroll to most recent messages", "Show %(count)s other previews|other": "Show %(count)s other previews", - "Show %(count)s other previews|one": "Show %(count)s other preview", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", - "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", "Invite to this space": "Invite to this space", "Invited": "Invited", @@ -1852,7 +1826,6 @@ "%(members)s and more": "%(members)s and more", "%(members)s and %(last)s": "%(members)s and %(last)s", "Seen by %(count)s people|other": "Seen by %(count)s people", - "Seen by %(count)s people|one": "Seen by %(count)s person", "Read receipts": "Read receipts", "Recently viewed": "Recently viewed", "Replying": "Replying", @@ -1866,7 +1839,6 @@ "Invite": "Invite", "Room options": "Room options", "(~%(count)s results)|other": "(~%(count)s results)", - "(~%(count)s results)|one": "(~%(count)s result)", "Join Room": "Join Room", "Video rooms are a beta feature": "Video rooms are a beta feature", "Video room": "Video room", @@ -1875,7 +1847,6 @@ "Private space": "Private space", "Private room": "Private room", "%(count)s members|other": "%(count)s members", - "%(count)s members|one": "%(count)s member", "Start new chat": "Start new chat", "Invite to space": "Invite to space", "You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space", @@ -1899,9 +1870,7 @@ "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", - "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Currently removing messages in %(count)s rooms|other": "Currently removing messages in %(count)s rooms", - "Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room", "%(spaceName)s menu": "%(spaceName)s menu", "Home options": "Home options", "Joining space …": "Joining space …", @@ -1967,19 +1936,15 @@ "A-Z": "A-Z", "List options": "List options", "Show %(count)s more|other": "Show %(count)s more", - "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", "Notification options": "Notification options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", - "%(count)s unread messages including mentions.|one": "1 unread mention.", "%(count)s unread messages.|other": "%(count)s unread messages.", - "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", "Video": "Video", "Joining…": "Joining…", "Joined": "Joined", "%(count)s participants|other": "%(count)s participants", - "%(count)s participants|one": "1 participant", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", "This room is running room version , which this homeserver has marked as unstable.": "This room is running room version , which this homeserver has marked as unstable.", @@ -1996,8 +1961,7 @@ "Admin Tools": "Admin Tools", "Revoke invite": "Revoke invite", "Invited by %(sender)s": "Invited by %(sender)s", - "%(count)s reply|other": "%(count)s replies", - "%(count)s reply|one": "%(count)s reply", + "%(count)s reply|other": "%(count)s reply", "Open thread": "Open thread", "Jump to first unread message.": "Jump to first unread message.", "Mark all as read": "Mark all as read", @@ -2082,10 +2046,8 @@ "Not trusted": "Not trusted", "Unable to load session list": "Unable to load session list", "%(count)s verified sessions|other": "%(count)s verified sessions", - "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", "%(count)s sessions|other": "%(count)s sessions", - "%(count)s sessions|one": "%(count)s session", "Hide sessions": "Hide sessions", "Message": "Message", "Jump to read receipt": "Jump to read receipt", @@ -2247,16 +2209,12 @@ "Vote not registered": "Vote not registered", "Sorry, your vote was not registered. Please try again.": "Sorry, your vote was not registered. Please try again.", "Final result based on %(count)s votes|other": "Final result based on %(count)s votes", - "Final result based on %(count)s votes|one": "Final result based on %(count)s vote", "Results will be visible when the poll is ended": "Results will be visible when the poll is ended", "No votes cast": "No votes cast", "%(count)s votes cast. Vote to see the results|other": "%(count)s votes cast. Vote to see the results", - "%(count)s votes cast. Vote to see the results|one": "%(count)s vote cast. Vote to see the results", "Based on %(count)s votes|other": "Based on %(count)s votes", - "Based on %(count)s votes|one": "Based on %(count)s vote", "edited": "edited", "%(count)s votes|other": "%(count)s votes", - "%(count)s votes|one": "%(count)s vote", "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", @@ -2339,73 +2297,39 @@ "Something went wrong!": "Something went wrong!", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", - "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times", - "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times", - "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft", "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times", - "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times", - "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times", - "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left", "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times", - "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times", - "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times", - "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times", - "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times", - "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times", - "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn", "were invited %(count)s times|other": "were invited %(count)s times", - "were invited %(count)s times|one": "were invited", "was invited %(count)s times|other": "was invited %(count)s times", - "was invited %(count)s times|one": "was invited", "were banned %(count)s times|other": "were banned %(count)s times", - "were banned %(count)s times|one": "were banned", "was banned %(count)s times|other": "was banned %(count)s times", - "was banned %(count)s times|one": "was banned", "were unbanned %(count)s times|other": "were unbanned %(count)s times", - "were unbanned %(count)s times|one": "were unbanned", "was unbanned %(count)s times|other": "was unbanned %(count)s times", - "was unbanned %(count)s times|one": "was unbanned", "were removed %(count)s times|other": "were removed %(count)s times", - "were removed %(count)s times|one": "were removed", "was removed %(count)s times|other": "was removed %(count)s times", - "was removed %(count)s times|one": "was removed", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times", - "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times", - "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times", - "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", - "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)smade no changes %(count)s times", - "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes", "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times", - "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes", "%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)schanged the server ACLs %(count)s times", - "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", - "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", "%(severalUsers)schanged the pinned messages for the room %(count)s times|other": "%(severalUsers)schanged the pinned messages for the room %(count)s times", - "%(severalUsers)schanged the pinned messages for the room %(count)s times|one": "%(severalUsers)schanged the pinned messages for the room", "%(oneUser)schanged the pinned messages for the room %(count)s times|other": "%(oneUser)schanged the pinned messages for the room %(count)s times", - "%(oneUser)schanged the pinned messages for the room %(count)s times|one": "%(oneUser)schanged the pinned messages for the room", - "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved %(count)s messages", - "%(severalUsers)sremoved a message %(count)s times|one": "%(severalUsers)sremoved a message", - "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved %(count)s messages", - "%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sremoved a message", + "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved a message %(count)s times", + "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved a message %(count)s times", "%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)ssent %(count)s hidden messages", - "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)ssent a hidden message", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)ssent %(count)s hidden messages", - "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)ssent a hidden message", "collapse": "collapse", "expand": "expand", "Rotate Left": "Rotate Left", @@ -2447,11 +2371,9 @@ "This address is already in use": "This address is already in use", "This address had invalid server or is already in use": "This address had invalid server or is already in use", "View all %(count)s members|other": "View all %(count)s members", - "View all %(count)s members|one": "View 1 member", "Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", - "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Edit topic": "Edit topic", "Click to read topic": "Click to read topic", "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", @@ -2494,7 +2416,6 @@ "Search for spaces": "Search for spaces", "Not all selected were added": "Not all selected were added", "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", - "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Direct Messages": "Direct Messages", "Add existing rooms": "Add existing rooms", "Want to add a new room instead?": "Want to add a new room instead?", @@ -2538,12 +2459,10 @@ "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", - "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|one": "You are about to remove %(count)s message by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", "Preserve system messages": "Preserve system messages", "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)", "Remove %(count)s messages|other": "Remove %(count)s messages", - "Remove %(count)s messages|one": "Remove 1 message", "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", @@ -2752,7 +2671,6 @@ "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", "%(count)s rooms|other": "%(count)s rooms", - "%(count)s rooms|one": "%(count)s room", "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only", "Select spaces": "Select spaces", "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decide which spaces can access this room. If a space is selected, its members can find and join .", @@ -2887,7 +2805,6 @@ "These files are too large to upload. The file size limit is %(limit)s.": "These files are too large to upload. The file size limit is %(limit)s.", "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Some files are too large to be uploaded. The file size limit is %(limit)s.", "Upload %(count)s other files|other": "Upload %(count)s other files", - "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", "Verify other device": "Verify other device", @@ -2901,7 +2818,6 @@ "The widget will verify your user ID, but won't be able to perform actions for you:": "The widget will verify your user ID, but won't be able to perform actions for you:", "Remember this": "Remember this", "%(count)s Members|other": "%(count)s Members", - "%(count)s Members|one": "%(count)s Member", "Public rooms": "Public rooms", "Use \"%(query)s\" to search": "Use \"%(query)s\" to search", "Search for": "Search for", @@ -2980,8 +2896,6 @@ "Filter results": "Filter results", "No results found": "No results found", "<%(count)s spaces>|other": "<%(count)s spaces>", - "<%(count)s spaces>|one": "", - "<%(count)s spaces>|zero": "", "Send custom state event": "Send custom state event", "Capabilities": "Capabilities", "Failed to load.": "Failed to load.", @@ -3212,7 +3126,6 @@ "No more results": "No more results", "Failed to reject invite": "Failed to reject invite", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", - "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "Joining": "Joining", "You don't have permission": "You don't have permission", "This room is suggested as a good one to join": "This room is suggested as a good one to join", @@ -3276,8 +3189,6 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", - "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", - "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Got an account? Sign in": "Got an account? Sign in", "New here? Create an account": "New here? Create an account", "Switch to light mode": "Switch to light mode", From 0b952579d5a8891168997ab4b6bdcbe1689ddd67 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:59:29 -0400 Subject: [PATCH 024/176] i18n changes undo --- src/i18n/strings/en_EN.json | 95 +++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index af06a837d85..3d1b5241cad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -516,7 +516,9 @@ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s added the alternative addresses %(addresses)s for this room.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s added alternative address %(addresses)s for this room.", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s removed the alternative addresses %(addresses)s for this room.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s removed alternative address %(addresses)s for this room.", "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.", "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.", "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.", @@ -566,6 +568,7 @@ "Dark": "Dark", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", + "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", "Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running", "Remain on your screen while running": "Remain on your screen while running", @@ -648,6 +651,7 @@ "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", "Attachment": "Attachment", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", + "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "a few seconds ago": "a few seconds ago", "about a minute ago": "about a minute ago", @@ -666,7 +670,11 @@ "%(space1Name)s and %(space2Name)s": "%(space1Name)s and %(space2Name)s", "In spaces %(space1Name)s and %(space2Name)s.": "In spaces %(space1Name)s and %(space2Name)s.", "%(spaceName)s and %(count)s others|other": "%(spaceName)s and %(count)s others", + "%(spaceName)s and %(count)s others|zero": "%(spaceName)s", + "%(spaceName)s and %(count)s others|one": "%(spaceName)s and %(count)s other", "In %(spaceName)s and %(count)s other spaces.|other": "In %(spaceName)s and %(count)s other spaces.", + "In %(spaceName)s and %(count)s other spaces.|zero": "In space %(spaceName)s.", + "In %(spaceName)s and %(count)s other spaces.|one": "In %(spaceName)s and %(count)s other space.", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "Unexpected server error trying to leave the room": "Unexpected server error trying to leave the room", "Can't leave Server Notices room": "Can't leave Server Notices room", @@ -742,7 +750,9 @@ "Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?", "Generating a ZIP": "Generating a ZIP", "Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s", + "Fetched %(count)s events out of %(total)s|one": "Fetched %(count)s event out of %(total)s", "Fetched %(count)s events so far|other": "Fetched %(count)s events so far", + "Fetched %(count)s events so far|one": "Fetched %(count)s event so far", "HTML": "HTML", "JSON": "JSON", "Plain Text": "Plain Text", @@ -758,9 +768,11 @@ "Processing event %(number)s out of %(total)s": "Processing event %(number)s out of %(total)s", "Starting export...": "Starting export...", "Fetched %(count)s events in %(seconds)ss|other": "Fetched %(count)s events in %(seconds)ss", + "Fetched %(count)s events in %(seconds)ss|one": "Fetched %(count)s event in %(seconds)ss", "Creating HTML...": "Creating HTML...", "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", + "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", @@ -1028,6 +1040,7 @@ "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", "%(count)s people joined|other": "%(count)s people joined", + "%(count)s people joined|one": "%(count)s person joined", "Audio devices": "Audio devices", "Audio input %(n)s": "Audio input %(n)s", "Mute microphone": "Mute microphone", @@ -1158,6 +1171,7 @@ "Find your people": "Find your people", "Welcome to %(brand)s": "Welcome to %(brand)s", "Only %(count)s steps to go|other": "Only %(count)s steps to go", + "Only %(count)s steps to go|one": "Only %(count)s step to go", "You did it!": "You did it!", "Complete these to get the most out of %(brand)s": "Complete these to get the most out of %(brand)s", "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", @@ -1287,6 +1301,7 @@ "Unverified devices": "Unverified devices", "Devices without encryption support": "Devices without encryption support", "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", + "Sign out %(count)s selected devices|one": "Sign out %(count)s selected device", "You aren't signed into any other devices.": "You aren't signed into any other devices.", "This device": "This device", "Failed to set display name": "Failed to set display name", @@ -1295,6 +1310,7 @@ "Rename": "Rename", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", "Manage": "Manage", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", @@ -1315,7 +1331,9 @@ "Anyone can find and join.": "Anyone can find and join.", "Upgrade required": "Upgrade required", "& %(count)s more|other": "& %(count)s more", + "& %(count)s more|one": "& %(count)s more", "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", + "Currently, %(count)s spaces have access|one": "Currently, a space has access", "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", "Spaces with access": "Spaces with access", "Anyone in can find and join. You can select other spaces too.": "Anyone in can find and join. You can select other spaces too.", @@ -1326,7 +1344,9 @@ "Upgrading room": "Upgrading room", "Loading new room": "Loading new room", "Sending invites... (%(progress)s out of %(count)s)|other": "Sending invites... (%(progress)s out of %(count)s)", + "Sending invites... (%(progress)s out of %(count)s)|one": "Sending invite...", "Updating spaces... (%(progress)s out of %(count)s)|other": "Updating spaces... (%(progress)s out of %(count)s)", + "Updating spaces... (%(progress)s out of %(count)s)|one": "Updating space...", "Message layout": "Message layout", "IRC (Experimental)": "IRC (Experimental)", "Modern": "Modern", @@ -1678,9 +1698,13 @@ "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Current session": "Current session", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", "Confirm signing out these devices|other": "Confirm signing out these devices", + "Confirm signing out these devices|one": "Confirm signing out this device", "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", + "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", "Sign out devices|other": "Sign out devices", + "Sign out devices|one": "Sign out device", "Authentication": "Authentication", "Session ID": "Session ID", "Last activity": "Last activity", @@ -1760,8 +1784,10 @@ "You can't see earlier messages": "You can't see earlier messages", "Scroll to most recent messages": "Scroll to most recent messages", "Show %(count)s other previews|other": "Show %(count)s other previews", + "Show %(count)s other previews|one": "Show %(count)s other preview", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", + "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", "Invite to this space": "Invite to this space", "Invited": "Invited", @@ -1826,6 +1852,7 @@ "%(members)s and more": "%(members)s and more", "%(members)s and %(last)s": "%(members)s and %(last)s", "Seen by %(count)s people|other": "Seen by %(count)s people", + "Seen by %(count)s people|one": "Seen by %(count)s person", "Read receipts": "Read receipts", "Recently viewed": "Recently viewed", "Replying": "Replying", @@ -1839,6 +1866,7 @@ "Invite": "Invite", "Room options": "Room options", "(~%(count)s results)|other": "(~%(count)s results)", + "(~%(count)s results)|one": "(~%(count)s result)", "Join Room": "Join Room", "Video rooms are a beta feature": "Video rooms are a beta feature", "Video room": "Video room", @@ -1847,6 +1875,7 @@ "Private space": "Private space", "Private room": "Private room", "%(count)s members|other": "%(count)s members", + "%(count)s members|one": "%(count)s member", "Start new chat": "Start new chat", "Invite to space": "Invite to space", "You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space", @@ -1870,7 +1899,9 @@ "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", + "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Currently removing messages in %(count)s rooms|other": "Currently removing messages in %(count)s rooms", + "Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room", "%(spaceName)s menu": "%(spaceName)s menu", "Home options": "Home options", "Joining space …": "Joining space …", @@ -1936,15 +1967,19 @@ "A-Z": "A-Z", "List options": "List options", "Show %(count)s more|other": "Show %(count)s more", + "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", "Notification options": "Notification options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", + "%(count)s unread messages including mentions.|one": "1 unread mention.", "%(count)s unread messages.|other": "%(count)s unread messages.", + "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", "Video": "Video", "Joining…": "Joining…", "Joined": "Joined", "%(count)s participants|other": "%(count)s participants", + "%(count)s participants|one": "1 participant", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", "This room is running room version , which this homeserver has marked as unstable.": "This room is running room version , which this homeserver has marked as unstable.", @@ -1961,7 +1996,8 @@ "Admin Tools": "Admin Tools", "Revoke invite": "Revoke invite", "Invited by %(sender)s": "Invited by %(sender)s", - "%(count)s reply|other": "%(count)s reply", + "%(count)s reply|other": "%(count)s replies", + "%(count)s reply|one": "%(count)s reply", "Open thread": "Open thread", "Jump to first unread message.": "Jump to first unread message.", "Mark all as read": "Mark all as read", @@ -2046,8 +2082,10 @@ "Not trusted": "Not trusted", "Unable to load session list": "Unable to load session list", "%(count)s verified sessions|other": "%(count)s verified sessions", + "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", "%(count)s sessions|other": "%(count)s sessions", + "%(count)s sessions|one": "%(count)s session", "Hide sessions": "Hide sessions", "Message": "Message", "Jump to read receipt": "Jump to read receipt", @@ -2209,12 +2247,16 @@ "Vote not registered": "Vote not registered", "Sorry, your vote was not registered. Please try again.": "Sorry, your vote was not registered. Please try again.", "Final result based on %(count)s votes|other": "Final result based on %(count)s votes", + "Final result based on %(count)s votes|one": "Final result based on %(count)s vote", "Results will be visible when the poll is ended": "Results will be visible when the poll is ended", "No votes cast": "No votes cast", "%(count)s votes cast. Vote to see the results|other": "%(count)s votes cast. Vote to see the results", + "%(count)s votes cast. Vote to see the results|one": "%(count)s vote cast. Vote to see the results", "Based on %(count)s votes|other": "Based on %(count)s votes", + "Based on %(count)s votes|one": "Based on %(count)s vote", "edited": "edited", "%(count)s votes|other": "%(count)s votes", + "%(count)s votes|one": "%(count)s vote", "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", @@ -2297,39 +2339,73 @@ "Something went wrong!": "Something went wrong!", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft", "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left", "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn", "were invited %(count)s times|other": "were invited %(count)s times", + "were invited %(count)s times|one": "were invited", "was invited %(count)s times|other": "was invited %(count)s times", + "was invited %(count)s times|one": "was invited", "were banned %(count)s times|other": "were banned %(count)s times", + "were banned %(count)s times|one": "were banned", "was banned %(count)s times|other": "was banned %(count)s times", + "was banned %(count)s times|one": "was banned", "were unbanned %(count)s times|other": "were unbanned %(count)s times", + "were unbanned %(count)s times|one": "were unbanned", "was unbanned %(count)s times|other": "was unbanned %(count)s times", + "was unbanned %(count)s times|one": "was unbanned", "were removed %(count)s times|other": "were removed %(count)s times", + "were removed %(count)s times|one": "were removed", "was removed %(count)s times|other": "was removed %(count)s times", + "was removed %(count)s times|one": "was removed", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)smade no changes %(count)s times", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes", "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes", "%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)schanged the server ACLs %(count)s times", + "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", + "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", "%(severalUsers)schanged the pinned messages for the room %(count)s times|other": "%(severalUsers)schanged the pinned messages for the room %(count)s times", + "%(severalUsers)schanged the pinned messages for the room %(count)s times|one": "%(severalUsers)schanged the pinned messages for the room", "%(oneUser)schanged the pinned messages for the room %(count)s times|other": "%(oneUser)schanged the pinned messages for the room %(count)s times", - "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved a message %(count)s times", - "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved a message %(count)s times", + "%(oneUser)schanged the pinned messages for the room %(count)s times|one": "%(oneUser)schanged the pinned messages for the room", + "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved %(count)s messages", + "%(severalUsers)sremoved a message %(count)s times|one": "%(severalUsers)sremoved a message", + "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved %(count)s messages", + "%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sremoved a message", "%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)ssent %(count)s hidden messages", + "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)ssent a hidden message", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)ssent %(count)s hidden messages", + "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)ssent a hidden message", "collapse": "collapse", "expand": "expand", "Rotate Left": "Rotate Left", @@ -2371,9 +2447,11 @@ "This address is already in use": "This address is already in use", "This address had invalid server or is already in use": "This address had invalid server or is already in use", "View all %(count)s members|other": "View all %(count)s members", + "View all %(count)s members|one": "View 1 member", "Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", + "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Edit topic": "Edit topic", "Click to read topic": "Click to read topic", "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", @@ -2416,6 +2494,7 @@ "Search for spaces": "Search for spaces", "Not all selected were added": "Not all selected were added", "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Direct Messages": "Direct Messages", "Add existing rooms": "Add existing rooms", "Want to add a new room instead?": "Want to add a new room instead?", @@ -2459,10 +2538,12 @@ "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|one": "You are about to remove %(count)s message by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", "Preserve system messages": "Preserve system messages", "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)", "Remove %(count)s messages|other": "Remove %(count)s messages", + "Remove %(count)s messages|one": "Remove 1 message", "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", @@ -2671,6 +2752,7 @@ "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", "%(count)s rooms|other": "%(count)s rooms", + "%(count)s rooms|one": "%(count)s room", "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only", "Select spaces": "Select spaces", "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decide which spaces can access this room. If a space is selected, its members can find and join .", @@ -2805,6 +2887,7 @@ "These files are too large to upload. The file size limit is %(limit)s.": "These files are too large to upload. The file size limit is %(limit)s.", "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Some files are too large to be uploaded. The file size limit is %(limit)s.", "Upload %(count)s other files|other": "Upload %(count)s other files", + "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", "Verify other device": "Verify other device", @@ -2818,6 +2901,7 @@ "The widget will verify your user ID, but won't be able to perform actions for you:": "The widget will verify your user ID, but won't be able to perform actions for you:", "Remember this": "Remember this", "%(count)s Members|other": "%(count)s Members", + "%(count)s Members|one": "%(count)s Member", "Public rooms": "Public rooms", "Use \"%(query)s\" to search": "Use \"%(query)s\" to search", "Search for": "Search for", @@ -2896,6 +2980,8 @@ "Filter results": "Filter results", "No results found": "No results found", "<%(count)s spaces>|other": "<%(count)s spaces>", + "<%(count)s spaces>|one": "", + "<%(count)s spaces>|zero": "", "Send custom state event": "Send custom state event", "Capabilities": "Capabilities", "Failed to load.": "Failed to load.", @@ -3126,6 +3212,7 @@ "No more results": "No more results", "Failed to reject invite": "Failed to reject invite", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", + "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "Joining": "Joining", "You don't have permission": "You don't have permission", "This room is suggested as a good one to join": "This room is suggested as a good one to join", @@ -3189,6 +3276,8 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", + "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", + "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Got an account? Sign in": "Got an account? Sign in", "New here? Create an account": "New here? Create an account", "Switch to light mode": "Switch to light mode", From c31009155e86d79120b35784e266f0400a12fab0 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Tue, 6 Sep 2022 00:04:52 -0400 Subject: [PATCH 025/176] i18n changes retry 6 --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d1b5241cad..1eb1035f857 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -100,8 +100,10 @@ "Empty room": "Empty room", "%(user1)s and %(user2)s": "%(user1)s and %(user2)s", "%(user)s and %(count)s others|other": "%(user)s and %(count)s others", + "%(user)s and %(count)s others|one": "%(user)s and 1 other", "Inviting %(user1)s and %(user2)s": "Inviting %(user1)s and %(user2)s", "Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others", + "Inviting %(user)s and %(count)s others|one": "Inviting %(user)s and 1 other", "Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)", "%(name)s is requesting verification": "%(name)s is requesting verification", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings", From e0a4fc3a7eb10aebf952c8646300d667e8baf810 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Tue, 6 Sep 2022 10:11:33 -0400 Subject: [PATCH 026/176] i18n changes retry 7 --- src/i18n/strings/en_EN.json | 100 ++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1eb1035f857..b9394758a65 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -16,40 +16,6 @@ "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", - "Call Failed": "Call Failed", - "User Busy": "User Busy", - "The user you called is busy.": "The user you called is busy.", - "The call could not be established": "The call could not be established", - "Answered Elsewhere": "Answered Elsewhere", - "The call was answered on another device.": "The call was answered on another device.", - "Call failed due to misconfigured server": "Call failed due to misconfigured server", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", - "Try using turn.matrix.org": "Try using turn.matrix.org", - "OK": "OK", - "Unable to access microphone": "Unable to access microphone", - "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", - "Unable to access webcam / microphone": "Unable to access webcam / microphone", - "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", - "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", - "Permission is granted to use the webcam": "Permission is granted to use the webcam", - "No other application is using the webcam": "No other application is using the webcam", - "Already in call": "Already in call", - "You're already in a call with this person.": "You're already in a call with this person.", - "Calls are unsupported": "Calls are unsupported", - "You cannot place calls in this browser.": "You cannot place calls in this browser.", - "Connectivity to the server has been lost": "Connectivity to the server has been lost", - "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", - "Too Many Calls": "Too Many Calls", - "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", - "You cannot place a call with yourself.": "You cannot place a call with yourself.", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", - "Unable to transfer call": "Unable to transfer call", - "Transfer Failed": "Transfer Failed", - "Failed to transfer call": "Failed to transfer call", - "Permission Required": "Permission Required", - "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "Upload Failed": "Upload Failed", @@ -92,6 +58,40 @@ "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Trust": "Trust", + "Call Failed": "Call Failed", + "User Busy": "User Busy", + "The user you called is busy.": "The user you called is busy.", + "The call could not be established": "The call could not be established", + "Answered Elsewhere": "Answered Elsewhere", + "The call was answered on another device.": "The call was answered on another device.", + "Call failed due to misconfigured server": "Call failed due to misconfigured server", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", + "Try using turn.matrix.org": "Try using turn.matrix.org", + "OK": "OK", + "Unable to access microphone": "Unable to access microphone", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", + "Unable to access webcam / microphone": "Unable to access webcam / microphone", + "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", + "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", + "Permission is granted to use the webcam": "Permission is granted to use the webcam", + "No other application is using the webcam": "No other application is using the webcam", + "Already in call": "Already in call", + "You're already in a call with this person.": "You're already in a call with this person.", + "Calls are unsupported": "Calls are unsupported", + "You cannot place calls in this browser.": "You cannot place calls in this browser.", + "Connectivity to the server has been lost": "Connectivity to the server has been lost", + "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", + "Too Many Calls": "Too Many Calls", + "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", + "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", + "Unable to transfer call": "Unable to transfer call", + "Transfer Failed": "Transfer Failed", + "Failed to transfer call": "Failed to transfer call", + "Permission Required": "Permission Required", + "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "We couldn't log you in": "We couldn't log you in", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", "Try again": "Try again", @@ -775,8 +775,6 @@ "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", - "%(creator)s created this DM.": "%(creator)s created this DM.", - "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", "Starting export process...": "Starting export process...", "Fetching events...": "Fetching events...", @@ -1052,6 +1050,7 @@ "Turn off camera": "Turn off camera", "Turn on camera": "Turn on camera", "Join": "Join", + "Dial": "Dial", "You are presenting": "You are presenting", "%(sharerName)s is presenting": "%(sharerName)s is presenting", "Your camera is turned off": "Your camera is turned off", @@ -1062,7 +1061,6 @@ "You held the call Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", "Connecting": "Connecting", - "Dial": "Dial", "Dialpad": "Dialpad", "Mute the microphone": "Mute the microphone", "Unmute the microphone": "Unmute the microphone", @@ -2161,17 +2159,6 @@ "%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.", "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", - "Call declined": "Call declined", - "Call back": "Call back", - "No answer": "No answer", - "Could not connect media": "Could not connect media", - "Connection failed": "Connection failed", - "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", - "An unknown error occurred": "An unknown error occurred", - "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", - "Retry": "Retry", - "Missed call": "Missed call", - "The call is in an unknown state!": "The call is in an unknown state!", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -2202,6 +2189,17 @@ "Message pending moderation": "Message pending moderation", "Pick a date to jump to": "Pick a date to jump to", "Go": "Go", + "Call declined": "Call declined", + "Call back": "Call back", + "No answer": "No answer", + "Could not connect media": "Could not connect media", + "Connection failed": "Connection failed", + "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", + "An unknown error occurred": "An unknown error occurred", + "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", + "Retry": "Retry", + "Missed call": "Missed call", + "The call is in an unknown state!": "The call is in an unknown state!", "Error processing audio message": "Error processing audio message", "View live location": "View live location", "React": "React", @@ -3024,11 +3022,11 @@ "Observe only": "Observe only", "No verification requests found": "No verification requests found", "There was an error finding this widget.": "There was an error finding this widget.", - "Resume": "Resume", - "Hold": "Hold", "Input devices": "Input devices", "Output devices": "Output devices", "Cameras": "Cameras", + "Resume": "Resume", + "Hold": "Hold", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Open in OpenStreetMap": "Open in OpenStreetMap", "Forward": "Forward", @@ -3180,6 +3178,8 @@ "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Verification requested": "Verification requested", "Logout": "Logout", + "%(creator)s created this DM.": "%(creator)s created this DM.", + "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "You're all caught up": "You're all caught up", "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", From 91c669af4f99c03ba7aade2007d995c798a7b626 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:09:10 -0400 Subject: [PATCH 027/176] autocomplete fix --- src/autocomplete/EmojiProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index e0859d8d953..693c0e6b830 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -165,7 +165,7 @@ export default class EmojiProvider extends AutocompleteProvider { completions = sortBy(uniq(completions), sorters); completions = completions.map(c => ({ - completion: c.emoji.unicode, + completion: this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode, component: ( { this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode } From 150771875720623f2dc16fc0864b32fc466283e2 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Sep 2022 15:37:04 -0400 Subject: [PATCH 028/176] added encryption and decryption for emotes --- src/autocomplete/EmojiProvider.tsx | 28 +++++++++++---- src/components/structures/MessagePanel.tsx | 3 -- src/components/views/messages/TextualBody.tsx | 36 ++++++++++++------- .../views/room_settings/RoomEmoteSettings.tsx | 28 +++++++++++---- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 693c0e6b830..9c68b626b14 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -33,6 +33,7 @@ import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji'; import { TimelineRenderingType } from '../contexts/RoomContext'; import * as recent from '../emojipicker/recent'; import { mediaFromMxc } from "../customisations/Media"; +import { decryptFile } from '../utils/DecryptFile'; const LIMIT = 20; @@ -76,16 +77,18 @@ export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - emotes: Dictionary; + emotes: Dictionary; + emotesPromise: Promise; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - this.emotes = {}; - for (const key in rawEmotes) { - this.emotes[key] = ""; - } + this.emotesPromise = this.decryptEmotes(rawEmotes); + this.emotes={}; + // for (const key in rawEmotes) { FOR UNENCRYPTED + // this.emotes[key] = ""; + // } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], @@ -101,6 +104,17 @@ export default class EmojiProvider extends AutocompleteProvider { this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); } + private async decryptEmotes(emotes: Object){ + const decryptede={} + for (const shortcode in emotes) { + const blob = await decryptFile(emotes[shortcode]); + const durl=URL.createObjectURL(blob); + decryptede[shortcode] = ""; + } + return decryptede + } + async getCompletions( query: string, selection: ISelectionRange, @@ -110,6 +124,8 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } + this.emotes=await this.emotesPromise + //console.log("emotes",this.emotes) const emojisAndEmotes=[...SORTED_EMOJI]; for (const key in this.emotes) { emojisAndEmotes.push({ diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 3e3589912e7..1a31bf48099 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -195,7 +195,6 @@ interface IState { ghostReadMarkers: string[]; showTypingNotifications: boolean; hideSender: boolean; - emotes: Dictionary; } interface IReadReceiptForUser { @@ -273,7 +272,6 @@ export default class MessagePanel extends React.Component { ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), hideSender: this.shouldHideSender(), - emotes: {}, }; // Cache these settings on mount since Settings is expensive to query, @@ -289,7 +287,6 @@ export default class MessagePanel extends React.Component { componentDidMount() { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); - //this.props.room?.currentState.on(RoomStateEvent.Update, this.getEmotes); this.isMounted = true; } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 32dc2ac9f60..70c6e3849e4 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -49,7 +49,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from '../../../utils/Reply'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { mediaFromMxc } from "../../../customisations/Media"; +import { decryptFile } from '../../../utils/DecryptFile'; const MAX_HIGHLIGHT_LENGTH = 4096; interface IState { @@ -58,6 +58,7 @@ interface IState { // track whether the preview widget is hidden widgetHidden: boolean; + finalEmotes: Dictionary; } export default class TextualBody extends React.Component { @@ -76,6 +77,7 @@ export default class TextualBody extends React.Component { this.state = { links: [], widgetHidden: false, + finalEmotes: {}, }; } @@ -86,6 +88,7 @@ export default class TextualBody extends React.Component { } private applyFormatting(): void { + this.decryptEmotes(); const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); this.activateSpoilers([this.contentRef.current]); @@ -560,7 +563,24 @@ export default class TextualBody extends React.Component { { `(${text})` } ); } - + private async decryptEmotes(){ + const client = MatrixClientPeg.get(); + const room = client.getRoom(this.props.mxEvent.getRoomId()); + //TODO: Do not encrypt/decrypt if room is not encrypted + const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const decryptede={} + for (const shortcode in rawEmotes) { + const blob = await decryptFile(rawEmotes[shortcode]); + const durl=URL.createObjectURL(blob); + decryptede[":" + shortcode + ":"] = ""; + } + this.setState({ + finalEmotes:decryptede + }); + this.forceUpdate(); + } render() { if (this.props.editState) { return ; @@ -572,16 +592,6 @@ export default class TextualBody extends React.Component { // only strip reply if this is the original replying event, edits thereafter do not have the fallback const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent); let body: ReactNode; - const client = MatrixClientPeg.get(); - const room = client.getRoom(mxEvent.getRoomId()); - //TODO: Decrypt emotes if encryption is added - const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); - const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const finalEmotes = {}; - for (const key in rawEmotes) { - finalEmotes[":" + key + ":"] = ""; - } if (SettingsStore.isEnabled("feature_extensible_events")) { const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; if (extev?.isEquivalentTo(M_MESSAGE)) { @@ -599,7 +609,7 @@ export default class TextualBody extends React.Component { stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, - emotes: finalEmotes, + emotes: this.state.finalEmotes, })); } } diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 4608df1397c..977ab17f45e 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -18,9 +18,10 @@ import React, { createRef } from 'react'; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { mediaFromMxc } from "../../../customisations/Media"; import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; +import { uploadFile } from "../../../ContentMessages"; +import { decryptFile } from "../../../utils/DecryptFile"; interface IProps { roomId: string; @@ -28,6 +29,7 @@ interface IProps { interface IState { emotes: Dictionary; + decryptedemotes: Dictionary; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; newEmoteCodeAdded: boolean; @@ -50,7 +52,7 @@ export default class RoomEmoteSettings extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); - //TODO: Decrypt the shortcodes and emotes if they are encrypted + //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const value = {}; @@ -60,6 +62,7 @@ export default class RoomEmoteSettings extends React.Component { this.state = { emotes: emotes, + decryptedemotes: {}, EmoteFieldsTouched: {}, newEmoteFileAdded: false, newEmoteCodeAdded: false, @@ -70,6 +73,7 @@ export default class RoomEmoteSettings extends React.Component { canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), value: value, }; + this.decryptEmotes(); } private uploadEmoteClick = (): void => { @@ -141,8 +145,8 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - const newEmote = await client.uploadContent(this.state.newEmoteFile); - emotesMxcs[this.state.newEmoteCode] = newEmote; + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile)//await client.uploadContent(this.state.newEmoteFile); + emotesMxcs[this.state.newEmoteCode] = newEmote.file; value[this.state.newEmoteCode] = this.state.newEmoteCode; } if (this.state.emotes) { @@ -172,6 +176,7 @@ export default class RoomEmoteSettings extends React.Component { newState.deletedItems = {}; } this.setState(newState as IState); + this.decryptEmotes(); }; private onEmoteChange = (e: React.ChangeEvent): void => { @@ -221,8 +226,17 @@ export default class RoomEmoteSettings extends React.Component { }); } }; - - public render(): JSX.Element { + private async decryptEmotes(){ + const decryptede={} + for (const shortcode in this.state.emotes) { + const blob = await decryptFile(this.state.emotes[shortcode]); + decryptede[shortcode] = URL.createObjectURL(blob); + } + this.setState({ + decryptedemotes:decryptede, + }); + } + public render(): JSX.Element { let emoteSettingsButtons; if ( this.state.canAddEmote @@ -262,7 +276,7 @@ export default class RoomEmoteSettings extends React.Component { />
    Date: Thu, 8 Sep 2022 15:44:27 -0400 Subject: [PATCH 029/176] encryption code cleanup --- src/autocomplete/EmojiProvider.tsx | 11 +++++------ src/components/views/messages/TextualBody.tsx | 8 ++++---- .../views/room_settings/RoomEmoteSettings.tsx | 12 ++++++------ 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 9c68b626b14..21bdbbc34fd 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -32,7 +32,6 @@ import SettingsStore from "../settings/SettingsStore"; import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji'; import { TimelineRenderingType } from '../contexts/RoomContext'; import * as recent from '../emojipicker/recent'; -import { mediaFromMxc } from "../customisations/Media"; import { decryptFile } from '../utils/DecryptFile'; const LIMIT = 20; @@ -104,15 +103,15 @@ export default class EmojiProvider extends AutocompleteProvider { this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); } - private async decryptEmotes(emotes: Object){ - const decryptede={} + private async decryptEmotes(emotes: Object) { + const decryptede={}; for (const shortcode in emotes) { - const blob = await decryptFile(emotes[shortcode]); + const blob = await decryptFile(emotes[shortcode]); const durl=URL.createObjectURL(blob); decryptede[shortcode] = ""; } - return decryptede + return decryptede; } async getCompletions( @@ -124,7 +123,7 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } - this.emotes=await this.emotesPromise + this.emotes=await this.emotesPromise; //console.log("emotes",this.emotes) const emojisAndEmotes=[...SORTED_EMOJI]; for (const key in this.emotes) { diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 70c6e3849e4..2cc2ee35262 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -563,21 +563,21 @@ export default class TextualBody extends React.Component { { `(${text})` } ); } - private async decryptEmotes(){ + private async decryptEmotes() { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.mxEvent.getRoomId()); //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const decryptede={} + const decryptede={}; for (const shortcode in rawEmotes) { - const blob = await decryptFile(rawEmotes[shortcode]); + const blob = await decryptFile(rawEmotes[shortcode]); const durl=URL.createObjectURL(blob); decryptede[":" + shortcode + ":"] = ""; } this.setState({ - finalEmotes:decryptede + finalEmotes: decryptede, }); this.forceUpdate(); } diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 977ab17f45e..5407650e94d 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -145,7 +145,7 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile)//await client.uploadContent(this.state.newEmoteFile); + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED emotesMxcs[this.state.newEmoteCode] = newEmote.file; value[this.state.newEmoteCode] = this.state.newEmoteCode; } @@ -226,17 +226,17 @@ export default class RoomEmoteSettings extends React.Component { }); } }; - private async decryptEmotes(){ - const decryptede={} + private async decryptEmotes() { + const decryptede={}; for (const shortcode in this.state.emotes) { - const blob = await decryptFile(this.state.emotes[shortcode]); + const blob = await decryptFile(this.state.emotes[shortcode]); decryptede[shortcode] = URL.createObjectURL(blob); } this.setState({ - decryptedemotes:decryptede, + decryptedemotes: decryptede, }); } - public render(): JSX.Element { + public render(): JSX.Element { let emoteSettingsButtons; if ( this.state.canAddEmote From 05ccbd59a2d39e3920904461ae9ad607fd42494b Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Sep 2022 16:17:30 -0400 Subject: [PATCH 030/176] encryption code type fixes --- src/components/views/messages/TextualBody.tsx | 2 +- src/components/views/room_settings/RoomEmoteSettings.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 2cc2ee35262..38c0f6ea5b1 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -58,7 +58,7 @@ interface IState { // track whether the preview widget is hidden widgetHidden: boolean; - finalEmotes: Dictionary; + finalEmotes: Dictionary; } export default class TextualBody extends React.Component { diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 5407650e94d..d5a6ea87fb3 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -22,13 +22,14 @@ import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; +import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; interface IProps { roomId: string; } interface IState { - emotes: Dictionary; + emotes: Dictionary; decryptedemotes: Dictionary; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; @@ -37,7 +38,7 @@ interface IState { newEmoteFile: File; canAddEmote: boolean; deleted: boolean; - deletedItems: Dictionary; + deletedItems: Dictionary; value: Dictionary; } From e5dcdbf1d8714c3fdfc4d2ff2a52a197bd48d39e Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:00:08 -0400 Subject: [PATCH 031/176] added multiple emote upload and read shortcode from filename --- .../views/room_settings/RoomEmoteSettings.tsx | 109 +++++++++++------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index d5a6ea87fb3..ef69068432b 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -34,8 +34,8 @@ interface IState { EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; newEmoteCodeAdded: boolean; - newEmoteCode: string; - newEmoteFile: File; + newEmoteCode: Array; + newEmoteFile: Array; canAddEmote: boolean; deleted: boolean; deletedItems: Dictionary; @@ -67,8 +67,8 @@ export default class RoomEmoteSettings extends React.Component { EmoteFieldsTouched: {}, newEmoteFileAdded: false, newEmoteCodeAdded: false, - newEmoteCode: "", - newEmoteFile: null, + newEmoteCode: [""], + newEmoteFile: [], deleted: false, deletedItems: {}, canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), @@ -105,13 +105,15 @@ export default class RoomEmoteSettings extends React.Component { EmoteFieldsTouched: {}, newEmoteFileAdded: false, newEmoteCodeAdded: false, + newEmoteCode: [""], + newEmoteFile: [], deleted: false, deletedItems: {}, value: value, }); - this.emoteUpload.current.value = ""; - this.emoteCodeUpload.current.value = ""; + //this.emoteUpload.current.value = ""; + //this.emoteCodeUpload.current.value = ""; }; private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); @@ -146,14 +148,16 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED - emotesMxcs[this.state.newEmoteCode] = newEmote.file; - value[this.state.newEmoteCode] = this.state.newEmoteCode; + for (var i = 0; i < this.state.newEmoteCode.length; i++) { + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; + } } if (this.state.emotes) { for (const shortcode in this.state.emotes) { if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) - && (shortcode === this.state.newEmoteCode)) { + && (shortcode in this.state.newEmoteCode)) { continue; } if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { @@ -167,14 +171,16 @@ export default class RoomEmoteSettings extends React.Component { } newState.value = value; await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); - this.emoteUpload.current.value = ""; - this.emoteCodeUpload.current.value = ""; + //this.emoteUpload.current.value = ""; + //this.emoteCodeUpload.current.value = ""; newState.newEmoteFileAdded = false; newState.newEmoteCodeAdded = false; newState.EmoteFieldsTouched = {}; newState.emotes = emotesMxcs; newState.deleted = false; newState.deletedItems = {}; + newState.newEmoteCode = [""] + newState.newEmoteFile =[] } this.setState(newState as IState); this.decryptEmotes(); @@ -198,32 +204,44 @@ export default class RoomEmoteSettings extends React.Component { return; } - const file = e.target.files[0]; - const reader = new FileReader(); - reader.onload = (ev) => { + const uploadedFiles=[] + const newCodes=[] + for (const file of e.target.files){ + const fileName = file.name.replace(/\.[^.]*$/,'') + uploadedFiles.push(file) + newCodes.push(fileName) + } + //reader.onload = (ev) => { this.setState({ + newEmoteCodeAdded: true, newEmoteFileAdded: true, - newEmoteFile: file, + newEmoteCode: newCodes, + newEmoteFile: uploadedFiles, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, }, }); - this.emoteUploadImage.current.src = URL.createObjectURL(file); - }; - reader.readAsDataURL(file); + //this.emoteUploadImage.current.src = URL.createObjectURL(file); + //}; + //reader.readAsDataURL(file); }; private onEmoteCodeAdd = (e: React.ChangeEvent): void => { if (e.target.value.length > 0) { + const updatedCode=this.state.newEmoteCode; + updatedCode[e.target.getAttribute("data-index")]=e.target.value; this.setState({ newEmoteCodeAdded: true, - newEmoteCode: e.target.value, + newEmoteCode: updatedCode, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, }, }); } else { + const updatedCode=this.state.newEmoteCode; + updatedCode[e.target.getAttribute("data-index")]=e.target.value; this.setState({ newEmoteCodeAdded: false, + newEmoteCode: updatedCode, }); } }; @@ -309,6 +327,35 @@ export default class RoomEmoteSettings extends React.Component { ); } + let uploadedEmotes =[]; + for (var i = 0; i < this.state.newEmoteCode.length; i++) { + const fileUrl=this.state.newEmoteFile[i]? URL.createObjectURL(this.state.newEmoteFile[i]):"" + uploadedEmotes.push( +
  • + + { + this.state.newEmoteFileAdded ? + : null + } + + { i==0? emoteUploadButton: null } +
  • + + ) + } return (
    { onClick={chromeFileInputFix} onChange={this.onEmoteFileAdd} accept="image/*" + multiple /> { emoteSettingsButtons } -
  • - - { - this.state.newEmoteFileAdded ? - : null - } - - { emoteUploadButton } -
  • + { uploadedEmotes } { existingEmotes } From 8d0a4e0fa3d671726f67e01dd1ad02c79947b85c Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:10:02 -0400 Subject: [PATCH 032/176] code cleanup --- .../views/room_settings/RoomEmoteSettings.tsx | 95 +++++++++---------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index ef69068432b..fb75165139b 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -148,11 +148,11 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - for (var i = 0; i < this.state.newEmoteCode.length; i++) { - const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; - value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; - } + for (let i = 0; i < this.state.newEmoteCode.length; i++) { + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; + } } if (this.state.emotes) { for (const shortcode in this.state.emotes) { @@ -179,8 +179,8 @@ export default class RoomEmoteSettings extends React.Component { newState.emotes = emotesMxcs; newState.deleted = false; newState.deletedItems = {}; - newState.newEmoteCode = [""] - newState.newEmoteFile =[] + newState.newEmoteCode = [""]; + newState.newEmoteFile =[]; } this.setState(newState as IState); this.decryptEmotes(); @@ -204,23 +204,23 @@ export default class RoomEmoteSettings extends React.Component { return; } - const uploadedFiles=[] - const newCodes=[] - for (const file of e.target.files){ - const fileName = file.name.replace(/\.[^.]*$/,'') - uploadedFiles.push(file) - newCodes.push(fileName) - } + const uploadedFiles=[]; + const newCodes=[]; + for (const file of e.target.files) { + const fileName = file.name.replace(/\.[^.]*$/, ''); + uploadedFiles.push(file); + newCodes.push(fileName); + } //reader.onload = (ev) => { - this.setState({ - newEmoteCodeAdded: true, - newEmoteFileAdded: true, - newEmoteCode: newCodes, - newEmoteFile: uploadedFiles, - EmoteFieldsTouched: { - ...this.state.EmoteFieldsTouched, - }, - }); + this.setState({ + newEmoteCodeAdded: true, + newEmoteFileAdded: true, + newEmoteCode: newCodes, + newEmoteFile: uploadedFiles, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); //this.emoteUploadImage.current.src = URL.createObjectURL(file); //}; //reader.readAsDataURL(file); @@ -327,34 +327,33 @@ export default class RoomEmoteSettings extends React.Component { ); } - let uploadedEmotes =[]; - for (var i = 0; i < this.state.newEmoteCode.length; i++) { - const fileUrl=this.state.newEmoteFile[i]? URL.createObjectURL(this.state.newEmoteFile[i]):"" + const uploadedEmotes = []; + for (let i = 0; i < this.state.newEmoteCode.length; i++) { + const fileUrl = this.state.newEmoteFile[i] ? URL.createObjectURL(this.state.newEmoteFile[i]) : ""; uploadedEmotes.push(
  • - - { - this.state.newEmoteFileAdded ? - : null - } - - { i==0? emoteUploadButton: null } -
  • + + { + this.state.newEmoteFileAdded ? + : null + } - ) + {i == 0 ? emoteUploadButton : null} + , + ); } return ( From 842bee475a7dea79bd21b81f34f5dccb78b2146f Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:16:54 -0400 Subject: [PATCH 033/176] code cleanup 2 --- src/components/views/room_settings/RoomEmoteSettings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index fb75165139b..f06fadea101 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -221,13 +221,13 @@ export default class RoomEmoteSettings extends React.Component { ...this.state.EmoteFieldsTouched, }, }); - //this.emoteUploadImage.current.src = URL.createObjectURL(file); + //this.emoteUploadImage.current.src = URL.createObjectURL(file); //}; //reader.readAsDataURL(file); }; private onEmoteCodeAdd = (e: React.ChangeEvent): void => { if (e.target.value.length > 0) { - const updatedCode=this.state.newEmoteCode; + const updatedCode = this.state.newEmoteCode; updatedCode[e.target.getAttribute("data-index")]=e.target.value; this.setState({ newEmoteCodeAdded: true, @@ -351,7 +351,7 @@ export default class RoomEmoteSettings extends React.Component { /> : null } - {i == 0 ? emoteUploadButton : null} + { i == 0 ? emoteUploadButton : null } , ); } From 4fefa5c5a4bcff078a7db35ee625cd14afd6d449 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:53:38 -0400 Subject: [PATCH 034/176] makes emotes work in public(unencrypted) rooms and encrypted rooms --- src/autocomplete/EmojiProvider.tsx | 21 +++++++---- src/components/views/messages/TextualBody.tsx | 13 +++++-- .../views/room_settings/RoomEmoteSettings.tsx | 35 +++++++++++++------ 3 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 21bdbbc34fd..c4665e24e32 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -22,6 +22,7 @@ import React from 'react'; import { uniq, sortBy } from 'lodash'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; @@ -33,6 +34,7 @@ import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji'; import { TimelineRenderingType } from '../contexts/RoomContext'; import * as recent from '../emojipicker/recent'; import { decryptFile } from '../utils/DecryptFile'; +import { mediaFromMxc } from '../customisations/Media'; const LIMIT = 20; @@ -82,7 +84,7 @@ export default class EmojiProvider extends AutocompleteProvider { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - this.emotesPromise = this.decryptEmotes(rawEmotes); + this.emotesPromise = this.decryptEmotes(rawEmotes,room?.roomId); this.emotes={}; // for (const key in rawEmotes) { FOR UNENCRYPTED // this.emotes[key] = ""; + if (isEnc) { + const blob = await decryptFile(emotes[shortcode]); + durl = URL.createObjectURL(blob); + } else { + durl = mediaFromMxc(emotes[shortcode]).srcHttp + } + decryptede[shortcode] = ""; } return decryptede; } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 38c0f6ea5b1..f7f64132ff5 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -50,6 +50,8 @@ import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from '../../../utils/Reply'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { decryptFile } from '../../../utils/DecryptFile'; +import { mediaFromMxc } from '../../../customisations/Media'; + const MAX_HIGHLIGHT_LENGTH = 4096; interface IState { @@ -570,9 +572,16 @@ export default class TextualBody extends React.Component { const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const decryptede={}; + let durl=""; + const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()) for (const shortcode in rawEmotes) { - const blob = await decryptFile(rawEmotes[shortcode]); - const durl=URL.createObjectURL(blob); + if (isEnc) { + const blob = await decryptFile(rawEmotes[shortcode]); + durl = URL.createObjectURL(blob); + } else { + durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp + } + decryptede[":" + shortcode + ":"] = ""; } diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index f06fadea101..bca980918a7 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -22,14 +22,14 @@ import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; -import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; +import { mediaFromMxc } from '../../../customisations/Media'; interface IProps { roomId: string; } interface IState { - emotes: Dictionary; + emotes: Dictionary; decryptedemotes: Dictionary; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; @@ -38,7 +38,7 @@ interface IState { newEmoteFile: Array; canAddEmote: boolean; deleted: boolean; - deletedItems: Dictionary; + deletedItems: Dictionary; value: Dictionary; } @@ -74,7 +74,11 @@ export default class RoomEmoteSettings extends React.Component { canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), value: value, }; - this.decryptEmotes(); + this.decryptEmotes(client.isRoomEncrypted(props.roomId)); + } + componentDidMount() { + const client = MatrixClientPeg.get(); + this.decryptEmotes(client.isRoomEncrypted(this.props.roomId)); } private uploadEmoteClick = (): void => { @@ -150,7 +154,12 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { for (let i = 0; i < this.state.newEmoteCode.length; i++) { const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + if (client.isRoomEncrypted(this.props.roomId)) { + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + } + else { + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url; + } value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; } } @@ -183,7 +192,7 @@ export default class RoomEmoteSettings extends React.Component { newState.newEmoteFile =[]; } this.setState(newState as IState); - this.decryptEmotes(); + this.decryptEmotes(client.isRoomEncrypted(this.props.roomId)); }; private onEmoteChange = (e: React.ChangeEvent): void => { @@ -245,15 +254,21 @@ export default class RoomEmoteSettings extends React.Component { }); } }; - private async decryptEmotes() { - const decryptede={}; + private async decryptEmotes(isEnc:boolean) { + const decryptede = {}; for (const shortcode in this.state.emotes) { - const blob = await decryptFile(this.state.emotes[shortcode]); - decryptede[shortcode] = URL.createObjectURL(blob); + if (isEnc) { + const blob = await decryptFile(this.state.emotes[shortcode]); + decryptede[shortcode] = URL.createObjectURL(blob); + } else { + decryptede[shortcode] = mediaFromMxc(this.state.emotes[shortcode]).srcHttp + } + } this.setState({ decryptedemotes: decryptede, }); + //this.forceUpdate(); } public render(): JSX.Element { let emoteSettingsButtons; From 53c9d35a87824f87db94fa275ebcd3742fe53ca6 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:01:08 -0400 Subject: [PATCH 035/176] code fix --- src/autocomplete/EmojiProvider.tsx | 9 ++++----- src/components/views/messages/TextualBody.tsx | 4 ++-- src/components/views/room_settings/RoomEmoteSettings.tsx | 8 +++----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index c4665e24e32..2dd9d5de421 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -23,7 +23,6 @@ import { uniq, sortBy } from 'lodash'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixClientPeg } from "../MatrixClientPeg"; - import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; import QueryMatcher from './QueryMatcher'; @@ -84,7 +83,7 @@ export default class EmojiProvider extends AutocompleteProvider { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - this.emotesPromise = this.decryptEmotes(rawEmotes,room?.roomId); + this.emotesPromise = this.decryptEmotes(rawEmotes, room?.roomId); this.emotes={}; // for (const key in rawEmotes) { FOR UNENCRYPTED // this.emotes[key] = ""; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index f7f64132ff5..75525ded8f6 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -573,13 +573,13 @@ export default class TextualBody extends React.Component { const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const decryptede={}; let durl=""; - const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()) + const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()); for (const shortcode in rawEmotes) { if (isEnc) { const blob = await decryptFile(rawEmotes[shortcode]); durl = URL.createObjectURL(blob); } else { - durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp + durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp; } decryptede[":" + shortcode + ":"] = " Date: Sun, 25 Sep 2022 01:38:58 -0400 Subject: [PATCH 037/176] build commits also has changes to bug fix for html rendering emotes --- package.json | 2 +- src/HtmlUtils.tsx | 82 +++++++++++++++++++++++++++++++++++++++++------ yarn.lock | 7 ++-- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 082ffb66ead..f7da0fdaea6 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "maplibre-gl": "^1.15.2", "matrix-encrypt-attachment": "^1.0.3", "matrix-events-sdk": "^0.0.1-beta.7", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "19.5.0", "matrix-widget-api": "^1.0.0", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index b44ebab2121..ed567a6284c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -425,20 +425,85 @@ function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | s const result: (JSX.Element | string)[] = []; let text = ''; let key = 0; + let cleanmsg = ''; + let titles = []; + let alts = []; + let spoilers=[]; + const $ = cheerio.load(message, { + // @ts-ignore: The `_useHtmlParser2` internal option is the + // simplest way to both parse and render using `htmlparser2`. + _useHtmlParser2: true, + decodeEntities: false, + }); + if (isHtmlMessage) { + + $("img").each(function (i, elm) { + + titles.push($(this).attr("title") ? $(this).attr("title") : null) + alts.push($(this).attr("alt") ? $(this).attr("alt") : null) + $(this).attr("title", "placeholder") + $(this).attr("alt", "placeholder") + } + ) + + $("span").each(function (i, elm) { + if($(this).attr("data-mx-spoiler")){ + spoilers.push($(this).attr("data-mx-spoiler")) + $(this).attr("data-mx-spoiler", "placeholder") + } + + }) + cleanmsg = $.html() + } + else { + cleanmsg = message; + } // We use lodash's grapheme splitter to avoid breaking apart compound emojis - for (const char of split(message, '')) { + for (const char of split(cleanmsg, '')) { if (EMOJIBASE_REGEX.test(char)) { - if (text) { - result.push(text); - text = ''; + if (!isHtmlMessage) { + if (text) { + result.push(text); + text = ''; + } + + result.push(emojiToSpan(char, key)); + key++; + } else { + text += emojiToSpan(char, key); } - result.push(emojiToSpan(char, key)); - key++; } else { text += char; } } + if (isHtmlMessage) { + console.log("text ",text) + const $ = cheerio.load(text, { + // @ts-ignore: The `_useHtmlParser2` internal option is the + // simplest way to both parse and render using `htmlparser2`. + _useHtmlParser2: true, + decodeEntities: false, + }); + let cnt = 0; + let cntalt = 0; + let splrcnt = 0; + $("img").each(function (i, elm) { + $(this).attr("title", titles[cnt]) + cnt = cnt + 1; + $(this).attr("alt", alts[cntalt]) + cntalt += 1; + } + ) + $("span").each(function (i, elm) { + if($(this).attr("data-mx-spoiler")){ + $(this).attr("data-mx-spoiler", spoilers[splrcnt]) + splrcnt+=1; + } + + }) + text = $.html() + } if (text) { result.push(text); } @@ -497,8 +562,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join('') - .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + return highlighter.applyHighlights(safeText, safeHighlights).join(''); }; } @@ -538,7 +602,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } let contentBody = safeBody ?? strippedBody; - contentBody = contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + //contentBody = contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); if (opts.returnString) { return contentBody; } diff --git a/yarn.lock b/yarn.lock index aae0dc19f3f..570609263d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2218,7 +2218,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^17", "@types/react@^17.0.49": +"@types/react@*", "@types/react@17.0.49", "@types/react@^17", "@types/react@^17.0.49": version "17.0.49" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.49.tgz#df87ba4ca8b7942209c3dc655846724539dc1049" integrity sha512-CCBPMZaPhcKkYUTqFs/hOWqKjPxhTEmnZWjlHHgIMop67DsXywf9B5Os9Hz8KSacjNOgIdnZVJamwl232uxoPg== @@ -6778,9 +6778,10 @@ matrix-events-sdk@^0.0.1-beta.7: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934" integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": +"matrix-js-sdk@19.5.0": version "19.5.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/59c82cb67972257cded956d25291961c21bd27ba" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-19.5.0.tgz#debc8e92b2e73204c8d5bc9a128a2d0ec0a92ade" + integrity sha512-WTmXMwyhGjUVv3eR71P9wdZj4qqNPgzg9Ud7V6kB3avhZJTZlIpNdHuldXXUdPJ8WTDKY+/yDtEFLIg8pj2Q8A== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From ce04fced93b2b99dea1916cef304a49195dfd465 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 26 Oct 2022 10:36:56 -0400 Subject: [PATCH 038/176] Removing any and dictionary types --- src/HtmlUtils.tsx | 2 +- src/autocomplete/EmojiProvider.tsx | 11 +++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index b44ebab2121..74b9940311b 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -396,7 +396,7 @@ interface IOpts { returnString?: boolean; forComposerQuote?: boolean; ref?: React.Ref; - emotes?: Dictionary; + emotes?: Map; } export interface IOptsReturnNode extends IOpts { diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index eaee6e5623f..c40c876a368 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -78,18 +78,13 @@ export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - emotes: Dictionary; - emotesPromise: Promise; + private emotes: Map; + private emotesPromise: Promise; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; this.emotesPromise = this.decryptEmotes(rawEmotes, room?.roomId); - this.emotes={}; - // for (const key in rawEmotes) { FOR UNENCRYPTED - // this.emotes[key] = ""; - // } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], @@ -106,7 +101,7 @@ export default class EmojiProvider extends AutocompleteProvider { } private async decryptEmotes(emotes: Object, roomId: string) { - const decryptede={}; + const decryptede=new Map(); const client = MatrixClientPeg.get(); let durl = ""; const isEnc=client.isRoomEncrypted(roomId); From d92c6f0ef7c1e27181be4082d25c527a29d8baf4 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:09:23 -0400 Subject: [PATCH 039/176] Replacing dictionary and any 2 --- src/HtmlUtils.tsx | 2 +- src/autocomplete/EmojiProvider.tsx | 6 +++--- src/components/views/messages/TextualBody.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 74b9940311b..f778f22f592 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -396,7 +396,7 @@ interface IOpts { returnString?: boolean; forComposerQuote?: boolean; ref?: React.Ref; - emotes?: Map; + emotes?: Map; } export interface IOptsReturnNode extends IOpts { diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index c40c876a368..a76377391ac 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -78,8 +78,8 @@ export default class EmojiProvider extends AutocompleteProvider { matcher: QueryMatcher; nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - private emotes: Map; - private emotesPromise: Promise; + private emotes: Map; + private emotesPromise: Promise>; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); @@ -101,7 +101,7 @@ export default class EmojiProvider extends AutocompleteProvider { } private async decryptEmotes(emotes: Object, roomId: string) { - const decryptede=new Map(); + const decryptede=new Map(); const client = MatrixClientPeg.get(); let durl = ""; const isEnc=client.isRoomEncrypted(roomId); diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index bd0207b91b3..eed4b25e75e 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -62,7 +62,7 @@ interface IState { // track whether the preview widget is hidden widgetHidden: boolean; - finalEmotes: Dictionary; + finalEmotes: Map; } export default class TextualBody extends React.Component { @@ -81,7 +81,7 @@ export default class TextualBody extends React.Component { this.state = { links: [], widgetHidden: false, - finalEmotes: {}, + finalEmotes: new Map(), }; } @@ -573,7 +573,7 @@ export default class TextualBody extends React.Component { //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const decryptede={}; + const decryptede=new Map; let durl=""; const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()); for (const shortcode in rawEmotes) { From 6bef01280cb24c7387e843a39cfdc0abda4d6a65 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 16 Dec 2022 09:59:42 -0500 Subject: [PATCH 040/176] Added custom emotes to the emojipicker --- src/HtmlUtils.tsx | 1 + src/components/views/emojipicker/Category.tsx | 2 +- src/components/views/emojipicker/Emoji.tsx | 4 +- .../views/emojipicker/EmojiPicker.tsx | 104 +++++++++++++++++- src/components/views/emojipicker/Preview.tsx | 4 +- .../views/rooms/MessageComposerButtons.tsx | 15 ++- src/emoji.ts | 4 + 7 files changed, 117 insertions(+), 17 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index f778f22f592..ca2bc9e5cf3 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -469,6 +469,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (opts.forComposerQuote) { sanitizeParams = composerSanitizeHtmlParams; } + let strippedBody: string; let safeBody: string; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx index 45ed7144e0d..b140a1539e8 100644 --- a/src/components/views/emojipicker/Category.tsx +++ b/src/components/views/emojipicker/Category.tsx @@ -24,7 +24,7 @@ import Emoji from './Emoji'; const OVERFLOW_ROWS = 3; -export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent"; +export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent" | "custom"; export interface ICategory { id: CategoryKey; diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index e36bd9ff5c7..6090cae96cc 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -40,11 +40,11 @@ class Emoji extends React.PureComponent { onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} className="mx_EmojiPicker_item_wrapper" - label={emoji.unicode} + label={emoji.customLabel?emoji.customLabel:emoji.unicode} disabled={this.props.disabled} >
    - { emoji.unicode } + { emoji.customComponent?emoji.customComponent:emoji.unicode }
    ); diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 95e0e24ae18..0386f4394da 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; - +import { useContext } from "react"; import { _t } from '../../../languageHandler'; import * as recent from '../../../emojipicker/recent'; import { DATA_BY_CATEGORY, getEmojiFromUnicode, IEmoji } from "../../../emoji"; @@ -26,6 +26,13 @@ import Search from "./Search"; import Preview from "./Preview"; import QuickReactions from "./QuickReactions"; import Category, { ICategory, CategoryKey } from "./Category"; +import { Room } from './matrix-js-sdk/src/models/room'; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import MatrixClientContext from '../../../contexts/MatrixClientContext'; +import RoomContext from '../../../contexts/RoomContext'; +import { mediaFromMxc } from '../../../customisations/Media'; +import { decryptFile } from '../../../utils/DecryptFile'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; @@ -38,6 +45,7 @@ interface IProps { showQuickReactions?: boolean; onChoose(unicode: string): boolean; isEmojiDisabled?: (unicode: string) => boolean; + room?:Room; } interface IState { @@ -51,15 +59,27 @@ interface IState { } class EmojiPicker extends React.Component { - private readonly recentlyUsed: IEmoji[]; + private recentlyUsed: IEmoji[]; private readonly memoizedDataByCategory: Record; private readonly categories: ICategory[]; private scrollRef = React.createRef>(); + private emotes: Map; + private emotesPromise: Promise>; + private finalEmotes: IEmoji[]; + private finalEmotesMap:Map; constructor(props: IProps) { super(props); + + const emotesEvent = props.room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + this.emotesPromise = this.decryptEmotes(rawEmotes, props.room?.roomId); + this.finalEmotes=[]; + this.finalEmotesMap=new Map(); + this.loadEmotes() + this.state = { filter: "", previewEmoji: null, @@ -71,16 +91,23 @@ class EmojiPicker extends React.Component { this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); this.memoizedDataByCategory = { recent: this.recentlyUsed, + custom: this.finalEmotes, ...DATA_BY_CATEGORY, }; this.categories = [{ id: "recent", name: _t("Frequently Used"), - enabled: this.recentlyUsed.length > 0, - visible: this.recentlyUsed.length > 0, + enabled: true, + visible: true, ref: React.createRef(), - }, { + },{ + id: "custom", + name: _t("Custom"), + enabled: true, + visible: true, + ref: React.createRef(), + },{ id: "people", name: _t("Smileys & People"), enabled: true, @@ -131,6 +158,67 @@ class EmojiPicker extends React.Component { }]; } + private async loadEmotes(){ + this.emotes=await this.emotesPromise + for (const key in this.emotes) { + this.finalEmotes.push( + { label: key, + shortcodes: [key], + hexcode: key, + unicode: ":"+key+":", + customLabel:key, + customComponent:this.emotes[key] + } + ); + this.finalEmotesMap.set((":"+key+":").trim(),{ + label: key, + shortcodes: [key], + hexcode: key, + unicode: ":"+key+":", + customLabel:key, + customComponent:this.emotes[key] + }); + } + + let rec=Array.from(new Set(recent.get())); + rec.forEach((v,i)=>{ + if(this.finalEmotesMap.get(v as string)){ + if(i>=this.recentlyUsed.length){ + this.recentlyUsed.push(this.finalEmotesMap.get(v as string)) + } + else{ + this.recentlyUsed[i]=this.finalEmotesMap.get(v as string) + } + + } else if(getEmojiFromUnicode(v as string)){ + if(i>=this.recentlyUsed.length){ + this.recentlyUsed.push(getEmojiFromUnicode(v as string)) + } + else{ + this.recentlyUsed[i]=getEmojiFromUnicode(v as string) + } + } + }) + this.onScroll(); + } + + private async decryptEmotes(emotes: Object, roomId: string) { + const decryptede=new Map(); + const client = MatrixClientPeg.get(); + let durl = ""; + const isEnc=client.isRoomEncrypted(roomId); + for (const shortcode in emotes) { + if (isEnc) { + const blob = await decryptFile(emotes[shortcode]); + durl = URL.createObjectURL(blob); + } else { + durl = mediaFromMxc(emotes[shortcode]).srcHttp; + } + decryptede[shortcode] = ; + } + return decryptede; + } + private onScroll = () => { const body = this.scrollRef.current?.containerRef.current; this.setState({ @@ -180,7 +268,11 @@ class EmojiPicker extends React.Component { if (lcFilter.includes(this.state.filter)) { emojis = this.memoizedDataByCategory[cat.id]; } else { + emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id]; + if(cat.id==="custom"){ + emojis=this.finalEmotes + } } emojis = emojis.filter(emoji => this.emojiMatchesFilter(emoji, lcFilter)); this.memoizedDataByCategory[cat.id] = emojis; @@ -266,7 +358,7 @@ class EmojiPicker extends React.Component { selectedEmojis={this.props.selectedEmojis} /> ); - const height = EmojiPicker.categoryHeightForEmojiCount(emojis.length); + const height = EmojiPicker.categoryHeightForEmojiCount(emojis?.length); heightBefore += height; return categoryElement; }) } diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index bcbbec1feb1..18d14d8270c 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -25,12 +25,12 @@ interface IProps { class Preview extends React.PureComponent { render() { - const { unicode, label, shortcodes: [shortcode] } = this.props.emoji; + const { unicode, label, shortcodes: [shortcode], customComponent } = this.props.emoji; return (
    - { unicode } + { customComponent?customComponent:unicode }
    diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index b77bff66a8f..89cfd25aebc 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -72,7 +72,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { let moreButtons: ReactElement[]; if (narrow) { mainButtons = [ - emojiButton(props), + emojiButton(props,room), ]; moreButtons = [ uploadButton(), // props passed via UploadButtonContext @@ -84,7 +84,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { ]; } else { mainButtons = [ - emojiButton(props), + emojiButton(props,room), uploadButton(), // props passed via UploadButtonContext ]; moreButtons = [ @@ -129,23 +129,25 @@ const MessageComposerButtons: React.FC = (props: IProps) => { ; }; -function emojiButton(props: IProps): ReactElement { +function emojiButton(props: IProps, room: Room): ReactElement { return ; } interface IEmojiButtonProps { addEmoji: (unicode: string) => boolean; menuPosition: AboveLeftOf; + room: Room; } -const EmojiButton: React.FC = ({ addEmoji, menuPosition }) => { +const EmojiButton: React.FC = ({ addEmoji, menuPosition, room}) => { const overflowMenuCloser = useContext(OverflowMenuContext); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); - let contextMenu: React.ReactElement | null = null; if (menuDisplayed) { const position = ( @@ -160,7 +162,7 @@ const EmojiButton: React.FC = ({ addEmoji, menuPosition }) => }} managed={false} > - + ; } @@ -169,6 +171,7 @@ const EmojiButton: React.FC = ({ addEmoji, menuPosition }) => { "mx_MessageComposer_button_highlight": menuDisplayed, }, + "mx_EmojiButton_icon" ); // TODO: replace ContextMenuTooltipButton with a unified representation of diff --git a/src/emoji.ts b/src/emoji.ts index a8b5a228be9..323c0d2a510 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -14,9 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import EMOJIBASE from 'emojibase-data/en/compact.json'; import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json'; + export interface IEmoji { label: string; group?: number; @@ -27,6 +29,8 @@ export interface IEmoji { unicode: string; skins?: Omit[]; // Currently unused emoticon?: string | string[]; + customLabel?:string; + customComponent?:React.Component; } // The unicode is stored without the variant selector From ceec93ef067cc879d85100f0bd3c0c47f0a50bba Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Sun, 18 Dec 2022 20:14:36 -0500 Subject: [PATCH 041/176] Update MessageComposerButtons.tsx --- .../views/rooms/MessageComposerButtons.tsx | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 6ceff4176ef..89cfd25aebc 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -17,7 +17,7 @@ limitations under the License. import classNames from 'classnames'; import { IEventRelation } from "matrix-js-sdk/src/models/event"; import { M_POLL_START } from "matrix-events-sdk"; -import React, { createContext, MouseEventHandler, ReactElement, useContext, useRef } from 'react'; +import React, { createContext, ReactElement, useContext, useRef } from 'react'; import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread'; @@ -25,8 +25,9 @@ import { THREAD_RELATION_TYPE } from 'matrix-js-sdk/src/models/thread'; import { _t } from '../../../languageHandler'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import { CollapsibleButton } from './CollapsibleButton'; -import { AboveLeftOf } from '../../structures/ContextMenu'; +import ContextMenu, { aboveLeftOf, AboveLeftOf, useContextMenu } from '../../structures/ContextMenu'; import dis from '../../../dispatcher/dispatcher'; +import EmojiPicker from '../emojipicker/EmojiPicker'; import ErrorDialog from "../dialogs/ErrorDialog"; import LocationButton from '../location/LocationButton'; import Modal from "../../../Modal"; @@ -38,8 +39,6 @@ import RoomContext from '../../../contexts/RoomContext'; import { useDispatcher } from "../../../hooks/useDispatcher"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import IconizedContextMenu, { IconizedContextMenuOptionList } from '../context_menus/IconizedContextMenu'; -import { EmojiButton } from './EmojiButton'; -import { useSettingValue } from '../../../hooks/useSettings'; interface IProps { addEmoji: (emoji: string) => boolean; @@ -56,8 +55,6 @@ interface IProps { toggleButtonMenu: () => void; showVoiceBroadcastButton: boolean; onStartVoiceBroadcastClick: () => void; - isRichTextEnabled: boolean; - onComposerModeClick: () => void; } type OverflowMenuCloser = () => void; @@ -67,8 +64,6 @@ const MessageComposerButtons: React.FC = (props: IProps) => { const matrixClient: MatrixClient = useContext(MatrixClientContext); const { room, roomId, narrow } = useContext(RoomContext); - const isWysiwygLabEnabled = useSettingValue('feature_wysiwyg_composer'); - if (props.haveRecording) { return null; } @@ -405,23 +400,4 @@ function showLocationButton( ); } -interface WysiwygToggleButtonProps { - isRichTextEnabled: boolean; - onClick: MouseEventHandler; -} - -function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonProps) { - const title = isRichTextEnabled ? _t("Hide formatting") : _t("Show formatting"); - - return ; -} - export default MessageComposerButtons; From 6b8ff65ed63f2a9c6a69ea46e80aaefde5b6cd60 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 28 Dec 2022 12:52:14 -0500 Subject: [PATCH 042/176] Add compatibility option with other clients --- .../views/room_settings/RoomEmoteSettings.tsx | 89 ++++++++++++++++++- .../views/rooms/SendMessageComposer.tsx | 54 +++++++++-- 2 files changed, 133 insertions(+), 10 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 68b2df5c7bb..390d9bcfde4 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -23,7 +23,12 @@ import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; import { mediaFromMxc } from '../../../customisations/Media'; - +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; +import SettingsFieldset from '../settings/SettingsFieldset'; +import LabelledToggleSwitch from '../elements/LabelledToggleSwitch'; +import { stringify } from '../dialogs/devtools/Event'; +const EMOTES_STATE=new UnstableValue("m.room.emotes","org.matrix.msc3892.emotes") +const COMPAT_STATE=new UnstableValue("m.room.clientemote_compatibility","org.matrix.msc3892.clientemote_compatibility") interface IProps { roomId: string; } @@ -40,6 +45,7 @@ interface IState { deleted: boolean; deletedItems: Dictionary; value: Dictionary; + compatiblity: boolean; } // TODO: Merge with EmoteSettings? @@ -47,12 +53,14 @@ export default class RoomEmoteSettings extends React.Component { private emoteUpload = createRef(); private emoteCodeUpload = createRef(); private emoteUploadImage = createRef(); + private imagePack:object; constructor(props: IProps) { super(props); const client = MatrixClientPeg.get(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); + //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; @@ -61,6 +69,15 @@ export default class RoomEmoteSettings extends React.Component { value[emote] = emote; } + const compatEvent = room.currentState.getStateEvents("m.room.clientemote_compatibility", ""); + const compat = compatEvent ? (compatEvent.getContent().isCompat || false) : false; + + const imagePackEvent = room.currentState.getStateEvents("m.image_pack", ""); + this.imagePack = imagePackEvent ? (imagePackEvent.getContent() || {"images":new Map()}) : {"images":new Map()}; + if(!this.imagePack["images"]){ + this.imagePack={"images":new Map()} + } + this.state = { emotes: emotes, decryptedemotes: {}, @@ -71,8 +88,9 @@ export default class RoomEmoteSettings extends React.Component { newEmoteFile: [], deleted: false, deletedItems: {}, - canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), + canAddEmote: room.currentState.maySendStateEvent("m.room.emotes", client.getUserId()), value: value, + compatibility:compat }; this.decryptEmotes(client.isRoomEncrypted(props.roomId)); } @@ -147,6 +165,7 @@ export default class RoomEmoteSettings extends React.Component { const newState: Partial = {}; const emotesMxcs = {}; const value = {}; + const newPack={"images":new Map()} // TODO: What do we do about errors? if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { @@ -160,25 +179,43 @@ export default class RoomEmoteSettings extends React.Component { emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url; } value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; + if(this.state.compatibility){ + if (client.isRoomEncrypted(this.props.roomId)) { + const compatNewEmote=await client.uploadContent(this.state.newEmoteFile[i]) + newPack["images"][this.state.newEmoteCode[i]] = {"url":compatNewEmote.content_uri}; + } else { + newPack["images"][this.state.newEmoteCode[i]] = {"url":newEmote.url}; + } + } } } if (this.state.emotes) { for (const shortcode in this.state.emotes) { if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) - && (shortcode in this.state.newEmoteCode)) { + && (shortcode in this.state.newEmoteCode)) { continue; } if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; + if (this.imagePack[shortcode]) { + newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { "url": this.imagePack[shortcode]["url"] }; + } + } else { emotesMxcs[shortcode] = this.state.emotes[shortcode]; value[shortcode] = shortcode; + if (this.imagePack["images"][shortcode]) { + newPack["images"][shortcode] = { "url": this.imagePack["images"][shortcode]["url"] } + } } } } newState.value = value; - await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); + await client.sendStateEvent(this.props.roomId, "m.room.emotes", emotesMxcs, ""); + this.imagePack=newPack + await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); + //this.emoteUpload.current.value = ""; //this.emoteCodeUpload.current.value = ""; newState.newEmoteFileAdded = false; @@ -253,6 +290,34 @@ export default class RoomEmoteSettings extends React.Component { }); } }; + + private onCompatChange = async (allowed: boolean) => { + + const client = MatrixClientPeg.get(); + await client.sendStateEvent(this.props.roomId, "m.room.clientemote_compatibility", { isCompat: allowed }, ""); + + if (allowed) { + for (const shortcode in this.state.emotes) { + if(!this.imagePack["images"][shortcode]){ + if (client.isRoomEncrypted(this.props.roomId)) { + const blob = await decryptFile(this.state.emotes[shortcode]); + const uploadedEmote = await client.uploadContent(blob) + this.imagePack["images"][shortcode] = { "url": uploadedEmote.content_uri }; + } else { + this.imagePack["images"][shortcode] = { "url": this.state.emotes[shortcode] }; + } + } + } + + await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); + } + + this.setState({ + compatibility: allowed, + } + ) + + } private async decryptEmotes(isEnc: boolean) { const decryptede = {}; for (const shortcode in this.state.emotes) { @@ -368,6 +433,7 @@ export default class RoomEmoteSettings extends React.Component { , ); } + const isCompat = this.state.compatibility return ( { multiple /> { emoteSettingsButtons } + { + + + + } { uploadedEmotes } { existingEmotes diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 8c423663f5d..03667a34ef6 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -39,8 +39,8 @@ import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer"; import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; import { findEditableEvent } from '../../../utils/EventUtils'; import SendHistoryManager from "../../../SendHistoryManager"; -import { CommandCategories } from '../../../SlashCommands'; -import ContentMessages from '../../../ContentMessages'; +import { CommandCategories } from "../../../SlashCommands"; +import ContentMessages, { uploadFile } from "../../../ContentMessages"; import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext"; import { Action } from "../../../dispatcher/actions"; import { containsEmoji } from "../../../effects/utils"; @@ -60,6 +60,7 @@ import { PosthogAnalytics } from "../../../PosthogAnalytics"; import { addReplyToMessageContent } from '../../../utils/Reply'; import { doMaybeLocalRoomAction } from '../../../utils/local-room'; + // Merges favouring the given relation export function attachRelation(content: IContent, relation?: IEventRelation): void { if (relation) { @@ -77,7 +78,10 @@ export function createMessageContent( relation: IEventRelation | undefined, permalinkCreator: RoomPermalinkCreator, includeReplyLegacyFallback = true, + emotes?:Map, + compat?:boolean, ): IContent { + const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -88,19 +92,33 @@ export function createMessageContent( model = unescapeMessage(model); const body = textSerialize(model); - + let emoteBody; + if (compat) { + emoteBody = body.replace(/:[\w+-]+:/g, m => emotes[m] ? emotes[m] : m) + } const content: IContent = { msgtype: isEmote ? "m.emote" : "m.text", body: body, }; - const formattedBody = htmlSerializeIfNeeded(model, { + let formattedBody = htmlSerializeIfNeeded(model, { forceHTML: !!replyToEvent, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { + if(compat){ + formattedBody=formattedBody.replace(/:[\w+-]+:/g, m => emotes[m] ? emotes[m] : m) + } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; } + else if (compat){ + if(body!=emoteBody){ + content.format="org.matrix.custom.html" + content.formatted_body = emoteBody + } + } + + attachRelation(content, relation); if (replyToEvent) { @@ -153,7 +171,9 @@ export class SendMessageComposer extends React.Component; + private compat: boolean; static defaultProps = { includeReplyLegacyFallback: true, }; @@ -167,6 +187,25 @@ export class SendMessageComposer extends React.Component()}) : {"images":new Map()}; + this.emotes=new Map() + + for (const shortcode in this.imagePack["images"]){ + this.emotes[":"+shortcode+":"]="" + } } public componentDidUpdate(prevProps: ISendMessageComposerProps): void { @@ -182,6 +221,8 @@ export class SendMessageComposer extends React.Component { // ignore any keypress while doing IME compositions if (this.editorRef.current?.isComposing(event)) { @@ -315,7 +356,6 @@ export class SendMessageComposer extends React.Component { const model = this.model; - if (model.isEmpty) { return; } @@ -391,6 +431,8 @@ export class SendMessageComposer extends React.Component Date: Wed, 28 Dec 2022 23:51:45 -0500 Subject: [PATCH 043/176] Update RoomEmoteSettings.tsx --- src/components/views/room_settings/RoomEmoteSettings.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 390d9bcfde4..b11d0c4f0cc 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -198,8 +198,8 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; - if (this.imagePack[shortcode]) { - newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { "url": this.imagePack[shortcode]["url"] }; + if (this.imagePack["images"][shortcode]) { + newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { "url": this.imagePack["images"][shortcode]["url"] }; } } else { @@ -456,8 +456,9 @@ export default class RoomEmoteSettings extends React.Component { Date: Thu, 29 Dec 2022 15:45:32 -0500 Subject: [PATCH 044/176] Set height on compatible emotes --- src/components/views/rooms/SendMessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 03667a34ef6..d1234f10b68 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -204,7 +204,7 @@ export class SendMessageComposer extends React.Component() for (const shortcode in this.imagePack["images"]){ - this.emotes[":"+shortcode+":"]="" + this.emotes[":"+shortcode+":"]=""+":"+shortcode+":" } } From 0ce29b3b366a5e9f5d4e6faa1397eaaff1518582 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 29 Dec 2022 17:53:48 -0500 Subject: [PATCH 045/176] Emojipicker updates --- src/components/views/rooms/EmojiButton.tsx | 9 +++- .../views/rooms/MessageComposerButtons.tsx | 52 +------------------ 2 files changed, 9 insertions(+), 52 deletions(-) diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index 9a02d70537c..06071fc48b9 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -22,14 +22,16 @@ import ContextMenu, { aboveLeftOf, AboveLeftOf, useContextMenu } from "../../str import EmojiPicker from "../emojipicker/EmojiPicker"; import { CollapsibleButton } from "./CollapsibleButton"; import { OverflowMenuContext } from "./MessageComposerButtons"; +import { Room } from 'matrix-js-sdk/src/models/room'; interface IEmojiButtonProps { addEmoji: (unicode: string) => boolean; menuPosition: AboveLeftOf; className?: string; + room: Room, } -export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps) { +export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiButtonProps) { const overflowMenuCloser = useContext(OverflowMenuContext); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -46,7 +48,7 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP }} managed={false} > - + ); } @@ -71,3 +73,6 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP ); } + + + diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 89e48ac037f..3de199062d6 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -40,6 +40,7 @@ import { useDispatcher } from "../../../hooks/useDispatcher"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; import { useSettingValue } from "../../../hooks/useSettings"; +import { EmojiButton } from "./EmojiButton"; interface IProps { addEmoji: (emoji: string) => boolean; @@ -150,7 +151,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { ); }; -function emojiButton(props: IProps): ReactElement { +function emojiButton(props: IProps, room: Room): ReactElement { return ( boolean; - menuPosition: AboveLeftOf; - room: Room; -} - -const EmojiButton: React.FC = ({ addEmoji, menuPosition, room}) => { - const overflowMenuCloser = useContext(OverflowMenuContext); - const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); - let contextMenu: React.ReactElement | null = null; - if (menuDisplayed) { - const position = ( - menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect()) - ); - - contextMenu = { - closeMenu(); - overflowMenuCloser?.(); - }} - managed={false} - > - - ; - } - - const className = classNames( - "mx_MessageComposer_button", - { - "mx_MessageComposer_button_highlight": menuDisplayed, - }, - "mx_EmojiButton_icon" - ); - - // TODO: replace ContextMenuTooltipButton with a unified representation of - // the header buttons and the right panel buttons - return - - - { contextMenu } - ; -}; function uploadButton(): ReactElement { return ; From d83c75060d93dc4605161468c361b209f773d8a8 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:47:59 -0500 Subject: [PATCH 046/176] Autocomplete bug fix --- src/autocomplete/Autocompleter.ts | 2 +- src/autocomplete/EmojiProvider.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 212cefdc187..39523a7298e 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -50,7 +50,7 @@ export interface ICompletion { const PROVIDERS = [UserProvider, RoomProvider, EmojiProvider, NotifProvider, CommandProvider, SpaceProvider]; // Providers will get rejected if they take longer than this. -const PROVIDER_COMPLETION_TIMEOUT = 3000; +const PROVIDER_COMPLETION_TIMEOUT = 30000; export interface IProviderCompletions { completions: ICompletion[]; diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 85c6c80e96b..d7a5548177c 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -96,7 +96,6 @@ export default class EmojiProvider extends AutocompleteProvider { // For removing punctuation shouldMatchWordsOnly: true, }); - this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); } @@ -128,7 +127,6 @@ export default class EmojiProvider extends AutocompleteProvider { return []; // don't give any suggestions if the user doesn't want them } this.emotes=await this.emotesPromise; - //console.log("emotes",this.emotes) const emojisAndEmotes=[...SORTED_EMOJI]; for (const key in this.emotes) { emojisAndEmotes.push({ @@ -181,7 +179,7 @@ export default class EmojiProvider extends AutocompleteProvider { }); completions = sortBy(uniq(completions), sorters); - completions = completions.map(c => ({ + return completions.map(c => ({ completion: this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode, component: ( From 6d2ba5d40f227dfba000a14a8dc4ea68f9f4c88c Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 29 Dec 2022 19:48:45 -0500 Subject: [PATCH 047/176] Update Autocompleter.ts --- src/autocomplete/Autocompleter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 39523a7298e..212cefdc187 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -50,7 +50,7 @@ export interface ICompletion { const PROVIDERS = [UserProvider, RoomProvider, EmojiProvider, NotifProvider, CommandProvider, SpaceProvider]; // Providers will get rejected if they take longer than this. -const PROVIDER_COMPLETION_TIMEOUT = 30000; +const PROVIDER_COMPLETION_TIMEOUT = 3000; export interface IProviderCompletions { completions: ICompletion[]; From 8d8d851908f8d0e1a16314e97c83759e186f3285 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Wed, 10 Aug 2022 15:03:58 -0400 Subject: [PATCH 048/176] emotes show up in messages --- res/css/views/rooms/_EventTile.pcss | 4 +- src/HtmlUtils.tsx | 27 +- src/components/structures/MessagePanel.tsx | 85 +- .../views/dialogs/RoomSettingsDialog.tsx | 26 +- src/components/views/messages/TextualBody.tsx | 65 +- .../views/room_settings/RoomEmoteSettings.tsx | 388 +++++ .../settings/tabs/room/EmoteSettingsTab.tsx | 75 + src/matrix-react-sdk - Shortcut.lnk | Bin 0 -> 1077 bytes .../room-list/previews/MessageEventPreview.ts | 2 +- src/utils/exportUtils/MessagePanel.tsx | 1343 +++++++++++++++++ 10 files changed, 1963 insertions(+), 52 deletions(-) create mode 100644 src/components/views/room_settings/RoomEmoteSettings.tsx create mode 100644 src/components/views/settings/tabs/room/EmoteSettingsTab.tsx create mode 100644 src/matrix-react-sdk - Shortcut.lnk create mode 100644 src/utils/exportUtils/MessagePanel.tsx diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 849609acf93..77b505a7b4a 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -826,7 +826,9 @@ $left-gutter: 64px; font-size: inherit !important; } } - +.mx_Emote{ + height: 30px; +} .mx_EventTile_e2eIcon { position: relative; width: 14px; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 89e9d76629a..4a045f81534 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -438,6 +438,7 @@ interface IOpts { returnString?: boolean; forComposerQuote?: boolean; ref?: React.Ref; + emotes?: Dictionary; } export interface IOptsReturnNode extends IOpts { @@ -517,7 +518,6 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (opts.forComposerQuote) { sanitizeParams = composerSanitizeHtmlParams; } - let strippedBody: string; let safeBody: string | undefined; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext @@ -539,7 +539,6 @@ export function bodyToHtml(content: IContent, highlights: Optional, op const highlighter = safeHighlights?.length ? new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink) : null; - if (isFormattedBody) { if (highlighter) { // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying @@ -547,8 +546,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function (safeText) { - return highlighter.applyHighlights(safeText, safeHighlights!).join(""); + + sanitizeParams.textFilter = function(safeText) { + return highlighter.applyHighlights(safeText, safeHighlights).join('').replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; } @@ -574,6 +574,12 @@ export function bodyToHtml(content: IContent, highlights: Optional, op delete sanitizeParams.textFilter; } + let contentBody = safeBody ?? strippedBody; + contentBody=contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + if (opts.returnString) { + return contentBody; + } + let emojiBody = false; if (!opts.disableBigEmoji && bodyHasEmoji) { const contentBody = safeBody ?? strippedBody; @@ -610,13 +616,18 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "mx_EventTile_bigEmoji": emojiBody, "markdown-body": isHtmlMessage && !emojiBody, }); - - let emojiBodyElements: JSX.Element[] | undefined; + let tmp=strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + if(tmp!=strippedBody){ + safeBody=tmp; + } + + + let emojiBodyElements: JSX.Element[]; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } - - return safeBody ? ( + + return safeBody ? ; } interface IReadReceiptForUser { @@ -277,6 +283,7 @@ export default class MessagePanel extends React.Component { ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), hideSender: this.shouldHideSender(), + emotes: {}, }; // Cache these settings on mount since Settings is expensive to query, @@ -284,16 +291,22 @@ export default class MessagePanel extends React.Component { // RoomContext, however we still need a fallback for roomless MessagePanels. this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); - this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( - "showTypingNotifications", - null, - this.onShowTypingNotificationsChange, - ); + this.showTypingNotificationsWatcherRef = + SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + + let emotesEvent=this.props.room.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let finalEmotes = {}; + for (let key in rawEmotes) { + this.state.emotes[":"+key+":"] = ""; + } + } public componentDidMount(): void { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); + //this.props.room?.currentState.on(RoomStateEvent.Update, this.getEmotes); this.isMounted = true; } @@ -782,7 +795,47 @@ export default class MessagePanel extends React.Component { isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); + // use txnId as key if available so that we don't remount during sending + if(mxEv.getType()==="m.room.message"){ + let messageText = mxEv.getContent().body; + //console.log(messageText); + let editedMessageText = messageText.replace(/:[\w+-]+:/, m => this.state.emotes[m] ? this.state.emotes[m] : m); + let m=[{body:messageText, + mimetype:"text/plain", + }, + { + body:editedMessageText, + mimetype:"text/html", + } + ]; + // if(mxEv.clearEvent){ + // console.log("clearevent",mxEv.getRoomId()); + // mxEv.clearEvent.content={ + // "format":"org.matrix.custom.html", + // "formatted_body":editedMessageText, + // "body":messageText, + // "msgtype":"m.text", + // "org.matrix.msc1767.message":m + // } + // } + // else{ + // console.log("no clearevent",mxEv); + // mxEv.content={ + // "format":"org.matrix.custom.html", + // "formatted_body":editedMessageText, + // "body":messageText, + // "msgtype":"m.text", + // "org.matrix.msc1767.message":m + // } + // } + + //mxEv.getContent().formatted_body = messageText; + //mxEv.clearEvent.content["org.matrix.msc1767.text"] = ""; + //mxEv.getContent().formatted_body = (hi); + //mxEv.getContent().format = "org.matrix.custom.html"; + + } ret.push( { "RoomSettingsNotifications", ), ); - + tabs.push(new Tab( + ROOM_NOTIFICATIONS_TAB, + _td("Emotes"), + "mx_RoomSettingsDialog_emotesIcon", + , + "RoomSettingsNotifications", + )); if (SettingsStore.getValue("feature_bridge_state")) { tabs.push( new Tab( diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index b02c52a64a3..3761678d3aa 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -45,11 +45,9 @@ import { IBodyProps } from "./IBodyProps"; import RoomContext from "../../../contexts/RoomContext"; import AccessibleButton from "../elements/AccessibleButton"; import { options as linkifyOpts } from "../../../linkify-matrix"; -import { getParentEventId } from "../../../utils/Reply"; -import { EditWysiwygComposer } from "../rooms/wysiwyg_composer"; -import { IEventTileOps } from "../rooms/EventTile"; +import { getParentEventId } from '../../../utils/Reply'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; - +import { mediaFromMxc } from "../../../customisations/Media"; const MAX_HIGHLIGHT_LENGTH = 4096; interface IState { @@ -579,19 +577,53 @@ export default class TextualBody extends React.Component { const content = mxEvent.getContent(); let isNotice = false; let isEmote = false; - // only strip reply if this is the original replying event, edits thereafter do not have the fallback const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent); - isEmote = content.msgtype === MsgType.Emote; - isNotice = content.msgtype === MsgType.Notice; - let body = HtmlUtils.bodyToHtml(content, this.props.highlights, { - disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"), - // Part of Replies fallback support - stripReplyFallback: stripReply, - ref: this.contentRef, - returnString: false, - }); - + let body: ReactNode; + const client = MatrixClientPeg.get(); + const room = client.getRoom(mxEvent.getRoomId()); + let emotesEvent=room.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let finalEmotes = {}; + for (let key in rawEmotes) { + finalEmotes[":"+key+":"] = ""; + } + if (SettingsStore.isEnabled("feature_extensible_events")) { + const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; + if (extev?.isEquivalentTo(M_MESSAGE)) { + isEmote = isEventLike(extev.wireFormat, LegacyMsgType.Emote); + isNotice = isEventLike(extev.wireFormat, LegacyMsgType.Notice); + body = (HtmlUtils.bodyToHtml({ + body: extev.text, + format: extev.html ? "org.matrix.custom.html" : undefined, + formatted_body: extev.html, + msgtype: MsgType.Text, + }, this.props.highlights, { + disableBigEmoji: isEmote + || !SettingsStore.getValue('TextualBody.enableBigEmoji'), + // Part of Replies fallback support + stripReplyFallback: stripReply, + ref: this.contentRef, + returnString: false, + emotes: finalEmotes, + })); + } + } + if (!body) { + isEmote = content.msgtype === MsgType.Emote; + isNotice = content.msgtype === MsgType.Notice; + body = (HtmlUtils.bodyToHtml(content, this.props.highlights, { + disableBigEmoji: isEmote + || !SettingsStore.getValue('TextualBody.enableBigEmoji'), + // Part of Replies fallback support + stripReplyFallback: stripReply, + ref: this.contentRef, + returnString: false, + })as any).replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m); + + } + //console.log(body); + //body.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (this.props.replacingEventId) { body = ( <> @@ -634,6 +666,8 @@ export default class TextualBody extends React.Component { ); } + //console.log(body.props.children); + //.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (isEmote) { return (
    @@ -655,6 +689,7 @@ export default class TextualBody extends React.Component {
    ); } + //console.log(body) return (
    {body} diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx new file mode 100644 index 00000000000..60f5edbc268 --- /dev/null +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -0,0 +1,388 @@ +/* +Copyright 2019 New Vector Ltd + +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, { createRef } from 'react'; +import classNames from "classnames"; + +import { _t } from "../../../languageHandler"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import Field from "../elements/Field"; +import { mediaFromMxc } from "../../../customisations/Media"; +import AccessibleButton from "../elements/AccessibleButton"; +import AvatarSetting from "../settings/AvatarSetting"; +import { htmlSerializeFromMdIfNeeded } from '../../../editor/serialize'; +import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; +import { string } from 'prop-types'; + +interface IProps { + roomId: string; +} + +interface IState { + emotes: Dictionary; + EmoteFieldsTouched: Record; + newEmoteFileAdded: boolean, + newEmoteCodeAdded: boolean, + newEmoteCode: string, + newEmoteFile: File, + canAddEmote: boolean; + deleted: boolean; + deletedItems: Dictionary; + value: Dictionary; +} + +// TODO: Merge with EmoteSettings? +export default class RoomEmoteSettings extends React.Component { + private emoteUpload = createRef(); + private emoteCodeUpload = createRef(); + private emoteUploadImage = createRef(); + constructor(props: IProps) { + super(props); + + const client = MatrixClientPeg.get(); + const room = client.getRoom(props.roomId); + if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); + + let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + console.log(room.currentState); + let emotes: Dictionary; + emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let value = {}; + for (let emote in emotes) { + value[emote] = emote; + } + //TODO: Decrypt the shortcodes and emotes if they are encrypted + if (emotes) { + console.log(room.roomId); + console.log(room.name); + console.log(emotes); + } + //if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); + //emotes={} + this.state = { + emotes: emotes, + EmoteFieldsTouched: {}, + newEmoteFileAdded: false, + newEmoteCodeAdded: false, + newEmoteCode: "", + newEmoteFile: null, + deleted: false, + deletedItems: {}, + canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), + value: value, + }; + } + + private uploadEmoteClick = (): void => { + this.emoteUpload.current.click(); + }; + + + private isSaveEnabled = () => { + return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || this.state.deleted; + }; + + private cancelEmoteChanges = async (e: React.MouseEvent): Promise => { + e.stopPropagation(); + e.preventDefault(); + let value = {}; + if (this.state.deleted) { + for (let key in this.state.deletedItems) { + this.state.emotes[key] = this.state.deletedItems[key]; + value[key] = key; + } + } + document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { + value[(field as HTMLInputElement).id] = (field as HTMLInputElement).id; + }) + if (!this.isSaveEnabled()) return; + this.setState({ + EmoteFieldsTouched: {}, + newEmoteFileAdded: false, + newEmoteCodeAdded: false, + deleted: false, + deletedItems: {}, + value: value, + }); + + this.emoteUpload.current.value = ""; + this.emoteCodeUpload.current.value = ""; + + }; + private deleteEmote = (e: React.MouseEvent): Promise => { + e.stopPropagation(); + e.preventDefault(); + let cleanemotes = {} + let deletedItems = this.state.deletedItems; + let value = {} + //console.log(e.currentTarget.getAttribute("name")); + let id = e.currentTarget.getAttribute("id") + for (let emote in this.state.emotes) { + if (emote != id) { + cleanemotes[emote] = this.state.emotes[emote] + value[emote] = emote + } + else { + deletedItems[emote] = this.state.emotes[emote] + } + } + + this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }) + // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { + // field.setAttribute("value",(field as HTMLInputElement).id); + // field.setAttribute("defaultValue",(field as HTMLInputElement).id); + // }) + // for(let DOMid in this.state.emotes){ + // document.getElementById(DOMid).setAttribute("value",DOMid); + // } + return; + + } + private saveEmote = async (e: React.FormEvent): Promise => { + e.stopPropagation(); + e.preventDefault(); + + if (!this.isSaveEnabled()) return; + const client = MatrixClientPeg.get(); + const newState: Partial = {}; + const emotesMxcs = {}; + let value = {}; + // TODO: What do we do about errors? + + if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { + //const emotes = await client.uploadContent(this.state.emotes); + //TODO: Encrypt the shortcode and the image data before uploading + if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { + const newEmote = await client.uploadContent(this.state.newEmoteFile); + emotesMxcs[this.state.newEmoteCode] = newEmote; + value[this.state.newEmoteCode] = this.state.newEmoteCode; + } + if (this.state.emotes) { + for (let shortcode in this.state.emotes) { + if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) && (shortcode === this.state.newEmoteCode)) { + continue; + } + if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { + emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; + value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; + + // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { + // if((field as HTMLInputElement).name===shortcode){ + // (field as HTMLInputElement).name= this.state.EmoteFieldsTouched[shortcode]; + // } + // }) + } + + else { + emotesMxcs[shortcode] = this.state.emotes[shortcode]; + value[shortcode] = shortcode; + } + + }; + } + //console.log(emotesMxcs); + newState.value = value; + await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); + this.emoteUpload.current.value = ""; + this.emoteCodeUpload.current.value = ""; + newState.newEmoteFileAdded = false; + newState.newEmoteCodeAdded = false; + newState.EmoteFieldsTouched = {}; + newState.emotes = emotesMxcs; + newState.deleted = false; + newState.deletedItems = {}; + + /*newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); + newState.originalAvatarUrl = newState.avatarUrl; + newState.avatarFile = null;*/ + } /*else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { + await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, ''); + }*/ + this.setState(newState as IState); + }; + + + private onEmoteChange = (e: React.ChangeEvent): void => { + const id = e.target.getAttribute("id"); + //e.target.setAttribute("value", e.target.value); + //const newEmotes = { ...this.state.emotes, [value]: value }; + //let newState=this.state.emotes; + let b = this.state.value + b[id] = e.target.value; + this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); + } + + private onEmoteFileAdd = (e: React.ChangeEvent): void => { + if (!e.target.files || !e.target.files.length) { + this.setState({ + newEmoteFileAdded: false, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); + return; + } + + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = (ev) => { + this.setState({ + newEmoteFileAdded: true, + newEmoteFile: file, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); + this.emoteUploadImage.current.src = URL.createObjectURL(file); + }; + reader.readAsDataURL(file); + }; + private onEmoteCodeAdd = (e: React.ChangeEvent): void => { + if (e.target.value.length > 0) { + this.setState({ + newEmoteCodeAdded: true, + newEmoteCode: e.target.value, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); + } + else { + this.setState({ + newEmoteCodeAdded: false, + }); + } + } + + public render(): JSX.Element { + let emoteSettingsButtons; + if ( + this.state.canAddEmote + ) { + emoteSettingsButtons = ( +
    + + {_t("Cancel")} + + + {_t("Save")} + +
    + ); + } + + let existingEmotes = []; + if (this.state.emotes) { + for (let emotecode in this.state.emotes) { + existingEmotes.push( +
  • + + +
    + + {_t("Delete")} + +
    +
  • + ) + } + + + } + + + let emoteUploadButton; + if (this.state.canAddEmote) { + emoteUploadButton = ( +
    + + {_t("Upload Emote")} + +
    + ); + } + + + return ( + + + +
  • + + { + this.state.newEmoteFileAdded ? + : null + } + + {emoteUploadButton} +
  • + { + existingEmotes + } + {emoteSettingsButtons} + + ); + } +} diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx new file mode 100644 index 00000000000..864fff3171e --- /dev/null +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -0,0 +1,75 @@ +/* +Copyright 2019 New Vector Ltd + +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, { ContextType } from 'react'; + +import { _t } from "../../../../../languageHandler"; +import RoomEmoteSettings from "../../../room_settings/RoomEmoteSettings"; +import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; +import dis from "../../../../../dispatcher/dispatcher"; +import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import { UIFeature } from "../../../../../settings/UIFeature"; +import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings"; +import AliasSettings from "../../../room_settings/AliasSettings"; +import PosthogTrackers from "../../../../../PosthogTrackers"; + +interface IProps { + roomId: string; +} + +interface IState { + isRoomPublished: boolean; +} + +export default class EmoteRoomSettingsTab extends React.Component { + public static contextType = MatrixClientContext; + context: ContextType; + + constructor(props: IProps, context: ContextType) { + super(props, context); + + this.state = { + isRoomPublished: false, // loaded async + }; + } + + + public render(): JSX.Element { + const client = this.context; + const room = client.getRoom(this.props.roomId); + + /*const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this + const canSetCanonical = room.currentState.mayClientSendStateEvent("m.room.canonical_alias", client); + const canonicalAliasEv = room.currentState.getStateEvents("m.room.canonical_alias", '');*/ + + const urlPreviewSettings = SettingsStore.getValue(UIFeature.URLPreviews) ? + : + null; + + + return ( +
    + +
    {_t("Emotes")}
    +
    + +
    + +
    + ); + } +} diff --git a/src/matrix-react-sdk - Shortcut.lnk b/src/matrix-react-sdk - Shortcut.lnk new file mode 100644 index 0000000000000000000000000000000000000000..cb57c92d572caad0631ea61e6b0fc0396a6dc407 GIT binary patch literal 1077 zcmb7CT}V@57=F$Yn&rh-CM7yYFfqo)41*d7!N%rAOi3j#&V^$;v(wy9=bVXn5e7x9 z>>^VlqR6nMG9sd)i)7ecS3@b*D2SjNL&Teqg6jDu6Z)a$dw9P0J@5I>^S0cKbj*MD6K&)qFZkK?DubF&3$Vr#MI$+P|2AKXG@dzCkj&p-F79C! zYuiDas8)+4S~s{YPzIHRk~E^qkZxpRF?69CRpcmdahHR?dIf_}kRXyU5Tzf2&M1Qz zAHW9fmuiWyp{c_zO$kQ~mmZA+M>kb3fiT%Ms2GNeUg3L$85gTYLAsKN$LSjgp``FG zLf8NywAn&5)cp7&rU9Qv%fBfj|Jl0++H!A1*5 z6KYb`4J~Y>G``P|TEF##9KNg1PwseKbevy!H}L+-u4U^)Y%RM`JeRBV{S%K-P5!gw zYVGdoUiQ2w`j8FItmSu3>#@Bm^HBm#gp>I9Q0!=d4_?UVrDzI8)ES2;X_{k8GWH`y zQ8OE3)v-^|mu!TqXu`AVc(xL(x7=Pe^^e{7JbQroZDNOLLTFrhQYg_@m4B*X5MUSL zLfLtFQjElf5)ulVB1GjoCn7EFlMB`-Z; CONTINUATION_MAX_INTERVAL) return false; + + // As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa + if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false; + + // Some events should appear as continuations from previous events of different types. + if (mxEvent.getType() !== prevEvent.getType() && + (!continuedTypes.includes(mxEvent.getType() as EventType) || + !continuedTypes.includes(prevEvent.getType() as EventType))) return false; + + // Check if the sender is the same and hasn't changed their displayname/avatar between these events + if (mxEvent.sender.userId !== prevEvent.sender.userId || + mxEvent.sender.name !== prevEvent.sender.name || + mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; + + // Thread summaries in the main timeline should break up a continuation on both sides + if (threadsEnabled && + (hasThreadSummary(mxEvent) || hasThreadSummary(prevEvent)) && + timelineRenderingType !== TimelineRenderingType.Thread + ) { + return false; + } + + // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile + if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false; + + return true; +} + +interface IProps { + // the list of MatrixEvents to display + events: MatrixEvent[]; + + // true to give the component a 'display: none' style. + hidden?: boolean; + + // true to show a spinner at the top of the timeline to indicate + // back-pagination in progress + backPaginating?: boolean; + + // true to show a spinner at the end of the timeline to indicate + // forward-pagination in progress + forwardPaginating?: boolean; + + // ID of an event to highlight. If undefined, no event will be highlighted. + highlightedEventId?: string; + + // The room these events are all in together, if any. + // (The notification panel won't have a room here, for example.) + room?: Room; + + // Should we show URL Previews + showUrlPreview?: boolean; + + // event after which we should show a read marker + readMarkerEventId?: string; + + // whether the read marker should be visible + readMarkerVisible?: boolean; + + // the userid of our user. This is used to suppress the read marker + // for pending messages. + ourUserId?: string; + + // whether the timeline can visually go back any further + canBackPaginate?: boolean; + + // whether to show read receipts + showReadReceipts?: boolean; + + // true if updates to the event list should cause the scroll panel to + // scroll down when we are at the bottom of the window. See ScrollPanel + // for more details. + stickyBottom?: boolean; + + // className for the panel + className: string; + + // show twelve hour timestamps + isTwelveHour?: boolean; + + // show timestamps always + alwaysShowTimestamps?: boolean; + + // whether to show reactions for an event + showReactions?: boolean; + + // which layout to use + layout?: Layout; + + resizeNotifier: ResizeNotifier; + permalinkCreator?: RoomPermalinkCreator; + editState?: EditorStateTransfer; + + // callback which is called when the panel is scrolled. + onScroll?(event: Event): void; + + // callback which is called when more content is needed. + onFillRequest?(backwards: boolean): Promise; + + // helper function to access relations for an event + onUnfillRequest?(backwards: boolean, scrollToken: string): void; + + getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations; + + hideThreadedMessages?: boolean; + disableGrouping?: boolean; + + callEventGroupers: Map; +} + +interface IState { + ghostReadMarkers: string[]; + showTypingNotifications: boolean; + hideSender: boolean; +} + +interface IReadReceiptForUser { + lastShownEventId: string; + receipt: IReadReceiptProps; +} + +/* (almost) stateless UI component which builds the event tiles in the room timeline. + */ +export default class MessagePanel extends React.Component { + static contextType = RoomContext; + public context!: React.ContextType; + + static defaultProps = { + disableGrouping: false, + }; + + // opaque readreceipt info for each userId; used by ReadReceiptMarker + // to manage its animations + private readonly readReceiptMap: { [userId: string]: IReadReceiptInfo } = {}; + + // Track read receipts by event ID. For each _shown_ event ID, we store + // the list of read receipts to display: + // [ + // { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // ] + // This is recomputed on each render. It's only stored on the component + // for ease of passing the data around since it's computed in one pass + // over all events. + private readReceiptsByEvent: Record = {}; + + // Track read receipts by user ID. For each user ID we've ever shown a + // a read receipt for, we store an object: + // { + // lastShownEventId: string, + // receipt: { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // } + // so that we can always keep receipts displayed by reverting back to + // the last shown event for that user ID when needed. This may feel like + // it duplicates the receipt storage in the room, but at this layer, we + // are tracking _shown_ event IDs, which the JS SDK knows nothing about. + // This is recomputed on each render, using the data from the previous + // render as our fallback for any user IDs we can't match a receipt to a + // displayed event in the current render cycle. + private readReceiptsByUserId: Record = {}; + + private readonly _showHiddenEvents: boolean; + private readonly threadsEnabled: boolean; + private isMounted = false; + + private readMarkerNode = createRef(); + private whoIsTyping = createRef(); + private scrollPanel = createRef(); + + private readonly showTypingNotificationsWatcherRef: string; + private eventTiles: Record = {}; + + // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. + public grouperKeyMap = new WeakMap(); + + constructor(props, context) { + super(props, context); + + this.state = { + // previous positions the read marker has been in, so we can + // display 'ghost' read markers that are animating away + ghostReadMarkers: [], + showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), + hideSender: this.shouldHideSender(), + }; + + // Cache these settings on mount since Settings is expensive to query, + // and we check this in a hot code path. This is also cached in our + // RoomContext, however we still need a fallback for roomless MessagePanels. + this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); + this.threadsEnabled = SettingsStore.getValue("feature_thread"); + + this.showTypingNotificationsWatcherRef = + SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + } + + componentDidMount() { + this.calculateRoomMembersCount(); + this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); + this.isMounted = true; + } + + componentWillUnmount() { + this.isMounted = false; + this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); + SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.layout !== this.props.layout) { + this.calculateRoomMembersCount(); + } + + if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { + const ghostReadMarkers = this.state.ghostReadMarkers; + ghostReadMarkers.push(prevProps.readMarkerEventId); + this.setState({ + ghostReadMarkers, + }); + } + + const pendingEditItem = this.pendingEditItem; + if (!this.props.editState && this.props.room && pendingEditItem) { + const event = this.props.room.findEventById(pendingEditItem); + defaultDispatcher.dispatch({ + action: Action.EditEvent, + event: !event?.isRedacted() ? event : null, + timelineRenderingType: this.context.timelineRenderingType, + }); + } + } + + private shouldHideSender(): boolean { + return this.props.room?.getInvitedAndJoinedMemberCount() <= 2 && this.props.layout === Layout.Bubble; + } + + private calculateRoomMembersCount = (): void => { + this.setState({ + hideSender: this.shouldHideSender(), + }); + }; + + private onShowTypingNotificationsChange = (): void => { + this.setState({ + showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), + }); + }; + + /* get the DOM node representing the given event */ + public getNodeForEventId(eventId: string): HTMLElement { + if (!this.eventTiles) { + return undefined; + } + + return this.eventTiles[eventId]?.ref?.current; + } + + public getTileForEventId(eventId: string): UnwrappedEventTile { + if (!this.eventTiles) { + return undefined; + } + return this.eventTiles[eventId]; + } + + /* return true if the content is fully scrolled down right now; else false. + */ + public isAtBottom(): boolean { + return this.scrollPanel.current?.isAtBottom(); + } + + /* get the current scroll state. See ScrollPanel.getScrollState for + * details. + * + * returns null if we are not mounted. + */ + public getScrollState(): IScrollState { + return this.scrollPanel.current?.getScrollState() ?? null; + } + + // returns one of: + // + // null: there is no read marker + // -1: read marker is above the window + // 0: read marker is within the window + // +1: read marker is below the window + public getReadMarkerPosition(): number { + const readMarker = this.readMarkerNode.current; + const messageWrapper = this.scrollPanel.current; + + if (!readMarker || !messageWrapper) { + return null; + } + + const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect(); + const readMarkerRect = readMarker.getBoundingClientRect(); + + // the read-marker pretends to have zero height when it is actually + // two pixels high; +2 here to account for that. + if (readMarkerRect.bottom + 2 < wrapperRect.top) { + return -1; + } else if (readMarkerRect.top < wrapperRect.bottom) { + return 0; + } else { + return 1; + } + } + + /* jump to the top of the content. + */ + public scrollToTop(): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToTop(); + } + } + + /* jump to the bottom of the content. + */ + public scrollToBottom(): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToBottom(); + } + } + + /** + * Page up/down. + * + * @param {number} mult: -1 to page up, +1 to page down + */ + public scrollRelative(mult: number): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollRelative(mult); + } + } + + /** + * Scroll up/down in response to a scroll key + * + * @param {KeyboardEvent} ev: the keyboard event to handle + */ + public handleScrollKey(ev: KeyboardEvent): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.handleScrollKey(ev); + } + } + + /* jump to the given event id. + * + * offsetBase gives the reference point for the pixelOffset. 0 means the + * top of the container, 1 means the bottom, and fractional values mean + * somewhere in the middle. If omitted, it defaults to 0. + * + * pixelOffset gives the number of pixels *above* the offsetBase that the + * node (specifically, the bottom of it) will be positioned. If omitted, it + * defaults to 0. + */ + public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); + } + } + + public scrollToEventIfNeeded(eventId: string): void { + const node = this.getNodeForEventId(eventId); + if (node) { + node.scrollIntoView({ + block: "nearest", + behavior: "instant", + }); + } + } + + private isUnmounting = (): boolean => { + return !this.isMounted; + }; + + public get showHiddenEvents(): boolean { + return this.context?.showHiddenEvents ?? this._showHiddenEvents; + } + + // TODO: Implement granular (per-room) hide options + public shouldShowEvent(mxEv: MatrixEvent, forceHideEvents = false): boolean { + if (this.props.hideThreadedMessages && this.threadsEnabled && this.props.room) { + const { shouldLiveInRoom } = this.props.room.eventShouldLiveIn(mxEv, this.props.events); + if (!shouldLiveInRoom) { + return false; + } + } + + if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) { + return false; // ignored = no show (only happens if the ignore happens after an event was received) + } + + if (this.showHiddenEvents && !forceHideEvents) { + return true; + } + + if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) { + return false; // no tile = no show + } + + // Always show highlighted event + if (this.props.highlightedEventId === mxEv.getId()) return true; + + return !shouldHideEvent(mxEv, this.context); + } + + public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode { + const visible = !isLastEvent && this.props.readMarkerVisible; + + if (this.props.readMarkerEventId === eventId) { + let hr; + // if the read marker comes at the end of the timeline (except + // for local echoes, which are excluded from RMs, because they + // don't have useful event ids), we don't want to show it, but + // we still want to create the
  • for it so that the + // algorithms which depend on its position on the screen aren't + // confused. + if (visible) { + hr =
    ; + } + + return ( +
  • + { hr } +
  • + ); + } else if (this.state.ghostReadMarkers.includes(eventId)) { + // We render 'ghost' read markers in the DOM while they + // transition away. This allows the actual read marker + // to be in the right place straight away without having + // to wait for the transition to finish. + // There are probably much simpler ways to do this transition, + // possibly using react-transition-group which handles keeping + // elements in the DOM whilst they transition out, although our + // case is a little more complex because only some of the items + // transition (ie. the read markers do but the event tiles do not) + // and TransitionGroup requires that all its children are Transitions. + const hr =
    ; + + // give it a key which depends on the event id. That will ensure that + // we get a new DOM node (restarting the animation) when the ghost + // moves to a different event. + return ( +
  • + { hr } +
  • + ); + } + + return null; + } + + private collectGhostReadMarker = (node: HTMLElement): void => { + if (node) { + // now the element has appeared, change the style which will trigger the CSS transition + requestAnimationFrame(() => { + node.style.width = '10%'; + node.style.opacity = '0'; + }); + } + }; + + private onGhostTransitionEnd = (ev: TransitionEvent): void => { + // we can now clean up the ghost element + const finishedEventId = (ev.target as HTMLElement).dataset.eventid; + this.setState({ + ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId), + }); + }; + + private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } { + const nextEvent = i < arr.length - 1 + ? arr[i + 1] + : null; + + // The next event with tile is used to to determine the 'last successful' flag + // when rendering the tile. The shouldShowEvent function is pretty quick at what + // it does, so this should have no significant cost even when a room is used for + // not-chat purposes. + const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e)); + + return { nextEvent, nextTile }; + } + + private get pendingEditItem(): string | undefined { + if (!this.props.room) { + return undefined; + } + + try { + return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType)); + } catch (err) { + logger.error(err); + return undefined; + } + } + + private getEventTiles(): ReactNode[] { + let i; + + // first figure out which is the last event in the list which we're + // actually going to show; this allows us to behave slightly + // differently for the last event in the list. (eg show timestamp) + // + // we also need to figure out which is the last event we show which isn't + // a local echo, to manage the read-marker. + let lastShownEvent; + + let lastShownNonLocalEchoIndex = -1; + for (i = this.props.events.length-1; i >= 0; i--) { + const mxEv = this.props.events[i]; + if (!this.shouldShowEvent(mxEv)) { + continue; + } + + if (lastShownEvent === undefined) { + lastShownEvent = mxEv; + } + + if (mxEv.status) { + // this is a local echo + continue; + } + + lastShownNonLocalEchoIndex = i; + break; + } + + const ret = []; + + let prevEvent = null; // the last event we showed + + // Note: the EventTile might still render a "sent/sending receipt" independent of + // this information. When not providing read receipt information, the tile is likely + // to assume that sent receipts are to be shown more often. + this.readReceiptsByEvent = {}; + if (this.props.showReadReceipts) { + this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(); + } + + let grouper: BaseGrouper = null; + + for (i = 0; i < this.props.events.length; i++) { + const mxEv = this.props.events[i]; + const eventId = mxEv.getId(); + const last = (mxEv === lastShownEvent); + const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i); + + if (grouper) { + if (grouper.shouldGroup(mxEv)) { + grouper.add(mxEv); + continue; + } else { + // not part of group, so get the group tiles, close the + // group, and continue like a normal event + ret.push(...grouper.getTiles()); + prevEvent = grouper.getNewPrevEvent(); + grouper = null; + } + } + + for (const Grouper of groupers) { + if (Grouper.canStartGroup(this, mxEv) && !this.props.disableGrouping) { + grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); + break; // break on first grouper + } + } + + if (!grouper) { + if (this.shouldShowEvent(mxEv)) { + // make sure we unpack the array returned by getTilesForEvent, + // otherwise React will auto-generate keys, and we will end up + // replacing all the DOM elements every time we paginate. + ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, false, nextEvent, nextTile)); + prevEvent = mxEv; + } + + const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); + if (readMarker) ret.push(readMarker); + } + } + + if (grouper) { + ret.push(...grouper.getTiles()); + } + + return ret; + } + + public getTilesForEvent( + prevEvent: MatrixEvent, + mxEv: MatrixEvent, + last = false, + isGrouped = false, + nextEvent?: MatrixEvent, + nextEventWithTile?: MatrixEvent, + ): ReactNode[] { + const ret = []; + + const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId(); + // local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators. + let ts1 = mxEv.getTs(); + let eventDate = mxEv.getDate(); + if (mxEv.status) { + eventDate = new Date(); + ts1 = eventDate.getTime(); + } + + // do we need a date separator since the last event? + const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); + if (wantsDateSeparator && !isGrouped && this.props.room) { + const dateSeparator = ( +
  • + +
  • + ); + ret.push(dateSeparator); + } + + let lastInSection = true; + if (nextEventWithTile) { + const nextEv = nextEventWithTile; + const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date()); + lastInSection = willWantDateSeparator || + mxEv.getSender() !== nextEv.getSender() || + getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage || + !shouldFormContinuation( + mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, + ); + } + + // is this a continuation of the previous message? + const continuation = !wantsDateSeparator && + shouldFormContinuation( + prevEvent, mxEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, + ); + + const eventId = mxEv.getId(); + const highlight = (eventId === this.props.highlightedEventId); + + const readReceipts = this.readReceiptsByEvent[eventId]; + + let isLastSuccessful = false; + const isSentState = s => !s || s === 'sent'; + const isSent = isSentState(mxEv.getAssociatedStatus()); + const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); + if (!hasNextEvent && isSent) { + isLastSuccessful = true; + } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { + isLastSuccessful = true; + } + + // This is a bit nuanced, but if our next event is hidden but a future event is not + // hidden then we're not the last successful. + if ( + nextEventWithTile && + nextEventWithTile !== nextEvent && + isSentState(nextEventWithTile.getAssociatedStatus()) + ) { + isLastSuccessful = false; + } + + // We only want to consider "last successful" if the event is sent by us, otherwise of course + // it's successful: we received it. + isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); + + const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); + // use txnId as key if available so that we don't remount during sending + ret.push( + , + ); + + return ret; + } + + public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean { + if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { + return false; + } + if (prevEvent == null) { + // first event in the panel: depends if we could back-paginate from + // here. + return !this.props.canBackPaginate; + } + return wantsDateSeparator(prevEvent.getDate(), nextEventDate); + } + + // Get a list of read receipts that should be shown next to this event + // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. + private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] { + const myUserId = MatrixClientPeg.get().credentials.userId; + + // get list of read receipts, sorted most recent first + const { room } = this.props; + if (!room) { + return null; + } + const receipts: IReadReceiptProps[] = []; + room.getReceiptsForEvent(event).forEach((r) => { + if ( + !r.userId || + !isSupportedReceiptType(r.type) || + r.userId === myUserId + ) { + return; // ignore non-read receipts and receipts from self. + } + if (MatrixClientPeg.get().isUserIgnored(r.userId)) { + return; // ignore ignored users + } + const member = room.getMember(r.userId); + receipts.push({ + userId: r.userId, + roomMember: member, + ts: r.data ? r.data.ts : 0, + }); + }); + return receipts; + } + + // Get an object that maps from event ID to a list of read receipts that + // should be shown next to that event. If a hidden event has read receipts, + // they are folded into the receipts of the last shown event. + private getReadReceiptsByShownEvent(): Record { + const receiptsByEvent = {}; + const receiptsByUserId = {}; + + let lastShownEventId; + for (const event of this.props.events) { + if (this.shouldShowEvent(event)) { + lastShownEventId = event.getId(); + } + if (!lastShownEventId) { + continue; + } + + const existingReceipts = receiptsByEvent[lastShownEventId] || []; + const newReceipts = this.getReadReceiptsForEvent(event); + receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); + + // Record these receipts along with their last shown event ID for + // each associated user ID. + for (const receipt of newReceipts) { + receiptsByUserId[receipt.userId] = { + lastShownEventId, + receipt, + }; + } + } + + // It's possible in some cases (for example, when a read receipt + // advances before we have paginated in the new event that it's marking + // received) that we can temporarily not have a matching event for + // someone which had one in the last. By looking through our previous + // mapping of receipts by user ID, we can cover recover any receipts + // that would have been lost by using the same event ID from last time. + for (const userId in this.readReceiptsByUserId) { + if (receiptsByUserId[userId]) { + continue; + } + const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId]; + const existingReceipts = receiptsByEvent[lastShownEventId] || []; + receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); + receiptsByUserId[userId] = { lastShownEventId, receipt }; + } + this.readReceiptsByUserId = receiptsByUserId; + + // After grouping receipts by shown events, do another pass to sort each + // receipt list. + for (const eventId in receiptsByEvent) { + receiptsByEvent[eventId].sort((r1, r2) => { + return r2.ts - r1.ts; + }); + } + + return receiptsByEvent; + } + + private collectEventTile = (eventId: string, node: UnwrappedEventTile): void => { + this.eventTiles[eventId] = node; + }; + + // once dynamic content in the events load, make the scrollPanel check the + // scroll offsets. + public onHeightChanged = (): void => { + const scrollPanel = this.scrollPanel.current; + if (scrollPanel) { + scrollPanel.checkScroll(); + } + }; + + private onTypingShown = (): void => { + const scrollPanel = this.scrollPanel.current; + // this will make the timeline grow, so checkScroll + scrollPanel.checkScroll(); + if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { + scrollPanel.preventShrinking(); + } + }; + + private onTypingHidden = (): void => { + const scrollPanel = this.scrollPanel.current; + if (scrollPanel) { + // as hiding the typing notifications doesn't + // update the scrollPanel, we tell it to apply + // the shrinking prevention once the typing notifs are hidden + scrollPanel.updatePreventShrinking(); + // order is important here as checkScroll will scroll down to + // reveal added padding to balance the notifs disappearing. + scrollPanel.checkScroll(); + } + }; + + public updateTimelineMinHeight(): void { + const scrollPanel = this.scrollPanel.current; + + if (scrollPanel) { + const isAtBottom = scrollPanel.isAtBottom(); + const whoIsTyping = this.whoIsTyping.current; + const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); + // when messages get added to the timeline, + // but somebody else is still typing, + // update the min-height, so once the last + // person stops typing, no jumping occurs + if (isAtBottom && isTypingVisible) { + scrollPanel.preventShrinking(); + } + } + } + + public onTimelineReset(): void { + const scrollPanel = this.scrollPanel.current; + if (scrollPanel) { + scrollPanel.clearPreventShrinking(); + } + } + + render() { + let topSpinner; + let bottomSpinner; + if (this.props.backPaginating) { + topSpinner =
  • ; + } + if (this.props.forwardPaginating) { + bottomSpinner =
  • ; + } + + const style = this.props.hidden ? { display: 'none' } : {}; + + let whoIsTyping; + if (this.props.room && + this.state.showTypingNotifications && + this.context.timelineRenderingType === TimelineRenderingType.Room + ) { + whoIsTyping = ( + ); + } + + let ircResizer = null; + if (this.props.layout == Layout.IRC) { + ircResizer = ; + } + + const classes = classNames(this.props.className, { + "mx_MessagePanel_narrow": this.context.narrow, + }); + + return ( + + + { topSpinner } + { this.getEventTiles() } + { whoIsTyping } + { bottomSpinner } + + + ); + } +} + +abstract class BaseGrouper { + static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true; + + public events: MatrixEvent[] = []; + // events that we include in the group but then eject out and place above the group. + public ejectedEvents: MatrixEvent[] = []; + public readMarker: ReactNode; + + constructor( + public readonly panel: MessagePanel, + public readonly event: MatrixEvent, + public readonly prevEvent: MatrixEvent, + public readonly lastShownEvent: MatrixEvent, + public readonly nextEvent?: MatrixEvent, + public readonly nextEventTile?: MatrixEvent, + ) { + this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent); + } + + public abstract shouldGroup(ev: MatrixEvent): boolean; + public abstract add(ev: MatrixEvent): void; + public abstract getTiles(): ReactNode[]; + public abstract getNewPrevEvent(): MatrixEvent; +} + +/* Grouper classes determine when events can be grouped together in a summary. + * Groupers should have the following methods: + * - canStartGroup (static): determines if a new group should be started with the + * given event + * - shouldGroup: determines if the given event should be added to an existing group + * - add: adds an event to an existing group (should only be called if shouldGroup + * return true) + * - getTiles: returns the tiles that represent the group + * - getNewPrevEvent: returns the event that should be used as the new prevEvent + * when determining things such as whether a date separator is necessary + */ + +// Wrap initial room creation events into a GenericEventListSummary +// Grouping only events sent by the same user that sent the `m.room.create` and only until +// the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event +class CreationGrouper extends BaseGrouper { + static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { + return ev.getType() === EventType.RoomCreate; + }; + + public shouldGroup(ev: MatrixEvent): boolean { + const panel = this.panel; + const createEvent = this.event; + if (!panel.shouldShowEvent(ev)) { + return true; + } + if (panel.wantsDateSeparator(this.event, ev.getDate())) { + return false; + } + if (ev.getType() === EventType.RoomMember + && (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) { + return false; + } + // beacons are not part of room creation configuration + // should be shown in timeline + if (M_BEACON_INFO.matches(ev.getType())) { + return false; + } + if (ev.isState() && ev.getSender() === createEvent.getSender()) { + return true; + } + + return false; + } + + public add(ev: MatrixEvent): void { + const panel = this.panel; + this.readMarker = this.readMarker || panel.readMarkerForEvent( + ev.getId(), + ev === this.lastShownEvent, + ); + if (!panel.shouldShowEvent(ev)) { + return; + } + if (ev.getType() === EventType.RoomEncryption) { + this.ejectedEvents.push(ev); + } else { + this.events.push(ev); + } + } + + public getTiles(): ReactNode[] { + // If we don't have any events to group, don't even try to group them. The logic + // below assumes that we have a group of events to deal with, but we might not if + // the events we were supposed to group were redacted. + if (!this.events || !this.events.length) return []; + + const panel = this.panel; + const ret: ReactNode[] = []; + const isGrouped = true; + const createEvent = this.event; + const lastShownEvent = this.lastShownEvent; + + if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) { + const ts = createEvent.getTs(); + ret.push( +
  • , + ); + } + + // If this m.room.create event should be shown (room upgrade) then show it before the summary + if (panel.shouldShowEvent(createEvent)) { + // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered + ret.push(...panel.getTilesForEvent(createEvent, createEvent)); + } + + for (const ejected of this.ejectedEvents) { + ret.push(...panel.getTilesForEvent( + createEvent, ejected, createEvent === lastShownEvent, isGrouped, + )); + } + + const eventTiles = this.events.map((e) => { + // In order to prevent DateSeparators from appearing in the expanded form + // of GenericEventListSummary, render each member event as if the previous + // one was itself. This way, the timestamp of the previous event === the + // timestamp of the current event, and no DateSeparator is inserted. + return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); + }).reduce((a, b) => a.concat(b), []); + // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one + const ev = this.events[this.events.length - 1]; + + let summaryText: string; + const roomId = ev.getRoomId(); + const creator = ev.sender ? ev.sender.name : ev.getSender(); + if (DMRoomMap.shared().getUserIdForRoomId(roomId)) { + summaryText = _t("%(creator)s created this DM.", { creator }); + } else { + summaryText = _t("%(creator)s created and configured the room.", { creator }); + } + + ret.push(); + + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + public getNewPrevEvent(): MatrixEvent { + return this.event; + } +} + +// Wrap consecutive grouped events in a ListSummary +class MainGrouper extends BaseGrouper { + static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { + if (!panel.shouldShowEvent(ev)) return false; + + if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { + return true; + } + + if (ev.isRedacted()) { + return true; + } + + if (panel.showHiddenEvents && !panel.shouldShowEvent(ev, true)) { + return true; + } + + return false; + }; + + constructor( + public readonly panel: MessagePanel, + public readonly event: MatrixEvent, + public readonly prevEvent: MatrixEvent, + public readonly lastShownEvent: MatrixEvent, + nextEvent: MatrixEvent, + nextEventTile: MatrixEvent, + ) { + super(panel, event, prevEvent, lastShownEvent, nextEvent, nextEventTile); + this.events = [event]; + } + + public shouldGroup(ev: MatrixEvent): boolean { + if (!this.panel.shouldShowEvent(ev)) { + // absorb hidden events so that they do not break up streams of messages & redaction events being grouped + return true; + } + if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { + return false; + } + if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { + return true; + } + if (ev.isRedacted()) { + return true; + } + if (this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev, true)) { + return true; + } + return false; + } + + public add(ev: MatrixEvent): void { + if (ev.getType() === EventType.RoomMember) { + // We can ignore any events that don't actually have a message to display + if (!hasText(ev, this.panel.showHiddenEvents)) return; + } + this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent); + if (!this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev)) { + // absorb hidden events to not split the summary + return; + } + this.events.push(ev); + } + + private generateKey(): string { + return "eventlistsummary-" + this.events[0].getId(); + } + + public getTiles(): ReactNode[] { + // If we don't have any events to group, don't even try to group them. The logic + // below assumes that we have a group of events to deal with, but we might not if + // the events we were supposed to group were redacted. + if (!this.events?.length) return []; + + const isGrouped = true; + const panel = this.panel; + const lastShownEvent = this.lastShownEvent; + const ret: ReactNode[] = []; + + if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { + const ts = this.events[0].getTs(); + ret.push( +
  • , + ); + } + + // Ensure that the key of the EventListSummary does not change with new events in either direction. + // This will prevent it from being re-created unnecessarily, and instead will allow new props to be provided. + // In turn, the shouldComponentUpdate method on ELS can be used to prevent unnecessary renderings. + const keyEvent = this.events.find(e => this.panel.grouperKeyMap.get(e)); + const key = keyEvent ? this.panel.grouperKeyMap.get(keyEvent) : this.generateKey(); + if (!keyEvent) { + // Populate the weak map with the key. + // Note that we only set the key on the specific event it refers to, since this group might get + // split up in the future by other intervening events. If we were to set the key on all events + // currently in the group, we would risk later giving the same key to multiple groups. + this.panel.grouperKeyMap.set(this.events[0], key); + } + + let highlightInSummary = false; + let eventTiles = this.events.map((e, i) => { + if (e.getId() === panel.props.highlightedEventId) { + highlightInSummary = true; + } + return panel.getTilesForEvent( + i === 0 ? this.prevEvent : this.events[i - 1], + e, + e === lastShownEvent, + isGrouped, + this.nextEvent, + this.nextEventTile, + ); + }).reduce((a, b) => a.concat(b), []); + + if (eventTiles.length === 0) { + eventTiles = null; + } + + // If a membership event is the start of visible history, tell the user + // why they can't see earlier messages + if (!this.panel.props.canBackPaginate && !this.prevEvent) { + ret.push(); + } + + ret.push( + + { eventTiles } + , + ); + + if (this.readMarker) { + ret.push(this.readMarker); + } + + return ret; + } + + public getNewPrevEvent(): MatrixEvent { + return this.events[this.events.length - 1]; + } +} + +// all the grouper classes that we use, ordered by priority +const groupers = [CreationGrouper, MainGrouper]; From 130fddaea7c2992f88b877ac437a4dce41edc1d2 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Wed, 10 Aug 2022 15:17:57 -0400 Subject: [PATCH 049/176] emotes show up in messages --- src/components/structures/MessagePanel.tsx | 47 +--------------------- 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 4883385787c..1c361be94b4 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -18,7 +18,7 @@ import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'rea import ReactDOM from 'react-dom'; import classNames from 'classnames'; import { Room } from 'matrix-js-sdk/src/models/room'; -import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; +import { EventType} from 'matrix-js-sdk/src/@types/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { Relations } from "matrix-js-sdk/src/models/relations"; import { logger } from 'matrix-js-sdk/src/logger'; @@ -294,12 +294,6 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - let emotesEvent=this.props.room.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let finalEmotes = {}; - for (let key in rawEmotes) { - this.state.emotes[":"+key+":"] = ""; - } } @@ -797,45 +791,6 @@ export default class MessagePanel extends React.Component { const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); // use txnId as key if available so that we don't remount during sending - if(mxEv.getType()==="m.room.message"){ - let messageText = mxEv.getContent().body; - //console.log(messageText); - let editedMessageText = messageText.replace(/:[\w+-]+:/, m => this.state.emotes[m] ? this.state.emotes[m] : m); - let m=[{body:messageText, - mimetype:"text/plain", - }, - { - body:editedMessageText, - mimetype:"text/html", - } - ]; - // if(mxEv.clearEvent){ - // console.log("clearevent",mxEv.getRoomId()); - // mxEv.clearEvent.content={ - // "format":"org.matrix.custom.html", - // "formatted_body":editedMessageText, - // "body":messageText, - // "msgtype":"m.text", - // "org.matrix.msc1767.message":m - // } - // } - // else{ - // console.log("no clearevent",mxEv); - // mxEv.content={ - // "format":"org.matrix.custom.html", - // "formatted_body":editedMessageText, - // "body":messageText, - // "msgtype":"m.text", - // "org.matrix.msc1767.message":m - // } - // } - - //mxEv.getContent().formatted_body = messageText; - //mxEv.clearEvent.content["org.matrix.msc1767.text"] = ""; - //mxEv.getContent().formatted_body = (hi); - //mxEv.getContent().format = "org.matrix.custom.html"; - - } ret.push( Date: Wed, 10 Aug 2022 21:22:20 -0400 Subject: [PATCH 050/176] added tooltips --- src/components/views/messages/TextualBody.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 3761678d3aa..d3ec103b906 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -582,12 +582,12 @@ export default class TextualBody extends React.Component { let body: ReactNode; const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); - let emotesEvent=room.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let finalEmotes = {}; - for (let key in rawEmotes) { - finalEmotes[":"+key+":"] = ""; - } + let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + let finalEmotes = {}; + for (let key in rawEmotes) { + finalEmotes[":" + key + ":"] = ""; + } if (SettingsStore.isEnabled("feature_extensible_events")) { const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; if (extev?.isEquivalentTo(M_MESSAGE)) { @@ -612,14 +612,14 @@ export default class TextualBody extends React.Component { if (!body) { isEmote = content.msgtype === MsgType.Emote; isNotice = content.msgtype === MsgType.Notice; - body = (HtmlUtils.bodyToHtml(content, this.props.highlights, { + body = HtmlUtils.bodyToHtml(content, this.props.highlights, { disableBigEmoji: isEmote || !SettingsStore.getValue('TextualBody.enableBigEmoji'), // Part of Replies fallback support stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, - })as any).replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m); + }); } //console.log(body); From 12e826d5303246fd824107260677e67b7cbd5dcc Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Thu, 11 Aug 2022 09:51:50 -0400 Subject: [PATCH 051/176] removed extra logging --- .../views/room_settings/RoomEmoteSettings.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 60f5edbc268..7d9615cf2e5 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -57,7 +57,7 @@ export default class RoomEmoteSettings extends React.Component { if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - console.log(room.currentState); + //console.log(room.currentState); let emotes: Dictionary; emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let value = {}; @@ -65,11 +65,11 @@ export default class RoomEmoteSettings extends React.Component { value[emote] = emote; } //TODO: Decrypt the shortcodes and emotes if they are encrypted - if (emotes) { - console.log(room.roomId); - console.log(room.name); - console.log(emotes); - } + // if (emotes) { + // console.log(room.roomId); + // console.log(room.name); + // console.log(emotes); + // } //if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); //emotes={} this.state = { From 6b07a6928e28f13ed929d501fe70245d641b3bf3 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi Date: Fri, 12 Aug 2022 16:31:31 -0400 Subject: [PATCH 052/176] edits to changelog --- CHANGELOG.md | 806 +----------------- src/components/views/messages/TextualBody.tsx | 5 +- .../views/room_settings/RoomEmoteSettings.tsx | 40 +- 3 files changed, 11 insertions(+), 840 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18942f5e65e..9a39978ff74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,803 +1,4 @@ -Changes in [3.73.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.73.0) (2023-06-06) -===================================================================================================== - -## ✨ Features - * When joining room in sub-space join the parents too ([\#11011](https://github.com/matrix-org/matrix-react-sdk/pull/11011)). - * Include thread replies in message previews ([\#10631](https://github.com/matrix-org/matrix-react-sdk/pull/10631)). Fixes vector-im/element-web#23920. - * Use semantic headings in space preferences ([\#11021](https://github.com/matrix-org/matrix-react-sdk/pull/11021)). Contributed by @kerryarchibald. - * Use semantic headings in user settings - Ignored users ([\#11006](https://github.com/matrix-org/matrix-react-sdk/pull/11006)). Contributed by @kerryarchibald. - * Use semantic headings in user settings - profile ([\#10973](https://github.com/matrix-org/matrix-react-sdk/pull/10973)). Fixes vector-im/element-web#25461. Contributed by @kerryarchibald. - * Use semantic headings in user settings - account ([\#10972](https://github.com/matrix-org/matrix-react-sdk/pull/10972)). Contributed by @kerryarchibald. - * Support `Insert from iPhone or iPad` in Safari ([\#10851](https://github.com/matrix-org/matrix-react-sdk/pull/10851)). Fixes vector-im/element-web#25327. Contributed by @SuperKenVery. - * Specify supportedStages for User Interactive Auth ([\#10975](https://github.com/matrix-org/matrix-react-sdk/pull/10975)). Fixes vector-im/element-web#19605. - * Pass device id to widgets ([\#10209](https://github.com/matrix-org/matrix-react-sdk/pull/10209)). Contributed by @Fox32. - * Use semantic headings in user settings - discovery ([\#10838](https://github.com/matrix-org/matrix-react-sdk/pull/10838)). Contributed by @kerryarchibald. - * Use semantic headings in user settings - Notifications ([\#10948](https://github.com/matrix-org/matrix-react-sdk/pull/10948)). Contributed by @kerryarchibald. - * Use semantic headings in user settings - spellcheck and language ([\#10959](https://github.com/matrix-org/matrix-react-sdk/pull/10959)). Contributed by @kerryarchibald. - * Use semantic headings in user settings Appearance ([\#10827](https://github.com/matrix-org/matrix-react-sdk/pull/10827)). Contributed by @kerryarchibald. - * Use semantic heading in user settings Sidebar & Voip ([\#10782](https://github.com/matrix-org/matrix-react-sdk/pull/10782)). Contributed by @kerryarchibald. - * Use semantic headings in user settings Security ([\#10774](https://github.com/matrix-org/matrix-react-sdk/pull/10774)). Contributed by @kerryarchibald. - * Use semantic headings in user settings - integrations and account deletion ([\#10837](https://github.com/matrix-org/matrix-react-sdk/pull/10837)). Fixes vector-im/element-web#25378. Contributed by @kerryarchibald. - * Use semantic headings in user settings Preferences ([\#10794](https://github.com/matrix-org/matrix-react-sdk/pull/10794)). Contributed by @kerryarchibald. - * Use semantic headings in user settings Keyboard ([\#10793](https://github.com/matrix-org/matrix-react-sdk/pull/10793)). Contributed by @kerryarchibald. - * RTE plain text mentions as pills ([\#10852](https://github.com/matrix-org/matrix-react-sdk/pull/10852)). Contributed by @alunturner. - * Use semantic headings in user settings Labs ([\#10773](https://github.com/matrix-org/matrix-react-sdk/pull/10773)). Contributed by @kerryarchibald. - * Use semantic list elements for menu lists and tab lists ([\#10902](https://github.com/matrix-org/matrix-react-sdk/pull/10902)). Fixes vector-im/element-web#24928. - * Fix aria-required-children axe violation ([\#10900](https://github.com/matrix-org/matrix-react-sdk/pull/10900)). Fixes vector-im/element-web#25342. - * Enable pagination for overlay timelines ([\#10757](https://github.com/matrix-org/matrix-react-sdk/pull/10757)). Fixes vector-im/voip-internal#107. - * Add tooltip to disabled invite button due to lack of permissions ([\#10869](https://github.com/matrix-org/matrix-react-sdk/pull/10869)). Fixes vector-im/element-web#9824. - * Respect configured auth_header_logo_url for default Welcome page ([\#10870](https://github.com/matrix-org/matrix-react-sdk/pull/10870)). - * Specify lazy loading for avatars ([\#10866](https://github.com/matrix-org/matrix-react-sdk/pull/10866)). Fixes vector-im/element-web#1983. - * Room and user mentions for plain text editor ([\#10665](https://github.com/matrix-org/matrix-react-sdk/pull/10665)). Contributed by @alunturner. - * Add audible notifcation on broadcast error ([\#10654](https://github.com/matrix-org/matrix-react-sdk/pull/10654)). Fixes vector-im/element-web#25132. - * Fall back from server generated thumbnail to original image ([\#10853](https://github.com/matrix-org/matrix-react-sdk/pull/10853)). - * Use semantically correct elements for room sublist context menu ([\#10831](https://github.com/matrix-org/matrix-react-sdk/pull/10831)). Fixes vector-im/customer-retainer#46. - * Avoid calling prepareToEncrypt onKeyDown ([\#10828](https://github.com/matrix-org/matrix-react-sdk/pull/10828)). - * Allows search to recognize full room links ([\#8275](https://github.com/matrix-org/matrix-react-sdk/pull/8275)). Contributed by @bolu-tife. - * "Show rooms with unread messages first" should not be on by default for new users ([\#10820](https://github.com/matrix-org/matrix-react-sdk/pull/10820)). Fixes vector-im/element-web#25304. Contributed by @kerryarchibald. - * Fix emitter handler leak in ThreadView ([\#10803](https://github.com/matrix-org/matrix-react-sdk/pull/10803)). - * Add better error for email invites without identity server ([\#10739](https://github.com/matrix-org/matrix-react-sdk/pull/10739)). Fixes vector-im/element-web#16893. - * Move reaction message previews out of labs ([\#10601](https://github.com/matrix-org/matrix-react-sdk/pull/10601)). Fixes vector-im/element-web#25083. - * Sort muted rooms to the bottom of their section of the room list ([\#10592](https://github.com/matrix-org/matrix-react-sdk/pull/10592)). Fixes vector-im/element-web#25131. Contributed by @kerryarchibald. - * Use semantic headings in user settings Help & About ([\#10752](https://github.com/matrix-org/matrix-react-sdk/pull/10752)). Contributed by @kerryarchibald. - * use ExternalLink components for external links ([\#10758](https://github.com/matrix-org/matrix-react-sdk/pull/10758)). Contributed by @kerryarchibald. - * Use semantic headings in space settings ([\#10751](https://github.com/matrix-org/matrix-react-sdk/pull/10751)). Contributed by @kerryarchibald. - * Use semantic headings for room settings content ([\#10734](https://github.com/matrix-org/matrix-react-sdk/pull/10734)). Contributed by @kerryarchibald. - -## 🐛 Bug Fixes - * Use consistent fonts for Japanese text ([\#10980](https://github.com/matrix-org/matrix-react-sdk/pull/10980)). Fixes vector-im/element-web#22333 and vector-im/element-web#23899. - * Fix: server picker validates unselected option ([\#11020](https://github.com/matrix-org/matrix-react-sdk/pull/11020)). Fixes vector-im/element-web#25488. Contributed by @kerryarchibald. - * Fix room list notification badges going missing in compact layout ([\#11022](https://github.com/matrix-org/matrix-react-sdk/pull/11022)). Fixes vector-im/element-web#25372. - * Fix call to `startSingleSignOn` passing enum in place of idpId ([\#10998](https://github.com/matrix-org/matrix-react-sdk/pull/10998)). Fixes vector-im/element-web#24953. - * Remove hover effect from user name on a DM creation UI ([\#10887](https://github.com/matrix-org/matrix-react-sdk/pull/10887)). Fixes vector-im/element-web#25305. Contributed by @luixxiul. - * Fix layout regression in public space invite dialog ([\#11009](https://github.com/matrix-org/matrix-react-sdk/pull/11009)). Fixes vector-im/element-web#25458. - * Fix layout regression in session dropdown ([\#10999](https://github.com/matrix-org/matrix-react-sdk/pull/10999)). Fixes vector-im/element-web#25448. - * Fix spacing regression in user settings - roles & permissions ([\#10993](https://github.com/matrix-org/matrix-react-sdk/pull/10993)). Fixes vector-im/element-web#25447 and vector-im/element-web#25451. Contributed by @kerryarchibald. - * Fall back to receipt timestamp if we have no event (react-sdk part) ([\#10974](https://github.com/matrix-org/matrix-react-sdk/pull/10974)). Fixes vector-im/element-web#10954. Contributed by @andybalaam. - * Fix: Room header 'view your device list' does not link to new session manager ([\#10979](https://github.com/matrix-org/matrix-react-sdk/pull/10979)). Fixes vector-im/element-web#25440. Contributed by @kerryarchibald. - * Fix display of devices without encryption support in Settings dialog ([\#10977](https://github.com/matrix-org/matrix-react-sdk/pull/10977)). Fixes vector-im/element-web#25413. - * Use aria descriptions instead of labels for TextWithTooltip ([\#10952](https://github.com/matrix-org/matrix-react-sdk/pull/10952)). Fixes vector-im/element-web#25398. - * Use grapheme-splitter instead of lodash for saving emoji from being ripped apart ([\#10976](https://github.com/matrix-org/matrix-react-sdk/pull/10976)). Fixes vector-im/element-web#22196. - * Fix: content overflow in settings subsection ([\#10960](https://github.com/matrix-org/matrix-react-sdk/pull/10960)). Fixes vector-im/element-web#25416. Contributed by @kerryarchibald. - * Make `Privacy Notice` external link on integration manager ToS clickable ([\#10914](https://github.com/matrix-org/matrix-react-sdk/pull/10914)). Fixes vector-im/element-web#25384. Contributed by @luixxiul. - * Ensure that open message context menus are updated when the event is sent ([\#10950](https://github.com/matrix-org/matrix-react-sdk/pull/10950)). - * Ensure that open sticker picker dialogs are updated when the widget configuration is updated. ([\#10945](https://github.com/matrix-org/matrix-react-sdk/pull/10945)). - * Fix big emoji in replies ([\#10932](https://github.com/matrix-org/matrix-react-sdk/pull/10932)). Fixes vector-im/element-web#24798. - * Hide empty `MessageActionBar` on message edit history dialog ([\#10447](https://github.com/matrix-org/matrix-react-sdk/pull/10447)). Fixes vector-im/element-web#24903. Contributed by @luixxiul. - * Fix roving tab index getting confused after dragging space order ([\#10901](https://github.com/matrix-org/matrix-react-sdk/pull/10901)). - * Ignore edits in message previews when they concern messages other than latest ([\#10868](https://github.com/matrix-org/matrix-react-sdk/pull/10868)). Fixes vector-im/element-web#14872. - * Send correct receipts when viewing a room ([\#10864](https://github.com/matrix-org/matrix-react-sdk/pull/10864)). Fixes vector-im/element-web#25196. - * Fix timeline search bar being overlapped by the right panel ([\#10809](https://github.com/matrix-org/matrix-react-sdk/pull/10809)). Fixes vector-im/element-web#25291. Contributed by @luixxiul. - * Fix the state shown for call in rooms ([\#10833](https://github.com/matrix-org/matrix-react-sdk/pull/10833)). - * Add string for membership event where both displayname & avatar change ([\#10880](https://github.com/matrix-org/matrix-react-sdk/pull/10880)). Fixes vector-im/element-web#18026. - * Fix people space notification badge not updating for new DM invites ([\#10849](https://github.com/matrix-org/matrix-react-sdk/pull/10849)). Fixes vector-im/element-web#23248. - * Fix regression in emoji picker order mangling after clearing filter ([\#10854](https://github.com/matrix-org/matrix-react-sdk/pull/10854)). Fixes vector-im/element-web#25323. - * Fix: Edit history modal crash ([\#10834](https://github.com/matrix-org/matrix-react-sdk/pull/10834)). Fixes vector-im/element-web#25309. Contributed by @kerryarchibald. - * Fix long room address and name not being clipped on room info card and update `_RoomSummaryCard.pcss` ([\#10811](https://github.com/matrix-org/matrix-react-sdk/pull/10811)). Fixes vector-im/element-web#25293. Contributed by @luixxiul. - * Treat thumbnail upload failures as complete upload failures ([\#10829](https://github.com/matrix-org/matrix-react-sdk/pull/10829)). Fixes vector-im/element-web#7069. - * Update finite automata to match user identifiers as per spec ([\#10798](https://github.com/matrix-org/matrix-react-sdk/pull/10798)). Fixes vector-im/element-web#25246. - * Fix icon on empty notification panel ([\#10817](https://github.com/matrix-org/matrix-react-sdk/pull/10817)). Fixes vector-im/element-web#25298 and vector-im/element-web#25302. Contributed by @luixxiul. - * Fix: Threads button is highlighted when I create a new room ([\#10819](https://github.com/matrix-org/matrix-react-sdk/pull/10819)). Fixes vector-im/element-web#25284. Contributed by @kerryarchibald. - * Fix the top heading of notification panel ([\#10818](https://github.com/matrix-org/matrix-react-sdk/pull/10818)). Fixes vector-im/element-web#25303. Contributed by @luixxiul. - * Fix the color of the verified E2EE icon on `RoomSummaryCard` ([\#10812](https://github.com/matrix-org/matrix-react-sdk/pull/10812)). Fixes vector-im/element-web#25295. Contributed by @luixxiul. - * Fix: No feedback when waiting for the server on a /delete_devices request with SSO ([\#10795](https://github.com/matrix-org/matrix-react-sdk/pull/10795)). Fixes vector-im/element-web#23096. Contributed by @kerryarchibald. - * Fix: reveal images when image previews are disabled ([\#10781](https://github.com/matrix-org/matrix-react-sdk/pull/10781)). Fixes vector-im/element-web#25271. Contributed by @kerryarchibald. - * Fix accessibility issues around the room list and space panel ([\#10717](https://github.com/matrix-org/matrix-react-sdk/pull/10717)). Fixes vector-im/element-web#13345. - * Ensure tooltip contents is linked via aria to the target element ([\#10729](https://github.com/matrix-org/matrix-react-sdk/pull/10729)). Fixes vector-im/customer-retainer#43. - -Changes in [3.72.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.72.0) (2023-05-10) -===================================================================================================== - -## ✨ Features - * Add UIFeature.locationSharing to hide location sharing ([\#10727](https://github.com/matrix-org/matrix-react-sdk/pull/10727)). - * Memoize field validation results ([\#10714](https://github.com/matrix-org/matrix-react-sdk/pull/10714)). - * Commands for plain text editor ([\#10567](https://github.com/matrix-org/matrix-react-sdk/pull/10567)). Contributed by @alunturner. - * Allow 16 lines of text in the rich text editors ([\#10670](https://github.com/matrix-org/matrix-react-sdk/pull/10670)). Contributed by @alunturner. - * Bail out of `RoomSettingsDialog` when room is not found ([\#10662](https://github.com/matrix-org/matrix-react-sdk/pull/10662)). Contributed by @kerryarchibald. - * Element-R: Populate device list for right-panel ([\#10671](https://github.com/matrix-org/matrix-react-sdk/pull/10671)). Contributed by @florianduros. - * Make existing and new issue URLs configurable ([\#10710](https://github.com/matrix-org/matrix-react-sdk/pull/10710)). Fixes vector-im/element-web#24424. - * Fix usages of ARIA tabpanel ([\#10628](https://github.com/matrix-org/matrix-react-sdk/pull/10628)). Fixes vector-im/element-web#25016. - * Element-R: Starting a DMs with a user ([\#10673](https://github.com/matrix-org/matrix-react-sdk/pull/10673)). Contributed by @florianduros. - * ARIA Accessibility improvements ([\#10675](https://github.com/matrix-org/matrix-react-sdk/pull/10675)). - * ARIA Accessibility improvements ([\#10674](https://github.com/matrix-org/matrix-react-sdk/pull/10674)). - * Add arrow key controls to emoji and reaction pickers ([\#10637](https://github.com/matrix-org/matrix-react-sdk/pull/10637)). Fixes vector-im/element-web#17189. - * Translate credits in help about section ([\#10676](https://github.com/matrix-org/matrix-react-sdk/pull/10676)). - -## 🐛 Bug Fixes - * Fix: reveal images when image previews are disabled ([\#10781](https://github.com/matrix-org/matrix-react-sdk/pull/10781)). Fixes vector-im/element-web#25271. Contributed by @kerryarchibald. - * Fix autocomplete not resetting properly on message send ([\#10741](https://github.com/matrix-org/matrix-react-sdk/pull/10741)). Fixes vector-im/element-web#25170. - * Fix start_sso not working with guests disabled ([\#10720](https://github.com/matrix-org/matrix-react-sdk/pull/10720)). Fixes vector-im/element-web#16624. - * Fix soft crash with Element call widgets ([\#10684](https://github.com/matrix-org/matrix-react-sdk/pull/10684)). - * Send correct receipt when marking a room as read ([\#10730](https://github.com/matrix-org/matrix-react-sdk/pull/10730)). Fixes vector-im/element-web#25207. - * Offload some more waveform processing onto a worker ([\#9223](https://github.com/matrix-org/matrix-react-sdk/pull/9223)). Fixes vector-im/element-web#19756. - * Consolidate login errors ([\#10722](https://github.com/matrix-org/matrix-react-sdk/pull/10722)). Fixes vector-im/element-web#17520. - * Fix all rooms search generating permalinks to wrong room id ([\#10625](https://github.com/matrix-org/matrix-react-sdk/pull/10625)). Fixes vector-im/element-web#25115. - * Posthog properly handle Analytics ID changing from under us ([\#10702](https://github.com/matrix-org/matrix-react-sdk/pull/10702)). Fixes vector-im/element-web#25187. - * Fix Clock being read as an absolute time rather than duration ([\#10706](https://github.com/matrix-org/matrix-react-sdk/pull/10706)). Fixes vector-im/element-web#22582. - * Properly translate errors in `ChangePassword.tsx` so they show up translated to the user but not in our logs ([\#10615](https://github.com/matrix-org/matrix-react-sdk/pull/10615)). Fixes vector-im/element-web#9597. Contributed by @MadLittleMods. - * Honour feature toggles in guest mode ([\#10651](https://github.com/matrix-org/matrix-react-sdk/pull/10651)). Fixes vector-im/element-web#24513. Contributed by @andybalaam. - * Fix default content in devtools event sender ([\#10699](https://github.com/matrix-org/matrix-react-sdk/pull/10699)). Contributed by @tulir. - * Fix a crash when a call ends while you're in it ([\#10681](https://github.com/matrix-org/matrix-react-sdk/pull/10681)). Fixes vector-im/element-web#25153. - * Fix lack of screen reader indication when triggering auto complete ([\#10664](https://github.com/matrix-org/matrix-react-sdk/pull/10664)). Fixes vector-im/element-web#11011. - * Fix typing tile duplicating users ([\#10678](https://github.com/matrix-org/matrix-react-sdk/pull/10678)). Fixes vector-im/element-web#25165. - * Fix wrong room topic tooltip position ([\#10667](https://github.com/matrix-org/matrix-react-sdk/pull/10667)). Fixes vector-im/element-web#25158. - * Fix create subspace dialog not working ([\#10652](https://github.com/matrix-org/matrix-react-sdk/pull/10652)). Fixes vector-im/element-web#24882. - -Changes in [3.71.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.71.1) (2023-04-25) -===================================================================================================== - -## 🐛 Bug Fixes - * Pick up matrix-js-sdk v25.0.0 (missed because of npmjs partial outage) - -Changes in [3.71.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.71.0) (2023-04-25) -===================================================================================================== - -## 🔒 Security - * Fixes for [CVE-2023-30609](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-30609) / GHSA-xv83-x443-7rmw - -## ✨ Features - * Pick sensible default option for phone country dropdown ([\#10627](https://github.com/matrix-org/matrix-react-sdk/pull/10627)). Fixes vector-im/element-web#3528. - * Relate field validation tooltip via aria-describedby ([\#10522](https://github.com/matrix-org/matrix-react-sdk/pull/10522)). Fixes vector-im/element-web#24963. - * Handle more completion types in rte autocomplete ([\#10560](https://github.com/matrix-org/matrix-react-sdk/pull/10560)). Contributed by @alunturner. - * Show a tile for an unloaded predecessor room if it has via_servers ([\#10483](https://github.com/matrix-org/matrix-react-sdk/pull/10483)). Contributed by @andybalaam. - * Exclude message timestamps from aria live region ([\#10584](https://github.com/matrix-org/matrix-react-sdk/pull/10584)). Fixes vector-im/element-web#5696. - * Make composer format bar an aria toolbar ([\#10583](https://github.com/matrix-org/matrix-react-sdk/pull/10583)). Fixes vector-im/element-web#11283. - * Improve accessibility of font slider ([\#10473](https://github.com/matrix-org/matrix-react-sdk/pull/10473)). Fixes vector-im/element-web#20168 and vector-im/element-web#24962. - * fix file size display from kB to KB ([\#10561](https://github.com/matrix-org/matrix-react-sdk/pull/10561)). Fixes vector-im/element-web#24866. Contributed by @NSV1991. - * Handle /me in rte ([\#10558](https://github.com/matrix-org/matrix-react-sdk/pull/10558)). Contributed by @alunturner. - * bind html with switch for manage extension setting option ([\#10553](https://github.com/matrix-org/matrix-react-sdk/pull/10553)). Contributed by @NSV1991. - * Handle command completions in RTE ([\#10521](https://github.com/matrix-org/matrix-react-sdk/pull/10521)). Contributed by @alunturner. - * Add room and user avatars to rte ([\#10497](https://github.com/matrix-org/matrix-react-sdk/pull/10497)). Contributed by @alunturner. - * Support for MSC3882 revision 1 ([\#10443](https://github.com/matrix-org/matrix-react-sdk/pull/10443)). Contributed by @hughns. - * Check profiles before starting a DM ([\#10472](https://github.com/matrix-org/matrix-react-sdk/pull/10472)). Fixes vector-im/element-web#24830. - * Quick settings: Change the copy / labels on the options ([\#10427](https://github.com/matrix-org/matrix-react-sdk/pull/10427)). Fixes vector-im/element-web#24522. Contributed by @justjanne. - * Update rte autocomplete styling ([\#10503](https://github.com/matrix-org/matrix-react-sdk/pull/10503)). Contributed by @alunturner. - -## 🐛 Bug Fixes - * Fix create subspace dialog not working ([\#10652](https://github.com/matrix-org/matrix-react-sdk/pull/10652)). Fixes vector-im/element-web#24882 - * Fix multiple accessibility defects identified by AXE ([\#10606](https://github.com/matrix-org/matrix-react-sdk/pull/10606)). - * Fix view source from edit history dialog always showing latest event ([\#10626](https://github.com/matrix-org/matrix-react-sdk/pull/10626)). Fixes vector-im/element-web#21859. - * #21451 Fix WebGL disabled error message ([\#10589](https://github.com/matrix-org/matrix-react-sdk/pull/10589)). Contributed by @rashmitpankhania. - * Properly translate errors in `AddThreepid.ts` so they show up translated to the user but not in our logs ([\#10432](https://github.com/matrix-org/matrix-react-sdk/pull/10432)). Contributed by @MadLittleMods. - * Fix overflow on auth pages ([\#10605](https://github.com/matrix-org/matrix-react-sdk/pull/10605)). Fixes vector-im/element-web#19548. - * Fix incorrect avatar background colour when using a custom theme ([\#10598](https://github.com/matrix-org/matrix-react-sdk/pull/10598)). Contributed by @jdauphant. - * Remove dependency on `org.matrix.e2e_cross_signing` unstable feature ([\#10593](https://github.com/matrix-org/matrix-react-sdk/pull/10593)). - * Update setting description to match reality ([\#10600](https://github.com/matrix-org/matrix-react-sdk/pull/10600)). Fixes vector-im/element-web#25106. - * Fix no identity server in help & about settings ([\#10563](https://github.com/matrix-org/matrix-react-sdk/pull/10563)). Fixes vector-im/element-web#25077. - * Fix: Images no longer reserve their space in the timeline correctly ([\#10571](https://github.com/matrix-org/matrix-react-sdk/pull/10571)). Fixes vector-im/element-web#25082. Contributed by @kerryarchibald. - * Fix issues with inhibited accessible focus outlines ([\#10579](https://github.com/matrix-org/matrix-react-sdk/pull/10579)). Fixes vector-im/element-web#19742. - * Fix read receipts falling from sky ([\#10576](https://github.com/matrix-org/matrix-react-sdk/pull/10576)). Fixes vector-im/element-web#25081. - * Fix avatar text issue in rte ([\#10559](https://github.com/matrix-org/matrix-react-sdk/pull/10559)). Contributed by @alunturner. - * fix resizer only work with left mouse click ([\#10546](https://github.com/matrix-org/matrix-react-sdk/pull/10546)). Contributed by @NSV1991. - * Fix send two join requests when joining a room from spotlight search ([\#10534](https://github.com/matrix-org/matrix-react-sdk/pull/10534)). Fixes vector-im/element-web#25054. - * Highlight event when any version triggered a highlight ([\#10502](https://github.com/matrix-org/matrix-react-sdk/pull/10502)). Fixes vector-im/element-web#24923 and vector-im/element-web#24970. Contributed by @kerryarchibald. - * Fix spacing of headings of integration manager on General settings tab ([\#10232](https://github.com/matrix-org/matrix-react-sdk/pull/10232)). Fixes vector-im/element-web#24085. Contributed by @luixxiul. - -Changes in [3.70.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.70.0) (2023-04-11) -===================================================================================================== - -## ✨ Features - * Style mentions as pills in rich text editor ([\#10448](https://github.com/matrix-org/matrix-react-sdk/pull/10448)). Contributed by @alunturner. - * Show room create icon if "UIComponent.roomCreation" is enabled ([\#10364](https://github.com/matrix-org/matrix-react-sdk/pull/10364)). Contributed by @maheichyk. - * Mentions as links rte ([\#10463](https://github.com/matrix-org/matrix-react-sdk/pull/10463)). Contributed by @alunturner. - * Better error handling in jump to date ([\#10405](https://github.com/matrix-org/matrix-react-sdk/pull/10405)). Contributed by @MadLittleMods. - * Show "Invite" menu option if "UIComponent.sendInvites" is enabled. ([\#10363](https://github.com/matrix-org/matrix-react-sdk/pull/10363)). Contributed by @maheichyk. - * Added `UserProfilesStore`, `LruCache` and user permalink profile caching ([\#10425](https://github.com/matrix-org/matrix-react-sdk/pull/10425)). Fixes vector-im/element-web#10559. - * Mentions as links rte ([\#10422](https://github.com/matrix-org/matrix-react-sdk/pull/10422)). Contributed by @alunturner. - * Implement MSC3952: intentional mentions ([\#9983](https://github.com/matrix-org/matrix-react-sdk/pull/9983)). - * Implement MSC3973: Search users in the user directory with the Widget API ([\#10269](https://github.com/matrix-org/matrix-react-sdk/pull/10269)). Contributed by @dhenneke. - * Permalinks to message are now displayed as pills ([\#10392](https://github.com/matrix-org/matrix-react-sdk/pull/10392)). Fixes vector-im/element-web#24751 and vector-im/element-web#24706. - * Show search,dial,explore in filterContainer if "UIComponent.filterContainer" is enabled ([\#10381](https://github.com/matrix-org/matrix-react-sdk/pull/10381)). Contributed by @maheichyk. - * Increase space panel collapse clickable area ([\#6084](https://github.com/matrix-org/matrix-react-sdk/pull/6084)). Fixes vector-im/element-web#17379. Contributed by @jaiwanth-v. - * Add fallback for replies to Polls ([\#10380](https://github.com/matrix-org/matrix-react-sdk/pull/10380)). Fixes vector-im/element-web#24197. Contributed by @kerryarchibald. - * Permalinks to rooms and users are now pillified ([\#10388](https://github.com/matrix-org/matrix-react-sdk/pull/10388)). Fixes vector-im/element-web#24825. - * Poll history - access poll history from room settings ([\#10356](https://github.com/matrix-org/matrix-react-sdk/pull/10356)). Contributed by @kerryarchibald. - * Add API params to mute audio and/or video in Jitsi calls by default ([\#10376](https://github.com/matrix-org/matrix-react-sdk/pull/10376)). Contributed by @dhenneke. - * Notifications: inline error message on notifications saving error ([\#10288](https://github.com/matrix-org/matrix-react-sdk/pull/10288)). Contributed by @kerryarchibald. - * Support dynamic room predecessor in SpaceProvider ([\#10348](https://github.com/matrix-org/matrix-react-sdk/pull/10348)). Contributed by @andybalaam. - * Support dynamic room predecessors for RoomProvider ([\#10346](https://github.com/matrix-org/matrix-react-sdk/pull/10346)). Contributed by @andybalaam. - * Support dynamic room predecessors in OwnBeaconStore ([\#10339](https://github.com/matrix-org/matrix-react-sdk/pull/10339)). Contributed by @andybalaam. - * Support dynamic room predecessors in ForwardDialog ([\#10344](https://github.com/matrix-org/matrix-react-sdk/pull/10344)). Contributed by @andybalaam. - * Support dynamic room predecessors in SpaceHierarchy ([\#10341](https://github.com/matrix-org/matrix-react-sdk/pull/10341)). Contributed by @andybalaam. - * Support dynamic room predecessors in AddExistingToSpaceDialog ([\#10342](https://github.com/matrix-org/matrix-react-sdk/pull/10342)). Contributed by @andybalaam. - * Support dynamic room predecessors in leave-behaviour ([\#10340](https://github.com/matrix-org/matrix-react-sdk/pull/10340)). Contributed by @andybalaam. - * Support dynamic room predecessors in StopGapWidgetDriver ([\#10338](https://github.com/matrix-org/matrix-react-sdk/pull/10338)). Contributed by @andybalaam. - * Support dynamic room predecessors in WidgetLayoutStore ([\#10326](https://github.com/matrix-org/matrix-react-sdk/pull/10326)). Contributed by @andybalaam. - * Support dynamic room predecessors in SpaceStore ([\#10332](https://github.com/matrix-org/matrix-react-sdk/pull/10332)). Contributed by @andybalaam. - * Sync polls push rules on changes to account_data ([\#10287](https://github.com/matrix-org/matrix-react-sdk/pull/10287)). Contributed by @kerryarchibald. - * Support dynamic room predecessors in BreadcrumbsStore ([\#10295](https://github.com/matrix-org/matrix-react-sdk/pull/10295)). Contributed by @andybalaam. - * Improved a11y for Field feedback and Secure Phrase input ([\#10320](https://github.com/matrix-org/matrix-react-sdk/pull/10320)). Contributed by @Sebbones. - * Support dynamic room predecessors in RoomNotificationStateStore ([\#10297](https://github.com/matrix-org/matrix-react-sdk/pull/10297)). Contributed by @andybalaam. - -## 🐛 Bug Fixes - * Allow editing with RTE to overflow for autocomplete visibility ([\#10499](https://github.com/matrix-org/matrix-react-sdk/pull/10499)). Contributed by @alunturner. - * Added auto focus to Github URL on opening of debug logs modal ([\#10479](https://github.com/matrix-org/matrix-react-sdk/pull/10479)). Contributed by @ShivamSpm. - * Fix detection of encryption for all users in a room ([\#10487](https://github.com/matrix-org/matrix-react-sdk/pull/10487)). Fixes vector-im/element-web#24995. - * Properly generate mentions when editing a reply with MSC3952 ([\#10486](https://github.com/matrix-org/matrix-react-sdk/pull/10486)). Fixes vector-im/element-web#24924. Contributed by @kerryarchibald. - * Improve performance of rendering a room with many hidden events ([\#10131](https://github.com/matrix-org/matrix-react-sdk/pull/10131)). Contributed by @andybalaam. - * Prevent future date selection in jump to date ([\#10419](https://github.com/matrix-org/matrix-react-sdk/pull/10419)). Fixes vector-im/element-web#20800. Contributed by @MadLittleMods. - * Add aria labels to message search bar to improve accessibility ([\#10476](https://github.com/matrix-org/matrix-react-sdk/pull/10476)). Fixes vector-im/element-web#24921. - * Fix decryption failure bar covering the timeline ([\#10360](https://github.com/matrix-org/matrix-react-sdk/pull/10360)). Fixes vector-im/element-web#24780 vector-im/element-web#24074 and vector-im/element-web#24183. Contributed by @luixxiul. - * Improve profile picture settings accessibility ([\#10470](https://github.com/matrix-org/matrix-react-sdk/pull/10470)). Fixes vector-im/element-web#24919. - * Handle group call redaction ([\#10465](https://github.com/matrix-org/matrix-react-sdk/pull/10465)). - * Display relative timestamp for threads on the same calendar day ([\#10399](https://github.com/matrix-org/matrix-react-sdk/pull/10399)). Fixes vector-im/element-web#24841. Contributed by @kerryarchibald. - * Fix timeline list and paragraph display issues ([\#10424](https://github.com/matrix-org/matrix-react-sdk/pull/10424)). Fixes vector-im/element-web#24602. Contributed by @alunturner. - * Use unique keys for voice broadcast pips ([\#10457](https://github.com/matrix-org/matrix-react-sdk/pull/10457)). Fixes vector-im/element-web#24959. - * Fix "show read receipts sent by other users" not applying to threads ([\#10445](https://github.com/matrix-org/matrix-react-sdk/pull/10445)). Fixes vector-im/element-web#24910. - * Fix joining public rooms without aliases in search dialog ([\#10437](https://github.com/matrix-org/matrix-react-sdk/pull/10437)). Fixes vector-im/element-web#23937. - * Add input validation for `m.direct` in `DMRoomMap` ([\#10436](https://github.com/matrix-org/matrix-react-sdk/pull/10436)). Fixes vector-im/element-web#24909. - * Reduce height reserved for "collapse" button's line on IRC layout ([\#10211](https://github.com/matrix-org/matrix-react-sdk/pull/10211)). Fixes vector-im/element-web#24605. Contributed by @luixxiul. - * Fix `creatorUserId is required` error when opening sticker picker ([\#10423](https://github.com/matrix-org/matrix-react-sdk/pull/10423)). - * Fix block/inline Element descendants error noise in `NewRoomIntro.tsx` ([\#10412](https://github.com/matrix-org/matrix-react-sdk/pull/10412)). Contributed by @MadLittleMods. - * Fix profile resizer to make first character of a line selectable in IRC layout ([\#10396](https://github.com/matrix-org/matrix-react-sdk/pull/10396)). Fixes vector-im/element-web#14764. Contributed by @luixxiul. - * Ensure space between wrapped lines of room name on IRC layout ([\#10188](https://github.com/matrix-org/matrix-react-sdk/pull/10188)). Fixes vector-im/element-web#24742. Contributed by @luixxiul. - * Remove unreadable alt attribute from the room status bar warning icon (nonsense to screenreaders) ([\#10402](https://github.com/matrix-org/matrix-react-sdk/pull/10402)). Contributed by @MadLittleMods. - * Fix big date separators when jump to date is enabled ([\#10404](https://github.com/matrix-org/matrix-react-sdk/pull/10404)). Fixes vector-im/element-web#22969. Contributed by @MadLittleMods. - * Fixes user authentication when registering via the module API ([\#10257](https://github.com/matrix-org/matrix-react-sdk/pull/10257)). Contributed by @maheichyk. - * Handle more edge cases in Space Hierarchy ([\#10280](https://github.com/matrix-org/matrix-react-sdk/pull/10280)). Contributed by @justjanne. - * Further improve performance with lots of hidden events ([\#10353](https://github.com/matrix-org/matrix-react-sdk/pull/10353)). Fixes vector-im/element-web#24480. Contributed by @andybalaam. - * Respect user cancelling upload flow by dismissing spinner ([\#10373](https://github.com/matrix-org/matrix-react-sdk/pull/10373)). Fixes vector-im/element-web#24667. - * When starting a DM, the end-to-end encryption status icon does now only appear if the DM can be encrypted ([\#10394](https://github.com/matrix-org/matrix-react-sdk/pull/10394)). Fixes vector-im/element-web#24397. - * Fix `[object Object]` in feedback metadata ([\#10390](https://github.com/matrix-org/matrix-react-sdk/pull/10390)). - * Fix pinned messages card saying nothing pinned while loading ([\#10385](https://github.com/matrix-org/matrix-react-sdk/pull/10385)). Fixes vector-im/element-web#24615. - * Fix import e2e key dialog staying disabled after paste ([\#10375](https://github.com/matrix-org/matrix-react-sdk/pull/10375)). Fixes vector-im/element-web#24818. - * Show all labs even if incompatible, with appropriate tooltip explaining requirements ([\#10369](https://github.com/matrix-org/matrix-react-sdk/pull/10369)). Fixes vector-im/element-web#24813. - * Fix UIFeature.Registration not applying to all paths ([\#10371](https://github.com/matrix-org/matrix-react-sdk/pull/10371)). Fixes vector-im/element-web#24814. - * Clicking on a user pill does now only open the profile in the right panel and no longer navigates to the home view. ([\#10359](https://github.com/matrix-org/matrix-react-sdk/pull/10359)). Fixes vector-im/element-web#24797. - * Fix start DM with pending third party invite ([\#10347](https://github.com/matrix-org/matrix-react-sdk/pull/10347)). Fixes vector-im/element-web#24781. - * Fix long display name overflowing reply tile on IRC layout ([\#10343](https://github.com/matrix-org/matrix-react-sdk/pull/10343)). Fixes vector-im/element-web#24738. Contributed by @luixxiul. - * Display redacted body on ThreadView in the same way as normal messages ([\#9016](https://github.com/matrix-org/matrix-react-sdk/pull/9016)). Fixes vector-im/element-web#24729. Contributed by @luixxiul. - * Handle more edge cases in ACL updates ([\#10279](https://github.com/matrix-org/matrix-react-sdk/pull/10279)). Contributed by @justjanne. - * Allow parsing png files to fail if thumbnailing is successful ([\#10308](https://github.com/matrix-org/matrix-react-sdk/pull/10308)). - -Changes in [3.69.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.69.1) (2023-03-31) -===================================================================================================== - -## 🐛 Bug Fixes - * Fix detection of encryption for all users in a room ([\#10487](https://github.com/matrix-org/matrix-react-sdk/pull/10487)). Fixes vector-im/element-web#24995. - -Changes in [3.69.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.69.0) (2023-03-28) -===================================================================================================== - -## 🔒 Security - * Fixes for [CVE-2023-28427](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28427) / GHSA-mwq8-fjpf-c2gr - * Fixes for [CVE-2023-28103](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28103) / GHSA-6g43-88cp-w5gv - -Changes in [3.68.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.68.0) (2023-03-15) -===================================================================================================== - -## ✨ Features - * Only allow to start a DM with one email if encryption by default is enabled ([\#10253](https://github.com/matrix-org/matrix-react-sdk/pull/10253)). Fixes vector-im/element-web#23133. - * DM rooms are now encrypted if encryption by default is enabled and only inviting a single email address. Any action in the result DM room will be blocked until the other has joined. ([\#10229](https://github.com/matrix-org/matrix-react-sdk/pull/10229)). - * Reduce bottom margin of ReplyChain on compact modern layout ([\#8972](https://github.com/matrix-org/matrix-react-sdk/pull/8972)). Fixes vector-im/element-web#22748. Contributed by @luixxiul. - * Support for v2 of MSC3903 ([\#10165](https://github.com/matrix-org/matrix-react-sdk/pull/10165)). Contributed by @hughns. - * When starting a DM, existing rooms with pending third-party invites will be reused. ([\#10256](https://github.com/matrix-org/matrix-react-sdk/pull/10256)). Fixes vector-im/element-web#23139. - * Polls push rules: synchronise poll rules with message rules ([\#10263](https://github.com/matrix-org/matrix-react-sdk/pull/10263)). Contributed by @kerryarchibald. - * New verification request toast button labels ([\#10259](https://github.com/matrix-org/matrix-react-sdk/pull/10259)). - * Remove padding around integration manager iframe ([\#10148](https://github.com/matrix-org/matrix-react-sdk/pull/10148)). - * Fix block code styling in rich text editor ([\#10246](https://github.com/matrix-org/matrix-react-sdk/pull/10246)). Contributed by @alunturner. - * Poll history: fetch more poll history ([\#10235](https://github.com/matrix-org/matrix-react-sdk/pull/10235)). Contributed by @kerryarchibald. - * Sort short/exact emoji matches before longer incomplete matches ([\#10212](https://github.com/matrix-org/matrix-react-sdk/pull/10212)). Fixes vector-im/element-web#23210. Contributed by @grimhilt. - * Poll history: detail screen ([\#10172](https://github.com/matrix-org/matrix-react-sdk/pull/10172)). Contributed by @kerryarchibald. - * Provide a more detailed error message than "No known servers" ([\#6048](https://github.com/matrix-org/matrix-react-sdk/pull/6048)). Fixes vector-im/element-web#13247. Contributed by @aaronraimist. - * Say when a call was answered from a different device ([\#10224](https://github.com/matrix-org/matrix-react-sdk/pull/10224)). - * Widget permissions customizations using module api ([\#10121](https://github.com/matrix-org/matrix-react-sdk/pull/10121)). Contributed by @maheichyk. - * Fix copy button icon overlapping with copyable text ([\#10227](https://github.com/matrix-org/matrix-react-sdk/pull/10227)). Contributed by @Adesh-Pandey. - * Support joining non-peekable rooms via the module API ([\#10154](https://github.com/matrix-org/matrix-react-sdk/pull/10154)). Contributed by @maheichyk. - * The "new login" toast does now display the same device information as in the settings. "No" does now open the device settings. "Yes, it was me" dismisses the toast. ([\#10200](https://github.com/matrix-org/matrix-react-sdk/pull/10200)). - * Do not prompt for a password when doing a „reset all“ after login ([\#10208](https://github.com/matrix-org/matrix-react-sdk/pull/10208)). - -## 🐛 Bug Fixes - * Fix incorrect copy in space creation flow ([\#10296](https://github.com/matrix-org/matrix-react-sdk/pull/10296)). Fixes vector-im/element-web#24741. - * Fix space settings dialog having rogue title tooltip ([\#10293](https://github.com/matrix-org/matrix-react-sdk/pull/10293)). Fixes vector-im/element-web#24740. - * Show spinner when starting a DM from the user profile (right panel) ([\#10290](https://github.com/matrix-org/matrix-react-sdk/pull/10290)). - * Reduce height of toggle on expanded view source event ([\#10283](https://github.com/matrix-org/matrix-react-sdk/pull/10283)). Fixes vector-im/element-web#22873. Contributed by @luixxiul. - * Pillify http and non-prefixed matrix.to links ([\#10277](https://github.com/matrix-org/matrix-react-sdk/pull/10277)). Fixes vector-im/element-web#20844. - * Fix some features not being configurable via `features` ([\#10276](https://github.com/matrix-org/matrix-react-sdk/pull/10276)). - * Fix starting a DM from the right panel in some cases ([\#10278](https://github.com/matrix-org/matrix-react-sdk/pull/10278)). Fixes vector-im/element-web#24722. - * Align info EventTile and normal EventTile on IRC layout ([\#10197](https://github.com/matrix-org/matrix-react-sdk/pull/10197)). Fixes vector-im/element-web#22782. Contributed by @luixxiul. - * Fix blowout of waveform of the voice message player on narrow UI ([\#8861](https://github.com/matrix-org/matrix-react-sdk/pull/8861)). Fixes vector-im/element-web#22604. Contributed by @luixxiul. - * Fix the hidden view source toggle on IRC layout ([\#10266](https://github.com/matrix-org/matrix-react-sdk/pull/10266)). Fixes vector-im/element-web#22872. Contributed by @luixxiul. - * Fix buttons on the room header being compressed due to long room name ([\#10155](https://github.com/matrix-org/matrix-react-sdk/pull/10155)). Contributed by @luixxiul. - * Use the room avatar as a placeholder in calls ([\#10231](https://github.com/matrix-org/matrix-react-sdk/pull/10231)). - * Fix calls showing as 'connecting' after hangup ([\#10223](https://github.com/matrix-org/matrix-react-sdk/pull/10223)). - * Prevent multiple Jitsi calls started at the same time ([\#10183](https://github.com/matrix-org/matrix-react-sdk/pull/10183)). Fixes vector-im/element-web#23009. - * Make localization keys compatible with agglutinative and/or SOV type languages ([\#10159](https://github.com/matrix-org/matrix-react-sdk/pull/10159)). Contributed by @luixxiul. - -Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.67.0) (2023-02-28) -===================================================================================================== - -## ✨ Features - * Display "The sender has blocked you from receiving this message" error message instead of "Unable to decrypt message" ([\#10202](https://github.com/matrix-org/matrix-react-sdk/pull/10202)). Contributed by @florianduros. - * Polls: show warning about undecryptable relations ([\#10179](https://github.com/matrix-org/matrix-react-sdk/pull/10179)). Contributed by @kerryarchibald. - * Poll history: fetch last 30 days of polls ([\#10157](https://github.com/matrix-org/matrix-react-sdk/pull/10157)). Contributed by @kerryarchibald. - * Poll history - ended polls list items ([\#10119](https://github.com/matrix-org/matrix-react-sdk/pull/10119)). Contributed by @kerryarchibald. - * Remove threads labs flag and the ability to disable threads ([\#9878](https://github.com/matrix-org/matrix-react-sdk/pull/9878)). Fixes vector-im/element-web#24365. - * Show a success dialog after setting up the key backup ([\#10177](https://github.com/matrix-org/matrix-react-sdk/pull/10177)). Fixes vector-im/element-web#24487. - * Release Sign in with QR out of labs ([\#10066](https://github.com/matrix-org/matrix-react-sdk/pull/10066)). Contributed by @hughns. - * Hide indent button in rte ([\#10149](https://github.com/matrix-org/matrix-react-sdk/pull/10149)). Contributed by @alunturner. - * Add option to find own location in map views ([\#10083](https://github.com/matrix-org/matrix-react-sdk/pull/10083)). - * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. - -## 🐛 Bug Fixes - * Stop access token overflowing the box ([\#10069](https://github.com/matrix-org/matrix-react-sdk/pull/10069)). Fixes vector-im/element-web#24023. Contributed by @sbjaj33. - * Add link to next file in the export ([\#10190](https://github.com/matrix-org/matrix-react-sdk/pull/10190)). Fixes vector-im/element-web#20272. Contributed by @grimhilt. - * Ended poll tiles: add ended the poll message ([\#10193](https://github.com/matrix-org/matrix-react-sdk/pull/10193)). Fixes vector-im/element-web#24579. Contributed by @kerryarchibald. - * Fix accidentally inverted condition for room ordering ([\#10178](https://github.com/matrix-org/matrix-react-sdk/pull/10178)). Fixes vector-im/element-web#24527. Contributed by @justjanne. - * Re-focus the composer on dialogue quit ([\#10007](https://github.com/matrix-org/matrix-react-sdk/pull/10007)). Fixes vector-im/element-web#22832. Contributed by @Ashu999. - * Try to resolve emails before creating a DM ([\#10164](https://github.com/matrix-org/matrix-react-sdk/pull/10164)). - * Disable poll response loading test ([\#10168](https://github.com/matrix-org/matrix-react-sdk/pull/10168)). Contributed by @justjanne. - * Fix email lookup in invite dialog ([\#10150](https://github.com/matrix-org/matrix-react-sdk/pull/10150)). Fixes vector-im/element-web#23353. - * Remove duplicate white space characters from translation keys ([\#10152](https://github.com/matrix-org/matrix-react-sdk/pull/10152)). Contributed by @luixxiul. - * Fix the caption of new sessions manager on Labs settings page for localization ([\#10143](https://github.com/matrix-org/matrix-react-sdk/pull/10143)). Contributed by @luixxiul. - * Prevent start another DM with a user if one already exists ([\#10127](https://github.com/matrix-org/matrix-react-sdk/pull/10127)). Fixes vector-im/element-web#23138. - * Remove white space characters before the horizontal ellipsis ([\#10130](https://github.com/matrix-org/matrix-react-sdk/pull/10130)). Contributed by @luixxiul. - * Fix Selectable Text on 'Delete All' and 'Retry All' Buttons ([\#10128](https://github.com/matrix-org/matrix-react-sdk/pull/10128)). Fixes vector-im/element-web#23232. Contributed by @akshattchhabra. - * Correctly Identify emoticons ([\#10108](https://github.com/matrix-org/matrix-react-sdk/pull/10108)). Fixes vector-im/element-web#19472. Contributed by @adarsh-sgh. - * Remove a redundant white space ([\#10129](https://github.com/matrix-org/matrix-react-sdk/pull/10129)). Contributed by @luixxiul. - -Changes in [3.66.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.66.0) (2023-02-14) -===================================================================================================== - -## ✨ Features - * Add option to find own location in map views ([\#10083](https://github.com/matrix-org/matrix-react-sdk/pull/10083)). - * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. - * Indicate unread messages in tab title ([\#10096](https://github.com/matrix-org/matrix-react-sdk/pull/10096)). Contributed by @tnt7864. - * Open message in editing mode when keyboard up is pressed (RTE) ([\#10079](https://github.com/matrix-org/matrix-react-sdk/pull/10079)). Contributed by @florianduros. - * Hide superseded rooms from the room list using dynamic room predecessors ([\#10068](https://github.com/matrix-org/matrix-react-sdk/pull/10068)). Contributed by @andybalaam. - * Support MSC3946 in RoomListStore ([\#10054](https://github.com/matrix-org/matrix-react-sdk/pull/10054)). Fixes vector-im/element-web#24325. Contributed by @andybalaam. - * Auto focus security key field ([\#10048](https://github.com/matrix-org/matrix-react-sdk/pull/10048)). - * use Poll model with relations API in poll rendering ([\#9877](https://github.com/matrix-org/matrix-react-sdk/pull/9877)). Contributed by @kerryarchibald. - * Support MSC3946 in the RoomCreate tile ([\#10041](https://github.com/matrix-org/matrix-react-sdk/pull/10041)). Fixes vector-im/element-web#24323. Contributed by @andybalaam. - * Update labs flag description for RTE ([\#10058](https://github.com/matrix-org/matrix-react-sdk/pull/10058)). Contributed by @florianduros. - * Change ul list style to disc when editing message ([\#10043](https://github.com/matrix-org/matrix-react-sdk/pull/10043)). Contributed by @alunturner. - * Improved click detection within PiP windows ([\#10040](https://github.com/matrix-org/matrix-react-sdk/pull/10040)). Fixes vector-im/element-web#24371. - * Add RTE keyboard navigation in editing ([\#9980](https://github.com/matrix-org/matrix-react-sdk/pull/9980)). Fixes vector-im/element-web#23621. Contributed by @florianduros. - * Paragraph integration for rich text editor ([\#10008](https://github.com/matrix-org/matrix-react-sdk/pull/10008)). Contributed by @alunturner. - * Add indentation increasing/decreasing to RTE ([\#10034](https://github.com/matrix-org/matrix-react-sdk/pull/10034)). Contributed by @florianduros. - * Add ignore user confirmation dialog ([\#6116](https://github.com/matrix-org/matrix-react-sdk/pull/6116)). Fixes vector-im/element-web#14746. - * Use monospace font for room, message IDs in View Source modal ([\#9956](https://github.com/matrix-org/matrix-react-sdk/pull/9956)). Fixes vector-im/element-web#21937. Contributed by @paragpoddar. - * Implement MSC3946 for AdvancedRoomSettingsTab ([\#9995](https://github.com/matrix-org/matrix-react-sdk/pull/9995)). Fixes vector-im/element-web#24322. Contributed by @andybalaam. - * Implementation of MSC3824 to make the client OIDC-aware ([\#8681](https://github.com/matrix-org/matrix-react-sdk/pull/8681)). Contributed by @hughns. - * Improves a11y for avatar uploads ([\#9985](https://github.com/matrix-org/matrix-react-sdk/pull/9985)). Contributed by @GoodGuyMarco. - * Add support for [token authenticated registration](https ([\#7275](https://github.com/matrix-org/matrix-react-sdk/pull/7275)). Fixes vector-im/element-web#18931. Contributed by @govynnus. - -## 🐛 Bug Fixes - * Remove duplicate white space characters from translation keys ([\#10152](https://github.com/matrix-org/matrix-react-sdk/pull/10152)). Contributed by @luixxiul. - * Fix the caption of new sessions manager on Labs settings page for localization ([\#10143](https://github.com/matrix-org/matrix-react-sdk/pull/10143)). Contributed by @luixxiul. - * Prevent start another DM with a user if one already exists ([\#10127](https://github.com/matrix-org/matrix-react-sdk/pull/10127)). Fixes vector-im/element-web#23138. - * Remove white space characters before the horizontal ellipsis ([\#10130](https://github.com/matrix-org/matrix-react-sdk/pull/10130)). Contributed by @luixxiul. - * Fix Selectable Text on 'Delete All' and 'Retry All' Buttons ([\#10128](https://github.com/matrix-org/matrix-react-sdk/pull/10128)). Fixes vector-im/element-web#23232. Contributed by @akshattchhabra. - * Correctly Identify emoticons ([\#10108](https://github.com/matrix-org/matrix-react-sdk/pull/10108)). Fixes vector-im/element-web#19472. Contributed by @adarsh-sgh. - * Should open new 1:1 chat room after leaving the old one ([\#9880](https://github.com/matrix-org/matrix-react-sdk/pull/9880)). Contributed by @ahmadkadri. - * Remove a redundant white space ([\#10129](https://github.com/matrix-org/matrix-react-sdk/pull/10129)). Contributed by @luixxiul. - * Fix a crash when removing persistent widgets (updated) ([\#10099](https://github.com/matrix-org/matrix-react-sdk/pull/10099)). Fixes vector-im/element-web#24412. Contributed by @andybalaam. - * Fix wrongly grouping 3pid invites into a single repeated transition ([\#10087](https://github.com/matrix-org/matrix-react-sdk/pull/10087)). Fixes vector-im/element-web#24432. - * Fix scrollbar colliding with checkbox in add to space section ([\#10093](https://github.com/matrix-org/matrix-react-sdk/pull/10093)). Fixes vector-im/element-web#23189. Contributed by @Arnabdaz. - * Add a whitespace character after 'broadcast?' ([\#10097](https://github.com/matrix-org/matrix-react-sdk/pull/10097)). Contributed by @luixxiul. - * Seekbar in broadcast PiP view is now updated when switching between different broadcasts ([\#10072](https://github.com/matrix-org/matrix-react-sdk/pull/10072)). Fixes vector-im/element-web#24415. - * Add border to "reject" button on room preview card for clickable area indication. It fixes vector-im/element-web#22623 ([\#9205](https://github.com/matrix-org/matrix-react-sdk/pull/9205)). Contributed by @gefgu. - * Element-R: fix rageshages ([\#10081](https://github.com/matrix-org/matrix-react-sdk/pull/10081)). Fixes vector-im/element-web#24430. - * Fix markdown paragraph display in timeline ([\#10071](https://github.com/matrix-org/matrix-react-sdk/pull/10071)). Fixes vector-im/element-web#24419. Contributed by @alunturner. - * Prevent the remaining broadcast time from being exceeded ([\#10070](https://github.com/matrix-org/matrix-react-sdk/pull/10070)). - * Fix cursor position when new line is created by pressing enter (RTE) ([\#10064](https://github.com/matrix-org/matrix-react-sdk/pull/10064)). Contributed by @florianduros. - * Ensure room is actually in space hierarchy when resolving its latest version ([\#10010](https://github.com/matrix-org/matrix-react-sdk/pull/10010)). - * Fix new line for inline code ([\#10062](https://github.com/matrix-org/matrix-react-sdk/pull/10062)). Contributed by @florianduros. - * Member avatars without canvas ([\#9990](https://github.com/matrix-org/matrix-react-sdk/pull/9990)). Contributed by @clarkf. - * Apply more general fix for base avatar regressions ([\#10045](https://github.com/matrix-org/matrix-react-sdk/pull/10045)). Fixes vector-im/element-web#24382 and vector-im/element-web#24370. - * Replace list, code block and quote icons by new icons ([\#10035](https://github.com/matrix-org/matrix-react-sdk/pull/10035)). Contributed by @florianduros. - * fix regional emojis converted to flags ([\#9294](https://github.com/matrix-org/matrix-react-sdk/pull/9294)). Fixes vector-im/element-web#19000. Contributed by @grimhilt. - * resolved emoji description text overflowing issue ([\#10028](https://github.com/matrix-org/matrix-react-sdk/pull/10028)). Contributed by @fahadNoufal. - * Fix MessageEditHistoryDialog crashing on complex input ([\#10018](https://github.com/matrix-org/matrix-react-sdk/pull/10018)). Fixes vector-im/element-web#23665. Contributed by @clarkf. - * Unify unread notification state determination ([\#9941](https://github.com/matrix-org/matrix-react-sdk/pull/9941)). Contributed by @clarkf. - * Fix layout and visual regressions around default avatars ([\#10031](https://github.com/matrix-org/matrix-react-sdk/pull/10031)). Fixes vector-im/element-web#24375 and vector-im/element-web#24369. - * Fix useUnreadNotifications exploding with falsey room, like in notif panel ([\#10030](https://github.com/matrix-org/matrix-react-sdk/pull/10030)). Fixes matrix-org/element-web-rageshakes#19334. - * Fix "[object Promise]" appearing in HTML exports ([\#9975](https://github.com/matrix-org/matrix-react-sdk/pull/9975)). Fixes vector-im/element-web#24272. Contributed by @clarkf. - * changing the color of message time stamp ([\#10016](https://github.com/matrix-org/matrix-react-sdk/pull/10016)). Contributed by @nawarajshah. - * Fix link creation with backward selection ([\#9986](https://github.com/matrix-org/matrix-react-sdk/pull/9986)). Fixes vector-im/element-web#24315. Contributed by @florianduros. - * Misaligned reply preview in thread composer #23396 ([\#9977](https://github.com/matrix-org/matrix-react-sdk/pull/9977)). Fixes vector-im/element-web#23396. Contributed by @mustafa-kapadia1483. - - -Changes in [3.65.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.65.0) (2023-01-31) -===================================================================================================== - -## ✨ Features - * Quotes for rte ([\#9932](https://github.com/matrix-org/matrix-react-sdk/pull/9932)). Contributed by @alunturner. - * Show the room name in the room header during calls ([\#9942](https://github.com/matrix-org/matrix-react-sdk/pull/9942)). Fixes vector-im/element-web#24268. - * Add code blocks to rich text editor ([\#9921](https://github.com/matrix-org/matrix-react-sdk/pull/9921)). Contributed by @alunturner. - * Add new style for inline code ([\#9936](https://github.com/matrix-org/matrix-react-sdk/pull/9936)). Contributed by @florianduros. - * Add disabled button state to rich text editor ([\#9930](https://github.com/matrix-org/matrix-react-sdk/pull/9930)). Contributed by @alunturner. - * Change the rageshake "app" for auto-rageshakes ([\#9909](https://github.com/matrix-org/matrix-react-sdk/pull/9909)). - * Device manager - tweak settings display ([\#9905](https://github.com/matrix-org/matrix-react-sdk/pull/9905)). Contributed by @kerryarchibald. - * Add list functionality to rich text editor ([\#9871](https://github.com/matrix-org/matrix-react-sdk/pull/9871)). Contributed by @alunturner. - -## 🐛 Bug Fixes - * Fix RTE focus behaviour in threads ([\#9969](https://github.com/matrix-org/matrix-react-sdk/pull/9969)). Fixes vector-im/element-web#23755. Contributed by @florianduros. - * #22204 Issue: Centered File info in lightbox ([\#9971](https://github.com/matrix-org/matrix-react-sdk/pull/9971)). Fixes vector-im/element-web#22204. Contributed by @Spartan09. - * Fix seekbar position for zero length audio ([\#9949](https://github.com/matrix-org/matrix-react-sdk/pull/9949)). Fixes vector-im/element-web#24248. - * Allow thread panel to be closed after being opened from notification ([\#9937](https://github.com/matrix-org/matrix-react-sdk/pull/9937)). Fixes vector-im/element-web#23764 vector-im/element-web#23852 and vector-im/element-web#24213. Contributed by @justjanne. - * Only highlight focused menu item if focus is supposed to be visible ([\#9945](https://github.com/matrix-org/matrix-react-sdk/pull/9945)). Fixes vector-im/element-web#23582. - * Prevent call durations from breaking onto multiple lines ([\#9944](https://github.com/matrix-org/matrix-react-sdk/pull/9944)). - * Tweak call lobby buttons to more closely match designs ([\#9943](https://github.com/matrix-org/matrix-react-sdk/pull/9943)). - * Do not show a broadcast as live immediately after the recording has stopped ([\#9947](https://github.com/matrix-org/matrix-react-sdk/pull/9947)). Fixes vector-im/element-web#24233. - * Clear the RTE before sending a message ([\#9948](https://github.com/matrix-org/matrix-react-sdk/pull/9948)). Contributed by @florianduros. - * Fix {enter} press in RTE ([\#9927](https://github.com/matrix-org/matrix-react-sdk/pull/9927)). Contributed by @florianduros. - * Fix the problem that the password reset email has to be confirmed twice ([\#9926](https://github.com/matrix-org/matrix-react-sdk/pull/9926)). Fixes vector-im/element-web#24226. - * replace .at() with array.length-1 ([\#9933](https://github.com/matrix-org/matrix-react-sdk/pull/9933)). Fixes matrix-org/element-web-rageshakes#19281. - * Fix broken threads list timestamp layout ([\#9922](https://github.com/matrix-org/matrix-react-sdk/pull/9922)). Fixes vector-im/element-web#24243 and vector-im/element-web#24191. Contributed by @justjanne. - * Disable multiple messages when {enter} is pressed multiple times ([\#9929](https://github.com/matrix-org/matrix-react-sdk/pull/9929)). Fixes vector-im/element-web#24249. Contributed by @florianduros. - * Fix logout devices when resetting the password ([\#9925](https://github.com/matrix-org/matrix-react-sdk/pull/9925)). Fixes vector-im/element-web#24228. - * Fix: Poll replies overflow when not enough space ([\#9924](https://github.com/matrix-org/matrix-react-sdk/pull/9924)). Fixes vector-im/element-web#24227. Contributed by @kerryarchibald. - * State event updates are not forwarded to the widget from invitation room ([\#9802](https://github.com/matrix-org/matrix-react-sdk/pull/9802)). Contributed by @maheichyk. - * Fix error when viewing source of redacted events ([\#9914](https://github.com/matrix-org/matrix-react-sdk/pull/9914)). Fixes vector-im/element-web#24165. Contributed by @clarkf. - * Replace outdated css attribute ([\#9912](https://github.com/matrix-org/matrix-react-sdk/pull/9912)). Fixes vector-im/element-web#24218. Contributed by @justjanne. - * Clear isLogin theme override when user is no longer viewing login screens ([\#9911](https://github.com/matrix-org/matrix-react-sdk/pull/9911)). Fixes vector-im/element-web#23893. - * Fix reply action in message context menu notif & file panels ([\#9895](https://github.com/matrix-org/matrix-react-sdk/pull/9895)). Fixes vector-im/element-web#23970. - * Fix issue where thread dropdown would not show up correctly ([\#9872](https://github.com/matrix-org/matrix-react-sdk/pull/9872)). Fixes vector-im/element-web#24040. Contributed by @justjanne. - * Fix unexpected composer growing ([\#9889](https://github.com/matrix-org/matrix-react-sdk/pull/9889)). Contributed by @florianduros. - * Fix misaligned timestamps for thread roots which are emotes ([\#9875](https://github.com/matrix-org/matrix-react-sdk/pull/9875)). Fixes vector-im/element-web#23897. Contributed by @justjanne. - -Changes in [3.64.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.2) (2023-01-20) -===================================================================================================== - -## 🐛 Bug Fixes - * Fix second occurence of a crash in older browsers - -Changes in [3.64.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.1) (2023-01-18) -===================================================================================================== - -## 🐛 Bug Fixes - * Fix crash in older browsers (replace .at() with array.length-1) ([\#9933](https://github.com/matrix-org/matrix-react-sdk/pull/9933)). Fixes matrix-org/element-web-rageshakes#19281. - -Changes in [3.64.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.0) (2023-01-18) -===================================================================================================== - -## ✨ Features - * Switch threads on for everyone ([\#9879](https://github.com/matrix-org/matrix-react-sdk/pull/9879)). - * Make threads use new UTD UI ([\#9876](https://github.com/matrix-org/matrix-react-sdk/pull/9876)). Fixes vector-im/element-web#24060. - * Add edit and remove actions to link in RTE ([\#9864](https://github.com/matrix-org/matrix-react-sdk/pull/9864)). - * Remove extensible events v1 experimental rendering ([\#9881](https://github.com/matrix-org/matrix-react-sdk/pull/9881)). - * Make create poll dialog scale better (PSG-929) ([\#9873](https://github.com/matrix-org/matrix-react-sdk/pull/9873)). Fixes vector-im/element-web#21855. - * Change RTE mode icons ([\#9861](https://github.com/matrix-org/matrix-react-sdk/pull/9861)). - * Device manager - prune client information events after remote sign out ([\#9874](https://github.com/matrix-org/matrix-react-sdk/pull/9874)). - * Check connection before starting broadcast ([\#9857](https://github.com/matrix-org/matrix-react-sdk/pull/9857)). - * Enable sent receipt for poll start events (PSG-962) ([\#9870](https://github.com/matrix-org/matrix-react-sdk/pull/9870)). - * Change clear notifications to have more readable copy ([\#9867](https://github.com/matrix-org/matrix-react-sdk/pull/9867)). - * Combine search results when the query is present in multiple successive messages ([\#9855](https://github.com/matrix-org/matrix-react-sdk/pull/9855)). Fixes vector-im/element-web#3977. Contributed by @grimhilt. - * Disable bubbles for broadcasts ([\#9860](https://github.com/matrix-org/matrix-react-sdk/pull/9860)). Fixes vector-im/element-web#24140. - * Enable reactions and replies for broadcasts ([\#9856](https://github.com/matrix-org/matrix-react-sdk/pull/9856)). Fixes vector-im/element-web#24042. - * Improve switching between rich and plain editing modes ([\#9776](https://github.com/matrix-org/matrix-react-sdk/pull/9776)). - * Redesign the picture-in-picture window ([\#9800](https://github.com/matrix-org/matrix-react-sdk/pull/9800)). Fixes vector-im/element-web#23980. - * User on-boarding tasks now appear in a static order. ([\#9799](https://github.com/matrix-org/matrix-react-sdk/pull/9799)). Contributed by @GoodGuyMarco. - * Device manager - contextual menus ([\#9832](https://github.com/matrix-org/matrix-react-sdk/pull/9832)). - * If listening a non-live broadcast and changing the room, the broadcast will be paused ([\#9825](https://github.com/matrix-org/matrix-react-sdk/pull/9825)). Fixes vector-im/element-web#24078. - * Consider own broadcasts from other device as a playback ([\#9821](https://github.com/matrix-org/matrix-react-sdk/pull/9821)). Fixes vector-im/element-web#24068. - * Add link creation to rich text editor ([\#9775](https://github.com/matrix-org/matrix-react-sdk/pull/9775)). - * Add mark as read option in room setting ([\#9798](https://github.com/matrix-org/matrix-react-sdk/pull/9798)). Fixes vector-im/element-web#24053. - * Device manager - current device design and copy tweaks ([\#9801](https://github.com/matrix-org/matrix-react-sdk/pull/9801)). - * Unify notifications panel event design ([\#9754](https://github.com/matrix-org/matrix-react-sdk/pull/9754)). - * Add actions for integration manager to send and read certain events ([\#9740](https://github.com/matrix-org/matrix-react-sdk/pull/9740)). - * Device manager - design tweaks ([\#9768](https://github.com/matrix-org/matrix-react-sdk/pull/9768)). - * Change room list sorting to activity and unread first by default ([\#9773](https://github.com/matrix-org/matrix-react-sdk/pull/9773)). Fixes vector-im/element-web#24014. - * Add a config flag to enable the rust crypto-sdk ([\#9759](https://github.com/matrix-org/matrix-react-sdk/pull/9759)). - * Improve decryption error UI by consolidating error messages and providing instructions when possible ([\#9544](https://github.com/matrix-org/matrix-react-sdk/pull/9544)). - * Honor font settings in Element Call ([\#9751](https://github.com/matrix-org/matrix-react-sdk/pull/9751)). Fixes vector-im/element-web#23661. - * Device manager - use deleteAccountData to prune device manager client information events ([\#9734](https://github.com/matrix-org/matrix-react-sdk/pull/9734)). - -## 🐛 Bug Fixes - * Display rooms & threads as unread (bold) if threads have unread messages. ([\#9763](https://github.com/matrix-org/matrix-react-sdk/pull/9763)). Fixes vector-im/element-web#23907. - * Don't prefer STIXGeneral over the default font ([\#9711](https://github.com/matrix-org/matrix-react-sdk/pull/9711)). Fixes vector-im/element-web#23899. - * Use the same avatar colour when creating 1:1 DM rooms ([\#9850](https://github.com/matrix-org/matrix-react-sdk/pull/9850)). Fixes vector-im/element-web#23476. - * Fix space lock icon size ([\#9854](https://github.com/matrix-org/matrix-react-sdk/pull/9854)). Fixes vector-im/element-web#24128. - * Make calls automatically disconnect if the widget disappears ([\#9862](https://github.com/matrix-org/matrix-react-sdk/pull/9862)). Fixes vector-im/element-web#23664. - * Fix emoji in RTE editing ([\#9827](https://github.com/matrix-org/matrix-react-sdk/pull/9827)). - * Fix export with attachments on formats txt and json ([\#9851](https://github.com/matrix-org/matrix-react-sdk/pull/9851)). Fixes vector-im/element-web#24130. Contributed by @grimhilt. - * Fixed empty `Content-Type` for encrypted uploads ([\#9848](https://github.com/matrix-org/matrix-react-sdk/pull/9848)). Contributed by @K3das. - * Fix sign-in instead link on password reset page ([\#9820](https://github.com/matrix-org/matrix-react-sdk/pull/9820)). Fixes vector-im/element-web#24087. - * The seekbar now initially shows the current position ([\#9796](https://github.com/matrix-org/matrix-react-sdk/pull/9796)). Fixes vector-im/element-web#24051. - * Fix: Editing a poll will silently change it to a closed poll ([\#9809](https://github.com/matrix-org/matrix-react-sdk/pull/9809)). Fixes vector-im/element-web#23176. - * Make call tiles look less broken in the right panel ([\#9808](https://github.com/matrix-org/matrix-react-sdk/pull/9808)). Fixes vector-im/element-web#23716. - * Prevent unnecessary m.direct updates ([\#9805](https://github.com/matrix-org/matrix-react-sdk/pull/9805)). Fixes vector-im/element-web#24059. - * Fix checkForPreJoinUISI for thread roots ([\#9803](https://github.com/matrix-org/matrix-react-sdk/pull/9803)). Fixes vector-im/element-web#24054. - * Snap in PiP widget when content changed ([\#9797](https://github.com/matrix-org/matrix-react-sdk/pull/9797)). Fixes vector-im/element-web#24050. - * Load RTE components only when RTE labs is enabled ([\#9804](https://github.com/matrix-org/matrix-react-sdk/pull/9804)). - * Ensure that events are correctly updated when they are edited. ([\#9789](https://github.com/matrix-org/matrix-react-sdk/pull/9789)). - * When stopping a broadcast also stop the playback ([\#9795](https://github.com/matrix-org/matrix-react-sdk/pull/9795)). Fixes vector-im/element-web#24052. - * Prevent to start two broadcasts at the same time ([\#9744](https://github.com/matrix-org/matrix-react-sdk/pull/9744)). Fixes vector-im/element-web#23973. - * Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. - * Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216. - -Changes in [3.63.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.63.0) (2022-12-21) -===================================================================================================== - -## ✨ Features - * Prevent unnecessary m.direct updates ([\#9805](https://github.com/matrix-org/matrix-react-sdk/pull/9805)). Fixes vector-im/element-web#24059. - * Fix checkForPreJoinUISI for thread roots ([\#9803](https://github.com/matrix-org/matrix-react-sdk/pull/9803)). Fixes vector-im/element-web#24054. - * Add inline code formatting to rich text editor ([\#9720](https://github.com/matrix-org/matrix-react-sdk/pull/9720)). - * Add emoji handling for plain text mode of the new rich text editor ([\#9727](https://github.com/matrix-org/matrix-react-sdk/pull/9727)). - * Overlay virtual room call events into main timeline ([\#9626](https://github.com/matrix-org/matrix-react-sdk/pull/9626)). Fixes vector-im/element-web#22929. - * Adds a new section under "Room Settings" > "Roles & Permissions" which adds the possibility to multiselect users from this room and grant them more permissions. ([\#9596](https://github.com/matrix-org/matrix-react-sdk/pull/9596)). Contributed by @GoodGuyMarco. - * Add emoji handling for rich text mode ([\#9661](https://github.com/matrix-org/matrix-react-sdk/pull/9661)). - * Add setting to hide bold notifications ([\#9705](https://github.com/matrix-org/matrix-react-sdk/pull/9705)). - * Further password reset flow enhancements ([\#9662](https://github.com/matrix-org/matrix-react-sdk/pull/9662)). - * Snooze the bulk unverified sessions reminder on dismiss ([\#9706](https://github.com/matrix-org/matrix-react-sdk/pull/9706)). - * Honor advanced audio processing settings when recording voice messages ([\#9610](https://github.com/matrix-org/matrix-react-sdk/pull/9610)). Contributed by @MrAnno. - * Improve the visual balance of bubble layout ([\#9704](https://github.com/matrix-org/matrix-react-sdk/pull/9704)). - * Add config setting to disable bulk unverified sessions nag ([\#9657](https://github.com/matrix-org/matrix-react-sdk/pull/9657)). - * Only display bulk unverified sessions nag when current sessions is verified ([\#9656](https://github.com/matrix-org/matrix-react-sdk/pull/9656)). - * Separate labs and betas more clearly ([\#8969](https://github.com/matrix-org/matrix-react-sdk/pull/8969)). Fixes vector-im/element-web#22706. - * Show user an error if we fail to create a DM for verification. ([\#9624](https://github.com/matrix-org/matrix-react-sdk/pull/9624)). - -## 🐛 Bug Fixes - * Fix issue where thread panel did not update correctly ([\#9746](https://github.com/matrix-org/matrix-react-sdk/pull/9746)). Fixes vector-im/element-web#23971. - * Remove async call to get virtual room from room load ([\#9743](https://github.com/matrix-org/matrix-react-sdk/pull/9743)). Fixes vector-im/element-web#23968. - * Check each thread for unread messages. ([\#9723](https://github.com/matrix-org/matrix-react-sdk/pull/9723)). - * Device manage - handle sessions that don't support encryption ([\#9717](https://github.com/matrix-org/matrix-react-sdk/pull/9717)). Fixes vector-im/element-web#23722. - * Fix hover state for formatting buttons (Rich text editor) (fix vector-im/element-web/issues/23832) ([\#9715](https://github.com/matrix-org/matrix-react-sdk/pull/9715)). - * Don't allow group calls to be unterminated ([\#9710](https://github.com/matrix-org/matrix-react-sdk/pull/9710)). - * Fix replies to emotes not showing as inline ([\#9707](https://github.com/matrix-org/matrix-react-sdk/pull/9707)). Fixes vector-im/element-web#23903. - * Update copy of 'Change layout' button to match Element Call ([\#9703](https://github.com/matrix-org/matrix-react-sdk/pull/9703)). - * Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)). - * bugfix: fix an issue where the Notifier would incorrectly fire for non-timeline events ([\#9664](https://github.com/matrix-org/matrix-react-sdk/pull/9664)). Fixes vector-im/element-web#17263. - * Fix power selector being wrongly disabled for admins themselves ([\#9681](https://github.com/matrix-org/matrix-react-sdk/pull/9681)). Fixes vector-im/element-web#23882. - * Show day counts in call durations ([\#9641](https://github.com/matrix-org/matrix-react-sdk/pull/9641)). - -Changes in [3.62.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0) (2022-12-06) -===================================================================================================== - -## ✨ Features - * Further improve replies ([\#6396](https://github.com/matrix-org/matrix-react-sdk/pull/6396)). Fixes vector-im/element-web#19074, vector-im/element-web#18194 vector-im/element-web#18027 and vector-im/element-web#19179. - * Enable users to join group calls from multiple devices ([\#9625](https://github.com/matrix-org/matrix-react-sdk/pull/9625)). - * fix(visual): make cursor a pointer for summaries ([\#9419](https://github.com/matrix-org/matrix-react-sdk/pull/9419)). Contributed by @r00ster91. - * Add placeholder for rich text editor ([\#9613](https://github.com/matrix-org/matrix-react-sdk/pull/9613)). - * Consolidate public room search experience ([\#9605](https://github.com/matrix-org/matrix-react-sdk/pull/9605)). Fixes vector-im/element-web#22846. - * New password reset flow ([\#9581](https://github.com/matrix-org/matrix-react-sdk/pull/9581)). Fixes vector-im/element-web#23131. - * Device manager - add tooltip to device details toggle ([\#9594](https://github.com/matrix-org/matrix-react-sdk/pull/9594)). - * sliding sync: add lazy-loading member support ([\#9530](https://github.com/matrix-org/matrix-react-sdk/pull/9530)). - * Limit formatting bar offset to top of composer ([\#9365](https://github.com/matrix-org/matrix-react-sdk/pull/9365)). Fixes vector-im/element-web#12359. Contributed by @owi92. - -## 🐛 Bug Fixes - * Fix issues around up arrow event edit shortcut ([\#9645](https://github.com/matrix-org/matrix-react-sdk/pull/9645)). Fixes vector-im/element-web#18497 and vector-im/element-web#18964. - * Fix search not being cleared when clicking on a result ([\#9635](https://github.com/matrix-org/matrix-react-sdk/pull/9635)). Fixes vector-im/element-web#23845. - * Fix screensharing in 1:1 calls ([\#9612](https://github.com/matrix-org/matrix-react-sdk/pull/9612)). Fixes vector-im/element-web#23808. - * Fix the background color flashing when joining a call ([\#9640](https://github.com/matrix-org/matrix-react-sdk/pull/9640)). - * Fix the size of the 'Private space' icon ([\#9638](https://github.com/matrix-org/matrix-react-sdk/pull/9638)). - * Fix reply editing in rich text editor (https ([\#9615](https://github.com/matrix-org/matrix-react-sdk/pull/9615)). - * Fix thread list jumping back down while scrolling ([\#9606](https://github.com/matrix-org/matrix-react-sdk/pull/9606)). Fixes vector-im/element-web#23727. - * Fix regression with TimelinePanel props updates not taking effect ([\#9608](https://github.com/matrix-org/matrix-react-sdk/pull/9608)). Fixes vector-im/element-web#23794. - * Fix form tooltip positioning ([\#9598](https://github.com/matrix-org/matrix-react-sdk/pull/9598)). Fixes vector-im/element-web#22861. - * Extract Search handling from RoomView into its own Component ([\#9574](https://github.com/matrix-org/matrix-react-sdk/pull/9574)). Fixes vector-im/element-web#498. - * Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)). - * Fix replies to emotes not showing as inline ([\#9707](https://github.com/matrix-org/matrix-react-sdk/pull/9707)). Fixes vector-im/element-web#23903. - -Changes in [3.61.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.61.0) (2022-11-22) -===================================================================================================== - -## ✨ Features - * Make clear notifications work with threads ([\#9575](https://github.com/matrix-org/matrix-react-sdk/pull/9575)). Fixes vector-im/element-web#23751. - * Change "None" to "Off" in notification options ([\#9539](https://github.com/matrix-org/matrix-react-sdk/pull/9539)). Contributed by @Arnei. - * Advanced audio processing settings ([\#8759](https://github.com/matrix-org/matrix-react-sdk/pull/8759)). Fixes vector-im/element-web#6278. Contributed by @MrAnno. - * Add way to create a user notice via config.json ([\#9559](https://github.com/matrix-org/matrix-react-sdk/pull/9559)). - * Improve design of the rich text editor ([\#9533](https://github.com/matrix-org/matrix-react-sdk/pull/9533)). Contributed by @florianduros. - * Enable user to zoom beyond image size ([\#5949](https://github.com/matrix-org/matrix-react-sdk/pull/5949)). Contributed by @jaiwanth-v. - * Fix: Move "Leave Space" option to the bottom of space context menu ([\#9535](https://github.com/matrix-org/matrix-react-sdk/pull/9535)). Contributed by @hanadi92. - -## 🐛 Bug Fixes - * Fix integration manager `get_open_id_token` action and add E2E tests ([\#9520](https://github.com/matrix-org/matrix-react-sdk/pull/9520)). - * Fix links being mangled by markdown processing ([\#9570](https://github.com/matrix-org/matrix-react-sdk/pull/9570)). Fixes vector-im/element-web#23743. - * Fix: inline links selecting radio button ([\#9543](https://github.com/matrix-org/matrix-react-sdk/pull/9543)). Contributed by @hanadi92. - * fix wrong error message in registration when phone number threepid is in use. ([\#9571](https://github.com/matrix-org/matrix-react-sdk/pull/9571)). Contributed by @bagvand. - * Fix missing avatar for show current profiles ([\#9563](https://github.com/matrix-org/matrix-react-sdk/pull/9563)). Fixes vector-im/element-web#23733. - * fix read receipts trickling down correctly ([\#9567](https://github.com/matrix-org/matrix-react-sdk/pull/9567)). Fixes vector-im/element-web#23746. - * Resilience fix for homeserver without thread notification support ([\#9565](https://github.com/matrix-org/matrix-react-sdk/pull/9565)). - * Don't switch to the home page needlessly after leaving a room ([\#9477](https://github.com/matrix-org/matrix-react-sdk/pull/9477)). - * Differentiate download and decryption errors when showing images ([\#9562](https://github.com/matrix-org/matrix-react-sdk/pull/9562)). Fixes vector-im/element-web#3892. - * Close context menu when a modal is opened to prevent user getting stuck ([\#9560](https://github.com/matrix-org/matrix-react-sdk/pull/9560)). Fixes vector-im/element-web#15610 and vector-im/element-web#10781. - * Fix TimelineReset handling when no room associated ([\#9553](https://github.com/matrix-org/matrix-react-sdk/pull/9553)). - * Always use current profile on thread events ([\#9524](https://github.com/matrix-org/matrix-react-sdk/pull/9524)). Fixes vector-im/element-web#23648. - * Fix `ThreadView` tests not using thread flag ([\#9547](https://github.com/matrix-org/matrix-react-sdk/pull/9547)). Contributed by @MadLittleMods. - * Handle deletion of `m.call` events ([\#9540](https://github.com/matrix-org/matrix-react-sdk/pull/9540)). Fixes vector-im/element-web#23663. - * Fix incorrect notification count after leaving a room with notifications ([\#9518](https://github.com/matrix-org/matrix-react-sdk/pull/9518)). Contributed by @Arnei. - -Changes in [3.60.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.60.0) (2022-11-08) -===================================================================================================== - -## ✨ Features - * Loading threads with server-side assistance ([\#9356](https://github.com/matrix-org/matrix-react-sdk/pull/9356)). Fixes vector-im/element-web#21807, vector-im/element-web#21799, vector-im/element-web#21911, vector-im/element-web#22141, vector-im/element-web#22157, vector-im/element-web#22641, vector-im/element-web#22501 vector-im/element-web#22438 and vector-im/element-web#21678. Contributed by @justjanne. - * Make thread replies trigger a room list re-ordering ([\#9510](https://github.com/matrix-org/matrix-react-sdk/pull/9510)). Fixes vector-im/element-web#21700. - * Device manager - add extra details to device security and renaming ([\#9501](https://github.com/matrix-org/matrix-react-sdk/pull/9501)). Contributed by @kerryarchibald. - * Add plain text mode to the wysiwyg composer ([\#9503](https://github.com/matrix-org/matrix-react-sdk/pull/9503)). Contributed by @florianduros. - * Sliding Sync: improve sort order, show subspace rooms, better tombstoned room handling ([\#9484](https://github.com/matrix-org/matrix-react-sdk/pull/9484)). - * Device manager - add learn more popups to filtered sessions section ([\#9497](https://github.com/matrix-org/matrix-react-sdk/pull/9497)). Contributed by @kerryarchibald. - * Show thread notification if thread timeline is closed ([\#9495](https://github.com/matrix-org/matrix-react-sdk/pull/9495)). Fixes vector-im/element-web#23589. - * Add message editing to wysiwyg composer ([\#9488](https://github.com/matrix-org/matrix-react-sdk/pull/9488)). Contributed by @florianduros. - * Device manager - confirm sign out of other sessions ([\#9487](https://github.com/matrix-org/matrix-react-sdk/pull/9487)). Contributed by @kerryarchibald. - * Automatically request logs from other users in a call when submitting logs ([\#9492](https://github.com/matrix-org/matrix-react-sdk/pull/9492)). - * Add thread notification with server assistance (MSC3773) ([\#9400](https://github.com/matrix-org/matrix-react-sdk/pull/9400)). Fixes vector-im/element-web#21114, vector-im/element-web#21413, vector-im/element-web#21416, vector-im/element-web#21433, vector-im/element-web#21481, vector-im/element-web#21798, vector-im/element-web#21823 vector-im/element-web#23192 and vector-im/element-web#21765. - * Support for login + E2EE set up with QR ([\#9403](https://github.com/matrix-org/matrix-react-sdk/pull/9403)). Contributed by @hughns. - * Allow pressing Enter to send messages in new composer ([\#9451](https://github.com/matrix-org/matrix-react-sdk/pull/9451)). Contributed by @andybalaam. - -## 🐛 Bug Fixes - * Fix regressions around media uploads failing and causing soft crashes ([\#9549](https://github.com/matrix-org/matrix-react-sdk/pull/9549)). Fixes matrix-org/element-web-rageshakes#16831, matrix-org/element-web-rageshakes#16824 matrix-org/element-web-rageshakes#16810 and vector-im/element-web#23641. - * Fix /myroomavatar slash command ([\#9536](https://github.com/matrix-org/matrix-react-sdk/pull/9536)). Fixes matrix-org/synapse#14321. - * Fix NotificationBadge unsent color ([\#9522](https://github.com/matrix-org/matrix-react-sdk/pull/9522)). Fixes vector-im/element-web#23646. - * Fix room list sorted by recent on app startup ([\#9515](https://github.com/matrix-org/matrix-react-sdk/pull/9515)). Fixes vector-im/element-web#23635. - * Reset custom power selector when blurred on empty ([\#9508](https://github.com/matrix-org/matrix-react-sdk/pull/9508)). Fixes vector-im/element-web#23481. - * Reinstate timeline/redaction callbacks when updating notification state ([\#9494](https://github.com/matrix-org/matrix-react-sdk/pull/9494)). Fixes vector-im/element-web#23554. - * Only render NotificationBadge when needed ([\#9493](https://github.com/matrix-org/matrix-react-sdk/pull/9493)). Fixes vector-im/element-web#23584. - * Fix embedded Element Call screen sharing ([\#9485](https://github.com/matrix-org/matrix-react-sdk/pull/9485)). Fixes vector-im/element-web#23571. - * Send Content-Type: application/json header for integration manager /register API ([\#9490](https://github.com/matrix-org/matrix-react-sdk/pull/9490)). Fixes vector-im/element-web#23580. - * Fix joining calls without audio or video inputs ([\#9486](https://github.com/matrix-org/matrix-react-sdk/pull/9486)). Fixes vector-im/element-web#23511. - * Ensure spaces in the spotlight dialog have rounded square avatars ([\#9480](https://github.com/matrix-org/matrix-react-sdk/pull/9480)). Fixes vector-im/element-web#23515. - * Only show mini avatar uploader in room intro when no avatar yet exists ([\#9479](https://github.com/matrix-org/matrix-react-sdk/pull/9479)). Fixes vector-im/element-web#23552. - * Fix threads fallback incorrectly targets root event ([\#9229](https://github.com/matrix-org/matrix-react-sdk/pull/9229)). Fixes vector-im/element-web#23147. - * Align video call icon with banner text ([\#9460](https://github.com/matrix-org/matrix-react-sdk/pull/9460)). - * Set relations helper when creating event tile context menu ([\#9253](https://github.com/matrix-org/matrix-react-sdk/pull/9253)). Fixes vector-im/element-web#22018. - * Device manager - put client/browser device metadata in correct section ([\#9447](https://github.com/matrix-org/matrix-react-sdk/pull/9447)). Contributed by @kerryarchibald. - * Update the room unread notification counter when the server changes the value without any related read receipt ([\#9438](https://github.com/matrix-org/matrix-react-sdk/pull/9438)). - -Changes in [3.59.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.59.1) (2022-11-01) -===================================================================================================== - -## 🐛 Bug Fixes - * Fix default behavior of Room.getBlacklistUnverifiedDevices ([\#2830](https://github.com/matrix-org/matrix-js-sdk/pull/2830)). Contributed by @duxovni. - * Catch server versions API call exception when starting the client ([\#2828](https://github.com/matrix-org/matrix-js-sdk/pull/2828)). Fixes vector-im/element-web#23634. - * Fix authedRequest including `Authorization: Bearer undefined` for password resets ([\#2822](https://github.com/matrix-org/matrix-js-sdk/pull/2822)). Fixes vector-im/element-web#23655. - -Changes in [3.59.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.59.0) (2022-10-25) -===================================================================================================== - -## ✨ Features - * Include a file-safe room name and ISO date in chat exports ([\#9440](https://github.com/matrix-org/matrix-react-sdk/pull/9440)). Fixes vector-im/element-web#21812 and vector-im/element-web#19724. - * Room call banner ([\#9378](https://github.com/matrix-org/matrix-react-sdk/pull/9378)). Fixes vector-im/element-web#23453. Contributed by @toger5. - * Device manager - spinners while devices are signing out ([\#9433](https://github.com/matrix-org/matrix-react-sdk/pull/9433)). Fixes vector-im/element-web#15865. - * Device manager - silence call ringers when local notifications are silenced ([\#9420](https://github.com/matrix-org/matrix-react-sdk/pull/9420)). - * Pass the current language to Element Call ([\#9427](https://github.com/matrix-org/matrix-react-sdk/pull/9427)). - * Hide screen-sharing button in Element Call on desktop ([\#9423](https://github.com/matrix-org/matrix-react-sdk/pull/9423)). - * Add reply support to WysiwygComposer ([\#9422](https://github.com/matrix-org/matrix-react-sdk/pull/9422)). Contributed by @florianduros. - * Disconnect other connected devices (of the same user) when joining an Element call ([\#9379](https://github.com/matrix-org/matrix-react-sdk/pull/9379)). - * Device manager - device tile main click target ([\#9409](https://github.com/matrix-org/matrix-react-sdk/pull/9409)). - * Add formatting buttons to the rich text editor ([\#9410](https://github.com/matrix-org/matrix-react-sdk/pull/9410)). Contributed by @florianduros. - * Device manager - current session context menu ([\#9386](https://github.com/matrix-org/matrix-react-sdk/pull/9386)). - * Remove piwik config fallback for privacy policy URL ([\#9390](https://github.com/matrix-org/matrix-react-sdk/pull/9390)). - * Add the first step to integrate the matrix wysiwyg composer ([\#9374](https://github.com/matrix-org/matrix-react-sdk/pull/9374)). Contributed by @florianduros. - * Device manager - UA parsing tweaks ([\#9382](https://github.com/matrix-org/matrix-react-sdk/pull/9382)). - * Device manager - remove client information events when disabling setting ([\#9384](https://github.com/matrix-org/matrix-react-sdk/pull/9384)). - * Add Element Call participant limit ([\#9358](https://github.com/matrix-org/matrix-react-sdk/pull/9358)). - * Add Element Call room settings ([\#9347](https://github.com/matrix-org/matrix-react-sdk/pull/9347)). - * Device manager - render extended device information ([\#9360](https://github.com/matrix-org/matrix-react-sdk/pull/9360)). - * New group call experience: Room header and PiP designs ([\#9351](https://github.com/matrix-org/matrix-react-sdk/pull/9351)). - * Pass language to Jitsi Widget ([\#9346](https://github.com/matrix-org/matrix-react-sdk/pull/9346)). Contributed by @Fox32. - * Add notifications and toasts for Element Call calls ([\#9337](https://github.com/matrix-org/matrix-react-sdk/pull/9337)). - * Device manager - device type icon ([\#9355](https://github.com/matrix-org/matrix-react-sdk/pull/9355)). - * Delete the remainder of groups ([\#9357](https://github.com/matrix-org/matrix-react-sdk/pull/9357)). Fixes vector-im/element-web#22770. - * Device manager - display client information in device details ([\#9315](https://github.com/matrix-org/matrix-react-sdk/pull/9315)). - -## 🐛 Bug Fixes - * Send Content-Type: application/json header for integration manager /register API ([\#9490](https://github.com/matrix-org/matrix-react-sdk/pull/9490)). Fixes vector-im/element-web#23580. - * Device manager - put client/browser device metadata in correct section ([\#9447](https://github.com/matrix-org/matrix-react-sdk/pull/9447)). - * update the room unread notification counter when the server changes the value without any related read receipt ([\#9438](https://github.com/matrix-org/matrix-react-sdk/pull/9438)). - * Don't show call banners in video rooms ([\#9441](https://github.com/matrix-org/matrix-react-sdk/pull/9441)). - * Prevent useContextMenu isOpen from being true if the button ref goes away ([\#9418](https://github.com/matrix-org/matrix-react-sdk/pull/9418)). Fixes matrix-org/element-web-rageshakes#15637. - * Automatically focus the WYSIWYG composer when you enter a room ([\#9412](https://github.com/matrix-org/matrix-react-sdk/pull/9412)). - * Improve the tooltips on the call lobby join button ([\#9428](https://github.com/matrix-org/matrix-react-sdk/pull/9428)). - * Pass the homeserver's base URL to Element Call ([\#9429](https://github.com/matrix-org/matrix-react-sdk/pull/9429)). Fixes vector-im/element-web#23301. - * Better accommodate long room names in call toasts ([\#9426](https://github.com/matrix-org/matrix-react-sdk/pull/9426)). - * Hide virtual widgets from the room info panel ([\#9424](https://github.com/matrix-org/matrix-react-sdk/pull/9424)). Fixes vector-im/element-web#23494. - * Inhibit clicking on sender avatar in threads list ([\#9417](https://github.com/matrix-org/matrix-react-sdk/pull/9417)). Fixes vector-im/element-web#23482. - * Correct the dir parameter of MSC3715 ([\#9391](https://github.com/matrix-org/matrix-react-sdk/pull/9391)). Contributed by @dhenneke. - * Use a more correct subset of users in `/remakeolm` developer command ([\#9402](https://github.com/matrix-org/matrix-react-sdk/pull/9402)). - * use correct default for notification silencing ([\#9388](https://github.com/matrix-org/matrix-react-sdk/pull/9388)). Fixes vector-im/element-web#23456. - * Device manager - eagerly create `m.local_notification_settings` events ([\#9353](https://github.com/matrix-org/matrix-react-sdk/pull/9353)). - * Close incoming Element call toast when viewing the call lobby ([\#9375](https://github.com/matrix-org/matrix-react-sdk/pull/9375)). - * Always allow enabling sending read receipts ([\#9367](https://github.com/matrix-org/matrix-react-sdk/pull/9367)). Fixes vector-im/element-web#23433. - * Fixes (vector-im/element-web/issues/22609) where the white theme is not applied when `white -> dark -> white` sequence is done. ([\#9320](https://github.com/matrix-org/matrix-react-sdk/pull/9320)). Contributed by @florianduros. - * Fix applying programmatically set height for "top" room layout ([\#9339](https://github.com/matrix-org/matrix-react-sdk/pull/9339)). Contributed by @Fox32. - -Changes in [3.58.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.1) (2022-10-11) -===================================================================================================== - -## 🐛 Bug Fixes - * Use correct default for notification silencing ([\#9388](https://github.com/matrix-org/matrix-react-sdk/pull/9388)). Fixes vector-im/element-web#23456. - -Changes in [3.58.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0) (2022-10-11) -=============================================================================================================== - -## Deprecations - * Legacy Piwik config.json option `piwik.policy_url` is deprecated in favour of `privacy_policy_url`. Support will be removed in the next release. - -## ✨ Features - * Device manager - select all devices ([\#9330](https://github.com/matrix-org/matrix-react-sdk/pull/9330)). - * New group call experience: Call tiles ([\#9332](https://github.com/matrix-org/matrix-react-sdk/pull/9332)). - * Add Shift key to FormatQuote keyboard shortcut ([\#9298](https://github.com/matrix-org/matrix-react-sdk/pull/9298)). Contributed by @owi92. - * Device manager - sign out of multiple sessions ([\#9325](https://github.com/matrix-org/matrix-react-sdk/pull/9325)). - * Display push toggle for web sessions (MSC3890) ([\#9327](https://github.com/matrix-org/matrix-react-sdk/pull/9327)). - * Add device notifications enabled switch ([\#9324](https://github.com/matrix-org/matrix-react-sdk/pull/9324)). - * Implement push notification toggle in device detail ([\#9308](https://github.com/matrix-org/matrix-react-sdk/pull/9308)). - * New group call experience: Starting and ending calls ([\#9318](https://github.com/matrix-org/matrix-react-sdk/pull/9318)). - * New group call experience: Room header call buttons ([\#9311](https://github.com/matrix-org/matrix-react-sdk/pull/9311)). - * Make device ID copyable in device list ([\#9297](https://github.com/matrix-org/matrix-react-sdk/pull/9297)). - * Use display name instead of user ID when rendering power events ([\#9295](https://github.com/matrix-org/matrix-react-sdk/pull/9295)). - * Read receipts for threads ([\#9239](https://github.com/matrix-org/matrix-react-sdk/pull/9239)). Fixes vector-im/element-web#23191. - -## 🐛 Bug Fixes - * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. - * Fix device selection in pre-join screen for Element Call video rooms ([\#9321](https://github.com/matrix-org/matrix-react-sdk/pull/9321)). Fixes vector-im/element-web#23331. - * Don't render a 1px high room topic if the room topic is empty ([\#9317](https://github.com/matrix-org/matrix-react-sdk/pull/9317)). Contributed by @Arnei. - * Don't show feedback prompts when that UIFeature is disabled ([\#9305](https://github.com/matrix-org/matrix-react-sdk/pull/9305)). Fixes vector-im/element-web#23327. - * Fix soft crash around unknown room pills ([\#9301](https://github.com/matrix-org/matrix-react-sdk/pull/9301)). Fixes matrix-org/element-web-rageshakes#15465. - * Fix spaces feedback prompt wrongly showing when feedback is disabled ([\#9302](https://github.com/matrix-org/matrix-react-sdk/pull/9302)). Fixes vector-im/element-web#23314. - * Fix tile soft crash in ReplyInThreadButton ([\#9300](https://github.com/matrix-org/matrix-react-sdk/pull/9300)). Fixes matrix-org/element-web-rageshakes#15493. - -Changes in [3.57.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.57.0) (2022-09-28) -===================================================================================================== - -## 🐛 Bug Fixes - * Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)). - -Changes in [3.56.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.56.0) (2022-09-28) -===================================================================================================== - -## 🔒 Security -* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249) -* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250) -* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251) -* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236) - -Changes in [3.55.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.55.0) (2022-09-20) -=============================================================================================================== - -## ✨ Features - * Element Call video rooms ([\#9267](https://github.com/matrix-org/matrix-react-sdk/pull/9267)). - * Device manager - rename session ([\#9282](https://github.com/matrix-org/matrix-react-sdk/pull/9282)). - * Allow widgets to read related events ([\#9210](https://github.com/matrix-org/matrix-react-sdk/pull/9210)). Contributed by @dhenneke. - * Device manager - logout of other session ([\#9280](https://github.com/matrix-org/matrix-react-sdk/pull/9280)). - * Device manager - logout current session ([\#9275](https://github.com/matrix-org/matrix-react-sdk/pull/9275)). - * Device manager - verify other devices ([\#9274](https://github.com/matrix-org/matrix-react-sdk/pull/9274)). - * Allow integration managers to remove users ([\#9211](https://github.com/matrix-org/matrix-react-sdk/pull/9211)). - * Device manager - add verify current session button ([\#9252](https://github.com/matrix-org/matrix-react-sdk/pull/9252)). - * Add NotifPanel dot back. ([\#9242](https://github.com/matrix-org/matrix-react-sdk/pull/9242)). Fixes vector-im/element-web#17641. - * Implement MSC3575: Sliding Sync ([\#8328](https://github.com/matrix-org/matrix-react-sdk/pull/8328)). - * Add the clipboard read permission for widgets ([\#9250](https://github.com/matrix-org/matrix-react-sdk/pull/9250)). Contributed by @stefanmuhle. - -## 🐛 Bug Fixes - * Make autocomplete pop-up wider in thread view ([\#9289](https://github.com/matrix-org/matrix-react-sdk/pull/9289)). - * Fix soft crash around inviting invalid MXIDs in start DM on first message flow ([\#9281](https://github.com/matrix-org/matrix-react-sdk/pull/9281)). Fixes matrix-org/element-web-rageshakes#15060 and matrix-org/element-web-rageshakes#15140. - * Fix in-reply-to previews not disappearing when swapping rooms ([\#9278](https://github.com/matrix-org/matrix-react-sdk/pull/9278)). - * Fix invalid instanceof operand window.OffscreenCanvas ([\#9276](https://github.com/matrix-org/matrix-react-sdk/pull/9276)). Fixes vector-im/element-web#23275. - * Fix memory leak caused by unremoved listener ([\#9273](https://github.com/matrix-org/matrix-react-sdk/pull/9273)). - * Fix thumbnail generation when offscreen canvas fails ([\#9272](https://github.com/matrix-org/matrix-react-sdk/pull/9272)). Fixes vector-im/element-web#23265. - * Prevent sliding sync from showing a room under multiple sublists ([\#9266](https://github.com/matrix-org/matrix-react-sdk/pull/9266)). - * Fix tile crash around tooltipify links ([\#9270](https://github.com/matrix-org/matrix-react-sdk/pull/9270)). Fixes vector-im/element-web#23253. - * Device manager - filter out nulled metadatas in device tile properly ([\#9251](https://github.com/matrix-org/matrix-react-sdk/pull/9251)). - * Fix a sliding sync bug which could cause rooms to loop ([\#9268](https://github.com/matrix-org/matrix-react-sdk/pull/9268)). - * Remove the grey gradient on images in bubbles in the timeline ([\#9241](https://github.com/matrix-org/matrix-react-sdk/pull/9241)). Fixes vector-im/element-web#21651. - * Fix html export not including images ([\#9260](https://github.com/matrix-org/matrix-react-sdk/pull/9260)). Fixes vector-im/element-web#22059. - * Fix possible soft crash from a race condition in space hierarchies ([\#9254](https://github.com/matrix-org/matrix-react-sdk/pull/9254)). Fixes matrix-org/element-web-rageshakes#15225. - * Disable all types of autocorrect, -complete, -capitalize, etc on Spotlight's search field ([\#9259](https://github.com/matrix-org/matrix-react-sdk/pull/9259)). - * Handle M_INVALID_USERNAME on /register/available ([\#9237](https://github.com/matrix-org/matrix-react-sdk/pull/9237)). Fixes vector-im/element-web#23161. - * Fix issue with quiet zone around QR code ([\#9243](https://github.com/matrix-org/matrix-react-sdk/pull/9243)). Fixes vector-im/element-web#23199. - -Changes in [3.54.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.54.0) (2022-09-13) -===================================================================================================== - -## ✨ Features - * Device manager - hide unverified security recommendation when only current session is unverified ([\#9228](https://github.com/matrix-org/matrix-react-sdk/pull/9228)). Contributed by @kerryarchibald. - * Device manager - scroll to filtered list from security recommendations ([\#9227](https://github.com/matrix-org/matrix-react-sdk/pull/9227)). Contributed by @kerryarchibald. - * Device manager - updated dropdown style in filtered device list ([\#9226](https://github.com/matrix-org/matrix-react-sdk/pull/9226)). Contributed by @kerryarchibald. - * Device manager - device type and verification icons on device tile ([\#9197](https://github.com/matrix-org/matrix-react-sdk/pull/9197)). Contributed by @kerryarchibald. - -## 🐛 Bug Fixes - * Description of DM room with more than two other people is now being displayed correctly ([\#9231](https://github.com/matrix-org/matrix-react-sdk/pull/9231)). Fixes vector-im/element-web#23094. - * Fix voice messages with multiple composers ([\#9208](https://github.com/matrix-org/matrix-react-sdk/pull/9208)). Fixes vector-im/element-web#23023. Contributed by @grimhilt. - * Fix suggested rooms going missing ([\#9236](https://github.com/matrix-org/matrix-react-sdk/pull/9236)). Fixes vector-im/element-web#23190. - * Fix tooltip infinitely recursing ([\#9235](https://github.com/matrix-org/matrix-react-sdk/pull/9235)). Fixes matrix-org/element-web-rageshakes#15107, matrix-org/element-web-rageshakes#15093 matrix-org/element-web-rageshakes#15092 and matrix-org/element-web-rageshakes#15077. - * Fix plain text export saving ([\#9230](https://github.com/matrix-org/matrix-react-sdk/pull/9230)). Contributed by @jryans. - * Add missing space in SecurityRoomSettingsTab ([\#9222](https://github.com/matrix-org/matrix-react-sdk/pull/9222)). Contributed by @gefgu. - * Make use of js-sdk roomNameGenerator to handle i18n for generated room names ([\#9209](https://github.com/matrix-org/matrix-react-sdk/pull/9209)). Fixes vector-im/element-web#21369. - * Fix progress bar regression throughout the app ([\#9219](https://github.com/matrix-org/matrix-react-sdk/pull/9219)). Fixes vector-im/element-web#23121. - * Reuse empty string & space string logic for event types in devtools ([\#9218](https://github.com/matrix-org/matrix-react-sdk/pull/9218)). Fixes vector-im/element-web#23115. - +<<<<<<< HEAD Changes in [3.53.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.53.0) (2022-08-31) ===================================================================================================== @@ -875,6 +76,11 @@ Changes in [3.52.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/ * Use default styling on nested numbered lists due to MD being sensitive ([\#9110](https://github.com/matrix-org/matrix-react-sdk/pull/9110)). Fixes vector-im/element-web#22935. * Fix replying using chat effect commands ([\#9101](https://github.com/matrix-org/matrix-react-sdk/pull/9101)). Fixes vector-im/element-web#22824. * The first message in a DM can no longer be a sticker. This has been changed to avoid issues with the integration manager. ([\#9180](https://github.com/matrix-org/matrix-react-sdk/pull/9180)). +======= +Added ability to upload room-specific custom emotes in Room Settings. These show up in the room's messages when the shortcode is in the message. +The file fixes were a local issue in which I had to copy the correct version of the files from another folder. Not a part of the emote feature. +Currently emotes are not encrypted and do not show up in autocomplete or the right side emoji panel. I think this could be a start for fully implementing custom emotes. +>>>>>>> 8a422d88e6 (edits to changelog) Changes in [3.51.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.51.0) (2022-08-02) ===================================================================================================== diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index d3ec103b906..2f4079be56c 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -582,6 +582,7 @@ export default class TextualBody extends React.Component { let body: ReactNode; const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); + //TODO: Decrypt emotes if encryption is added let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let finalEmotes = {}; @@ -622,8 +623,6 @@ export default class TextualBody extends React.Component { }); } - //console.log(body); - //body.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (this.props.replacingEventId) { body = ( <> @@ -666,8 +665,6 @@ export default class TextualBody extends React.Component { ); } - //console.log(body.props.children); - //.replace(/:[\w+-]+:/, m => finalEmotes[m] ? finalEmotes[m] : m) if (isEmote) { return (
    diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 7d9615cf2e5..eecc98a94ef 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -55,23 +55,15 @@ export default class RoomEmoteSettings extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); - + //TODO: Decrypt the shortcodes and emotes if they are encrypted let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - //console.log(room.currentState); let emotes: Dictionary; emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let value = {}; for (let emote in emotes) { value[emote] = emote; } - //TODO: Decrypt the shortcodes and emotes if they are encrypted - // if (emotes) { - // console.log(room.roomId); - // console.log(room.name); - // console.log(emotes); - // } - //if (avatarUrl) avatarUrl = mediaFromMxc(avatarUrl).getSquareThumbnailHttp(96); - //emotes={} + this.state = { emotes: emotes, EmoteFieldsTouched: {}, @@ -128,7 +120,6 @@ export default class RoomEmoteSettings extends React.Component { let cleanemotes = {} let deletedItems = this.state.deletedItems; let value = {} - //console.log(e.currentTarget.getAttribute("name")); let id = e.currentTarget.getAttribute("id") for (let emote in this.state.emotes) { if (emote != id) { @@ -141,13 +132,6 @@ export default class RoomEmoteSettings extends React.Component { } this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }) - // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { - // field.setAttribute("value",(field as HTMLInputElement).id); - // field.setAttribute("defaultValue",(field as HTMLInputElement).id); - // }) - // for(let DOMid in this.state.emotes){ - // document.getElementById(DOMid).setAttribute("value",DOMid); - // } return; } @@ -163,7 +147,6 @@ export default class RoomEmoteSettings extends React.Component { // TODO: What do we do about errors? if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { - //const emotes = await client.uploadContent(this.state.emotes); //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { const newEmote = await client.uploadContent(this.state.newEmoteFile); @@ -178,12 +161,6 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; - - // document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { - // if((field as HTMLInputElement).name===shortcode){ - // (field as HTMLInputElement).name= this.state.EmoteFieldsTouched[shortcode]; - // } - // }) } else { @@ -193,7 +170,6 @@ export default class RoomEmoteSettings extends React.Component { }; } - //console.log(emotesMxcs); newState.value = value; await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); this.emoteUpload.current.value = ""; @@ -205,21 +181,13 @@ export default class RoomEmoteSettings extends React.Component { newState.deleted = false; newState.deletedItems = {}; - /*newState.avatarUrl = mediaFromMxc(uri).getSquareThumbnailHttp(96); - newState.originalAvatarUrl = newState.avatarUrl; - newState.avatarFile = null;*/ - } /*else if (this.state.originalAvatarUrl !== this.state.avatarUrl) { - await client.sendStateEvent(this.props.roomId, 'm.room.avatar', {}, ''); - }*/ + } this.setState(newState as IState); }; private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); - //e.target.setAttribute("value", e.target.value); - //const newEmotes = { ...this.state.emotes, [value]: value }; - //let newState=this.state.emotes; let b = this.state.value b[id] = e.target.value; this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); @@ -306,7 +274,7 @@ export default class RoomEmoteSettings extends React.Component { />
    Date: Sat, 13 Aug 2022 00:12:45 -0400 Subject: [PATCH 053/176] fixed bug with User Appearance Settings Tab caused by changes in TextualBody.tsx --- src/components/views/messages/TextualBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 2f4079be56c..c97dec78fcc 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -583,7 +583,7 @@ export default class TextualBody extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); //TODO: Decrypt emotes if encryption is added - let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; let finalEmotes = {}; for (let key in rawEmotes) { From 1fc4b9fe8969880aed496292d4ec68820164eefe Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Sat, 13 Aug 2022 12:50:37 -0400 Subject: [PATCH 054/176] added autocomplete custom emote functionality --- src/autocomplete/Components.tsx | 8 ++++---- src/autocomplete/EmojiProvider.tsx | 32 ++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index b3ded3f76ad..f34b90c421b 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -69,10 +69,10 @@ export const PillCompletion = forwardRef((props, ref) aria-selected={ariaSelectedAttribute} ref={ref} > - {children} - {title} - {subtitle} - {description} + { children } + {} + { subtitle } + { description }
    ); }); diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index bad4477768e..dcf45cf0444 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -33,6 +33,7 @@ import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji"; import { TimelineRenderingType } from "../contexts/RoomContext"; import * as recent from "../emojipicker/recent"; import { filterBoolean } from "../utils/arrays"; +import { mediaFromMxc } from "../customisations/Media"; const LIMIT = 20; @@ -80,9 +81,15 @@ export default class EmojiProvider extends AutocompleteProvider { public matcher: QueryMatcher; public nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - - public constructor(room: Room, renderingType?: TimelineRenderingType) { + emotes:Dictionary; + constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); + let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + this.emotes = {}; + for (let key in rawEmotes) { + this.emotes[key] = ""; + } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [(o) => o.emoji.shortcodes.map((s) => `:${s}:`)], @@ -107,8 +114,21 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } - - let completions: ISortedEmoji[] = []; + let emojisAndEmotes=[...SORTED_EMOJI]; + for(let key in this.emotes){ + emojisAndEmotes.push({ + emoji:{label:key, + shortcodes:[this.emotes[key]], + hexcode:"", + unicode:":"+key+":", + + }, + _orderBy:0 + }) + } + this.matcher.setObjects(emojisAndEmotes); + this.nameMatcher.setObjects(emojisAndEmotes); + let completions = []; const { command, range } = this.getCurrentCommand(query, selection); if (command && command[0].length > 2) { @@ -165,8 +185,8 @@ export default class EmojiProvider extends AutocompleteProvider { return completions.map((c) => ({ completion: c.emoji.unicode, component: ( - - {c.emoji.unicode} + + { this.emotes[c.emoji.shortcodes[0]]? this.emotes[c.emoji.shortcodes[0]]:c.emoji.unicode } ), range: range!, From b740d1a9419d6a6aa6061a6c883e2294bbfea042 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 17:08:12 -0400 Subject: [PATCH 055/176] Removing merge conflict blocks and fixing some formatting --- CHANGELOG.md | 3 - src/HtmlUtils.tsx | 4 +- .../views/dialogs/RoomSettingsDialog.tsx | 83 +++++++------------ .../views/room_settings/RoomEmoteSettings.tsx | 4 +- 4 files changed, 36 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a39978ff74..10666fc797a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ -<<<<<<< HEAD Changes in [3.53.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.53.0) (2022-08-31) ===================================================================================================== @@ -76,11 +75,9 @@ Changes in [3.52.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/ * Use default styling on nested numbered lists due to MD being sensitive ([\#9110](https://github.com/matrix-org/matrix-react-sdk/pull/9110)). Fixes vector-im/element-web#22935. * Fix replying using chat effect commands ([\#9101](https://github.com/matrix-org/matrix-react-sdk/pull/9101)). Fixes vector-im/element-web#22824. * The first message in a DM can no longer be a sticker. This has been changed to avoid issues with the integration manager. ([\#9180](https://github.com/matrix-org/matrix-react-sdk/pull/9180)). -======= Added ability to upload room-specific custom emotes in Room Settings. These show up in the room's messages when the shortcode is in the message. The file fixes were a local issue in which I had to copy the correct version of the files from another folder. Not a part of the emote feature. Currently emotes are not encrypted and do not show up in autocomplete or the right side emoji panel. I think this could be a start for fully implementing custom emotes. ->>>>>>> 8a422d88e6 (edits to changelog) Changes in [3.51.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.51.0) (2022-08-02) ===================================================================================================== diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 4a045f81534..7203e78d5db 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -575,11 +575,11 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } let contentBody = safeBody ?? strippedBody; - contentBody=contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + contentBody = contentBody.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); if (opts.returnString) { return contentBody; } - + let emojiBody = false; if (!opts.disableBigEmoji && bodyHasEmoji) { const contentBody = safeBody ?? strippedBody; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 597b51fda96..d527b740850 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -121,57 +121,38 @@ class RoomSettingsDialog extends React.Component { private getTabs(): NonEmptyArray> { const tabs: Tab[] = []; - tabs.push( - new Tab( - RoomSettingsTab.General, - _td("General"), - "mx_RoomSettingsDialog_settingsIcon", - , - "RoomSettingsGeneral", - ), - ); - if (SettingsStore.getValue("feature_group_calls")) { - tabs.push( - new Tab( - RoomSettingsTab.Voip, - _td("Voice & Video"), - "mx_RoomSettingsDialog_voiceIcon", - , - ), - ); - } - tabs.push( - new Tab( - RoomSettingsTab.Security, - _td("Security & Privacy"), - "mx_RoomSettingsDialog_securityIcon", - this.props.onFinished(true)} />, - "RoomSettingsSecurityPrivacy", - ), - ); - tabs.push( - new Tab( - RoomSettingsTab.Roles, - _td("Roles & Permissions"), - "mx_RoomSettingsDialog_rolesIcon", - , - "RoomSettingsRolesPermissions", - ), - ); - tabs.push( - new Tab( - RoomSettingsTab.Notifications, - _td("Notifications"), - "mx_RoomSettingsDialog_notificationsIcon", - ( - this.props.onFinished(true)} - /> - ), - "RoomSettingsNotifications", - ), - ); + tabs.push(new Tab( + ROOM_GENERAL_TAB, + _td("General"), + "mx_RoomSettingsDialog_settingsIcon", + , + "RoomSettingsGeneral", + )); + tabs.push(new Tab( + ROOM_SECURITY_TAB, + _td("Security & Privacy"), + "mx_RoomSettingsDialog_securityIcon", + this.props.onFinished(true)} + />, + "RoomSettingsSecurityPrivacy", + )); + tabs.push(new Tab( + ROOM_ROLES_TAB, + _td("Roles & Permissions"), + "mx_RoomSettingsDialog_rolesIcon", + , + "RoomSettingsRolesPermissions", + )); + tabs.push(new Tab( + ROOM_NOTIFICATIONS_TAB, + _td("Notifications"), + "mx_RoomSettingsDialog_notificationsIcon", + this.props.onFinished(true)} />, + "RoomSettingsNotifications", + )); + tabs.push(new Tab( ROOM_NOTIFICATIONS_TAB, _td("Emotes"), diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index eecc98a94ef..2c977f26c18 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -63,7 +63,7 @@ export default class RoomEmoteSettings extends React.Component { for (let emote in emotes) { value[emote] = emote; } - + this.state = { emotes: emotes, EmoteFieldsTouched: {}, @@ -181,7 +181,7 @@ export default class RoomEmoteSettings extends React.Component { newState.deleted = false; newState.deletedItems = {}; - } + } this.setState(newState as IState); }; From 9659d79479dcd0eda9d41a561f4ea716c7a91fae Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 17:36:43 -0400 Subject: [PATCH 056/176] some lint fixes --- res/css/views/rooms/_EventTile.pcss | 2 +- .../views/room_settings/RoomEmoteSettings.tsx | 75 +++++++++---------- .../settings/tabs/room/EmoteSettingsTab.tsx | 17 ----- 3 files changed, 36 insertions(+), 58 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 77b505a7b4a..29b0e24ae4d 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -826,7 +826,7 @@ $left-gutter: 64px; font-size: inherit !important; } } -.mx_Emote{ +.mx_Emote { height: 30px; } .mx_EventTile_e2eIcon { diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 2c977f26c18..5385ee471bc 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -15,17 +15,13 @@ limitations under the License. */ import React, { createRef } from 'react'; -import classNames from "classnames"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Field from "../elements/Field"; import { mediaFromMxc } from "../../../customisations/Media"; import AccessibleButton from "../elements/AccessibleButton"; -import AvatarSetting from "../settings/AvatarSetting"; -import { htmlSerializeFromMdIfNeeded } from '../../../editor/serialize'; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; -import { string } from 'prop-types'; interface IProps { roomId: string; @@ -34,10 +30,10 @@ interface IProps { interface IState { emotes: Dictionary; EmoteFieldsTouched: Record; - newEmoteFileAdded: boolean, - newEmoteCodeAdded: boolean, - newEmoteCode: string, - newEmoteFile: File, + newEmoteFileAdded: boolean; + newEmoteCodeAdded: boolean; + newEmoteCode: string; + newEmoteFile: File; canAddEmote: boolean; deleted: boolean; deletedItems: Dictionary; @@ -56,10 +52,10 @@ export default class RoomEmoteSettings extends React.Component { const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); //TODO: Decrypt the shortcodes and emotes if they are encrypted - let emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - let emotes: Dictionary; - emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let value = {}; + const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + + const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const value = {}; for (let emote in emotes) { value[emote] = emote; } @@ -84,22 +80,24 @@ export default class RoomEmoteSettings extends React.Component { private isSaveEnabled = () => { - return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || this.state.deleted; + return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || + (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || + (this.state.deleted); }; private cancelEmoteChanges = async (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); - let value = {}; + const value = {}; if (this.state.deleted) { - for (let key in this.state.deletedItems) { + for (const key in this.state.deletedItems) { this.state.emotes[key] = this.state.deletedItems[key]; value[key] = key; } } document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { value[(field as HTMLInputElement).id] = (field as HTMLInputElement).id; - }) + }); if (!this.isSaveEnabled()) return; this.setState({ EmoteFieldsTouched: {}, @@ -117,17 +115,17 @@ export default class RoomEmoteSettings extends React.Component { private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); - let cleanemotes = {} - let deletedItems = this.state.deletedItems; - let value = {} - let id = e.currentTarget.getAttribute("id") - for (let emote in this.state.emotes) { + const cleanemotes = {}; + const deletedItems = this.state.deletedItems; + const value = {}; + const id = e.currentTarget.getAttribute("id"); + for (const emote in this.state.emotes) { if (emote != id) { - cleanemotes[emote] = this.state.emotes[emote] - value[emote] = emote + cleanemotes[emote] = this.state.emotes[emote]; + value[emote] = emote; } else { - deletedItems[emote] = this.state.emotes[emote] + deletedItems[emote] = this.state.emotes[emote]; } } @@ -154,7 +152,7 @@ export default class RoomEmoteSettings extends React.Component { value[this.state.newEmoteCode] = this.state.newEmoteCode; } if (this.state.emotes) { - for (let shortcode in this.state.emotes) { + for (const shortcode in this.state.emotes) { if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) && (shortcode === this.state.newEmoteCode)) { continue; } @@ -257,8 +255,7 @@ export default class RoomEmoteSettings extends React.Component { {_t("Save")}
    - ); - } + )} let existingEmotes = []; if (this.state.emotes) { @@ -273,7 +270,8 @@ export default class RoomEmoteSettings extends React.Component { className="mx_EmoteSettings_existingEmoteCode" /> -
    @@ -288,13 +286,10 @@ export default class RoomEmoteSettings extends React.Component {
    - ) + ); } - - } - let emoteUploadButton; if (this.state.canAddEmote) { emoteUploadButton = ( @@ -313,22 +308,22 @@ export default class RoomEmoteSettings extends React.Component { return (
  • { this.state.newEmoteFileAdded ? : null } - {emoteUploadButton} + { emoteUploadButton }
  • { existingEmotes } - {emoteSettingsButtons} + { emoteSettingsButtons }
    ); } diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx index 864fff3171e..d412ffedb01 100644 --- a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -18,14 +18,7 @@ import React, { ContextType } from 'react'; import { _t } from "../../../../../languageHandler"; import RoomEmoteSettings from "../../../room_settings/RoomEmoteSettings"; -import AccessibleButton, { ButtonEvent } from "../../../elements/AccessibleButton"; -import dis from "../../../../../dispatcher/dispatcher"; import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; -import SettingsStore from "../../../../../settings/SettingsStore"; -import { UIFeature } from "../../../../../settings/UIFeature"; -import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings"; -import AliasSettings from "../../../room_settings/AliasSettings"; -import PosthogTrackers from "../../../../../PosthogTrackers"; interface IProps { roomId: string; @@ -47,20 +40,10 @@ export default class EmoteRoomSettingsTab extends React.Component : - null; - - return (
    From d528ff3aae32744aea947f49d66cc309b4a4c5f6 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:02:16 -0400 Subject: [PATCH 057/176] more lint fixes --- src/HtmlUtils.tsx | 11 +++--- src/autocomplete/Components.tsx | 2 +- src/autocomplete/EmojiProvider.tsx | 27 +++++++------- src/components/structures/MessagePanel.tsx | 9 ++--- src/components/views/messages/TextualBody.tsx | 11 +++--- .../views/room_settings/RoomEmoteSettings.tsx | 36 +++++++++---------- .../settings/tabs/room/EmoteSettingsTab.tsx | 5 +-- .../room-list/previews/MessageEventPreview.ts | 2 +- 8 files changed, 47 insertions(+), 56 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 7203e78d5db..00a35ee4463 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -546,9 +546,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join('').replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + return highlighter.applyHighlights(safeText, safeHighlights).join('') + .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; } @@ -616,17 +616,16 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "mx_EventTile_bigEmoji": emojiBody, "markdown-body": isHtmlMessage && !emojiBody, }); - let tmp=strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); - if(tmp!=strippedBody){ + const tmp=strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + if (tmp!=strippedBody) { safeBody=tmp; } - let emojiBodyElements: JSX.Element[]; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } - + return safeBody ? ((props, ref) ref={ref} > { children } - {} + { } { subtitle } { description }
    diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index dcf45cf0444..3ecd912ae38 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -81,14 +81,15 @@ export default class EmojiProvider extends AutocompleteProvider { public matcher: QueryMatcher; public nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - emotes:Dictionary; + emotes: Dictionary; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); - let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; this.emotes = {}; - for (let key in rawEmotes) { - this.emotes[key] = ""; + for (const key in rawEmotes) { + this.emotes[key] = ""; } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], @@ -114,17 +115,17 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } - let emojisAndEmotes=[...SORTED_EMOJI]; - for(let key in this.emotes){ + const emojisAndEmotes=[...SORTED_EMOJI]; + for(const key in this.emotes) { emojisAndEmotes.push({ - emoji:{label:key, - shortcodes:[this.emotes[key]], - hexcode:"", - unicode:":"+key+":", + emoji: {label: key, + shortcodes: [this.emotes[key]], + hexcode: "", + unicode: ":"+key+":", }, - _orderBy:0 - }) + _orderBy: 0, + }); } this.matcher.setObjects(emojisAndEmotes); this.nameMatcher.setObjects(emojisAndEmotes); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 1c361be94b4..d90af4e837a 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -18,7 +18,7 @@ import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'rea import ReactDOM from 'react-dom'; import classNames from 'classnames'; import { Room } from 'matrix-js-sdk/src/models/room'; -import { EventType} from 'matrix-js-sdk/src/@types/event'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { Relations } from "matrix-js-sdk/src/models/relations"; import { logger } from 'matrix-js-sdk/src/logger'; @@ -27,7 +27,6 @@ import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; import { Optional } from "matrix-events-sdk"; - import shouldHideEvent from '../../shouldHideEvent'; import { wantsDateSeparator } from '../../DateUtils'; import { MatrixClientPeg } from '../../MatrixClientPeg'; @@ -206,7 +205,7 @@ interface IState { ghostReadMarkers: string[]; showTypingNotifications: boolean; hideSender: boolean; - emotes:Dictionary; + emotes: Dictionary; } interface IReadReceiptForUser { @@ -293,8 +292,6 @@ export default class MessagePanel extends React.Component { this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - - } public componentDidMount(): void { @@ -789,7 +786,7 @@ export default class MessagePanel extends React.Component { isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); - + // use txnId as key if available so that we don't remount during sending ret.push( { const client = MatrixClientPeg.get(); const room = client.getRoom(mxEvent.getRoomId()); //TODO: Decrypt emotes if encryption is added - let emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); - let rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - let finalEmotes = {}; - for (let key in rawEmotes) { - finalEmotes[":" + key + ":"] = ""; + const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const finalEmotes = {}; + for (const key in rawEmotes) { + finalEmotes[":" + key + ":"] = ""; } if (SettingsStore.isEnabled("feature_extensible_events")) { const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 5385ee471bc..5642e60caed 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -18,7 +18,6 @@ import React, { createRef } from 'react'; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import Field from "../elements/Field"; import { mediaFromMxc } from "../../../customisations/Media"; import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; @@ -53,10 +52,9 @@ export default class RoomEmoteSettings extends React.Component { if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); //TODO: Decrypt the shortcodes and emotes if they are encrypted const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); - const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const value = {}; - for (let emote in emotes) { + for (const emote in emotes) { value[emote] = emote; } @@ -78,10 +76,9 @@ export default class RoomEmoteSettings extends React.Component { this.emoteUpload.current.click(); }; - private isSaveEnabled = () => { return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || - (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || + (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || (this.state.deleted); }; @@ -141,7 +138,7 @@ export default class RoomEmoteSettings extends React.Component { const client = MatrixClientPeg.get(); const newState: Partial = {}; const emotesMxcs = {}; - let value = {}; + const value = {}; // TODO: What do we do about errors? if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { @@ -181,12 +178,11 @@ export default class RoomEmoteSettings extends React.Component { } this.setState(newState as IState); - }; - + } private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); - let b = this.state.value + const b = this.state.value; b[id] = e.target.value; this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); } @@ -241,18 +237,18 @@ export default class RoomEmoteSettings extends React.Component { emoteSettingsButtons = (
    - {_t("Cancel")} + { _t("Cancel") } - {_t("Save")} + { _t("Save") }
    )} @@ -264,9 +260,9 @@ export default class RoomEmoteSettings extends React.Component {
  • @@ -282,7 +278,7 @@ export default class RoomEmoteSettings extends React.Component { aria-label="Close" id={emotecode} > - {_t("Delete")} + { _t("Delete") }
  • @@ -295,7 +291,7 @@ export default class RoomEmoteSettings extends React.Component { emoteUploadButton = (
    {_t("Upload Emote")} diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx index d412ffedb01..d8aba88defb 100644 --- a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -41,13 +41,10 @@ export default class EmoteRoomSettingsTab extends React.Component -
    {_t("Emotes")}
    +
    { _t("Emotes") }
    diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index 04cbad99061..e942101c351 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -65,7 +65,7 @@ export class MessageEventPreview implements IPreview { // run it through DOMParser to fixup encoded html entities body = new DOMParser().parseFromString(sanitised, "text/html").documentElement.textContent; } - + body = sanitizeForTranslation(body); if (msgtype === MsgType.Emote) { From d8a50690caffa6de5e9d78d4816d5d21f9dae788 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:16:25 -0400 Subject: [PATCH 058/176] even more lint fixes --- src/HtmlUtils.tsx | 6 +-- src/autocomplete/EmojiProvider.tsx | 4 +- src/components/views/messages/TextualBody.tsx | 3 +- .../views/room_settings/RoomEmoteSettings.tsx | 43 +++++++++---------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 00a35ee4463..2cb4b269a40 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -546,9 +546,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function(safeText) { + sanitizeParams.textFilter = function (safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join('') - .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; } @@ -620,7 +620,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (tmp!=strippedBody) { safeBody=tmp; } - + let emojiBodyElements: JSX.Element[]; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 3ecd912ae38..b62f593d404 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -116,9 +116,9 @@ export default class EmojiProvider extends AutocompleteProvider { return []; // don't give any suggestions if the user doesn't want them } const emojisAndEmotes=[...SORTED_EMOJI]; - for(const key in this.emotes) { + for (const key in this.emotes) { emojisAndEmotes.push({ - emoji: {label: key, + emoji: { label: key, shortcodes: [this.emotes[key]], hexcode: "", unicode: ":"+key+":", diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 06a96983b7c..93ee9997f85 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -587,7 +587,7 @@ export default class TextualBody extends React.Component { const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const finalEmotes = {}; for (const key in rawEmotes) { - finalEmotes[":" + key + ":"] = ""; } if (SettingsStore.isEnabled("feature_extensible_events")) { @@ -622,7 +622,6 @@ export default class TextualBody extends React.Component { ref: this.contentRef, returnString: false, }); - } if (this.props.replacingEventId) { body = ( diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 5642e60caed..bd1423fbff4 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -126,9 +126,8 @@ export default class RoomEmoteSettings extends React.Component { } } - this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }) + this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }); return; - } private saveEmote = async (e: React.FormEvent): Promise => { e.stopPropagation(); @@ -150,7 +149,8 @@ export default class RoomEmoteSettings extends React.Component { } if (this.state.emotes) { for (const shortcode in this.state.emotes) { - if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) && (shortcode === this.state.newEmoteCode)) { + if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) + && (shortcode === this.state.newEmoteCode)) { continue; } if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { @@ -163,7 +163,7 @@ export default class RoomEmoteSettings extends React.Component { value[shortcode] = shortcode; } - }; + } } newState.value = value; await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); @@ -175,17 +175,16 @@ export default class RoomEmoteSettings extends React.Component { newState.emotes = emotesMxcs; newState.deleted = false; newState.deletedItems = {}; - } this.setState(newState as IState); - } + }; private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); const b = this.state.value; b[id] = e.target.value; this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); - } + }; private onEmoteFileAdd = (e: React.ChangeEvent): void => { if (!e.target.files || !e.target.files.length) { @@ -227,7 +226,7 @@ export default class RoomEmoteSettings extends React.Component { newEmoteCodeAdded: false, }); } - } + }; public render(): JSX.Element { let emoteSettingsButtons; @@ -237,21 +236,21 @@ export default class RoomEmoteSettings extends React.Component { emoteSettingsButtons = (
    { _t("Cancel") } { _t("Save") }
    - )} + )}; let existingEmotes = []; if (this.state.emotes) { @@ -260,13 +259,13 @@ export default class RoomEmoteSettings extends React.Component {
  • - @@ -304,7 +303,7 @@ export default class RoomEmoteSettings extends React.Component { return (
    { type="file" ref={ this.emoteUpload } className="mx_EmoteSettings_emoteUpload" - onClick={ chromeFileInputFix } - onChange={ this.onEmoteFileAdd } + onClick={chromeFileInputFix} + onChange={this.onEmoteFileAdd} accept="image/*" />
  • { this.state.newEmoteFileAdded ? : null } From 24d63bc31c1fabc6c5a8ca8380475cf4cc8bb67c Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:44:24 -0400 Subject: [PATCH 059/176] lint fixes 4 --- src/HtmlUtils.tsx | 2 +- .../views/room_settings/RoomEmoteSettings.tsx | 31 ++++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 2cb4b269a40..8e12feea81f 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -546,7 +546,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function (safeText) { + sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join('') .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); }; diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index bd1423fbff4..e3c8334dea7 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -107,7 +107,6 @@ export default class RoomEmoteSettings extends React.Component { this.emoteUpload.current.value = ""; this.emoteCodeUpload.current.value = ""; - }; private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); @@ -120,15 +119,14 @@ export default class RoomEmoteSettings extends React.Component { if (emote != id) { cleanemotes[emote] = this.state.emotes[emote]; value[emote] = emote; - } - else { + } else { deletedItems[emote] = this.state.emotes[emote]; } } this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }); return; - } + }; private saveEmote = async (e: React.FormEvent): Promise => { e.stopPropagation(); e.preventDefault(); @@ -156,13 +154,10 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; - } - - else { + } else { emotesMxcs[shortcode] = this.state.emotes[shortcode]; value[shortcode] = shortcode; } - } } newState.value = value; @@ -220,8 +215,7 @@ export default class RoomEmoteSettings extends React.Component { ...this.state.EmoteFieldsTouched, }, }); - } - else { + } else { this.setState({ newEmoteCodeAdded: false, }); @@ -250,11 +244,12 @@ export default class RoomEmoteSettings extends React.Component { { _t("Save") }
  • - )}; + ); + } - let existingEmotes = []; + const existingEmotes = []; if (this.state.emotes) { - for (let emotecode in this.state.emotes) { + for (const emotecode in this.state.emotes) { existingEmotes.push(
  • { /> + src={ + mediaFromMxc(this.state.emotes[emotecode]).srcHttp + } />
    { aria-label="Close" id={emotecode} > - { _t("Delete") } + {_t("Delete")}
  • @@ -310,7 +305,7 @@ export default class RoomEmoteSettings extends React.Component { > Date: Fri, 2 Sep 2022 18:48:37 -0400 Subject: [PATCH 060/176] lint fixes 5 --- src/components/views/room_settings/RoomEmoteSettings.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index e3c8334dea7..77c8a4e24c0 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -272,10 +272,10 @@ export default class RoomEmoteSettings extends React.Component { aria-label="Close" id={emotecode} > - {_t("Delete")} + { _t("Delete") }
    - + , ); } } @@ -285,16 +285,15 @@ export default class RoomEmoteSettings extends React.Component { emoteUploadButton = (
    - {_t("Upload Emote")} + { _t("Upload Emote") }
    ); } - return ( Date: Fri, 2 Sep 2022 19:05:49 -0400 Subject: [PATCH 061/176] removed extraneous file that got accidentally copied over --- src/utils/exportUtils/MessagePanel.tsx | 1343 ------------------------ 1 file changed, 1343 deletions(-) delete mode 100644 src/utils/exportUtils/MessagePanel.tsx diff --git a/src/utils/exportUtils/MessagePanel.tsx b/src/utils/exportUtils/MessagePanel.tsx deleted file mode 100644 index 71f698c9f83..00000000000 --- a/src/utils/exportUtils/MessagePanel.tsx +++ /dev/null @@ -1,1343 +0,0 @@ -/* -Copyright 2016 - 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react'; -import ReactDOM from 'react-dom'; -import classNames from 'classnames'; -import { Room } from 'matrix-js-sdk/src/models/room'; -import { EventType } from 'matrix-js-sdk/src/@types/event'; -import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { Relations } from "matrix-js-sdk/src/models/relations"; -import { logger } from 'matrix-js-sdk/src/logger'; -import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; -import { M_BEACON_INFO } from 'matrix-js-sdk/src/@types/beacon'; -import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; - -import shouldHideEvent from '../../shouldHideEvent'; -import { wantsDateSeparator } from '../../DateUtils'; -import { MatrixClientPeg } from '../../MatrixClientPeg'; -import SettingsStore from '../../settings/SettingsStore'; -import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; -import { Layout } from "../../settings/enums/Layout"; -import { _t } from "../../languageHandler"; -import EventTile, { UnwrappedEventTile, IReadReceiptProps } from "../views/rooms/EventTile"; -import { hasText } from "../../TextForEvent"; -import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; -import DMRoomMap from "../../utils/DMRoomMap"; -import NewRoomIntro from "../views/rooms/NewRoomIntro"; -import HistoryTile from "../views/rooms/HistoryTile"; -import defaultDispatcher from '../../dispatcher/dispatcher'; -import CallEventGrouper from "./CallEventGrouper"; -import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile'; -import ScrollPanel, { IScrollState } from "./ScrollPanel"; -import GenericEventListSummary from '../views/elements/GenericEventListSummary'; -import EventListSummary from '../views/elements/EventListSummary'; -import DateSeparator from '../views/messages/DateSeparator'; -import ErrorBoundary from '../views/elements/ErrorBoundary'; -import ResizeNotifier from "../../utils/ResizeNotifier"; -import Spinner from "../views/elements/Spinner"; -import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; -import EditorStateTransfer from "../../utils/EditorStateTransfer"; -import { Action } from '../../dispatcher/actions'; -import { getEventDisplayInfo } from "../../utils/EventRenderingUtils"; -import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; -import { haveRendererForEvent } from "../../events/EventTileFactory"; -import { editorRoomKey } from "../../Editing"; -import { hasThreadSummary } from "../../utils/EventUtils"; - -const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes -const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; -const groupedStateEvents = [ - EventType.RoomMember, - EventType.RoomThirdPartyInvite, - EventType.RoomServerAcl, - EventType.RoomPinnedEvents, -]; - -// check if there is a previous event and it has the same sender as this event -// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL -export function shouldFormContinuation( - prevEvent: MatrixEvent, - mxEvent: MatrixEvent, - showHiddenEvents: boolean, - threadsEnabled: boolean, - timelineRenderingType?: TimelineRenderingType, -): boolean { - if (timelineRenderingType === TimelineRenderingType.ThreadsList) return false; - // sanity check inputs - if (!prevEvent?.sender || !mxEvent.sender) return false; - // check if within the max continuation period - if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; - - // As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa - if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false; - - // Some events should appear as continuations from previous events of different types. - if (mxEvent.getType() !== prevEvent.getType() && - (!continuedTypes.includes(mxEvent.getType() as EventType) || - !continuedTypes.includes(prevEvent.getType() as EventType))) return false; - - // Check if the sender is the same and hasn't changed their displayname/avatar between these events - if (mxEvent.sender.userId !== prevEvent.sender.userId || - mxEvent.sender.name !== prevEvent.sender.name || - mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false; - - // Thread summaries in the main timeline should break up a continuation on both sides - if (threadsEnabled && - (hasThreadSummary(mxEvent) || hasThreadSummary(prevEvent)) && - timelineRenderingType !== TimelineRenderingType.Thread - ) { - return false; - } - - // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile - if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false; - - return true; -} - -interface IProps { - // the list of MatrixEvents to display - events: MatrixEvent[]; - - // true to give the component a 'display: none' style. - hidden?: boolean; - - // true to show a spinner at the top of the timeline to indicate - // back-pagination in progress - backPaginating?: boolean; - - // true to show a spinner at the end of the timeline to indicate - // forward-pagination in progress - forwardPaginating?: boolean; - - // ID of an event to highlight. If undefined, no event will be highlighted. - highlightedEventId?: string; - - // The room these events are all in together, if any. - // (The notification panel won't have a room here, for example.) - room?: Room; - - // Should we show URL Previews - showUrlPreview?: boolean; - - // event after which we should show a read marker - readMarkerEventId?: string; - - // whether the read marker should be visible - readMarkerVisible?: boolean; - - // the userid of our user. This is used to suppress the read marker - // for pending messages. - ourUserId?: string; - - // whether the timeline can visually go back any further - canBackPaginate?: boolean; - - // whether to show read receipts - showReadReceipts?: boolean; - - // true if updates to the event list should cause the scroll panel to - // scroll down when we are at the bottom of the window. See ScrollPanel - // for more details. - stickyBottom?: boolean; - - // className for the panel - className: string; - - // show twelve hour timestamps - isTwelveHour?: boolean; - - // show timestamps always - alwaysShowTimestamps?: boolean; - - // whether to show reactions for an event - showReactions?: boolean; - - // which layout to use - layout?: Layout; - - resizeNotifier: ResizeNotifier; - permalinkCreator?: RoomPermalinkCreator; - editState?: EditorStateTransfer; - - // callback which is called when the panel is scrolled. - onScroll?(event: Event): void; - - // callback which is called when more content is needed. - onFillRequest?(backwards: boolean): Promise; - - // helper function to access relations for an event - onUnfillRequest?(backwards: boolean, scrollToken: string): void; - - getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations; - - hideThreadedMessages?: boolean; - disableGrouping?: boolean; - - callEventGroupers: Map; -} - -interface IState { - ghostReadMarkers: string[]; - showTypingNotifications: boolean; - hideSender: boolean; -} - -interface IReadReceiptForUser { - lastShownEventId: string; - receipt: IReadReceiptProps; -} - -/* (almost) stateless UI component which builds the event tiles in the room timeline. - */ -export default class MessagePanel extends React.Component { - static contextType = RoomContext; - public context!: React.ContextType; - - static defaultProps = { - disableGrouping: false, - }; - - // opaque readreceipt info for each userId; used by ReadReceiptMarker - // to manage its animations - private readonly readReceiptMap: { [userId: string]: IReadReceiptInfo } = {}; - - // Track read receipts by event ID. For each _shown_ event ID, we store - // the list of read receipts to display: - // [ - // { - // userId: string, - // member: RoomMember, - // ts: number, - // }, - // ] - // This is recomputed on each render. It's only stored on the component - // for ease of passing the data around since it's computed in one pass - // over all events. - private readReceiptsByEvent: Record = {}; - - // Track read receipts by user ID. For each user ID we've ever shown a - // a read receipt for, we store an object: - // { - // lastShownEventId: string, - // receipt: { - // userId: string, - // member: RoomMember, - // ts: number, - // }, - // } - // so that we can always keep receipts displayed by reverting back to - // the last shown event for that user ID when needed. This may feel like - // it duplicates the receipt storage in the room, but at this layer, we - // are tracking _shown_ event IDs, which the JS SDK knows nothing about. - // This is recomputed on each render, using the data from the previous - // render as our fallback for any user IDs we can't match a receipt to a - // displayed event in the current render cycle. - private readReceiptsByUserId: Record = {}; - - private readonly _showHiddenEvents: boolean; - private readonly threadsEnabled: boolean; - private isMounted = false; - - private readMarkerNode = createRef(); - private whoIsTyping = createRef(); - private scrollPanel = createRef(); - - private readonly showTypingNotificationsWatcherRef: string; - private eventTiles: Record = {}; - - // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. - public grouperKeyMap = new WeakMap(); - - constructor(props, context) { - super(props, context); - - this.state = { - // previous positions the read marker has been in, so we can - // display 'ghost' read markers that are animating away - ghostReadMarkers: [], - showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - hideSender: this.shouldHideSender(), - }; - - // Cache these settings on mount since Settings is expensive to query, - // and we check this in a hot code path. This is also cached in our - // RoomContext, however we still need a fallback for roomless MessagePanels. - this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); - this.threadsEnabled = SettingsStore.getValue("feature_thread"); - - this.showTypingNotificationsWatcherRef = - SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - } - - componentDidMount() { - this.calculateRoomMembersCount(); - this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); - this.isMounted = true; - } - - componentWillUnmount() { - this.isMounted = false; - this.props.room?.currentState.off(RoomStateEvent.Update, this.calculateRoomMembersCount); - SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); - } - - componentDidUpdate(prevProps, prevState) { - if (prevProps.layout !== this.props.layout) { - this.calculateRoomMembersCount(); - } - - if (prevProps.readMarkerVisible && this.props.readMarkerEventId !== prevProps.readMarkerEventId) { - const ghostReadMarkers = this.state.ghostReadMarkers; - ghostReadMarkers.push(prevProps.readMarkerEventId); - this.setState({ - ghostReadMarkers, - }); - } - - const pendingEditItem = this.pendingEditItem; - if (!this.props.editState && this.props.room && pendingEditItem) { - const event = this.props.room.findEventById(pendingEditItem); - defaultDispatcher.dispatch({ - action: Action.EditEvent, - event: !event?.isRedacted() ? event : null, - timelineRenderingType: this.context.timelineRenderingType, - }); - } - } - - private shouldHideSender(): boolean { - return this.props.room?.getInvitedAndJoinedMemberCount() <= 2 && this.props.layout === Layout.Bubble; - } - - private calculateRoomMembersCount = (): void => { - this.setState({ - hideSender: this.shouldHideSender(), - }); - }; - - private onShowTypingNotificationsChange = (): void => { - this.setState({ - showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - }); - }; - - /* get the DOM node representing the given event */ - public getNodeForEventId(eventId: string): HTMLElement { - if (!this.eventTiles) { - return undefined; - } - - return this.eventTiles[eventId]?.ref?.current; - } - - public getTileForEventId(eventId: string): UnwrappedEventTile { - if (!this.eventTiles) { - return undefined; - } - return this.eventTiles[eventId]; - } - - /* return true if the content is fully scrolled down right now; else false. - */ - public isAtBottom(): boolean { - return this.scrollPanel.current?.isAtBottom(); - } - - /* get the current scroll state. See ScrollPanel.getScrollState for - * details. - * - * returns null if we are not mounted. - */ - public getScrollState(): IScrollState { - return this.scrollPanel.current?.getScrollState() ?? null; - } - - // returns one of: - // - // null: there is no read marker - // -1: read marker is above the window - // 0: read marker is within the window - // +1: read marker is below the window - public getReadMarkerPosition(): number { - const readMarker = this.readMarkerNode.current; - const messageWrapper = this.scrollPanel.current; - - if (!readMarker || !messageWrapper) { - return null; - } - - const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect(); - const readMarkerRect = readMarker.getBoundingClientRect(); - - // the read-marker pretends to have zero height when it is actually - // two pixels high; +2 here to account for that. - if (readMarkerRect.bottom + 2 < wrapperRect.top) { - return -1; - } else if (readMarkerRect.top < wrapperRect.bottom) { - return 0; - } else { - return 1; - } - } - - /* jump to the top of the content. - */ - public scrollToTop(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToTop(); - } - } - - /* jump to the bottom of the content. - */ - public scrollToBottom(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToBottom(); - } - } - - /** - * Page up/down. - * - * @param {number} mult: -1 to page up, +1 to page down - */ - public scrollRelative(mult: number): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollRelative(mult); - } - } - - /** - * Scroll up/down in response to a scroll key - * - * @param {KeyboardEvent} ev: the keyboard event to handle - */ - public handleScrollKey(ev: KeyboardEvent): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.handleScrollKey(ev); - } - } - - /* jump to the given event id. - * - * offsetBase gives the reference point for the pixelOffset. 0 means the - * top of the container, 1 means the bottom, and fractional values mean - * somewhere in the middle. If omitted, it defaults to 0. - * - * pixelOffset gives the number of pixels *above* the offsetBase that the - * node (specifically, the bottom of it) will be positioned. If omitted, it - * defaults to 0. - */ - public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); - } - } - - public scrollToEventIfNeeded(eventId: string): void { - const node = this.getNodeForEventId(eventId); - if (node) { - node.scrollIntoView({ - block: "nearest", - behavior: "instant", - }); - } - } - - private isUnmounting = (): boolean => { - return !this.isMounted; - }; - - public get showHiddenEvents(): boolean { - return this.context?.showHiddenEvents ?? this._showHiddenEvents; - } - - // TODO: Implement granular (per-room) hide options - public shouldShowEvent(mxEv: MatrixEvent, forceHideEvents = false): boolean { - if (this.props.hideThreadedMessages && this.threadsEnabled && this.props.room) { - const { shouldLiveInRoom } = this.props.room.eventShouldLiveIn(mxEv, this.props.events); - if (!shouldLiveInRoom) { - return false; - } - } - - if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) { - return false; // ignored = no show (only happens if the ignore happens after an event was received) - } - - if (this.showHiddenEvents && !forceHideEvents) { - return true; - } - - if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) { - return false; // no tile = no show - } - - // Always show highlighted event - if (this.props.highlightedEventId === mxEv.getId()) return true; - - return !shouldHideEvent(mxEv, this.context); - } - - public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode { - const visible = !isLastEvent && this.props.readMarkerVisible; - - if (this.props.readMarkerEventId === eventId) { - let hr; - // if the read marker comes at the end of the timeline (except - // for local echoes, which are excluded from RMs, because they - // don't have useful event ids), we don't want to show it, but - // we still want to create the
  • for it so that the - // algorithms which depend on its position on the screen aren't - // confused. - if (visible) { - hr =
    ; - } - - return ( -
  • - { hr } -
  • - ); - } else if (this.state.ghostReadMarkers.includes(eventId)) { - // We render 'ghost' read markers in the DOM while they - // transition away. This allows the actual read marker - // to be in the right place straight away without having - // to wait for the transition to finish. - // There are probably much simpler ways to do this transition, - // possibly using react-transition-group which handles keeping - // elements in the DOM whilst they transition out, although our - // case is a little more complex because only some of the items - // transition (ie. the read markers do but the event tiles do not) - // and TransitionGroup requires that all its children are Transitions. - const hr =
    ; - - // give it a key which depends on the event id. That will ensure that - // we get a new DOM node (restarting the animation) when the ghost - // moves to a different event. - return ( -
  • - { hr } -
  • - ); - } - - return null; - } - - private collectGhostReadMarker = (node: HTMLElement): void => { - if (node) { - // now the element has appeared, change the style which will trigger the CSS transition - requestAnimationFrame(() => { - node.style.width = '10%'; - node.style.opacity = '0'; - }); - } - }; - - private onGhostTransitionEnd = (ev: TransitionEvent): void => { - // we can now clean up the ghost element - const finishedEventId = (ev.target as HTMLElement).dataset.eventid; - this.setState({ - ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId), - }); - }; - - private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } { - const nextEvent = i < arr.length - 1 - ? arr[i + 1] - : null; - - // The next event with tile is used to to determine the 'last successful' flag - // when rendering the tile. The shouldShowEvent function is pretty quick at what - // it does, so this should have no significant cost even when a room is used for - // not-chat purposes. - const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e)); - - return { nextEvent, nextTile }; - } - - private get pendingEditItem(): string | undefined { - if (!this.props.room) { - return undefined; - } - - try { - return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType)); - } catch (err) { - logger.error(err); - return undefined; - } - } - - private getEventTiles(): ReactNode[] { - let i; - - // first figure out which is the last event in the list which we're - // actually going to show; this allows us to behave slightly - // differently for the last event in the list. (eg show timestamp) - // - // we also need to figure out which is the last event we show which isn't - // a local echo, to manage the read-marker. - let lastShownEvent; - - let lastShownNonLocalEchoIndex = -1; - for (i = this.props.events.length-1; i >= 0; i--) { - const mxEv = this.props.events[i]; - if (!this.shouldShowEvent(mxEv)) { - continue; - } - - if (lastShownEvent === undefined) { - lastShownEvent = mxEv; - } - - if (mxEv.status) { - // this is a local echo - continue; - } - - lastShownNonLocalEchoIndex = i; - break; - } - - const ret = []; - - let prevEvent = null; // the last event we showed - - // Note: the EventTile might still render a "sent/sending receipt" independent of - // this information. When not providing read receipt information, the tile is likely - // to assume that sent receipts are to be shown more often. - this.readReceiptsByEvent = {}; - if (this.props.showReadReceipts) { - this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(); - } - - let grouper: BaseGrouper = null; - - for (i = 0; i < this.props.events.length; i++) { - const mxEv = this.props.events[i]; - const eventId = mxEv.getId(); - const last = (mxEv === lastShownEvent); - const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i); - - if (grouper) { - if (grouper.shouldGroup(mxEv)) { - grouper.add(mxEv); - continue; - } else { - // not part of group, so get the group tiles, close the - // group, and continue like a normal event - ret.push(...grouper.getTiles()); - prevEvent = grouper.getNewPrevEvent(); - grouper = null; - } - } - - for (const Grouper of groupers) { - if (Grouper.canStartGroup(this, mxEv) && !this.props.disableGrouping) { - grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile); - break; // break on first grouper - } - } - - if (!grouper) { - if (this.shouldShowEvent(mxEv)) { - // make sure we unpack the array returned by getTilesForEvent, - // otherwise React will auto-generate keys, and we will end up - // replacing all the DOM elements every time we paginate. - ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, false, nextEvent, nextTile)); - prevEvent = mxEv; - } - - const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); - if (readMarker) ret.push(readMarker); - } - } - - if (grouper) { - ret.push(...grouper.getTiles()); - } - - return ret; - } - - public getTilesForEvent( - prevEvent: MatrixEvent, - mxEv: MatrixEvent, - last = false, - isGrouped = false, - nextEvent?: MatrixEvent, - nextEventWithTile?: MatrixEvent, - ): ReactNode[] { - const ret = []; - - const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId(); - // local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators. - let ts1 = mxEv.getTs(); - let eventDate = mxEv.getDate(); - if (mxEv.status) { - eventDate = new Date(); - ts1 = eventDate.getTime(); - } - - // do we need a date separator since the last event? - const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); - if (wantsDateSeparator && !isGrouped && this.props.room) { - const dateSeparator = ( -
  • - -
  • - ); - ret.push(dateSeparator); - } - - let lastInSection = true; - if (nextEventWithTile) { - const nextEv = nextEventWithTile; - const willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEv.getDate() || new Date()); - lastInSection = willWantDateSeparator || - mxEv.getSender() !== nextEv.getSender() || - getEventDisplayInfo(nextEv, this.showHiddenEvents).isInfoMessage || - !shouldFormContinuation( - mxEv, nextEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, - ); - } - - // is this a continuation of the previous message? - const continuation = !wantsDateSeparator && - shouldFormContinuation( - prevEvent, mxEv, this.showHiddenEvents, this.threadsEnabled, this.context.timelineRenderingType, - ); - - const eventId = mxEv.getId(); - const highlight = (eventId === this.props.highlightedEventId); - - const readReceipts = this.readReceiptsByEvent[eventId]; - - let isLastSuccessful = false; - const isSentState = s => !s || s === 'sent'; - const isSent = isSentState(mxEv.getAssociatedStatus()); - const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); - if (!hasNextEvent && isSent) { - isLastSuccessful = true; - } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { - isLastSuccessful = true; - } - - // This is a bit nuanced, but if our next event is hidden but a future event is not - // hidden then we're not the last successful. - if ( - nextEventWithTile && - nextEventWithTile !== nextEvent && - isSentState(nextEventWithTile.getAssociatedStatus()) - ) { - isLastSuccessful = false; - } - - // We only want to consider "last successful" if the event is sent by us, otherwise of course - // it's successful: we received it. - isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); - - const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); - // use txnId as key if available so that we don't remount during sending - ret.push( - , - ); - - return ret; - } - - public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean { - if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { - return false; - } - if (prevEvent == null) { - // first event in the panel: depends if we could back-paginate from - // here. - return !this.props.canBackPaginate; - } - return wantsDateSeparator(prevEvent.getDate(), nextEventDate); - } - - // Get a list of read receipts that should be shown next to this event - // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. - private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] { - const myUserId = MatrixClientPeg.get().credentials.userId; - - // get list of read receipts, sorted most recent first - const { room } = this.props; - if (!room) { - return null; - } - const receipts: IReadReceiptProps[] = []; - room.getReceiptsForEvent(event).forEach((r) => { - if ( - !r.userId || - !isSupportedReceiptType(r.type) || - r.userId === myUserId - ) { - return; // ignore non-read receipts and receipts from self. - } - if (MatrixClientPeg.get().isUserIgnored(r.userId)) { - return; // ignore ignored users - } - const member = room.getMember(r.userId); - receipts.push({ - userId: r.userId, - roomMember: member, - ts: r.data ? r.data.ts : 0, - }); - }); - return receipts; - } - - // Get an object that maps from event ID to a list of read receipts that - // should be shown next to that event. If a hidden event has read receipts, - // they are folded into the receipts of the last shown event. - private getReadReceiptsByShownEvent(): Record { - const receiptsByEvent = {}; - const receiptsByUserId = {}; - - let lastShownEventId; - for (const event of this.props.events) { - if (this.shouldShowEvent(event)) { - lastShownEventId = event.getId(); - } - if (!lastShownEventId) { - continue; - } - - const existingReceipts = receiptsByEvent[lastShownEventId] || []; - const newReceipts = this.getReadReceiptsForEvent(event); - receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); - - // Record these receipts along with their last shown event ID for - // each associated user ID. - for (const receipt of newReceipts) { - receiptsByUserId[receipt.userId] = { - lastShownEventId, - receipt, - }; - } - } - - // It's possible in some cases (for example, when a read receipt - // advances before we have paginated in the new event that it's marking - // received) that we can temporarily not have a matching event for - // someone which had one in the last. By looking through our previous - // mapping of receipts by user ID, we can cover recover any receipts - // that would have been lost by using the same event ID from last time. - for (const userId in this.readReceiptsByUserId) { - if (receiptsByUserId[userId]) { - continue; - } - const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId]; - const existingReceipts = receiptsByEvent[lastShownEventId] || []; - receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); - receiptsByUserId[userId] = { lastShownEventId, receipt }; - } - this.readReceiptsByUserId = receiptsByUserId; - - // After grouping receipts by shown events, do another pass to sort each - // receipt list. - for (const eventId in receiptsByEvent) { - receiptsByEvent[eventId].sort((r1, r2) => { - return r2.ts - r1.ts; - }); - } - - return receiptsByEvent; - } - - private collectEventTile = (eventId: string, node: UnwrappedEventTile): void => { - this.eventTiles[eventId] = node; - }; - - // once dynamic content in the events load, make the scrollPanel check the - // scroll offsets. - public onHeightChanged = (): void => { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - scrollPanel.checkScroll(); - } - }; - - private onTypingShown = (): void => { - const scrollPanel = this.scrollPanel.current; - // this will make the timeline grow, so checkScroll - scrollPanel.checkScroll(); - if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { - scrollPanel.preventShrinking(); - } - }; - - private onTypingHidden = (): void => { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - // as hiding the typing notifications doesn't - // update the scrollPanel, we tell it to apply - // the shrinking prevention once the typing notifs are hidden - scrollPanel.updatePreventShrinking(); - // order is important here as checkScroll will scroll down to - // reveal added padding to balance the notifs disappearing. - scrollPanel.checkScroll(); - } - }; - - public updateTimelineMinHeight(): void { - const scrollPanel = this.scrollPanel.current; - - if (scrollPanel) { - const isAtBottom = scrollPanel.isAtBottom(); - const whoIsTyping = this.whoIsTyping.current; - const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); - // when messages get added to the timeline, - // but somebody else is still typing, - // update the min-height, so once the last - // person stops typing, no jumping occurs - if (isAtBottom && isTypingVisible) { - scrollPanel.preventShrinking(); - } - } - } - - public onTimelineReset(): void { - const scrollPanel = this.scrollPanel.current; - if (scrollPanel) { - scrollPanel.clearPreventShrinking(); - } - } - - render() { - let topSpinner; - let bottomSpinner; - if (this.props.backPaginating) { - topSpinner =
  • ; - } - if (this.props.forwardPaginating) { - bottomSpinner =
  • ; - } - - const style = this.props.hidden ? { display: 'none' } : {}; - - let whoIsTyping; - if (this.props.room && - this.state.showTypingNotifications && - this.context.timelineRenderingType === TimelineRenderingType.Room - ) { - whoIsTyping = ( - ); - } - - let ircResizer = null; - if (this.props.layout == Layout.IRC) { - ircResizer = ; - } - - const classes = classNames(this.props.className, { - "mx_MessagePanel_narrow": this.context.narrow, - }); - - return ( - - - { topSpinner } - { this.getEventTiles() } - { whoIsTyping } - { bottomSpinner } - - - ); - } -} - -abstract class BaseGrouper { - static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true; - - public events: MatrixEvent[] = []; - // events that we include in the group but then eject out and place above the group. - public ejectedEvents: MatrixEvent[] = []; - public readMarker: ReactNode; - - constructor( - public readonly panel: MessagePanel, - public readonly event: MatrixEvent, - public readonly prevEvent: MatrixEvent, - public readonly lastShownEvent: MatrixEvent, - public readonly nextEvent?: MatrixEvent, - public readonly nextEventTile?: MatrixEvent, - ) { - this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent); - } - - public abstract shouldGroup(ev: MatrixEvent): boolean; - public abstract add(ev: MatrixEvent): void; - public abstract getTiles(): ReactNode[]; - public abstract getNewPrevEvent(): MatrixEvent; -} - -/* Grouper classes determine when events can be grouped together in a summary. - * Groupers should have the following methods: - * - canStartGroup (static): determines if a new group should be started with the - * given event - * - shouldGroup: determines if the given event should be added to an existing group - * - add: adds an event to an existing group (should only be called if shouldGroup - * return true) - * - getTiles: returns the tiles that represent the group - * - getNewPrevEvent: returns the event that should be used as the new prevEvent - * when determining things such as whether a date separator is necessary - */ - -// Wrap initial room creation events into a GenericEventListSummary -// Grouping only events sent by the same user that sent the `m.room.create` and only until -// the first non-state event, beacon_info event or membership event which is not regarding the sender of the `m.room.create` event -class CreationGrouper extends BaseGrouper { - static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { - return ev.getType() === EventType.RoomCreate; - }; - - public shouldGroup(ev: MatrixEvent): boolean { - const panel = this.panel; - const createEvent = this.event; - if (!panel.shouldShowEvent(ev)) { - return true; - } - if (panel.wantsDateSeparator(this.event, ev.getDate())) { - return false; - } - if (ev.getType() === EventType.RoomMember - && (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) { - return false; - } - // beacons are not part of room creation configuration - // should be shown in timeline - if (M_BEACON_INFO.matches(ev.getType())) { - return false; - } - if (ev.isState() && ev.getSender() === createEvent.getSender()) { - return true; - } - - return false; - } - - public add(ev: MatrixEvent): void { - const panel = this.panel; - this.readMarker = this.readMarker || panel.readMarkerForEvent( - ev.getId(), - ev === this.lastShownEvent, - ); - if (!panel.shouldShowEvent(ev)) { - return; - } - if (ev.getType() === EventType.RoomEncryption) { - this.ejectedEvents.push(ev); - } else { - this.events.push(ev); - } - } - - public getTiles(): ReactNode[] { - // If we don't have any events to group, don't even try to group them. The logic - // below assumes that we have a group of events to deal with, but we might not if - // the events we were supposed to group were redacted. - if (!this.events || !this.events.length) return []; - - const panel = this.panel; - const ret: ReactNode[] = []; - const isGrouped = true; - const createEvent = this.event; - const lastShownEvent = this.lastShownEvent; - - if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) { - const ts = createEvent.getTs(); - ret.push( -
  • , - ); - } - - // If this m.room.create event should be shown (room upgrade) then show it before the summary - if (panel.shouldShowEvent(createEvent)) { - // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered - ret.push(...panel.getTilesForEvent(createEvent, createEvent)); - } - - for (const ejected of this.ejectedEvents) { - ret.push(...panel.getTilesForEvent( - createEvent, ejected, createEvent === lastShownEvent, isGrouped, - )); - } - - const eventTiles = this.events.map((e) => { - // In order to prevent DateSeparators from appearing in the expanded form - // of GenericEventListSummary, render each member event as if the previous - // one was itself. This way, the timestamp of the previous event === the - // timestamp of the current event, and no DateSeparator is inserted. - return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); - }).reduce((a, b) => a.concat(b), []); - // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one - const ev = this.events[this.events.length - 1]; - - let summaryText: string; - const roomId = ev.getRoomId(); - const creator = ev.sender ? ev.sender.name : ev.getSender(); - if (DMRoomMap.shared().getUserIdForRoomId(roomId)) { - summaryText = _t("%(creator)s created this DM.", { creator }); - } else { - summaryText = _t("%(creator)s created and configured the room.", { creator }); - } - - ret.push(); - - ret.push( - - { eventTiles } - , - ); - - if (this.readMarker) { - ret.push(this.readMarker); - } - - return ret; - } - - public getNewPrevEvent(): MatrixEvent { - return this.event; - } -} - -// Wrap consecutive grouped events in a ListSummary -class MainGrouper extends BaseGrouper { - static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { - if (!panel.shouldShowEvent(ev)) return false; - - if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { - return true; - } - - if (ev.isRedacted()) { - return true; - } - - if (panel.showHiddenEvents && !panel.shouldShowEvent(ev, true)) { - return true; - } - - return false; - }; - - constructor( - public readonly panel: MessagePanel, - public readonly event: MatrixEvent, - public readonly prevEvent: MatrixEvent, - public readonly lastShownEvent: MatrixEvent, - nextEvent: MatrixEvent, - nextEventTile: MatrixEvent, - ) { - super(panel, event, prevEvent, lastShownEvent, nextEvent, nextEventTile); - this.events = [event]; - } - - public shouldGroup(ev: MatrixEvent): boolean { - if (!this.panel.shouldShowEvent(ev)) { - // absorb hidden events so that they do not break up streams of messages & redaction events being grouped - return true; - } - if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { - return false; - } - if (ev.isState() && groupedStateEvents.includes(ev.getType() as EventType)) { - return true; - } - if (ev.isRedacted()) { - return true; - } - if (this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev, true)) { - return true; - } - return false; - } - - public add(ev: MatrixEvent): void { - if (ev.getType() === EventType.RoomMember) { - // We can ignore any events that don't actually have a message to display - if (!hasText(ev, this.panel.showHiddenEvents)) return; - } - this.readMarker = this.readMarker || this.panel.readMarkerForEvent(ev.getId(), ev === this.lastShownEvent); - if (!this.panel.showHiddenEvents && !this.panel.shouldShowEvent(ev)) { - // absorb hidden events to not split the summary - return; - } - this.events.push(ev); - } - - private generateKey(): string { - return "eventlistsummary-" + this.events[0].getId(); - } - - public getTiles(): ReactNode[] { - // If we don't have any events to group, don't even try to group them. The logic - // below assumes that we have a group of events to deal with, but we might not if - // the events we were supposed to group were redacted. - if (!this.events?.length) return []; - - const isGrouped = true; - const panel = this.panel; - const lastShownEvent = this.lastShownEvent; - const ret: ReactNode[] = []; - - if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { - const ts = this.events[0].getTs(); - ret.push( -
  • , - ); - } - - // Ensure that the key of the EventListSummary does not change with new events in either direction. - // This will prevent it from being re-created unnecessarily, and instead will allow new props to be provided. - // In turn, the shouldComponentUpdate method on ELS can be used to prevent unnecessary renderings. - const keyEvent = this.events.find(e => this.panel.grouperKeyMap.get(e)); - const key = keyEvent ? this.panel.grouperKeyMap.get(keyEvent) : this.generateKey(); - if (!keyEvent) { - // Populate the weak map with the key. - // Note that we only set the key on the specific event it refers to, since this group might get - // split up in the future by other intervening events. If we were to set the key on all events - // currently in the group, we would risk later giving the same key to multiple groups. - this.panel.grouperKeyMap.set(this.events[0], key); - } - - let highlightInSummary = false; - let eventTiles = this.events.map((e, i) => { - if (e.getId() === panel.props.highlightedEventId) { - highlightInSummary = true; - } - return panel.getTilesForEvent( - i === 0 ? this.prevEvent : this.events[i - 1], - e, - e === lastShownEvent, - isGrouped, - this.nextEvent, - this.nextEventTile, - ); - }).reduce((a, b) => a.concat(b), []); - - if (eventTiles.length === 0) { - eventTiles = null; - } - - // If a membership event is the start of visible history, tell the user - // why they can't see earlier messages - if (!this.panel.props.canBackPaginate && !this.prevEvent) { - ret.push(); - } - - ret.push( - - { eventTiles } - , - ); - - if (this.readMarker) { - ret.push(this.readMarker); - } - - return ret; - } - - public getNewPrevEvent(): MatrixEvent { - return this.events[this.events.length - 1]; - } -} - -// all the grouper classes that we use, ordered by priority -const groupers = [CreationGrouper, MainGrouper]; From e242ca20e1ce3e7e8db3f5d0b42b2ea36efad7a3 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 19:30:35 -0400 Subject: [PATCH 062/176] fixed issue in autocomplete where title of normal emote was notsurrounded in colons --- src/autocomplete/EmojiProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index b62f593d404..052b1287b0b 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -186,7 +186,7 @@ export default class EmojiProvider extends AutocompleteProvider { return completions.map((c) => ({ completion: c.emoji.unicode, component: ( - + { this.emotes[c.emoji.shortcodes[0]]? this.emotes[c.emoji.shortcodes[0]]:c.emoji.unicode } ), From 135ce7d8e47470c51f36404e52875d352b5ef0fe Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 20:17:02 -0400 Subject: [PATCH 063/176] changed i18n files to include emote phrases --- src/i18n/strings/en_EN.json | 23 +++++++++++++++-------- src/i18n/strings/en_US.json | 3 +++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 84ca822e85d..a6b194b31e7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1044,7 +1044,6 @@ "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", "Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list", - "Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list", "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode": "Low bandwidth mode", "Requires compatible homeserver.": "Requires compatible homeserver.", @@ -1099,8 +1098,8 @@ "Get stuff done by finding your teammates": "Get stuff done by finding your teammates", "Find people": "Find people", "Find and invite your community members": "Find and invite your community members", - "Download %(brand)s": "Download %(brand)s", - "Don’t miss a thing by taking %(brand)s with you": "Don’t miss a thing by taking %(brand)s with you", + "Download Element": "Download Element", + "Don’t miss a thing by taking Element with you": "Don’t miss a thing by taking Element with you", "Download apps": "Download apps", "Set up your profile": "Set up your profile", "Make sure people know it’s really you": "Make sure people know it’s really you", @@ -1743,7 +1742,7 @@ "Select the roles required to change various parts of the space": "Select the roles required to change various parts of the space", "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", "Are you sure you want to add encryption to this public room?": "Are you sure you want to add encryption to this public room?", - "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", + "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", "To avoid these issues, create a new encrypted room for the conversation you plan to have.": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", "Enable encryption?": "Enable encryption?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", @@ -1936,6 +1935,9 @@ "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", "Send voice message": "Send voice message", + "Emoji": "Emoji", + "Emote": "Emote", + "Emotes": "Emotes", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", @@ -2036,6 +2038,7 @@ "System Alerts": "System Alerts", "Historical": "Historical", "Suggested Rooms": "Suggested Rooms", + "Empty room": "Empty room", "Add space": "Add space", "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", @@ -2443,6 +2446,7 @@ "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", + "Show all": "Show all", "Reactions": "Reactions", "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s", "reacted with %(shortName)s": "reacted with %(shortName)s", @@ -2693,6 +2697,7 @@ "We don't record or profile any account data": "We don't record or profile any account data", "We don't share information with third parties": "We don't share information with third parties", "You can turn this off anytime in settings": "You can turn this off anytime in settings", + "Download %(brand)s": "Download %(brand)s", "Download %(brand)s Desktop": "Download %(brand)s Desktop", "iOS": "iOS", "%(qrCode)s or %(appLinks)s": "%(qrCode)s or %(appLinks)s", @@ -2903,6 +2908,7 @@ "a key signature": "a key signature", "%(brand)s encountered an error during upload of:": "%(brand)s encountered an error during upload of:", "Upload completed": "Upload completed", + "Upload Emote": "Upload Emote", "Cancelled signature upload": "Cancelled signature upload", "Unable to upload": "Unable to upload", "Signature upload success": "Signature upload success", @@ -2943,6 +2949,7 @@ "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", + "Session ID": "Session ID", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.", @@ -3065,8 +3072,8 @@ "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", "Not Trusted": "Not Trusted", - "Manually verify by text": "Manually verify by text", - "Interactively verify by emoji": "Interactively verify by emoji", + "Manually Verify by Text": "Manually Verify by Text", + "Interactively verify by Emoji": "Interactively verify by Emoji", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload all": "Upload all", @@ -3228,11 +3235,11 @@ "Observe only": "Observe only", "No verification requests found": "No verification requests found", "There was an error finding this widget.": "There was an error finding this widget.", + "Resume": "Resume", + "Hold": "Hold", "Input devices": "Input devices", "Output devices": "Output devices", "Cameras": "Cameras", - "Resume": "Resume", - "Hold": "Hold", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Open in OpenStreetMap": "Open in OpenStreetMap", "Forward": "Forward", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 383d052e177..e13b84d73a8 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -50,6 +50,8 @@ "Email": "Email", "Email address": "Email address", "Emoji": "Emoji", + "Emote": "Emote", + "Emotes": "Emotes", "Error": "Error", "Error decrypting attachment": "Error decrypting attachment", "Export": "Export", @@ -176,6 +178,7 @@ "Unmute": "Unmute", "Upload avatar": "Upload avatar", "Upload Failed": "Upload Failed", + "Upload Emote": "Upload Emote", "Usage": "Usage", "Users": "Users", "Verification Pending": "Verification Pending", From 0340ccfb15820efa1abbfacefe685429b465f884 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 21:24:40 -0400 Subject: [PATCH 064/176] added some missing css --- res/css/_components.pcss | 1 + .../views/dialogs/_RoomSettingsDialog.pcss | 3 + res/css/views/settings/_EmoteSettings.pcss | 77 +++++++++++++++++++ .../views/dialogs/RoomSettingsDialog.tsx | 4 +- 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 res/css/views/settings/_EmoteSettings.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 56628095f2f..9d9ae0b44ac 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -325,6 +325,7 @@ @import "./views/settings/_Notifications.pcss"; @import "./views/settings/_PhoneNumbers.pcss"; @import "./views/settings/_ProfileSettings.pcss"; +@import "./views/settings/_EmoteSettings.pcss"; @import "./views/settings/_SecureBackupPanel.pcss"; @import "./views/settings/_SetIdServer.pcss"; @import "./views/settings/_SetIntegrationManager.pcss"; diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index f57f6361932..76b256d62bc 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -49,6 +49,9 @@ limitations under the License. .mx_RoomSettingsDialog_warningIcon::before { mask-image: url("$(res)/img/element-icons/room/settings/advanced.svg"); } +.mx_RoomSettingsDialog_emotesIcon::before { + mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); +} .mx_RoomSettingsDialog .mx_Dialog_title { -ms-text-overflow: ellipsis; diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss new file mode 100644 index 00000000000..21c65311cec --- /dev/null +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -0,0 +1,77 @@ +/* +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EmoteSettings { + margin-inline-end: var(--SettingsTab_fullWidthField-margin-inline-end); + border-bottom: 1px solid $quinary-content; + + .mx_EmoteSettings_emoteUpload { + display: none; + } + .mx_EmoteSettings_addEmoteField { + display: flex; + width: 100%; + } + .mx_EmoteSettings_emoteField { + } + .mx_EmoteSettings_uploadButton { + margin-left:auto; + align-self: center; + } + .mx_EmoteSettings_uploadedEmoteImage{ + height: 30px; + width: var(--emote-image-width)*30/var(--emote-image-height); + margin-left:30px; + align-self: center; + } + .mx_EmoteSettings_Emote { + display: flex; + + .mx_EmoteSettings_Emote_controls { + flex-grow: 1; + margin-inline-end: 54px; + + .mx_Field:first-child { + margin-top: 0; + } + + .mx_EmoteSettings_Emote_controls_topic { + & > textarea { + font-family: inherit; + resize: vertical; + } + + &.mx_EmoteSettings_Emote_controls_topic--room textarea { + min-height: 4em; + } + } + + .mx_EmoteSettings_Emote_controls_userId { + margin-inline-end: $spacing-20; + } + } + } + + .mx_EmoteSettings_buttons { + margin-top: 10px; /* 18px is already accounted for by the

    above the buttons */ + //margin-bottom: $spacing-28; + + > .mx_AccessibleButton_kind_link { + font-size: $font-14px; + margin-inline-end: 10px; /* TODO: Use a spacing variable */ + } + } +} diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index d527b740850..df4d3853aaf 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -154,11 +154,11 @@ class RoomSettingsDialog extends React.Component { )); tabs.push(new Tab( - ROOM_NOTIFICATIONS_TAB, + ROOM_EMOTES_TAB, _td("Emotes"), "mx_RoomSettingsDialog_emotesIcon", , - "RoomSettingsNotifications", + "RoomSettingsEmotes", )); if (SettingsStore.getValue("feature_bridge_state")) { tabs.push( From dbbe58923aaba3f7e702b9cec5c6f0959e28af90 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:13:09 -0400 Subject: [PATCH 065/176] added some more missing changes --- res/css/views/settings/_EmoteSettings.pcss | 8 ++++---- src/autocomplete/EmojiProvider.tsx | 8 ++++---- src/components/views/room_settings/RoomEmoteSettings.tsx | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index 21c65311cec..6f4222fc764 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -28,13 +28,13 @@ limitations under the License. .mx_EmoteSettings_emoteField { } .mx_EmoteSettings_uploadButton { - margin-left:auto; + margin-left: auto; align-self: center; } - .mx_EmoteSettings_uploadedEmoteImage{ + .mx_EmoteSettings_uploadedEmoteImage { height: 30px; - width: var(--emote-image-width)*30/var(--emote-image-height); - margin-left:30px; + width: var(--emote-image-width) *30/var(--emote-image-height); + margin-left: 30px; align-self: center; } .mx_EmoteSettings_Emote { diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 052b1287b0b..369f2e3b300 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -120,8 +120,8 @@ export default class EmojiProvider extends AutocompleteProvider { emojisAndEmotes.push({ emoji: { label: key, shortcodes: [this.emotes[key]], - hexcode: "", - unicode: ":"+key+":", + hexcode: key, + unicode: this.emotes[key], }, _orderBy: 0, @@ -186,8 +186,8 @@ export default class EmojiProvider extends AutocompleteProvider { return completions.map((c) => ({ completion: c.emoji.unicode, component: ( - - { this.emotes[c.emoji.shortcodes[0]]? this.emotes[c.emoji.shortcodes[0]]:c.emoji.unicode } + + { this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode } ), range: range!, diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 77c8a4e24c0..bc8ad73e888 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -310,6 +310,7 @@ export default class RoomEmoteSettings extends React.Component { onChange={this.onEmoteFileAdd} accept="image/*" /> + { emoteSettingsButtons }

  • { { existingEmotes } - { emoteSettingsButtons } + ); } From 1c103b0c6c2f2ab1bce9a1e4c7c1b4ddef04bb01 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:27:26 -0400 Subject: [PATCH 066/176] lint fixes 6 --- res/css/views/settings/_EmoteSettings.pcss | 2 -- src/components/views/dialogs/RoomSettingsDialog.tsx | 2 +- src/components/views/room_settings/RoomEmoteSettings.tsx | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index 6f4222fc764..4dee60d6fbc 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -25,8 +25,6 @@ limitations under the License. display: flex; width: 100%; } - .mx_EmoteSettings_emoteField { - } .mx_EmoteSettings_uploadButton { margin-left: auto; align-self: center; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index df4d3853aaf..a6c4958152e 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -158,7 +158,7 @@ class RoomSettingsDialog extends React.Component { _td("Emotes"), "mx_RoomSettingsDialog_emotesIcon", , - "RoomSettingsEmotes", + "RoomSettingsNotifications", )); if (SettingsStore.getValue("feature_bridge_state")) { tabs.push( diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index bc8ad73e888..4608df1397c 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -334,7 +334,6 @@ export default class RoomEmoteSettings extends React.Component { { existingEmotes } - ); } From 9b657f4052b0102650a33ebf60b033071725eb62 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 20:44:52 -0400 Subject: [PATCH 067/176] i18n fixes/changes --- src/i18n/strings/en_EN.json | 3 --- src/i18n/strings/en_US.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a6b194b31e7..ad0db3887dd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1936,8 +1936,6 @@ "You do not have permission to post to this room": "You do not have permission to post to this room", "Send voice message": "Send voice message", "Emoji": "Emoji", - "Emote": "Emote", - "Emotes": "Emotes", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", @@ -2908,7 +2906,6 @@ "a key signature": "a key signature", "%(brand)s encountered an error during upload of:": "%(brand)s encountered an error during upload of:", "Upload completed": "Upload completed", - "Upload Emote": "Upload Emote", "Cancelled signature upload": "Cancelled signature upload", "Unable to upload": "Unable to upload", "Signature upload success": "Signature upload success", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index e13b84d73a8..383d052e177 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -50,8 +50,6 @@ "Email": "Email", "Email address": "Email address", "Emoji": "Emoji", - "Emote": "Emote", - "Emotes": "Emotes", "Error": "Error", "Error decrypting attachment": "Error decrypting attachment", "Export": "Export", @@ -178,7 +176,6 @@ "Unmute": "Unmute", "Upload avatar": "Upload avatar", "Upload Failed": "Upload Failed", - "Upload Emote": "Upload Emote", "Usage": "Usage", "Users": "Users", "Verification Pending": "Verification Pending", From 3e1d525d0f445880b8599022bf5d2ae90eab60fb Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 20:53:35 -0400 Subject: [PATCH 068/176] i18n changes retry --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ad0db3887dd..8b3674f4e09 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1936,6 +1936,8 @@ "You do not have permission to post to this room": "You do not have permission to post to this room", "Send voice message": "Send voice message", "Emoji": "Emoji", + "Emotes": "Emotes", + "Upload Emote": "Upload Emote", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", From eaa8f1b4126f2ea1dc84a84a9548b30c8c4d77d3 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:35:22 -0400 Subject: [PATCH 069/176] i18n changes retry 3 --- src/i18n/strings/en_EN.json | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8b3674f4e09..41fc19f91ae 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -836,6 +836,8 @@ "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", + "%(creator)s created this DM.": "%(creator)s created this DM.", + "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", "Starting export process…": "Starting export process…", "Fetching events…": "Fetching events…", @@ -1044,6 +1046,7 @@ "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", "Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list", + "Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list", "Show hidden events in timeline": "Show hidden events in timeline", "Low bandwidth mode": "Low bandwidth mode", "Requires compatible homeserver.": "Requires compatible homeserver.", @@ -1098,8 +1101,8 @@ "Get stuff done by finding your teammates": "Get stuff done by finding your teammates", "Find people": "Find people", "Find and invite your community members": "Find and invite your community members", - "Download Element": "Download Element", - "Don’t miss a thing by taking Element with you": "Don’t miss a thing by taking Element with you", + "Download %(brand)s": "Download %(brand)s", + "Don’t miss a thing by taking %(brand)s with you": "Don’t miss a thing by taking %(brand)s with you", "Download apps": "Download apps", "Set up your profile": "Set up your profile", "Make sure people know it’s really you": "Make sure people know it’s really you", @@ -1682,6 +1685,7 @@ "This room is bridging messages to the following platforms. Learn more.": "This room is bridging messages to the following platforms. Learn more.", "This room isn't bridging messages to any platforms. Learn more.": "This room isn't bridging messages to any platforms. Learn more.", "Bridges": "Bridges", + "Emotes": "Emotes", "Room Addresses": "Room Addresses", "Uploaded sound": "Uploaded sound", "Get notifications as set up in your settings": "Get notifications as set up in your settings", @@ -1742,7 +1746,7 @@ "Select the roles required to change various parts of the space": "Select the roles required to change various parts of the space", "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", "Are you sure you want to add encryption to this public room?": "Are you sure you want to add encryption to this public room?", - "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms.Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", + "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.": "It's not recommended to add encryption to public rooms. Anyone can find and join public rooms, so anyone can read messages in them. You'll get none of the benefits of encryption, and you won't be able to turn it off later. Encrypting messages in a public room will make receiving and sending messages slower.", "To avoid these issues, create a new encrypted room for the conversation you plan to have.": "To avoid these issues, create a new encrypted room for the conversation you plan to have.", "Enable encryption?": "Enable encryption?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", @@ -1936,8 +1940,6 @@ "You do not have permission to post to this room": "You do not have permission to post to this room", "Send voice message": "Send voice message", "Emoji": "Emoji", - "Emotes": "Emotes", - "Upload Emote": "Upload Emote", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", @@ -2038,7 +2040,6 @@ "System Alerts": "System Alerts", "Historical": "Historical", "Suggested Rooms": "Suggested Rooms", - "Empty room": "Empty room", "Add space": "Add space", "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", @@ -2186,6 +2187,7 @@ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", "Show more": "Show more", + "Upload Emote": "Upload Emote", "Room Name": "Room Name", "Room Topic": "Room Topic", "Room avatar": "Room avatar", @@ -2446,7 +2448,6 @@ "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", - "Show all": "Show all", "Reactions": "Reactions", "%(reactors)s reacted with %(content)s": "%(reactors)s reacted with %(content)s", "reacted with %(shortName)s": "reacted with %(shortName)s", @@ -2697,7 +2698,6 @@ "We don't record or profile any account data": "We don't record or profile any account data", "We don't share information with third parties": "We don't share information with third parties", "You can turn this off anytime in settings": "You can turn this off anytime in settings", - "Download %(brand)s": "Download %(brand)s", "Download %(brand)s Desktop": "Download %(brand)s Desktop", "iOS": "iOS", "%(qrCode)s or %(appLinks)s": "%(qrCode)s or %(appLinks)s", @@ -2948,7 +2948,6 @@ "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", - "Session ID": "Session ID", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.", @@ -3071,8 +3070,8 @@ "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) signed in to a new session without verifying it:", "Ask this user to verify their session, or manually verify it below.": "Ask this user to verify their session, or manually verify it below.", "Not Trusted": "Not Trusted", - "Manually Verify by Text": "Manually Verify by Text", - "Interactively verify by Emoji": "Interactively verify by Emoji", + "Manually verify by text": "Manually verify by text", + "Interactively verify by emoji": "Interactively verify by emoji", "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload all": "Upload all", @@ -3414,8 +3413,6 @@ "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Verification requested": "Verification requested", "Logout": "Logout", - "%(creator)s created this DM.": "%(creator)s created this DM.", - "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "You're all caught up": "You're all caught up", "You have no visible notifications.": "You have no visible notifications.", "Search failed": "Search failed", From 4c11a9ad78627843ab74492d9152f8d7b634a9c3 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:44:15 -0400 Subject: [PATCH 070/176] i18n changes retry 4 --- src/i18n/strings/en_EN.json | 932 ++++++++++++------------------------ 1 file changed, 309 insertions(+), 623 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 41fc19f91ae..af06a837d85 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1,8 +1,6 @@ { - "Identity server not set": "Identity server not set", "This email address is already in use": "This email address is already in use", "This phone number is already in use": "This phone number is already in use", - "No identity access token found": "No identity access token found", "Use Single Sign On to continue": "Use Single Sign On to continue", "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirm adding this email address by using Single Sign On to prove your identity.", "Single Sign On": "Single Sign On", @@ -11,7 +9,6 @@ "Confirm": "Confirm", "Add Email Address": "Add Email Address", "Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email", - "The add / bind with MSISDN flow is misconfigured": "The add / bind with MSISDN flow is misconfigured", "Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirm adding this phone number by using Single Sign On to prove your identity.", "Confirm adding phone number": "Confirm adding phone number", "Click the button below to confirm adding this phone number.": "Click the button below to confirm adding this phone number.", @@ -19,11 +16,43 @@ "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", - "Attachment": "Attachment", + "Call Failed": "Call Failed", + "User Busy": "User Busy", + "The user you called is busy.": "The user you called is busy.", + "The call could not be established": "The call could not be established", + "Answered Elsewhere": "Answered Elsewhere", + "The call was answered on another device.": "The call was answered on another device.", + "Call failed due to misconfigured server": "Call failed due to misconfigured server", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", + "Try using turn.matrix.org": "Try using turn.matrix.org", + "OK": "OK", + "Unable to access microphone": "Unable to access microphone", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", + "Unable to access webcam / microphone": "Unable to access webcam / microphone", + "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", + "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", + "Permission is granted to use the webcam": "Permission is granted to use the webcam", + "No other application is using the webcam": "No other application is using the webcam", + "Already in call": "Already in call", + "You're already in a call with this person.": "You're already in a call with this person.", + "Calls are unsupported": "Calls are unsupported", + "You cannot place calls in this browser.": "You cannot place calls in this browser.", + "Connectivity to the server has been lost": "Connectivity to the server has been lost", + "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", + "Too Many Calls": "Too Many Calls", + "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", + "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", + "Unable to transfer call": "Unable to transfer call", + "Transfer Failed": "Transfer Failed", + "Failed to transfer call": "Failed to transfer call", + "Permission Required": "Permission Required", + "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "Upload Failed": "Upload Failed", - "Cannot invite user by email without an identity server. You can connect to one under \"Settings\".": "Cannot invite user by email without an identity server. You can connect to one under \"Settings\".", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "The server does not support the room version specified.": "The server does not support the room version specified.", "Failure to create room": "Failure to create room", @@ -52,78 +81,34 @@ "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", - "%(hours)sh %(minutes)sm %(seconds)ss left": "%(hours)sh %(minutes)sm %(seconds)ss left", - "%(minutes)sm %(seconds)ss left": "%(minutes)sm %(seconds)ss left", - "%(seconds)ss left": "%(seconds)ss left", "%(date)s at %(time)s": "%(date)s at %(time)s", "%(value)sd": "%(value)sd", "%(value)sh": "%(value)sh", "%(value)sm": "%(value)sm", "%(value)ss": "%(value)ss", - "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss": "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss", - "%(hours)sh %(minutes)sm %(seconds)ss": "%(hours)sh %(minutes)sm %(seconds)ss", - "%(minutes)sm %(seconds)ss": "%(minutes)sm %(seconds)ss", + "That link is no longer supported": "That link is no longer supported", + "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.", "Identity server has no terms of service": "Identity server has no terms of service", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Trust": "Trust", - "Call Failed": "Call Failed", - "User Busy": "User Busy", - "The user you called is busy.": "The user you called is busy.", - "The call could not be established": "The call could not be established", - "Answered Elsewhere": "Answered Elsewhere", - "The call was answered on another device.": "The call was answered on another device.", - "Call failed due to misconfigured server": "Call failed due to misconfigured server", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", - "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", - "Try using %(server)s": "Try using %(server)s", - "OK": "OK", - "Unable to access microphone": "Unable to access microphone", - "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", - "Unable to access webcam / microphone": "Unable to access webcam / microphone", - "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", - "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", - "Permission is granted to use the webcam": "Permission is granted to use the webcam", - "No other application is using the webcam": "No other application is using the webcam", - "Already in call": "Already in call", - "You're already in a call with this person.": "You're already in a call with this person.", - "Calls are unsupported": "Calls are unsupported", - "You cannot place calls in this browser.": "You cannot place calls in this browser.", - "Connectivity to the server has been lost": "Connectivity to the server has been lost", - "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", - "Too Many Calls": "Too Many Calls", - "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", - "You cannot place a call with yourself.": "You cannot place a call with yourself.", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", - "Unable to transfer call": "Unable to transfer call", - "Transfer Failed": "Transfer Failed", - "Failed to transfer call": "Failed to transfer call", - "Permission Required": "Permission Required", - "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "We couldn't log you in": "We couldn't log you in", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", "Try again": "Try again", - "User is not logged in": "User is not logged in", - "Database unexpectedly closed": "Database unexpectedly closed", - "This may be caused by having the app open in multiple tabs or due to clearing browser data.": "This may be caused by having the app open in multiple tabs or due to clearing browser data.", - "Reload": "Reload", + "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.", + "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.", "Empty room": "Empty room", "%(user1)s and %(user2)s": "%(user1)s and %(user2)s", "%(user)s and %(count)s others|other": "%(user)s and %(count)s others", - "%(user)s and %(count)s others|one": "%(user)s and 1 other", "Inviting %(user1)s and %(user2)s": "Inviting %(user1)s and %(user2)s", "Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others", - "Inviting %(user)s and %(count)s others|one": "Inviting %(user)s and 1 other", "Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)", - "Default Device": "Default Device", "%(name)s is requesting verification": "%(name)s is requesting verification", - "%(senderName)s started a voice broadcast": "%(senderName)s started a voice broadcast", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again", "Unable to enable Notifications": "Unable to enable Notifications", "This email address was not found": "This email address was not found", - "Your email address does not appear to be associated with a Matrix ID on this homeserver.": "Your email address does not appear to be associated with a Matrix ID on this homeserver.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", "United Kingdom": "United Kingdom", "United States": "United States", "Afghanistan": "Afghanistan", @@ -374,10 +359,9 @@ "Zambia": "Zambia", "Zimbabwe": "Zimbabwe", "Sign In or Create Account": "Sign In or Create Account", - "Sign In": "Sign In", "Use your account or create a new one to continue.": "Use your account or create a new one to continue.", - "Use your account to continue.": "Use your account to continue.", "Create Account": "Create Account", + "Sign In": "Sign In", "Default": "Default", "Restricted": "Restricted", "Moderator": "Moderator", @@ -390,7 +374,6 @@ "Some invites couldn't be sent": "Some invites couldn't be sent", "You need to be logged in.": "You need to be logged in.", "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", - "You need to be able to kick users to do that.": "You need to be able to kick users to do that.", "Unable to create widget.": "Unable to create widget.", "Missing roomId.": "Missing roomId.", "Failed to send request.": "Failed to send request.", @@ -398,8 +381,6 @@ "Power level must be positive integer.": "Power level must be positive integer.", "You are not in this room.": "You are not in this room.", "You do not have permission to do that in this room.": "You do not have permission to do that in this room.", - "Failed to send event": "Failed to send event", - "Failed to read events": "Failed to read events", "Missing room_id in request": "Missing room_id in request", "Room %(roomId)s not visible": "Room %(roomId)s not visible", "Missing user_id in request": "Missing user_id in request", @@ -441,7 +422,6 @@ "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", "Continue": "Continue", "Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.", - "User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility": "User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility", "Joins room with given address": "Joins room with given address", "Leave room": "Leave room", "Unrecognised room address: %(roomAlias)s": "Unrecognised room address: %(roomAlias)s", @@ -461,13 +441,12 @@ "Opens the Developer Tools dialog": "Opens the Developer Tools dialog", "Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room", "Please supply a widget URL or embed code": "Please supply a widget URL or embed code", - "iframe has no src attribute": "iframe has no src attribute", "Please supply a https:// or http:// widget URL": "Please supply a https:// or http:// widget URL", "You cannot modify widgets in this room.": "You cannot modify widgets in this room.", "Verifies a user, session, and pubkey tuple": "Verifies a user, session, and pubkey tuple", "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)": "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", "Session already verified!": "Session already verified!", - "WARNING: session already verified, but keys do NOT MATCH!": "WARNING: session already verified, but keys do NOT MATCH!", + "WARNING: Session already verified, but keys do NOT MATCH!": "WARNING: Session already verified, but keys do NOT MATCH!", "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", "Verified key": "Verified key", "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.", @@ -487,12 +466,9 @@ "No active call in this room": "No active call in this room", "Takes the call in the current room off hold": "Takes the call in the current room off hold", "Converts the room to a DM": "Converts the room to a DM", - "Could not find room": "Could not find room", "Converts the DM to a room": "Converts the DM to a room", "Displays action": "Displays action", "Someone": "Someone", - "Video call started in %(roomName)s.": "Video call started in %(roomName)s.", - "Video call started in %(roomName)s. (not supported by this browser)": "Video call started in %(roomName)s. (not supported by this browser)", "%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)", "%(senderName)s placed a video call.": "%(senderName)s placed a video call.", @@ -502,7 +478,6 @@ "%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s", "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s banned %(targetName)s: %(reason)s", "%(senderName)s banned %(targetName)s": "%(senderName)s banned %(targetName)s", - "%(oldDisplayName)s changed their display name and profile picture": "%(oldDisplayName)s changed their display name and profile picture", "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s changed their display name to %(displayName)s", "%(senderName)s set their display name to %(displayName)s": "%(senderName)s set their display name to %(displayName)s", "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removed their display name (%(oldDisplayName)s)", @@ -541,9 +516,7 @@ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s added the alternative addresses %(addresses)s for this room.", - "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s added alternative address %(addresses)s for this room.", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s removed the alternative addresses %(addresses)s for this room.", - "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s removed alternative address %(addresses)s for this room.", "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.", "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.", "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.", @@ -593,7 +566,6 @@ "Dark": "Dark", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", - "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", "Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running", "Remain on your screen while running": "Remain on your screen while running", @@ -658,38 +630,6 @@ "Send %(msgtype)s messages as you in your active room": "Send %(msgtype)s messages as you in your active room", "See %(msgtype)s messages posted to this room": "See %(msgtype)s messages posted to this room", "See %(msgtype)s messages posted to your active room": "See %(msgtype)s messages posted to your active room", - "Can't start a new voice broadcast": "Can't start a new voice broadcast", - "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.", - "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.", - "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.", - "Connection error": "Connection error", - "Unfortunately we're unable to start a recording right now. Please try again later.": "Unfortunately we're unable to start a recording right now. Please try again later.", - "Can’t start a call": "Can’t start a call", - "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.", - "You ended a voice broadcast": "You ended a voice broadcast", - "%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast", - "You ended a voice broadcast": "You ended a voice broadcast", - "%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast", - "Unable to decrypt voice broadcast": "Unable to decrypt voice broadcast", - "Unable to play this voice broadcast": "Unable to play this voice broadcast", - "Stop live broadcasting?": "Stop live broadcasting?", - "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.", - "Yes, stop broadcast": "Yes, stop broadcast", - "Listen to live broadcast?": "Listen to live broadcast?", - "If you start listening to this live broadcast, your current live broadcast recording will be ended.": "If you start listening to this live broadcast, your current live broadcast recording will be ended.", - "Yes, end my recording": "Yes, end my recording", - "No": "No", - "30s backward": "30s backward", - "30s forward": "30s forward", - "Go live": "Go live", - "resume voice broadcast": "resume voice broadcast", - "pause voice broadcast": "pause voice broadcast", - "Change input device": "Change input device", - "Live": "Live", - "Voice broadcast": "Voice broadcast", - "Buffering…": "Buffering…", - "play voice broadcast": "play voice broadcast", - "Connection error - Recording paused": "Connection error - Recording paused", "Cannot reach homeserver": "Cannot reach homeserver", "Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin", "Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured", @@ -704,16 +644,10 @@ "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", - "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", - "Unable to connect to Homeserver. Retrying…": "Unable to connect to Homeserver. Retrying…", - "This account has been deactivated.": "This account has been deactivated.", - "Incorrect username and/or password.": "Incorrect username and/or password.", - "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", - "There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.", - "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", - "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", + "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", + "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", + "Attachment": "Attachment", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", - "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "a few seconds ago": "a few seconds ago", "about a minute ago": "about a minute ago", @@ -732,11 +666,7 @@ "%(space1Name)s and %(space2Name)s": "%(space1Name)s and %(space2Name)s", "In spaces %(space1Name)s and %(space2Name)s.": "In spaces %(space1Name)s and %(space2Name)s.", "%(spaceName)s and %(count)s others|other": "%(spaceName)s and %(count)s others", - "%(spaceName)s and %(count)s others|zero": "%(spaceName)s", - "%(spaceName)s and %(count)s others|one": "%(spaceName)s and %(count)s other", "In %(spaceName)s and %(count)s other spaces.|other": "In %(spaceName)s and %(count)s other spaces.", - "In %(spaceName)s and %(count)s other spaces.|zero": "In space %(spaceName)s.", - "In %(spaceName)s and %(count)s other spaces.|one": "In %(spaceName)s and %(count)s other space.", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "Unexpected server error trying to leave the room": "Unexpected server error trying to leave the room", "Can't leave Server Notices room": "Can't leave Server Notices room", @@ -785,15 +715,19 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", + "Unnamed room": "Unnamed room", + "Unable to join network": "Unable to join network", + "%(brand)s does not know how to join a room on this network": "%(brand)s does not know how to join a room on this network", + "Room not found": "Room not found", + "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", + "Fetching third party location failed": "Fetching third party location failed", + "Unable to look up room ID from server": "Unable to look up room ID from server", "Error upgrading room": "Error upgrading room", "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Share your public space": "Share your public space", "Unknown App": "Unknown App", - "No media permissions": "No media permissions", - "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "This homeserver is not configured to display maps.": "This homeserver is not configured to display maps.", - "WebGL is required to display maps, please enable it in your browser settings.": "WebGL is required to display maps, please enable it in your browser settings.", "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.", "Toggle attribution": "Toggle attribution", "Map feedback": "Map feedback", @@ -805,17 +739,10 @@ "Reset bearing to north": "Reset bearing to north", "Zoom in": "Zoom in", "Zoom out": "Zoom out", - "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.", - "Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.", - "Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.", - "Unknown error fetching location. Please try again later.": "Unknown error fetching location. Please try again later.", "Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?", - "Unnamed Room": "Unnamed Room", "Generating a ZIP": "Generating a ZIP", "Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s", - "Fetched %(count)s events out of %(total)s|one": "Fetched %(count)s event out of %(total)s", "Fetched %(count)s events so far|other": "Fetched %(count)s events so far", - "Fetched %(count)s events so far|one": "Fetched %(count)s event so far", "HTML": "HTML", "JSON": "JSON", "Plain Text": "Plain Text", @@ -829,19 +756,17 @@ "Topic: %(topic)s": "Topic: %(topic)s", "Error fetching file": "Error fetching file", "Processing event %(number)s out of %(total)s": "Processing event %(number)s out of %(total)s", - "Starting export…": "Starting export…", + "Starting export...": "Starting export...", "Fetched %(count)s events in %(seconds)ss|other": "Fetched %(count)s events in %(seconds)ss", - "Fetched %(count)s events in %(seconds)ss|one": "Fetched %(count)s event in %(seconds)ss", - "Creating HTML…": "Creating HTML…", + "Creating HTML...": "Creating HTML...", "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", - "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", - "Starting export process…": "Starting export process…", - "Fetching events…": "Fetching events…", - "Creating output…": "Creating output…", + "Starting export process...": "Starting export process...", + "Fetching events...": "Fetching events...", + "Creating output...": "Creating output...", "Enable": "Enable", "That's fine": "That's fine", "Stop": "Stop", @@ -849,27 +774,22 @@ "Learn more": "Learn more", "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More": "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More", "Yes": "Yes", + "No": "No", "Help improve %(analyticsOwner)s": "Help improve %(analyticsOwner)s", - "You have unverified sessions": "You have unverified sessions", + "You have unverified logins": "You have unverified logins", "Review to ensure your account is safe": "Review to ensure your account is safe", "Review": "Review", "Later": "Later", "Don't miss a reply": "Don't miss a reply", "Notifications": "Notifications", "Enable desktop notifications": "Enable desktop notifications", - "Join": "Join", - "Unknown room": "Unknown room", - "Video call started": "Video call started", - "Video": "Video", - "Close": "Close", - "Sound on": "Sound on", - "Silence call": "Silence call", - "Notifications silenced": "Notifications silenced", "Unknown caller": "Unknown caller", "Voice call": "Voice call", "Video call": "Video call", "Decline": "Decline", "Accept": "Accept", + "Sound on": "Sound on", + "Silence call": "Silence call", "Use app for a better experience": "Use app for a better experience", "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.": "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.", "Use app": "Use app", @@ -886,7 +806,8 @@ "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data", "Other users may not trust it": "Other users may not trust it", "New login. Was this you?": "New login. Was this you?", - "Yes, it was me": "Yes, it was me", + "%(deviceId)s from %(ip)s": "%(deviceId)s from %(ip)s", + "Check your devices": "Check your devices", "What's new?": "What's new?", "What's New": "What's New", "Update": "Update", @@ -898,8 +819,6 @@ "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "The person who invited you has already left.": "The person who invited you has already left.", "The person who invited you has already left, or their server is offline.": "The person who invited you has already left, or their server is offline.", - "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.": "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.", - "If you know a room address, try joining through that instead.": "If you know a room address, try joining through that instead.", "Failed to join": "Failed to join", "Connection lost": "Connection lost", "You were disconnected from the call. (Error: %(message)s)": "You were disconnected from the call. (Error: %(message)s)", @@ -920,29 +839,22 @@ "%(senderName)s is calling": "%(senderName)s is calling", "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s", "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", - "You reacted %(reaction)s to %(message)s": "You reacted %(reaction)s to %(message)s", - "%(sender)s reacted %(reaction)s to %(message)s": "%(sender)s reacted %(reaction)s to %(message)s", + "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Threads": "Threads", "Back to chat": "Back to chat", "Room information": "Room information", "Room members": "Room members", "Back to thread": "Back to thread", - "None": "None", - "Bold": "Bold", - "Grey": "Grey", - "Red": "Red", - "Unsent": "Unsent", - "unknown": "unknown", "Change notification settings": "Change notification settings", "Messaging": "Messaging", "Profile": "Profile", "Spaces": "Spaces", "Widgets": "Widgets", "Rooms": "Rooms", - "Voice & Video": "Voice & Video", "Moderation": "Moderation", "Analytics": "Analytics", + "Message Previews": "Message Previews", "Themes": "Themes", "Encryption": "Encryption", "Experimental": "Experimental", @@ -956,50 +868,42 @@ "Yes, the chat timeline is displayed alongside the video.": "Yes, the chat timeline is displayed alongside the video.", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", "Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog", - "Requires your server to support the stable version of MSC3827": "Requires your server to support the stable version of MSC3827", "Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.", - "Report to moderators": "Report to moderators", - "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", + "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators", "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Message Pinning": "Message Pinning", - "Rich text editor": "Rich text editor", - "Use rich text instead of Markdown in the message composer.": "Use rich text instead of Markdown in the message composer.", + "Threaded messaging": "Threaded messaging", + "Keep discussions organised with threads.": "Keep discussions organised with threads.", + "Threads help keep conversations on-topic and easy to track. Learn more.": "Threads help keep conversations on-topic and easy to track. Learn more.", + "How can I start a thread?": "How can I start a thread?", + "Use “%(replyInThread)s” when hovering over a message.": "Use “%(replyInThread)s” when hovering over a message.", + "Reply in thread": "Reply in thread", + "How can I leave the beta?": "How can I leave the beta?", + "To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.", + "Leave the beta": "Leave the beta", "Render simple counters in room header": "Render simple counters in room header", - "New ways to ignore people": "New ways to ignore people", - "Currently experimental.": "Currently experimental.", + "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Support adding custom themes": "Support adding custom themes", + "Show message previews for reactions in DMs": "Show message previews for reactions in DMs", + "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", + "Show extensible event representation of events": "Show extensible event representation of events", "Show current avatar and name for users in message history": "Show current avatar and name for users in message history", "Show HTML representation of room topics": "Show HTML representation of room topics", "Show info about bridges in room settings": "Show info about bridges in room settings", "Use new room breadcrumbs": "Use new room breadcrumbs", - "Right panel stays open": "Right panel stays open", - "Defaults to room member list.": "Defaults to room member list.", + "Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)", "Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)", - "Requires your server to support MSC3030": "Requires your server to support MSC3030", "Send read receipts": "Send read receipts", - "Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.", - "Sliding Sync mode": "Sliding Sync mode", - "Under active development, cannot be disabled.": "Under active development, cannot be disabled.", - "Element Call video rooms": "Element Call video rooms", - "New group call experience": "New group call experience", - "Live Location Sharing": "Live Location Sharing", - "Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.", - "Dynamic room predecessors": "Dynamic room predecessors", - "Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)", - "Favourite Messages": "Favourite Messages", - "Under active development.": "Under active development.", - "Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length", - "Enable new native OIDC flows (Under active development)": "Enable new native OIDC flows (Under active development)", - "Rust cryptography implementation": "Rust cryptography implementation", + "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", + "Favourite Messages (under active development)": "Favourite Messages (under active development)", + "Use new session manager (under active development)": "Use new session manager (under active development)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Show stickers button": "Show stickers button", "Show polls button": "Show polls button", "Insert a trailing colon after user mentions at the start of a message": "Insert a trailing colon after user mentions at the start of a message", - "Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)", - "Enable intentional mentions": "Enable intentional mentions", "Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)", @@ -1014,7 +918,7 @@ "Expand code blocks by default": "Expand code blocks by default", "Show line numbers in code blocks": "Show line numbers in code blocks", "Jump to the bottom of the timeline when you send a message": "Jump to the bottom of the timeline when you send a message", - "Show avatars in user, room and event mentions": "Show avatars in user, room and event mentions", + "Show avatars in user and room mentions": "Show avatars in user and room mentions", "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", @@ -1025,19 +929,13 @@ "Surround selected text when typing special characters": "Surround selected text when typing special characters", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Enable Markdown": "Enable Markdown", - "Start messages with /plain to send without markdown.": "Start messages with /plain to send without markdown.", + "Start messages with /plain to send without markdown and /md to send with.": "Start messages with /plain to send without markdown and /md to send with.", "Mirror local video feed": "Mirror local video feed", "Match system theme": "Match system theme", "Use a system font": "Use a system font", "System font name": "System font name", - "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", - "When enabled, the other party might be able to see your IP address": "When enabled, the other party might be able to see your IP address", - "Automatic gain control": "Automatic gain control", - "Echo cancellation": "Echo cancellation", - "Noise suppression": "Noise suppression", - "Show NSFW content": "Show NSFW content", + "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)", "Send analytics data": "Send analytics data", - "Record the client name, version, and url to recognise sessions more easily in session manager": "Record the client name, version, and url to recognise sessions more easily in session manager", "Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session", "Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session", "Enable inline URL previews by default": "Enable inline URL previews by default", @@ -1045,12 +943,13 @@ "Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", + "Order rooms by name": "Order rooms by name", + "Show rooms with unread notifications first": "Show rooms with unread notifications first", "Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list", "Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list", "Show hidden events in timeline": "Show hidden events in timeline", - "Low bandwidth mode": "Low bandwidth mode", - "Requires compatible homeserver.": "Requires compatible homeserver.", - "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.", + "Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)", + "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", "Show previews/thumbnails for images": "Show previews/thumbnails for images", "Enable message search in encrypted rooms": "Enable message search in encrypted rooms", "How fast should messages be downloaded.": "How fast should messages be downloaded.", @@ -1068,8 +967,10 @@ "Always show the window menu bar": "Always show the window menu bar", "Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close", "Enable hardware acceleration": "Enable hardware acceleration", - "Can currently only be enabled via config.json": "Can currently only be enabled via config.json", - "Log out and back in to disable": "Log out and back in to disable", + "Partial Support for Threads": "Partial Support for Threads", + "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.", + "Do you want to enable threads anyway?": "Do you want to enable threads anyway?", + "Yes, enable": "Yes, enable", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", @@ -1088,10 +989,6 @@ "When rooms are upgraded": "When rooms are upgraded", "My Ban List": "My Ban List", "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", - "Connecting": "Connecting", - "Sorry — this call is currently full": "Sorry — this call is currently full", - "User": "User", - "Room": "Room", "Create account": "Create account", "You made it!": "You made it!", "Find and invite your friends": "Find and invite your friends", @@ -1130,16 +1027,16 @@ "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", - "Failed to download source media, no source url was found": "Failed to download source media, no source url was found", + "%(count)s people joined|other": "%(count)s people joined", "Audio devices": "Audio devices", + "Audio input %(n)s": "Audio input %(n)s", "Mute microphone": "Mute microphone", "Unmute microphone": "Unmute microphone", "Video devices": "Video devices", + "Video input %(n)s": "Video input %(n)s", "Turn off camera": "Turn off camera", "Turn on camera": "Turn on camera", - "%(count)s people joined|other": "%(count)s people joined", - "%(count)s people joined|one": "%(count)s person joined", - "Dial": "Dial", + "Join": "Join", "You are presenting": "You are presenting", "%(sharerName)s is presenting": "%(sharerName)s is presenting", "Your camera is turned off": "Your camera is turned off", @@ -1149,6 +1046,8 @@ "You held the call Switch": "You held the call Switch", "You held the call Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", + "Connecting": "Connecting", + "Dial": "Dial", "Dialpad": "Dialpad", "Mute the microphone": "Mute the microphone", "Unmute the microphone": "Unmute the microphone", @@ -1160,7 +1059,7 @@ "Show sidebar": "Show sidebar", "More": "More", "Hangup": "Hangup", - "Fill screen": "Fill screen", + "Fill Screen": "Fill Screen", "Pin": "Pin", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", @@ -1246,6 +1145,9 @@ "Headphones": "Headphones", "Folder": "Folder", "Welcome": "Welcome", + "How are you finding %(brand)s so far?": "How are you finding %(brand)s so far?", + "We’d appreciate any feedback on how you’re finding %(brand)s.": "We’d appreciate any feedback on how you’re finding %(brand)s.", + "Feedback": "Feedback", "Secure messaging for friends and family": "Secure messaging for friends and family", "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.", "Start your first chat": "Start your first chat", @@ -1256,14 +1158,10 @@ "Find your people": "Find your people", "Welcome to %(brand)s": "Welcome to %(brand)s", "Only %(count)s steps to go|other": "Only %(count)s steps to go", - "Only %(count)s steps to go|one": "Only %(count)s step to go", "You did it!": "You did it!", "Complete these to get the most out of %(brand)s": "Complete these to get the most out of %(brand)s", "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", - "%(deviceId)s from %(ip)s": "%(deviceId)s from %(ip)s", - "Ignore": "Ignore", - "Ignore (%(counter)s)": "Ignore (%(counter)s)", - "Verify Session": "Verify Session", + "Decline (%(counter)s)": "Decline (%(counter)s)", "Accept to continue:": "Accept to continue:", "Quick settings": "Quick settings", "All settings": "All settings", @@ -1283,6 +1181,10 @@ "No results": "No results", "Search %(spaceName)s": "Search %(spaceName)s", "Please enter a name for the space": "Please enter a name for the space", + "Spaces are a new feature.": "Spaces are a new feature.", + "Spaces feedback": "Spaces feedback", + "Thank you for trying Spaces. Your feedback will help inform the next versions.": "Thank you for trying Spaces. Your feedback will help inform the next versions.", + "Give feedback.": "Give feedback.", "e.g. my-space": "e.g. my-space", "Address": "Address", "Create a space": "Create a space", @@ -1297,7 +1199,7 @@ "Your private space": "Your private space", "Add some details to help people recognise it.": "Add some details to help people recognise it.", "You can change these anytime.": "You can change these anytime.", - "Creating…": "Creating…", + "Creating...": "Creating...", "Create": "Create", "Show all rooms": "Show all rooms", "Options": "Options", @@ -1312,7 +1214,7 @@ "Failed to save space settings.": "Failed to save space settings.", "General": "General", "Edit settings relating to your space.": "Edit settings relating to your space.", - "Saving…": "Saving…", + "Saving...": "Saving...", "Save Changes": "Save Changes", "Leave Space": "Leave Space", "Failed to update the guest access of this space": "Failed to update the guest access of this space", @@ -1332,23 +1234,19 @@ "Jump to first unread room.": "Jump to first unread room.", "Jump to first invite.": "Jump to first invite.", "Space options": "Space options", - "Failed to change power level": "Failed to change power level", - "Add privileged users": "Add privileged users", - "Give one or multiple users in this room more privileges": "Give one or multiple users in this room more privileges", - "Search users in this room…": "Search users in this room…", - "Apply": "Apply", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", "Workspace: ": "Workspace: ", "Channel: ": "Channel: ", + "Failed to upload profile picture!": "Failed to upload profile picture!", + "Upload new:": "Upload new:", "No display name": "No display name", "Warning!": "Warning!", "Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable.": "Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable.", "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.": "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.", "You can also ask your homeserver admin to upgrade the server to change this behaviour.": "You can also ask your homeserver admin to upgrade the server to change this behaviour.", "Export E2E room keys": "Export E2E room keys", - "Error while changing password: %(error)s": "Error while changing password: %(error)s", "New passwords don't match": "New passwords don't match", "Passwords can't be empty": "Passwords can't be empty", "Do you want to set an email address?": "Do you want to set an email address?", @@ -1381,9 +1279,22 @@ "Cryptography": "Cryptography", "Session ID:": "Session ID:", "Session key:": "Session key:", + "Your homeserver does not support device management.": "Your homeserver does not support device management.", + "Unable to load device list": "Unable to load device list", + "Deselect all": "Deselect all", + "Select all": "Select all", + "Verified devices": "Verified devices", + "Unverified devices": "Unverified devices", + "Devices without encryption support": "Devices without encryption support", + "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", + "You aren't signed into any other devices.": "You aren't signed into any other devices.", + "This device": "This device", + "Failed to set display name": "Failed to set display name", + "Sign Out": "Sign Out", + "Display Name": "Display Name", + "Rename": "Rename", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", - "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", "Manage": "Manage", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", @@ -1395,7 +1306,7 @@ "Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt", "Image size in the timeline": "Image size in the timeline", "Large": "Large", - "Connecting to integration manager…": "Connecting to integration manager…", + "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", "Integration manager": "Integration manager", @@ -1404,9 +1315,7 @@ "Anyone can find and join.": "Anyone can find and join.", "Upgrade required": "Upgrade required", "& %(count)s more|other": "& %(count)s more", - "& %(count)s more|one": "& %(count)s more", "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", - "Currently, %(count)s spaces have access|one": "Currently, a space has access", "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", "Spaces with access": "Spaces with access", "Anyone in can find and join. You can select other spaces too.": "Anyone in can find and join. You can select other spaces too.", @@ -1417,9 +1326,7 @@ "Upgrading room": "Upgrading room", "Loading new room": "Loading new room", "Sending invites... (%(progress)s out of %(count)s)|other": "Sending invites... (%(progress)s out of %(count)s)", - "Sending invites... (%(progress)s out of %(count)s)|one": "Sending invite...", "Updating spaces... (%(progress)s out of %(count)s)|other": "Updating spaces... (%(progress)s out of %(count)s)", - "Updating spaces... (%(progress)s out of %(count)s)|one": "Updating space...", "Message layout": "Message layout", "IRC (Experimental)": "IRC (Experimental)", "Modern": "Modern", @@ -1427,38 +1334,35 @@ "Messages containing keywords": "Messages containing keywords", "Error saving notification preferences": "Error saving notification preferences", "An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.", - "Enable notifications for this account": "Enable notifications for this account", - "Turn off to disable notifications on all your devices and sessions": "Turn off to disable notifications on all your devices and sessions", + "Enable for this account": "Enable for this account", "Enable email notifications for %(email)s": "Enable email notifications for %(email)s", - "Enable notifications for this device": "Enable notifications for this device", "Enable desktop notifications for this session": "Enable desktop notifications for this session", "Show message in desktop notification": "Show message in desktop notification", "Enable audible notifications for this session": "Enable audible notifications for this session", - "Mark all as read": "Mark all as read", + "Clear notifications": "Clear notifications", "Keyword": "Keyword", "New keyword": "New keyword", "On": "On", "Off": "Off", "Noisy": "Noisy", - "An error occurred when updating your notification preferences. Please try to toggle your option again.": "An error occurred when updating your notification preferences. Please try to toggle your option again.", "Global": "Global", "Mentions & keywords": "Mentions & keywords", "Notification targets": "Notification targets", "There was an error loading your notification settings.": "There was an error loading your notification settings.", "Failed to save your profile": "Failed to save your profile", "The operation could not be completed": "The operation could not be completed", - "Display Name": "Display Name", + "Upgrade to your own domain": "Upgrade to your own domain", "Profile picture": "Profile picture", "Save": "Save", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Unable to load key backup status": "Unable to load key backup status", "Restore from Backup": "Restore from Backup", - "This session is backing up your keys.": "This session is backing up your keys.", + "This session is backing up your keys. ": "This session is backing up your keys. ", "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.", "Connect this session to Key Backup": "Connect this session to Key Backup", - "Backing up %(sessionsRemaining)s keys…": "Backing up %(sessionsRemaining)s keys…", + "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", "All keys backed up": "All keys backed up", "Backup has a valid signature from this user": "Backup has a valid signature from this user", "Backup has a invalid signature from this user": "Backup has a invalid signature from this user", @@ -1529,42 +1433,34 @@ "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", - "Checking for an update…": "Checking for an update…", + "Checking for an update...": "Checking for an update...", "No update available.": "No update available.", - "Downloading update…": "Downloading update…", + "Downloading update...": "Downloading update...", "New version available. Update now.": "New version available. Update now.", "Check for update": "Check for update", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "Customise your appearance": "Customise your appearance", "Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.", - "Unknown password change error (%(stringifiedError)s)": "Unknown password change error (%(stringifiedError)s)", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", - "%(errorMessage)s (HTTP status %(httpStatus)s)": "%(errorMessage)s (HTTP status %(httpStatus)s)", - "Error changing password": "Error changing password", "Your password was successfully changed.": "Your password was successfully changed.", "You will not receive push notifications on other devices until you sign back in to them.": "You will not receive push notifications on other devices until you sign back in to them.", "Success": "Success", "Email addresses": "Email addresses", "Phone numbers": "Phone numbers", - "Set a new account password…": "Set a new account password…", - "Your account details are managed separately at %(hostname)s.": "Your account details are managed separately at %(hostname)s.", - "Manage account": "Manage account", + "Set a new account password...": "Set a new account password...", "Account": "Account", "Language and region": "Language and region", "Spell check": "Spell check", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", - "Deactivate account": "Deactivate account", "Account management": "Account management", "Deactivating your account is a permanent action — be careful!": "Deactivating your account is a permanent action — be careful!", "Deactivate Account": "Deactivate Account", + "Deactivate account": "Deactivate account", "Discovery": "Discovery", "%(brand)s version:": "%(brand)s version:", "Olm version:": "Olm version:", "Legal": "Legal", "Credits": "Credits", - "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.": "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.", - "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0.": "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0.", - "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.": "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.", "For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "For help with using %(brand)s, click here or start a chat with our bot using the button below.", "Chat with %(brand)s Bot": "Chat with %(brand)s Bot", @@ -1577,16 +1473,14 @@ "FAQ": "FAQ", "Keyboard Shortcuts": "Keyboard Shortcuts", "Versions": "Versions", - "Homeserver is %(homeserverUrl)s": "Homeserver is %(homeserverUrl)s", - "Identity server is %(identityServerUrl)s": "Identity server is %(identityServerUrl)s", + "Homeserver is": "Homeserver is", + "Identity server is": "Identity server is", "Access Token": "Access Token", "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", "Clear cache and reload": "Clear cache and reload", "Keyboard": "Keyboard", - "Upcoming features": "Upcoming features", - "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.": "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.", - "Early previews": "Early previews", - "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.", + "Labs": "Labs", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", @@ -1595,9 +1489,11 @@ "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", + "None": "None", "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", "Server rules": "Server rules", "User rules": "User rules", + "Close": "Close", "You have not ignored anyone.": "You have not ignored anyone.", "You are currently ignoring:": "You are currently ignoring:", "You are not subscribed to any lists": "You are not subscribed to any lists", @@ -1609,9 +1505,10 @@ "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", "Personal ban list": "Personal ban list", - "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named '%(myBanList)s' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named '%(myBanList)s' - stay in this room to keep the ban list in effect.", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", "Server or user ID to ignore": "Server or user ID to ignore", "eg: @bot:* or example.org": "eg: @bot:* or example.org", + "Ignore": "Ignore", "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", @@ -1624,11 +1521,11 @@ "Displaying time": "Displaying time", "Presence": "Presence", "Share your activity and status with others.": "Share your activity and status with others.", + "Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.", "Composer": "Composer", "Code blocks": "Code blocks", "Images, GIFs and videos": "Images, GIFs and videos", "Timeline": "Timeline", - "Room directory": "Room directory", "Enable hardware acceleration (restart %(appName)s to take effect)": "Enable hardware acceleration (restart %(appName)s to take effect)", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", @@ -1642,12 +1539,13 @@ "Message search": "Message search", "Cross-signing": "Cross-signing", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", + "Okay": "Okay", "Privacy": "Privacy", "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", + "Where you're signed in": "Where you're signed in", + "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", "Sessions": "Sessions", - "Sign out": "Sign out", - "Are you sure you want to sign out of %(count)s sessions?|other": "Are you sure you want to sign out of %(count)s sessions?", - "Are you sure you want to sign out of %(count)s sessions?|one": "Are you sure you want to sign out of %(count)s session?", + "Other sessions": "Other sessions", "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", @@ -1658,6 +1556,9 @@ "Group all your people in one place.": "Group all your people in one place.", "Rooms outside of a space": "Rooms outside of a space", "Group all your rooms that aren't part of a space in one place.": "Group all your rooms that aren't part of a space in one place.", + "Default Device": "Default Device", + "No media permissions": "No media permissions", + "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", "Request media permissions": "Request media permissions", "Audio Output": "Audio Output", @@ -1666,14 +1567,9 @@ "No Microphones detected": "No Microphones detected", "Camera": "Camera", "No Webcams detected": "No Webcams detected", - "Voice settings": "Voice settings", - "Automatically adjust the microphone volume": "Automatically adjust the microphone volume", - "Video settings": "Video settings", - "Voice processing": "Voice processing", - "Connection": "Connection", - "Allow fallback call assist server (%(server)s)": "Allow fallback call assist server (%(server)s)", + "Voice & Video": "Voice & Video", "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", - "Warning: upgrading a room will not automatically migrate room members to the new version of the room. 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.": "Warning: upgrading a room will not automatically migrate room members to the new version of the room. 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.", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. 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.": "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. 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.", "Upgrade this space to the recommended room version": "Upgrade this space to the recommended room version", "Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version", "View older version of %(spaceName)s.": "View older version of %(spaceName)s.", @@ -1697,7 +1593,6 @@ "Sounds": "Sounds", "Notification sound": "Notification sound", "Set a new custom sound": "Set a new custom sound", - "Upload custom sound": "Upload custom sound", "Browse": "Browse", "Failed to unban": "Failed to unban", "Unban": "Unban", @@ -1724,10 +1619,7 @@ "Send reactions": "Send reactions", "Remove messages sent by me": "Remove messages sent by me", "Modify widgets": "Modify widgets", - "Voice broadcasts": "Voice broadcasts", "Manage pinned events": "Manage pinned events", - "Start %(brand)s calls": "Start %(brand)s calls", - "Join %(brand)s calls": "Join %(brand)s calls", "Default role": "Default role", "Send messages": "Send messages", "Invite users": "Invite users", @@ -1767,10 +1659,6 @@ "Security & Privacy": "Security & Privacy", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Encrypted": "Encrypted", - "Enable %(brand)s as an additional calling option in this room": "Enable %(brand)s as an additional calling option in this room", - "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.", - "You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.", - "Call type": "Call type", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", "Your email address hasn't been verified yet": "Your email address hasn't been verified yet", @@ -1788,70 +1676,32 @@ "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", - "Sign out of all other sessions (%(otherSessionsCount)s)": "Sign out of all other sessions (%(otherSessionsCount)s)", "Current session": "Current session", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", - "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", "Confirm signing out these devices|other": "Confirm signing out these devices", - "Confirm signing out these devices|one": "Confirm signing out this device", "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", - "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", "Sign out devices|other": "Sign out devices", - "Sign out devices|one": "Sign out device", "Authentication": "Authentication", - "Failed to set display name": "Failed to set display name", - "Rename session": "Rename session", - "Please be aware that session names are also visible to people you communicate with.": "Please be aware that session names are also visible to people you communicate with.", - "Renaming sessions": "Renaming sessions", - "Other users in direct messages and rooms that you join are able to view a full list of your sessions.": "Other users in direct messages and rooms that you join are able to view a full list of your sessions.", - "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.": "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.", - "Rename": "Rename", "Session ID": "Session ID", "Last activity": "Last activity", - "Application": "Application", - "Version": "Version", - "URL": "URL", "Device": "Device", - "Model": "Model", - "Operating system": "Operating system", - "Browser": "Browser", "IP address": "IP address", "Session details": "Session details", - "Toggle push notifications on this session.": "Toggle push notifications on this session.", - "Push notifications": "Push notifications", - "Receive push notifications on this session.": "Receive push notifications on this session.", - "Sign out of this session": "Sign out of this session", - "Hide details": "Hide details", - "Show details": "Show details", + "Toggle device details": "Toggle device details", "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", "Verified": "Verified", "Unverified": "Unverified", - "Verified sessions": "Verified sessions", - "Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.": "Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.", - "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.": "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.", - "Unverified sessions": "Unverified sessions", - "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.", - "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.", - "Unverified session": "Unverified session", - "This session doesn't support encryption and thus can't be verified.": "This session doesn't support encryption and thus can't be verified.", - "You won't be able to participate in rooms where encryption is enabled when using this session.": "You won't be able to participate in rooms where encryption is enabled when using this session.", - "For best security and privacy, it is recommended to use Matrix clients that support encryption.": "For best security and privacy, it is recommended to use Matrix clients that support encryption.", - "Inactive sessions": "Inactive sessions", - "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.", - "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.", - "Desktop session": "Desktop session", - "Mobile session": "Mobile session", - "Web session": "Web session", - "Unknown session type": "Unknown session type", - "Your current session is ready for secure messaging.": "Your current session is ready for secure messaging.", - "This session is ready for secure messaging.": "This session is ready for secure messaging.", + "Unknown device type": "Unknown device type", "Verified session": "Verified session", - "Verify your current session for enhanced secure messaging.": "Verify your current session for enhanced secure messaging.", + "This session is ready for secure messaging.": "This session is ready for secure messaging.", + "Unverified session": "Unverified session", "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", - "Verify session": "Verify session", + "Verified sessions": "Verified sessions", "For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.", + "Unverified sessions": "Unverified sessions", "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", - "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.", + "Inactive sessions": "Inactive sessions", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", "No verified sessions found.": "No verified sessions found.", "No unverified sessions found.": "No unverified sessions found.", "No inactive sessions found.": "No inactive sessions found.", @@ -1864,20 +1714,9 @@ "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer", "Filter devices": "Filter devices", "Show": "Show", - "Deselect all": "Deselect all", - "Select all": "Select all", - "%(count)s sessions selected|other": "%(count)s sessions selected", - "%(count)s sessions selected|one": "%(count)s session selected", - "Sign in with QR code": "Sign in with QR code", - "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", - "Show QR code": "Show QR code", - "Sign out of %(count)s sessions|other": "Sign out of %(count)s sessions", - "Sign out of %(count)s sessions|one": "Sign out of %(count)s session", - "Other sessions": "Other sessions", "Security recommendations": "Security recommendations", - "Improve your account security by following these recommendations.": "Improve your account security by following these recommendations.", + "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", "View all": "View all", - "Failed to set pusher state": "Failed to set pusher state", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", @@ -1895,19 +1734,24 @@ "This room is end-to-end encrypted": "This room is end-to-end encrypted", "Everyone in this room is verified": "Everyone in this room is verified", "Edit message": "Edit message", - "Emoji": "Emoji", "Mod": "Mod", - "Invite": "Invite", "From a thread": "From a thread", "This event could not be displayed": "This event could not be displayed", - " in %(room)s": " in %(room)s", + "Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.", + "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.", + "If your other sessions do not have the key for this message you will not be able to decrypt them.": "If your other sessions do not have the key for this message you will not be able to decrypt them.", + "Key request sent.": "Key request sent.", + "Re-request encryption keys from your other sessions.": "Re-request encryption keys from your other sessions.", + "Message Actions": "Message Actions", + "View in room": "View in room", + "Copy link to thread": "Copy link to thread", + "This message cannot be decrypted": "This message cannot be decrypted", "Encrypted by an unverified session": "Encrypted by an unverified session", "Unencrypted": "Unencrypted", "Encrypted by a deleted session": "Encrypted by a deleted session", "The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.", - "This message could not be decrypted": "This message could not be decrypted", - "Sending your message…": "Sending your message…", - "Encrypting your message…": "Encrypting your message…", + "Sending your message...": "Sending your message...", + "Encrypting your message...": "Encrypting your message...", "Your message was sent": "Your message was sent", "Failed to send": "Failed to send", "You don't have permission to view messages from before you were invited.": "You don't have permission to view messages from before you were invited.", @@ -1916,15 +1760,10 @@ "You can't see earlier messages": "You can't see earlier messages", "Scroll to most recent messages": "Scroll to most recent messages", "Show %(count)s other previews|other": "Show %(count)s other previews", - "Show %(count)s other previews|one": "Show %(count)s other preview", "Close preview": "Close preview", - "%(count)s participants|other": "%(count)s participants", - "%(count)s participants|one": "1 participant", "and %(count)s others...|other": "and %(count)s others...", - "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", "Invite to this space": "Invite to this space", - "You do not have permission to invite users": "You do not have permission to invite users", "Invited": "Invited", "Filter room members": "Filter room members", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", @@ -1938,6 +1777,7 @@ "The conversation continues here.": "The conversation continues here.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", + "%(seconds)ss left": "%(seconds)ss left", "Send voice message": "Send voice message", "Emoji": "Emoji", "Hide stickers": "Hide stickers", @@ -1945,17 +1785,14 @@ "Voice Message": "Voice Message", "You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.", "Poll": "Poll", - "Hide formatting": "Hide formatting", - "Show formatting": "Show formatting", - "Formatting": "Formatting", + "Bold": "Bold", "Italics": "Italics", "Strikethrough": "Strikethrough", "Code block": "Code block", "Quote": "Quote", "Insert link": "Insert link", - "Send your first message to invite to chat": "Send your first message to invite to chat", - "Once everyone has joined, you’ll be able to chat": "Once everyone has joined, you’ll be able to chat", "This is the beginning of your direct message history with .": "This is the beginning of your direct message history with .", + "Send your first message to invite to chat": "Send your first message to invite to chat", "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.", "Topic: %(topic)s (edit)": "Topic: %(topic)s (edit)", "Topic: %(topic)s ": "Topic: %(topic)s ", @@ -1984,35 +1821,25 @@ "Idle": "Idle", "Offline": "Offline", "Unknown": "Unknown", + "Preview": "Preview", + "View": "View", "%(members)s and more": "%(members)s and more", "%(members)s and %(last)s": "%(members)s and %(last)s", "Seen by %(count)s people|other": "Seen by %(count)s people", - "Seen by %(count)s people|one": "Seen by %(count)s person", "Read receipts": "Read receipts", "Recently viewed": "Recently viewed", "Replying": "Replying", "Room %(name)s": "Room %(name)s", "Recently visited rooms": "Recently visited rooms", "No recently visited rooms": "No recently visited rooms", - "Video call (Jitsi)": "Video call (Jitsi)", - "Video call (%(brand)s)": "Video call (%(brand)s)", - "Ongoing call": "Ongoing call", - "You do not have permission to start video calls": "You do not have permission to start video calls", - "There's no one here to call": "There's no one here to call", - "You do not have permission to start voice calls": "You do not have permission to start voice calls", - "Freedom": "Freedom", - "Spotlight": "Spotlight", - "Change layout": "Change layout", "Forget room": "Forget room", "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", "Search": "Search", - "Close call": "Close call", - "View chat timeline": "View chat timeline", + "Invite": "Invite", "Room options": "Room options", - "Join Room": "Join Room", "(~%(count)s results)|other": "(~%(count)s results)", - "(~%(count)s results)|one": "(~%(count)s result)", + "Join Room": "Join Room", "Video rooms are a beta feature": "Video rooms are a beta feature", "Video room": "Video room", "Public space": "Public space", @@ -2020,7 +1847,6 @@ "Private space": "Private space", "Private room": "Private room", "%(count)s members|other": "%(count)s members", - "%(count)s members|one": "%(count)s member", "Start new chat": "Start new chat", "Invite to space": "Invite to space", "You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space", @@ -2044,16 +1870,14 @@ "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", - "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Currently removing messages in %(count)s rooms|other": "Currently removing messages in %(count)s rooms", - "Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room", "%(spaceName)s menu": "%(spaceName)s menu", "Home options": "Home options", - "Joining space…": "Joining space…", - "Joining room…": "Joining room…", - "Joining…": "Joining…", - "Loading…": "Loading…", - "Rejecting invite…": "Rejecting invite…", + "Joining space …": "Joining space …", + "Joining room …": "Joining room …", + "Joining …": "Joining …", + "Loading …": "Loading …", + "Rejecting invite …": "Rejecting invite …", "Join the room to participate": "Join the room to participate", "Join the conversation with an account": "Join the conversation with an account", "Sign Up": "Sign Up", @@ -2112,15 +1936,15 @@ "A-Z": "A-Z", "List options": "List options", "Show %(count)s more|other": "Show %(count)s more", - "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", "Notification options": "Notification options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", - "%(count)s unread messages including mentions.|one": "1 unread mention.", "%(count)s unread messages.|other": "%(count)s unread messages.", - "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", + "Video": "Video", + "Joining…": "Joining…", "Joined": "Joined", + "%(count)s participants|other": "%(count)s participants", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", "This room is running room version , which this homeserver has marked as unstable.": "This room is running room version , which this homeserver has marked as unstable.", @@ -2128,8 +1952,6 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", - "Search this room": "Search this room", - "Search all rooms": "Search all rooms", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", @@ -2139,30 +1961,15 @@ "Admin Tools": "Admin Tools", "Revoke invite": "Revoke invite", "Invited by %(sender)s": "Invited by %(sender)s", - "%(count)s reply|other": "%(count)s replies", - "%(count)s reply|one": "%(count)s reply", + "%(count)s reply|other": "%(count)s reply", "Open thread": "Open thread", - "Unable to decrypt message": "Unable to decrypt message", "Jump to first unread message.": "Jump to first unread message.", + "Mark all as read": "Mark all as read", "Unable to access your microphone": "Unable to access your microphone", "We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.", "No microphone found": "No microphone found", "We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.", "Stop recording": "Stop recording", - "Italic": "Italic", - "Underline": "Underline", - "Bulleted list": "Bulleted list", - "Numbered list": "Numbered list", - "Indent increase": "Indent increase", - "Indent decrease": "Indent decrease", - "Code": "Code", - "Link": "Link", - "Edit link": "Edit link", - "Create a link": "Create a link", - "Text": "Text", - "Message Actions": "Message Actions", - "View in room": "View in room", - "Copy link to thread": "Copy link to thread", "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", @@ -2216,13 +2023,11 @@ "The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to", "Yours, or the other users' internet connection": "Yours, or the other users' internet connection", "Yours, or the other users' session": "Yours, or the other users' session", - "Error starting verification": "Error starting verification", - "We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.", "Nothing pinned, yet": "Nothing pinned, yet", "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", "Chat": "Chat", - "Room info": "Room info", + "Room Info": "Room Info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Maximise": "Maximise", "Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel", @@ -2233,22 +2038,18 @@ "Not encrypted": "Not encrypted", "About": "About", "Files": "Files", - "Poll history": "Poll history", "Pinned": "Pinned", "Export chat": "Export chat", "Share room": "Share room", "Room settings": "Room settings", "Trusted": "Trusted", "Not trusted": "Not trusted", + "Unable to load session list": "Unable to load session list", "%(count)s verified sessions|other": "%(count)s verified sessions", - "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", "%(count)s sessions|other": "%(count)s sessions", - "%(count)s sessions|one": "%(count)s session", "Hide sessions": "Hide sessions", "Message": "Message", - "Ignore %(user)s": "Ignore %(user)s", - "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "All messages and invites from this user will be hidden. Are you sure you want to ignore them?", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", "Share Link to User": "Share Link to User", @@ -2282,6 +2083,7 @@ "Failed to mute user": "Failed to mute user", "Unmute": "Unmute", "Mute": "Mute", + "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", "Deactivate user?": "Deactivate user?", @@ -2297,7 +2099,7 @@ "Compare unique emoji": "Compare unique emoji", "Compare a unique set of emoji if you don't have a camera on either device": "Compare a unique set of emoji if you don't have a camera on either device", "Start": "Start", - "%(qrCode)s or %(emojiCompare)s": "%(qrCode)s or %(emojiCompare)s", + "or": "or", "Verify this device by completing one of the following:": "Verify this device by completing one of the following:", "Verify by scanning": "Verify by scanning", "Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:", @@ -2319,26 +2121,17 @@ "%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.", "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", - "%(count)s votes|other": "%(count)s votes", - "%(count)s votes|one": "%(count)s vote", - "View poll in timeline": "View poll in timeline", - "Active polls": "Active polls", - "Past polls": "Past polls", - "Loading polls": "Loading polls", - "Load more polls": "Load more polls", - "There are no active polls in this room": "There are no active polls in this room", - "There are no past polls in this room": "There are no past polls in this room", - "There are no active polls. Load more polls to view polls for previous months": "There are no active polls. Load more polls to view polls for previous months", - "There are no past polls. Load more polls to view polls for previous months": "There are no past polls. Load more polls to view polls for previous months", - "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months", - "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no active polls for the past day. Load more polls to view polls for previous months", - "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months", - "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no past polls for the past day. Load more polls to view polls for previous months", - "View poll": "View poll", - "Final result based on %(count)s votes|other": "Final result based on %(count)s votes", - "Final result based on %(count)s votes|one": "Final result based on %(count)s vote", - "%(name)s started a video call": "%(name)s started a video call", - "Video call ended": "Video call ended", + "Call declined": "Call declined", + "Call back": "Call back", + "No answer": "No answer", + "Could not connect media": "Could not connect media", + "Connection failed": "Connection failed", + "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", + "An unknown error occurred": "An unknown error occurred", + "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", + "Retry": "Retry", + "Missed call": "Missed call", + "The call is in an unknown state!": "The call is in an unknown state!", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -2348,20 +2141,11 @@ "Saturday": "Saturday", "Today": "Today", "Yesterday": "Yesterday", - "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", - "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.": "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.", - "Server returned %(statusCode)s with error code %(errorCode)s": "Server returned %(statusCode)s with error code %(errorCode)s", - "unknown status code": "unknown status code", - "unavailable": "unavailable", - "Please submit debug logs to help us track down the problem.": "Please submit debug logs to help us track down the problem.", - "Unable to find event at that date": "Unable to find event at that date", - "Error details": "Error details", + "Unable to find event at that date. (%(code)s)": "Unable to find event at that date. (%(code)s)", "Last week": "Last week", "Last month": "Last month", "The beginning of the room": "The beginning of the room", "Jump to date": "Jump to date", - "The sender has blocked you from receiving this message": "The sender has blocked you from receiving this message", - "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", "Downloading": "Downloading", "Decrypting": "Decrypting", "Download": "Download", @@ -2378,23 +2162,12 @@ "Message pending moderation": "Message pending moderation", "Pick a date to jump to": "Pick a date to jump to", "Go": "Go", - "Call declined": "Call declined", - "Call back": "Call back", - "Answered elsewhere": "Answered elsewhere", - "Missed call": "Missed call", - "No answer": "No answer", - "Could not connect media": "Could not connect media", - "Connection failed": "Connection failed", - "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", - "An unknown error occurred": "An unknown error occurred", - "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", - "Retry": "Retry", - "The call is in an unknown state!": "The call is in an unknown state!", "Error processing audio message": "Error processing audio message", "View live location": "View live location", "React": "React", - "Reply in thread": "Reply in thread", "Can't create a thread from an event with an existing relation": "Can't create a thread from an event with an existing relation", + "Beta feature": "Beta feature", + "Beta feature. Click to learn more.": "Beta feature. Click to learn more.", "Favourite": "Favourite", "Edit": "Edit", "Reply": "Reply", @@ -2406,9 +2179,7 @@ "Decrypt %(text)s": "Decrypt %(text)s", "Invalid file%(extra)s": "Invalid file%(extra)s", "Image": "Image", - "Unable to show image due to error": "Unable to show image due to error", "Error decrypting image": "Error decrypting image", - "Error downloading image": "Error downloading image", "Show image": "Show image", "Join the conference at the top of this room": "Join the conference at the top of this room", "Join the conference from the room information card on the right": "Join the conference from the room information card on the right", @@ -2425,7 +2196,8 @@ "You cancelled": "You cancelled", "%(name)s declined": "%(name)s declined", "%(name)s cancelled": "%(name)s cancelled", - "Declining…": "Declining…", + "Accepting …": "Accepting …", + "Declining …": "Declining …", "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", "Expand map": "Expand map", @@ -2436,15 +2208,13 @@ "Sorry, you can't edit a poll after votes have been cast.": "Sorry, you can't edit a poll after votes have been cast.", "Vote not registered": "Vote not registered", "Sorry, your vote was not registered. Please try again.": "Sorry, your vote was not registered. Please try again.", - "Due to decryption errors, some votes may not be counted": "Due to decryption errors, some votes may not be counted", + "Final result based on %(count)s votes|other": "Final result based on %(count)s votes", "Results will be visible when the poll is ended": "Results will be visible when the poll is ended", "No votes cast": "No votes cast", "%(count)s votes cast. Vote to see the results|other": "%(count)s votes cast. Vote to see the results", - "%(count)s votes cast. Vote to see the results|one": "%(count)s vote cast. Vote to see the results", "Based on %(count)s votes|other": "Based on %(count)s votes", - "Based on %(count)s votes|one": "Based on %(count)s vote", "edited": "edited", - "Ended a poll": "Ended a poll", + "%(count)s votes|other": "%(count)s votes", "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", @@ -2455,10 +2225,8 @@ "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", - "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", - "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it. It's possible that guessing the server from the room ID will work. If you want to try, click this link:": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it. It's possible that guessing the server from the room ID will work. If you want to try, click this link:", - "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it.": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it.", "Click here to see older messages.": "Click here to see older messages.", + "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", "Add an Integration": "Add an Integration", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", "Edited at %(date)s": "Edited at %(date)s", @@ -2476,6 +2244,10 @@ "Click to move the pin": "Click to move the pin", "Click to drop a pin": "Click to drop a pin", "Share location": "Share location", + "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.", + "Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.", + "Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.", + "Unknown error fetching location. Please try again later.": "Unknown error fetching location. Please try again later.", "You don't have permission to share locations": "You don't have permission to share locations", "You need to have the right permissions in order to share locations in this room.": "You need to have the right permissions in order to share locations in this room.", "We couldn't send your location": "We couldn't send your location", @@ -2501,9 +2273,7 @@ "Your display name": "Your display name", "Your avatar URL": "Your avatar URL", "Your user ID": "Your user ID", - "Your device ID": "Your device ID", "Your theme": "Your theme", - "Your language": "Your language", "%(brand)s URL": "%(brand)s URL", "Room ID": "Room ID", "Widget ID": "Widget ID", @@ -2512,6 +2282,7 @@ "Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", + "Loading...": "Loading...", "Error loading Widget": "Error loading Widget", "Error - Mixed content": "Error - Mixed content", "Un-maximise": "Un-maximise", @@ -2526,82 +2297,45 @@ "Something went wrong!": "Something went wrong!", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", - "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times", - "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times", - "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft", "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times", - "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times", - "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times", - "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left", "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times", - "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times", - "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times", - "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times", - "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times", - "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times", - "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn", "were invited %(count)s times|other": "were invited %(count)s times", - "were invited %(count)s times|one": "were invited", "was invited %(count)s times|other": "was invited %(count)s times", - "was invited %(count)s times|one": "was invited", "were banned %(count)s times|other": "were banned %(count)s times", - "were banned %(count)s times|one": "were banned", "was banned %(count)s times|other": "was banned %(count)s times", - "was banned %(count)s times|one": "was banned", "were unbanned %(count)s times|other": "were unbanned %(count)s times", - "were unbanned %(count)s times|one": "were unbanned", "was unbanned %(count)s times|other": "was unbanned %(count)s times", - "was unbanned %(count)s times|one": "was unbanned", "were removed %(count)s times|other": "were removed %(count)s times", - "were removed %(count)s times|one": "were removed", "was removed %(count)s times|other": "was removed %(count)s times", - "was removed %(count)s times|one": "was removed", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times", - "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times", - "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times", - "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", - "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)smade no changes %(count)s times", - "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes", "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times", - "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes", "%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)schanged the server ACLs %(count)s times", - "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", - "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", "%(severalUsers)schanged the pinned messages for the room %(count)s times|other": "%(severalUsers)schanged the pinned messages for the room %(count)s times", - "%(severalUsers)schanged the pinned messages for the room %(count)s times|one": "%(severalUsers)schanged the pinned messages for the room", "%(oneUser)schanged the pinned messages for the room %(count)s times|other": "%(oneUser)schanged the pinned messages for the room %(count)s times", - "%(oneUser)schanged the pinned messages for the room %(count)s times|one": "%(oneUser)schanged the pinned messages for the room", - "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved %(count)s messages", - "%(severalUsers)sremoved a message %(count)s times|one": "%(severalUsers)sremoved a message", - "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved %(count)s messages", - "%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sremoved a message", + "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved a message %(count)s times", + "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved a message %(count)s times", "%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)ssent %(count)s hidden messages", - "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)ssent a hidden message", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)ssent %(count)s hidden messages", - "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)ssent a hidden message", "collapse": "collapse", "expand": "expand", - "Image view": "Image view", "Rotate Left": "Rotate Left", "Rotate Right": "Rotate Right", "Information": "Information", "Language Dropdown": "Language Dropdown", - "Message in %(room)s": "Message in %(room)s", - "Message from %(user)s": "Message from %(user)s", "Create poll": "Create poll", "Create Poll": "Create Poll", "Edit poll": "Edit poll", @@ -2613,7 +2347,7 @@ "Closed poll": "Closed poll", "What is your poll question or topic?": "What is your poll question or topic?", "Question or topic": "Question or topic", - "Write something…": "Write something…", + "Write something...": "Write something...", "Create options": "Create options", "Option %(number)s": "Option %(number)s", "Write an option": "Write an option", @@ -2637,15 +2371,12 @@ "This address is already in use": "This address is already in use", "This address had invalid server or is already in use": "This address had invalid server or is already in use", "View all %(count)s members|other": "View all %(count)s members", - "View all %(count)s members|one": "View 1 member", "Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", - "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Edit topic": "Edit topic", "Click to read topic": "Click to read topic", "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", - "Desktop app logo": "Desktop app logo", "Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files", "Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages", "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files", @@ -2655,7 +2386,6 @@ "Join millions for free on the largest public server": "Join millions for free on the largest public server", "Homeserver": "Homeserver", "Help": "Help", - "WARNING: ": "WARNING: ", "Choose a locale": "Choose a locale", "Continue with %(provider)s": "Continue with %(provider)s", "Sign in with single sign-on": "Sign in with single sign-on", @@ -2686,7 +2416,6 @@ "Search for spaces": "Search for spaces", "Not all selected were added": "Not all selected were added", "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", - "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Direct Messages": "Direct Messages", "Add existing rooms": "Add existing rooms", "Want to add a new room instead?": "Want to add a new room instead?", @@ -2700,15 +2429,14 @@ "You can turn this off anytime in settings": "You can turn this off anytime in settings", "Download %(brand)s Desktop": "Download %(brand)s Desktop", "iOS": "iOS", - "%(qrCode)s or %(appLinks)s": "%(qrCode)s or %(appLinks)s", "Download on the App Store": "Download on the App Store", "Android": "Android", "Get it on Google Play": "Get it on Google Play", "Get it on F-Droid": "Get it on F-Droid", "App Store® and the Apple logo® are trademarks of Apple Inc.": "App Store® and the Apple logo® are trademarks of Apple Inc.", "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play and the Google Play logo are trademarks of Google LLC.", - "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?", "The following users may not exist": "The following users may not exist", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?", "Invite anyway and never warn me again": "Invite anyway and never warn me again", "Invite anyway": "Invite anyway", "Close dialog": "Close dialog", @@ -2731,14 +2459,10 @@ "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", - "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|one": "You are about to remove %(count)s message by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", "Preserve system messages": "Preserve system messages", "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)", "Remove %(count)s messages|other": "Remove %(count)s messages", - "Remove %(count)s messages|one": "Remove 1 message", - "Can't start voice message": "Can't start voice message", - "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.": "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.", "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", @@ -2752,7 +2476,6 @@ "Clear all data": "Clear all data", "Please enter a name for the room": "Please enter a name for the room", "Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.", - "Unnamed Space": "Unnamed Space", "You can change this at any time from room settings.": "You can change this at any time from room settings.", "Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .", "Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.", @@ -2781,7 +2504,8 @@ "Space visibility": "Space visibility", "Private space (invite only)": "Private space (invite only)", "Want to add an existing space instead?": "Want to add an existing space instead?", - "Adding…": "Adding…", + "Adding...": "Adding...", + "Sign out": "Sign out", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this", "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.", "Incompatible Database": "Incompatible Database", @@ -2801,11 +2525,11 @@ "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number": "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number", "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?": "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?", "Hide my messages from new joiners": "Hide my messages from new joiners", + "Room": "Room", "Send custom timeline event": "Send custom timeline event", "Explore room state": "Explore room state", "Explore room account data": "Explore room account data", "View servers in room": "View servers in room", - "Notifications debug": "Notifications debug", "Verification explorer": "Verification explorer", "Active Widgets": "Active Widgets", "Explore account data": "Explore account data", @@ -2821,7 +2545,7 @@ "End Poll": "End Poll", "Are you sure you want to end this poll? This will show the final results of the poll and stop people from being able to vote.": "Are you sure you want to end this poll? This will show the final results of the poll and stop people from being able to vote.", "An error has occurred.": "An error has occurred.", - "Processing…": "Processing…", + "Processing...": "Processing...", "Enter a number between %(min)s and %(max)s": "Enter a number between %(min)s and %(max)s", "Size can only be a number between %(min)s MB and %(max)s MB": "Size can only be a number between %(min)s MB and %(max)s MB", "Number of messages can only be a number between %(min)s and %(max)s": "Number of messages can only be a number between %(min)s and %(max)s", @@ -2842,7 +2566,6 @@ "Feedback sent": "Feedback sent", "Comment": "Comment", "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.", - "Feedback": "Feedback", "You may contact me if you want to follow up or to let me test out upcoming ideas": "You may contact me if you want to follow up or to let me test out upcoming ideas", "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", "Report a bug": "Report a bug", @@ -2858,14 +2581,28 @@ "Search for rooms or people": "Search for rooms or people", "Feedback sent! Thanks, we appreciate it!": "Feedback sent! Thanks, we appreciate it!", "You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions", + "Confirm abort of host creation": "Confirm abort of host creation", + "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", + "Abort": "Abort", + "Failed to connect to your homeserver. Please close this dialog and try again.": "Failed to connect to your homeserver. Please close this dialog and try again.", + "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.", + "Learn more in our , and .": "Learn more in our , and .", + "Cookie Policy": "Cookie Policy", + "Privacy Policy": "Privacy Policy", + "Terms of Service": "Terms of Service", + "You should know": "You should know", + "%(hostSignupBrand)s Setup": "%(hostSignupBrand)s Setup", + "Maximise dialog": "Maximise dialog", + "Minimise dialog": "Minimise dialog", + "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.", "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.", - "Waiting for partner to confirm…": "Waiting for partner to confirm…", + "Waiting for partner to confirm...": "Waiting for partner to confirm...", "Incoming Verification Request": "Incoming Verification Request", "Integrations are disabled": "Integrations are disabled", - "Enable '%(manageIntegrations)s' in Settings to do this.": "Enable '%(manageIntegrations)s' in Settings to do this.", + "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", "Integrations not allowed": "Integrations not allowed", "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.", "To continue, use Single Sign On to prove your identity.": "To continue, use Single Sign On to prove your identity.", @@ -2873,9 +2610,6 @@ "Click the button below to confirm your identity.": "Click the button below to confirm your identity.", "Invite by email": "Invite by email", "We couldn't create your DM.": "We couldn't create your DM.", - "Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?", - "Start DM anyway and never warn me again": "Start DM anyway and never warn me again", - "Start DM anyway": "Start DM anyway", "Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.", "We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.", "A call can only be transferred to a single user.": "A call can only be transferred to a single user.", @@ -2891,7 +2625,9 @@ "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.", "If you can't see who you're looking for, send them your invite link below.": "If you can't see who you're looking for, send them your invite link below.", "Or send invite link": "Or send invite link", + "Unnamed Space": "Unnamed Space", "Invite to %(roomName)s": "Invite to %(roomName)s", + "Unnamed Room": "Unnamed Room", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", "Invite someone using their name, username (like ) or share this space.": "Invite someone using their name, username (like ) or share this space.", "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", @@ -2899,7 +2635,6 @@ "Invited people will be able to read old messages.": "Invited people will be able to read old messages.", "Transfer": "Transfer", "Consult first": "Consult first", - "Invites by email can only be sent one at a time": "Invites by email can only be sent one at a time", "User Directory": "User Directory", "Dial pad": "Dial pad", "a new master key signature": "a new master key signature", @@ -2936,7 +2671,6 @@ "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", "%(count)s rooms|other": "%(count)s rooms", - "%(count)s rooms|one": "%(count)s room", "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only", "Select spaces": "Select spaces", "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decide which spaces can access this room. If a space is selected, its members can find and join .", @@ -2950,6 +2684,7 @@ "Session name": "Session name", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", + "Verify session": "Verify session", "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.", "Message edits": "Message edits", "Modal Widget": "Modal Widget", @@ -2958,15 +2693,14 @@ "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", "Email (optional)": "Email (optional)", "Please fill why you're reporting.": "Please fill why you're reporting.", - "Unable to create room with moderation bot": "Unable to create room with moderation bot", "Ignore user": "Ignore user", "Check if you want to hide all current and future messages from this user.": "Check if you want to hide all current and future messages from this user.", "What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.", - "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", + "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.", "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.", "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.", - "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s.", + "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.", "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.", "Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.", "Report Content": "Report Content", @@ -3041,16 +2775,9 @@ "Link to selected message": "Link to selected message", "Link to room": "Link to room", "Command Help": "Command Help", - "Checking…": "Checking…", - "Your server has native support": "Your server has native support", - "Your server lacks native support": "Your server lacks native support", - "Your server lacks native support, you must specify a proxy": "Your server lacks native support, you must specify a proxy", - "Sliding Sync configuration": "Sliding Sync configuration", - "To disable you will need to log out and back in, use with caution!": "To disable you will need to log out and back in, use with caution!", - "Proxy URL (optional)": "Proxy URL (optional)", - "Proxy URL": "Proxy URL", "Sections to show": "Sections to show", "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.", + "Space settings": "Space settings", "Settings - %(spaceName)s": "Settings - %(spaceName)s", "To help us prevent this in future, please send us logs.": "To help us prevent this in future, please send us logs.", "Missing session data": "Missing session data", @@ -3059,7 +2786,6 @@ "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", "Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs", - "Terms of Service": "Terms of Service", "To continue you need to accept the terms of this service.": "To continue you need to accept the terms of this service.", "Service": "Service", "Summary": "Summary", @@ -3079,10 +2805,8 @@ "These files are too large to upload. The file size limit is %(limit)s.": "These files are too large to upload. The file size limit is %(limit)s.", "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Some files are too large to be uploaded. The file size limit is %(limit)s.", "Upload %(count)s other files|other": "Upload %(count)s other files", - "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", - "Labs": "Labs", "Verify other device": "Verify other device", "Verification Request": "Verification Request", "Approve widget permissions": "Approve widget permissions", @@ -3093,13 +2817,10 @@ "Allow this widget to verify your identity": "Allow this widget to verify your identity", "The widget will verify your user ID, but won't be able to perform actions for you:": "The widget will verify your user ID, but won't be able to perform actions for you:", "Remember this": "Remember this", - "Unnamed room": "Unnamed room", "%(count)s Members|other": "%(count)s Members", - "%(count)s Members|one": "%(count)s Member", "Public rooms": "Public rooms", "Use \"%(query)s\" to search": "Use \"%(query)s\" to search", "Search for": "Search for", - "View": "View", "Spaces you're in": "Spaces you're in", "Show rooms": "Show rooms", "Show spaces": "Show spaces", @@ -3121,6 +2842,7 @@ "Use to scroll": "Use to scroll", "Search Dialog": "Search Dialog", "Remove search filter for %(filter)s": "Remove search filter for %(filter)s", + "Results not as expected? Please give feedback.": "Results not as expected? Please give feedback.", "Wrong file type": "Wrong file type", "Looks good!": "Looks good!", "Wrong Security Key": "Wrong Security Key", @@ -3134,7 +2856,6 @@ "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", "Security Key": "Security Key", "Use your Security Key to continue.": "Use your Security Key to continue.", - "%(securityKey)s or %(recoveryFile)s": "%(securityKey)s or %(recoveryFile)s", "Destroy cross-signing keys?": "Destroy cross-signing keys?", "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.", "Clear cross-signing keys": "Clear cross-signing keys", @@ -3142,7 +2863,7 @@ "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Unable to set up keys": "Unable to set up keys", "Restoring keys from backup": "Restoring keys from backup", - "Fetching keys from server…": "Fetching keys from server…", + "Fetching keys from server...": "Fetching keys from server...", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", "Unable to load backup status": "Unable to load backup status", "Security Key mismatch": "Security Key mismatch", @@ -3161,6 +2882,7 @@ "Enter Security Key": "Enter Security Key", "This looks like a valid Security Key!": "This looks like a valid Security Key!", "Not a valid Security Key": "Not a valid Security Key", + "Warning: You should only set up key backup from a trusted computer.": "Warning: You should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.", "If you've forgotten your Security Key you can ": "If you've forgotten your Security Key you can ", "Send custom account data event": "Send custom account data event", @@ -3173,27 +2895,7 @@ "Event Content": "Event Content", "Filter results": "Filter results", "No results found": "No results found", - "Room status": "Room status", - "Room unread status: %(status)s, count: %(count)s|other": "Room unread status: %(status)s, count: %(count)s", - "Room unread status: %(status)s, count: %(count)s|zero": "Room unread status: %(status)s", - "Notification state is %(notificationState)s": "Notification state is %(notificationState)s", - "Room is encrypted ✅": "Room is encrypted ✅", - "Room is not encrypted 🚨": "Room is not encrypted 🚨", - "Main timeline": "Main timeline", - "Total: ": "Total: ", - "Highlight: ": "Highlight: ", - "Dot: ": "Dot: ", - "User read up to: ": "User read up to: ", - "No receipt found": "No receipt found", - "Last event:": "Last event:", - "ID: ": "ID: ", - "Type: ": "Type: ", - "Sender: ": "Sender: ", - "Threads timeline": "Threads timeline", - "Thread Id: ": "Thread Id: ", "<%(count)s spaces>|other": "<%(count)s spaces>", - "<%(count)s spaces>|one": "", - "<%(count)s spaces>|zero": "", "Send custom state event": "Send custom state event", "Capabilities": "Capabilities", "Failed to load.": "Failed to load.", @@ -3221,6 +2923,7 @@ "Value": "Value", "Value in this room": "Value in this room", "Edit setting": "Edit setting", + "Unsent": "Unsent", "Requested": "Requested", "Ready": "Ready", "Started": "Started", @@ -3254,9 +2957,8 @@ "Copy room link": "Copy room link", "Low Priority": "Low Priority", "Forget Room": "Forget Room", - "Mark as read": "Mark as read", - "Match default setting": "Match default setting", - "Mute room": "Mute room", + "Use default": "Use default", + "Mentions & Keywords": "Mentions & Keywords", "See room timeline (devtools)": "See room timeline (devtools)", "Space": "Space", "Space home": "Space home", @@ -3278,11 +2980,10 @@ "Beta": "Beta", "Leaving the beta will reload %(brand)s.": "Leaving the beta will reload %(brand)s.", "Joining the beta will reload %(brand)s.": "Joining the beta will reload %(brand)s.", - "Leave the beta": "Leave the beta", "Join the beta": "Join the beta", "Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s", "Live until %(expiryTime)s": "Live until %(expiryTime)s", - "Loading live location…": "Loading live location…", + "Loading live location...": "Loading live location...", "Live location ended": "Live location ended", "Live location error": "Live location error", "No live locations": "No live locations", @@ -3319,36 +3020,14 @@ "Token incorrect": "Token incorrect", "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", "Please enter the code it contains:": "Please enter the code it contains:", + "Code": "Code", "Submit": "Submit", - "Enter a registration token provided by the homeserver administrator.": "Enter a registration token provided by the homeserver administrator.", - "Registration token": "Registration token", "Something went wrong in confirming your identity. Cancel and try again.": "Something went wrong in confirming your identity. Cancel and try again.", "Start authentication": "Start authentication", - "Sign in new device": "Sign in new device", - "The linking wasn't completed in the required time.": "The linking wasn't completed in the required time.", - "The scanned code is invalid.": "The scanned code is invalid.", - "Linking with this device is not supported.": "Linking with this device is not supported.", - "The request was declined on the other device.": "The request was declined on the other device.", - "The other device is already signed in.": "The other device is already signed in.", - "The other device isn't signed in.": "The other device isn't signed in.", - "The request was cancelled.": "The request was cancelled.", - "An unexpected error occurred.": "An unexpected error occurred.", - "The homeserver doesn't support signing in another device.": "The homeserver doesn't support signing in another device.", - "Devices connected": "Devices connected", - "Check that the code below matches with your other device:": "Check that the code below matches with your other device:", - "By approving access for this device, it will have full access to your account.": "By approving access for this device, it will have full access to your account.", - "Scan the QR code below with your device that's signed out.": "Scan the QR code below with your device that's signed out.", - "Start at the sign in screen": "Start at the sign in screen", - "Select '%(scanQRCode)s'": "Select '%(scanQRCode)s'", - "Scan QR code": "Scan QR code", - "Review and approve the sign in": "Review and approve the sign in", - "Connecting…": "Connecting…", - "Waiting for device to sign in": "Waiting for device to sign in", - "Completing set up of your new device": "Completing set up of your new device", "Enter password": "Enter password", "Nice, strong password!": "Nice, strong password!", "Password is allowed, but unsafe": "Password is allowed, but unsafe", - "Keep going…": "Keep going…", + "Keep going...": "Keep going...", "Enter username": "Enter username", "Enter phone number": "Enter phone number", "That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again", @@ -3415,9 +3094,20 @@ "Logout": "Logout", "You're all caught up": "You're all caught up", "You have no visible notifications.": "You have no visible notifications.", - "Search failed": "Search failed", - "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", - "No more results": "No more results", + "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", + "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", + "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", + "Delete the room address %(alias)s and remove %(name)s from the directory?": "Delete the room address %(alias)s and remove %(name)s from the directory?", + "Remove %(name)s from the directory?": "Remove %(name)s from the directory?", + "Remove from Directory": "Remove from Directory", + "remove %(name)s from the directory.": "remove %(name)s from the directory.", + "delete the address.": "delete the address.", + "The server may be unavailable or overloaded": "The server may be unavailable or overloaded", + "No results for \"%(query)s\"": "No results for \"%(query)s\"", + "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.", + "Find a room…": "Find a room…", + "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", + "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.", @@ -3431,18 +3121,20 @@ "We're creating a room with %(names)s": "We're creating a room with %(names)s", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", + "Search failed": "Search failed", + "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", + "No more results": "No more results", "Failed to reject invite": "Failed to reject invite", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", - "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "Joining": "Joining", "You don't have permission": "You don't have permission", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", - "Unknown error": "Unknown error", "Select a room below first": "Select a room below first", + "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", + "Removing...": "Removing...", "Mark as not suggested": "Mark as not suggested", "Mark as suggested": "Mark as suggested", - "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", "Failed to load list of rooms.": "Failed to load list of rooms.", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", @@ -3455,9 +3147,10 @@ "Room name": "Room name", "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", - "Creating rooms…": "Creating rooms…", + "Creating rooms...": "Creating rooms...", "What do you want to organise?": "What do you want to organise?", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", + "Search for rooms or spaces": "Search for rooms or spaces", "Share %(name)s": "Share %(name)s", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", "Go to my first room": "Go to my first room", @@ -3469,9 +3162,10 @@ "Me and my teammates": "Me and my teammates", "A private space for you and your teammates": "A private space for you and your teammates", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", - "Inviting…": "Inviting…", + "Inviting...": "Inviting...", "Invite your teammates": "Invite your teammates", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", + "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.", "Invite by username": "Invite by username", "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?", "Let's create a room for each of them.": "Let's create a room for each of them.", @@ -3488,13 +3182,13 @@ "Threads help keep your conversations on-topic and easy to track.": "Threads help keep your conversations on-topic and easy to track.", "Tip: Use “%(replyInThread)s” when hovering over a message.": "Tip: Use “%(replyInThread)s” when hovering over a message.", "Keep discussions organised with threads": "Keep discussions organised with threads", + "Threads are a beta feature": "Threads are a beta feature", + "Give feedback": "Give feedback", "Thread": "Thread", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", - "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", - "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Got an account? Sign in": "Got an account? Sign in", "New here? Create an account": "New here? Create an account", "Switch to light mode": "Switch to light mode", @@ -3503,31 +3197,31 @@ "User menu": "User menu", "Could not load user profile": "Could not load user profile", "Decrypted event source": "Decrypted event source", - "Decrypted source unavailable": "Decrypted source unavailable", "Original event source": "Original event source", "Event ID: %(eventId)s": "Event ID: %(eventId)s", - "Thread root ID: %(threadRootId)s": "Thread root ID: %(threadRootId)s", - "Waiting for users to join %(brand)s": "Waiting for users to join %(brand)s", - "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted": "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted", "Unable to verify this device": "Unable to verify this device", "Verify this device": "Verify this device", "Device verified": "Device verified", "Really reset verification keys?": "Really reset verification keys?", "Skip verification for now": "Skip verification for now", - "Too many attempts in a short time. Wait some time before trying again.": "Too many attempts in a short time. Wait some time before trying again.", - "Too many attempts in a short time. Retry after %(timeout)s.": "Too many attempts in a short time. Retry after %(timeout)s.", + "Failed to send email": "Failed to send email", "Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.", "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.": "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.", - "Reset password": "Reset password", - "Reset your password": "Reset your password", - "Confirm new password": "Confirm new password", + "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", + "The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.", "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", - "Sign out of all devices": "Sign out of all devices", + "Sign out all devices": "Sign out all devices", + "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", + "Send Reset Email": "Send Reset Email", + "Sign in instead": "Sign in instead", + "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", + "I have verified my email address": "I have verified my email address", "Your password has been reset.": "Your password has been reset.", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.", "Return to login screen": "Return to login screen", + "Set a new password": "Set a new password", "Invalid homeserver discovery response": "Invalid homeserver discovery response", "Failed to get autodiscovery configuration from server": "Failed to get autodiscovery configuration from server", "Invalid base_url for m.homeserver": "Invalid base_url for m.homeserver", @@ -3537,17 +3231,24 @@ "Identity server URL does not appear to be a valid identity server": "Identity server URL does not appear to be a valid identity server", "General failure": "General failure", "This homeserver does not support login using email address.": "This homeserver does not support login using email address.", + "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", + "This account has been deactivated.": "This account has been deactivated.", + "Incorrect username and/or password.": "Incorrect username and/or password.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", - "Syncing…": "Syncing…", - "Signing In…": "Signing In…", + "There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", + "Syncing...": "Syncing...", + "Signing In...": "Signing In...", "If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while", "New? Create account": "New? Create account", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", "Someone already has that username, please try another.": "Someone already has that username, please try another.", - "That e-mail address or phone number is already in use.": "That e-mail address or phone number is already in use.", + "That e-mail address is already in use.": "That e-mail address is already in use.", "Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s", "Already have an account? Sign in here": "Already have an account? Sign in here", @@ -3568,7 +3269,7 @@ "Without verifying, you won't have access to all your messages and may appear as untrusted to others.": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", "I'll verify later": "I'll verify later", "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", - "Please only proceed if you're sure you've lost all of your other devices and your Security Key.": "Please only proceed if you're sure you've lost all of your other devices and your Security Key.", + "Please only proceed if you're sure you've lost all of your other devices and your security key.": "Please only proceed if you're sure you've lost all of your other devices and your security key.", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Incorrect password": "Incorrect password", "Failed to re-authenticate": "Failed to re-authenticate", @@ -3579,20 +3280,7 @@ "You cannot sign in to your account. Please contact your homeserver admin for more information.": "You cannot sign in to your account. Please contact your homeserver admin for more information.", "You're signed out": "You're signed out", "Clear personal data": "Clear personal data", - "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", - "Follow the instructions sent to %(email)s": "Follow the instructions sent to %(email)s", - "Wrong email address?": "Wrong email address?", - "Re-enter email address": "Re-enter email address", - "Did not receive it?": "Did not receive it?", - "Verification link email resent!": "Verification link email resent!", - "Send email": "Send email", - "Enter your email to reset password": "Enter your email to reset password", - "%(homeserver)s will send you a verification link to let you reset your password.": "%(homeserver)s will send you a verification link to let you reset your password.", - "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", - "The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.", - "Sign in instead": "Sign in instead", - "Verify your email to continue": "Verify your email to continue", - "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s": "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s", + "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", "Commands": "Commands", "Command Autocomplete": "Command Autocomplete", "Emoji Autocomplete": "Emoji Autocomplete", @@ -3613,7 +3301,7 @@ "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", "Enter your Security Phrase a second time to confirm it.": "Enter your Security Phrase a second time to confirm it.", - "Repeat your Security Phrase…": "Repeat your Security Phrase…", + "Repeat your Security Phrase...": "Repeat your Security Phrase...", "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", "Your Security Key": "Your Security Key", @@ -3628,7 +3316,7 @@ "Secure your backup with a Security Phrase": "Secure your backup with a Security Phrase", "Confirm your Security Phrase": "Confirm your Security Phrase", "Make a copy of your Security Key": "Make a copy of your Security Key", - "Starting backup…": "Starting backup…", + "Starting backup...": "Starting backup...", "Success!": "Success!", "Create key backup": "Create key backup", "Unable to create key backup": "Unable to create key backup", @@ -3641,10 +3329,8 @@ "Restore": "Restore", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", - "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.": "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", + "Enter a security phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.": "Enter a security phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.", - "%(downloadButton)s or %(copyButton)s": "%(downloadButton)s or %(copyButton)s", - "Your keys are now being backed up from this device.": "Your keys are now being backed up from this device.", "Unable to query secret storage status": "Unable to query secret storage status", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", @@ -3652,10 +3338,10 @@ "Set a Security Phrase": "Set a Security Phrase", "Confirm Security Phrase": "Confirm Security Phrase", "Save your Security Key": "Save your Security Key", - "Secure Backup successful": "Secure Backup successful", "Unable to set up secret storage": "Unable to set up secret storage", "Passphrases must match": "Passphrases must match", "Passphrase must not be empty": "Passphrase must not be empty", + "Unknown error": "Unknown error", "Export room keys": "Export room keys", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", From 6026b34bb9a8c36d58fcbb44589e206103f46b8c Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:59:29 -0400 Subject: [PATCH 071/176] i18n changes undo --- src/i18n/strings/en_EN.json | 95 +++++++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index af06a837d85..3d1b5241cad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -516,7 +516,9 @@ "%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.", "%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.", "%(senderName)s added the alternative addresses %(addresses)s for this room.|other": "%(senderName)s added the alternative addresses %(addresses)s for this room.", + "%(senderName)s added the alternative addresses %(addresses)s for this room.|one": "%(senderName)s added alternative address %(addresses)s for this room.", "%(senderName)s removed the alternative addresses %(addresses)s for this room.|other": "%(senderName)s removed the alternative addresses %(addresses)s for this room.", + "%(senderName)s removed the alternative addresses %(addresses)s for this room.|one": "%(senderName)s removed alternative address %(addresses)s for this room.", "%(senderName)s changed the alternative addresses for this room.": "%(senderName)s changed the alternative addresses for this room.", "%(senderName)s changed the main and alternative addresses for this room.": "%(senderName)s changed the main and alternative addresses for this room.", "%(senderName)s changed the addresses for this room.": "%(senderName)s changed the addresses for this room.", @@ -566,6 +568,7 @@ "Dark": "Dark", "%(displayName)s is typing …": "%(displayName)s is typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", + "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", "Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running", "Remain on your screen while running": "Remain on your screen while running", @@ -648,6 +651,7 @@ "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", "Attachment": "Attachment", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", + "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "a few seconds ago": "a few seconds ago", "about a minute ago": "about a minute ago", @@ -666,7 +670,11 @@ "%(space1Name)s and %(space2Name)s": "%(space1Name)s and %(space2Name)s", "In spaces %(space1Name)s and %(space2Name)s.": "In spaces %(space1Name)s and %(space2Name)s.", "%(spaceName)s and %(count)s others|other": "%(spaceName)s and %(count)s others", + "%(spaceName)s and %(count)s others|zero": "%(spaceName)s", + "%(spaceName)s and %(count)s others|one": "%(spaceName)s and %(count)s other", "In %(spaceName)s and %(count)s other spaces.|other": "In %(spaceName)s and %(count)s other spaces.", + "In %(spaceName)s and %(count)s other spaces.|zero": "In space %(spaceName)s.", + "In %(spaceName)s and %(count)s other spaces.|one": "In %(spaceName)s and %(count)s other space.", "%(name)s (%(userId)s)": "%(name)s (%(userId)s)", "Unexpected server error trying to leave the room": "Unexpected server error trying to leave the room", "Can't leave Server Notices room": "Can't leave Server Notices room", @@ -742,7 +750,9 @@ "Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?", "Generating a ZIP": "Generating a ZIP", "Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s", + "Fetched %(count)s events out of %(total)s|one": "Fetched %(count)s event out of %(total)s", "Fetched %(count)s events so far|other": "Fetched %(count)s events so far", + "Fetched %(count)s events so far|one": "Fetched %(count)s event so far", "HTML": "HTML", "JSON": "JSON", "Plain Text": "Plain Text", @@ -758,9 +768,11 @@ "Processing event %(number)s out of %(total)s": "Processing event %(number)s out of %(total)s", "Starting export...": "Starting export...", "Fetched %(count)s events in %(seconds)ss|other": "Fetched %(count)s events in %(seconds)ss", + "Fetched %(count)s events in %(seconds)ss|one": "Fetched %(count)s event in %(seconds)ss", "Creating HTML...": "Creating HTML...", "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", + "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", @@ -1028,6 +1040,7 @@ "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", "%(count)s people joined|other": "%(count)s people joined", + "%(count)s people joined|one": "%(count)s person joined", "Audio devices": "Audio devices", "Audio input %(n)s": "Audio input %(n)s", "Mute microphone": "Mute microphone", @@ -1158,6 +1171,7 @@ "Find your people": "Find your people", "Welcome to %(brand)s": "Welcome to %(brand)s", "Only %(count)s steps to go|other": "Only %(count)s steps to go", + "Only %(count)s steps to go|one": "Only %(count)s step to go", "You did it!": "You did it!", "Complete these to get the most out of %(brand)s": "Complete these to get the most out of %(brand)s", "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", @@ -1287,6 +1301,7 @@ "Unverified devices": "Unverified devices", "Devices without encryption support": "Devices without encryption support", "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", + "Sign out %(count)s selected devices|one": "Sign out %(count)s selected device", "You aren't signed into any other devices.": "You aren't signed into any other devices.", "This device": "This device", "Failed to set display name": "Failed to set display name", @@ -1295,6 +1310,7 @@ "Rename": "Rename", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", "Manage": "Manage", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", @@ -1315,7 +1331,9 @@ "Anyone can find and join.": "Anyone can find and join.", "Upgrade required": "Upgrade required", "& %(count)s more|other": "& %(count)s more", + "& %(count)s more|one": "& %(count)s more", "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", + "Currently, %(count)s spaces have access|one": "Currently, a space has access", "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", "Spaces with access": "Spaces with access", "Anyone in can find and join. You can select other spaces too.": "Anyone in can find and join. You can select other spaces too.", @@ -1326,7 +1344,9 @@ "Upgrading room": "Upgrading room", "Loading new room": "Loading new room", "Sending invites... (%(progress)s out of %(count)s)|other": "Sending invites... (%(progress)s out of %(count)s)", + "Sending invites... (%(progress)s out of %(count)s)|one": "Sending invite...", "Updating spaces... (%(progress)s out of %(count)s)|other": "Updating spaces... (%(progress)s out of %(count)s)", + "Updating spaces... (%(progress)s out of %(count)s)|one": "Updating space...", "Message layout": "Message layout", "IRC (Experimental)": "IRC (Experimental)", "Modern": "Modern", @@ -1678,9 +1698,13 @@ "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", "Current session": "Current session", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", + "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", "Confirm signing out these devices|other": "Confirm signing out these devices", + "Confirm signing out these devices|one": "Confirm signing out this device", "Click the button below to confirm signing out these devices.|other": "Click the button below to confirm signing out these devices.", + "Click the button below to confirm signing out these devices.|one": "Click the button below to confirm signing out this device.", "Sign out devices|other": "Sign out devices", + "Sign out devices|one": "Sign out device", "Authentication": "Authentication", "Session ID": "Session ID", "Last activity": "Last activity", @@ -1760,8 +1784,10 @@ "You can't see earlier messages": "You can't see earlier messages", "Scroll to most recent messages": "Scroll to most recent messages", "Show %(count)s other previews|other": "Show %(count)s other previews", + "Show %(count)s other previews|one": "Show %(count)s other preview", "Close preview": "Close preview", "and %(count)s others...|other": "and %(count)s others...", + "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", "Invite to this space": "Invite to this space", "Invited": "Invited", @@ -1826,6 +1852,7 @@ "%(members)s and more": "%(members)s and more", "%(members)s and %(last)s": "%(members)s and %(last)s", "Seen by %(count)s people|other": "Seen by %(count)s people", + "Seen by %(count)s people|one": "Seen by %(count)s person", "Read receipts": "Read receipts", "Recently viewed": "Recently viewed", "Replying": "Replying", @@ -1839,6 +1866,7 @@ "Invite": "Invite", "Room options": "Room options", "(~%(count)s results)|other": "(~%(count)s results)", + "(~%(count)s results)|one": "(~%(count)s result)", "Join Room": "Join Room", "Video rooms are a beta feature": "Video rooms are a beta feature", "Video room": "Video room", @@ -1847,6 +1875,7 @@ "Private space": "Private space", "Private room": "Private room", "%(count)s members|other": "%(count)s members", + "%(count)s members|one": "%(count)s member", "Start new chat": "Start new chat", "Invite to space": "Invite to space", "You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space", @@ -1870,7 +1899,9 @@ "You do not have permissions to add spaces to this space": "You do not have permissions to add spaces to this space", "Join public room": "Join public room", "Currently joining %(count)s rooms|other": "Currently joining %(count)s rooms", + "Currently joining %(count)s rooms|one": "Currently joining %(count)s room", "Currently removing messages in %(count)s rooms|other": "Currently removing messages in %(count)s rooms", + "Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room", "%(spaceName)s menu": "%(spaceName)s menu", "Home options": "Home options", "Joining space …": "Joining space …", @@ -1936,15 +1967,19 @@ "A-Z": "A-Z", "List options": "List options", "Show %(count)s more|other": "Show %(count)s more", + "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", "Notification options": "Notification options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", + "%(count)s unread messages including mentions.|one": "1 unread mention.", "%(count)s unread messages.|other": "%(count)s unread messages.", + "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", "Video": "Video", "Joining…": "Joining…", "Joined": "Joined", "%(count)s participants|other": "%(count)s participants", + "%(count)s participants|one": "1 participant", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", "This room is running room version , which this homeserver has marked as unstable.": "This room is running room version , which this homeserver has marked as unstable.", @@ -1961,7 +1996,8 @@ "Admin Tools": "Admin Tools", "Revoke invite": "Revoke invite", "Invited by %(sender)s": "Invited by %(sender)s", - "%(count)s reply|other": "%(count)s reply", + "%(count)s reply|other": "%(count)s replies", + "%(count)s reply|one": "%(count)s reply", "Open thread": "Open thread", "Jump to first unread message.": "Jump to first unread message.", "Mark all as read": "Mark all as read", @@ -2046,8 +2082,10 @@ "Not trusted": "Not trusted", "Unable to load session list": "Unable to load session list", "%(count)s verified sessions|other": "%(count)s verified sessions", + "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", "%(count)s sessions|other": "%(count)s sessions", + "%(count)s sessions|one": "%(count)s session", "Hide sessions": "Hide sessions", "Message": "Message", "Jump to read receipt": "Jump to read receipt", @@ -2209,12 +2247,16 @@ "Vote not registered": "Vote not registered", "Sorry, your vote was not registered. Please try again.": "Sorry, your vote was not registered. Please try again.", "Final result based on %(count)s votes|other": "Final result based on %(count)s votes", + "Final result based on %(count)s votes|one": "Final result based on %(count)s vote", "Results will be visible when the poll is ended": "Results will be visible when the poll is ended", "No votes cast": "No votes cast", "%(count)s votes cast. Vote to see the results|other": "%(count)s votes cast. Vote to see the results", + "%(count)s votes cast. Vote to see the results|one": "%(count)s vote cast. Vote to see the results", "Based on %(count)s votes|other": "Based on %(count)s votes", + "Based on %(count)s votes|one": "Based on %(count)s vote", "edited": "edited", "%(count)s votes|other": "%(count)s votes", + "%(count)s votes|one": "%(count)s vote", "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", @@ -2297,39 +2339,73 @@ "Something went wrong!": "Something went wrong!", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", + "%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)sjoined", "%(oneUser)sjoined %(count)s times|other": "%(oneUser)sjoined %(count)s times", + "%(oneUser)sjoined %(count)s times|one": "%(oneUser)sjoined", "%(severalUsers)sleft %(count)s times|other": "%(severalUsers)sleft %(count)s times", + "%(severalUsers)sleft %(count)s times|one": "%(severalUsers)sleft", "%(oneUser)sleft %(count)s times|other": "%(oneUser)sleft %(count)s times", + "%(oneUser)sleft %(count)s times|one": "%(oneUser)sleft", "%(severalUsers)sjoined and left %(count)s times|other": "%(severalUsers)sjoined and left %(count)s times", + "%(severalUsers)sjoined and left %(count)s times|one": "%(severalUsers)sjoined and left", "%(oneUser)sjoined and left %(count)s times|other": "%(oneUser)sjoined and left %(count)s times", + "%(oneUser)sjoined and left %(count)s times|one": "%(oneUser)sjoined and left", "%(severalUsers)sleft and rejoined %(count)s times|other": "%(severalUsers)sleft and rejoined %(count)s times", + "%(severalUsers)sleft and rejoined %(count)s times|one": "%(severalUsers)sleft and rejoined", "%(oneUser)sleft and rejoined %(count)s times|other": "%(oneUser)sleft and rejoined %(count)s times", + "%(oneUser)sleft and rejoined %(count)s times|one": "%(oneUser)sleft and rejoined", "%(severalUsers)srejected their invitations %(count)s times|other": "%(severalUsers)srejected their invitations %(count)s times", + "%(severalUsers)srejected their invitations %(count)s times|one": "%(severalUsers)srejected their invitations", "%(oneUser)srejected their invitation %(count)s times|other": "%(oneUser)srejected their invitation %(count)s times", + "%(oneUser)srejected their invitation %(count)s times|one": "%(oneUser)srejected their invitation", "%(severalUsers)shad their invitations withdrawn %(count)s times|other": "%(severalUsers)shad their invitations withdrawn %(count)s times", + "%(severalUsers)shad their invitations withdrawn %(count)s times|one": "%(severalUsers)shad their invitations withdrawn", "%(oneUser)shad their invitation withdrawn %(count)s times|other": "%(oneUser)shad their invitation withdrawn %(count)s times", + "%(oneUser)shad their invitation withdrawn %(count)s times|one": "%(oneUser)shad their invitation withdrawn", "were invited %(count)s times|other": "were invited %(count)s times", + "were invited %(count)s times|one": "were invited", "was invited %(count)s times|other": "was invited %(count)s times", + "was invited %(count)s times|one": "was invited", "were banned %(count)s times|other": "were banned %(count)s times", + "were banned %(count)s times|one": "were banned", "was banned %(count)s times|other": "was banned %(count)s times", + "was banned %(count)s times|one": "was banned", "were unbanned %(count)s times|other": "were unbanned %(count)s times", + "were unbanned %(count)s times|one": "were unbanned", "was unbanned %(count)s times|other": "was unbanned %(count)s times", + "was unbanned %(count)s times|one": "was unbanned", "were removed %(count)s times|other": "were removed %(count)s times", + "were removed %(count)s times|one": "were removed", "was removed %(count)s times|other": "was removed %(count)s times", + "was removed %(count)s times|one": "was removed", "%(severalUsers)schanged their name %(count)s times|other": "%(severalUsers)schanged their name %(count)s times", + "%(severalUsers)schanged their name %(count)s times|one": "%(severalUsers)schanged their name", "%(oneUser)schanged their name %(count)s times|other": "%(oneUser)schanged their name %(count)s times", + "%(oneUser)schanged their name %(count)s times|one": "%(oneUser)schanged their name", "%(severalUsers)schanged their avatar %(count)s times|other": "%(severalUsers)schanged their avatar %(count)s times", + "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", + "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", "%(severalUsers)smade no changes %(count)s times|other": "%(severalUsers)smade no changes %(count)s times", + "%(severalUsers)smade no changes %(count)s times|one": "%(severalUsers)smade no changes", "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)smade no changes %(count)s times", + "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)smade no changes", "%(severalUsers)schanged the server ACLs %(count)s times|other": "%(severalUsers)schanged the server ACLs %(count)s times", + "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", + "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", "%(severalUsers)schanged the pinned messages for the room %(count)s times|other": "%(severalUsers)schanged the pinned messages for the room %(count)s times", + "%(severalUsers)schanged the pinned messages for the room %(count)s times|one": "%(severalUsers)schanged the pinned messages for the room", "%(oneUser)schanged the pinned messages for the room %(count)s times|other": "%(oneUser)schanged the pinned messages for the room %(count)s times", - "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved a message %(count)s times", - "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved a message %(count)s times", + "%(oneUser)schanged the pinned messages for the room %(count)s times|one": "%(oneUser)schanged the pinned messages for the room", + "%(severalUsers)sremoved a message %(count)s times|other": "%(severalUsers)sremoved %(count)s messages", + "%(severalUsers)sremoved a message %(count)s times|one": "%(severalUsers)sremoved a message", + "%(oneUser)sremoved a message %(count)s times|other": "%(oneUser)sremoved %(count)s messages", + "%(oneUser)sremoved a message %(count)s times|one": "%(oneUser)sremoved a message", "%(severalUsers)ssent %(count)s hidden messages|other": "%(severalUsers)ssent %(count)s hidden messages", + "%(severalUsers)ssent %(count)s hidden messages|one": "%(severalUsers)ssent a hidden message", "%(oneUser)ssent %(count)s hidden messages|other": "%(oneUser)ssent %(count)s hidden messages", + "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)ssent a hidden message", "collapse": "collapse", "expand": "expand", "Rotate Left": "Rotate Left", @@ -2371,9 +2447,11 @@ "This address is already in use": "This address is already in use", "This address had invalid server or is already in use": "This address had invalid server or is already in use", "View all %(count)s members|other": "View all %(count)s members", + "View all %(count)s members|one": "View 1 member", "Including you, %(commaSeparatedMembers)s": "Including you, %(commaSeparatedMembers)s", "Including %(commaSeparatedMembers)s": "Including %(commaSeparatedMembers)s", "%(count)s people you know have already joined|other": "%(count)s people you know have already joined", + "%(count)s people you know have already joined|one": "%(count)s person you know has already joined", "Edit topic": "Edit topic", "Click to read topic": "Click to read topic", "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", @@ -2416,6 +2494,7 @@ "Search for spaces": "Search for spaces", "Not all selected were added": "Not all selected were added", "Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)", + "Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...", "Direct Messages": "Direct Messages", "Add existing rooms": "Add existing rooms", "Want to add a new room instead?": "Want to add a new room instead?", @@ -2459,10 +2538,12 @@ "Try scrolling up in the timeline to see if there are any earlier ones.": "Try scrolling up in the timeline to see if there are any earlier ones.", "Remove recent messages by %(user)s": "Remove recent messages by %(user)s", "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|other": "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", + "You are about to remove %(count)s messages by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?|one": "You are about to remove %(count)s message by %(user)s. This will remove them permanently for everyone in the conversation. Do you wish to continue?", "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.": "For a large amount of messages, this might take some time. Please don't refresh your client in the meantime.", "Preserve system messages": "Preserve system messages", "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)", "Remove %(count)s messages|other": "Remove %(count)s messages", + "Remove %(count)s messages|one": "Remove 1 message", "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", @@ -2671,6 +2752,7 @@ "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", "%(count)s rooms|other": "%(count)s rooms", + "%(count)s rooms|one": "%(count)s room", "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only", "Select spaces": "Select spaces", "Decide which spaces can access this room. If a space is selected, its members can find and join .": "Decide which spaces can access this room. If a space is selected, its members can find and join .", @@ -2805,6 +2887,7 @@ "These files are too large to upload. The file size limit is %(limit)s.": "These files are too large to upload. The file size limit is %(limit)s.", "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Some files are too large to be uploaded. The file size limit is %(limit)s.", "Upload %(count)s other files|other": "Upload %(count)s other files", + "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", "Verify other device": "Verify other device", @@ -2818,6 +2901,7 @@ "The widget will verify your user ID, but won't be able to perform actions for you:": "The widget will verify your user ID, but won't be able to perform actions for you:", "Remember this": "Remember this", "%(count)s Members|other": "%(count)s Members", + "%(count)s Members|one": "%(count)s Member", "Public rooms": "Public rooms", "Use \"%(query)s\" to search": "Use \"%(query)s\" to search", "Search for": "Search for", @@ -2896,6 +2980,8 @@ "Filter results": "Filter results", "No results found": "No results found", "<%(count)s spaces>|other": "<%(count)s spaces>", + "<%(count)s spaces>|one": "", + "<%(count)s spaces>|zero": "", "Send custom state event": "Send custom state event", "Capabilities": "Capabilities", "Failed to load.": "Failed to load.", @@ -3126,6 +3212,7 @@ "No more results": "No more results", "Failed to reject invite": "Failed to reject invite", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", + "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "Joining": "Joining", "You don't have permission": "You don't have permission", "This room is suggested as a good one to join": "This room is suggested as a good one to join", @@ -3189,6 +3276,8 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", + "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", + "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Got an account? Sign in": "Got an account? Sign in", "New here? Create an account": "New here? Create an account", "Switch to light mode": "Switch to light mode", From 1662830e8227ee6b55cafd7779373dc87075f144 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Tue, 6 Sep 2022 00:04:52 -0400 Subject: [PATCH 072/176] i18n changes retry 6 --- src/i18n/strings/en_EN.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3d1b5241cad..1eb1035f857 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -100,8 +100,10 @@ "Empty room": "Empty room", "%(user1)s and %(user2)s": "%(user1)s and %(user2)s", "%(user)s and %(count)s others|other": "%(user)s and %(count)s others", + "%(user)s and %(count)s others|one": "%(user)s and 1 other", "Inviting %(user1)s and %(user2)s": "Inviting %(user1)s and %(user2)s", "Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others", + "Inviting %(user)s and %(count)s others|one": "Inviting %(user)s and 1 other", "Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)", "%(name)s is requesting verification": "%(name)s is requesting verification", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings", From af641b7b7dcfd5f580d540016bdcb7b712d77c31 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Tue, 6 Sep 2022 10:11:33 -0400 Subject: [PATCH 073/176] i18n changes retry 7 --- src/i18n/strings/en_EN.json | 100 ++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1eb1035f857..b9394758a65 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -16,40 +16,6 @@ "Error": "Error", "Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.", "Dismiss": "Dismiss", - "Call Failed": "Call Failed", - "User Busy": "User Busy", - "The user you called is busy.": "The user you called is busy.", - "The call could not be established": "The call could not be established", - "Answered Elsewhere": "Answered Elsewhere", - "The call was answered on another device.": "The call was answered on another device.", - "Call failed due to misconfigured server": "Call failed due to misconfigured server", - "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", - "Try using turn.matrix.org": "Try using turn.matrix.org", - "OK": "OK", - "Unable to access microphone": "Unable to access microphone", - "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", - "Unable to access webcam / microphone": "Unable to access webcam / microphone", - "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", - "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", - "Permission is granted to use the webcam": "Permission is granted to use the webcam", - "No other application is using the webcam": "No other application is using the webcam", - "Already in call": "Already in call", - "You're already in a call with this person.": "You're already in a call with this person.", - "Calls are unsupported": "Calls are unsupported", - "You cannot place calls in this browser.": "You cannot place calls in this browser.", - "Connectivity to the server has been lost": "Connectivity to the server has been lost", - "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", - "Too Many Calls": "Too Many Calls", - "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", - "You cannot place a call with yourself.": "You cannot place a call with yourself.", - "Unable to look up phone number": "Unable to look up phone number", - "There was an error looking up the phone number": "There was an error looking up the phone number", - "Unable to transfer call": "Unable to transfer call", - "Transfer Failed": "Transfer Failed", - "Failed to transfer call": "Failed to transfer call", - "Permission Required": "Permission Required", - "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "The file '%(fileName)s' failed to upload.": "The file '%(fileName)s' failed to upload.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "The file '%(fileName)s' exceeds this homeserver's size limit for uploads", "Upload Failed": "Upload Failed", @@ -92,6 +58,40 @@ "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", "Trust": "Trust", + "Call Failed": "Call Failed", + "User Busy": "User Busy", + "The user you called is busy.": "The user you called is busy.", + "The call could not be established": "The call could not be established", + "Answered Elsewhere": "Answered Elsewhere", + "The call was answered on another device.": "The call was answered on another device.", + "Call failed due to misconfigured server": "Call failed due to misconfigured server", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", + "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", + "Try using turn.matrix.org": "Try using turn.matrix.org", + "OK": "OK", + "Unable to access microphone": "Unable to access microphone", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", + "Unable to access webcam / microphone": "Unable to access webcam / microphone", + "Call failed because webcam or microphone could not be accessed. Check that:": "Call failed because webcam or microphone could not be accessed. Check that:", + "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", + "Permission is granted to use the webcam": "Permission is granted to use the webcam", + "No other application is using the webcam": "No other application is using the webcam", + "Already in call": "Already in call", + "You're already in a call with this person.": "You're already in a call with this person.", + "Calls are unsupported": "Calls are unsupported", + "You cannot place calls in this browser.": "You cannot place calls in this browser.", + "Connectivity to the server has been lost": "Connectivity to the server has been lost", + "You cannot place calls without a connection to the server.": "You cannot place calls without a connection to the server.", + "Too Many Calls": "Too Many Calls", + "You've reached the maximum number of simultaneous calls.": "You've reached the maximum number of simultaneous calls.", + "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "Unable to look up phone number": "Unable to look up phone number", + "There was an error looking up the phone number": "There was an error looking up the phone number", + "Unable to transfer call": "Unable to transfer call", + "Transfer Failed": "Transfer Failed", + "Failed to transfer call": "Failed to transfer call", + "Permission Required": "Permission Required", + "You do not have permission to start a conference call in this room": "You do not have permission to start a conference call in this room", "We couldn't log you in": "We couldn't log you in", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", "Try again": "Try again", @@ -775,8 +775,6 @@ "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", - "%(creator)s created this DM.": "%(creator)s created this DM.", - "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "File Attached": "File Attached", "Starting export process...": "Starting export process...", "Fetching events...": "Fetching events...", @@ -1052,6 +1050,7 @@ "Turn off camera": "Turn off camera", "Turn on camera": "Turn on camera", "Join": "Join", + "Dial": "Dial", "You are presenting": "You are presenting", "%(sharerName)s is presenting": "%(sharerName)s is presenting", "Your camera is turned off": "Your camera is turned off", @@ -1062,7 +1061,6 @@ "You held the call Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", "Connecting": "Connecting", - "Dial": "Dial", "Dialpad": "Dialpad", "Mute the microphone": "Mute the microphone", "Unmute the microphone": "Unmute the microphone", @@ -2161,17 +2159,6 @@ "%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.", "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", - "Call declined": "Call declined", - "Call back": "Call back", - "No answer": "No answer", - "Could not connect media": "Could not connect media", - "Connection failed": "Connection failed", - "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", - "An unknown error occurred": "An unknown error occurred", - "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", - "Retry": "Retry", - "Missed call": "Missed call", - "The call is in an unknown state!": "The call is in an unknown state!", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -2202,6 +2189,17 @@ "Message pending moderation": "Message pending moderation", "Pick a date to jump to": "Pick a date to jump to", "Go": "Go", + "Call declined": "Call declined", + "Call back": "Call back", + "No answer": "No answer", + "Could not connect media": "Could not connect media", + "Connection failed": "Connection failed", + "Their device couldn't start the camera or microphone": "Their device couldn't start the camera or microphone", + "An unknown error occurred": "An unknown error occurred", + "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", + "Retry": "Retry", + "Missed call": "Missed call", + "The call is in an unknown state!": "The call is in an unknown state!", "Error processing audio message": "Error processing audio message", "View live location": "View live location", "React": "React", @@ -3024,11 +3022,11 @@ "Observe only": "Observe only", "No verification requests found": "No verification requests found", "There was an error finding this widget.": "There was an error finding this widget.", - "Resume": "Resume", - "Hold": "Hold", "Input devices": "Input devices", "Output devices": "Output devices", "Cameras": "Cameras", + "Resume": "Resume", + "Hold": "Hold", "Resend %(unsentCount)s reaction(s)": "Resend %(unsentCount)s reaction(s)", "Open in OpenStreetMap": "Open in OpenStreetMap", "Forward": "Forward", @@ -3180,6 +3178,8 @@ "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Verification requested": "Verification requested", "Logout": "Logout", + "%(creator)s created this DM.": "%(creator)s created this DM.", + "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "You're all caught up": "You're all caught up", "You have no visible notifications.": "You have no visible notifications.", "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", From f7a4e5728b7909d4582ec1404a6dc8ce5ff61fda Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Tue, 6 Sep 2022 12:09:10 -0400 Subject: [PATCH 074/176] autocomplete fix --- src/autocomplete/EmojiProvider.tsx | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 369f2e3b300..6c0282cf980 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -168,23 +168,8 @@ export default class EmojiProvider extends AutocompleteProvider { } }); - //if there is an exact shortcode match in the frequently used emojis, it goes before everything - for (let i = 0; i < recentlyUsedAutocomplete.length; i++) { - if (recentlyUsedAutocomplete[i].emoji.shortcodes[0] === trimmedMatch) { - const exactMatchEmoji = recentlyUsedAutocomplete[i]; - for (let j = i; j > 0; j--) { - recentlyUsedAutocomplete[j] = recentlyUsedAutocomplete[j - 1]; - } - recentlyUsedAutocomplete[0] = exactMatchEmoji; - break; - } - } - - completions = recentlyUsedAutocomplete.concat(completions); - completions = uniqBy(completions, "emoji"); - - return completions.map((c) => ({ - completion: c.emoji.unicode, + completions = completions.map(c => ({ + completion: this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode, component: ( { this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode } From dc7bee732e715e49d426476d5e3947f81b6373fa Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Sep 2022 15:37:04 -0400 Subject: [PATCH 075/176] added encryption and decryption for emotes --- src/autocomplete/EmojiProvider.tsx | 30 +++++++++---- src/components/structures/MessagePanel.tsx | 3 -- src/components/views/messages/TextualBody.tsx | 42 ++++++++++++------- .../views/room_settings/RoomEmoteSettings.tsx | 28 +++++++++---- 4 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 6c0282cf980..17b5b9536f0 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -34,6 +34,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext"; import * as recent from "../emojipicker/recent"; import { filterBoolean } from "../utils/arrays"; import { mediaFromMxc } from "../customisations/Media"; +import { decryptFile } from '../utils/DecryptFile'; const LIMIT = 20; @@ -81,16 +82,18 @@ export default class EmojiProvider extends AutocompleteProvider { public matcher: QueryMatcher; public nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - emotes: Dictionary; + emotes: Dictionary; + emotesPromise: Promise; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - this.emotes = {}; - for (const key in rawEmotes) { - this.emotes[key] = ""; - } + this.emotesPromise = this.decryptEmotes(rawEmotes); + this.emotes={}; + // for (const key in rawEmotes) { FOR UNENCRYPTED + // this.emotes[key] = ""; + // } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [(o) => o.emoji.shortcodes.map((s) => `:${s}:`)], @@ -106,7 +109,18 @@ export default class EmojiProvider extends AutocompleteProvider { this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode)))); } - public async getCompletions( + private async decryptEmotes(emotes: Object){ + const decryptede={} + for (const shortcode in emotes) { + const blob = await decryptFile(emotes[shortcode]); + const durl=URL.createObjectURL(blob); + decryptede[shortcode] = ""; + } + return decryptede + } + + async getCompletions( query: string, selection: ISelectionRange, force?: boolean, @@ -115,6 +129,8 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } + this.emotes=await this.emotesPromise + //console.log("emotes",this.emotes) const emojisAndEmotes=[...SORTED_EMOJI]; for (const key in this.emotes) { emojisAndEmotes.push({ diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index d90af4e837a..5a329497f77 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -205,7 +205,6 @@ interface IState { ghostReadMarkers: string[]; showTypingNotifications: boolean; hideSender: boolean; - emotes: Dictionary; } interface IReadReceiptForUser { @@ -282,7 +281,6 @@ export default class MessagePanel extends React.Component { ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), hideSender: this.shouldHideSender(), - emotes: {}, }; // Cache these settings on mount since Settings is expensive to query, @@ -297,7 +295,6 @@ export default class MessagePanel extends React.Component { public componentDidMount(): void { this.calculateRoomMembersCount(); this.props.room?.currentState.on(RoomStateEvent.Update, this.calculateRoomMembersCount); - //this.props.room?.currentState.on(RoomStateEvent.Update, this.getEmotes); this.isMounted = true; } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 93ee9997f85..05f10fd580b 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -47,7 +47,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from '../../../utils/Reply'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { mediaFromMxc } from "../../../customisations/Media"; +import { decryptFile } from '../../../utils/DecryptFile'; const MAX_HIGHLIGHT_LENGTH = 4096; interface IState { @@ -56,6 +56,7 @@ interface IState { // track whether the preview widget is hidden widgetHidden: boolean; + finalEmotes: Dictionary; } export default class TextualBody extends React.Component { @@ -74,6 +75,7 @@ export default class TextualBody extends React.Component { this.state = { links: [], widgetHidden: false, + finalEmotes: {}, }; } @@ -84,8 +86,9 @@ export default class TextualBody extends React.Component { } private applyFormatting(): void { - // Function is only called from render / componentDidMount → contentRef is set - const content = this.contentRef.current!; + this.decryptEmotes(); + const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); + this.activateSpoilers([this.contentRef.current]); const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); this.activateSpoilers([content]); @@ -563,8 +566,25 @@ export default class TextualBody extends React.Component { } return {`(${text})`}; } - - public render(): React.ReactNode { + private async decryptEmotes(){ + const client = MatrixClientPeg.get(); + const room = client.getRoom(this.props.mxEvent.getRoomId()); + //TODO: Do not encrypt/decrypt if room is not encrypted + const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const decryptede={} + for (const shortcode in rawEmotes) { + const blob = await decryptFile(rawEmotes[shortcode]); + const durl=URL.createObjectURL(blob); + decryptede[":" + shortcode + ":"] = ""; + } + this.setState({ + finalEmotes:decryptede + }); + this.forceUpdate(); + } + render() { if (this.props.editState) { const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); return isWysiwygComposerEnabled ? ( @@ -580,16 +600,6 @@ export default class TextualBody extends React.Component { // only strip reply if this is the original replying event, edits thereafter do not have the fallback const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent); let body: ReactNode; - const client = MatrixClientPeg.get(); - const room = client.getRoom(mxEvent.getRoomId()); - //TODO: Decrypt emotes if encryption is added - const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); - const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const finalEmotes = {}; - for (const key in rawEmotes) { - finalEmotes[":" + key + ":"] = ""; - } if (SettingsStore.isEnabled("feature_extensible_events")) { const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; if (extev?.isEquivalentTo(M_MESSAGE)) { @@ -607,7 +617,7 @@ export default class TextualBody extends React.Component { stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, - emotes: finalEmotes, + emotes: this.state.finalEmotes, })); } } diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 4608df1397c..977ab17f45e 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -18,9 +18,10 @@ import React, { createRef } from 'react'; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { mediaFromMxc } from "../../../customisations/Media"; import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; +import { uploadFile } from "../../../ContentMessages"; +import { decryptFile } from "../../../utils/DecryptFile"; interface IProps { roomId: string; @@ -28,6 +29,7 @@ interface IProps { interface IState { emotes: Dictionary; + decryptedemotes: Dictionary; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; newEmoteCodeAdded: boolean; @@ -50,7 +52,7 @@ export default class RoomEmoteSettings extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); - //TODO: Decrypt the shortcodes and emotes if they are encrypted + //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const value = {}; @@ -60,6 +62,7 @@ export default class RoomEmoteSettings extends React.Component { this.state = { emotes: emotes, + decryptedemotes: {}, EmoteFieldsTouched: {}, newEmoteFileAdded: false, newEmoteCodeAdded: false, @@ -70,6 +73,7 @@ export default class RoomEmoteSettings extends React.Component { canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), value: value, }; + this.decryptEmotes(); } private uploadEmoteClick = (): void => { @@ -141,8 +145,8 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - const newEmote = await client.uploadContent(this.state.newEmoteFile); - emotesMxcs[this.state.newEmoteCode] = newEmote; + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile)//await client.uploadContent(this.state.newEmoteFile); + emotesMxcs[this.state.newEmoteCode] = newEmote.file; value[this.state.newEmoteCode] = this.state.newEmoteCode; } if (this.state.emotes) { @@ -172,6 +176,7 @@ export default class RoomEmoteSettings extends React.Component { newState.deletedItems = {}; } this.setState(newState as IState); + this.decryptEmotes(); }; private onEmoteChange = (e: React.ChangeEvent): void => { @@ -221,8 +226,17 @@ export default class RoomEmoteSettings extends React.Component { }); } }; - - public render(): JSX.Element { + private async decryptEmotes(){ + const decryptede={} + for (const shortcode in this.state.emotes) { + const blob = await decryptFile(this.state.emotes[shortcode]); + decryptede[shortcode] = URL.createObjectURL(blob); + } + this.setState({ + decryptedemotes:decryptede, + }); + } + public render(): JSX.Element { let emoteSettingsButtons; if ( this.state.canAddEmote @@ -262,7 +276,7 @@ export default class RoomEmoteSettings extends React.Component { />
    Date: Thu, 8 Sep 2022 15:44:27 -0400 Subject: [PATCH 076/176] encryption code cleanup --- src/components/views/messages/TextualBody.tsx | 8 ++++---- .../views/room_settings/RoomEmoteSettings.tsx | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 05f10fd580b..6c8bc237279 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -566,21 +566,21 @@ export default class TextualBody extends React.Component { } return {`(${text})`}; } - private async decryptEmotes(){ + private async decryptEmotes() { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.mxEvent.getRoomId()); //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const decryptede={} + const decryptede={}; for (const shortcode in rawEmotes) { - const blob = await decryptFile(rawEmotes[shortcode]); + const blob = await decryptFile(rawEmotes[shortcode]); const durl=URL.createObjectURL(blob); decryptede[":" + shortcode + ":"] = ""; } this.setState({ - finalEmotes:decryptede + finalEmotes: decryptede, }); this.forceUpdate(); } diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 977ab17f45e..5407650e94d 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -145,7 +145,7 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile)//await client.uploadContent(this.state.newEmoteFile); + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED emotesMxcs[this.state.newEmoteCode] = newEmote.file; value[this.state.newEmoteCode] = this.state.newEmoteCode; } @@ -226,17 +226,17 @@ export default class RoomEmoteSettings extends React.Component { }); } }; - private async decryptEmotes(){ - const decryptede={} + private async decryptEmotes() { + const decryptede={}; for (const shortcode in this.state.emotes) { - const blob = await decryptFile(this.state.emotes[shortcode]); + const blob = await decryptFile(this.state.emotes[shortcode]); decryptede[shortcode] = URL.createObjectURL(blob); } this.setState({ - decryptedemotes:decryptede, + decryptedemotes: decryptede, }); } - public render(): JSX.Element { + public render(): JSX.Element { let emoteSettingsButtons; if ( this.state.canAddEmote From 3c39b308a2417a5a7ea36fd460725a810752a1a4 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Sep 2022 16:17:30 -0400 Subject: [PATCH 077/176] encryption code type fixes --- src/components/views/messages/TextualBody.tsx | 2 +- src/components/views/room_settings/RoomEmoteSettings.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 6c8bc237279..07355ca5af7 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -56,7 +56,7 @@ interface IState { // track whether the preview widget is hidden widgetHidden: boolean; - finalEmotes: Dictionary; + finalEmotes: Dictionary; } export default class TextualBody extends React.Component { diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 5407650e94d..d5a6ea87fb3 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -22,13 +22,14 @@ import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; +import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; interface IProps { roomId: string; } interface IState { - emotes: Dictionary; + emotes: Dictionary; decryptedemotes: Dictionary; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; @@ -37,7 +38,7 @@ interface IState { newEmoteFile: File; canAddEmote: boolean; deleted: boolean; - deletedItems: Dictionary; + deletedItems: Dictionary; value: Dictionary; } From 31ad8fcbdee4c0d0c3382f22f6851fdcd15661a9 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:00:08 -0400 Subject: [PATCH 078/176] added multiple emote upload and read shortcode from filename --- .../views/room_settings/RoomEmoteSettings.tsx | 109 +++++++++++------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index d5a6ea87fb3..ef69068432b 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -34,8 +34,8 @@ interface IState { EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; newEmoteCodeAdded: boolean; - newEmoteCode: string; - newEmoteFile: File; + newEmoteCode: Array; + newEmoteFile: Array; canAddEmote: boolean; deleted: boolean; deletedItems: Dictionary; @@ -67,8 +67,8 @@ export default class RoomEmoteSettings extends React.Component { EmoteFieldsTouched: {}, newEmoteFileAdded: false, newEmoteCodeAdded: false, - newEmoteCode: "", - newEmoteFile: null, + newEmoteCode: [""], + newEmoteFile: [], deleted: false, deletedItems: {}, canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), @@ -105,13 +105,15 @@ export default class RoomEmoteSettings extends React.Component { EmoteFieldsTouched: {}, newEmoteFileAdded: false, newEmoteCodeAdded: false, + newEmoteCode: [""], + newEmoteFile: [], deleted: false, deletedItems: {}, value: value, }); - this.emoteUpload.current.value = ""; - this.emoteCodeUpload.current.value = ""; + //this.emoteUpload.current.value = ""; + //this.emoteCodeUpload.current.value = ""; }; private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); @@ -146,14 +148,16 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED - emotesMxcs[this.state.newEmoteCode] = newEmote.file; - value[this.state.newEmoteCode] = this.state.newEmoteCode; + for (var i = 0; i < this.state.newEmoteCode.length; i++) { + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; + } } if (this.state.emotes) { for (const shortcode in this.state.emotes) { if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) - && (shortcode === this.state.newEmoteCode)) { + && (shortcode in this.state.newEmoteCode)) { continue; } if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { @@ -167,14 +171,16 @@ export default class RoomEmoteSettings extends React.Component { } newState.value = value; await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); - this.emoteUpload.current.value = ""; - this.emoteCodeUpload.current.value = ""; + //this.emoteUpload.current.value = ""; + //this.emoteCodeUpload.current.value = ""; newState.newEmoteFileAdded = false; newState.newEmoteCodeAdded = false; newState.EmoteFieldsTouched = {}; newState.emotes = emotesMxcs; newState.deleted = false; newState.deletedItems = {}; + newState.newEmoteCode = [""] + newState.newEmoteFile =[] } this.setState(newState as IState); this.decryptEmotes(); @@ -198,32 +204,44 @@ export default class RoomEmoteSettings extends React.Component { return; } - const file = e.target.files[0]; - const reader = new FileReader(); - reader.onload = (ev) => { + const uploadedFiles=[] + const newCodes=[] + for (const file of e.target.files){ + const fileName = file.name.replace(/\.[^.]*$/,'') + uploadedFiles.push(file) + newCodes.push(fileName) + } + //reader.onload = (ev) => { this.setState({ + newEmoteCodeAdded: true, newEmoteFileAdded: true, - newEmoteFile: file, + newEmoteCode: newCodes, + newEmoteFile: uploadedFiles, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, }, }); - this.emoteUploadImage.current.src = URL.createObjectURL(file); - }; - reader.readAsDataURL(file); + //this.emoteUploadImage.current.src = URL.createObjectURL(file); + //}; + //reader.readAsDataURL(file); }; private onEmoteCodeAdd = (e: React.ChangeEvent): void => { if (e.target.value.length > 0) { + const updatedCode=this.state.newEmoteCode; + updatedCode[e.target.getAttribute("data-index")]=e.target.value; this.setState({ newEmoteCodeAdded: true, - newEmoteCode: e.target.value, + newEmoteCode: updatedCode, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, }, }); } else { + const updatedCode=this.state.newEmoteCode; + updatedCode[e.target.getAttribute("data-index")]=e.target.value; this.setState({ newEmoteCodeAdded: false, + newEmoteCode: updatedCode, }); } }; @@ -309,6 +327,35 @@ export default class RoomEmoteSettings extends React.Component { ); } + let uploadedEmotes =[]; + for (var i = 0; i < this.state.newEmoteCode.length; i++) { + const fileUrl=this.state.newEmoteFile[i]? URL.createObjectURL(this.state.newEmoteFile[i]):"" + uploadedEmotes.push( +
  • + + { + this.state.newEmoteFileAdded ? + : null + } + + { i==0? emoteUploadButton: null } +
  • + + ) + } return (
    { onClick={chromeFileInputFix} onChange={this.onEmoteFileAdd} accept="image/*" + multiple /> { emoteSettingsButtons } -
  • - - { - this.state.newEmoteFileAdded ? - : null - } - - { emoteUploadButton } -
  • + { uploadedEmotes } { existingEmotes } From 3cf41dd6d16d996cb729289a354811349147ea6e Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:10:02 -0400 Subject: [PATCH 079/176] code cleanup --- .../views/room_settings/RoomEmoteSettings.tsx | 95 +++++++++---------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index ef69068432b..fb75165139b 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -148,11 +148,11 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { - for (var i = 0; i < this.state.newEmoteCode.length; i++) { - const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; - value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; - } + for (let i = 0; i < this.state.newEmoteCode.length; i++) { + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; + } } if (this.state.emotes) { for (const shortcode in this.state.emotes) { @@ -179,8 +179,8 @@ export default class RoomEmoteSettings extends React.Component { newState.emotes = emotesMxcs; newState.deleted = false; newState.deletedItems = {}; - newState.newEmoteCode = [""] - newState.newEmoteFile =[] + newState.newEmoteCode = [""]; + newState.newEmoteFile =[]; } this.setState(newState as IState); this.decryptEmotes(); @@ -204,23 +204,23 @@ export default class RoomEmoteSettings extends React.Component { return; } - const uploadedFiles=[] - const newCodes=[] - for (const file of e.target.files){ - const fileName = file.name.replace(/\.[^.]*$/,'') - uploadedFiles.push(file) - newCodes.push(fileName) - } + const uploadedFiles=[]; + const newCodes=[]; + for (const file of e.target.files) { + const fileName = file.name.replace(/\.[^.]*$/, ''); + uploadedFiles.push(file); + newCodes.push(fileName); + } //reader.onload = (ev) => { - this.setState({ - newEmoteCodeAdded: true, - newEmoteFileAdded: true, - newEmoteCode: newCodes, - newEmoteFile: uploadedFiles, - EmoteFieldsTouched: { - ...this.state.EmoteFieldsTouched, - }, - }); + this.setState({ + newEmoteCodeAdded: true, + newEmoteFileAdded: true, + newEmoteCode: newCodes, + newEmoteFile: uploadedFiles, + EmoteFieldsTouched: { + ...this.state.EmoteFieldsTouched, + }, + }); //this.emoteUploadImage.current.src = URL.createObjectURL(file); //}; //reader.readAsDataURL(file); @@ -327,34 +327,33 @@ export default class RoomEmoteSettings extends React.Component { ); } - let uploadedEmotes =[]; - for (var i = 0; i < this.state.newEmoteCode.length; i++) { - const fileUrl=this.state.newEmoteFile[i]? URL.createObjectURL(this.state.newEmoteFile[i]):"" + const uploadedEmotes = []; + for (let i = 0; i < this.state.newEmoteCode.length; i++) { + const fileUrl = this.state.newEmoteFile[i] ? URL.createObjectURL(this.state.newEmoteFile[i]) : ""; uploadedEmotes.push(
  • - - { - this.state.newEmoteFileAdded ? - : null - } - - { i==0? emoteUploadButton: null } -
  • + + { + this.state.newEmoteFileAdded ? + : null + } - ) + {i == 0 ? emoteUploadButton : null} + , + ); } return ( From 380cc54e429ffdbdfd34f768629766fbdf907879 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:16:54 -0400 Subject: [PATCH 080/176] code cleanup 2 --- src/components/views/room_settings/RoomEmoteSettings.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index fb75165139b..f06fadea101 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -221,13 +221,13 @@ export default class RoomEmoteSettings extends React.Component { ...this.state.EmoteFieldsTouched, }, }); - //this.emoteUploadImage.current.src = URL.createObjectURL(file); + //this.emoteUploadImage.current.src = URL.createObjectURL(file); //}; //reader.readAsDataURL(file); }; private onEmoteCodeAdd = (e: React.ChangeEvent): void => { if (e.target.value.length > 0) { - const updatedCode=this.state.newEmoteCode; + const updatedCode = this.state.newEmoteCode; updatedCode[e.target.getAttribute("data-index")]=e.target.value; this.setState({ newEmoteCodeAdded: true, @@ -351,7 +351,7 @@ export default class RoomEmoteSettings extends React.Component { /> : null } - {i == 0 ? emoteUploadButton : null} + { i == 0 ? emoteUploadButton : null } , ); } From 9f5d49af9a268a49e8e04d8b592645ba30785f01 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Sep 2022 14:53:38 -0400 Subject: [PATCH 081/176] makes emotes work in public(unencrypted) rooms and encrypted rooms --- src/components/views/messages/TextualBody.tsx | 13 +++++-- .../views/room_settings/RoomEmoteSettings.tsx | 35 +++++++++++++------ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 07355ca5af7..60322d60dcf 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -48,6 +48,8 @@ import { options as linkifyOpts } from "../../../linkify-matrix"; import { getParentEventId } from '../../../utils/Reply'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { decryptFile } from '../../../utils/DecryptFile'; +import { mediaFromMxc } from '../../../customisations/Media'; + const MAX_HIGHLIGHT_LENGTH = 4096; interface IState { @@ -573,9 +575,16 @@ export default class TextualBody extends React.Component { const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const decryptede={}; + let durl=""; + const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()) for (const shortcode in rawEmotes) { - const blob = await decryptFile(rawEmotes[shortcode]); - const durl=URL.createObjectURL(blob); + if (isEnc) { + const blob = await decryptFile(rawEmotes[shortcode]); + durl = URL.createObjectURL(blob); + } else { + durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp + } + decryptede[":" + shortcode + ":"] = ""; } diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index f06fadea101..bca980918a7 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -22,14 +22,14 @@ import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; -import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; +import { mediaFromMxc } from '../../../customisations/Media'; interface IProps { roomId: string; } interface IState { - emotes: Dictionary; + emotes: Dictionary; decryptedemotes: Dictionary; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; @@ -38,7 +38,7 @@ interface IState { newEmoteFile: Array; canAddEmote: boolean; deleted: boolean; - deletedItems: Dictionary; + deletedItems: Dictionary; value: Dictionary; } @@ -74,7 +74,11 @@ export default class RoomEmoteSettings extends React.Component { canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), value: value, }; - this.decryptEmotes(); + this.decryptEmotes(client.isRoomEncrypted(props.roomId)); + } + componentDidMount() { + const client = MatrixClientPeg.get(); + this.decryptEmotes(client.isRoomEncrypted(this.props.roomId)); } private uploadEmoteClick = (): void => { @@ -150,7 +154,12 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { for (let i = 0; i < this.state.newEmoteCode.length; i++) { const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + if (client.isRoomEncrypted(this.props.roomId)) { + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + } + else { + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url; + } value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; } } @@ -183,7 +192,7 @@ export default class RoomEmoteSettings extends React.Component { newState.newEmoteFile =[]; } this.setState(newState as IState); - this.decryptEmotes(); + this.decryptEmotes(client.isRoomEncrypted(this.props.roomId)); }; private onEmoteChange = (e: React.ChangeEvent): void => { @@ -245,15 +254,21 @@ export default class RoomEmoteSettings extends React.Component { }); } }; - private async decryptEmotes() { - const decryptede={}; + private async decryptEmotes(isEnc:boolean) { + const decryptede = {}; for (const shortcode in this.state.emotes) { - const blob = await decryptFile(this.state.emotes[shortcode]); - decryptede[shortcode] = URL.createObjectURL(blob); + if (isEnc) { + const blob = await decryptFile(this.state.emotes[shortcode]); + decryptede[shortcode] = URL.createObjectURL(blob); + } else { + decryptede[shortcode] = mediaFromMxc(this.state.emotes[shortcode]).srcHttp + } + } this.setState({ decryptedemotes: decryptede, }); + //this.forceUpdate(); } public render(): JSX.Element { let emoteSettingsButtons; From 2dfbc90461faea3afaed859d013ff0e71b2344a8 Mon Sep 17 00:00:00 2001 From: nipun-mallipeddi <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Sep 2022 15:01:08 -0400 Subject: [PATCH 082/176] code fix --- src/components/views/messages/TextualBody.tsx | 4 ++-- src/components/views/room_settings/RoomEmoteSettings.tsx | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 60322d60dcf..5c551c0a84f 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -576,13 +576,13 @@ export default class TextualBody extends React.Component { const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const decryptede={}; let durl=""; - const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()) + const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()); for (const shortcode in rawEmotes) { if (isEnc) { const blob = await decryptFile(rawEmotes[shortcode]); durl = URL.createObjectURL(blob); } else { - durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp + durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp; } decryptede[":" + shortcode + ":"] = " + { emoji.customComponent?emoji.customComponent:emoji.unicode } ); diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 3158d52e553..9c0745cd564 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -37,6 +37,11 @@ import { Key } from "../../../Keyboard"; import { clamp } from "../../../utils/numbers"; import { ButtonEvent } from "../elements/AccessibleButton"; import { Ref } from "../../../accessibility/roving/types"; +import Category, { ICategory, CategoryKey } from "./Category"; +import { Room } from './matrix-js-sdk/src/models/room'; +import { mediaFromMxc } from '../../../customisations/Media'; +import { decryptFile } from '../../../utils/DecryptFile'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; @@ -49,6 +54,7 @@ interface IProps { onChoose(unicode: string): boolean; onFinished(): void; isEmojiDisabled?: (unicode: string) => boolean; + room?:Room; } interface IState { @@ -62,15 +68,27 @@ interface IState { } class EmojiPicker extends React.Component { - private readonly recentlyUsed: IEmoji[]; + private recentlyUsed: IEmoji[]; private readonly memoizedDataByCategory: Record; private readonly categories: ICategory[]; private scrollRef = React.createRef>(); - public constructor(props: IProps) { + private emotes: Map; + private emotesPromise: Promise>; + private finalEmotes: IEmoji[]; + private finalEmotesMap:Map; + constructor(props: IProps) { super(props); + + const emotesEvent = props.room?.currentState.getStateEvents("m.room.emotes", ""); + const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + this.emotesPromise = this.decryptEmotes(rawEmotes, props.room?.roomId); + this.finalEmotes=[]; + this.finalEmotesMap=new Map(); + this.loadEmotes() + this.state = { filter: "", scrollTop: 0, @@ -81,77 +99,135 @@ class EmojiPicker extends React.Component { this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode)))); this.memoizedDataByCategory = { recent: this.recentlyUsed, + custom: this.finalEmotes, ...DATA_BY_CATEGORY, }; - this.categories = [ - { - id: "recent", - name: _t("Frequently Used"), - enabled: this.recentlyUsed.length > 0, - visible: this.recentlyUsed.length > 0, - ref: React.createRef(), - }, - { - id: "people", - name: _t("Smileys & People"), - enabled: true, - visible: true, - ref: React.createRef(), - }, - { - id: "nature", - name: _t("Animals & Nature"), - enabled: true, - visible: false, - ref: React.createRef(), - }, - { - id: "foods", - name: _t("Food & Drink"), - enabled: true, - visible: false, - ref: React.createRef(), - }, - { - id: "activity", - name: _t("Activities"), - enabled: true, - visible: false, - ref: React.createRef(), - }, - { - id: "places", - name: _t("Travel & Places"), - enabled: true, - visible: false, - ref: React.createRef(), - }, - { - id: "objects", - name: _t("Objects"), - enabled: true, - visible: false, - ref: React.createRef(), - }, - { - id: "symbols", - name: _t("Symbols"), - enabled: true, - visible: false, - ref: React.createRef(), - }, - { - id: "flags", - name: _t("Flags"), - enabled: true, - visible: false, - ref: React.createRef(), - }, - ]; + this.categories = [{ + id: "recent", + name: _t("Frequently Used"), + enabled: true, + visible: true, + ref: React.createRef(), + },{ + id: "custom", + name: _t("Custom"), + enabled: true, + visible: true, + ref: React.createRef(), + },{ + id: "people", + name: _t("Smileys & People"), + enabled: true, + visible: true, + ref: React.createRef(), + }, { + id: "nature", + name: _t("Animals & Nature"), + enabled: true, + visible: false, + ref: React.createRef(), + }, { + id: "foods", + name: _t("Food & Drink"), + enabled: true, + visible: false, + ref: React.createRef(), + }, { + id: "activity", + name: _t("Activities"), + enabled: true, + visible: false, + ref: React.createRef(), + }, { + id: "places", + name: _t("Travel & Places"), + enabled: true, + visible: false, + ref: React.createRef(), + }, { + id: "objects", + name: _t("Objects"), + enabled: true, + visible: false, + ref: React.createRef(), + }, { + id: "symbols", + name: _t("Symbols"), + enabled: true, + visible: false, + ref: React.createRef(), + }, { + id: "flags", + name: _t("Flags"), + enabled: true, + visible: false, + ref: React.createRef(), + }]; + } + + private async loadEmotes(){ + this.emotes=await this.emotesPromise + for (const key in this.emotes) { + this.finalEmotes.push( + { label: key, + shortcodes: [key], + hexcode: key, + unicode: ":"+key+":", + customLabel:key, + customComponent:this.emotes[key] + } + ); + this.finalEmotesMap.set((":"+key+":").trim(),{ + label: key, + shortcodes: [key], + hexcode: key, + unicode: ":"+key+":", + customLabel:key, + customComponent:this.emotes[key] + }); + } + + let rec=Array.from(new Set(recent.get())); + rec.forEach((v,i)=>{ + if(this.finalEmotesMap.get(v as string)){ + if(i>=this.recentlyUsed.length){ + this.recentlyUsed.push(this.finalEmotesMap.get(v as string)) + } + else{ + this.recentlyUsed[i]=this.finalEmotesMap.get(v as string) + } + + } else if(getEmojiFromUnicode(v as string)){ + if(i>=this.recentlyUsed.length){ + this.recentlyUsed.push(getEmojiFromUnicode(v as string)) + } + else{ + this.recentlyUsed[i]=getEmojiFromUnicode(v as string) + } + } + }) + this.onScroll(); } - private onScroll = (): void => { + private async decryptEmotes(emotes: Object, roomId: string) { + const decryptede=new Map(); + const client = MatrixClientPeg.get(); + let durl = ""; + const isEnc=client.isRoomEncrypted(roomId); + for (const shortcode in emotes) { + if (isEnc) { + const blob = await decryptFile(emotes[shortcode]); + durl = URL.createObjectURL(blob); + } else { + durl = mediaFromMxc(emotes[shortcode]).srcHttp; + } + decryptede[shortcode] = ; + } + return decryptede; + } + + private onScroll = () => { const body = this.scrollRef.current?.containerRef.current; if (!body) return; this.setState({ @@ -266,7 +342,11 @@ class EmojiPicker extends React.Component { if (lcFilter.includes(this.state.filter)) { emojis = this.memoizedDataByCategory[cat.id]; } else { + emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id]; + if(cat.id==="custom"){ + emojis=this.finalEmotes + } } if (lcFilter !== "") { diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index a13958efe1d..ea9f780e1f7 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -24,16 +24,14 @@ interface IProps { } class Preview extends React.PureComponent { - public render(): React.ReactNode { - const { - unicode, - label, - shortcodes: [shortcode], - } = this.props.emoji; + render() { + const { unicode, label, shortcodes: [shortcode], customComponent } = this.props.emoji; return (
    -
    {unicode}
    +
    + { customComponent?customComponent:unicode } +
    {label}
    {shortcode}
    diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index dc95b5044e5..befc3e68470 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -79,15 +79,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { let moreButtons: ReactNode[]; if (narrow) { mainButtons = [ - isWysiwygLabEnabled ? ( - - ) : ( - emojiButton(props) - ), + emojiButton(props,room), ]; moreButtons = [ uploadButton(), // props passed via UploadButtonContext @@ -99,15 +91,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { ]; } else { mainButtons = [ - isWysiwygLabEnabled ? ( - - ) : ( - emojiButton(props) - ), + emojiButton(props,room), uploadButton(), // props passed via UploadButtonContext ]; moreButtons = [ @@ -154,13 +138,60 @@ const MessageComposerButtons: React.FC = (props: IProps) => { ); }; -function emojiButton(props: IProps): ReactElement { - return ( - ; +} + +interface IEmojiButtonProps { + addEmoji: (unicode: string) => boolean; + menuPosition: AboveLeftOf; + room: Room; +} + +const EmojiButton: React.FC = ({ addEmoji, menuPosition, room}) => { + const overflowMenuCloser = useContext(OverflowMenuContext); + const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); + let contextMenu: React.ReactElement | null = null; + if (menuDisplayed) { + const position = ( + menuPosition ?? aboveLeftOf(button.current.getBoundingClientRect()) + ); + + contextMenu = { + closeMenu(); + overflowMenuCloser?.(); + }} + managed={false} + > + + ; + } + + const className = classNames( + "mx_MessageComposer_button", + { + "mx_MessageComposer_button_highlight": menuDisplayed, + }, + "mx_EmojiButton_icon" + ); + + // TODO: replace ContextMenuTooltipButton with a unified representation of + // the header buttons and the right panel buttons + return + ); } diff --git a/src/emoji.ts b/src/emoji.ts index 12f136fbcf7..4c532c9f1b8 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -14,8 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import EMOJIBASE from "emojibase-data/en/compact.json"; -import SHORTCODES from "emojibase-data/en/shortcodes/iamcal.json"; +import React from 'react'; +import EMOJIBASE from 'emojibase-data/en/compact.json'; +import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json'; + export interface IEmoji { label: string; @@ -27,6 +29,8 @@ export interface IEmoji { unicode: string; skins?: Omit[]; // Currently unused emoticon?: string | string[]; + customLabel?:string; + customComponent?:React.Component; } // The unicode is stored without the variant selector From 8a28f23032acb73238076799d8a0747dddb753c3 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 28 Dec 2022 12:52:14 -0500 Subject: [PATCH 086/176] Add compatibility option with other clients --- .../views/room_settings/RoomEmoteSettings.tsx | 89 ++++++++++++++++++- .../views/rooms/SendMessageComposer.tsx | 48 ++++++++-- 2 files changed, 127 insertions(+), 10 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 68b2df5c7bb..390d9bcfde4 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -23,7 +23,12 @@ import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; import { mediaFromMxc } from '../../../customisations/Media'; - +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; +import SettingsFieldset from '../settings/SettingsFieldset'; +import LabelledToggleSwitch from '../elements/LabelledToggleSwitch'; +import { stringify } from '../dialogs/devtools/Event'; +const EMOTES_STATE=new UnstableValue("m.room.emotes","org.matrix.msc3892.emotes") +const COMPAT_STATE=new UnstableValue("m.room.clientemote_compatibility","org.matrix.msc3892.clientemote_compatibility") interface IProps { roomId: string; } @@ -40,6 +45,7 @@ interface IState { deleted: boolean; deletedItems: Dictionary; value: Dictionary; + compatiblity: boolean; } // TODO: Merge with EmoteSettings? @@ -47,12 +53,14 @@ export default class RoomEmoteSettings extends React.Component { private emoteUpload = createRef(); private emoteCodeUpload = createRef(); private emoteUploadImage = createRef(); + private imagePack:object; constructor(props: IProps) { super(props); const client = MatrixClientPeg.get(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); + //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; @@ -61,6 +69,15 @@ export default class RoomEmoteSettings extends React.Component { value[emote] = emote; } + const compatEvent = room.currentState.getStateEvents("m.room.clientemote_compatibility", ""); + const compat = compatEvent ? (compatEvent.getContent().isCompat || false) : false; + + const imagePackEvent = room.currentState.getStateEvents("m.image_pack", ""); + this.imagePack = imagePackEvent ? (imagePackEvent.getContent() || {"images":new Map()}) : {"images":new Map()}; + if(!this.imagePack["images"]){ + this.imagePack={"images":new Map()} + } + this.state = { emotes: emotes, decryptedemotes: {}, @@ -71,8 +88,9 @@ export default class RoomEmoteSettings extends React.Component { newEmoteFile: [], deleted: false, deletedItems: {}, - canAddEmote: room.currentState.maySendStateEvent('m.room.emotes', client.getUserId()), + canAddEmote: room.currentState.maySendStateEvent("m.room.emotes", client.getUserId()), value: value, + compatibility:compat }; this.decryptEmotes(client.isRoomEncrypted(props.roomId)); } @@ -147,6 +165,7 @@ export default class RoomEmoteSettings extends React.Component { const newState: Partial = {}; const emotesMxcs = {}; const value = {}; + const newPack={"images":new Map()} // TODO: What do we do about errors? if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { @@ -160,25 +179,43 @@ export default class RoomEmoteSettings extends React.Component { emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url; } value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; + if(this.state.compatibility){ + if (client.isRoomEncrypted(this.props.roomId)) { + const compatNewEmote=await client.uploadContent(this.state.newEmoteFile[i]) + newPack["images"][this.state.newEmoteCode[i]] = {"url":compatNewEmote.content_uri}; + } else { + newPack["images"][this.state.newEmoteCode[i]] = {"url":newEmote.url}; + } + } } } if (this.state.emotes) { for (const shortcode in this.state.emotes) { if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) - && (shortcode in this.state.newEmoteCode)) { + && (shortcode in this.state.newEmoteCode)) { continue; } if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; + if (this.imagePack[shortcode]) { + newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { "url": this.imagePack[shortcode]["url"] }; + } + } else { emotesMxcs[shortcode] = this.state.emotes[shortcode]; value[shortcode] = shortcode; + if (this.imagePack["images"][shortcode]) { + newPack["images"][shortcode] = { "url": this.imagePack["images"][shortcode]["url"] } + } } } } newState.value = value; - await client.sendStateEvent(this.props.roomId, 'm.room.emotes', emotesMxcs, ""); + await client.sendStateEvent(this.props.roomId, "m.room.emotes", emotesMxcs, ""); + this.imagePack=newPack + await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); + //this.emoteUpload.current.value = ""; //this.emoteCodeUpload.current.value = ""; newState.newEmoteFileAdded = false; @@ -253,6 +290,34 @@ export default class RoomEmoteSettings extends React.Component { }); } }; + + private onCompatChange = async (allowed: boolean) => { + + const client = MatrixClientPeg.get(); + await client.sendStateEvent(this.props.roomId, "m.room.clientemote_compatibility", { isCompat: allowed }, ""); + + if (allowed) { + for (const shortcode in this.state.emotes) { + if(!this.imagePack["images"][shortcode]){ + if (client.isRoomEncrypted(this.props.roomId)) { + const blob = await decryptFile(this.state.emotes[shortcode]); + const uploadedEmote = await client.uploadContent(blob) + this.imagePack["images"][shortcode] = { "url": uploadedEmote.content_uri }; + } else { + this.imagePack["images"][shortcode] = { "url": this.state.emotes[shortcode] }; + } + } + } + + await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); + } + + this.setState({ + compatibility: allowed, + } + ) + + } private async decryptEmotes(isEnc: boolean) { const decryptede = {}; for (const shortcode in this.state.emotes) { @@ -368,6 +433,7 @@ export default class RoomEmoteSettings extends React.Component { , ); } + const isCompat = this.state.compatibility return ( { multiple /> { emoteSettingsButtons } + { + + + + } { uploadedEmotes } { existingEmotes diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 33526c36780..0ddf1748443 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -40,7 +40,7 @@ import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from "../ import { findEditableEvent } from "../../../utils/EventUtils"; import SendHistoryManager from "../../../SendHistoryManager"; import { CommandCategories } from "../../../SlashCommands"; -import ContentMessages from "../../../ContentMessages"; +import ContentMessages, { uploadFile } from "../../../ContentMessages"; import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext"; import { Action } from "../../../dispatcher/actions"; import { containsEmoji } from "../../../effects/utils"; @@ -159,6 +159,7 @@ export function attachMentions( } } + // Merges favouring the given relation export function attachRelation(content: IContent, relation?: IEventRelation): void { if (relation) { @@ -177,7 +178,10 @@ export function createMessageContent( relation: IEventRelation | undefined, permalinkCreator?: RoomPermalinkCreator, includeReplyLegacyFallback = true, + emotes?:Map, + compat?:boolean, ): IContent { + const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -188,19 +192,33 @@ export function createMessageContent( model = unescapeMessage(model); const body = textSerialize(model); - + let emoteBody; + if (compat) { + emoteBody = body.replace(/:[\w+-]+:/g, m => emotes[m] ? emotes[m] : m) + } const content: IContent = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, body: body, }; - const formattedBody = htmlSerializeIfNeeded(model, { + let formattedBody = htmlSerializeIfNeeded(model, { forceHTML: !!replyToEvent, useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { + if(compat){ + formattedBody=formattedBody.replace(/:[\w+-]+:/g, m => emotes[m] ? emotes[m] : m) + } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; } + else if (compat){ + if(body!=emoteBody){ + content.format="org.matrix.custom.html" + content.formatted_body = emoteBody + } + } + + // Build the mentions property and add it to the event content. attachMentions(sender, content, model, replyToEvent); @@ -255,8 +273,10 @@ export class SendMessageComposer extends React.Component; + private compat: boolean; + static defaultProps = { includeReplyLegacyFallback: true, }; @@ -281,6 +301,19 @@ export class SendMessageComposer extends React.Component()}) : {"images":new Map()}; + this.emotes=new Map() + + for (const shortcode in this.imagePack["images"]){ + this.emotes[":"+shortcode+":"]="" + } } public componentDidUpdate(prevProps: ISendMessageComposerProps): void { @@ -296,6 +329,8 @@ export class SendMessageComposer extends React.Component { // ignore any keypress while doing IME compositions if (this.editorRef.current?.isComposing(event)) { @@ -435,7 +470,6 @@ export class SendMessageComposer extends React.Component { const model = this.model; - if (model.isEmpty) { return; } @@ -525,6 +559,8 @@ export class SendMessageComposer extends React.Component Date: Wed, 28 Dec 2022 23:51:45 -0500 Subject: [PATCH 087/176] Update RoomEmoteSettings.tsx --- src/components/views/room_settings/RoomEmoteSettings.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 390d9bcfde4..b11d0c4f0cc 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -198,8 +198,8 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; - if (this.imagePack[shortcode]) { - newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { "url": this.imagePack[shortcode]["url"] }; + if (this.imagePack["images"][shortcode]) { + newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { "url": this.imagePack["images"][shortcode]["url"] }; } } else { @@ -456,8 +456,9 @@ export default class RoomEmoteSettings extends React.Component { Date: Thu, 29 Dec 2022 15:45:32 -0500 Subject: [PATCH 088/176] Set height on compatible emotes --- src/components/views/rooms/SendMessageComposer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0ddf1748443..0e175d9c07c 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -312,7 +312,7 @@ export class SendMessageComposer extends React.Component() for (const shortcode in this.imagePack["images"]){ - this.emotes[":"+shortcode+":"]="" + this.emotes[":"+shortcode+":"]=""+":"+shortcode+":" } } From b664b3a6c22a16ff34be95467f895c6559ab4229 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 30 Dec 2022 14:37:08 -0500 Subject: [PATCH 089/176] Update EmojiButton.tsx --- src/components/views/rooms/EmojiButton.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index b35aa2aef55..14f5090172c 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -22,14 +22,16 @@ import ContextMenu, { aboveLeftOf, MenuProps, useContextMenu } from "../../struc import EmojiPicker from "../emojipicker/EmojiPicker"; import { CollapsibleButton } from "./CollapsibleButton"; import { OverflowMenuContext } from "./MessageComposerButtons"; +import { Room } from 'matrix-js-sdk/src/models/room'; interface IEmojiButtonProps { addEmoji: (unicode: string) => boolean; menuPosition?: MenuProps; className?: string; + room?: Room; } -export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps): JSX.Element { +export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiButtonProps) { const overflowMenuCloser = useContext(OverflowMenuContext); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -42,8 +44,15 @@ export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonP }; contextMenu = ( - - + { + closeMenu(); + overflowMenuCloser?.(); + }} + managed={false} + > + ); } From 583684c0bdd11f449bf3b06ce83cdc7e04362077 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Jun 2023 01:12:28 -0400 Subject: [PATCH 090/176] Code format changes and some documentation --- res/css/views/rooms/_EventTile.pcss | 1 + res/css/views/settings/_EmoteSettings.pcss | 3 +-- src/HtmlUtils.tsx | 4 ++-- src/autocomplete/EmojiProvider.tsx | 2 +- src/components/structures/MessagePanel.tsx | 1 - 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 29b0e24ae4d..82b0897d97e 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -826,6 +826,7 @@ $left-gutter: 64px; font-size: inherit !important; } } + .mx_Emote { height: 30px; } diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index 4dee60d6fbc..73f69f6d501 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -31,7 +31,7 @@ limitations under the License. } .mx_EmoteSettings_uploadedEmoteImage { height: 30px; - width: var(--emote-image-width) *30/var(--emote-image-height); + width: var(--emote-image-width) *30/var(--emote-image-height); // Sets emote height to 30px and scales the width accordingly margin-left: 30px; align-self: center; } @@ -65,7 +65,6 @@ limitations under the License. .mx_EmoteSettings_buttons { margin-top: 10px; /* 18px is already accounted for by the

    above the buttons */ - //margin-bottom: $spacing-28; > .mx_AccessibleButton_kind_link { font-size: $font-14px; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index ff70d44116d..c3189c4df72 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -680,8 +680,8 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "mx_EventTile_bigEmoji": emojiBody, "markdown-body": isHtmlMessage && !emojiBody, }); - const tmp=strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); - if (tmp!=strippedBody) { + const tmp = strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + if (tmp !== strippedBody) { safeBody=tmp; } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index c7500cc6684..8a32c1785a4 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -130,7 +130,7 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } - this.emotes=await this.emotesPromise; + this.emotes = await this.emotesPromise; const emojisAndEmotes=[...SORTED_EMOJI]; for (const key in this.emotes) { emojisAndEmotes.push({ diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 5a329497f77..52553f155f6 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -783,7 +783,6 @@ export default class MessagePanel extends React.Component { isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId(); const callEventGrouper = this.props.callEventGroupers.get(mxEv.getContent().call_id); - // use txnId as key if available so that we don't remount during sending ret.push( Date: Thu, 8 Jun 2023 01:24:00 -0400 Subject: [PATCH 091/176] clean up --- CHANGELOG.md | 803 +++++++++++++++++++++++++++- src/matrix-react-sdk - Shortcut.lnk | Bin 1077 -> 0 bytes 2 files changed, 800 insertions(+), 3 deletions(-) delete mode 100644 src/matrix-react-sdk - Shortcut.lnk diff --git a/CHANGELOG.md b/CHANGELOG.md index 10666fc797a..18942f5e65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,803 @@ +Changes in [3.73.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.73.0) (2023-06-06) +===================================================================================================== + +## ✨ Features + * When joining room in sub-space join the parents too ([\#11011](https://github.com/matrix-org/matrix-react-sdk/pull/11011)). + * Include thread replies in message previews ([\#10631](https://github.com/matrix-org/matrix-react-sdk/pull/10631)). Fixes vector-im/element-web#23920. + * Use semantic headings in space preferences ([\#11021](https://github.com/matrix-org/matrix-react-sdk/pull/11021)). Contributed by @kerryarchibald. + * Use semantic headings in user settings - Ignored users ([\#11006](https://github.com/matrix-org/matrix-react-sdk/pull/11006)). Contributed by @kerryarchibald. + * Use semantic headings in user settings - profile ([\#10973](https://github.com/matrix-org/matrix-react-sdk/pull/10973)). Fixes vector-im/element-web#25461. Contributed by @kerryarchibald. + * Use semantic headings in user settings - account ([\#10972](https://github.com/matrix-org/matrix-react-sdk/pull/10972)). Contributed by @kerryarchibald. + * Support `Insert from iPhone or iPad` in Safari ([\#10851](https://github.com/matrix-org/matrix-react-sdk/pull/10851)). Fixes vector-im/element-web#25327. Contributed by @SuperKenVery. + * Specify supportedStages for User Interactive Auth ([\#10975](https://github.com/matrix-org/matrix-react-sdk/pull/10975)). Fixes vector-im/element-web#19605. + * Pass device id to widgets ([\#10209](https://github.com/matrix-org/matrix-react-sdk/pull/10209)). Contributed by @Fox32. + * Use semantic headings in user settings - discovery ([\#10838](https://github.com/matrix-org/matrix-react-sdk/pull/10838)). Contributed by @kerryarchibald. + * Use semantic headings in user settings - Notifications ([\#10948](https://github.com/matrix-org/matrix-react-sdk/pull/10948)). Contributed by @kerryarchibald. + * Use semantic headings in user settings - spellcheck and language ([\#10959](https://github.com/matrix-org/matrix-react-sdk/pull/10959)). Contributed by @kerryarchibald. + * Use semantic headings in user settings Appearance ([\#10827](https://github.com/matrix-org/matrix-react-sdk/pull/10827)). Contributed by @kerryarchibald. + * Use semantic heading in user settings Sidebar & Voip ([\#10782](https://github.com/matrix-org/matrix-react-sdk/pull/10782)). Contributed by @kerryarchibald. + * Use semantic headings in user settings Security ([\#10774](https://github.com/matrix-org/matrix-react-sdk/pull/10774)). Contributed by @kerryarchibald. + * Use semantic headings in user settings - integrations and account deletion ([\#10837](https://github.com/matrix-org/matrix-react-sdk/pull/10837)). Fixes vector-im/element-web#25378. Contributed by @kerryarchibald. + * Use semantic headings in user settings Preferences ([\#10794](https://github.com/matrix-org/matrix-react-sdk/pull/10794)). Contributed by @kerryarchibald. + * Use semantic headings in user settings Keyboard ([\#10793](https://github.com/matrix-org/matrix-react-sdk/pull/10793)). Contributed by @kerryarchibald. + * RTE plain text mentions as pills ([\#10852](https://github.com/matrix-org/matrix-react-sdk/pull/10852)). Contributed by @alunturner. + * Use semantic headings in user settings Labs ([\#10773](https://github.com/matrix-org/matrix-react-sdk/pull/10773)). Contributed by @kerryarchibald. + * Use semantic list elements for menu lists and tab lists ([\#10902](https://github.com/matrix-org/matrix-react-sdk/pull/10902)). Fixes vector-im/element-web#24928. + * Fix aria-required-children axe violation ([\#10900](https://github.com/matrix-org/matrix-react-sdk/pull/10900)). Fixes vector-im/element-web#25342. + * Enable pagination for overlay timelines ([\#10757](https://github.com/matrix-org/matrix-react-sdk/pull/10757)). Fixes vector-im/voip-internal#107. + * Add tooltip to disabled invite button due to lack of permissions ([\#10869](https://github.com/matrix-org/matrix-react-sdk/pull/10869)). Fixes vector-im/element-web#9824. + * Respect configured auth_header_logo_url for default Welcome page ([\#10870](https://github.com/matrix-org/matrix-react-sdk/pull/10870)). + * Specify lazy loading for avatars ([\#10866](https://github.com/matrix-org/matrix-react-sdk/pull/10866)). Fixes vector-im/element-web#1983. + * Room and user mentions for plain text editor ([\#10665](https://github.com/matrix-org/matrix-react-sdk/pull/10665)). Contributed by @alunturner. + * Add audible notifcation on broadcast error ([\#10654](https://github.com/matrix-org/matrix-react-sdk/pull/10654)). Fixes vector-im/element-web#25132. + * Fall back from server generated thumbnail to original image ([\#10853](https://github.com/matrix-org/matrix-react-sdk/pull/10853)). + * Use semantically correct elements for room sublist context menu ([\#10831](https://github.com/matrix-org/matrix-react-sdk/pull/10831)). Fixes vector-im/customer-retainer#46. + * Avoid calling prepareToEncrypt onKeyDown ([\#10828](https://github.com/matrix-org/matrix-react-sdk/pull/10828)). + * Allows search to recognize full room links ([\#8275](https://github.com/matrix-org/matrix-react-sdk/pull/8275)). Contributed by @bolu-tife. + * "Show rooms with unread messages first" should not be on by default for new users ([\#10820](https://github.com/matrix-org/matrix-react-sdk/pull/10820)). Fixes vector-im/element-web#25304. Contributed by @kerryarchibald. + * Fix emitter handler leak in ThreadView ([\#10803](https://github.com/matrix-org/matrix-react-sdk/pull/10803)). + * Add better error for email invites without identity server ([\#10739](https://github.com/matrix-org/matrix-react-sdk/pull/10739)). Fixes vector-im/element-web#16893. + * Move reaction message previews out of labs ([\#10601](https://github.com/matrix-org/matrix-react-sdk/pull/10601)). Fixes vector-im/element-web#25083. + * Sort muted rooms to the bottom of their section of the room list ([\#10592](https://github.com/matrix-org/matrix-react-sdk/pull/10592)). Fixes vector-im/element-web#25131. Contributed by @kerryarchibald. + * Use semantic headings in user settings Help & About ([\#10752](https://github.com/matrix-org/matrix-react-sdk/pull/10752)). Contributed by @kerryarchibald. + * use ExternalLink components for external links ([\#10758](https://github.com/matrix-org/matrix-react-sdk/pull/10758)). Contributed by @kerryarchibald. + * Use semantic headings in space settings ([\#10751](https://github.com/matrix-org/matrix-react-sdk/pull/10751)). Contributed by @kerryarchibald. + * Use semantic headings for room settings content ([\#10734](https://github.com/matrix-org/matrix-react-sdk/pull/10734)). Contributed by @kerryarchibald. + +## 🐛 Bug Fixes + * Use consistent fonts for Japanese text ([\#10980](https://github.com/matrix-org/matrix-react-sdk/pull/10980)). Fixes vector-im/element-web#22333 and vector-im/element-web#23899. + * Fix: server picker validates unselected option ([\#11020](https://github.com/matrix-org/matrix-react-sdk/pull/11020)). Fixes vector-im/element-web#25488. Contributed by @kerryarchibald. + * Fix room list notification badges going missing in compact layout ([\#11022](https://github.com/matrix-org/matrix-react-sdk/pull/11022)). Fixes vector-im/element-web#25372. + * Fix call to `startSingleSignOn` passing enum in place of idpId ([\#10998](https://github.com/matrix-org/matrix-react-sdk/pull/10998)). Fixes vector-im/element-web#24953. + * Remove hover effect from user name on a DM creation UI ([\#10887](https://github.com/matrix-org/matrix-react-sdk/pull/10887)). Fixes vector-im/element-web#25305. Contributed by @luixxiul. + * Fix layout regression in public space invite dialog ([\#11009](https://github.com/matrix-org/matrix-react-sdk/pull/11009)). Fixes vector-im/element-web#25458. + * Fix layout regression in session dropdown ([\#10999](https://github.com/matrix-org/matrix-react-sdk/pull/10999)). Fixes vector-im/element-web#25448. + * Fix spacing regression in user settings - roles & permissions ([\#10993](https://github.com/matrix-org/matrix-react-sdk/pull/10993)). Fixes vector-im/element-web#25447 and vector-im/element-web#25451. Contributed by @kerryarchibald. + * Fall back to receipt timestamp if we have no event (react-sdk part) ([\#10974](https://github.com/matrix-org/matrix-react-sdk/pull/10974)). Fixes vector-im/element-web#10954. Contributed by @andybalaam. + * Fix: Room header 'view your device list' does not link to new session manager ([\#10979](https://github.com/matrix-org/matrix-react-sdk/pull/10979)). Fixes vector-im/element-web#25440. Contributed by @kerryarchibald. + * Fix display of devices without encryption support in Settings dialog ([\#10977](https://github.com/matrix-org/matrix-react-sdk/pull/10977)). Fixes vector-im/element-web#25413. + * Use aria descriptions instead of labels for TextWithTooltip ([\#10952](https://github.com/matrix-org/matrix-react-sdk/pull/10952)). Fixes vector-im/element-web#25398. + * Use grapheme-splitter instead of lodash for saving emoji from being ripped apart ([\#10976](https://github.com/matrix-org/matrix-react-sdk/pull/10976)). Fixes vector-im/element-web#22196. + * Fix: content overflow in settings subsection ([\#10960](https://github.com/matrix-org/matrix-react-sdk/pull/10960)). Fixes vector-im/element-web#25416. Contributed by @kerryarchibald. + * Make `Privacy Notice` external link on integration manager ToS clickable ([\#10914](https://github.com/matrix-org/matrix-react-sdk/pull/10914)). Fixes vector-im/element-web#25384. Contributed by @luixxiul. + * Ensure that open message context menus are updated when the event is sent ([\#10950](https://github.com/matrix-org/matrix-react-sdk/pull/10950)). + * Ensure that open sticker picker dialogs are updated when the widget configuration is updated. ([\#10945](https://github.com/matrix-org/matrix-react-sdk/pull/10945)). + * Fix big emoji in replies ([\#10932](https://github.com/matrix-org/matrix-react-sdk/pull/10932)). Fixes vector-im/element-web#24798. + * Hide empty `MessageActionBar` on message edit history dialog ([\#10447](https://github.com/matrix-org/matrix-react-sdk/pull/10447)). Fixes vector-im/element-web#24903. Contributed by @luixxiul. + * Fix roving tab index getting confused after dragging space order ([\#10901](https://github.com/matrix-org/matrix-react-sdk/pull/10901)). + * Ignore edits in message previews when they concern messages other than latest ([\#10868](https://github.com/matrix-org/matrix-react-sdk/pull/10868)). Fixes vector-im/element-web#14872. + * Send correct receipts when viewing a room ([\#10864](https://github.com/matrix-org/matrix-react-sdk/pull/10864)). Fixes vector-im/element-web#25196. + * Fix timeline search bar being overlapped by the right panel ([\#10809](https://github.com/matrix-org/matrix-react-sdk/pull/10809)). Fixes vector-im/element-web#25291. Contributed by @luixxiul. + * Fix the state shown for call in rooms ([\#10833](https://github.com/matrix-org/matrix-react-sdk/pull/10833)). + * Add string for membership event where both displayname & avatar change ([\#10880](https://github.com/matrix-org/matrix-react-sdk/pull/10880)). Fixes vector-im/element-web#18026. + * Fix people space notification badge not updating for new DM invites ([\#10849](https://github.com/matrix-org/matrix-react-sdk/pull/10849)). Fixes vector-im/element-web#23248. + * Fix regression in emoji picker order mangling after clearing filter ([\#10854](https://github.com/matrix-org/matrix-react-sdk/pull/10854)). Fixes vector-im/element-web#25323. + * Fix: Edit history modal crash ([\#10834](https://github.com/matrix-org/matrix-react-sdk/pull/10834)). Fixes vector-im/element-web#25309. Contributed by @kerryarchibald. + * Fix long room address and name not being clipped on room info card and update `_RoomSummaryCard.pcss` ([\#10811](https://github.com/matrix-org/matrix-react-sdk/pull/10811)). Fixes vector-im/element-web#25293. Contributed by @luixxiul. + * Treat thumbnail upload failures as complete upload failures ([\#10829](https://github.com/matrix-org/matrix-react-sdk/pull/10829)). Fixes vector-im/element-web#7069. + * Update finite automata to match user identifiers as per spec ([\#10798](https://github.com/matrix-org/matrix-react-sdk/pull/10798)). Fixes vector-im/element-web#25246. + * Fix icon on empty notification panel ([\#10817](https://github.com/matrix-org/matrix-react-sdk/pull/10817)). Fixes vector-im/element-web#25298 and vector-im/element-web#25302. Contributed by @luixxiul. + * Fix: Threads button is highlighted when I create a new room ([\#10819](https://github.com/matrix-org/matrix-react-sdk/pull/10819)). Fixes vector-im/element-web#25284. Contributed by @kerryarchibald. + * Fix the top heading of notification panel ([\#10818](https://github.com/matrix-org/matrix-react-sdk/pull/10818)). Fixes vector-im/element-web#25303. Contributed by @luixxiul. + * Fix the color of the verified E2EE icon on `RoomSummaryCard` ([\#10812](https://github.com/matrix-org/matrix-react-sdk/pull/10812)). Fixes vector-im/element-web#25295. Contributed by @luixxiul. + * Fix: No feedback when waiting for the server on a /delete_devices request with SSO ([\#10795](https://github.com/matrix-org/matrix-react-sdk/pull/10795)). Fixes vector-im/element-web#23096. Contributed by @kerryarchibald. + * Fix: reveal images when image previews are disabled ([\#10781](https://github.com/matrix-org/matrix-react-sdk/pull/10781)). Fixes vector-im/element-web#25271. Contributed by @kerryarchibald. + * Fix accessibility issues around the room list and space panel ([\#10717](https://github.com/matrix-org/matrix-react-sdk/pull/10717)). Fixes vector-im/element-web#13345. + * Ensure tooltip contents is linked via aria to the target element ([\#10729](https://github.com/matrix-org/matrix-react-sdk/pull/10729)). Fixes vector-im/customer-retainer#43. + +Changes in [3.72.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.72.0) (2023-05-10) +===================================================================================================== + +## ✨ Features + * Add UIFeature.locationSharing to hide location sharing ([\#10727](https://github.com/matrix-org/matrix-react-sdk/pull/10727)). + * Memoize field validation results ([\#10714](https://github.com/matrix-org/matrix-react-sdk/pull/10714)). + * Commands for plain text editor ([\#10567](https://github.com/matrix-org/matrix-react-sdk/pull/10567)). Contributed by @alunturner. + * Allow 16 lines of text in the rich text editors ([\#10670](https://github.com/matrix-org/matrix-react-sdk/pull/10670)). Contributed by @alunturner. + * Bail out of `RoomSettingsDialog` when room is not found ([\#10662](https://github.com/matrix-org/matrix-react-sdk/pull/10662)). Contributed by @kerryarchibald. + * Element-R: Populate device list for right-panel ([\#10671](https://github.com/matrix-org/matrix-react-sdk/pull/10671)). Contributed by @florianduros. + * Make existing and new issue URLs configurable ([\#10710](https://github.com/matrix-org/matrix-react-sdk/pull/10710)). Fixes vector-im/element-web#24424. + * Fix usages of ARIA tabpanel ([\#10628](https://github.com/matrix-org/matrix-react-sdk/pull/10628)). Fixes vector-im/element-web#25016. + * Element-R: Starting a DMs with a user ([\#10673](https://github.com/matrix-org/matrix-react-sdk/pull/10673)). Contributed by @florianduros. + * ARIA Accessibility improvements ([\#10675](https://github.com/matrix-org/matrix-react-sdk/pull/10675)). + * ARIA Accessibility improvements ([\#10674](https://github.com/matrix-org/matrix-react-sdk/pull/10674)). + * Add arrow key controls to emoji and reaction pickers ([\#10637](https://github.com/matrix-org/matrix-react-sdk/pull/10637)). Fixes vector-im/element-web#17189. + * Translate credits in help about section ([\#10676](https://github.com/matrix-org/matrix-react-sdk/pull/10676)). + +## 🐛 Bug Fixes + * Fix: reveal images when image previews are disabled ([\#10781](https://github.com/matrix-org/matrix-react-sdk/pull/10781)). Fixes vector-im/element-web#25271. Contributed by @kerryarchibald. + * Fix autocomplete not resetting properly on message send ([\#10741](https://github.com/matrix-org/matrix-react-sdk/pull/10741)). Fixes vector-im/element-web#25170. + * Fix start_sso not working with guests disabled ([\#10720](https://github.com/matrix-org/matrix-react-sdk/pull/10720)). Fixes vector-im/element-web#16624. + * Fix soft crash with Element call widgets ([\#10684](https://github.com/matrix-org/matrix-react-sdk/pull/10684)). + * Send correct receipt when marking a room as read ([\#10730](https://github.com/matrix-org/matrix-react-sdk/pull/10730)). Fixes vector-im/element-web#25207. + * Offload some more waveform processing onto a worker ([\#9223](https://github.com/matrix-org/matrix-react-sdk/pull/9223)). Fixes vector-im/element-web#19756. + * Consolidate login errors ([\#10722](https://github.com/matrix-org/matrix-react-sdk/pull/10722)). Fixes vector-im/element-web#17520. + * Fix all rooms search generating permalinks to wrong room id ([\#10625](https://github.com/matrix-org/matrix-react-sdk/pull/10625)). Fixes vector-im/element-web#25115. + * Posthog properly handle Analytics ID changing from under us ([\#10702](https://github.com/matrix-org/matrix-react-sdk/pull/10702)). Fixes vector-im/element-web#25187. + * Fix Clock being read as an absolute time rather than duration ([\#10706](https://github.com/matrix-org/matrix-react-sdk/pull/10706)). Fixes vector-im/element-web#22582. + * Properly translate errors in `ChangePassword.tsx` so they show up translated to the user but not in our logs ([\#10615](https://github.com/matrix-org/matrix-react-sdk/pull/10615)). Fixes vector-im/element-web#9597. Contributed by @MadLittleMods. + * Honour feature toggles in guest mode ([\#10651](https://github.com/matrix-org/matrix-react-sdk/pull/10651)). Fixes vector-im/element-web#24513. Contributed by @andybalaam. + * Fix default content in devtools event sender ([\#10699](https://github.com/matrix-org/matrix-react-sdk/pull/10699)). Contributed by @tulir. + * Fix a crash when a call ends while you're in it ([\#10681](https://github.com/matrix-org/matrix-react-sdk/pull/10681)). Fixes vector-im/element-web#25153. + * Fix lack of screen reader indication when triggering auto complete ([\#10664](https://github.com/matrix-org/matrix-react-sdk/pull/10664)). Fixes vector-im/element-web#11011. + * Fix typing tile duplicating users ([\#10678](https://github.com/matrix-org/matrix-react-sdk/pull/10678)). Fixes vector-im/element-web#25165. + * Fix wrong room topic tooltip position ([\#10667](https://github.com/matrix-org/matrix-react-sdk/pull/10667)). Fixes vector-im/element-web#25158. + * Fix create subspace dialog not working ([\#10652](https://github.com/matrix-org/matrix-react-sdk/pull/10652)). Fixes vector-im/element-web#24882. + +Changes in [3.71.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.71.1) (2023-04-25) +===================================================================================================== + +## 🐛 Bug Fixes + * Pick up matrix-js-sdk v25.0.0 (missed because of npmjs partial outage) + +Changes in [3.71.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.71.0) (2023-04-25) +===================================================================================================== + +## 🔒 Security + * Fixes for [CVE-2023-30609](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-30609) / GHSA-xv83-x443-7rmw + +## ✨ Features + * Pick sensible default option for phone country dropdown ([\#10627](https://github.com/matrix-org/matrix-react-sdk/pull/10627)). Fixes vector-im/element-web#3528. + * Relate field validation tooltip via aria-describedby ([\#10522](https://github.com/matrix-org/matrix-react-sdk/pull/10522)). Fixes vector-im/element-web#24963. + * Handle more completion types in rte autocomplete ([\#10560](https://github.com/matrix-org/matrix-react-sdk/pull/10560)). Contributed by @alunturner. + * Show a tile for an unloaded predecessor room if it has via_servers ([\#10483](https://github.com/matrix-org/matrix-react-sdk/pull/10483)). Contributed by @andybalaam. + * Exclude message timestamps from aria live region ([\#10584](https://github.com/matrix-org/matrix-react-sdk/pull/10584)). Fixes vector-im/element-web#5696. + * Make composer format bar an aria toolbar ([\#10583](https://github.com/matrix-org/matrix-react-sdk/pull/10583)). Fixes vector-im/element-web#11283. + * Improve accessibility of font slider ([\#10473](https://github.com/matrix-org/matrix-react-sdk/pull/10473)). Fixes vector-im/element-web#20168 and vector-im/element-web#24962. + * fix file size display from kB to KB ([\#10561](https://github.com/matrix-org/matrix-react-sdk/pull/10561)). Fixes vector-im/element-web#24866. Contributed by @NSV1991. + * Handle /me in rte ([\#10558](https://github.com/matrix-org/matrix-react-sdk/pull/10558)). Contributed by @alunturner. + * bind html with switch for manage extension setting option ([\#10553](https://github.com/matrix-org/matrix-react-sdk/pull/10553)). Contributed by @NSV1991. + * Handle command completions in RTE ([\#10521](https://github.com/matrix-org/matrix-react-sdk/pull/10521)). Contributed by @alunturner. + * Add room and user avatars to rte ([\#10497](https://github.com/matrix-org/matrix-react-sdk/pull/10497)). Contributed by @alunturner. + * Support for MSC3882 revision 1 ([\#10443](https://github.com/matrix-org/matrix-react-sdk/pull/10443)). Contributed by @hughns. + * Check profiles before starting a DM ([\#10472](https://github.com/matrix-org/matrix-react-sdk/pull/10472)). Fixes vector-im/element-web#24830. + * Quick settings: Change the copy / labels on the options ([\#10427](https://github.com/matrix-org/matrix-react-sdk/pull/10427)). Fixes vector-im/element-web#24522. Contributed by @justjanne. + * Update rte autocomplete styling ([\#10503](https://github.com/matrix-org/matrix-react-sdk/pull/10503)). Contributed by @alunturner. + +## 🐛 Bug Fixes + * Fix create subspace dialog not working ([\#10652](https://github.com/matrix-org/matrix-react-sdk/pull/10652)). Fixes vector-im/element-web#24882 + * Fix multiple accessibility defects identified by AXE ([\#10606](https://github.com/matrix-org/matrix-react-sdk/pull/10606)). + * Fix view source from edit history dialog always showing latest event ([\#10626](https://github.com/matrix-org/matrix-react-sdk/pull/10626)). Fixes vector-im/element-web#21859. + * #21451 Fix WebGL disabled error message ([\#10589](https://github.com/matrix-org/matrix-react-sdk/pull/10589)). Contributed by @rashmitpankhania. + * Properly translate errors in `AddThreepid.ts` so they show up translated to the user but not in our logs ([\#10432](https://github.com/matrix-org/matrix-react-sdk/pull/10432)). Contributed by @MadLittleMods. + * Fix overflow on auth pages ([\#10605](https://github.com/matrix-org/matrix-react-sdk/pull/10605)). Fixes vector-im/element-web#19548. + * Fix incorrect avatar background colour when using a custom theme ([\#10598](https://github.com/matrix-org/matrix-react-sdk/pull/10598)). Contributed by @jdauphant. + * Remove dependency on `org.matrix.e2e_cross_signing` unstable feature ([\#10593](https://github.com/matrix-org/matrix-react-sdk/pull/10593)). + * Update setting description to match reality ([\#10600](https://github.com/matrix-org/matrix-react-sdk/pull/10600)). Fixes vector-im/element-web#25106. + * Fix no identity server in help & about settings ([\#10563](https://github.com/matrix-org/matrix-react-sdk/pull/10563)). Fixes vector-im/element-web#25077. + * Fix: Images no longer reserve their space in the timeline correctly ([\#10571](https://github.com/matrix-org/matrix-react-sdk/pull/10571)). Fixes vector-im/element-web#25082. Contributed by @kerryarchibald. + * Fix issues with inhibited accessible focus outlines ([\#10579](https://github.com/matrix-org/matrix-react-sdk/pull/10579)). Fixes vector-im/element-web#19742. + * Fix read receipts falling from sky ([\#10576](https://github.com/matrix-org/matrix-react-sdk/pull/10576)). Fixes vector-im/element-web#25081. + * Fix avatar text issue in rte ([\#10559](https://github.com/matrix-org/matrix-react-sdk/pull/10559)). Contributed by @alunturner. + * fix resizer only work with left mouse click ([\#10546](https://github.com/matrix-org/matrix-react-sdk/pull/10546)). Contributed by @NSV1991. + * Fix send two join requests when joining a room from spotlight search ([\#10534](https://github.com/matrix-org/matrix-react-sdk/pull/10534)). Fixes vector-im/element-web#25054. + * Highlight event when any version triggered a highlight ([\#10502](https://github.com/matrix-org/matrix-react-sdk/pull/10502)). Fixes vector-im/element-web#24923 and vector-im/element-web#24970. Contributed by @kerryarchibald. + * Fix spacing of headings of integration manager on General settings tab ([\#10232](https://github.com/matrix-org/matrix-react-sdk/pull/10232)). Fixes vector-im/element-web#24085. Contributed by @luixxiul. + +Changes in [3.70.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.70.0) (2023-04-11) +===================================================================================================== + +## ✨ Features + * Style mentions as pills in rich text editor ([\#10448](https://github.com/matrix-org/matrix-react-sdk/pull/10448)). Contributed by @alunturner. + * Show room create icon if "UIComponent.roomCreation" is enabled ([\#10364](https://github.com/matrix-org/matrix-react-sdk/pull/10364)). Contributed by @maheichyk. + * Mentions as links rte ([\#10463](https://github.com/matrix-org/matrix-react-sdk/pull/10463)). Contributed by @alunturner. + * Better error handling in jump to date ([\#10405](https://github.com/matrix-org/matrix-react-sdk/pull/10405)). Contributed by @MadLittleMods. + * Show "Invite" menu option if "UIComponent.sendInvites" is enabled. ([\#10363](https://github.com/matrix-org/matrix-react-sdk/pull/10363)). Contributed by @maheichyk. + * Added `UserProfilesStore`, `LruCache` and user permalink profile caching ([\#10425](https://github.com/matrix-org/matrix-react-sdk/pull/10425)). Fixes vector-im/element-web#10559. + * Mentions as links rte ([\#10422](https://github.com/matrix-org/matrix-react-sdk/pull/10422)). Contributed by @alunturner. + * Implement MSC3952: intentional mentions ([\#9983](https://github.com/matrix-org/matrix-react-sdk/pull/9983)). + * Implement MSC3973: Search users in the user directory with the Widget API ([\#10269](https://github.com/matrix-org/matrix-react-sdk/pull/10269)). Contributed by @dhenneke. + * Permalinks to message are now displayed as pills ([\#10392](https://github.com/matrix-org/matrix-react-sdk/pull/10392)). Fixes vector-im/element-web#24751 and vector-im/element-web#24706. + * Show search,dial,explore in filterContainer if "UIComponent.filterContainer" is enabled ([\#10381](https://github.com/matrix-org/matrix-react-sdk/pull/10381)). Contributed by @maheichyk. + * Increase space panel collapse clickable area ([\#6084](https://github.com/matrix-org/matrix-react-sdk/pull/6084)). Fixes vector-im/element-web#17379. Contributed by @jaiwanth-v. + * Add fallback for replies to Polls ([\#10380](https://github.com/matrix-org/matrix-react-sdk/pull/10380)). Fixes vector-im/element-web#24197. Contributed by @kerryarchibald. + * Permalinks to rooms and users are now pillified ([\#10388](https://github.com/matrix-org/matrix-react-sdk/pull/10388)). Fixes vector-im/element-web#24825. + * Poll history - access poll history from room settings ([\#10356](https://github.com/matrix-org/matrix-react-sdk/pull/10356)). Contributed by @kerryarchibald. + * Add API params to mute audio and/or video in Jitsi calls by default ([\#10376](https://github.com/matrix-org/matrix-react-sdk/pull/10376)). Contributed by @dhenneke. + * Notifications: inline error message on notifications saving error ([\#10288](https://github.com/matrix-org/matrix-react-sdk/pull/10288)). Contributed by @kerryarchibald. + * Support dynamic room predecessor in SpaceProvider ([\#10348](https://github.com/matrix-org/matrix-react-sdk/pull/10348)). Contributed by @andybalaam. + * Support dynamic room predecessors for RoomProvider ([\#10346](https://github.com/matrix-org/matrix-react-sdk/pull/10346)). Contributed by @andybalaam. + * Support dynamic room predecessors in OwnBeaconStore ([\#10339](https://github.com/matrix-org/matrix-react-sdk/pull/10339)). Contributed by @andybalaam. + * Support dynamic room predecessors in ForwardDialog ([\#10344](https://github.com/matrix-org/matrix-react-sdk/pull/10344)). Contributed by @andybalaam. + * Support dynamic room predecessors in SpaceHierarchy ([\#10341](https://github.com/matrix-org/matrix-react-sdk/pull/10341)). Contributed by @andybalaam. + * Support dynamic room predecessors in AddExistingToSpaceDialog ([\#10342](https://github.com/matrix-org/matrix-react-sdk/pull/10342)). Contributed by @andybalaam. + * Support dynamic room predecessors in leave-behaviour ([\#10340](https://github.com/matrix-org/matrix-react-sdk/pull/10340)). Contributed by @andybalaam. + * Support dynamic room predecessors in StopGapWidgetDriver ([\#10338](https://github.com/matrix-org/matrix-react-sdk/pull/10338)). Contributed by @andybalaam. + * Support dynamic room predecessors in WidgetLayoutStore ([\#10326](https://github.com/matrix-org/matrix-react-sdk/pull/10326)). Contributed by @andybalaam. + * Support dynamic room predecessors in SpaceStore ([\#10332](https://github.com/matrix-org/matrix-react-sdk/pull/10332)). Contributed by @andybalaam. + * Sync polls push rules on changes to account_data ([\#10287](https://github.com/matrix-org/matrix-react-sdk/pull/10287)). Contributed by @kerryarchibald. + * Support dynamic room predecessors in BreadcrumbsStore ([\#10295](https://github.com/matrix-org/matrix-react-sdk/pull/10295)). Contributed by @andybalaam. + * Improved a11y for Field feedback and Secure Phrase input ([\#10320](https://github.com/matrix-org/matrix-react-sdk/pull/10320)). Contributed by @Sebbones. + * Support dynamic room predecessors in RoomNotificationStateStore ([\#10297](https://github.com/matrix-org/matrix-react-sdk/pull/10297)). Contributed by @andybalaam. + +## 🐛 Bug Fixes + * Allow editing with RTE to overflow for autocomplete visibility ([\#10499](https://github.com/matrix-org/matrix-react-sdk/pull/10499)). Contributed by @alunturner. + * Added auto focus to Github URL on opening of debug logs modal ([\#10479](https://github.com/matrix-org/matrix-react-sdk/pull/10479)). Contributed by @ShivamSpm. + * Fix detection of encryption for all users in a room ([\#10487](https://github.com/matrix-org/matrix-react-sdk/pull/10487)). Fixes vector-im/element-web#24995. + * Properly generate mentions when editing a reply with MSC3952 ([\#10486](https://github.com/matrix-org/matrix-react-sdk/pull/10486)). Fixes vector-im/element-web#24924. Contributed by @kerryarchibald. + * Improve performance of rendering a room with many hidden events ([\#10131](https://github.com/matrix-org/matrix-react-sdk/pull/10131)). Contributed by @andybalaam. + * Prevent future date selection in jump to date ([\#10419](https://github.com/matrix-org/matrix-react-sdk/pull/10419)). Fixes vector-im/element-web#20800. Contributed by @MadLittleMods. + * Add aria labels to message search bar to improve accessibility ([\#10476](https://github.com/matrix-org/matrix-react-sdk/pull/10476)). Fixes vector-im/element-web#24921. + * Fix decryption failure bar covering the timeline ([\#10360](https://github.com/matrix-org/matrix-react-sdk/pull/10360)). Fixes vector-im/element-web#24780 vector-im/element-web#24074 and vector-im/element-web#24183. Contributed by @luixxiul. + * Improve profile picture settings accessibility ([\#10470](https://github.com/matrix-org/matrix-react-sdk/pull/10470)). Fixes vector-im/element-web#24919. + * Handle group call redaction ([\#10465](https://github.com/matrix-org/matrix-react-sdk/pull/10465)). + * Display relative timestamp for threads on the same calendar day ([\#10399](https://github.com/matrix-org/matrix-react-sdk/pull/10399)). Fixes vector-im/element-web#24841. Contributed by @kerryarchibald. + * Fix timeline list and paragraph display issues ([\#10424](https://github.com/matrix-org/matrix-react-sdk/pull/10424)). Fixes vector-im/element-web#24602. Contributed by @alunturner. + * Use unique keys for voice broadcast pips ([\#10457](https://github.com/matrix-org/matrix-react-sdk/pull/10457)). Fixes vector-im/element-web#24959. + * Fix "show read receipts sent by other users" not applying to threads ([\#10445](https://github.com/matrix-org/matrix-react-sdk/pull/10445)). Fixes vector-im/element-web#24910. + * Fix joining public rooms without aliases in search dialog ([\#10437](https://github.com/matrix-org/matrix-react-sdk/pull/10437)). Fixes vector-im/element-web#23937. + * Add input validation for `m.direct` in `DMRoomMap` ([\#10436](https://github.com/matrix-org/matrix-react-sdk/pull/10436)). Fixes vector-im/element-web#24909. + * Reduce height reserved for "collapse" button's line on IRC layout ([\#10211](https://github.com/matrix-org/matrix-react-sdk/pull/10211)). Fixes vector-im/element-web#24605. Contributed by @luixxiul. + * Fix `creatorUserId is required` error when opening sticker picker ([\#10423](https://github.com/matrix-org/matrix-react-sdk/pull/10423)). + * Fix block/inline Element descendants error noise in `NewRoomIntro.tsx` ([\#10412](https://github.com/matrix-org/matrix-react-sdk/pull/10412)). Contributed by @MadLittleMods. + * Fix profile resizer to make first character of a line selectable in IRC layout ([\#10396](https://github.com/matrix-org/matrix-react-sdk/pull/10396)). Fixes vector-im/element-web#14764. Contributed by @luixxiul. + * Ensure space between wrapped lines of room name on IRC layout ([\#10188](https://github.com/matrix-org/matrix-react-sdk/pull/10188)). Fixes vector-im/element-web#24742. Contributed by @luixxiul. + * Remove unreadable alt attribute from the room status bar warning icon (nonsense to screenreaders) ([\#10402](https://github.com/matrix-org/matrix-react-sdk/pull/10402)). Contributed by @MadLittleMods. + * Fix big date separators when jump to date is enabled ([\#10404](https://github.com/matrix-org/matrix-react-sdk/pull/10404)). Fixes vector-im/element-web#22969. Contributed by @MadLittleMods. + * Fixes user authentication when registering via the module API ([\#10257](https://github.com/matrix-org/matrix-react-sdk/pull/10257)). Contributed by @maheichyk. + * Handle more edge cases in Space Hierarchy ([\#10280](https://github.com/matrix-org/matrix-react-sdk/pull/10280)). Contributed by @justjanne. + * Further improve performance with lots of hidden events ([\#10353](https://github.com/matrix-org/matrix-react-sdk/pull/10353)). Fixes vector-im/element-web#24480. Contributed by @andybalaam. + * Respect user cancelling upload flow by dismissing spinner ([\#10373](https://github.com/matrix-org/matrix-react-sdk/pull/10373)). Fixes vector-im/element-web#24667. + * When starting a DM, the end-to-end encryption status icon does now only appear if the DM can be encrypted ([\#10394](https://github.com/matrix-org/matrix-react-sdk/pull/10394)). Fixes vector-im/element-web#24397. + * Fix `[object Object]` in feedback metadata ([\#10390](https://github.com/matrix-org/matrix-react-sdk/pull/10390)). + * Fix pinned messages card saying nothing pinned while loading ([\#10385](https://github.com/matrix-org/matrix-react-sdk/pull/10385)). Fixes vector-im/element-web#24615. + * Fix import e2e key dialog staying disabled after paste ([\#10375](https://github.com/matrix-org/matrix-react-sdk/pull/10375)). Fixes vector-im/element-web#24818. + * Show all labs even if incompatible, with appropriate tooltip explaining requirements ([\#10369](https://github.com/matrix-org/matrix-react-sdk/pull/10369)). Fixes vector-im/element-web#24813. + * Fix UIFeature.Registration not applying to all paths ([\#10371](https://github.com/matrix-org/matrix-react-sdk/pull/10371)). Fixes vector-im/element-web#24814. + * Clicking on a user pill does now only open the profile in the right panel and no longer navigates to the home view. ([\#10359](https://github.com/matrix-org/matrix-react-sdk/pull/10359)). Fixes vector-im/element-web#24797. + * Fix start DM with pending third party invite ([\#10347](https://github.com/matrix-org/matrix-react-sdk/pull/10347)). Fixes vector-im/element-web#24781. + * Fix long display name overflowing reply tile on IRC layout ([\#10343](https://github.com/matrix-org/matrix-react-sdk/pull/10343)). Fixes vector-im/element-web#24738. Contributed by @luixxiul. + * Display redacted body on ThreadView in the same way as normal messages ([\#9016](https://github.com/matrix-org/matrix-react-sdk/pull/9016)). Fixes vector-im/element-web#24729. Contributed by @luixxiul. + * Handle more edge cases in ACL updates ([\#10279](https://github.com/matrix-org/matrix-react-sdk/pull/10279)). Contributed by @justjanne. + * Allow parsing png files to fail if thumbnailing is successful ([\#10308](https://github.com/matrix-org/matrix-react-sdk/pull/10308)). + +Changes in [3.69.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.69.1) (2023-03-31) +===================================================================================================== + +## 🐛 Bug Fixes + * Fix detection of encryption for all users in a room ([\#10487](https://github.com/matrix-org/matrix-react-sdk/pull/10487)). Fixes vector-im/element-web#24995. + +Changes in [3.69.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.69.0) (2023-03-28) +===================================================================================================== + +## 🔒 Security + * Fixes for [CVE-2023-28427](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28427) / GHSA-mwq8-fjpf-c2gr + * Fixes for [CVE-2023-28103](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2023-28103) / GHSA-6g43-88cp-w5gv + +Changes in [3.68.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.68.0) (2023-03-15) +===================================================================================================== + +## ✨ Features + * Only allow to start a DM with one email if encryption by default is enabled ([\#10253](https://github.com/matrix-org/matrix-react-sdk/pull/10253)). Fixes vector-im/element-web#23133. + * DM rooms are now encrypted if encryption by default is enabled and only inviting a single email address. Any action in the result DM room will be blocked until the other has joined. ([\#10229](https://github.com/matrix-org/matrix-react-sdk/pull/10229)). + * Reduce bottom margin of ReplyChain on compact modern layout ([\#8972](https://github.com/matrix-org/matrix-react-sdk/pull/8972)). Fixes vector-im/element-web#22748. Contributed by @luixxiul. + * Support for v2 of MSC3903 ([\#10165](https://github.com/matrix-org/matrix-react-sdk/pull/10165)). Contributed by @hughns. + * When starting a DM, existing rooms with pending third-party invites will be reused. ([\#10256](https://github.com/matrix-org/matrix-react-sdk/pull/10256)). Fixes vector-im/element-web#23139. + * Polls push rules: synchronise poll rules with message rules ([\#10263](https://github.com/matrix-org/matrix-react-sdk/pull/10263)). Contributed by @kerryarchibald. + * New verification request toast button labels ([\#10259](https://github.com/matrix-org/matrix-react-sdk/pull/10259)). + * Remove padding around integration manager iframe ([\#10148](https://github.com/matrix-org/matrix-react-sdk/pull/10148)). + * Fix block code styling in rich text editor ([\#10246](https://github.com/matrix-org/matrix-react-sdk/pull/10246)). Contributed by @alunturner. + * Poll history: fetch more poll history ([\#10235](https://github.com/matrix-org/matrix-react-sdk/pull/10235)). Contributed by @kerryarchibald. + * Sort short/exact emoji matches before longer incomplete matches ([\#10212](https://github.com/matrix-org/matrix-react-sdk/pull/10212)). Fixes vector-im/element-web#23210. Contributed by @grimhilt. + * Poll history: detail screen ([\#10172](https://github.com/matrix-org/matrix-react-sdk/pull/10172)). Contributed by @kerryarchibald. + * Provide a more detailed error message than "No known servers" ([\#6048](https://github.com/matrix-org/matrix-react-sdk/pull/6048)). Fixes vector-im/element-web#13247. Contributed by @aaronraimist. + * Say when a call was answered from a different device ([\#10224](https://github.com/matrix-org/matrix-react-sdk/pull/10224)). + * Widget permissions customizations using module api ([\#10121](https://github.com/matrix-org/matrix-react-sdk/pull/10121)). Contributed by @maheichyk. + * Fix copy button icon overlapping with copyable text ([\#10227](https://github.com/matrix-org/matrix-react-sdk/pull/10227)). Contributed by @Adesh-Pandey. + * Support joining non-peekable rooms via the module API ([\#10154](https://github.com/matrix-org/matrix-react-sdk/pull/10154)). Contributed by @maheichyk. + * The "new login" toast does now display the same device information as in the settings. "No" does now open the device settings. "Yes, it was me" dismisses the toast. ([\#10200](https://github.com/matrix-org/matrix-react-sdk/pull/10200)). + * Do not prompt for a password when doing a „reset all“ after login ([\#10208](https://github.com/matrix-org/matrix-react-sdk/pull/10208)). + +## 🐛 Bug Fixes + * Fix incorrect copy in space creation flow ([\#10296](https://github.com/matrix-org/matrix-react-sdk/pull/10296)). Fixes vector-im/element-web#24741. + * Fix space settings dialog having rogue title tooltip ([\#10293](https://github.com/matrix-org/matrix-react-sdk/pull/10293)). Fixes vector-im/element-web#24740. + * Show spinner when starting a DM from the user profile (right panel) ([\#10290](https://github.com/matrix-org/matrix-react-sdk/pull/10290)). + * Reduce height of toggle on expanded view source event ([\#10283](https://github.com/matrix-org/matrix-react-sdk/pull/10283)). Fixes vector-im/element-web#22873. Contributed by @luixxiul. + * Pillify http and non-prefixed matrix.to links ([\#10277](https://github.com/matrix-org/matrix-react-sdk/pull/10277)). Fixes vector-im/element-web#20844. + * Fix some features not being configurable via `features` ([\#10276](https://github.com/matrix-org/matrix-react-sdk/pull/10276)). + * Fix starting a DM from the right panel in some cases ([\#10278](https://github.com/matrix-org/matrix-react-sdk/pull/10278)). Fixes vector-im/element-web#24722. + * Align info EventTile and normal EventTile on IRC layout ([\#10197](https://github.com/matrix-org/matrix-react-sdk/pull/10197)). Fixes vector-im/element-web#22782. Contributed by @luixxiul. + * Fix blowout of waveform of the voice message player on narrow UI ([\#8861](https://github.com/matrix-org/matrix-react-sdk/pull/8861)). Fixes vector-im/element-web#22604. Contributed by @luixxiul. + * Fix the hidden view source toggle on IRC layout ([\#10266](https://github.com/matrix-org/matrix-react-sdk/pull/10266)). Fixes vector-im/element-web#22872. Contributed by @luixxiul. + * Fix buttons on the room header being compressed due to long room name ([\#10155](https://github.com/matrix-org/matrix-react-sdk/pull/10155)). Contributed by @luixxiul. + * Use the room avatar as a placeholder in calls ([\#10231](https://github.com/matrix-org/matrix-react-sdk/pull/10231)). + * Fix calls showing as 'connecting' after hangup ([\#10223](https://github.com/matrix-org/matrix-react-sdk/pull/10223)). + * Prevent multiple Jitsi calls started at the same time ([\#10183](https://github.com/matrix-org/matrix-react-sdk/pull/10183)). Fixes vector-im/element-web#23009. + * Make localization keys compatible with agglutinative and/or SOV type languages ([\#10159](https://github.com/matrix-org/matrix-react-sdk/pull/10159)). Contributed by @luixxiul. + +Changes in [3.67.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.67.0) (2023-02-28) +===================================================================================================== + +## ✨ Features + * Display "The sender has blocked you from receiving this message" error message instead of "Unable to decrypt message" ([\#10202](https://github.com/matrix-org/matrix-react-sdk/pull/10202)). Contributed by @florianduros. + * Polls: show warning about undecryptable relations ([\#10179](https://github.com/matrix-org/matrix-react-sdk/pull/10179)). Contributed by @kerryarchibald. + * Poll history: fetch last 30 days of polls ([\#10157](https://github.com/matrix-org/matrix-react-sdk/pull/10157)). Contributed by @kerryarchibald. + * Poll history - ended polls list items ([\#10119](https://github.com/matrix-org/matrix-react-sdk/pull/10119)). Contributed by @kerryarchibald. + * Remove threads labs flag and the ability to disable threads ([\#9878](https://github.com/matrix-org/matrix-react-sdk/pull/9878)). Fixes vector-im/element-web#24365. + * Show a success dialog after setting up the key backup ([\#10177](https://github.com/matrix-org/matrix-react-sdk/pull/10177)). Fixes vector-im/element-web#24487. + * Release Sign in with QR out of labs ([\#10066](https://github.com/matrix-org/matrix-react-sdk/pull/10066)). Contributed by @hughns. + * Hide indent button in rte ([\#10149](https://github.com/matrix-org/matrix-react-sdk/pull/10149)). Contributed by @alunturner. + * Add option to find own location in map views ([\#10083](https://github.com/matrix-org/matrix-react-sdk/pull/10083)). + * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. + +## 🐛 Bug Fixes + * Stop access token overflowing the box ([\#10069](https://github.com/matrix-org/matrix-react-sdk/pull/10069)). Fixes vector-im/element-web#24023. Contributed by @sbjaj33. + * Add link to next file in the export ([\#10190](https://github.com/matrix-org/matrix-react-sdk/pull/10190)). Fixes vector-im/element-web#20272. Contributed by @grimhilt. + * Ended poll tiles: add ended the poll message ([\#10193](https://github.com/matrix-org/matrix-react-sdk/pull/10193)). Fixes vector-im/element-web#24579. Contributed by @kerryarchibald. + * Fix accidentally inverted condition for room ordering ([\#10178](https://github.com/matrix-org/matrix-react-sdk/pull/10178)). Fixes vector-im/element-web#24527. Contributed by @justjanne. + * Re-focus the composer on dialogue quit ([\#10007](https://github.com/matrix-org/matrix-react-sdk/pull/10007)). Fixes vector-im/element-web#22832. Contributed by @Ashu999. + * Try to resolve emails before creating a DM ([\#10164](https://github.com/matrix-org/matrix-react-sdk/pull/10164)). + * Disable poll response loading test ([\#10168](https://github.com/matrix-org/matrix-react-sdk/pull/10168)). Contributed by @justjanne. + * Fix email lookup in invite dialog ([\#10150](https://github.com/matrix-org/matrix-react-sdk/pull/10150)). Fixes vector-im/element-web#23353. + * Remove duplicate white space characters from translation keys ([\#10152](https://github.com/matrix-org/matrix-react-sdk/pull/10152)). Contributed by @luixxiul. + * Fix the caption of new sessions manager on Labs settings page for localization ([\#10143](https://github.com/matrix-org/matrix-react-sdk/pull/10143)). Contributed by @luixxiul. + * Prevent start another DM with a user if one already exists ([\#10127](https://github.com/matrix-org/matrix-react-sdk/pull/10127)). Fixes vector-im/element-web#23138. + * Remove white space characters before the horizontal ellipsis ([\#10130](https://github.com/matrix-org/matrix-react-sdk/pull/10130)). Contributed by @luixxiul. + * Fix Selectable Text on 'Delete All' and 'Retry All' Buttons ([\#10128](https://github.com/matrix-org/matrix-react-sdk/pull/10128)). Fixes vector-im/element-web#23232. Contributed by @akshattchhabra. + * Correctly Identify emoticons ([\#10108](https://github.com/matrix-org/matrix-react-sdk/pull/10108)). Fixes vector-im/element-web#19472. Contributed by @adarsh-sgh. + * Remove a redundant white space ([\#10129](https://github.com/matrix-org/matrix-react-sdk/pull/10129)). Contributed by @luixxiul. + +Changes in [3.66.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.66.0) (2023-02-14) +===================================================================================================== + +## ✨ Features + * Add option to find own location in map views ([\#10083](https://github.com/matrix-org/matrix-react-sdk/pull/10083)). + * Render poll end events in timeline ([\#10027](https://github.com/matrix-org/matrix-react-sdk/pull/10027)). Contributed by @kerryarchibald. + * Indicate unread messages in tab title ([\#10096](https://github.com/matrix-org/matrix-react-sdk/pull/10096)). Contributed by @tnt7864. + * Open message in editing mode when keyboard up is pressed (RTE) ([\#10079](https://github.com/matrix-org/matrix-react-sdk/pull/10079)). Contributed by @florianduros. + * Hide superseded rooms from the room list using dynamic room predecessors ([\#10068](https://github.com/matrix-org/matrix-react-sdk/pull/10068)). Contributed by @andybalaam. + * Support MSC3946 in RoomListStore ([\#10054](https://github.com/matrix-org/matrix-react-sdk/pull/10054)). Fixes vector-im/element-web#24325. Contributed by @andybalaam. + * Auto focus security key field ([\#10048](https://github.com/matrix-org/matrix-react-sdk/pull/10048)). + * use Poll model with relations API in poll rendering ([\#9877](https://github.com/matrix-org/matrix-react-sdk/pull/9877)). Contributed by @kerryarchibald. + * Support MSC3946 in the RoomCreate tile ([\#10041](https://github.com/matrix-org/matrix-react-sdk/pull/10041)). Fixes vector-im/element-web#24323. Contributed by @andybalaam. + * Update labs flag description for RTE ([\#10058](https://github.com/matrix-org/matrix-react-sdk/pull/10058)). Contributed by @florianduros. + * Change ul list style to disc when editing message ([\#10043](https://github.com/matrix-org/matrix-react-sdk/pull/10043)). Contributed by @alunturner. + * Improved click detection within PiP windows ([\#10040](https://github.com/matrix-org/matrix-react-sdk/pull/10040)). Fixes vector-im/element-web#24371. + * Add RTE keyboard navigation in editing ([\#9980](https://github.com/matrix-org/matrix-react-sdk/pull/9980)). Fixes vector-im/element-web#23621. Contributed by @florianduros. + * Paragraph integration for rich text editor ([\#10008](https://github.com/matrix-org/matrix-react-sdk/pull/10008)). Contributed by @alunturner. + * Add indentation increasing/decreasing to RTE ([\#10034](https://github.com/matrix-org/matrix-react-sdk/pull/10034)). Contributed by @florianduros. + * Add ignore user confirmation dialog ([\#6116](https://github.com/matrix-org/matrix-react-sdk/pull/6116)). Fixes vector-im/element-web#14746. + * Use monospace font for room, message IDs in View Source modal ([\#9956](https://github.com/matrix-org/matrix-react-sdk/pull/9956)). Fixes vector-im/element-web#21937. Contributed by @paragpoddar. + * Implement MSC3946 for AdvancedRoomSettingsTab ([\#9995](https://github.com/matrix-org/matrix-react-sdk/pull/9995)). Fixes vector-im/element-web#24322. Contributed by @andybalaam. + * Implementation of MSC3824 to make the client OIDC-aware ([\#8681](https://github.com/matrix-org/matrix-react-sdk/pull/8681)). Contributed by @hughns. + * Improves a11y for avatar uploads ([\#9985](https://github.com/matrix-org/matrix-react-sdk/pull/9985)). Contributed by @GoodGuyMarco. + * Add support for [token authenticated registration](https ([\#7275](https://github.com/matrix-org/matrix-react-sdk/pull/7275)). Fixes vector-im/element-web#18931. Contributed by @govynnus. + +## 🐛 Bug Fixes + * Remove duplicate white space characters from translation keys ([\#10152](https://github.com/matrix-org/matrix-react-sdk/pull/10152)). Contributed by @luixxiul. + * Fix the caption of new sessions manager on Labs settings page for localization ([\#10143](https://github.com/matrix-org/matrix-react-sdk/pull/10143)). Contributed by @luixxiul. + * Prevent start another DM with a user if one already exists ([\#10127](https://github.com/matrix-org/matrix-react-sdk/pull/10127)). Fixes vector-im/element-web#23138. + * Remove white space characters before the horizontal ellipsis ([\#10130](https://github.com/matrix-org/matrix-react-sdk/pull/10130)). Contributed by @luixxiul. + * Fix Selectable Text on 'Delete All' and 'Retry All' Buttons ([\#10128](https://github.com/matrix-org/matrix-react-sdk/pull/10128)). Fixes vector-im/element-web#23232. Contributed by @akshattchhabra. + * Correctly Identify emoticons ([\#10108](https://github.com/matrix-org/matrix-react-sdk/pull/10108)). Fixes vector-im/element-web#19472. Contributed by @adarsh-sgh. + * Should open new 1:1 chat room after leaving the old one ([\#9880](https://github.com/matrix-org/matrix-react-sdk/pull/9880)). Contributed by @ahmadkadri. + * Remove a redundant white space ([\#10129](https://github.com/matrix-org/matrix-react-sdk/pull/10129)). Contributed by @luixxiul. + * Fix a crash when removing persistent widgets (updated) ([\#10099](https://github.com/matrix-org/matrix-react-sdk/pull/10099)). Fixes vector-im/element-web#24412. Contributed by @andybalaam. + * Fix wrongly grouping 3pid invites into a single repeated transition ([\#10087](https://github.com/matrix-org/matrix-react-sdk/pull/10087)). Fixes vector-im/element-web#24432. + * Fix scrollbar colliding with checkbox in add to space section ([\#10093](https://github.com/matrix-org/matrix-react-sdk/pull/10093)). Fixes vector-im/element-web#23189. Contributed by @Arnabdaz. + * Add a whitespace character after 'broadcast?' ([\#10097](https://github.com/matrix-org/matrix-react-sdk/pull/10097)). Contributed by @luixxiul. + * Seekbar in broadcast PiP view is now updated when switching between different broadcasts ([\#10072](https://github.com/matrix-org/matrix-react-sdk/pull/10072)). Fixes vector-im/element-web#24415. + * Add border to "reject" button on room preview card for clickable area indication. It fixes vector-im/element-web#22623 ([\#9205](https://github.com/matrix-org/matrix-react-sdk/pull/9205)). Contributed by @gefgu. + * Element-R: fix rageshages ([\#10081](https://github.com/matrix-org/matrix-react-sdk/pull/10081)). Fixes vector-im/element-web#24430. + * Fix markdown paragraph display in timeline ([\#10071](https://github.com/matrix-org/matrix-react-sdk/pull/10071)). Fixes vector-im/element-web#24419. Contributed by @alunturner. + * Prevent the remaining broadcast time from being exceeded ([\#10070](https://github.com/matrix-org/matrix-react-sdk/pull/10070)). + * Fix cursor position when new line is created by pressing enter (RTE) ([\#10064](https://github.com/matrix-org/matrix-react-sdk/pull/10064)). Contributed by @florianduros. + * Ensure room is actually in space hierarchy when resolving its latest version ([\#10010](https://github.com/matrix-org/matrix-react-sdk/pull/10010)). + * Fix new line for inline code ([\#10062](https://github.com/matrix-org/matrix-react-sdk/pull/10062)). Contributed by @florianduros. + * Member avatars without canvas ([\#9990](https://github.com/matrix-org/matrix-react-sdk/pull/9990)). Contributed by @clarkf. + * Apply more general fix for base avatar regressions ([\#10045](https://github.com/matrix-org/matrix-react-sdk/pull/10045)). Fixes vector-im/element-web#24382 and vector-im/element-web#24370. + * Replace list, code block and quote icons by new icons ([\#10035](https://github.com/matrix-org/matrix-react-sdk/pull/10035)). Contributed by @florianduros. + * fix regional emojis converted to flags ([\#9294](https://github.com/matrix-org/matrix-react-sdk/pull/9294)). Fixes vector-im/element-web#19000. Contributed by @grimhilt. + * resolved emoji description text overflowing issue ([\#10028](https://github.com/matrix-org/matrix-react-sdk/pull/10028)). Contributed by @fahadNoufal. + * Fix MessageEditHistoryDialog crashing on complex input ([\#10018](https://github.com/matrix-org/matrix-react-sdk/pull/10018)). Fixes vector-im/element-web#23665. Contributed by @clarkf. + * Unify unread notification state determination ([\#9941](https://github.com/matrix-org/matrix-react-sdk/pull/9941)). Contributed by @clarkf. + * Fix layout and visual regressions around default avatars ([\#10031](https://github.com/matrix-org/matrix-react-sdk/pull/10031)). Fixes vector-im/element-web#24375 and vector-im/element-web#24369. + * Fix useUnreadNotifications exploding with falsey room, like in notif panel ([\#10030](https://github.com/matrix-org/matrix-react-sdk/pull/10030)). Fixes matrix-org/element-web-rageshakes#19334. + * Fix "[object Promise]" appearing in HTML exports ([\#9975](https://github.com/matrix-org/matrix-react-sdk/pull/9975)). Fixes vector-im/element-web#24272. Contributed by @clarkf. + * changing the color of message time stamp ([\#10016](https://github.com/matrix-org/matrix-react-sdk/pull/10016)). Contributed by @nawarajshah. + * Fix link creation with backward selection ([\#9986](https://github.com/matrix-org/matrix-react-sdk/pull/9986)). Fixes vector-im/element-web#24315. Contributed by @florianduros. + * Misaligned reply preview in thread composer #23396 ([\#9977](https://github.com/matrix-org/matrix-react-sdk/pull/9977)). Fixes vector-im/element-web#23396. Contributed by @mustafa-kapadia1483. + + +Changes in [3.65.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.65.0) (2023-01-31) +===================================================================================================== + +## ✨ Features + * Quotes for rte ([\#9932](https://github.com/matrix-org/matrix-react-sdk/pull/9932)). Contributed by @alunturner. + * Show the room name in the room header during calls ([\#9942](https://github.com/matrix-org/matrix-react-sdk/pull/9942)). Fixes vector-im/element-web#24268. + * Add code blocks to rich text editor ([\#9921](https://github.com/matrix-org/matrix-react-sdk/pull/9921)). Contributed by @alunturner. + * Add new style for inline code ([\#9936](https://github.com/matrix-org/matrix-react-sdk/pull/9936)). Contributed by @florianduros. + * Add disabled button state to rich text editor ([\#9930](https://github.com/matrix-org/matrix-react-sdk/pull/9930)). Contributed by @alunturner. + * Change the rageshake "app" for auto-rageshakes ([\#9909](https://github.com/matrix-org/matrix-react-sdk/pull/9909)). + * Device manager - tweak settings display ([\#9905](https://github.com/matrix-org/matrix-react-sdk/pull/9905)). Contributed by @kerryarchibald. + * Add list functionality to rich text editor ([\#9871](https://github.com/matrix-org/matrix-react-sdk/pull/9871)). Contributed by @alunturner. + +## 🐛 Bug Fixes + * Fix RTE focus behaviour in threads ([\#9969](https://github.com/matrix-org/matrix-react-sdk/pull/9969)). Fixes vector-im/element-web#23755. Contributed by @florianduros. + * #22204 Issue: Centered File info in lightbox ([\#9971](https://github.com/matrix-org/matrix-react-sdk/pull/9971)). Fixes vector-im/element-web#22204. Contributed by @Spartan09. + * Fix seekbar position for zero length audio ([\#9949](https://github.com/matrix-org/matrix-react-sdk/pull/9949)). Fixes vector-im/element-web#24248. + * Allow thread panel to be closed after being opened from notification ([\#9937](https://github.com/matrix-org/matrix-react-sdk/pull/9937)). Fixes vector-im/element-web#23764 vector-im/element-web#23852 and vector-im/element-web#24213. Contributed by @justjanne. + * Only highlight focused menu item if focus is supposed to be visible ([\#9945](https://github.com/matrix-org/matrix-react-sdk/pull/9945)). Fixes vector-im/element-web#23582. + * Prevent call durations from breaking onto multiple lines ([\#9944](https://github.com/matrix-org/matrix-react-sdk/pull/9944)). + * Tweak call lobby buttons to more closely match designs ([\#9943](https://github.com/matrix-org/matrix-react-sdk/pull/9943)). + * Do not show a broadcast as live immediately after the recording has stopped ([\#9947](https://github.com/matrix-org/matrix-react-sdk/pull/9947)). Fixes vector-im/element-web#24233. + * Clear the RTE before sending a message ([\#9948](https://github.com/matrix-org/matrix-react-sdk/pull/9948)). Contributed by @florianduros. + * Fix {enter} press in RTE ([\#9927](https://github.com/matrix-org/matrix-react-sdk/pull/9927)). Contributed by @florianduros. + * Fix the problem that the password reset email has to be confirmed twice ([\#9926](https://github.com/matrix-org/matrix-react-sdk/pull/9926)). Fixes vector-im/element-web#24226. + * replace .at() with array.length-1 ([\#9933](https://github.com/matrix-org/matrix-react-sdk/pull/9933)). Fixes matrix-org/element-web-rageshakes#19281. + * Fix broken threads list timestamp layout ([\#9922](https://github.com/matrix-org/matrix-react-sdk/pull/9922)). Fixes vector-im/element-web#24243 and vector-im/element-web#24191. Contributed by @justjanne. + * Disable multiple messages when {enter} is pressed multiple times ([\#9929](https://github.com/matrix-org/matrix-react-sdk/pull/9929)). Fixes vector-im/element-web#24249. Contributed by @florianduros. + * Fix logout devices when resetting the password ([\#9925](https://github.com/matrix-org/matrix-react-sdk/pull/9925)). Fixes vector-im/element-web#24228. + * Fix: Poll replies overflow when not enough space ([\#9924](https://github.com/matrix-org/matrix-react-sdk/pull/9924)). Fixes vector-im/element-web#24227. Contributed by @kerryarchibald. + * State event updates are not forwarded to the widget from invitation room ([\#9802](https://github.com/matrix-org/matrix-react-sdk/pull/9802)). Contributed by @maheichyk. + * Fix error when viewing source of redacted events ([\#9914](https://github.com/matrix-org/matrix-react-sdk/pull/9914)). Fixes vector-im/element-web#24165. Contributed by @clarkf. + * Replace outdated css attribute ([\#9912](https://github.com/matrix-org/matrix-react-sdk/pull/9912)). Fixes vector-im/element-web#24218. Contributed by @justjanne. + * Clear isLogin theme override when user is no longer viewing login screens ([\#9911](https://github.com/matrix-org/matrix-react-sdk/pull/9911)). Fixes vector-im/element-web#23893. + * Fix reply action in message context menu notif & file panels ([\#9895](https://github.com/matrix-org/matrix-react-sdk/pull/9895)). Fixes vector-im/element-web#23970. + * Fix issue where thread dropdown would not show up correctly ([\#9872](https://github.com/matrix-org/matrix-react-sdk/pull/9872)). Fixes vector-im/element-web#24040. Contributed by @justjanne. + * Fix unexpected composer growing ([\#9889](https://github.com/matrix-org/matrix-react-sdk/pull/9889)). Contributed by @florianduros. + * Fix misaligned timestamps for thread roots which are emotes ([\#9875](https://github.com/matrix-org/matrix-react-sdk/pull/9875)). Fixes vector-im/element-web#23897. Contributed by @justjanne. + +Changes in [3.64.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.2) (2023-01-20) +===================================================================================================== + +## 🐛 Bug Fixes + * Fix second occurence of a crash in older browsers + +Changes in [3.64.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.1) (2023-01-18) +===================================================================================================== + +## 🐛 Bug Fixes + * Fix crash in older browsers (replace .at() with array.length-1) ([\#9933](https://github.com/matrix-org/matrix-react-sdk/pull/9933)). Fixes matrix-org/element-web-rageshakes#19281. + +Changes in [3.64.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.64.0) (2023-01-18) +===================================================================================================== + +## ✨ Features + * Switch threads on for everyone ([\#9879](https://github.com/matrix-org/matrix-react-sdk/pull/9879)). + * Make threads use new UTD UI ([\#9876](https://github.com/matrix-org/matrix-react-sdk/pull/9876)). Fixes vector-im/element-web#24060. + * Add edit and remove actions to link in RTE ([\#9864](https://github.com/matrix-org/matrix-react-sdk/pull/9864)). + * Remove extensible events v1 experimental rendering ([\#9881](https://github.com/matrix-org/matrix-react-sdk/pull/9881)). + * Make create poll dialog scale better (PSG-929) ([\#9873](https://github.com/matrix-org/matrix-react-sdk/pull/9873)). Fixes vector-im/element-web#21855. + * Change RTE mode icons ([\#9861](https://github.com/matrix-org/matrix-react-sdk/pull/9861)). + * Device manager - prune client information events after remote sign out ([\#9874](https://github.com/matrix-org/matrix-react-sdk/pull/9874)). + * Check connection before starting broadcast ([\#9857](https://github.com/matrix-org/matrix-react-sdk/pull/9857)). + * Enable sent receipt for poll start events (PSG-962) ([\#9870](https://github.com/matrix-org/matrix-react-sdk/pull/9870)). + * Change clear notifications to have more readable copy ([\#9867](https://github.com/matrix-org/matrix-react-sdk/pull/9867)). + * Combine search results when the query is present in multiple successive messages ([\#9855](https://github.com/matrix-org/matrix-react-sdk/pull/9855)). Fixes vector-im/element-web#3977. Contributed by @grimhilt. + * Disable bubbles for broadcasts ([\#9860](https://github.com/matrix-org/matrix-react-sdk/pull/9860)). Fixes vector-im/element-web#24140. + * Enable reactions and replies for broadcasts ([\#9856](https://github.com/matrix-org/matrix-react-sdk/pull/9856)). Fixes vector-im/element-web#24042. + * Improve switching between rich and plain editing modes ([\#9776](https://github.com/matrix-org/matrix-react-sdk/pull/9776)). + * Redesign the picture-in-picture window ([\#9800](https://github.com/matrix-org/matrix-react-sdk/pull/9800)). Fixes vector-im/element-web#23980. + * User on-boarding tasks now appear in a static order. ([\#9799](https://github.com/matrix-org/matrix-react-sdk/pull/9799)). Contributed by @GoodGuyMarco. + * Device manager - contextual menus ([\#9832](https://github.com/matrix-org/matrix-react-sdk/pull/9832)). + * If listening a non-live broadcast and changing the room, the broadcast will be paused ([\#9825](https://github.com/matrix-org/matrix-react-sdk/pull/9825)). Fixes vector-im/element-web#24078. + * Consider own broadcasts from other device as a playback ([\#9821](https://github.com/matrix-org/matrix-react-sdk/pull/9821)). Fixes vector-im/element-web#24068. + * Add link creation to rich text editor ([\#9775](https://github.com/matrix-org/matrix-react-sdk/pull/9775)). + * Add mark as read option in room setting ([\#9798](https://github.com/matrix-org/matrix-react-sdk/pull/9798)). Fixes vector-im/element-web#24053. + * Device manager - current device design and copy tweaks ([\#9801](https://github.com/matrix-org/matrix-react-sdk/pull/9801)). + * Unify notifications panel event design ([\#9754](https://github.com/matrix-org/matrix-react-sdk/pull/9754)). + * Add actions for integration manager to send and read certain events ([\#9740](https://github.com/matrix-org/matrix-react-sdk/pull/9740)). + * Device manager - design tweaks ([\#9768](https://github.com/matrix-org/matrix-react-sdk/pull/9768)). + * Change room list sorting to activity and unread first by default ([\#9773](https://github.com/matrix-org/matrix-react-sdk/pull/9773)). Fixes vector-im/element-web#24014. + * Add a config flag to enable the rust crypto-sdk ([\#9759](https://github.com/matrix-org/matrix-react-sdk/pull/9759)). + * Improve decryption error UI by consolidating error messages and providing instructions when possible ([\#9544](https://github.com/matrix-org/matrix-react-sdk/pull/9544)). + * Honor font settings in Element Call ([\#9751](https://github.com/matrix-org/matrix-react-sdk/pull/9751)). Fixes vector-im/element-web#23661. + * Device manager - use deleteAccountData to prune device manager client information events ([\#9734](https://github.com/matrix-org/matrix-react-sdk/pull/9734)). + +## 🐛 Bug Fixes + * Display rooms & threads as unread (bold) if threads have unread messages. ([\#9763](https://github.com/matrix-org/matrix-react-sdk/pull/9763)). Fixes vector-im/element-web#23907. + * Don't prefer STIXGeneral over the default font ([\#9711](https://github.com/matrix-org/matrix-react-sdk/pull/9711)). Fixes vector-im/element-web#23899. + * Use the same avatar colour when creating 1:1 DM rooms ([\#9850](https://github.com/matrix-org/matrix-react-sdk/pull/9850)). Fixes vector-im/element-web#23476. + * Fix space lock icon size ([\#9854](https://github.com/matrix-org/matrix-react-sdk/pull/9854)). Fixes vector-im/element-web#24128. + * Make calls automatically disconnect if the widget disappears ([\#9862](https://github.com/matrix-org/matrix-react-sdk/pull/9862)). Fixes vector-im/element-web#23664. + * Fix emoji in RTE editing ([\#9827](https://github.com/matrix-org/matrix-react-sdk/pull/9827)). + * Fix export with attachments on formats txt and json ([\#9851](https://github.com/matrix-org/matrix-react-sdk/pull/9851)). Fixes vector-im/element-web#24130. Contributed by @grimhilt. + * Fixed empty `Content-Type` for encrypted uploads ([\#9848](https://github.com/matrix-org/matrix-react-sdk/pull/9848)). Contributed by @K3das. + * Fix sign-in instead link on password reset page ([\#9820](https://github.com/matrix-org/matrix-react-sdk/pull/9820)). Fixes vector-im/element-web#24087. + * The seekbar now initially shows the current position ([\#9796](https://github.com/matrix-org/matrix-react-sdk/pull/9796)). Fixes vector-im/element-web#24051. + * Fix: Editing a poll will silently change it to a closed poll ([\#9809](https://github.com/matrix-org/matrix-react-sdk/pull/9809)). Fixes vector-im/element-web#23176. + * Make call tiles look less broken in the right panel ([\#9808](https://github.com/matrix-org/matrix-react-sdk/pull/9808)). Fixes vector-im/element-web#23716. + * Prevent unnecessary m.direct updates ([\#9805](https://github.com/matrix-org/matrix-react-sdk/pull/9805)). Fixes vector-im/element-web#24059. + * Fix checkForPreJoinUISI for thread roots ([\#9803](https://github.com/matrix-org/matrix-react-sdk/pull/9803)). Fixes vector-im/element-web#24054. + * Snap in PiP widget when content changed ([\#9797](https://github.com/matrix-org/matrix-react-sdk/pull/9797)). Fixes vector-im/element-web#24050. + * Load RTE components only when RTE labs is enabled ([\#9804](https://github.com/matrix-org/matrix-react-sdk/pull/9804)). + * Ensure that events are correctly updated when they are edited. ([\#9789](https://github.com/matrix-org/matrix-react-sdk/pull/9789)). + * When stopping a broadcast also stop the playback ([\#9795](https://github.com/matrix-org/matrix-react-sdk/pull/9795)). Fixes vector-im/element-web#24052. + * Prevent to start two broadcasts at the same time ([\#9744](https://github.com/matrix-org/matrix-react-sdk/pull/9744)). Fixes vector-im/element-web#23973. + * Correctly handle limited sync responses by resetting the thread timeline ([\#3056](https://github.com/matrix-org/matrix-js-sdk/pull/3056)). Fixes vector-im/element-web#23952. + * Fix failure to start in firefox private browser ([\#3058](https://github.com/matrix-org/matrix-js-sdk/pull/3058)). Fixes vector-im/element-web#24216. + +Changes in [3.63.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.63.0) (2022-12-21) +===================================================================================================== + +## ✨ Features + * Prevent unnecessary m.direct updates ([\#9805](https://github.com/matrix-org/matrix-react-sdk/pull/9805)). Fixes vector-im/element-web#24059. + * Fix checkForPreJoinUISI for thread roots ([\#9803](https://github.com/matrix-org/matrix-react-sdk/pull/9803)). Fixes vector-im/element-web#24054. + * Add inline code formatting to rich text editor ([\#9720](https://github.com/matrix-org/matrix-react-sdk/pull/9720)). + * Add emoji handling for plain text mode of the new rich text editor ([\#9727](https://github.com/matrix-org/matrix-react-sdk/pull/9727)). + * Overlay virtual room call events into main timeline ([\#9626](https://github.com/matrix-org/matrix-react-sdk/pull/9626)). Fixes vector-im/element-web#22929. + * Adds a new section under "Room Settings" > "Roles & Permissions" which adds the possibility to multiselect users from this room and grant them more permissions. ([\#9596](https://github.com/matrix-org/matrix-react-sdk/pull/9596)). Contributed by @GoodGuyMarco. + * Add emoji handling for rich text mode ([\#9661](https://github.com/matrix-org/matrix-react-sdk/pull/9661)). + * Add setting to hide bold notifications ([\#9705](https://github.com/matrix-org/matrix-react-sdk/pull/9705)). + * Further password reset flow enhancements ([\#9662](https://github.com/matrix-org/matrix-react-sdk/pull/9662)). + * Snooze the bulk unverified sessions reminder on dismiss ([\#9706](https://github.com/matrix-org/matrix-react-sdk/pull/9706)). + * Honor advanced audio processing settings when recording voice messages ([\#9610](https://github.com/matrix-org/matrix-react-sdk/pull/9610)). Contributed by @MrAnno. + * Improve the visual balance of bubble layout ([\#9704](https://github.com/matrix-org/matrix-react-sdk/pull/9704)). + * Add config setting to disable bulk unverified sessions nag ([\#9657](https://github.com/matrix-org/matrix-react-sdk/pull/9657)). + * Only display bulk unverified sessions nag when current sessions is verified ([\#9656](https://github.com/matrix-org/matrix-react-sdk/pull/9656)). + * Separate labs and betas more clearly ([\#8969](https://github.com/matrix-org/matrix-react-sdk/pull/8969)). Fixes vector-im/element-web#22706. + * Show user an error if we fail to create a DM for verification. ([\#9624](https://github.com/matrix-org/matrix-react-sdk/pull/9624)). + +## 🐛 Bug Fixes + * Fix issue where thread panel did not update correctly ([\#9746](https://github.com/matrix-org/matrix-react-sdk/pull/9746)). Fixes vector-im/element-web#23971. + * Remove async call to get virtual room from room load ([\#9743](https://github.com/matrix-org/matrix-react-sdk/pull/9743)). Fixes vector-im/element-web#23968. + * Check each thread for unread messages. ([\#9723](https://github.com/matrix-org/matrix-react-sdk/pull/9723)). + * Device manage - handle sessions that don't support encryption ([\#9717](https://github.com/matrix-org/matrix-react-sdk/pull/9717)). Fixes vector-im/element-web#23722. + * Fix hover state for formatting buttons (Rich text editor) (fix vector-im/element-web/issues/23832) ([\#9715](https://github.com/matrix-org/matrix-react-sdk/pull/9715)). + * Don't allow group calls to be unterminated ([\#9710](https://github.com/matrix-org/matrix-react-sdk/pull/9710)). + * Fix replies to emotes not showing as inline ([\#9707](https://github.com/matrix-org/matrix-react-sdk/pull/9707)). Fixes vector-im/element-web#23903. + * Update copy of 'Change layout' button to match Element Call ([\#9703](https://github.com/matrix-org/matrix-react-sdk/pull/9703)). + * Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)). + * bugfix: fix an issue where the Notifier would incorrectly fire for non-timeline events ([\#9664](https://github.com/matrix-org/matrix-react-sdk/pull/9664)). Fixes vector-im/element-web#17263. + * Fix power selector being wrongly disabled for admins themselves ([\#9681](https://github.com/matrix-org/matrix-react-sdk/pull/9681)). Fixes vector-im/element-web#23882. + * Show day counts in call durations ([\#9641](https://github.com/matrix-org/matrix-react-sdk/pull/9641)). + +Changes in [3.62.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.62.0) (2022-12-06) +===================================================================================================== + +## ✨ Features + * Further improve replies ([\#6396](https://github.com/matrix-org/matrix-react-sdk/pull/6396)). Fixes vector-im/element-web#19074, vector-im/element-web#18194 vector-im/element-web#18027 and vector-im/element-web#19179. + * Enable users to join group calls from multiple devices ([\#9625](https://github.com/matrix-org/matrix-react-sdk/pull/9625)). + * fix(visual): make cursor a pointer for summaries ([\#9419](https://github.com/matrix-org/matrix-react-sdk/pull/9419)). Contributed by @r00ster91. + * Add placeholder for rich text editor ([\#9613](https://github.com/matrix-org/matrix-react-sdk/pull/9613)). + * Consolidate public room search experience ([\#9605](https://github.com/matrix-org/matrix-react-sdk/pull/9605)). Fixes vector-im/element-web#22846. + * New password reset flow ([\#9581](https://github.com/matrix-org/matrix-react-sdk/pull/9581)). Fixes vector-im/element-web#23131. + * Device manager - add tooltip to device details toggle ([\#9594](https://github.com/matrix-org/matrix-react-sdk/pull/9594)). + * sliding sync: add lazy-loading member support ([\#9530](https://github.com/matrix-org/matrix-react-sdk/pull/9530)). + * Limit formatting bar offset to top of composer ([\#9365](https://github.com/matrix-org/matrix-react-sdk/pull/9365)). Fixes vector-im/element-web#12359. Contributed by @owi92. + +## 🐛 Bug Fixes + * Fix issues around up arrow event edit shortcut ([\#9645](https://github.com/matrix-org/matrix-react-sdk/pull/9645)). Fixes vector-im/element-web#18497 and vector-im/element-web#18964. + * Fix search not being cleared when clicking on a result ([\#9635](https://github.com/matrix-org/matrix-react-sdk/pull/9635)). Fixes vector-im/element-web#23845. + * Fix screensharing in 1:1 calls ([\#9612](https://github.com/matrix-org/matrix-react-sdk/pull/9612)). Fixes vector-im/element-web#23808. + * Fix the background color flashing when joining a call ([\#9640](https://github.com/matrix-org/matrix-react-sdk/pull/9640)). + * Fix the size of the 'Private space' icon ([\#9638](https://github.com/matrix-org/matrix-react-sdk/pull/9638)). + * Fix reply editing in rich text editor (https ([\#9615](https://github.com/matrix-org/matrix-react-sdk/pull/9615)). + * Fix thread list jumping back down while scrolling ([\#9606](https://github.com/matrix-org/matrix-react-sdk/pull/9606)). Fixes vector-im/element-web#23727. + * Fix regression with TimelinePanel props updates not taking effect ([\#9608](https://github.com/matrix-org/matrix-react-sdk/pull/9608)). Fixes vector-im/element-web#23794. + * Fix form tooltip positioning ([\#9598](https://github.com/matrix-org/matrix-react-sdk/pull/9598)). Fixes vector-im/element-web#22861. + * Extract Search handling from RoomView into its own Component ([\#9574](https://github.com/matrix-org/matrix-react-sdk/pull/9574)). Fixes vector-im/element-web#498. + * Fix call splitbrains when switching between rooms ([\#9692](https://github.com/matrix-org/matrix-react-sdk/pull/9692)). + * Fix replies to emotes not showing as inline ([\#9707](https://github.com/matrix-org/matrix-react-sdk/pull/9707)). Fixes vector-im/element-web#23903. + +Changes in [3.61.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.61.0) (2022-11-22) +===================================================================================================== + +## ✨ Features + * Make clear notifications work with threads ([\#9575](https://github.com/matrix-org/matrix-react-sdk/pull/9575)). Fixes vector-im/element-web#23751. + * Change "None" to "Off" in notification options ([\#9539](https://github.com/matrix-org/matrix-react-sdk/pull/9539)). Contributed by @Arnei. + * Advanced audio processing settings ([\#8759](https://github.com/matrix-org/matrix-react-sdk/pull/8759)). Fixes vector-im/element-web#6278. Contributed by @MrAnno. + * Add way to create a user notice via config.json ([\#9559](https://github.com/matrix-org/matrix-react-sdk/pull/9559)). + * Improve design of the rich text editor ([\#9533](https://github.com/matrix-org/matrix-react-sdk/pull/9533)). Contributed by @florianduros. + * Enable user to zoom beyond image size ([\#5949](https://github.com/matrix-org/matrix-react-sdk/pull/5949)). Contributed by @jaiwanth-v. + * Fix: Move "Leave Space" option to the bottom of space context menu ([\#9535](https://github.com/matrix-org/matrix-react-sdk/pull/9535)). Contributed by @hanadi92. + +## 🐛 Bug Fixes + * Fix integration manager `get_open_id_token` action and add E2E tests ([\#9520](https://github.com/matrix-org/matrix-react-sdk/pull/9520)). + * Fix links being mangled by markdown processing ([\#9570](https://github.com/matrix-org/matrix-react-sdk/pull/9570)). Fixes vector-im/element-web#23743. + * Fix: inline links selecting radio button ([\#9543](https://github.com/matrix-org/matrix-react-sdk/pull/9543)). Contributed by @hanadi92. + * fix wrong error message in registration when phone number threepid is in use. ([\#9571](https://github.com/matrix-org/matrix-react-sdk/pull/9571)). Contributed by @bagvand. + * Fix missing avatar for show current profiles ([\#9563](https://github.com/matrix-org/matrix-react-sdk/pull/9563)). Fixes vector-im/element-web#23733. + * fix read receipts trickling down correctly ([\#9567](https://github.com/matrix-org/matrix-react-sdk/pull/9567)). Fixes vector-im/element-web#23746. + * Resilience fix for homeserver without thread notification support ([\#9565](https://github.com/matrix-org/matrix-react-sdk/pull/9565)). + * Don't switch to the home page needlessly after leaving a room ([\#9477](https://github.com/matrix-org/matrix-react-sdk/pull/9477)). + * Differentiate download and decryption errors when showing images ([\#9562](https://github.com/matrix-org/matrix-react-sdk/pull/9562)). Fixes vector-im/element-web#3892. + * Close context menu when a modal is opened to prevent user getting stuck ([\#9560](https://github.com/matrix-org/matrix-react-sdk/pull/9560)). Fixes vector-im/element-web#15610 and vector-im/element-web#10781. + * Fix TimelineReset handling when no room associated ([\#9553](https://github.com/matrix-org/matrix-react-sdk/pull/9553)). + * Always use current profile on thread events ([\#9524](https://github.com/matrix-org/matrix-react-sdk/pull/9524)). Fixes vector-im/element-web#23648. + * Fix `ThreadView` tests not using thread flag ([\#9547](https://github.com/matrix-org/matrix-react-sdk/pull/9547)). Contributed by @MadLittleMods. + * Handle deletion of `m.call` events ([\#9540](https://github.com/matrix-org/matrix-react-sdk/pull/9540)). Fixes vector-im/element-web#23663. + * Fix incorrect notification count after leaving a room with notifications ([\#9518](https://github.com/matrix-org/matrix-react-sdk/pull/9518)). Contributed by @Arnei. + +Changes in [3.60.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.60.0) (2022-11-08) +===================================================================================================== + +## ✨ Features + * Loading threads with server-side assistance ([\#9356](https://github.com/matrix-org/matrix-react-sdk/pull/9356)). Fixes vector-im/element-web#21807, vector-im/element-web#21799, vector-im/element-web#21911, vector-im/element-web#22141, vector-im/element-web#22157, vector-im/element-web#22641, vector-im/element-web#22501 vector-im/element-web#22438 and vector-im/element-web#21678. Contributed by @justjanne. + * Make thread replies trigger a room list re-ordering ([\#9510](https://github.com/matrix-org/matrix-react-sdk/pull/9510)). Fixes vector-im/element-web#21700. + * Device manager - add extra details to device security and renaming ([\#9501](https://github.com/matrix-org/matrix-react-sdk/pull/9501)). Contributed by @kerryarchibald. + * Add plain text mode to the wysiwyg composer ([\#9503](https://github.com/matrix-org/matrix-react-sdk/pull/9503)). Contributed by @florianduros. + * Sliding Sync: improve sort order, show subspace rooms, better tombstoned room handling ([\#9484](https://github.com/matrix-org/matrix-react-sdk/pull/9484)). + * Device manager - add learn more popups to filtered sessions section ([\#9497](https://github.com/matrix-org/matrix-react-sdk/pull/9497)). Contributed by @kerryarchibald. + * Show thread notification if thread timeline is closed ([\#9495](https://github.com/matrix-org/matrix-react-sdk/pull/9495)). Fixes vector-im/element-web#23589. + * Add message editing to wysiwyg composer ([\#9488](https://github.com/matrix-org/matrix-react-sdk/pull/9488)). Contributed by @florianduros. + * Device manager - confirm sign out of other sessions ([\#9487](https://github.com/matrix-org/matrix-react-sdk/pull/9487)). Contributed by @kerryarchibald. + * Automatically request logs from other users in a call when submitting logs ([\#9492](https://github.com/matrix-org/matrix-react-sdk/pull/9492)). + * Add thread notification with server assistance (MSC3773) ([\#9400](https://github.com/matrix-org/matrix-react-sdk/pull/9400)). Fixes vector-im/element-web#21114, vector-im/element-web#21413, vector-im/element-web#21416, vector-im/element-web#21433, vector-im/element-web#21481, vector-im/element-web#21798, vector-im/element-web#21823 vector-im/element-web#23192 and vector-im/element-web#21765. + * Support for login + E2EE set up with QR ([\#9403](https://github.com/matrix-org/matrix-react-sdk/pull/9403)). Contributed by @hughns. + * Allow pressing Enter to send messages in new composer ([\#9451](https://github.com/matrix-org/matrix-react-sdk/pull/9451)). Contributed by @andybalaam. + +## 🐛 Bug Fixes + * Fix regressions around media uploads failing and causing soft crashes ([\#9549](https://github.com/matrix-org/matrix-react-sdk/pull/9549)). Fixes matrix-org/element-web-rageshakes#16831, matrix-org/element-web-rageshakes#16824 matrix-org/element-web-rageshakes#16810 and vector-im/element-web#23641. + * Fix /myroomavatar slash command ([\#9536](https://github.com/matrix-org/matrix-react-sdk/pull/9536)). Fixes matrix-org/synapse#14321. + * Fix NotificationBadge unsent color ([\#9522](https://github.com/matrix-org/matrix-react-sdk/pull/9522)). Fixes vector-im/element-web#23646. + * Fix room list sorted by recent on app startup ([\#9515](https://github.com/matrix-org/matrix-react-sdk/pull/9515)). Fixes vector-im/element-web#23635. + * Reset custom power selector when blurred on empty ([\#9508](https://github.com/matrix-org/matrix-react-sdk/pull/9508)). Fixes vector-im/element-web#23481. + * Reinstate timeline/redaction callbacks when updating notification state ([\#9494](https://github.com/matrix-org/matrix-react-sdk/pull/9494)). Fixes vector-im/element-web#23554. + * Only render NotificationBadge when needed ([\#9493](https://github.com/matrix-org/matrix-react-sdk/pull/9493)). Fixes vector-im/element-web#23584. + * Fix embedded Element Call screen sharing ([\#9485](https://github.com/matrix-org/matrix-react-sdk/pull/9485)). Fixes vector-im/element-web#23571. + * Send Content-Type: application/json header for integration manager /register API ([\#9490](https://github.com/matrix-org/matrix-react-sdk/pull/9490)). Fixes vector-im/element-web#23580. + * Fix joining calls without audio or video inputs ([\#9486](https://github.com/matrix-org/matrix-react-sdk/pull/9486)). Fixes vector-im/element-web#23511. + * Ensure spaces in the spotlight dialog have rounded square avatars ([\#9480](https://github.com/matrix-org/matrix-react-sdk/pull/9480)). Fixes vector-im/element-web#23515. + * Only show mini avatar uploader in room intro when no avatar yet exists ([\#9479](https://github.com/matrix-org/matrix-react-sdk/pull/9479)). Fixes vector-im/element-web#23552. + * Fix threads fallback incorrectly targets root event ([\#9229](https://github.com/matrix-org/matrix-react-sdk/pull/9229)). Fixes vector-im/element-web#23147. + * Align video call icon with banner text ([\#9460](https://github.com/matrix-org/matrix-react-sdk/pull/9460)). + * Set relations helper when creating event tile context menu ([\#9253](https://github.com/matrix-org/matrix-react-sdk/pull/9253)). Fixes vector-im/element-web#22018. + * Device manager - put client/browser device metadata in correct section ([\#9447](https://github.com/matrix-org/matrix-react-sdk/pull/9447)). Contributed by @kerryarchibald. + * Update the room unread notification counter when the server changes the value without any related read receipt ([\#9438](https://github.com/matrix-org/matrix-react-sdk/pull/9438)). + +Changes in [3.59.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.59.1) (2022-11-01) +===================================================================================================== + +## 🐛 Bug Fixes + * Fix default behavior of Room.getBlacklistUnverifiedDevices ([\#2830](https://github.com/matrix-org/matrix-js-sdk/pull/2830)). Contributed by @duxovni. + * Catch server versions API call exception when starting the client ([\#2828](https://github.com/matrix-org/matrix-js-sdk/pull/2828)). Fixes vector-im/element-web#23634. + * Fix authedRequest including `Authorization: Bearer undefined` for password resets ([\#2822](https://github.com/matrix-org/matrix-js-sdk/pull/2822)). Fixes vector-im/element-web#23655. + +Changes in [3.59.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.59.0) (2022-10-25) +===================================================================================================== + +## ✨ Features + * Include a file-safe room name and ISO date in chat exports ([\#9440](https://github.com/matrix-org/matrix-react-sdk/pull/9440)). Fixes vector-im/element-web#21812 and vector-im/element-web#19724. + * Room call banner ([\#9378](https://github.com/matrix-org/matrix-react-sdk/pull/9378)). Fixes vector-im/element-web#23453. Contributed by @toger5. + * Device manager - spinners while devices are signing out ([\#9433](https://github.com/matrix-org/matrix-react-sdk/pull/9433)). Fixes vector-im/element-web#15865. + * Device manager - silence call ringers when local notifications are silenced ([\#9420](https://github.com/matrix-org/matrix-react-sdk/pull/9420)). + * Pass the current language to Element Call ([\#9427](https://github.com/matrix-org/matrix-react-sdk/pull/9427)). + * Hide screen-sharing button in Element Call on desktop ([\#9423](https://github.com/matrix-org/matrix-react-sdk/pull/9423)). + * Add reply support to WysiwygComposer ([\#9422](https://github.com/matrix-org/matrix-react-sdk/pull/9422)). Contributed by @florianduros. + * Disconnect other connected devices (of the same user) when joining an Element call ([\#9379](https://github.com/matrix-org/matrix-react-sdk/pull/9379)). + * Device manager - device tile main click target ([\#9409](https://github.com/matrix-org/matrix-react-sdk/pull/9409)). + * Add formatting buttons to the rich text editor ([\#9410](https://github.com/matrix-org/matrix-react-sdk/pull/9410)). Contributed by @florianduros. + * Device manager - current session context menu ([\#9386](https://github.com/matrix-org/matrix-react-sdk/pull/9386)). + * Remove piwik config fallback for privacy policy URL ([\#9390](https://github.com/matrix-org/matrix-react-sdk/pull/9390)). + * Add the first step to integrate the matrix wysiwyg composer ([\#9374](https://github.com/matrix-org/matrix-react-sdk/pull/9374)). Contributed by @florianduros. + * Device manager - UA parsing tweaks ([\#9382](https://github.com/matrix-org/matrix-react-sdk/pull/9382)). + * Device manager - remove client information events when disabling setting ([\#9384](https://github.com/matrix-org/matrix-react-sdk/pull/9384)). + * Add Element Call participant limit ([\#9358](https://github.com/matrix-org/matrix-react-sdk/pull/9358)). + * Add Element Call room settings ([\#9347](https://github.com/matrix-org/matrix-react-sdk/pull/9347)). + * Device manager - render extended device information ([\#9360](https://github.com/matrix-org/matrix-react-sdk/pull/9360)). + * New group call experience: Room header and PiP designs ([\#9351](https://github.com/matrix-org/matrix-react-sdk/pull/9351)). + * Pass language to Jitsi Widget ([\#9346](https://github.com/matrix-org/matrix-react-sdk/pull/9346)). Contributed by @Fox32. + * Add notifications and toasts for Element Call calls ([\#9337](https://github.com/matrix-org/matrix-react-sdk/pull/9337)). + * Device manager - device type icon ([\#9355](https://github.com/matrix-org/matrix-react-sdk/pull/9355)). + * Delete the remainder of groups ([\#9357](https://github.com/matrix-org/matrix-react-sdk/pull/9357)). Fixes vector-im/element-web#22770. + * Device manager - display client information in device details ([\#9315](https://github.com/matrix-org/matrix-react-sdk/pull/9315)). + +## 🐛 Bug Fixes + * Send Content-Type: application/json header for integration manager /register API ([\#9490](https://github.com/matrix-org/matrix-react-sdk/pull/9490)). Fixes vector-im/element-web#23580. + * Device manager - put client/browser device metadata in correct section ([\#9447](https://github.com/matrix-org/matrix-react-sdk/pull/9447)). + * update the room unread notification counter when the server changes the value without any related read receipt ([\#9438](https://github.com/matrix-org/matrix-react-sdk/pull/9438)). + * Don't show call banners in video rooms ([\#9441](https://github.com/matrix-org/matrix-react-sdk/pull/9441)). + * Prevent useContextMenu isOpen from being true if the button ref goes away ([\#9418](https://github.com/matrix-org/matrix-react-sdk/pull/9418)). Fixes matrix-org/element-web-rageshakes#15637. + * Automatically focus the WYSIWYG composer when you enter a room ([\#9412](https://github.com/matrix-org/matrix-react-sdk/pull/9412)). + * Improve the tooltips on the call lobby join button ([\#9428](https://github.com/matrix-org/matrix-react-sdk/pull/9428)). + * Pass the homeserver's base URL to Element Call ([\#9429](https://github.com/matrix-org/matrix-react-sdk/pull/9429)). Fixes vector-im/element-web#23301. + * Better accommodate long room names in call toasts ([\#9426](https://github.com/matrix-org/matrix-react-sdk/pull/9426)). + * Hide virtual widgets from the room info panel ([\#9424](https://github.com/matrix-org/matrix-react-sdk/pull/9424)). Fixes vector-im/element-web#23494. + * Inhibit clicking on sender avatar in threads list ([\#9417](https://github.com/matrix-org/matrix-react-sdk/pull/9417)). Fixes vector-im/element-web#23482. + * Correct the dir parameter of MSC3715 ([\#9391](https://github.com/matrix-org/matrix-react-sdk/pull/9391)). Contributed by @dhenneke. + * Use a more correct subset of users in `/remakeolm` developer command ([\#9402](https://github.com/matrix-org/matrix-react-sdk/pull/9402)). + * use correct default for notification silencing ([\#9388](https://github.com/matrix-org/matrix-react-sdk/pull/9388)). Fixes vector-im/element-web#23456. + * Device manager - eagerly create `m.local_notification_settings` events ([\#9353](https://github.com/matrix-org/matrix-react-sdk/pull/9353)). + * Close incoming Element call toast when viewing the call lobby ([\#9375](https://github.com/matrix-org/matrix-react-sdk/pull/9375)). + * Always allow enabling sending read receipts ([\#9367](https://github.com/matrix-org/matrix-react-sdk/pull/9367)). Fixes vector-im/element-web#23433. + * Fixes (vector-im/element-web/issues/22609) where the white theme is not applied when `white -> dark -> white` sequence is done. ([\#9320](https://github.com/matrix-org/matrix-react-sdk/pull/9320)). Contributed by @florianduros. + * Fix applying programmatically set height for "top" room layout ([\#9339](https://github.com/matrix-org/matrix-react-sdk/pull/9339)). Contributed by @Fox32. + +Changes in [3.58.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.1) (2022-10-11) +===================================================================================================== + +## 🐛 Bug Fixes + * Use correct default for notification silencing ([\#9388](https://github.com/matrix-org/matrix-react-sdk/pull/9388)). Fixes vector-im/element-web#23456. + +Changes in [3.58.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.58.0) (2022-10-11) +=============================================================================================================== + +## Deprecations + * Legacy Piwik config.json option `piwik.policy_url` is deprecated in favour of `privacy_policy_url`. Support will be removed in the next release. + +## ✨ Features + * Device manager - select all devices ([\#9330](https://github.com/matrix-org/matrix-react-sdk/pull/9330)). + * New group call experience: Call tiles ([\#9332](https://github.com/matrix-org/matrix-react-sdk/pull/9332)). + * Add Shift key to FormatQuote keyboard shortcut ([\#9298](https://github.com/matrix-org/matrix-react-sdk/pull/9298)). Contributed by @owi92. + * Device manager - sign out of multiple sessions ([\#9325](https://github.com/matrix-org/matrix-react-sdk/pull/9325)). + * Display push toggle for web sessions (MSC3890) ([\#9327](https://github.com/matrix-org/matrix-react-sdk/pull/9327)). + * Add device notifications enabled switch ([\#9324](https://github.com/matrix-org/matrix-react-sdk/pull/9324)). + * Implement push notification toggle in device detail ([\#9308](https://github.com/matrix-org/matrix-react-sdk/pull/9308)). + * New group call experience: Starting and ending calls ([\#9318](https://github.com/matrix-org/matrix-react-sdk/pull/9318)). + * New group call experience: Room header call buttons ([\#9311](https://github.com/matrix-org/matrix-react-sdk/pull/9311)). + * Make device ID copyable in device list ([\#9297](https://github.com/matrix-org/matrix-react-sdk/pull/9297)). + * Use display name instead of user ID when rendering power events ([\#9295](https://github.com/matrix-org/matrix-react-sdk/pull/9295)). + * Read receipts for threads ([\#9239](https://github.com/matrix-org/matrix-react-sdk/pull/9239)). Fixes vector-im/element-web#23191. + +## 🐛 Bug Fixes + * Use the correct sender key when checking shared secret ([\#2730](https://github.com/matrix-org/matrix-js-sdk/pull/2730)). Fixes vector-im/element-web#23374. + * Fix device selection in pre-join screen for Element Call video rooms ([\#9321](https://github.com/matrix-org/matrix-react-sdk/pull/9321)). Fixes vector-im/element-web#23331. + * Don't render a 1px high room topic if the room topic is empty ([\#9317](https://github.com/matrix-org/matrix-react-sdk/pull/9317)). Contributed by @Arnei. + * Don't show feedback prompts when that UIFeature is disabled ([\#9305](https://github.com/matrix-org/matrix-react-sdk/pull/9305)). Fixes vector-im/element-web#23327. + * Fix soft crash around unknown room pills ([\#9301](https://github.com/matrix-org/matrix-react-sdk/pull/9301)). Fixes matrix-org/element-web-rageshakes#15465. + * Fix spaces feedback prompt wrongly showing when feedback is disabled ([\#9302](https://github.com/matrix-org/matrix-react-sdk/pull/9302)). Fixes vector-im/element-web#23314. + * Fix tile soft crash in ReplyInThreadButton ([\#9300](https://github.com/matrix-org/matrix-react-sdk/pull/9300)). Fixes matrix-org/element-web-rageshakes#15493. + +Changes in [3.57.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.57.0) (2022-09-28) +===================================================================================================== + +## 🐛 Bug Fixes + * Bump IDB crypto store version ([\#2705](https://github.com/matrix-org/matrix-js-sdk/pull/2705)). + +Changes in [3.56.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.56.0) (2022-09-28) +===================================================================================================== + +## 🔒 Security +* Fix for [CVE-2022-39249](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39249) +* Fix for [CVE-2022-39250](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39250) +* Fix for [CVE-2022-39251](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39251) +* Fix for [CVE-2022-39236](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE%2D2022%2D39236) + +Changes in [3.55.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.55.0) (2022-09-20) +=============================================================================================================== + +## ✨ Features + * Element Call video rooms ([\#9267](https://github.com/matrix-org/matrix-react-sdk/pull/9267)). + * Device manager - rename session ([\#9282](https://github.com/matrix-org/matrix-react-sdk/pull/9282)). + * Allow widgets to read related events ([\#9210](https://github.com/matrix-org/matrix-react-sdk/pull/9210)). Contributed by @dhenneke. + * Device manager - logout of other session ([\#9280](https://github.com/matrix-org/matrix-react-sdk/pull/9280)). + * Device manager - logout current session ([\#9275](https://github.com/matrix-org/matrix-react-sdk/pull/9275)). + * Device manager - verify other devices ([\#9274](https://github.com/matrix-org/matrix-react-sdk/pull/9274)). + * Allow integration managers to remove users ([\#9211](https://github.com/matrix-org/matrix-react-sdk/pull/9211)). + * Device manager - add verify current session button ([\#9252](https://github.com/matrix-org/matrix-react-sdk/pull/9252)). + * Add NotifPanel dot back. ([\#9242](https://github.com/matrix-org/matrix-react-sdk/pull/9242)). Fixes vector-im/element-web#17641. + * Implement MSC3575: Sliding Sync ([\#8328](https://github.com/matrix-org/matrix-react-sdk/pull/8328)). + * Add the clipboard read permission for widgets ([\#9250](https://github.com/matrix-org/matrix-react-sdk/pull/9250)). Contributed by @stefanmuhle. + +## 🐛 Bug Fixes + * Make autocomplete pop-up wider in thread view ([\#9289](https://github.com/matrix-org/matrix-react-sdk/pull/9289)). + * Fix soft crash around inviting invalid MXIDs in start DM on first message flow ([\#9281](https://github.com/matrix-org/matrix-react-sdk/pull/9281)). Fixes matrix-org/element-web-rageshakes#15060 and matrix-org/element-web-rageshakes#15140. + * Fix in-reply-to previews not disappearing when swapping rooms ([\#9278](https://github.com/matrix-org/matrix-react-sdk/pull/9278)). + * Fix invalid instanceof operand window.OffscreenCanvas ([\#9276](https://github.com/matrix-org/matrix-react-sdk/pull/9276)). Fixes vector-im/element-web#23275. + * Fix memory leak caused by unremoved listener ([\#9273](https://github.com/matrix-org/matrix-react-sdk/pull/9273)). + * Fix thumbnail generation when offscreen canvas fails ([\#9272](https://github.com/matrix-org/matrix-react-sdk/pull/9272)). Fixes vector-im/element-web#23265. + * Prevent sliding sync from showing a room under multiple sublists ([\#9266](https://github.com/matrix-org/matrix-react-sdk/pull/9266)). + * Fix tile crash around tooltipify links ([\#9270](https://github.com/matrix-org/matrix-react-sdk/pull/9270)). Fixes vector-im/element-web#23253. + * Device manager - filter out nulled metadatas in device tile properly ([\#9251](https://github.com/matrix-org/matrix-react-sdk/pull/9251)). + * Fix a sliding sync bug which could cause rooms to loop ([\#9268](https://github.com/matrix-org/matrix-react-sdk/pull/9268)). + * Remove the grey gradient on images in bubbles in the timeline ([\#9241](https://github.com/matrix-org/matrix-react-sdk/pull/9241)). Fixes vector-im/element-web#21651. + * Fix html export not including images ([\#9260](https://github.com/matrix-org/matrix-react-sdk/pull/9260)). Fixes vector-im/element-web#22059. + * Fix possible soft crash from a race condition in space hierarchies ([\#9254](https://github.com/matrix-org/matrix-react-sdk/pull/9254)). Fixes matrix-org/element-web-rageshakes#15225. + * Disable all types of autocorrect, -complete, -capitalize, etc on Spotlight's search field ([\#9259](https://github.com/matrix-org/matrix-react-sdk/pull/9259)). + * Handle M_INVALID_USERNAME on /register/available ([\#9237](https://github.com/matrix-org/matrix-react-sdk/pull/9237)). Fixes vector-im/element-web#23161. + * Fix issue with quiet zone around QR code ([\#9243](https://github.com/matrix-org/matrix-react-sdk/pull/9243)). Fixes vector-im/element-web#23199. + +Changes in [3.54.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.54.0) (2022-09-13) +===================================================================================================== + +## ✨ Features + * Device manager - hide unverified security recommendation when only current session is unverified ([\#9228](https://github.com/matrix-org/matrix-react-sdk/pull/9228)). Contributed by @kerryarchibald. + * Device manager - scroll to filtered list from security recommendations ([\#9227](https://github.com/matrix-org/matrix-react-sdk/pull/9227)). Contributed by @kerryarchibald. + * Device manager - updated dropdown style in filtered device list ([\#9226](https://github.com/matrix-org/matrix-react-sdk/pull/9226)). Contributed by @kerryarchibald. + * Device manager - device type and verification icons on device tile ([\#9197](https://github.com/matrix-org/matrix-react-sdk/pull/9197)). Contributed by @kerryarchibald. + +## 🐛 Bug Fixes + * Description of DM room with more than two other people is now being displayed correctly ([\#9231](https://github.com/matrix-org/matrix-react-sdk/pull/9231)). Fixes vector-im/element-web#23094. + * Fix voice messages with multiple composers ([\#9208](https://github.com/matrix-org/matrix-react-sdk/pull/9208)). Fixes vector-im/element-web#23023. Contributed by @grimhilt. + * Fix suggested rooms going missing ([\#9236](https://github.com/matrix-org/matrix-react-sdk/pull/9236)). Fixes vector-im/element-web#23190. + * Fix tooltip infinitely recursing ([\#9235](https://github.com/matrix-org/matrix-react-sdk/pull/9235)). Fixes matrix-org/element-web-rageshakes#15107, matrix-org/element-web-rageshakes#15093 matrix-org/element-web-rageshakes#15092 and matrix-org/element-web-rageshakes#15077. + * Fix plain text export saving ([\#9230](https://github.com/matrix-org/matrix-react-sdk/pull/9230)). Contributed by @jryans. + * Add missing space in SecurityRoomSettingsTab ([\#9222](https://github.com/matrix-org/matrix-react-sdk/pull/9222)). Contributed by @gefgu. + * Make use of js-sdk roomNameGenerator to handle i18n for generated room names ([\#9209](https://github.com/matrix-org/matrix-react-sdk/pull/9209)). Fixes vector-im/element-web#21369. + * Fix progress bar regression throughout the app ([\#9219](https://github.com/matrix-org/matrix-react-sdk/pull/9219)). Fixes vector-im/element-web#23121. + * Reuse empty string & space string logic for event types in devtools ([\#9218](https://github.com/matrix-org/matrix-react-sdk/pull/9218)). Fixes vector-im/element-web#23115. + Changes in [3.53.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.53.0) (2022-08-31) ===================================================================================================== @@ -75,9 +875,6 @@ Changes in [3.52.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/ * Use default styling on nested numbered lists due to MD being sensitive ([\#9110](https://github.com/matrix-org/matrix-react-sdk/pull/9110)). Fixes vector-im/element-web#22935. * Fix replying using chat effect commands ([\#9101](https://github.com/matrix-org/matrix-react-sdk/pull/9101)). Fixes vector-im/element-web#22824. * The first message in a DM can no longer be a sticker. This has been changed to avoid issues with the integration manager. ([\#9180](https://github.com/matrix-org/matrix-react-sdk/pull/9180)). -Added ability to upload room-specific custom emotes in Room Settings. These show up in the room's messages when the shortcode is in the message. -The file fixes were a local issue in which I had to copy the correct version of the files from another folder. Not a part of the emote feature. -Currently emotes are not encrypted and do not show up in autocomplete or the right side emoji panel. I think this could be a start for fully implementing custom emotes. Changes in [3.51.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.51.0) (2022-08-02) ===================================================================================================== diff --git a/src/matrix-react-sdk - Shortcut.lnk b/src/matrix-react-sdk - Shortcut.lnk deleted file mode 100644 index cb57c92d572caad0631ea61e6b0fc0396a6dc407..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1077 zcmb7CT}V@57=F$Yn&rh-CM7yYFfqo)41*d7!N%rAOi3j#&V^$;v(wy9=bVXn5e7x9 z>>^VlqR6nMG9sd)i)7ecS3@b*D2SjNL&Teqg6jDu6Z)a$dw9P0J@5I>^S0cKbj*MD6K&)qFZkK?DubF&3$Vr#MI$+P|2AKXG@dzCkj&p-F79C! zYuiDas8)+4S~s{YPzIHRk~E^qkZxpRF?69CRpcmdahHR?dIf_}kRXyU5Tzf2&M1Qz zAHW9fmuiWyp{c_zO$kQ~mmZA+M>kb3fiT%Ms2GNeUg3L$85gTYLAsKN$LSjgp``FG zLf8NywAn&5)cp7&rU9Qv%fBfj|Jl0++H!A1*5 z6KYb`4J~Y>G``P|TEF##9KNg1PwseKbevy!H}L+-u4U^)Y%RM`JeRBV{S%K-P5!gw zYVGdoUiQ2w`j8FItmSu3>#@Bm^HBm#gp>I9Q0!=d4_?UVrDzI8)ES2;X_{k8GWH`y zQ8OE3)v-^|mu!TqXu`AVc(xL(x7=Pe^^e{7JbQroZDNOLLTFrhQYg_@m4B*X5MUSL zLfLtFQjElf5)ulVB1GjoCn7EFlMB`-Z; Date: Thu, 8 Jun 2023 11:01:09 -0400 Subject: [PATCH 092/176] fix in emoji --- src/emoji.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emoji.ts b/src/emoji.ts index 4c532c9f1b8..1979eb622ce 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -21,9 +21,9 @@ import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json'; export interface IEmoji { label: string; - group: number; + group?: number; hexcode: string; - order: number; + order?: number; shortcodes: string[]; tags?: string[]; unicode: string; From 461c4f46bcff077113b5ba53c69bf928d0ba3e2e Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Jun 2023 11:12:56 -0400 Subject: [PATCH 093/176] cache regex and fix parenthesis issue --- src/HtmlUtils.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index c3189c4df72..f9762b5483d 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -57,6 +57,8 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, "i"); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; +const CUSTOM_EMOTES_REGEX =/:[\w+-]+:/g; + export const PERMITTED_URL_SCHEMES = [ "bitcoin", "ftp", @@ -613,7 +615,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join('') - .replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + .replace(CUSTOM_EMOTES_REGEX, m => opts.emotes[m] ? opts.emotes[m] : m); }; } @@ -680,7 +682,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "mx_EventTile_bigEmoji": emojiBody, "markdown-body": isHtmlMessage && !emojiBody, }); - const tmp = strippedBody?.replace(/:[\w+-]+:/g, m => opts.emotes[m] ? opts.emotes[m] : m); + const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, m => opts.emotes[m] ? opts.emotes[m] : m); if (tmp !== strippedBody) { safeBody=tmp; } @@ -690,7 +692,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } - return safeBody ? + return safeBody ? , op dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> - ) : ( + : {emojiBodyElements || strippedBody} - ); + ; } /** From 3b3d696489a1286121fedc55d2955326842b09b3 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Jun 2023 11:30:44 -0400 Subject: [PATCH 094/176] Add UnstableValue and revert i18n --- src/components/views/messages/TextualBody.tsx | 4 +- .../views/room_settings/RoomEmoteSettings.tsx | 10 +- .../views/rooms/SendMessageComposer.tsx | 7 +- src/i18n/strings/en_EN.json | 762 +++++++++++------- 4 files changed, 505 insertions(+), 278 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 47e81424f24..c36d347caaa 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -49,8 +49,10 @@ import { getParentEventId } from '../../../utils/Reply'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { decryptFile } from '../../../utils/DecryptFile'; import { mediaFromMxc } from '../../../customisations/Media'; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; const MAX_HIGHLIGHT_LENGTH = 4096; +const EMOTES_STATE=new UnstableValue("m.room.emotes","org.matrix.msc3892.emotes") interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. @@ -572,7 +574,7 @@ export default class TextualBody extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.mxEvent.getRoomId()); //TODO: Do not encrypt/decrypt if room is not encrypted - const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE, ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const decryptede=new Map; let durl=""; diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index b11d0c4f0cc..87c66e23979 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -62,14 +62,14 @@ export default class RoomEmoteSettings extends React.Component { if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); //TODO: Do not encrypt/decrypt if room is not encrypted - const emotesEvent = room.currentState.getStateEvents("m.room.emotes", ""); + const emotesEvent = room.currentState.getStateEvents(EMOTES_STATE, ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const value = {}; for (const emote in emotes) { value[emote] = emote; } - const compatEvent = room.currentState.getStateEvents("m.room.clientemote_compatibility", ""); + const compatEvent = room.currentState.getStateEvents(COMPAT_STATE, ""); const compat = compatEvent ? (compatEvent.getContent().isCompat || false) : false; const imagePackEvent = room.currentState.getStateEvents("m.image_pack", ""); @@ -88,7 +88,7 @@ export default class RoomEmoteSettings extends React.Component { newEmoteFile: [], deleted: false, deletedItems: {}, - canAddEmote: room.currentState.maySendStateEvent("m.room.emotes", client.getUserId()), + canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE, client.getUserId()), value: value, compatibility:compat }; @@ -212,7 +212,7 @@ export default class RoomEmoteSettings extends React.Component { } } newState.value = value; - await client.sendStateEvent(this.props.roomId, "m.room.emotes", emotesMxcs, ""); + await client.sendStateEvent(this.props.roomId, EMOTES_STATE, emotesMxcs, ""); this.imagePack=newPack await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); @@ -294,7 +294,7 @@ export default class RoomEmoteSettings extends React.Component { private onCompatChange = async (allowed: boolean) => { const client = MatrixClientPeg.get(); - await client.sendStateEvent(this.props.roomId, "m.room.clientemote_compatibility", { isCompat: allowed }, ""); + await client.sendStateEvent(this.props.roomId, COMPAT_STATE, { isCompat: allowed }, ""); if (allowed) { for (const shortcode in this.state.emotes) { diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 01bfb6cacd0..f6964d6295e 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -23,6 +23,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { Room } from "matrix-js-sdk/src/models/room"; import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import dis from "../../../dispatcher/dispatcher"; import EditorModel from "../../../editor/model"; @@ -76,6 +77,10 @@ import { getBlobSafeMimeType } from "../../../utils/blobs"; * @param replyToEvent - The event being replied to or undefined if it is not a reply. * @param editedContent - The content of the parent event being edited. */ + +const COMPAT_STATE=new UnstableValue("m.room.clientemote_compatibility","org.matrix.msc3892.clientemote_compatibility") + + export function attachMentions( sender: string, content: IContent, @@ -305,7 +310,7 @@ export class SendMessageComposer extends React.ComponentCommunities are no longer supported and have been replaced by spaces.Learn more about spaces here.": "You're trying to access a community link (%(groupId)s).
    Communities are no longer supported and have been replaced by spaces.Learn more about spaces here.", + "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss": "%(days)sd %(hours)sh %(minutes)sm %(seconds)ss", + "%(hours)sh %(minutes)sm %(seconds)ss": "%(hours)sh %(minutes)sm %(seconds)ss", + "%(minutes)sm %(seconds)ss": "%(minutes)sm %(seconds)ss", "Identity server has no terms of service": "Identity server has no terms of service", "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", "Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.", @@ -66,8 +75,8 @@ "The call was answered on another device.": "The call was answered on another device.", "Call failed due to misconfigured server": "Call failed due to misconfigured server", "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.": "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", - "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at turn.matrix.org, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", - "Try using turn.matrix.org": "Try using turn.matrix.org", + "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", + "Try using %(server)s": "Try using %(server)s", "OK": "OK", "Unable to access microphone": "Unable to access microphone", "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", @@ -95,8 +104,10 @@ "We couldn't log you in": "We couldn't log you in", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", "Try again": "Try again", - "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.", - "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.": "Your homeserver rejected your log in attempt. This could be due to things just taking too long. Please try again. If this continues, please contact your homeserver administrator.", + "User is not logged in": "User is not logged in", + "Database unexpectedly closed": "Database unexpectedly closed", + "This may be caused by having the app open in multiple tabs or due to clearing browser data.": "This may be caused by having the app open in multiple tabs or due to clearing browser data.", + "Reload": "Reload", "Empty room": "Empty room", "%(user1)s and %(user2)s": "%(user1)s and %(user2)s", "%(user)s and %(count)s others|other": "%(user)s and %(count)s others", @@ -105,12 +116,14 @@ "Inviting %(user)s and %(count)s others|other": "Inviting %(user)s and %(count)s others", "Inviting %(user)s and %(count)s others|one": "Inviting %(user)s and 1 other", "Empty room (was %(oldName)s)": "Empty room (was %(oldName)s)", + "Default Device": "Default Device", "%(name)s is requesting verification": "%(name)s is requesting verification", + "%(senderName)s started a voice broadcast": "%(senderName)s started a voice broadcast", "%(brand)s does not have permission to send you notifications - please check your browser settings": "%(brand)s does not have permission to send you notifications - please check your browser settings", "%(brand)s was not given permission to send notifications - please try again": "%(brand)s was not given permission to send notifications - please try again", "Unable to enable Notifications": "Unable to enable Notifications", "This email address was not found": "This email address was not found", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", + "Your email address does not appear to be associated with a Matrix ID on this homeserver.": "Your email address does not appear to be associated with a Matrix ID on this homeserver.", "United Kingdom": "United Kingdom", "United States": "United States", "Afghanistan": "Afghanistan", @@ -361,9 +374,10 @@ "Zambia": "Zambia", "Zimbabwe": "Zimbabwe", "Sign In or Create Account": "Sign In or Create Account", + "Sign In": "Sign In", "Use your account or create a new one to continue.": "Use your account or create a new one to continue.", + "Use your account to continue.": "Use your account to continue.", "Create Account": "Create Account", - "Sign In": "Sign In", "Default": "Default", "Restricted": "Restricted", "Moderator": "Moderator", @@ -376,6 +390,7 @@ "Some invites couldn't be sent": "Some invites couldn't be sent", "You need to be logged in.": "You need to be logged in.", "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", + "You need to be able to kick users to do that.": "You need to be able to kick users to do that.", "Unable to create widget.": "Unable to create widget.", "Missing roomId.": "Missing roomId.", "Failed to send request.": "Failed to send request.", @@ -383,6 +398,8 @@ "Power level must be positive integer.": "Power level must be positive integer.", "You are not in this room.": "You are not in this room.", "You do not have permission to do that in this room.": "You do not have permission to do that in this room.", + "Failed to send event": "Failed to send event", + "Failed to read events": "Failed to read events", "Missing room_id in request": "Missing room_id in request", "Room %(roomId)s not visible": "Room %(roomId)s not visible", "Missing user_id in request": "Missing user_id in request", @@ -424,6 +441,7 @@ "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.": "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", "Continue": "Continue", "Use an identity server to invite by email. Manage in Settings.": "Use an identity server to invite by email. Manage in Settings.", + "User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility": "User (%(user)s) did not end up as invited to %(roomId)s but no error was given from the inviter utility", "Joins room with given address": "Joins room with given address", "Leave room": "Leave room", "Unrecognised room address: %(roomAlias)s": "Unrecognised room address: %(roomAlias)s", @@ -443,12 +461,13 @@ "Opens the Developer Tools dialog": "Opens the Developer Tools dialog", "Adds a custom widget by URL to the room": "Adds a custom widget by URL to the room", "Please supply a widget URL or embed code": "Please supply a widget URL or embed code", + "iframe has no src attribute": "iframe has no src attribute", "Please supply a https:// or http:// widget URL": "Please supply a https:// or http:// widget URL", "You cannot modify widgets in this room.": "You cannot modify widgets in this room.", "Verifies a user, session, and pubkey tuple": "Verifies a user, session, and pubkey tuple", "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)": "Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", "Session already verified!": "Session already verified!", - "WARNING: Session already verified, but keys do NOT MATCH!": "WARNING: Session already verified, but keys do NOT MATCH!", + "WARNING: session already verified, but keys do NOT MATCH!": "WARNING: session already verified, but keys do NOT MATCH!", "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", "Verified key": "Verified key", "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.", @@ -468,9 +487,12 @@ "No active call in this room": "No active call in this room", "Takes the call in the current room off hold": "Takes the call in the current room off hold", "Converts the room to a DM": "Converts the room to a DM", + "Could not find room": "Could not find room", "Converts the DM to a room": "Converts the DM to a room", "Displays action": "Displays action", "Someone": "Someone", + "Video call started in %(roomName)s.": "Video call started in %(roomName)s.", + "Video call started in %(roomName)s. (not supported by this browser)": "Video call started in %(roomName)s. (not supported by this browser)", "%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)", "%(senderName)s placed a video call.": "%(senderName)s placed a video call.", @@ -480,6 +502,7 @@ "%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s", "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s banned %(targetName)s: %(reason)s", "%(senderName)s banned %(targetName)s": "%(senderName)s banned %(targetName)s", + "%(oldDisplayName)s changed their display name and profile picture": "%(oldDisplayName)s changed their display name and profile picture", "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s changed their display name to %(displayName)s", "%(senderName)s set their display name to %(displayName)s": "%(senderName)s set their display name to %(displayName)s", "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removed their display name (%(oldDisplayName)s)", @@ -635,6 +658,38 @@ "Send %(msgtype)s messages as you in your active room": "Send %(msgtype)s messages as you in your active room", "See %(msgtype)s messages posted to this room": "See %(msgtype)s messages posted to this room", "See %(msgtype)s messages posted to your active room": "See %(msgtype)s messages posted to your active room", + "Can't start a new voice broadcast": "Can't start a new voice broadcast", + "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.", + "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.", + "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.", + "Connection error": "Connection error", + "Unfortunately we're unable to start a recording right now. Please try again later.": "Unfortunately we're unable to start a recording right now. Please try again later.", + "Can’t start a call": "Can’t start a call", + "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.", + "You ended a voice broadcast": "You ended a voice broadcast", + "%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast", + "You ended a voice broadcast": "You ended a voice broadcast", + "%(senderName)s ended a voice broadcast": "%(senderName)s ended a voice broadcast", + "Unable to decrypt voice broadcast": "Unable to decrypt voice broadcast", + "Unable to play this voice broadcast": "Unable to play this voice broadcast", + "Stop live broadcasting?": "Stop live broadcasting?", + "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.", + "Yes, stop broadcast": "Yes, stop broadcast", + "Listen to live broadcast?": "Listen to live broadcast?", + "If you start listening to this live broadcast, your current live broadcast recording will be ended.": "If you start listening to this live broadcast, your current live broadcast recording will be ended.", + "Yes, end my recording": "Yes, end my recording", + "No": "No", + "30s backward": "30s backward", + "30s forward": "30s forward", + "Go live": "Go live", + "resume voice broadcast": "resume voice broadcast", + "pause voice broadcast": "pause voice broadcast", + "Change input device": "Change input device", + "Live": "Live", + "Voice broadcast": "Voice broadcast", + "Buffering…": "Buffering…", + "play voice broadcast": "play voice broadcast", + "Connection error - Recording paused": "Connection error - Recording paused", "Cannot reach homeserver": "Cannot reach homeserver", "Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin", "Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured", @@ -649,9 +704,14 @@ "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has been blocked by its administrator.": "This homeserver has been blocked by its administrator.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", - "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", - "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", - "Attachment": "Attachment", + "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", + "Unable to connect to Homeserver. Retrying…": "Unable to connect to Homeserver. Retrying…", + "This account has been deactivated.": "This account has been deactivated.", + "Incorrect username and/or password.": "Incorrect username and/or password.", + "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", + "There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.", + "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", @@ -725,19 +785,15 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", - "Unnamed room": "Unnamed room", - "Unable to join network": "Unable to join network", - "%(brand)s does not know how to join a room on this network": "%(brand)s does not know how to join a room on this network", - "Room not found": "Room not found", - "Couldn't find a matching Matrix room": "Couldn't find a matching Matrix room", - "Fetching third party location failed": "Fetching third party location failed", - "Unable to look up room ID from server": "Unable to look up room ID from server", "Error upgrading room": "Error upgrading room", "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Share your public space": "Share your public space", "Unknown App": "Unknown App", + "No media permissions": "No media permissions", + "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "This homeserver is not configured to display maps.": "This homeserver is not configured to display maps.", + "WebGL is required to display maps, please enable it in your browser settings.": "WebGL is required to display maps, please enable it in your browser settings.", "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.": "This homeserver is not configured correctly to display maps, or the configured map server may be unreachable.", "Toggle attribution": "Toggle attribution", "Map feedback": "Map feedback", @@ -749,7 +805,12 @@ "Reset bearing to north": "Reset bearing to north", "Zoom in": "Zoom in", "Zoom out": "Zoom out", + "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.", + "Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.", + "Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.", + "Unknown error fetching location. Please try again later.": "Unknown error fetching location. Please try again later.", "Are you sure you want to exit during this export?": "Are you sure you want to exit during this export?", + "Unnamed Room": "Unnamed Room", "Generating a ZIP": "Generating a ZIP", "Fetched %(count)s events out of %(total)s|other": "Fetched %(count)s events out of %(total)s", "Fetched %(count)s events out of %(total)s|one": "Fetched %(count)s event out of %(total)s", @@ -768,17 +829,17 @@ "Topic: %(topic)s": "Topic: %(topic)s", "Error fetching file": "Error fetching file", "Processing event %(number)s out of %(total)s": "Processing event %(number)s out of %(total)s", - "Starting export...": "Starting export...", + "Starting export…": "Starting export…", "Fetched %(count)s events in %(seconds)ss|other": "Fetched %(count)s events in %(seconds)ss", "Fetched %(count)s events in %(seconds)ss|one": "Fetched %(count)s event in %(seconds)ss", - "Creating HTML...": "Creating HTML...", + "Creating HTML…": "Creating HTML…", "Export successful!": "Export successful!", "Exported %(count)s events in %(seconds)s seconds|other": "Exported %(count)s events in %(seconds)s seconds", "Exported %(count)s events in %(seconds)s seconds|one": "Exported %(count)s event in %(seconds)s seconds", "File Attached": "File Attached", - "Starting export process...": "Starting export process...", - "Fetching events...": "Fetching events...", - "Creating output...": "Creating output...", + "Starting export process…": "Starting export process…", + "Fetching events…": "Fetching events…", + "Creating output…": "Creating output…", "Enable": "Enable", "That's fine": "That's fine", "Stop": "Stop", @@ -786,22 +847,27 @@ "Learn more": "Learn more", "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More": "Share anonymous data to help us identify issues. Nothing personal. No third parties. Learn More", "Yes": "Yes", - "No": "No", "Help improve %(analyticsOwner)s": "Help improve %(analyticsOwner)s", - "You have unverified logins": "You have unverified logins", + "You have unverified sessions": "You have unverified sessions", "Review to ensure your account is safe": "Review to ensure your account is safe", "Review": "Review", "Later": "Later", "Don't miss a reply": "Don't miss a reply", "Notifications": "Notifications", "Enable desktop notifications": "Enable desktop notifications", + "Join": "Join", + "Unknown room": "Unknown room", + "Video call started": "Video call started", + "Video": "Video", + "Close": "Close", + "Sound on": "Sound on", + "Silence call": "Silence call", + "Notifications silenced": "Notifications silenced", "Unknown caller": "Unknown caller", "Voice call": "Voice call", "Video call": "Video call", "Decline": "Decline", "Accept": "Accept", - "Sound on": "Sound on", - "Silence call": "Silence call", "Use app for a better experience": "Use app for a better experience", "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.": "%(brand)s is experimental on a mobile web browser. For a better experience and the latest features, use our free native app.", "Use app": "Use app", @@ -818,8 +884,7 @@ "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data", "Other users may not trust it": "Other users may not trust it", "New login. Was this you?": "New login. Was this you?", - "%(deviceId)s from %(ip)s": "%(deviceId)s from %(ip)s", - "Check your devices": "Check your devices", + "Yes, it was me": "Yes, it was me", "What's new?": "What's new?", "What's New": "What's New", "Update": "Update", @@ -831,6 +896,8 @@ "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "The person who invited you has already left.": "The person who invited you has already left.", "The person who invited you has already left, or their server is offline.": "The person who invited you has already left, or their server is offline.", + "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.": "You attempted to join using a room ID without providing a list of servers to join through. Room IDs are internal identifiers and cannot be used to join a room without additional information.", + "If you know a room address, try joining through that instead.": "If you know a room address, try joining through that instead.", "Failed to join": "Failed to join", "Connection lost": "Connection lost", "You were disconnected from the call. (Error: %(message)s)": "You were disconnected from the call. (Error: %(message)s)", @@ -851,22 +918,29 @@ "%(senderName)s is calling": "%(senderName)s is calling", "* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s", "%(senderName)s: %(message)s": "%(senderName)s: %(message)s", - "%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s", + "You reacted %(reaction)s to %(message)s": "You reacted %(reaction)s to %(message)s", + "%(sender)s reacted %(reaction)s to %(message)s": "%(sender)s reacted %(reaction)s to %(message)s", "%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s", "Threads": "Threads", "Back to chat": "Back to chat", "Room information": "Room information", "Room members": "Room members", "Back to thread": "Back to thread", + "None": "None", + "Bold": "Bold", + "Grey": "Grey", + "Red": "Red", + "Unsent": "Unsent", + "unknown": "unknown", "Change notification settings": "Change notification settings", "Messaging": "Messaging", "Profile": "Profile", "Spaces": "Spaces", "Widgets": "Widgets", "Rooms": "Rooms", + "Voice & Video": "Voice & Video", "Moderation": "Moderation", "Analytics": "Analytics", - "Message Previews": "Message Previews", "Themes": "Themes", "Encryption": "Encryption", "Experimental": "Experimental", @@ -880,42 +954,50 @@ "Yes, the chat timeline is displayed alongside the video.": "Yes, the chat timeline is displayed alongside the video.", "Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.", "Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog", + "Requires your server to support the stable version of MSC3827": "Requires your server to support the stable version of MSC3827", "Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.", - "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators", + "Report to moderators": "Report to moderators", + "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Message Pinning": "Message Pinning", - "Threaded messaging": "Threaded messaging", - "Keep discussions organised with threads.": "Keep discussions organised with threads.", - "Threads help keep conversations on-topic and easy to track. Learn more.": "Threads help keep conversations on-topic and easy to track. Learn more.", - "How can I start a thread?": "How can I start a thread?", - "Use “%(replyInThread)s” when hovering over a message.": "Use “%(replyInThread)s” when hovering over a message.", - "Reply in thread": "Reply in thread", - "How can I leave the beta?": "How can I leave the beta?", - "To leave, return to this page and use the “%(leaveTheBeta)s” button.": "To leave, return to this page and use the “%(leaveTheBeta)s” button.", - "Leave the beta": "Leave the beta", + "Rich text editor": "Rich text editor", + "Use rich text instead of Markdown in the message composer.": "Use rich text instead of Markdown in the message composer.", "Render simple counters in room header": "Render simple counters in room header", - "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", + "New ways to ignore people": "New ways to ignore people", + "Currently experimental.": "Currently experimental.", "Support adding custom themes": "Support adding custom themes", - "Show message previews for reactions in DMs": "Show message previews for reactions in DMs", - "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", - "Show extensible event representation of events": "Show extensible event representation of events", "Show current avatar and name for users in message history": "Show current avatar and name for users in message history", "Show HTML representation of room topics": "Show HTML representation of room topics", "Show info about bridges in room settings": "Show info about bridges in room settings", "Use new room breadcrumbs": "Use new room breadcrumbs", - "Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)", + "Right panel stays open": "Right panel stays open", + "Defaults to room member list.": "Defaults to room member list.", "Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)", + "Requires your server to support MSC3030": "Requires your server to support MSC3030", "Send read receipts": "Send read receipts", - "Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)", - "Favourite Messages (under active development)": "Favourite Messages (under active development)", - "Use new session manager (under active development)": "Use new session manager (under active development)", + "Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.", + "Sliding Sync mode": "Sliding Sync mode", + "Under active development, cannot be disabled.": "Under active development, cannot be disabled.", + "Element Call video rooms": "Element Call video rooms", + "New group call experience": "New group call experience", + "Live Location Sharing": "Live Location Sharing", + "Temporary implementation. Locations persist in room history.": "Temporary implementation. Locations persist in room history.", + "Dynamic room predecessors": "Dynamic room predecessors", + "Enable MSC3946 (to support late-arriving room archives)": "Enable MSC3946 (to support late-arriving room archives)", + "Favourite Messages": "Favourite Messages", + "Under active development.": "Under active development.", + "Force 15s voice broadcast chunk length": "Force 15s voice broadcast chunk length", + "Enable new native OIDC flows (Under active development)": "Enable new native OIDC flows (Under active development)", + "Rust cryptography implementation": "Rust cryptography implementation", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Show stickers button": "Show stickers button", "Show polls button": "Show polls button", "Insert a trailing colon after user mentions at the start of a message": "Insert a trailing colon after user mentions at the start of a message", + "Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)", + "Enable intentional mentions": "Enable intentional mentions", "Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)", @@ -930,7 +1012,7 @@ "Expand code blocks by default": "Expand code blocks by default", "Show line numbers in code blocks": "Show line numbers in code blocks", "Jump to the bottom of the timeline when you send a message": "Jump to the bottom of the timeline when you send a message", - "Show avatars in user and room mentions": "Show avatars in user and room mentions", + "Show avatars in user, room and event mentions": "Show avatars in user, room and event mentions", "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", @@ -941,13 +1023,19 @@ "Surround selected text when typing special characters": "Surround selected text when typing special characters", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Enable Markdown": "Enable Markdown", - "Start messages with /plain to send without markdown and /md to send with.": "Start messages with /plain to send without markdown and /md to send with.", + "Start messages with /plain to send without markdown.": "Start messages with /plain to send without markdown.", "Mirror local video feed": "Mirror local video feed", "Match system theme": "Match system theme", "Use a system font": "Use a system font", "System font name": "System font name", - "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)", + "Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls", + "When enabled, the other party might be able to see your IP address": "When enabled, the other party might be able to see your IP address", + "Automatic gain control": "Automatic gain control", + "Echo cancellation": "Echo cancellation", + "Noise suppression": "Noise suppression", + "Show NSFW content": "Show NSFW content", "Send analytics data": "Send analytics data", + "Record the client name, version, and url to recognise sessions more easily in session manager": "Record the client name, version, and url to recognise sessions more easily in session manager", "Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session", "Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session", "Enable inline URL previews by default": "Enable inline URL previews by default", @@ -955,13 +1043,12 @@ "Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", - "Order rooms by name": "Order rooms by name", - "Show rooms with unread notifications first": "Show rooms with unread notifications first", "Show shortcuts to recently viewed rooms above the room list": "Show shortcuts to recently viewed rooms above the room list", "Show shortcut to welcome checklist above the room list": "Show shortcut to welcome checklist above the room list", "Show hidden events in timeline": "Show hidden events in timeline", - "Low bandwidth mode (requires compatible homeserver)": "Low bandwidth mode (requires compatible homeserver)", - "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)": "Allow fallback call assist server turn.matrix.org when your homeserver does not offer one (your IP address would be shared during a call)", + "Low bandwidth mode": "Low bandwidth mode", + "Requires compatible homeserver.": "Requires compatible homeserver.", + "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.": "Only applies if your homeserver does not offer one. Your IP address would be shared during a call.", "Show previews/thumbnails for images": "Show previews/thumbnails for images", "Enable message search in encrypted rooms": "Enable message search in encrypted rooms", "How fast should messages be downloaded.": "How fast should messages be downloaded.", @@ -979,10 +1066,8 @@ "Always show the window menu bar": "Always show the window menu bar", "Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close", "Enable hardware acceleration": "Enable hardware acceleration", - "Partial Support for Threads": "Partial Support for Threads", - "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.": "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. Learn more.", - "Do you want to enable threads anyway?": "Do you want to enable threads anyway?", - "Yes, enable": "Yes, enable", + "Can currently only be enabled via config.json": "Can currently only be enabled via config.json", + "Log out and back in to disable": "Log out and back in to disable", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading logs": "Uploading logs", @@ -1001,6 +1086,10 @@ "When rooms are upgraded": "When rooms are upgraded", "My Ban List": "My Ban List", "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", + "Connecting": "Connecting", + "Sorry — this call is currently full": "Sorry — this call is currently full", + "User": "User", + "Room": "Room", "Create account": "Create account", "You made it!": "You made it!", "Find and invite your friends": "Find and invite your friends", @@ -1039,17 +1128,15 @@ "You can use /help to list available commands. Did you mean to send this as a message?": "You can use /help to list available commands. Did you mean to send this as a message?", "Hint: Begin your message with // to start it with a slash.": "Hint: Begin your message with // to start it with a slash.", "Send as message": "Send as message", - "%(count)s people joined|other": "%(count)s people joined", - "%(count)s people joined|one": "%(count)s person joined", + "Failed to download source media, no source url was found": "Failed to download source media, no source url was found", "Audio devices": "Audio devices", - "Audio input %(n)s": "Audio input %(n)s", "Mute microphone": "Mute microphone", "Unmute microphone": "Unmute microphone", "Video devices": "Video devices", - "Video input %(n)s": "Video input %(n)s", "Turn off camera": "Turn off camera", "Turn on camera": "Turn on camera", - "Join": "Join", + "%(count)s people joined|other": "%(count)s people joined", + "%(count)s people joined|one": "%(count)s person joined", "Dial": "Dial", "You are presenting": "You are presenting", "%(sharerName)s is presenting": "%(sharerName)s is presenting", @@ -1060,7 +1147,6 @@ "You held the call Switch": "You held the call Switch", "You held the call Resume": "You held the call Resume", "%(peerName)s held the call": "%(peerName)s held the call", - "Connecting": "Connecting", "Dialpad": "Dialpad", "Mute the microphone": "Mute the microphone", "Unmute the microphone": "Unmute the microphone", @@ -1072,7 +1158,7 @@ "Show sidebar": "Show sidebar", "More": "More", "Hangup": "Hangup", - "Fill Screen": "Fill Screen", + "Fill screen": "Fill screen", "Pin": "Pin", "Return to call": "Return to call", "%(name)s on hold": "%(name)s on hold", @@ -1158,9 +1244,6 @@ "Headphones": "Headphones", "Folder": "Folder", "Welcome": "Welcome", - "How are you finding %(brand)s so far?": "How are you finding %(brand)s so far?", - "We’d appreciate any feedback on how you’re finding %(brand)s.": "We’d appreciate any feedback on how you’re finding %(brand)s.", - "Feedback": "Feedback", "Secure messaging for friends and family": "Secure messaging for friends and family", "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.": "With free end-to-end encrypted messaging, and unlimited voice and video calls, %(brand)s is a great way to stay in touch.", "Start your first chat": "Start your first chat", @@ -1175,7 +1258,10 @@ "You did it!": "You did it!", "Complete these to get the most out of %(brand)s": "Complete these to get the most out of %(brand)s", "Your server isn't responding to some requests.": "Your server isn't responding to some requests.", - "Decline (%(counter)s)": "Decline (%(counter)s)", + "%(deviceId)s from %(ip)s": "%(deviceId)s from %(ip)s", + "Ignore": "Ignore", + "Ignore (%(counter)s)": "Ignore (%(counter)s)", + "Verify Session": "Verify Session", "Accept to continue:": "Accept to continue:", "Quick settings": "Quick settings", "All settings": "All settings", @@ -1195,10 +1281,6 @@ "No results": "No results", "Search %(spaceName)s": "Search %(spaceName)s", "Please enter a name for the space": "Please enter a name for the space", - "Spaces are a new feature.": "Spaces are a new feature.", - "Spaces feedback": "Spaces feedback", - "Thank you for trying Spaces. Your feedback will help inform the next versions.": "Thank you for trying Spaces. Your feedback will help inform the next versions.", - "Give feedback.": "Give feedback.", "e.g. my-space": "e.g. my-space", "Address": "Address", "Create a space": "Create a space", @@ -1213,7 +1295,7 @@ "Your private space": "Your private space", "Add some details to help people recognise it.": "Add some details to help people recognise it.", "You can change these anytime.": "You can change these anytime.", - "Creating...": "Creating...", + "Creating…": "Creating…", "Create": "Create", "Show all rooms": "Show all rooms", "Options": "Options", @@ -1228,7 +1310,7 @@ "Failed to save space settings.": "Failed to save space settings.", "General": "General", "Edit settings relating to your space.": "Edit settings relating to your space.", - "Saving...": "Saving...", + "Saving…": "Saving…", "Save Changes": "Save Changes", "Leave Space": "Leave Space", "Failed to update the guest access of this space": "Failed to update the guest access of this space", @@ -1248,19 +1330,23 @@ "Jump to first unread room.": "Jump to first unread room.", "Jump to first invite.": "Jump to first invite.", "Space options": "Space options", + "Failed to change power level": "Failed to change power level", + "Add privileged users": "Add privileged users", + "Give one or multiple users in this room more privileges": "Give one or multiple users in this room more privileges", + "Search users in this room…": "Search users in this room…", + "Apply": "Apply", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", "Workspace: ": "Workspace: ", "Channel: ": "Channel: ", - "Failed to upload profile picture!": "Failed to upload profile picture!", - "Upload new:": "Upload new:", "No display name": "No display name", "Warning!": "Warning!", "Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable.": "Changing your password on this homeserver will cause all of your other devices to be signed out. This will delete the message encryption keys stored on them, and may make encrypted chat history unreadable.", "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.": "If you want to retain access to your chat history in encrypted rooms you should first export your room keys and re-import them afterwards.", "You can also ask your homeserver admin to upgrade the server to change this behaviour.": "You can also ask your homeserver admin to upgrade the server to change this behaviour.", "Export E2E room keys": "Export E2E room keys", + "Error while changing password: %(error)s": "Error while changing password: %(error)s", "New passwords don't match": "New passwords don't match", "Passwords can't be empty": "Passwords can't be empty", "Do you want to set an email address?": "Do you want to set an email address?", @@ -1293,21 +1379,6 @@ "Cryptography": "Cryptography", "Session ID:": "Session ID:", "Session key:": "Session key:", - "Your homeserver does not support device management.": "Your homeserver does not support device management.", - "Unable to load device list": "Unable to load device list", - "Deselect all": "Deselect all", - "Select all": "Select all", - "Verified devices": "Verified devices", - "Unverified devices": "Unverified devices", - "Devices without encryption support": "Devices without encryption support", - "Sign out %(count)s selected devices|other": "Sign out %(count)s selected devices", - "Sign out %(count)s selected devices|one": "Sign out %(count)s selected device", - "You aren't signed into any other devices.": "You aren't signed into any other devices.", - "This device": "This device", - "Failed to set display name": "Failed to set display name", - "Sign Out": "Sign Out", - "Display Name": "Display Name", - "Rename": "Rename", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(rooms)s room.", @@ -1322,7 +1393,7 @@ "Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt", "Image size in the timeline": "Image size in the timeline", "Large": "Large", - "Connecting to integration manager...": "Connecting to integration manager...", + "Connecting to integration manager…": "Connecting to integration manager…", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", "Integration manager": "Integration manager", @@ -1354,35 +1425,38 @@ "Messages containing keywords": "Messages containing keywords", "Error saving notification preferences": "Error saving notification preferences", "An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.", - "Enable for this account": "Enable for this account", + "Enable notifications for this account": "Enable notifications for this account", + "Turn off to disable notifications on all your devices and sessions": "Turn off to disable notifications on all your devices and sessions", "Enable email notifications for %(email)s": "Enable email notifications for %(email)s", + "Enable notifications for this device": "Enable notifications for this device", "Enable desktop notifications for this session": "Enable desktop notifications for this session", "Show message in desktop notification": "Show message in desktop notification", "Enable audible notifications for this session": "Enable audible notifications for this session", - "Clear notifications": "Clear notifications", + "Mark all as read": "Mark all as read", "Keyword": "Keyword", "New keyword": "New keyword", "On": "On", "Off": "Off", "Noisy": "Noisy", + "An error occurred when updating your notification preferences. Please try to toggle your option again.": "An error occurred when updating your notification preferences. Please try to toggle your option again.", "Global": "Global", "Mentions & keywords": "Mentions & keywords", "Notification targets": "Notification targets", "There was an error loading your notification settings.": "There was an error loading your notification settings.", "Failed to save your profile": "Failed to save your profile", "The operation could not be completed": "The operation could not be completed", - "Upgrade to your own domain": "Upgrade to your own domain", + "Display Name": "Display Name", "Profile picture": "Profile picture", "Save": "Save", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Unable to load key backup status": "Unable to load key backup status", "Restore from Backup": "Restore from Backup", - "This session is backing up your keys. ": "This session is backing up your keys. ", + "This session is backing up your keys.": "This session is backing up your keys.", "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.", "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.": "Connect this session to key backup before signing out to avoid losing any keys that may only be on this session.", "Connect this session to Key Backup": "Connect this session to Key Backup", - "Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...", + "Backing up %(sessionsRemaining)s keys…": "Backing up %(sessionsRemaining)s keys…", "All keys backed up": "All keys backed up", "Backup has a valid signature from this user": "Backup has a valid signature from this user", "Backup has a invalid signature from this user": "Backup has a invalid signature from this user", @@ -1453,34 +1527,42 @@ "Custom theme URL": "Custom theme URL", "Add theme": "Add theme", "Error encountered (%(errorDetail)s).": "Error encountered (%(errorDetail)s).", - "Checking for an update...": "Checking for an update...", + "Checking for an update…": "Checking for an update…", "No update available.": "No update available.", - "Downloading update...": "Downloading update...", + "Downloading update…": "Downloading update…", "New version available. Update now.": "New version available. Update now.", "Check for update": "Check for update", "Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.", "Customise your appearance": "Customise your appearance", "Appearance Settings only affect this %(brand)s session.": "Appearance Settings only affect this %(brand)s session.", + "Unknown password change error (%(stringifiedError)s)": "Unknown password change error (%(stringifiedError)s)", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", + "%(errorMessage)s (HTTP status %(httpStatus)s)": "%(errorMessage)s (HTTP status %(httpStatus)s)", + "Error changing password": "Error changing password", "Your password was successfully changed.": "Your password was successfully changed.", "You will not receive push notifications on other devices until you sign back in to them.": "You will not receive push notifications on other devices until you sign back in to them.", "Success": "Success", "Email addresses": "Email addresses", "Phone numbers": "Phone numbers", - "Set a new account password...": "Set a new account password...", + "Set a new account password…": "Set a new account password…", + "Your account details are managed separately at %(hostname)s.": "Your account details are managed separately at %(hostname)s.", + "Manage account": "Manage account", "Account": "Account", "Language and region": "Language and region", "Spell check": "Spell check", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", + "Deactivate account": "Deactivate account", "Account management": "Account management", "Deactivating your account is a permanent action — be careful!": "Deactivating your account is a permanent action — be careful!", "Deactivate Account": "Deactivate Account", - "Deactivate account": "Deactivate account", "Discovery": "Discovery", "%(brand)s version:": "%(brand)s version:", "Olm version:": "Olm version:", "Legal": "Legal", "Credits": "Credits", + "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.": "The default cover photo is © Jesús Roncero used under the terms of CC-BY-SA 4.0.", + "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0.": "The twemoji-colr font is © Mozilla Foundation used under the terms of Apache 2.0.", + "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.": "The Twemoji emoji art is © Twitter, Inc and other contributors used under the terms of CC-BY 4.0.", "For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "For help with using %(brand)s, click here or start a chat with our bot using the button below.", "Chat with %(brand)s Bot": "Chat with %(brand)s Bot", @@ -1493,14 +1575,16 @@ "FAQ": "FAQ", "Keyboard Shortcuts": "Keyboard Shortcuts", "Versions": "Versions", - "Homeserver is": "Homeserver is", - "Identity server is": "Identity server is", + "Homeserver is %(homeserverUrl)s": "Homeserver is %(homeserverUrl)s", + "Identity server is %(identityServerUrl)s": "Identity server is %(identityServerUrl)s", "Access Token": "Access Token", "Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.", "Clear cache and reload": "Clear cache and reload", "Keyboard": "Keyboard", - "Labs": "Labs", - "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.", + "Upcoming features": "Upcoming features", + "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.": "What's next for %(brand)s? Labs are the best way to get things early, test out new features and help shape them before they actually launch.", + "Early previews": "Early previews", + "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.": "Feeling experimental? Try out our latest ideas in development. These features are not finalised; they may be unstable, may change, or may be dropped altogether. Learn more.", "Ignored/Blocked": "Ignored/Blocked", "Error adding ignored user/server": "Error adding ignored user/server", "Something went wrong. Please try again or view your console for hints.": "Something went wrong. Please try again or view your console for hints.", @@ -1509,11 +1593,9 @@ "Error removing ignored user/server": "Error removing ignored user/server", "Error unsubscribing from list": "Error unsubscribing from list", "Please try again or view your console for hints.": "Please try again or view your console for hints.", - "None": "None", "Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s", "Server rules": "Server rules", "User rules": "User rules", - "Close": "Close", "You have not ignored anyone.": "You have not ignored anyone.", "You are currently ignoring:": "You are currently ignoring:", "You are not subscribed to any lists": "You are not subscribed to any lists", @@ -1525,10 +1607,9 @@ "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.": "Add users and servers you want to ignore here. Use asterisks to have %(brand)s match any characters. For example, @bot:* would ignore all users that have the name 'bot' on any server.", "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.": "Ignoring people is done through ban lists which contain rules for who to ban. Subscribing to a ban list means the users/servers blocked by that list will be hidden from you.", "Personal ban list": "Personal ban list", - "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named 'My Ban List' - stay in this room to keep the ban list in effect.", + "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named '%(myBanList)s' - stay in this room to keep the ban list in effect.": "Your personal ban list holds all the users/servers you personally don't want to see messages from. After ignoring your first user/server, a new room will show up in your room list named '%(myBanList)s' - stay in this room to keep the ban list in effect.", "Server or user ID to ignore": "Server or user ID to ignore", "eg: @bot:* or example.org": "eg: @bot:* or example.org", - "Ignore": "Ignore", "Subscribed lists": "Subscribed lists", "Subscribing to a ban list will cause you to join it!": "Subscribing to a ban list will cause you to join it!", "If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.", @@ -1541,11 +1622,11 @@ "Displaying time": "Displaying time", "Presence": "Presence", "Share your activity and status with others.": "Share your activity and status with others.", - "Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.", "Composer": "Composer", "Code blocks": "Code blocks", "Images, GIFs and videos": "Images, GIFs and videos", "Timeline": "Timeline", + "Room directory": "Room directory", "Enable hardware acceleration (restart %(appName)s to take effect)": "Enable hardware acceleration (restart %(appName)s to take effect)", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", @@ -1559,13 +1640,12 @@ "Message search": "Message search", "Cross-signing": "Cross-signing", "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.": "Your server admin has disabled end-to-end encryption by default in private rooms & Direct Messages.", - "Okay": "Okay", "Privacy": "Privacy", "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", - "Where you're signed in": "Where you're signed in", - "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", "Sessions": "Sessions", - "Other sessions": "Other sessions", + "Sign out": "Sign out", + "Are you sure you want to sign out of %(count)s sessions?|other": "Are you sure you want to sign out of %(count)s sessions?", + "Are you sure you want to sign out of %(count)s sessions?|one": "Are you sure you want to sign out of %(count)s session?", "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.": "For best security, verify your sessions and sign out from any session that you don't recognize or use anymore.", "Sidebar": "Sidebar", "Spaces to show": "Spaces to show", @@ -1576,9 +1656,6 @@ "Group all your people in one place.": "Group all your people in one place.", "Rooms outside of a space": "Rooms outside of a space", "Group all your rooms that aren't part of a space in one place.": "Group all your rooms that aren't part of a space in one place.", - "Default Device": "Default Device", - "No media permissions": "No media permissions", - "You may need to manually permit %(brand)s to access your microphone/webcam": "You may need to manually permit %(brand)s to access your microphone/webcam", "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", "Request media permissions": "Request media permissions", "Audio Output": "Audio Output", @@ -1587,9 +1664,14 @@ "No Microphones detected": "No Microphones detected", "Camera": "Camera", "No Webcams detected": "No Webcams detected", - "Voice & Video": "Voice & Video", + "Voice settings": "Voice settings", + "Automatically adjust the microphone volume": "Automatically adjust the microphone volume", + "Video settings": "Video settings", + "Voice processing": "Voice processing", + "Connection": "Connection", + "Allow fallback call assist server (%(server)s)": "Allow fallback call assist server (%(server)s)", "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", - "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. 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.": "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. 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.", + "Warning: upgrading a room will not automatically migrate room members to the new version of the room. 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.": "Warning: upgrading a room will not automatically migrate room members to the new version of the room. 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.", "Upgrade this space to the recommended room version": "Upgrade this space to the recommended room version", "Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version", "View older version of %(spaceName)s.": "View older version of %(spaceName)s.", @@ -1601,7 +1683,6 @@ "This room is bridging messages to the following platforms. Learn more.": "This room is bridging messages to the following platforms. Learn more.", "This room isn't bridging messages to any platforms. Learn more.": "This room isn't bridging messages to any platforms. Learn more.", "Bridges": "Bridges", - "Emotes": "Emotes", "Room Addresses": "Room Addresses", "Uploaded sound": "Uploaded sound", "Get notifications as set up in your settings": "Get notifications as set up in your settings", @@ -1613,6 +1694,7 @@ "Sounds": "Sounds", "Notification sound": "Notification sound", "Set a new custom sound": "Set a new custom sound", + "Upload custom sound": "Upload custom sound", "Browse": "Browse", "Failed to unban": "Failed to unban", "Unban": "Unban", @@ -1639,7 +1721,10 @@ "Send reactions": "Send reactions", "Remove messages sent by me": "Remove messages sent by me", "Modify widgets": "Modify widgets", + "Voice broadcasts": "Voice broadcasts", "Manage pinned events": "Manage pinned events", + "Start %(brand)s calls": "Start %(brand)s calls", + "Join %(brand)s calls": "Join %(brand)s calls", "Default role": "Default role", "Send messages": "Send messages", "Invite users": "Invite users", @@ -1679,6 +1764,10 @@ "Security & Privacy": "Security & Privacy", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Encrypted": "Encrypted", + "Enable %(brand)s as an additional calling option in this room": "Enable %(brand)s as an additional calling option in this room", + "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.", + "You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.", + "Call type": "Call type", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", "Your email address hasn't been verified yet": "Your email address hasn't been verified yet", @@ -1696,6 +1785,7 @@ "Please enter verification code sent via text.": "Please enter verification code sent via text.", "Verification code": "Verification code", "Discovery options will appear once you have added a phone number above.": "Discovery options will appear once you have added a phone number above.", + "Sign out of all other sessions (%(otherSessionsCount)s)": "Sign out of all other sessions (%(otherSessionsCount)s)", "Current session": "Current session", "Confirm logging out these devices by using Single Sign On to prove your identity.|other": "Confirm logging out these devices by using Single Sign On to prove your identity.", "Confirm logging out these devices by using Single Sign On to prove your identity.|one": "Confirm logging out this device by using Single Sign On to prove your identity.", @@ -1706,26 +1796,59 @@ "Sign out devices|other": "Sign out devices", "Sign out devices|one": "Sign out device", "Authentication": "Authentication", + "Failed to set display name": "Failed to set display name", + "Rename session": "Rename session", + "Please be aware that session names are also visible to people you communicate with.": "Please be aware that session names are also visible to people you communicate with.", + "Renaming sessions": "Renaming sessions", + "Other users in direct messages and rooms that you join are able to view a full list of your sessions.": "Other users in direct messages and rooms that you join are able to view a full list of your sessions.", + "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.": "This provides them with confidence that they are really speaking to you, but it also means they can see the session name you enter here.", + "Rename": "Rename", "Session ID": "Session ID", "Last activity": "Last activity", + "Application": "Application", + "Version": "Version", + "URL": "URL", "Device": "Device", + "Model": "Model", + "Operating system": "Operating system", + "Browser": "Browser", "IP address": "IP address", "Session details": "Session details", - "Toggle device details": "Toggle device details", + "Toggle push notifications on this session.": "Toggle push notifications on this session.", + "Push notifications": "Push notifications", + "Receive push notifications on this session.": "Receive push notifications on this session.", + "Sign out of this session": "Sign out of this session", + "Hide details": "Hide details", + "Show details": "Show details", "Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days", "Verified": "Verified", "Unverified": "Unverified", - "Unknown device type": "Unknown device type", - "Verified session": "Verified session", - "This session is ready for secure messaging.": "This session is ready for secure messaging.", + "Verified sessions": "Verified sessions", + "Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.": "Verified sessions are anywhere you are using this account after entering your passphrase or confirming your identity with another verified session.", + "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.": "This means that you have all the keys needed to unlock your encrypted messages and confirm to other users that you trust this session.", + "Unverified sessions": "Unverified sessions", + "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.": "Unverified sessions are sessions that have logged in with your credentials but have not been cross-verified.", + "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.": "You should make especially certain that you recognise these sessions as they could represent an unauthorised use of your account.", "Unverified session": "Unverified session", + "This session doesn't support encryption and thus can't be verified.": "This session doesn't support encryption and thus can't be verified.", + "You won't be able to participate in rooms where encryption is enabled when using this session.": "You won't be able to participate in rooms where encryption is enabled when using this session.", + "For best security and privacy, it is recommended to use Matrix clients that support encryption.": "For best security and privacy, it is recommended to use Matrix clients that support encryption.", + "Inactive sessions": "Inactive sessions", + "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.": "Inactive sessions are sessions you have not used in some time, but they continue to receive encryption keys.", + "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.": "Removing inactive sessions improves security and performance, and makes it easier for you to identify if a new session is suspicious.", + "Desktop session": "Desktop session", + "Mobile session": "Mobile session", + "Web session": "Web session", + "Unknown session type": "Unknown session type", + "Your current session is ready for secure messaging.": "Your current session is ready for secure messaging.", + "This session is ready for secure messaging.": "This session is ready for secure messaging.", + "Verified session": "Verified session", + "Verify your current session for enhanced secure messaging.": "Verify your current session for enhanced secure messaging.", "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", - "Verified sessions": "Verified sessions", + "Verify session": "Verify session", "For best security, sign out from any session that you don't recognize or use anymore.": "For best security, sign out from any session that you don't recognize or use anymore.", - "Unverified sessions": "Unverified sessions", "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.": "Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore.", - "Inactive sessions": "Inactive sessions", - "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore", + "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.": "Consider signing out from old sessions (%(inactiveAgeDays)s days or older) you don't use anymore.", "No verified sessions found.": "No verified sessions found.", "No unverified sessions found.": "No unverified sessions found.", "No inactive sessions found.": "No inactive sessions found.", @@ -1738,9 +1861,20 @@ "Inactive for %(inactiveAgeDays)s days or longer": "Inactive for %(inactiveAgeDays)s days or longer", "Filter devices": "Filter devices", "Show": "Show", + "Deselect all": "Deselect all", + "Select all": "Select all", + "%(count)s sessions selected|other": "%(count)s sessions selected", + "%(count)s sessions selected|one": "%(count)s session selected", + "Sign in with QR code": "Sign in with QR code", + "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.", + "Show QR code": "Show QR code", + "Sign out of %(count)s sessions|other": "Sign out of %(count)s sessions", + "Sign out of %(count)s sessions|one": "Sign out of %(count)s session", + "Other sessions": "Other sessions", "Security recommendations": "Security recommendations", - "Improve your account security by following these recommendations": "Improve your account security by following these recommendations", + "Improve your account security by following these recommendations.": "Improve your account security by following these recommendations.", "View all": "View all", + "Failed to set pusher state": "Failed to set pusher state", "Unable to remove contact information": "Unable to remove contact information", "Remove %(email)s?": "Remove %(email)s?", "Invalid Email Address": "Invalid Email Address", @@ -1758,24 +1892,19 @@ "This room is end-to-end encrypted": "This room is end-to-end encrypted", "Everyone in this room is verified": "Everyone in this room is verified", "Edit message": "Edit message", + "Emoji": "Emoji", "Mod": "Mod", + "Invite": "Invite", "From a thread": "From a thread", "This event could not be displayed": "This event could not be displayed", - "Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.", - "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.", - "If your other sessions do not have the key for this message you will not be able to decrypt them.": "If your other sessions do not have the key for this message you will not be able to decrypt them.", - "Key request sent.": "Key request sent.", - "Re-request encryption keys from your other sessions.": "Re-request encryption keys from your other sessions.", - "Message Actions": "Message Actions", - "View in room": "View in room", - "Copy link to thread": "Copy link to thread", - "This message cannot be decrypted": "This message cannot be decrypted", + " in %(room)s": " in %(room)s", "Encrypted by an unverified session": "Encrypted by an unverified session", "Unencrypted": "Unencrypted", "Encrypted by a deleted session": "Encrypted by a deleted session", "The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.", - "Sending your message...": "Sending your message...", - "Encrypting your message...": "Encrypting your message...", + "This message could not be decrypted": "This message could not be decrypted", + "Sending your message…": "Sending your message…", + "Encrypting your message…": "Encrypting your message…", "Your message was sent": "Your message was sent", "Failed to send": "Failed to send", "You don't have permission to view messages from before you were invited.": "You don't have permission to view messages from before you were invited.", @@ -1786,10 +1915,13 @@ "Show %(count)s other previews|other": "Show %(count)s other previews", "Show %(count)s other previews|one": "Show %(count)s other preview", "Close preview": "Close preview", + "%(count)s participants|other": "%(count)s participants", + "%(count)s participants|one": "1 participant", "and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|one": "and one other...", "Invite to this room": "Invite to this room", "Invite to this space": "Invite to this space", + "You do not have permission to invite users": "You do not have permission to invite users", "Invited": "Invited", "Filter room members": "Filter room members", "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", @@ -1803,22 +1935,23 @@ "The conversation continues here.": "The conversation continues here.", "This room has been replaced and is no longer active.": "This room has been replaced and is no longer active.", "You do not have permission to post to this room": "You do not have permission to post to this room", - "%(seconds)ss left": "%(seconds)ss left", "Send voice message": "Send voice message", - "Emoji": "Emoji", "Hide stickers": "Hide stickers", "Sticker": "Sticker", "Voice Message": "Voice Message", "You do not have permission to start polls in this room.": "You do not have permission to start polls in this room.", "Poll": "Poll", - "Bold": "Bold", + "Hide formatting": "Hide formatting", + "Show formatting": "Show formatting", + "Formatting": "Formatting", "Italics": "Italics", "Strikethrough": "Strikethrough", "Code block": "Code block", "Quote": "Quote", "Insert link": "Insert link", - "This is the beginning of your direct message history with .": "This is the beginning of your direct message history with .", "Send your first message to invite to chat": "Send your first message to invite to chat", + "Once everyone has joined, you’ll be able to chat": "Once everyone has joined, you’ll be able to chat", + "This is the beginning of your direct message history with .": "This is the beginning of your direct message history with .", "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.", "Topic: %(topic)s (edit)": "Topic: %(topic)s (edit)", "Topic: %(topic)s ": "Topic: %(topic)s ", @@ -1847,8 +1980,6 @@ "Idle": "Idle", "Offline": "Offline", "Unknown": "Unknown", - "Preview": "Preview", - "View": "View", "%(members)s and more": "%(members)s and more", "%(members)s and %(last)s": "%(members)s and %(last)s", "Seen by %(count)s people|other": "Seen by %(count)s people", @@ -1859,15 +1990,25 @@ "Room %(name)s": "Room %(name)s", "Recently visited rooms": "Recently visited rooms", "No recently visited rooms": "No recently visited rooms", + "Video call (Jitsi)": "Video call (Jitsi)", + "Video call (%(brand)s)": "Video call (%(brand)s)", + "Ongoing call": "Ongoing call", + "You do not have permission to start video calls": "You do not have permission to start video calls", + "There's no one here to call": "There's no one here to call", + "You do not have permission to start voice calls": "You do not have permission to start voice calls", + "Freedom": "Freedom", + "Spotlight": "Spotlight", + "Change layout": "Change layout", "Forget room": "Forget room", "Hide Widgets": "Hide Widgets", "Show Widgets": "Show Widgets", "Search": "Search", - "Invite": "Invite", + "Close call": "Close call", + "View chat timeline": "View chat timeline", "Room options": "Room options", + "Join Room": "Join Room", "(~%(count)s results)|other": "(~%(count)s results)", "(~%(count)s results)|one": "(~%(count)s result)", - "Join Room": "Join Room", "Video rooms are a beta feature": "Video rooms are a beta feature", "Video room": "Video room", "Public space": "Public space", @@ -1904,11 +2045,11 @@ "Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room", "%(spaceName)s menu": "%(spaceName)s menu", "Home options": "Home options", - "Joining space …": "Joining space …", - "Joining room …": "Joining room …", - "Joining …": "Joining …", - "Loading …": "Loading …", - "Rejecting invite …": "Rejecting invite …", + "Joining space…": "Joining space…", + "Joining room…": "Joining room…", + "Joining…": "Joining…", + "Loading…": "Loading…", + "Rejecting invite…": "Rejecting invite…", "Join the room to participate": "Join the room to participate", "Join the conversation with an account": "Join the conversation with an account", "Sign Up": "Sign Up", @@ -1975,11 +2116,7 @@ "%(count)s unread messages.|other": "%(count)s unread messages.", "%(count)s unread messages.|one": "1 unread message.", "Unread messages.": "Unread messages.", - "Video": "Video", - "Joining…": "Joining…", "Joined": "Joined", - "%(count)s participants|other": "%(count)s participants", - "%(count)s participants|one": "1 participant", "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.", "This room has already been upgraded.": "This room has already been upgraded.", "This room is running room version , which this homeserver has marked as unstable.": "This room is running room version , which this homeserver has marked as unstable.", @@ -1987,6 +2124,8 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", + "Search this room": "Search this room", + "Search all rooms": "Search all rooms", "Failed to connect to integration manager": "Failed to connect to integration manager", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", @@ -1999,13 +2138,27 @@ "%(count)s reply|other": "%(count)s replies", "%(count)s reply|one": "%(count)s reply", "Open thread": "Open thread", + "Unable to decrypt message": "Unable to decrypt message", "Jump to first unread message.": "Jump to first unread message.", - "Mark all as read": "Mark all as read", "Unable to access your microphone": "Unable to access your microphone", "We were unable to access your microphone. Please check your browser settings and try again.": "We were unable to access your microphone. Please check your browser settings and try again.", "No microphone found": "No microphone found", "We didn't find a microphone on your device. Please check your settings and try again.": "We didn't find a microphone on your device. Please check your settings and try again.", "Stop recording": "Stop recording", + "Italic": "Italic", + "Underline": "Underline", + "Bulleted list": "Bulleted list", + "Numbered list": "Numbered list", + "Indent increase": "Indent increase", + "Indent decrease": "Indent decrease", + "Code": "Code", + "Link": "Link", + "Edit link": "Edit link", + "Create a link": "Create a link", + "Text": "Text", + "Message Actions": "Message Actions", + "View in room": "View in room", + "Copy link to thread": "Copy link to thread", "Error updating main address": "Error updating main address", "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.", "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.", @@ -2030,7 +2183,6 @@ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", "Show more": "Show more", - "Upload Emote": "Upload Emote", "Room Name": "Room Name", "Room Topic": "Room Topic", "Room avatar": "Room avatar", @@ -2059,11 +2211,13 @@ "The homeserver the user you're verifying is connected to": "The homeserver the user you're verifying is connected to", "Yours, or the other users' internet connection": "Yours, or the other users' internet connection", "Yours, or the other users' session": "Yours, or the other users' session", + "Error starting verification": "Error starting verification", + "We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.", "Nothing pinned, yet": "Nothing pinned, yet", "If you have permissions, open the menu on any message and select Pin to stick them here.": "If you have permissions, open the menu on any message and select Pin to stick them here.", "Pinned messages": "Pinned messages", "Chat": "Chat", - "Room Info": "Room Info", + "Room info": "Room info", "You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets", "Maximise": "Maximise", "Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel", @@ -2074,13 +2228,13 @@ "Not encrypted": "Not encrypted", "About": "About", "Files": "Files", + "Poll history": "Poll history", "Pinned": "Pinned", "Export chat": "Export chat", "Share room": "Share room", "Room settings": "Room settings", "Trusted": "Trusted", "Not trusted": "Not trusted", - "Unable to load session list": "Unable to load session list", "%(count)s verified sessions|other": "%(count)s verified sessions", "%(count)s verified sessions|one": "1 verified session", "Hide verified sessions": "Hide verified sessions", @@ -2088,6 +2242,8 @@ "%(count)s sessions|one": "%(count)s session", "Hide sessions": "Hide sessions", "Message": "Message", + "Ignore %(user)s": "Ignore %(user)s", + "All messages and invites from this user will be hidden. Are you sure you want to ignore them?": "All messages and invites from this user will be hidden. Are you sure you want to ignore them?", "Jump to read receipt": "Jump to read receipt", "Mention": "Mention", "Share Link to User": "Share Link to User", @@ -2121,7 +2277,6 @@ "Failed to mute user": "Failed to mute user", "Unmute": "Unmute", "Mute": "Mute", - "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", "Deactivate user?": "Deactivate user?", @@ -2137,7 +2292,7 @@ "Compare unique emoji": "Compare unique emoji", "Compare a unique set of emoji if you don't have a camera on either device": "Compare a unique set of emoji if you don't have a camera on either device", "Start": "Start", - "or": "or", + "%(qrCode)s or %(emojiCompare)s": "%(qrCode)s or %(emojiCompare)s", "Verify this device by completing one of the following:": "Verify this device by completing one of the following:", "Verify by scanning": "Verify by scanning", "Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:", @@ -2159,6 +2314,26 @@ "%(displayName)s cancelled verification.": "%(displayName)s cancelled verification.", "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", + "%(count)s votes|other": "%(count)s votes", + "%(count)s votes|one": "%(count)s vote", + "View poll in timeline": "View poll in timeline", + "Active polls": "Active polls", + "Past polls": "Past polls", + "Loading polls": "Loading polls", + "Load more polls": "Load more polls", + "There are no active polls in this room": "There are no active polls in this room", + "There are no past polls in this room": "There are no past polls in this room", + "There are no active polls. Load more polls to view polls for previous months": "There are no active polls. Load more polls to view polls for previous months", + "There are no past polls. Load more polls to view polls for previous months": "There are no past polls. Load more polls to view polls for previous months", + "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months", + "There are no active polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no active polls for the past day. Load more polls to view polls for previous months", + "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|other": "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months", + "There are no past polls for the past %(count)s days. Load more polls to view polls for previous months|one": "There are no past polls for the past day. Load more polls to view polls for previous months", + "View poll": "View poll", + "Final result based on %(count)s votes|other": "Final result based on %(count)s votes", + "Final result based on %(count)s votes|one": "Final result based on %(count)s vote", + "%(name)s started a video call": "%(name)s started a video call", + "Video call ended": "Video call ended", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -2168,11 +2343,20 @@ "Saturday": "Saturday", "Today": "Today", "Yesterday": "Yesterday", - "Unable to find event at that date. (%(code)s)": "Unable to find event at that date. (%(code)s)", + "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", + "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.": "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.", + "Server returned %(statusCode)s with error code %(errorCode)s": "Server returned %(statusCode)s with error code %(errorCode)s", + "unknown status code": "unknown status code", + "unavailable": "unavailable", + "Please submit debug logs to help us track down the problem.": "Please submit debug logs to help us track down the problem.", + "Unable to find event at that date": "Unable to find event at that date", + "Error details": "Error details", "Last week": "Last week", "Last month": "Last month", "The beginning of the room": "The beginning of the room", "Jump to date": "Jump to date", + "The sender has blocked you from receiving this message": "The sender has blocked you from receiving this message", + "%(displayName)s (%(matrixId)s)": "%(displayName)s (%(matrixId)s)", "Downloading": "Downloading", "Decrypting": "Decrypting", "Download": "Download", @@ -2191,6 +2375,8 @@ "Go": "Go", "Call declined": "Call declined", "Call back": "Call back", + "Answered elsewhere": "Answered elsewhere", + "Missed call": "Missed call", "No answer": "No answer", "Could not connect media": "Could not connect media", "Connection failed": "Connection failed", @@ -2198,14 +2384,12 @@ "An unknown error occurred": "An unknown error occurred", "Unknown failure: %(reason)s": "Unknown failure: %(reason)s", "Retry": "Retry", - "Missed call": "Missed call", "The call is in an unknown state!": "The call is in an unknown state!", "Error processing audio message": "Error processing audio message", "View live location": "View live location", "React": "React", + "Reply in thread": "Reply in thread", "Can't create a thread from an event with an existing relation": "Can't create a thread from an event with an existing relation", - "Beta feature": "Beta feature", - "Beta feature. Click to learn more.": "Beta feature. Click to learn more.", "Favourite": "Favourite", "Edit": "Edit", "Reply": "Reply", @@ -2217,7 +2401,9 @@ "Decrypt %(text)s": "Decrypt %(text)s", "Invalid file%(extra)s": "Invalid file%(extra)s", "Image": "Image", + "Unable to show image due to error": "Unable to show image due to error", "Error decrypting image": "Error decrypting image", + "Error downloading image": "Error downloading image", "Show image": "Show image", "Join the conference at the top of this room": "Join the conference at the top of this room", "Join the conference from the room information card on the right": "Join the conference from the room information card on the right", @@ -2234,8 +2420,7 @@ "You cancelled": "You cancelled", "%(name)s declined": "%(name)s declined", "%(name)s cancelled": "%(name)s cancelled", - "Accepting …": "Accepting …", - "Declining …": "Declining …", + "Declining…": "Declining…", "%(name)s wants to verify": "%(name)s wants to verify", "You sent a verification request": "You sent a verification request", "Expand map": "Expand map", @@ -2246,8 +2431,7 @@ "Sorry, you can't edit a poll after votes have been cast.": "Sorry, you can't edit a poll after votes have been cast.", "Vote not registered": "Vote not registered", "Sorry, your vote was not registered. Please try again.": "Sorry, your vote was not registered. Please try again.", - "Final result based on %(count)s votes|other": "Final result based on %(count)s votes", - "Final result based on %(count)s votes|one": "Final result based on %(count)s vote", + "Due to decryption errors, some votes may not be counted": "Due to decryption errors, some votes may not be counted", "Results will be visible when the poll is ended": "Results will be visible when the poll is ended", "No votes cast": "No votes cast", "%(count)s votes cast. Vote to see the results|other": "%(count)s votes cast. Vote to see the results", @@ -2255,8 +2439,7 @@ "Based on %(count)s votes|other": "Based on %(count)s votes", "Based on %(count)s votes|one": "Based on %(count)s vote", "edited": "edited", - "%(count)s votes|other": "%(count)s votes", - "%(count)s votes|one": "%(count)s vote", + "Ended a poll": "Ended a poll", "Error decrypting video": "Error decrypting video", "Error processing voice message": "Error processing voice message", "Add reaction": "Add reaction", @@ -2267,8 +2450,10 @@ "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", - "Click here to see older messages.": "Click here to see older messages.", "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", + "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it. It's possible that guessing the server from the room ID will work. If you want to try, click this link:": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it. It's possible that guessing the server from the room ID will work. If you want to try, click this link:", + "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it.": "Can't find the old version of this room (room ID: %(roomId)s), and we have not been provided with 'via_servers' to look for it.", + "Click here to see older messages.": "Click here to see older messages.", "Add an Integration": "Add an Integration", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", "Edited at %(date)s": "Edited at %(date)s", @@ -2286,10 +2471,6 @@ "Click to move the pin": "Click to move the pin", "Click to drop a pin": "Click to drop a pin", "Share location": "Share location", - "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.": "%(brand)s was denied permission to fetch your location. Please allow location access in your browser settings.", - "Failed to fetch your location. Please try again later.": "Failed to fetch your location. Please try again later.", - "Timed out trying to fetch your location. Please try again later.": "Timed out trying to fetch your location. Please try again later.", - "Unknown error fetching location. Please try again later.": "Unknown error fetching location. Please try again later.", "You don't have permission to share locations": "You don't have permission to share locations", "You need to have the right permissions in order to share locations in this room.": "You need to have the right permissions in order to share locations in this room.", "We couldn't send your location": "We couldn't send your location", @@ -2315,7 +2496,9 @@ "Your display name": "Your display name", "Your avatar URL": "Your avatar URL", "Your user ID": "Your user ID", + "Your device ID": "Your device ID", "Your theme": "Your theme", + "Your language": "Your language", "%(brand)s URL": "%(brand)s URL", "Room ID": "Room ID", "Widget ID": "Widget ID", @@ -2324,7 +2507,6 @@ "Widgets do not use message encryption.": "Widgets do not use message encryption.", "Widget added by": "Widget added by", "This widget may use cookies.": "This widget may use cookies.", - "Loading...": "Loading...", "Error loading Widget": "Error loading Widget", "Error - Mixed content": "Error - Mixed content", "Un-maximise": "Un-maximise", @@ -2408,10 +2590,13 @@ "%(oneUser)ssent %(count)s hidden messages|one": "%(oneUser)ssent a hidden message", "collapse": "collapse", "expand": "expand", + "Image view": "Image view", "Rotate Left": "Rotate Left", "Rotate Right": "Rotate Right", "Information": "Information", "Language Dropdown": "Language Dropdown", + "Message in %(room)s": "Message in %(room)s", + "Message from %(user)s": "Message from %(user)s", "Create poll": "Create poll", "Create Poll": "Create Poll", "Edit poll": "Edit poll", @@ -2423,7 +2608,7 @@ "Closed poll": "Closed poll", "What is your poll question or topic?": "What is your poll question or topic?", "Question or topic": "Question or topic", - "Write something...": "Write something...", + "Write something…": "Write something…", "Create options": "Create options", "Option %(number)s": "Option %(number)s", "Write an option": "Write an option", @@ -2455,6 +2640,7 @@ "Edit topic": "Edit topic", "Click to read topic": "Click to read topic", "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", + "Desktop app logo": "Desktop app logo", "Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files", "Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages", "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files", @@ -2464,6 +2650,7 @@ "Join millions for free on the largest public server": "Join millions for free on the largest public server", "Homeserver": "Homeserver", "Help": "Help", + "WARNING: ": "WARNING: ", "Choose a locale": "Choose a locale", "Continue with %(provider)s": "Continue with %(provider)s", "Sign in with single sign-on": "Sign in with single sign-on", @@ -2508,14 +2695,15 @@ "You can turn this off anytime in settings": "You can turn this off anytime in settings", "Download %(brand)s Desktop": "Download %(brand)s Desktop", "iOS": "iOS", + "%(qrCode)s or %(appLinks)s": "%(qrCode)s or %(appLinks)s", "Download on the App Store": "Download on the App Store", "Android": "Android", "Get it on Google Play": "Get it on Google Play", "Get it on F-Droid": "Get it on F-Droid", "App Store® and the Apple logo® are trademarks of Apple Inc.": "App Store® and the Apple logo® are trademarks of Apple Inc.", "Google Play and the Google Play logo are trademarks of Google LLC.": "Google Play and the Google Play logo are trademarks of Google LLC.", - "The following users may not exist": "The following users may not exist", "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?", + "The following users may not exist": "The following users may not exist", "Invite anyway and never warn me again": "Invite anyway and never warn me again", "Invite anyway": "Invite anyway", "Close dialog": "Close dialog", @@ -2544,6 +2732,8 @@ "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)", "Remove %(count)s messages|other": "Remove %(count)s messages", "Remove %(count)s messages|one": "Remove 1 message", + "Can't start voice message": "Can't start voice message", + "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.": "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.", "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", @@ -2557,6 +2747,7 @@ "Clear all data": "Clear all data", "Please enter a name for the room": "Please enter a name for the room", "Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.", + "Unnamed Space": "Unnamed Space", "You can change this at any time from room settings.": "You can change this at any time from room settings.", "Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .", "Anyone will be able to find and join this room.": "Anyone will be able to find and join this room.", @@ -2585,8 +2776,7 @@ "Space visibility": "Space visibility", "Private space (invite only)": "Private space (invite only)", "Want to add an existing space instead?": "Want to add an existing space instead?", - "Adding...": "Adding...", - "Sign out": "Sign out", + "Adding…": "Adding…", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this", "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.": "You've previously used a newer version of %(brand)s with this session. To use this version again with end to end encryption, you will need to sign out and back in again.", "Incompatible Database": "Incompatible Database", @@ -2606,11 +2796,11 @@ "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number": "You will be removed from the identity server: your friends will no longer be able to find you with your email or phone number", "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?": "Your old messages will still be visible to people who received them, just like emails you sent in the past. Would you like to hide your sent messages from people who join rooms in the future?", "Hide my messages from new joiners": "Hide my messages from new joiners", - "Room": "Room", "Send custom timeline event": "Send custom timeline event", "Explore room state": "Explore room state", "Explore room account data": "Explore room account data", "View servers in room": "View servers in room", + "Notifications debug": "Notifications debug", "Verification explorer": "Verification explorer", "Active Widgets": "Active Widgets", "Explore account data": "Explore account data", @@ -2626,7 +2816,7 @@ "End Poll": "End Poll", "Are you sure you want to end this poll? This will show the final results of the poll and stop people from being able to vote.": "Are you sure you want to end this poll? This will show the final results of the poll and stop people from being able to vote.", "An error has occurred.": "An error has occurred.", - "Processing...": "Processing...", + "Processing…": "Processing…", "Enter a number between %(min)s and %(max)s": "Enter a number between %(min)s and %(max)s", "Size can only be a number between %(min)s MB and %(max)s MB": "Size can only be a number between %(min)s MB and %(max)s MB", "Number of messages can only be a number between %(min)s and %(max)s": "Number of messages can only be a number between %(min)s and %(max)s", @@ -2647,6 +2837,7 @@ "Feedback sent": "Feedback sent", "Comment": "Comment", "Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.", + "Feedback": "Feedback", "You may contact me if you want to follow up or to let me test out upcoming ideas": "You may contact me if you want to follow up or to let me test out upcoming ideas", "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.": "PRO TIP: If you start a bug, please submit debug logs to help us track down the problem.", "Report a bug": "Report a bug", @@ -2662,28 +2853,14 @@ "Search for rooms or people": "Search for rooms or people", "Feedback sent! Thanks, we appreciate it!": "Feedback sent! Thanks, we appreciate it!", "You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions", - "Confirm abort of host creation": "Confirm abort of host creation", - "Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.", - "Abort": "Abort", - "Failed to connect to your homeserver. Please close this dialog and try again.": "Failed to connect to your homeserver. Please close this dialog and try again.", - "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.": "Continuing temporarily allows the %(hostSignupBrand)s setup process to access your account to fetch verified email addresses. This data is not stored.", - "Learn more in our , and .": "Learn more in our , and .", - "Cookie Policy": "Cookie Policy", - "Privacy Policy": "Privacy Policy", - "Terms of Service": "Terms of Service", - "You should know": "You should know", - "%(hostSignupBrand)s Setup": "%(hostSignupBrand)s Setup", - "Maximise dialog": "Maximise dialog", - "Minimise dialog": "Minimise dialog", - "Upgrade to %(hostSignupBrand)s": "Upgrade to %(hostSignupBrand)s", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.": "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.", "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.", - "Waiting for partner to confirm...": "Waiting for partner to confirm...", + "Waiting for partner to confirm…": "Waiting for partner to confirm…", "Incoming Verification Request": "Incoming Verification Request", "Integrations are disabled": "Integrations are disabled", - "Enable 'Manage Integrations' in Settings to do this.": "Enable 'Manage Integrations' in Settings to do this.", + "Enable '%(manageIntegrations)s' in Settings to do this.": "Enable '%(manageIntegrations)s' in Settings to do this.", "Integrations not allowed": "Integrations not allowed", "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.", "To continue, use Single Sign On to prove your identity.": "To continue, use Single Sign On to prove your identity.", @@ -2691,6 +2868,9 @@ "Click the button below to confirm your identity.": "Click the button below to confirm your identity.", "Invite by email": "Invite by email", "We couldn't create your DM.": "We couldn't create your DM.", + "Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?": "Unable to find profiles for the Matrix IDs listed below - would you like to start a DM anyway?", + "Start DM anyway and never warn me again": "Start DM anyway and never warn me again", + "Start DM anyway": "Start DM anyway", "Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.", "We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.", "A call can only be transferred to a single user.": "A call can only be transferred to a single user.", @@ -2706,9 +2886,7 @@ "Some suggestions may be hidden for privacy.": "Some suggestions may be hidden for privacy.", "If you can't see who you're looking for, send them your invite link below.": "If you can't see who you're looking for, send them your invite link below.", "Or send invite link": "Or send invite link", - "Unnamed Space": "Unnamed Space", "Invite to %(roomName)s": "Invite to %(roomName)s", - "Unnamed Room": "Unnamed Room", "Invite someone using their name, email address, username (like ) or share this space.": "Invite someone using their name, email address, username (like ) or share this space.", "Invite someone using their name, username (like ) or share this space.": "Invite someone using their name, username (like ) or share this space.", "Invite someone using their name, email address, username (like ) or share this room.": "Invite someone using their name, email address, username (like ) or share this room.", @@ -2716,6 +2894,7 @@ "Invited people will be able to read old messages.": "Invited people will be able to read old messages.", "Transfer": "Transfer", "Consult first": "Consult first", + "Invites by email can only be sent one at a time": "Invites by email can only be sent one at a time", "User Directory": "User Directory", "Dial pad": "Dial pad", "a new master key signature": "a new master key signature", @@ -2766,7 +2945,6 @@ "Session name": "Session name", "Session key": "Session key", "If they don't match, the security of your communication may be compromised.": "If they don't match, the security of your communication may be compromised.", - "Verify session": "Verify session", "Your homeserver doesn't seem to support this feature.": "Your homeserver doesn't seem to support this feature.", "Message edits": "Message edits", "Modal Widget": "Modal Widget", @@ -2775,14 +2953,15 @@ "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.", "Email (optional)": "Email (optional)", "Please fill why you're reporting.": "Please fill why you're reporting.", + "Unable to create room with moderation bot": "Unable to create room with moderation bot", "Ignore user": "Ignore user", "Check if you want to hide all current and future messages from this user.": "Check if you want to hide all current and future messages from this user.", "What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.", - "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", + "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.", "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.", "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.", "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.", - "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.", + "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s.", "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.", "Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.", "Report Content": "Report Content", @@ -2857,9 +3036,16 @@ "Link to selected message": "Link to selected message", "Link to room": "Link to room", "Command Help": "Command Help", + "Checking…": "Checking…", + "Your server has native support": "Your server has native support", + "Your server lacks native support": "Your server lacks native support", + "Your server lacks native support, you must specify a proxy": "Your server lacks native support, you must specify a proxy", + "Sliding Sync configuration": "Sliding Sync configuration", + "To disable you will need to log out and back in, use with caution!": "To disable you will need to log out and back in, use with caution!", + "Proxy URL (optional)": "Proxy URL (optional)", + "Proxy URL": "Proxy URL", "Sections to show": "Sections to show", "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.", - "Space settings": "Space settings", "Settings - %(spaceName)s": "Settings - %(spaceName)s", "To help us prevent this in future, please send us logs.": "To help us prevent this in future, please send us logs.", "Missing session data": "Missing session data", @@ -2868,6 +3054,7 @@ "Find others by phone or email": "Find others by phone or email", "Be found by phone or email": "Be found by phone or email", "Use bots, bridges, widgets and sticker packs": "Use bots, bridges, widgets and sticker packs", + "Terms of Service": "Terms of Service", "To continue you need to accept the terms of this service.": "To continue you need to accept the terms of this service.", "Service": "Service", "Summary": "Summary", @@ -2890,6 +3077,7 @@ "Upload %(count)s other files|one": "Upload %(count)s other file", "Cancel All": "Cancel All", "Upload Error": "Upload Error", + "Labs": "Labs", "Verify other device": "Verify other device", "Verification Request": "Verification Request", "Approve widget permissions": "Approve widget permissions", @@ -2900,11 +3088,13 @@ "Allow this widget to verify your identity": "Allow this widget to verify your identity", "The widget will verify your user ID, but won't be able to perform actions for you:": "The widget will verify your user ID, but won't be able to perform actions for you:", "Remember this": "Remember this", + "Unnamed room": "Unnamed room", "%(count)s Members|other": "%(count)s Members", "%(count)s Members|one": "%(count)s Member", "Public rooms": "Public rooms", "Use \"%(query)s\" to search": "Use \"%(query)s\" to search", "Search for": "Search for", + "View": "View", "Spaces you're in": "Spaces you're in", "Show rooms": "Show rooms", "Show spaces": "Show spaces", @@ -2926,7 +3116,6 @@ "Use to scroll": "Use to scroll", "Search Dialog": "Search Dialog", "Remove search filter for %(filter)s": "Remove search filter for %(filter)s", - "Results not as expected? Please give feedback.": "Results not as expected? Please give feedback.", "Wrong file type": "Wrong file type", "Looks good!": "Looks good!", "Wrong Security Key": "Wrong Security Key", @@ -2940,6 +3129,7 @@ "Enter your Security Phrase or to continue.": "Enter your Security Phrase or to continue.", "Security Key": "Security Key", "Use your Security Key to continue.": "Use your Security Key to continue.", + "%(securityKey)s or %(recoveryFile)s": "%(securityKey)s or %(recoveryFile)s", "Destroy cross-signing keys?": "Destroy cross-signing keys?", "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.": "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.", "Clear cross-signing keys": "Clear cross-signing keys", @@ -2947,7 +3137,7 @@ "Click the button below to confirm setting up encryption.": "Click the button below to confirm setting up encryption.", "Unable to set up keys": "Unable to set up keys", "Restoring keys from backup": "Restoring keys from backup", - "Fetching keys from server...": "Fetching keys from server...", + "Fetching keys from server…": "Fetching keys from server…", "%(completed)s of %(total)s keys restored": "%(completed)s of %(total)s keys restored", "Unable to load backup status": "Unable to load backup status", "Security Key mismatch": "Security Key mismatch", @@ -2966,7 +3156,6 @@ "Enter Security Key": "Enter Security Key", "This looks like a valid Security Key!": "This looks like a valid Security Key!", "Not a valid Security Key": "Not a valid Security Key", - "Warning: You should only set up key backup from a trusted computer.": "Warning: You should only set up key backup from a trusted computer.", "Access your secure message history and set up secure messaging by entering your Security Key.": "Access your secure message history and set up secure messaging by entering your Security Key.", "If you've forgotten your Security Key you can ": "If you've forgotten your Security Key you can ", "Send custom account data event": "Send custom account data event", @@ -2979,6 +3168,24 @@ "Event Content": "Event Content", "Filter results": "Filter results", "No results found": "No results found", + "Room status": "Room status", + "Room unread status: %(status)s, count: %(count)s|other": "Room unread status: %(status)s, count: %(count)s", + "Room unread status: %(status)s, count: %(count)s|zero": "Room unread status: %(status)s", + "Notification state is %(notificationState)s": "Notification state is %(notificationState)s", + "Room is encrypted ✅": "Room is encrypted ✅", + "Room is not encrypted 🚨": "Room is not encrypted 🚨", + "Main timeline": "Main timeline", + "Total: ": "Total: ", + "Highlight: ": "Highlight: ", + "Dot: ": "Dot: ", + "User read up to: ": "User read up to: ", + "No receipt found": "No receipt found", + "Last event:": "Last event:", + "ID: ": "ID: ", + "Type: ": "Type: ", + "Sender: ": "Sender: ", + "Threads timeline": "Threads timeline", + "Thread Id: ": "Thread Id: ", "<%(count)s spaces>|other": "<%(count)s spaces>", "<%(count)s spaces>|one": "", "<%(count)s spaces>|zero": "", @@ -3009,7 +3216,6 @@ "Value": "Value", "Value in this room": "Value in this room", "Edit setting": "Edit setting", - "Unsent": "Unsent", "Requested": "Requested", "Ready": "Ready", "Started": "Started", @@ -3043,8 +3249,9 @@ "Copy room link": "Copy room link", "Low Priority": "Low Priority", "Forget Room": "Forget Room", - "Use default": "Use default", - "Mentions & Keywords": "Mentions & Keywords", + "Mark as read": "Mark as read", + "Match default setting": "Match default setting", + "Mute room": "Mute room", "See room timeline (devtools)": "See room timeline (devtools)", "Space": "Space", "Space home": "Space home", @@ -3066,10 +3273,11 @@ "Beta": "Beta", "Leaving the beta will reload %(brand)s.": "Leaving the beta will reload %(brand)s.", "Joining the beta will reload %(brand)s.": "Joining the beta will reload %(brand)s.", + "Leave the beta": "Leave the beta", "Join the beta": "Join the beta", "Updated %(humanizedUpdateTime)s": "Updated %(humanizedUpdateTime)s", "Live until %(expiryTime)s": "Live until %(expiryTime)s", - "Loading live location...": "Loading live location...", + "Loading live location…": "Loading live location…", "Live location ended": "Live location ended", "Live location error": "Live location error", "No live locations": "No live locations", @@ -3106,14 +3314,36 @@ "Token incorrect": "Token incorrect", "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", "Please enter the code it contains:": "Please enter the code it contains:", - "Code": "Code", "Submit": "Submit", + "Enter a registration token provided by the homeserver administrator.": "Enter a registration token provided by the homeserver administrator.", + "Registration token": "Registration token", "Something went wrong in confirming your identity. Cancel and try again.": "Something went wrong in confirming your identity. Cancel and try again.", "Start authentication": "Start authentication", + "Sign in new device": "Sign in new device", + "The linking wasn't completed in the required time.": "The linking wasn't completed in the required time.", + "The scanned code is invalid.": "The scanned code is invalid.", + "Linking with this device is not supported.": "Linking with this device is not supported.", + "The request was declined on the other device.": "The request was declined on the other device.", + "The other device is already signed in.": "The other device is already signed in.", + "The other device isn't signed in.": "The other device isn't signed in.", + "The request was cancelled.": "The request was cancelled.", + "An unexpected error occurred.": "An unexpected error occurred.", + "The homeserver doesn't support signing in another device.": "The homeserver doesn't support signing in another device.", + "Devices connected": "Devices connected", + "Check that the code below matches with your other device:": "Check that the code below matches with your other device:", + "By approving access for this device, it will have full access to your account.": "By approving access for this device, it will have full access to your account.", + "Scan the QR code below with your device that's signed out.": "Scan the QR code below with your device that's signed out.", + "Start at the sign in screen": "Start at the sign in screen", + "Select '%(scanQRCode)s'": "Select '%(scanQRCode)s'", + "Scan QR code": "Scan QR code", + "Review and approve the sign in": "Review and approve the sign in", + "Connecting…": "Connecting…", + "Waiting for device to sign in": "Waiting for device to sign in", + "Completing set up of your new device": "Completing set up of your new device", "Enter password": "Enter password", "Nice, strong password!": "Nice, strong password!", "Password is allowed, but unsafe": "Password is allowed, but unsafe", - "Keep going...": "Keep going...", + "Keep going…": "Keep going…", "Enter username": "Enter username", "Enter phone number": "Enter phone number", "That phone number doesn't look quite right, please check and try again": "That phone number doesn't look quite right, please check and try again", @@ -3182,20 +3412,9 @@ "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "You're all caught up": "You're all caught up", "You have no visible notifications.": "You have no visible notifications.", - "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.", - "%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.", - "The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.", - "Delete the room address %(alias)s and remove %(name)s from the directory?": "Delete the room address %(alias)s and remove %(name)s from the directory?", - "Remove %(name)s from the directory?": "Remove %(name)s from the directory?", - "Remove from Directory": "Remove from Directory", - "remove %(name)s from the directory.": "remove %(name)s from the directory.", - "delete the address.": "delete the address.", - "The server may be unavailable or overloaded": "The server may be unavailable or overloaded", - "No results for \"%(query)s\"": "No results for \"%(query)s\"", - "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.", - "Find a room…": "Find a room…", - "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", - "If you can't find the room you're looking for, ask for an invite or create a new room.": "If you can't find the room you're looking for, ask for an invite or create a new room.", + "Search failed": "Search failed", + "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", + "No more results": "No more results", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.", @@ -3209,9 +3428,6 @@ "We're creating a room with %(names)s": "We're creating a room with %(names)s", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", - "Search failed": "Search failed", - "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", - "No more results": "No more results", "Failed to reject invite": "Failed to reject invite", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", @@ -3219,11 +3435,11 @@ "You don't have permission": "You don't have permission", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", + "Unknown error": "Unknown error", "Select a room below first": "Select a room below first", - "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", - "Removing...": "Removing...", "Mark as not suggested": "Mark as not suggested", "Mark as suggested": "Mark as suggested", + "Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later", "Failed to load list of rooms.": "Failed to load list of rooms.", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", @@ -3236,10 +3452,9 @@ "Room name": "Room name", "Failed to create initial space rooms": "Failed to create initial space rooms", "Skip for now": "Skip for now", - "Creating rooms...": "Creating rooms...", + "Creating rooms…": "Creating rooms…", "What do you want to organise?": "What do you want to organise?", "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.", - "Search for rooms or spaces": "Search for rooms or spaces", "Share %(name)s": "Share %(name)s", "It's just you at the moment, it will be even better with others.": "It's just you at the moment, it will be even better with others.", "Go to my first room": "Go to my first room", @@ -3251,10 +3466,9 @@ "Me and my teammates": "Me and my teammates", "A private space for you and your teammates": "A private space for you and your teammates", "Failed to invite the following users to your space: %(csvUsers)s": "Failed to invite the following users to your space: %(csvUsers)s", - "Inviting...": "Inviting...", + "Inviting…": "Inviting…", "Invite your teammates": "Invite your teammates", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", - "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.", "Invite by username": "Invite by username", "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?", "Let's create a room for each of them.": "Let's create a room for each of them.", @@ -3271,8 +3485,6 @@ "Threads help keep your conversations on-topic and easy to track.": "Threads help keep your conversations on-topic and easy to track.", "Tip: Use “%(replyInThread)s” when hovering over a message.": "Tip: Use “%(replyInThread)s” when hovering over a message.", "Keep discussions organised with threads": "Keep discussions organised with threads", - "Threads are a beta feature": "Threads are a beta feature", - "Give feedback": "Give feedback", "Thread": "Thread", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", @@ -3288,31 +3500,31 @@ "User menu": "User menu", "Could not load user profile": "Could not load user profile", "Decrypted event source": "Decrypted event source", + "Decrypted source unavailable": "Decrypted source unavailable", "Original event source": "Original event source", "Event ID: %(eventId)s": "Event ID: %(eventId)s", + "Thread root ID: %(threadRootId)s": "Thread root ID: %(threadRootId)s", + "Waiting for users to join %(brand)s": "Waiting for users to join %(brand)s", + "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted": "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted", "Unable to verify this device": "Unable to verify this device", "Verify this device": "Verify this device", "Device verified": "Device verified", "Really reset verification keys?": "Really reset verification keys?", "Skip verification for now": "Skip verification for now", - "Failed to send email": "Failed to send email", + "Too many attempts in a short time. Wait some time before trying again.": "Too many attempts in a short time. Wait some time before trying again.", + "Too many attempts in a short time. Retry after %(timeout)s.": "Too many attempts in a short time. Retry after %(timeout)s.", "Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "Resetting your password on this homeserver will cause all of your devices to be signed out. This will delete the message encryption keys stored on them, making encrypted chat history unreadable.", "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.": "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.": "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.", - "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", - "The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.", + "Reset password": "Reset password", + "Reset your password": "Reset your password", + "Confirm new password": "Confirm new password", "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", - "Sign out all devices": "Sign out all devices", - "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", - "Send Reset Email": "Send Reset Email", - "Sign in instead": "Sign in instead", - "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.": "An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", - "I have verified my email address": "I have verified my email address", + "Sign out of all devices": "Sign out of all devices", "Your password has been reset.": "Your password has been reset.", "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.", "Return to login screen": "Return to login screen", - "Set a new password": "Set a new password", "Invalid homeserver discovery response": "Invalid homeserver discovery response", "Failed to get autodiscovery configuration from server": "Failed to get autodiscovery configuration from server", "Invalid base_url for m.homeserver": "Invalid base_url for m.homeserver", @@ -3322,24 +3534,17 @@ "Identity server URL does not appear to be a valid identity server": "Identity server URL does not appear to be a valid identity server", "General failure": "General failure", "This homeserver does not support login using email address.": "This homeserver does not support login using email address.", - "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", - "This account has been deactivated.": "This account has been deactivated.", - "Incorrect username and/or password.": "Incorrect username and/or password.", - "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", - "There was a problem communicating with the homeserver, please try again later.": "There was a problem communicating with the homeserver, please try again later.", - "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", - "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", - "Syncing...": "Syncing...", - "Signing In...": "Signing In...", + "Syncing…": "Syncing…", + "Signing In…": "Signing In…", "If you've joined lots of rooms, this might take a while": "If you've joined lots of rooms, this might take a while", "New? Create account": "New? Create account", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", "Someone already has that username, please try another.": "Someone already has that username, please try another.", - "That e-mail address is already in use.": "That e-mail address is already in use.", + "That e-mail address or phone number is already in use.": "That e-mail address or phone number is already in use.", "Continue with %(ssoButtons)s": "Continue with %(ssoButtons)s", "%(ssoButtons)s Or %(usernamePassword)s": "%(ssoButtons)s Or %(usernamePassword)s", "Already have an account? Sign in here": "Already have an account? Sign in here", @@ -3360,7 +3565,7 @@ "Without verifying, you won't have access to all your messages and may appear as untrusted to others.": "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", "I'll verify later": "I'll verify later", "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.": "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", - "Please only proceed if you're sure you've lost all of your other devices and your security key.": "Please only proceed if you're sure you've lost all of your other devices and your security key.", + "Please only proceed if you're sure you've lost all of your other devices and your Security Key.": "Please only proceed if you're sure you've lost all of your other devices and your Security Key.", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Incorrect password": "Incorrect password", "Failed to re-authenticate": "Failed to re-authenticate", @@ -3371,7 +3576,20 @@ "You cannot sign in to your account. Please contact your homeserver admin for more information.": "You cannot sign in to your account. Please contact your homeserver admin for more information.", "You're signed out": "You're signed out", "Clear personal data": "Clear personal data", - "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: Your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", + "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.": "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", + "Follow the instructions sent to %(email)s": "Follow the instructions sent to %(email)s", + "Wrong email address?": "Wrong email address?", + "Re-enter email address": "Re-enter email address", + "Did not receive it?": "Did not receive it?", + "Verification link email resent!": "Verification link email resent!", + "Send email": "Send email", + "Enter your email to reset password": "Enter your email to reset password", + "%(homeserver)s will send you a verification link to let you reset your password.": "%(homeserver)s will send you a verification link to let you reset your password.", + "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", + "The email address doesn't appear to be valid.": "The email address doesn't appear to be valid.", + "Sign in instead": "Sign in instead", + "Verify your email to continue": "Verify your email to continue", + "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s": "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s", "Commands": "Commands", "Command Autocomplete": "Command Autocomplete", "Emoji Autocomplete": "Emoji Autocomplete", @@ -3392,7 +3610,7 @@ "That doesn't match.": "That doesn't match.", "Go back to set it again.": "Go back to set it again.", "Enter your Security Phrase a second time to confirm it.": "Enter your Security Phrase a second time to confirm it.", - "Repeat your Security Phrase...": "Repeat your Security Phrase...", + "Repeat your Security Phrase…": "Repeat your Security Phrase…", "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.": "Your Security Key is a safety net - you can use it to restore access to your encrypted messages if you forget your Security Phrase.", "Keep a copy of it somewhere secure, like a password manager or even a safe.": "Keep a copy of it somewhere secure, like a password manager or even a safe.", "Your Security Key": "Your Security Key", @@ -3407,7 +3625,7 @@ "Secure your backup with a Security Phrase": "Secure your backup with a Security Phrase", "Confirm your Security Phrase": "Confirm your Security Phrase", "Make a copy of your Security Key": "Make a copy of your Security Key", - "Starting backup...": "Starting backup...", + "Starting backup…": "Starting backup…", "Success!": "Success!", "Create key backup": "Create key backup", "Unable to create key backup": "Unable to create key backup", @@ -3420,8 +3638,10 @@ "Restore": "Restore", "You'll need to authenticate with the server to confirm the upgrade.": "You'll need to authenticate with the server to confirm the upgrade.", "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.": "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", - "Enter a security phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.": "Enter a security phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", + "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.": "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.": "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.", + "%(downloadButton)s or %(copyButton)s": "%(downloadButton)s or %(copyButton)s", + "Your keys are now being backed up from this device.": "Your keys are now being backed up from this device.", "Unable to query secret storage status": "Unable to query secret storage status", "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.": "If you cancel now, you may lose encrypted messages & data if you lose access to your logins.", "You can also set up Secure Backup & manage your keys in Settings.": "You can also set up Secure Backup & manage your keys in Settings.", @@ -3429,10 +3649,10 @@ "Set a Security Phrase": "Set a Security Phrase", "Confirm Security Phrase": "Confirm Security Phrase", "Save your Security Key": "Save your Security Key", + "Secure Backup successful": "Secure Backup successful", "Unable to set up secret storage": "Unable to set up secret storage", "Passphrases must match": "Passphrases must match", "Passphrase must not be empty": "Passphrase must not be empty", - "Unknown error": "Unknown error", "Export room keys": "Export room keys", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", From 57d5ec35b10ee38b90ac2475ef6345f893190349 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Jun 2023 11:34:02 -0400 Subject: [PATCH 095/176] Undo accidental deletion --- src/HtmlUtils.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index f9762b5483d..053df4c71c3 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -28,6 +28,7 @@ import { IContent } from "matrix-js-sdk/src/models/event"; import { Optional } from "matrix-events-sdk"; import _Linkify from "linkify-react"; import escapeHtml from "escape-html"; +import GraphemeSplitter from "graphemer"; import { _linkifyElement, From ed1f4029a9141607cac8691673e6535a7866cf1c Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:28:53 -0400 Subject: [PATCH 096/176] fix: code style and white space --- src/HtmlUtils.tsx | 3 ++- src/autocomplete/EmojiProvider.tsx | 22 +++++++++------- src/components/structures/MessagePanel.tsx | 26 ++++++++----------- .../views/emojipicker/EmojiPicker.tsx | 1 - src/components/views/messages/TextualBody.tsx | 5 ++-- .../views/rooms/MessageComposerButtons.tsx | 1 - .../views/rooms/SendMessageComposer.tsx | 10 ++----- src/emoji.ts | 6 ++--- 8 files changed, 33 insertions(+), 41 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 053df4c71c3..38bc88cff8e 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -585,7 +585,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (opts.forComposerQuote) { sanitizeParams = composerSanitizeHtmlParams; } - + let strippedBody: string; let safeBody: string | undefined; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext @@ -607,6 +607,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op const highlighter = safeHighlights?.length ? new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink) : null; + if (isFormattedBody) { if (highlighter) { // XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 8a32c1785a4..49224814b06 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -24,17 +24,18 @@ import EMOTICON_REGEX from "emojibase-regex/emoticon"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixClientPeg } from "../MatrixClientPeg"; -import { _t } from '../languageHandler'; -import AutocompleteProvider from './AutocompleteProvider'; -import QueryMatcher from './QueryMatcher'; -import { PillCompletion } from './Components'; -import { ICompletion, ISelectionRange } from './Autocompleter'; +import { _t } from "../languageHandler"; +import AutocompleteProvider from "./AutocompleteProvider"; +import QueryMatcher from "./QueryMatcher"; +import { PillCompletion } from "./Components"; +import { ICompletion, ISelectionRange } from "./Autocompleter"; import SettingsStore from "../settings/SettingsStore"; -import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji'; -import { TimelineRenderingType } from '../contexts/RoomContext'; -import * as recent from '../emojipicker/recent'; -import { decryptFile } from '../utils/DecryptFile'; -import { mediaFromMxc } from '../customisations/Media'; +import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji"; +import { TimelineRenderingType } from "../contexts/RoomContext"; +import * as recent from "../emojipicker/recent"; +import { filterBoolean } from "../utils/arrays"; +import { decryptFile } from "../utils/DecryptFile"; +import { mediaFromMxc } from "../customisations/Media"; const LIMIT = 20; @@ -130,6 +131,7 @@ export default class EmojiProvider extends AutocompleteProvider { if (!SettingsStore.getValue("MessageComposerInput.suggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } + this.emotes = await this.emotesPromise; const emojisAndEmotes=[...SORTED_EMOJI]; for (const key in this.emotes) { diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 52553f155f6..9045acf5e15 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -14,23 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from 'react'; -import ReactDOM from 'react-dom'; -import classNames from 'classnames'; -import { Room } from 'matrix-js-sdk/src/models/room'; -import { EventType } from 'matrix-js-sdk/src/@types/event'; -import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; -import { Relations } from "matrix-js-sdk/src/models/relations"; -import { logger } from 'matrix-js-sdk/src/logger'; +import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from "react"; +import ReactDOM from "react-dom"; +import classNames from "classnames"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { logger } from "matrix-js-sdk/src/logger"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; import { Optional } from "matrix-events-sdk"; -import shouldHideEvent from '../../shouldHideEvent'; -import { wantsDateSeparator } from '../../DateUtils'; -import { MatrixClientPeg } from '../../MatrixClientPeg'; -import SettingsStore from '../../settings/SettingsStore'; +import shouldHideEvent from "../../shouldHideEvent"; +import { wantsDateSeparator } from "../../DateUtils"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import SettingsStore from "../../settings/SettingsStore"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { Layout } from "../../settings/enums/Layout"; import { _t } from "../../languageHandler"; @@ -59,9 +58,6 @@ import { haveRendererForEvent } from "../../events/EventTileFactory"; import { editorRoomKey } from "../../Editing"; import { hasThreadSummary } from "../../utils/EventUtils"; import { VoiceBroadcastInfoEventType } from "../../voice-broadcast"; -import { mediaFromMxc } from "../../customisations/Media"; -import { mockStateEventImplementation } from '../../../test/test-utils'; - const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 9c0745cd564..6bab7a7c303 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -342,7 +342,6 @@ class EmojiPicker extends React.Component { if (lcFilter.includes(this.state.filter)) { emojis = this.memoizedDataByCategory[cat.id]; } else { - emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id]; if(cat.id==="custom"){ emojis=this.finalEmotes diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index c36d347caaa..13ecd6db4ad 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -45,7 +45,9 @@ import { IBodyProps } from "./IBodyProps"; import RoomContext from "../../../contexts/RoomContext"; import AccessibleButton from "../elements/AccessibleButton"; import { options as linkifyOpts } from "../../../linkify-matrix"; -import { getParentEventId } from '../../../utils/Reply'; +import { getParentEventId } from "../../../utils/Reply"; +import { EditWysiwygComposer } from "../rooms/wysiwyg_composer"; +import { IEventTileOps } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { decryptFile } from '../../../utils/DecryptFile'; import { mediaFromMxc } from '../../../customisations/Media'; @@ -707,7 +709,6 @@ export default class TextualBody extends React.Component {

    ); } - //console.log(body) return (
    {body} diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 10c519596b4..df3576bb677 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -148,7 +148,6 @@ function emojiButton(props: IProps, room: Room): ReactElement { ); } - function uploadButton(): ReactElement { return ; } diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index f6964d6295e..a9343c988bb 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -80,7 +80,6 @@ import { getBlobSafeMimeType } from "../../../utils/blobs"; const COMPAT_STATE=new UnstableValue("m.room.clientemote_compatibility","org.matrix.msc3892.clientemote_compatibility") - export function attachMentions( sender: string, content: IContent, @@ -164,8 +163,6 @@ export function attachMentions( } } - - // Merges favouring the given relation export function attachRelation(content: IContent, relation?: IEventRelation): void { if (relation) { @@ -187,7 +184,6 @@ export function createMessageContent( emotes?:Map, compat?:boolean, ): IContent { - const isEmote = containsEmote(model); if (isEmote) { model = stripEmoteCommand(model); @@ -198,6 +194,7 @@ export function createMessageContent( model = unescapeMessage(model); const body = textSerialize(model); + let emoteBody; if (compat) { emoteBody = body.replace(/:[\w+-]+:/g, m => emotes[m] ? emotes[m] : m) @@ -224,8 +221,6 @@ export function createMessageContent( } } - - // Build the mentions property and add it to the event content. attachMentions(sender, content, model, replyToEvent); @@ -335,8 +330,6 @@ export class SendMessageComposer extends React.Component { // ignore any keypress while doing IME compositions if (this.editorRef.current?.isComposing(event)) { @@ -476,6 +469,7 @@ export class SendMessageComposer extends React.Component { const model = this.model; + if (model.isEmpty) { return; } diff --git a/src/emoji.ts b/src/emoji.ts index 1979eb622ce..918cd49b092 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import EMOJIBASE from 'emojibase-data/en/compact.json'; -import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json'; +import React from "react"; +import EMOJIBASE from "emojibase-data/en/compact.json"; +import SHORTCODES from "emojibase-data/en/shortcodes/iamcal.json"; export interface IEmoji { From 6cfd79ef2f6d45476c401a6f19ac57a72a95699b Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:34:34 -0400 Subject: [PATCH 097/176] fix: other lint and code style issues --- src/components/views/dialogs/RoomSettingsDialog.tsx | 1 - src/components/views/emojipicker/EmojiPicker.tsx | 1 - src/components/views/rooms/EmojiButton.tsx | 5 +---- src/components/views/rooms/SendMessageComposer.tsx | 2 +- src/emoji.ts | 1 - 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index a6c4958152e..0a49693788f 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -152,7 +152,6 @@ class RoomSettingsDialog extends React.Component { this.props.onFinished(true)} />, "RoomSettingsNotifications", )); - tabs.push(new Tab( ROOM_EMOTES_TAB, _td("Emotes"), diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 6bab7a7c303..a24990b88a7 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -81,7 +81,6 @@ class EmojiPicker extends React.Component { constructor(props: IProps) { super(props); - const emotesEvent = props.room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; this.emotesPromise = this.decryptEmotes(rawEmotes, props.room?.roomId); diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index ac01cbbf164..bfb9ecf7289 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -22,7 +22,7 @@ import ContextMenu, { aboveLeftOf, MenuProps, useContextMenu } from "../../struc import EmojiPicker from "../emojipicker/EmojiPicker"; import { CollapsibleButton } from "./CollapsibleButton"; import { OverflowMenuContext } from "./MessageComposerButtons"; -import { Room } from 'matrix-js-sdk/src/models/room'; +import { Room } from "matrix-js-sdk/src/models/room"; interface IEmojiButtonProps { addEmoji: (unicode: string) => boolean; @@ -77,6 +77,3 @@ export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiB ); } - - - diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index a9343c988bb..9e659ebbeb1 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -469,7 +469,7 @@ export class SendMessageComposer extends React.Component { const model = this.model; - + if (model.isEmpty) { return; } diff --git a/src/emoji.ts b/src/emoji.ts index 918cd49b092..577d92a33fb 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -18,7 +18,6 @@ import React from "react"; import EMOJIBASE from "emojibase-data/en/compact.json"; import SHORTCODES from "emojibase-data/en/shortcodes/iamcal.json"; - export interface IEmoji { label: string; group?: number; From ae011314d5165fa66608d76b4ea4dbafa06850c3 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:42:33 -0400 Subject: [PATCH 098/176] cleanup: comments in RoomEmoteSettings.tsx --- .../views/room_settings/RoomEmoteSettings.tsx | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 87c66e23979..e3d906e3d4f 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { createRef } from "react"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -24,9 +24,8 @@ import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; import { mediaFromMxc } from '../../../customisations/Media'; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; -import SettingsFieldset from '../settings/SettingsFieldset'; -import LabelledToggleSwitch from '../elements/LabelledToggleSwitch'; -import { stringify } from '../dialogs/devtools/Event'; +import SettingsFieldset from "../settings/SettingsFieldset"; +import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; const EMOTES_STATE=new UnstableValue("m.room.emotes","org.matrix.msc3892.emotes") const COMPAT_STATE=new UnstableValue("m.room.clientemote_compatibility","org.matrix.msc3892.clientemote_compatibility") interface IProps { @@ -48,7 +47,6 @@ interface IState { compatiblity: boolean; } -// TODO: Merge with EmoteSettings? export default class RoomEmoteSettings extends React.Component { private emoteUpload = createRef(); private emoteCodeUpload = createRef(); @@ -61,7 +59,6 @@ export default class RoomEmoteSettings extends React.Component { const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); - //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room.currentState.getStateEvents(EMOTES_STATE, ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const value = {}; @@ -134,8 +131,6 @@ export default class RoomEmoteSettings extends React.Component { value: value, }); - //this.emoteUpload.current.value = ""; - //this.emoteCodeUpload.current.value = ""; }; private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); @@ -166,13 +161,11 @@ export default class RoomEmoteSettings extends React.Component { const emotesMxcs = {}; const value = {}; const newPack={"images":new Map()} - // TODO: What do we do about errors? if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { - //TODO: Encrypt the shortcode and the image data before uploading if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { for (let i = 0; i < this.state.newEmoteCode.length; i++) { - const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); //await client.uploadContent(this.state.newEmoteFile); FOR UNENCRYPTED + const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); if (client.isRoomEncrypted(this.props.roomId)) { emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; } else { @@ -216,8 +209,6 @@ export default class RoomEmoteSettings extends React.Component { this.imagePack=newPack await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); - //this.emoteUpload.current.value = ""; - //this.emoteCodeUpload.current.value = ""; newState.newEmoteFileAdded = false; newState.newEmoteCodeAdded = false; newState.EmoteFieldsTouched = {}; @@ -256,7 +247,6 @@ export default class RoomEmoteSettings extends React.Component { uploadedFiles.push(file); newCodes.push(fileName); } - //reader.onload = (ev) => { this.setState({ newEmoteCodeAdded: true, newEmoteFileAdded: true, @@ -266,9 +256,6 @@ export default class RoomEmoteSettings extends React.Component { ...this.state.EmoteFieldsTouched, }, }); - //this.emoteUploadImage.current.src = URL.createObjectURL(file); - //}; - //reader.readAsDataURL(file); }; private onEmoteCodeAdd = (e: React.ChangeEvent): void => { if (e.target.value.length > 0) { @@ -331,7 +318,6 @@ export default class RoomEmoteSettings extends React.Component { this.setState({ decryptedemotes: decryptede, }); - //this.forceUpdate(); } public render(): JSX.Element { let emoteSettingsButtons; From 9e4dba72b6257781e155110b1c9758001f4f2443 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Thu, 8 Jun 2023 12:44:28 -0400 Subject: [PATCH 099/176] cleanup: comments from _EmoteSettings.pcss --- res/css/views/settings/_EmoteSettings.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index 73f69f6d501..f2ea722bd73 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -68,7 +68,7 @@ limitations under the License. > .mx_AccessibleButton_kind_link { font-size: $font-14px; - margin-inline-end: 10px; /* TODO: Use a spacing variable */ + margin-inline-end: 10px; } } } From 9f775608a9e1acda58211711e7291a6a31db0e5e Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Jun 2023 14:10:12 -0400 Subject: [PATCH 100/176] Unstable value update --- .../views/emojipicker/EmojiPicker.tsx | 7 ++++++- src/components/views/messages/TextualBody.tsx | 4 ++-- .../views/room_settings/RoomEmoteSettings.tsx | 21 ++++++++++--------- .../views/rooms/SendMessageComposer.tsx | 7 ++++--- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index a24990b88a7..0261f8af913 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -42,6 +42,7 @@ import { Room } from './matrix-js-sdk/src/models/room'; import { mediaFromMxc } from '../../../customisations/Media'; import { decryptFile } from '../../../utils/DecryptFile'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; @@ -49,6 +50,8 @@ export const EMOJIS_PER_ROW = 8; const ZERO_WIDTH_JOINER = "\u200D"; +const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") + interface IProps { selectedEmojis?: Set; onChoose(unicode: string): boolean; @@ -81,7 +84,8 @@ class EmojiPicker extends React.Component { constructor(props: IProps) { super(props); - const emotesEvent = props.room?.currentState.getStateEvents("m.room.emotes", ""); + + const emotesEvent = props.room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; this.emotesPromise = this.decryptEmotes(rawEmotes, props.room?.roomId); this.finalEmotes=[]; @@ -341,6 +345,7 @@ class EmojiPicker extends React.Component { if (lcFilter.includes(this.state.filter)) { emojis = this.memoizedDataByCategory[cat.id]; } else { + emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id]; if(cat.id==="custom"){ emojis=this.finalEmotes diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 13ecd6db4ad..e252248ad90 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -54,7 +54,7 @@ import { mediaFromMxc } from '../../../customisations/Media'; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; const MAX_HIGHLIGHT_LENGTH = 4096; -const EMOTES_STATE=new UnstableValue("m.room.emotes","org.matrix.msc3892.emotes") +const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. @@ -576,7 +576,7 @@ export default class TextualBody extends React.Component { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.mxEvent.getRoomId()); //TODO: Do not encrypt/decrypt if room is not encrypted - const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE, ""); + const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const decryptede=new Map; let durl=""; diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index e3d906e3d4f..177ed80acdd 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -26,8 +26,9 @@ import { mediaFromMxc } from '../../../customisations/Media'; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import SettingsFieldset from "../settings/SettingsFieldset"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; -const EMOTES_STATE=new UnstableValue("m.room.emotes","org.matrix.msc3892.emotes") -const COMPAT_STATE=new UnstableValue("m.room.clientemote_compatibility","org.matrix.msc3892.clientemote_compatibility") +const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") +const COMPAT_STATE=new UnstableValue("org.matrix.msc3892.clientemote_compatibility","m.room.clientemote_compatibility") +const EMOTES_COMP=new UnstableValue("im.ponies.room_emotes","m.room.room_emotes") interface IProps { roomId: string; } @@ -59,17 +60,17 @@ export default class RoomEmoteSettings extends React.Component { const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); - const emotesEvent = room.currentState.getStateEvents(EMOTES_STATE, ""); + const emotesEvent = room.currentState.getStateEvents(EMOTES_STATE.name, ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const value = {}; for (const emote in emotes) { value[emote] = emote; } - const compatEvent = room.currentState.getStateEvents(COMPAT_STATE, ""); + const compatEvent = room.currentState.getStateEvents(COMPAT_STATE.name, ""); const compat = compatEvent ? (compatEvent.getContent().isCompat || false) : false; - const imagePackEvent = room.currentState.getStateEvents("m.image_pack", ""); + const imagePackEvent = room.currentState.getStateEvents(EMOTES_COMP.name, ""); this.imagePack = imagePackEvent ? (imagePackEvent.getContent() || {"images":new Map()}) : {"images":new Map()}; if(!this.imagePack["images"]){ this.imagePack={"images":new Map()} @@ -85,7 +86,7 @@ export default class RoomEmoteSettings extends React.Component { newEmoteFile: [], deleted: false, deletedItems: {}, - canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE, client.getUserId()), + canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE.name, client.getUserId()), value: value, compatibility:compat }; @@ -205,9 +206,9 @@ export default class RoomEmoteSettings extends React.Component { } } newState.value = value; - await client.sendStateEvent(this.props.roomId, EMOTES_STATE, emotesMxcs, ""); + await client.sendStateEvent(this.props.roomId, EMOTES_STATE.name, emotesMxcs, ""); this.imagePack=newPack - await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); + await client.sendStateEvent(this.props.roomId, EMOTES_COMP.name, this.imagePack, ""); newState.newEmoteFileAdded = false; newState.newEmoteCodeAdded = false; @@ -281,7 +282,7 @@ export default class RoomEmoteSettings extends React.Component { private onCompatChange = async (allowed: boolean) => { const client = MatrixClientPeg.get(); - await client.sendStateEvent(this.props.roomId, COMPAT_STATE, { isCompat: allowed }, ""); + await client.sendStateEvent(this.props.roomId, COMPAT_STATE.name, { isCompat: allowed }, ""); if (allowed) { for (const shortcode in this.state.emotes) { @@ -296,7 +297,7 @@ export default class RoomEmoteSettings extends React.Component { } } - await client.sendStateEvent(this.props.roomId, "m.image_pack", this.imagePack, ""); + await client.sendStateEvent(this.props.roomId, EMOTES_COMP.name, this.imagePack, ""); } this.setState({ diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 9e659ebbeb1..f2171f1bcfb 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -78,7 +78,8 @@ import { getBlobSafeMimeType } from "../../../utils/blobs"; * @param editedContent - The content of the parent event being edited. */ -const COMPAT_STATE=new UnstableValue("m.room.clientemote_compatibility","org.matrix.msc3892.clientemote_compatibility") +const COMPAT_STATE=new UnstableValue("org.matrix.msc3892.clientemote_compatibility","m.room.clientemote_compatibility") +const EMOTES_COMP=new UnstableValue("im.ponies.room_emotes","m.room.room_emotes") export function attachMentions( sender: string, @@ -305,10 +306,10 @@ export class SendMessageComposer extends React.Component()}) : {"images":new Map()}; this.emotes=new Map() From 9d50f1a5dd642ffa11f88c37d63b9ae4a1e3df80 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Jun 2023 14:23:04 -0400 Subject: [PATCH 101/176] Typo fix --- src/components/views/room_settings/RoomEmoteSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 177ed80acdd..00855e98ce0 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -45,7 +45,7 @@ interface IState { deleted: boolean; deletedItems: Dictionary; value: Dictionary; - compatiblity: boolean; + compatibility: boolean; } export default class RoomEmoteSettings extends React.Component { From c79780d73895ce9d8cb36b1111d9da8dd3b800aa Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 8 Jun 2023 14:57:05 -0400 Subject: [PATCH 102/176] added comment about use of dangerouslySetInnerHTML --- src/autocomplete/Components.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index f894e3acb8b..fbf39d3d4c8 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -51,6 +51,9 @@ interface IPillCompletionProps extends ITextualCompletionProps { children?: React.ReactNode; } +/*The HTML in the dangerouslySetInnerHTML for title below is generated in and comes from EmojiProvider so it should be safe to set it here +if there are still concerns the sanitizeHTML function can be used +*/ export const PillCompletion = forwardRef((props, ref) => { const { title, From 9fec256183d9c3481d819e1fa94982fa80457520 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:39:04 -0400 Subject: [PATCH 103/176] Directly use react components instead of using dangerouslySetInnerHTML --- src/autocomplete/Components.tsx | 7 +++---- src/autocomplete/EmojiProvider.tsx | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index fbf39d3d4c8..0ffadc49971 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -51,9 +51,7 @@ interface IPillCompletionProps extends ITextualCompletionProps { children?: React.ReactNode; } -/*The HTML in the dangerouslySetInnerHTML for title below is generated in and comes from EmojiProvider so it should be safe to set it here -if there are still concerns the sanitizeHTML function can be used -*/ + export const PillCompletion = forwardRef((props, ref) => { const { title, @@ -62,6 +60,7 @@ export const PillCompletion = forwardRef((props, ref) className, children, "aria-selected": ariaSelectedAttribute, + isEmote, ...restProps } = props; return ( @@ -73,7 +72,7 @@ export const PillCompletion = forwardRef((props, ref) ref={ref} > { children } - { } + { isEmote?:title } { subtitle } { description }
    diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 49224814b06..1b0b8d5aa6f 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -105,21 +105,20 @@ export default class EmojiProvider extends AutocompleteProvider { } private async decryptEmotes(emotes: Object, roomId: string) { - const decryptede=new Map(); + const decryptedEmoteMap=new Map(); const client = MatrixClientPeg.get(); - let durl = ""; + let decryptedurl = ""; const isEnc=client.isRoomEncrypted(roomId); for (const shortcode in emotes) { if (isEnc) { const blob = await decryptFile(emotes[shortcode]); - durl = URL.createObjectURL(blob); + decryptedurl = URL.createObjectURL(blob); } else { - durl = mediaFromMxc(emotes[shortcode]).srcHttp; + decryptedurl = mediaFromMxc(emotes[shortcode]).srcHttp; } - decryptede[shortcode] = ""; + decryptedEmoteMap[":"+shortcode+":"] = decryptedurl } - return decryptede; + return decryptedEmoteMap; } async getCompletions( @@ -137,7 +136,7 @@ export default class EmojiProvider extends AutocompleteProvider { for (const key in this.emotes) { emojisAndEmotes.push({ emoji: { label: key, - shortcodes: [this.emotes[key]], + shortcodes: [key], hexcode: key, unicode: this.emotes[key], @@ -187,10 +186,10 @@ export default class EmojiProvider extends AutocompleteProvider { }); return completions.map(c => ({ - completion: this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode, + completion: this.emotes[c.emoji.hexcode]? c.emoji.hexcode:c.emoji.unicode, component: ( - - { this.emotes[c.emoji.hexcode]? ":"+c.emoji.hexcode+":":c.emoji.unicode } + + { this.emotes[c.emoji.hexcode]? c.emoji.hexcode:c.emoji.unicode } ), range: range!, From 3e2f5064aee832dc8517234208ffe93990035b8d Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:53:59 -0400 Subject: [PATCH 104/176] bug fix in textualbody --- src/components/views/messages/TextualBody.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index e252248ad90..526545be383 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -93,6 +93,7 @@ export default class TextualBody extends React.Component { private applyFormatting(): void { this.decryptEmotes(); + const content = this.contentRef.current!; const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); this.activateSpoilers([this.contentRef.current]); @@ -644,6 +645,7 @@ export default class TextualBody extends React.Component { stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, + emotes: this.state.finalEmotes, }); } if (this.props.replacingEventId) { From 832f316f432edb6c060bd88698608fbfc281c8f4 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:39:18 -0400 Subject: [PATCH 105/176] Restrict characters for shortcodes --- src/components/views/messages/TextualBody.tsx | 2 +- .../views/room_settings/RoomEmoteSettings.tsx | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 526545be383..e89f2e1565b 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -590,7 +590,7 @@ export default class TextualBody extends React.Component { durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp; } - decryptede[":" + shortcode + ":"] = ""; } this.setState({ diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 00855e98ce0..eb0fe57cab1 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -29,6 +29,7 @@ import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") const COMPAT_STATE=new UnstableValue("org.matrix.msc3892.clientemote_compatibility","m.room.clientemote_compatibility") const EMOTES_COMP=new UnstableValue("im.ponies.room_emotes","m.room.room_emotes") +const SHORTCODE_REGEX=/[^a-zA-Z0-9]/g; interface IProps { roomId: string; } @@ -226,8 +227,8 @@ export default class RoomEmoteSettings extends React.Component { private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); const b = this.state.value; - b[id] = e.target.value; - this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value } }); + b[id] = e.target.value.replace(SHORTCODE_REGEX, ""); + this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value.replace(SHORTCODE_REGEX, "") } }); }; private onEmoteFileAdd = (e: React.ChangeEvent): void => { @@ -259,9 +260,9 @@ export default class RoomEmoteSettings extends React.Component { }); }; private onEmoteCodeAdd = (e: React.ChangeEvent): void => { - if (e.target.value.length > 0) { + if (e.target.value.replace(SHORTCODE_REGEX, "").length > 0) { const updatedCode = this.state.newEmoteCode; - updatedCode[e.target.getAttribute("data-index")]=e.target.value; + updatedCode[e.target.getAttribute("data-index")]=e.target.value.replace(SHORTCODE_REGEX, ""); this.setState({ newEmoteCodeAdded: true, newEmoteCode: updatedCode, @@ -271,7 +272,7 @@ export default class RoomEmoteSettings extends React.Component { }); } else { const updatedCode=this.state.newEmoteCode; - updatedCode[e.target.getAttribute("data-index")]=e.target.value; + updatedCode[e.target.getAttribute("data-index")]=e.target.value.replace(SHORTCODE_REGEX, ""); this.setState({ newEmoteCodeAdded: false, newEmoteCode: updatedCode, From 99b135477f254403773b57e659182dc5208d2690 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 9 Jun 2023 17:23:36 -0400 Subject: [PATCH 106/176] allow underscore in shortcode --- src/components/views/messages/TextualBody.tsx | 2 +- src/components/views/room_settings/RoomEmoteSettings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index e89f2e1565b..5bfd2b6f500 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -590,7 +590,7 @@ export default class TextualBody extends React.Component { durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp; } - decryptede[":" + shortcode + ":"] = ""; } this.setState({ diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index eb0fe57cab1..acb8df7a808 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -29,7 +29,7 @@ import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") const COMPAT_STATE=new UnstableValue("org.matrix.msc3892.clientemote_compatibility","m.room.clientemote_compatibility") const EMOTES_COMP=new UnstableValue("im.ponies.room_emotes","m.room.room_emotes") -const SHORTCODE_REGEX=/[^a-zA-Z0-9]/g; +const SHORTCODE_REGEX=/[^a-zA-Z0-9_]/g; interface IProps { roomId: string; } From 646dd3bd096210eb930c37b2c8734d13706c6779 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 9 Jun 2023 22:46:39 -0400 Subject: [PATCH 107/176] Remove cheerio based bug fix --- src/HtmlUtils.tsx | 80 +++++------------------------------------------ 1 file changed, 8 insertions(+), 72 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 38bc88cff8e..ff616bbea03 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -476,91 +476,27 @@ export function formatEmojis(message: string | undefined, isHtmlMessage: boolean let text = ""; let key = 0; - let cleanmsg = ''; - let titles = []; - let alts = []; - let spoilers=[]; - const $ = cheerio.load(message, { - // @ts-ignore: The `_useHtmlParser2` internal option is the - // simplest way to both parse and render using `htmlparser2`. - _useHtmlParser2: true, - decodeEntities: false, - }); - if (isHtmlMessage) { - - $("img").each(function (i, elm) { - - titles.push($(this).attr("title") ? $(this).attr("title") : null) - alts.push($(this).attr("alt") ? $(this).attr("alt") : null) - $(this).attr("title", "placeholder") - $(this).attr("alt", "placeholder") - } - ) - - $("span").each(function (i, elm) { - if($(this).attr("data-mx-spoiler")){ - spoilers.push($(this).attr("data-mx-spoiler")) - $(this).attr("data-mx-spoiler", "placeholder") - } - - }) - cleanmsg = $.html() - } - else { - cleanmsg = message; - } - // We use lodash's grapheme splitter to avoid breaking apart compound emojis - for (const char of split(cleanmsg, '')) { + const splitter = new GraphemeSplitter(); + for (const char of splitter.iterateGraphemes(message)) { if (EMOJIBASE_REGEX.test(char)) { - if (!isHtmlMessage) { - if (text) { - result.push(text); - text = ''; - } - - result.push(emojiToSpan(char, key)); - key++; - } else { - text += emojiToSpan(char, key); + if (text) { + result.push(text); + text = ""; } + result.push(emojiToSpan(char, key)); + key++; } else { text += char; } } - if (isHtmlMessage) { - console.log("text ",text) - const $ = cheerio.load(text, { - // @ts-ignore: The `_useHtmlParser2` internal option is the - // simplest way to both parse and render using `htmlparser2`. - _useHtmlParser2: true, - decodeEntities: false, - }); - let cnt = 0; - let cntalt = 0; - let splrcnt = 0; - $("img").each(function (i, elm) { - $(this).attr("title", titles[cnt]) - cnt = cnt + 1; - $(this).attr("alt", alts[cntalt]) - cntalt += 1; - } - ) - $("span").each(function (i, elm) { - if($(this).attr("data-mx-spoiler")){ - $(this).attr("data-mx-spoiler", spoilers[splrcnt]) - splrcnt+=1; - } - - }) - text = $.html() - } if (text) { result.push(text); } return result; } + /* turn a matrix event body into html * * content: 'content' of the MatrixEvent From 351e4a5b19531447ab2970368990ce83b2f42041 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 12 Jun 2023 09:36:32 -0400 Subject: [PATCH 108/176] Restore some accidentally removed code from emojiprovider --- src/autocomplete/EmojiProvider.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 1b0b8d5aa6f..f43f05ec07e 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -185,6 +185,21 @@ export default class EmojiProvider extends AutocompleteProvider { } }); + //if there is an exact shortcode match in the frequently used emojis, it goes before everything + for (let i = 0; i < recentlyUsedAutocomplete.length; i++) { + if (recentlyUsedAutocomplete[i].emoji.shortcodes[0] === trimmedMatch) { + const exactMatchEmoji = recentlyUsedAutocomplete[i]; + for (let j = i; j > 0; j--) { + recentlyUsedAutocomplete[j] = recentlyUsedAutocomplete[j - 1]; + } + recentlyUsedAutocomplete[0] = exactMatchEmoji; + break; + } + } + + completions = recentlyUsedAutocomplete.concat(completions); + completions = uniqBy(completions, "emoji"); + return completions.map(c => ({ completion: this.emotes[c.emoji.hexcode]? c.emoji.hexcode:c.emoji.unicode, component: ( From 029e1bc1bb5c9ca6999018d0da28f197b0fffa69 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 09:15:58 -0400 Subject: [PATCH 109/176] Remove some duplicated code --- src/components/views/emojipicker/EmojiPicker.tsx | 1 - src/components/views/messages/TextualBody.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 0261f8af913..fd77f803710 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -37,7 +37,6 @@ import { Key } from "../../../Keyboard"; import { clamp } from "../../../utils/numbers"; import { ButtonEvent } from "../elements/AccessibleButton"; import { Ref } from "../../../accessibility/roving/types"; -import Category, { ICategory, CategoryKey } from "./Category"; import { Room } from './matrix-js-sdk/src/models/room'; import { mediaFromMxc } from '../../../customisations/Media'; import { decryptFile } from '../../../utils/DecryptFile'; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 5bfd2b6f500..9cb33aa7887 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -94,9 +94,6 @@ export default class TextualBody extends React.Component { private applyFormatting(): void { this.decryptEmotes(); const content = this.contentRef.current!; - const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); - this.activateSpoilers([this.contentRef.current]); - const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); this.activateSpoilers([content]); From c825e41d3cd2c9246cd6690d09d4ec8d2771c782 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 09:35:06 -0400 Subject: [PATCH 110/176] Update RoomSettingsDialog.tsx --- .../views/dialogs/RoomSettingsDialog.tsx | 104 +++++++++++------- 1 file changed, 65 insertions(+), 39 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 0a49693788f..a433b2e5078 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -41,13 +41,18 @@ import { NonEmptyArray } from "../../../@types/common"; import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab"; import ErrorBoundary from "../elements/ErrorBoundary"; -export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; -export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; -export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; -export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; -export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; -export const ROOM_EMOTES_TAB = "ROOM_EMOTES_TAB"; -export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; + +export const enum RoomSettingsTab { + General = "ROOM_GENERAL_TAB", + Voip = "ROOM_VOIP_TAB", + Security = "ROOM_SECURITY_TAB", + Roles = "ROOM_ROLES_TAB", + Notifications = "ROOM_NOTIFICATIONS_TAB", + Bridges = "ROOM_BRIDGES_TAB", + Advanced = "ROOM_ADVANCED_TAB", + Emotes = "ROOM_EMOTES_TAB", + PollHistory = "ROOM_POLL_HISTORY_TAB", +} interface IProps { roomId: string; @@ -121,39 +126,60 @@ class RoomSettingsDialog extends React.Component { private getTabs(): NonEmptyArray> { const tabs: Tab[] = []; + tabs.push( + new Tab( + RoomSettingsTab.General, + _td("General"), + "mx_RoomSettingsDialog_settingsIcon", + , + "RoomSettingsGeneral", + ), + ); + if (SettingsStore.getValue("feature_group_calls")) { + tabs.push( + new Tab( + RoomSettingsTab.Voip, + _td("Voice & Video"), + "mx_RoomSettingsDialog_voiceIcon", + , + ), + ); + } + tabs.push( + new Tab( + RoomSettingsTab.Security, + _td("Security & Privacy"), + "mx_RoomSettingsDialog_securityIcon", + this.props.onFinished(true)} />, + "RoomSettingsSecurityPrivacy", + ), + ); + tabs.push( + new Tab( + RoomSettingsTab.Roles, + _td("Roles & Permissions"), + "mx_RoomSettingsDialog_rolesIcon", + , + "RoomSettingsRolesPermissions", + ), + ); + tabs.push( + new Tab( + RoomSettingsTab.Notifications, + _td("Notifications"), + "mx_RoomSettingsDialog_notificationsIcon", + ( + this.props.onFinished(true)} + /> + ), + "RoomSettingsNotifications", + ), + ); + tabs.push(new Tab( - ROOM_GENERAL_TAB, - _td("General"), - "mx_RoomSettingsDialog_settingsIcon", - , - "RoomSettingsGeneral", - )); - tabs.push(new Tab( - ROOM_SECURITY_TAB, - _td("Security & Privacy"), - "mx_RoomSettingsDialog_securityIcon", - this.props.onFinished(true)} - />, - "RoomSettingsSecurityPrivacy", - )); - tabs.push(new Tab( - ROOM_ROLES_TAB, - _td("Roles & Permissions"), - "mx_RoomSettingsDialog_rolesIcon", - , - "RoomSettingsRolesPermissions", - )); - tabs.push(new Tab( - ROOM_NOTIFICATIONS_TAB, - _td("Notifications"), - "mx_RoomSettingsDialog_notificationsIcon", - this.props.onFinished(true)} />, - "RoomSettingsNotifications", - )); - tabs.push(new Tab( - ROOM_EMOTES_TAB, + RoomSettingsTab.Emotes, _td("Emotes"), "mx_RoomSettingsDialog_emotesIcon", , From d717eb126ac454828ca0385f36f8b23130d86517 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 10:19:16 -0400 Subject: [PATCH 111/176] Lint fixes 1 --- src/autocomplete/EmojiProvider.tsx | 1 - src/components/views/emojipicker/Emoji.tsx | 1 - .../views/emojipicker/EmojiPicker.tsx | 16 +++++++------- src/components/views/rooms/EmojiButton.tsx | 2 +- .../views/rooms/MessageComposerButtons.tsx | 22 +++++++++++++++++-- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index f43f05ec07e..25c4663c9f7 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -33,7 +33,6 @@ import SettingsStore from "../settings/SettingsStore"; import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji"; import { TimelineRenderingType } from "../contexts/RoomContext"; import * as recent from "../emojipicker/recent"; -import { filterBoolean } from "../utils/arrays"; import { decryptFile } from "../utils/DecryptFile"; import { mediaFromMxc } from "../customisations/Media"; diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index 6d5555bdcd7..52e7e7e4e3b 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -43,7 +43,6 @@ class Emoji extends React.PureComponent { onMouseEnter={() => onMouseEnter(emoji)} onMouseLeave={() => onMouseLeave(emoji)} className="mx_EmojiPicker_item_wrapper" - label={emoji.customLabel?emoji.customLabel:emoji.unicode} disabled={this.props.disabled} role={this.props.role} focusOnMouseOver diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index fd77f803710..2062176a76f 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -37,7 +37,7 @@ import { Key } from "../../../Keyboard"; import { clamp } from "../../../utils/numbers"; import { ButtonEvent } from "../elements/AccessibleButton"; import { Ref } from "../../../accessibility/roving/types"; -import { Room } from './matrix-js-sdk/src/models/room'; +import { Room } from 'matrix-js-sdk/src/models/room'; import { mediaFromMxc } from '../../../customisations/Media'; import { decryptFile } from '../../../utils/DecryptFile'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -77,7 +77,7 @@ class EmojiPicker extends React.Component { private scrollRef = React.createRef>(); private emotes: Map; - private emotesPromise: Promise>; + private emotesPromise: Promise>; private finalEmotes: IEmoji[]; private finalEmotesMap:Map; constructor(props: IProps) { @@ -213,20 +213,20 @@ class EmojiPicker extends React.Component { } private async decryptEmotes(emotes: Object, roomId: string) { - const decryptede=new Map(); + const decryptedemotes=new Map(); const client = MatrixClientPeg.get(); - let durl = ""; + let decryptedurl = ""; const isEnc=client.isRoomEncrypted(roomId); for (const shortcode in emotes) { if (isEnc) { const blob = await decryptFile(emotes[shortcode]); - durl = URL.createObjectURL(blob); + decryptedurl = URL.createObjectURL(blob); } else { - durl = mediaFromMxc(emotes[shortcode]).srcHttp; + decryptedurl = mediaFromMxc(emotes[shortcode]).srcHttp; } - decryptede[shortcode] = ; + decryptedemotes[shortcode] = ; } - return decryptede; + return decryptedemotes; } private onScroll = () => { diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index bfb9ecf7289..10493d9f5b0 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -28,7 +28,7 @@ interface IEmojiButtonProps { addEmoji: (unicode: string) => boolean; menuPosition?: MenuProps; className?: string; - room: Room, + room?: Room, } export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiButtonProps) { diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index df3576bb677..079db2886d4 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -58,6 +58,8 @@ interface IProps { toggleButtonMenu: () => void; showVoiceBroadcastButton: boolean; onStartVoiceBroadcastClick: () => void; + isRichTextEnabled: boolean; + onComposerModeClick: () => void; } type OverflowMenuCloser = () => void; @@ -77,7 +79,15 @@ const MessageComposerButtons: React.FC = (props: IProps) => { let moreButtons: ReactNode[]; if (narrow) { mainButtons = [ - emojiButton(props,room), + isWysiwygLabEnabled ? ( + + ) : ( + emojiButton(props) + ), ]; moreButtons = [ uploadButton(), // props passed via UploadButtonContext @@ -89,7 +99,15 @@ const MessageComposerButtons: React.FC = (props: IProps) => { ]; } else { mainButtons = [ - emojiButton(props,room), + isWysiwygLabEnabled ? ( + + ) : ( + emojiButton(props) + ), uploadButton(), // props passed via UploadButtonContext ]; moreButtons = [ From 3cd801649cc2dace92239638eccb834a871a4372 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:05:45 -0400 Subject: [PATCH 112/176] Lint fixes 2 --- src/components/views/messages/TextualBody.tsx | 45 +++++-------------- src/components/views/rooms/EmojiButton.tsx | 2 +- .../views/rooms/MessageComposerButtons.tsx | 4 +- .../views/rooms/SendMessageComposer.tsx | 2 +- 4 files changed, 14 insertions(+), 39 deletions(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 9cb33aa7887..f4f0786d07c 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -610,41 +610,16 @@ export default class TextualBody extends React.Component { let isEmote = false; // only strip reply if this is the original replying event, edits thereafter do not have the fallback const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent); - let body: ReactNode; - if (SettingsStore.isEnabled("feature_extensible_events")) { - const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent; - if (extev?.isEquivalentTo(M_MESSAGE)) { - isEmote = isEventLike(extev.wireFormat, LegacyMsgType.Emote); - isNotice = isEventLike(extev.wireFormat, LegacyMsgType.Notice); - body = (HtmlUtils.bodyToHtml({ - body: extev.text, - format: extev.html ? "org.matrix.custom.html" : undefined, - formatted_body: extev.html, - msgtype: MsgType.Text, - }, this.props.highlights, { - disableBigEmoji: isEmote - || !SettingsStore.getValue('TextualBody.enableBigEmoji'), - // Part of Replies fallback support - stripReplyFallback: stripReply, - ref: this.contentRef, - returnString: false, - emotes: this.state.finalEmotes, - })); - } - } - if (!body) { - isEmote = content.msgtype === MsgType.Emote; - isNotice = content.msgtype === MsgType.Notice; - body = HtmlUtils.bodyToHtml(content, this.props.highlights, { - disableBigEmoji: isEmote - || !SettingsStore.getValue('TextualBody.enableBigEmoji'), - // Part of Replies fallback support - stripReplyFallback: stripReply, - ref: this.contentRef, - returnString: false, - emotes: this.state.finalEmotes, - }); - } + isEmote = content.msgtype === MsgType.Emote; + isNotice = content.msgtype === MsgType.Notice; + let body = HtmlUtils.bodyToHtml(content, this.props.highlights, { + disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"), + // Part of Replies fallback support + stripReplyFallback: stripReply, + ref: this.contentRef, + returnString: false, + emotes:this.state.finalEmotes + }); if (this.props.replacingEventId) { body = ( <> diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index 10493d9f5b0..9011c428950 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -52,7 +52,7 @@ export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiB }} managed={false} > - + ); } diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 079db2886d4..9170f8e7d97 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -86,7 +86,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { onClick={props.onComposerModeClick} /> ) : ( - emojiButton(props) + emojiButton(props,room) ), ]; moreButtons = [ @@ -106,7 +106,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { onClick={props.onComposerModeClick} /> ) : ( - emojiButton(props) + emojiButton(props,room) ), uploadButton(), // props passed via UploadButtonContext ]; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index f2171f1bcfb..7c692ef888a 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -41,7 +41,7 @@ import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from "../ import { findEditableEvent } from "../../../utils/EventUtils"; import SendHistoryManager from "../../../SendHistoryManager"; import { CommandCategories } from "../../../SlashCommands"; -import ContentMessages, { uploadFile } from "../../../ContentMessages"; +import ContentMessages from "../../../ContentMessages"; import { withMatrixClientHOC, MatrixClientProps } from "../../../contexts/MatrixClientContext"; import { Action } from "../../../dispatcher/actions"; import { containsEmoji } from "../../../effects/utils"; From 9fd3d1eea016287a789b2beba3efc6e06ba450d8 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 12:19:59 -0400 Subject: [PATCH 113/176] Some fixes for tests --- src/autocomplete/EmojiProvider.tsx | 6 +++--- src/components/views/dialogs/RoomSettingsDialog.tsx | 1 + src/components/views/emojipicker/EmojiPicker.tsx | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 25c4663c9f7..670c9426042 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -35,6 +35,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext"; import * as recent from "../emojipicker/recent"; import { decryptFile } from "../utils/DecryptFile"; import { mediaFromMxc } from "../customisations/Media"; +import { MatrixClient } from "matrix-js-sdk/src/client"; const LIMIT = 20; @@ -105,9 +106,8 @@ export default class EmojiProvider extends AutocompleteProvider { private async decryptEmotes(emotes: Object, roomId: string) { const decryptedEmoteMap=new Map(); - const client = MatrixClientPeg.get(); let decryptedurl = ""; - const isEnc=client.isRoomEncrypted(roomId); + const isEnc=MatrixClientPeg.get().isRoomEncrypted(roomId); for (const shortcode in emotes) { if (isEnc) { const blob = await decryptFile(emotes[shortcode]); @@ -202,7 +202,7 @@ export default class EmojiProvider extends AutocompleteProvider { return completions.map(c => ({ completion: this.emotes[c.emoji.hexcode]? c.emoji.hexcode:c.emoji.unicode, component: ( - + { this.emotes[c.emoji.hexcode]? c.emoji.hexcode:c.emoji.unicode } ), diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index a433b2e5078..af28abb5448 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -185,6 +185,7 @@ class RoomSettingsDialog extends React.Component { , "RoomSettingsNotifications", )); + if (SettingsStore.getValue("feature_bridge_state")) { tabs.push( new Tab( diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 2062176a76f..4bc4fafbdf1 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -108,14 +108,14 @@ class EmojiPicker extends React.Component { this.categories = [{ id: "recent", name: _t("Frequently Used"), - enabled: true, - visible: true, + enabled: this.recentlyUsed.length > 0, + visible: this.recentlyUsed.length > 0, ref: React.createRef(), },{ id: "custom", name: _t("Custom"), enabled: true, - visible: true, + visible: this.finalEmotes.length > 0, ref: React.createRef(), },{ id: "people", @@ -214,9 +214,8 @@ class EmojiPicker extends React.Component { private async decryptEmotes(emotes: Object, roomId: string) { const decryptedemotes=new Map(); - const client = MatrixClientPeg.get(); let decryptedurl = ""; - const isEnc=client.isRoomEncrypted(roomId); + const isEnc=MatrixClientPeg.get().isRoomEncrypted(roomId); for (const shortcode in emotes) { if (isEnc) { const blob = await decryptFile(emotes[shortcode]); From 4912b8190539c62b839c80319e643bd661a74f5d Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 14:19:44 -0400 Subject: [PATCH 114/176] Add some null checks and test fixes --- src/autocomplete/EmojiProvider.tsx | 2 +- .../views/emojipicker/EmojiPicker.tsx | 11 +++++++---- src/components/views/messages/TextualBody.tsx | 18 +++++++++--------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 670c9426042..3716dc76603 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -107,7 +107,7 @@ export default class EmojiProvider extends AutocompleteProvider { private async decryptEmotes(emotes: Object, roomId: string) { const decryptedEmoteMap=new Map(); let decryptedurl = ""; - const isEnc=MatrixClientPeg.get().isRoomEncrypted(roomId); + const isEnc=MatrixClientPeg.get()?.isRoomEncrypted(roomId); for (const shortcode in emotes) { if (isEnc) { const blob = await decryptFile(emotes[shortcode]); diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 4bc4fafbdf1..85d419ba52a 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -104,7 +104,6 @@ class EmojiPicker extends React.Component { custom: this.finalEmotes, ...DATA_BY_CATEGORY, }; - this.categories = [{ id: "recent", name: _t("Frequently Used"), @@ -170,6 +169,7 @@ class EmojiPicker extends React.Component { private async loadEmotes(){ this.emotes=await this.emotesPromise + const recentlyUsedCopy=this.recentlyUsed for (const key in this.emotes) { this.finalEmotes.push( { label: key, @@ -209,19 +209,22 @@ class EmojiPicker extends React.Component { } } }) - this.onScroll(); + if(this.recentlyUsed!=recentlyUsedCopy){ + this.onScroll(); + } + } private async decryptEmotes(emotes: Object, roomId: string) { const decryptedemotes=new Map(); let decryptedurl = ""; - const isEnc=MatrixClientPeg.get().isRoomEncrypted(roomId); + const isEnc=MatrixClientPeg.get()?.isRoomEncrypted(roomId); for (const shortcode in emotes) { if (isEnc) { const blob = await decryptFile(emotes[shortcode]); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(emotes[shortcode]).srcHttp; + decryptedurl = mediaFromMxc(emotes[shortcode])?.srcHttp; } decryptedemotes[shortcode] = ; } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index f4f0786d07c..8f1b751084a 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -572,26 +572,26 @@ export default class TextualBody extends React.Component { } private async decryptEmotes() { const client = MatrixClientPeg.get(); - const room = client.getRoom(this.props.mxEvent.getRoomId()); + const room = client?.getRoom(this.props.mxEvent.getRoomId()); //TODO: Do not encrypt/decrypt if room is not encrypted const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const decryptede=new Map; - let durl=""; - const isEnc=client.isRoomEncrypted(this.props.mxEvent.getRoomId()); + const decryptedemotes=new Map; + let decryptedurl=""; + const isEnc=client?.isRoomEncrypted(this.props.mxEvent.getRoomId()); for (const shortcode in rawEmotes) { if (isEnc) { const blob = await decryptFile(rawEmotes[shortcode]); - durl = URL.createObjectURL(blob); + decryptedurl = URL.createObjectURL(blob); } else { - durl = mediaFromMxc(rawEmotes[shortcode]).srcHttp; + decryptedurl = mediaFromMxc(rawEmotes[shortcode])?.srcHttp; } - decryptede[":" + shortcode + ":"] = ""; + decryptedemotes[":" + shortcode + ":"] = ""; } this.setState({ - finalEmotes: decryptede, + finalEmotes: decryptedemotes, }); this.forceUpdate(); } From ab9e31e6aebb76ef7fb3b1b0fb5552fbec2cb70b Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:16:41 -0400 Subject: [PATCH 115/176] emojipicker bug fix and check for room --- src/autocomplete/EmojiProvider.tsx | 4 ++- .../views/emojipicker/EmojiPicker.tsx | 30 ++++++++++--------- src/components/views/messages/TextualBody.tsx | 4 ++- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 3716dc76603..cbf5b6f7984 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -89,7 +89,9 @@ export default class EmojiProvider extends AutocompleteProvider { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - this.emotesPromise = this.decryptEmotes(rawEmotes, room?.roomId); + if(room){ + this.emotesPromise = this.decryptEmotes(rawEmotes, room?.roomId); + } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], funcs: [(o) => o.emoji.shortcodes.map((s) => `:${s}:`)], diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 85d419ba52a..e087491c3fb 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -86,10 +86,15 @@ class EmojiPicker extends React.Component { const emotesEvent = props.room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - this.emotesPromise = this.decryptEmotes(rawEmotes, props.room?.roomId); + if(props.room){ + this.emotesPromise = this.decryptEmotes(rawEmotes, props.room?.roomId); + } + this.finalEmotes=[]; this.finalEmotesMap=new Map(); - this.loadEmotes() + if(props.room){ + this.loadEmotes() + } this.state = { filter: "", @@ -169,7 +174,6 @@ class EmojiPicker extends React.Component { private async loadEmotes(){ this.emotes=await this.emotesPromise - const recentlyUsedCopy=this.recentlyUsed for (const key in this.emotes) { this.finalEmotes.push( { label: key, @@ -190,29 +194,27 @@ class EmojiPicker extends React.Component { }); } - let rec=Array.from(new Set(recent.get())); + let rec=Array.from(new Set(filterBoolean(recent.get().map(x=>getEmojiFromUnicode(x)?getEmojiFromUnicode(x):this.finalEmotesMap.get(x as string)))));//Array.from(new Set(recent.get())); rec.forEach((v,i)=>{ - if(this.finalEmotesMap.get(v as string)){ + if(this.finalEmotesMap.get(v.unicode)){ if(i>=this.recentlyUsed.length){ - this.recentlyUsed.push(this.finalEmotesMap.get(v as string)) + this.recentlyUsed.push(this.finalEmotesMap.get(v.unicode)) } else{ - this.recentlyUsed[i]=this.finalEmotesMap.get(v as string) + this.recentlyUsed[i]=this.finalEmotesMap.get(v.unicode) } - } else if(getEmojiFromUnicode(v as string)){ + } else if(getEmojiFromUnicode(v.unicode)){ if(i>=this.recentlyUsed.length){ - this.recentlyUsed.push(getEmojiFromUnicode(v as string)) + this.recentlyUsed.push(getEmojiFromUnicode(v.unicode)) } else{ - this.recentlyUsed[i]=getEmojiFromUnicode(v as string) + this.recentlyUsed[i]=getEmojiFromUnicode(v.unicode) } } }) - if(this.recentlyUsed!=recentlyUsedCopy){ - this.onScroll(); - } - + + this.onScroll(); } private async decryptEmotes(emotes: Object, roomId: string) { diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 8f1b751084a..0ad2f68185d 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -92,7 +92,9 @@ export default class TextualBody extends React.Component { } private applyFormatting(): void { - this.decryptEmotes(); + if(MatrixClientPeg.get()?.getRoom(this.props.mxEvent.getRoomId())){ + this.decryptEmotes(); + } const content = this.contentRef.current!; const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers"); this.activateSpoilers([content]); From d52f35bc6933a579a2527a6c7521e4122cf1906a Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:57:16 -0400 Subject: [PATCH 116/176] test fix 3 --- src/components/views/emojipicker/EmojiPicker.tsx | 2 +- src/components/views/messages/TextualBody.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index e087491c3fb..55a5f3e32e5 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -119,7 +119,7 @@ class EmojiPicker extends React.Component { id: "custom", name: _t("Custom"), enabled: true, - visible: this.finalEmotes.length > 0, + visible: true, ref: React.createRef(), },{ id: "people", diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 0ad2f68185d..ceb8cdd4b60 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -574,12 +574,14 @@ export default class TextualBody extends React.Component { } private async decryptEmotes() { const client = MatrixClientPeg.get(); - const room = client?.getRoom(this.props.mxEvent.getRoomId()); - //TODO: Do not encrypt/decrypt if room is not encrypted + const room = client.getRoom(this.props.mxEvent.getRoomId()); const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; const decryptedemotes=new Map; let decryptedurl=""; + if(!room){ + return + } const isEnc=client?.isRoomEncrypted(this.props.mxEvent.getRoomId()); for (const shortcode in rawEmotes) { if (isEnc) { From 864ea5797299c748327a23337463053cecb0fa60 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 17:20:14 -0400 Subject: [PATCH 117/176] Update EmojiProvider.tsx --- src/autocomplete/EmojiProvider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index cbf5b6f7984..87759702ac3 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -35,7 +35,6 @@ import { TimelineRenderingType } from "../contexts/RoomContext"; import * as recent from "../emojipicker/recent"; import { decryptFile } from "../utils/DecryptFile"; import { mediaFromMxc } from "../customisations/Media"; -import { MatrixClient } from "matrix-js-sdk/src/client"; const LIMIT = 20; From 9ab8ce82f710e365aade3782b90d4c0b462e7353 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 17:40:57 -0400 Subject: [PATCH 118/176] keep custom emotes enabled in emojipicker --- src/components/views/emojipicker/EmojiPicker.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 55a5f3e32e5..289e986b357 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -380,6 +380,9 @@ class EmojiPicker extends React.Component { this.memoizedDataByCategory[cat.id] = emojis; cat.enabled = emojis.length > 0; + if(cat.id=="custom"){ + cat.enabled = true + } // The setState below doesn't re-render the header and we already have the refs for updateVisibility, so... if (cat.ref.current) { cat.ref.current.disabled = !cat.enabled; From b53ae441fc76f2ec89a4498c3b859229e4c3f48d Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 14 Jun 2023 20:35:05 -0400 Subject: [PATCH 119/176] Ran i18n --- src/i18n/strings/en_EN.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 84ca822e85d..5f5208eeb6c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1683,6 +1683,7 @@ "This room is bridging messages to the following platforms. Learn more.": "This room is bridging messages to the following platforms. Learn more.", "This room isn't bridging messages to any platforms. Learn more.": "This room isn't bridging messages to any platforms. Learn more.", "Bridges": "Bridges", + "Emotes": "Emotes", "Room Addresses": "Room Addresses", "Uploaded sound": "Uploaded sound", "Get notifications as set up in your settings": "Get notifications as set up in your settings", @@ -2183,6 +2184,10 @@ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)", "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", "Show more": "Show more", + "Upload Emote": "Upload Emote", + "Emote Compatibility on Other Clients": "Emote Compatibility on Other Clients", + "This will allow emotes sent to be compatible with non-Element clients that have custom emotes. This uses a different spec and emote images will be stored on the server unencrypted. Emotes sent before this setting is enabled will not work on the other clients. The room will have to be reloaded for this change to take effect. NOTE: The first time you turn this setting on in an encrypted room it may take some time so please be patient and wait until the toggle switch shows that it is on.": "This will allow emotes sent to be compatible with non-Element clients that have custom emotes. This uses a different spec and emote images will be stored on the server unencrypted. Emotes sent before this setting is enabled will not work on the other clients. The room will have to be reloaded for this change to take effect. NOTE: The first time you turn this setting on in an encrypted room it may take some time so please be patient and wait until the toggle switch shows that it is on.", + "Compatibility": "Compatibility", "Room Name": "Room Name", "Room Topic": "Room Topic", "Room avatar": "Room avatar", @@ -2481,6 +2486,7 @@ "Drop a Pin": "Drop a Pin", "What location type do you want to share?": "What location type do you want to share?", "Frequently Used": "Frequently Used", + "Custom": "Custom", "Smileys & People": "Smileys & People", "Animals & Nature": "Animals & Nature", "Food & Drink": "Food & Drink", From 35e091ec4691b631ea274a95ac281d55b2809b40 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Thu, 15 Jun 2023 08:28:07 -0400 Subject: [PATCH 120/176] fix: automatic lint fixes --- src/HtmlUtils.tsx | 2 +- src/autocomplete/Components.tsx | 2 +- src/components/views/emojipicker/EmojiPicker.tsx | 9 +++++---- src/components/views/messages/TextualBody.tsx | 2 +- src/components/views/room_settings/RoomEmoteSettings.tsx | 6 ++---- src/components/views/rooms/EmojiButton.tsx | 4 ++-- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index ff616bbea03..1faf02c8a21 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -579,7 +579,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op delete sanitizeParams.textFilter; } - let contentBody = safeBody ?? strippedBody; + const contentBody = safeBody ?? strippedBody; if (opts.returnString) { return contentBody; } diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 0ffadc49971..84ef85dd98f 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -72,7 +72,7 @@ export const PillCompletion = forwardRef((props, ref) ref={ref} > { children } - { isEmote?:title } + { isEmote?:title } { subtitle } { description }
    diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 289e986b357..1889599b84f 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -16,6 +16,8 @@ limitations under the License. */ import React, { Dispatch } from "react"; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { _t } from "../../../languageHandler"; import * as recent from "../../../emojipicker/recent"; @@ -37,11 +39,10 @@ import { Key } from "../../../Keyboard"; import { clamp } from "../../../utils/numbers"; import { ButtonEvent } from "../elements/AccessibleButton"; import { Ref } from "../../../accessibility/roving/types"; -import { Room } from 'matrix-js-sdk/src/models/room'; import { mediaFromMxc } from '../../../customisations/Media'; import { decryptFile } from '../../../utils/DecryptFile'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; + export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; @@ -194,7 +195,7 @@ class EmojiPicker extends React.Component { }); } - let rec=Array.from(new Set(filterBoolean(recent.get().map(x=>getEmojiFromUnicode(x)?getEmojiFromUnicode(x):this.finalEmotesMap.get(x as string)))));//Array.from(new Set(recent.get())); + const rec=Array.from(new Set(filterBoolean(recent.get().map(x=>getEmojiFromUnicode(x)?getEmojiFromUnicode(x):this.finalEmotesMap.get(x as string)))));//Array.from(new Set(recent.get())); rec.forEach((v,i)=>{ if(this.finalEmotesMap.get(v.unicode)){ if(i>=this.recentlyUsed.length){ @@ -228,7 +229,7 @@ class EmojiPicker extends React.Component { } else { decryptedurl = mediaFromMxc(emotes[shortcode])?.srcHttp; } - decryptedemotes[shortcode] = ; + decryptedemotes[shortcode] = ; } return decryptedemotes; } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index ceb8cdd4b60..10b3429beea 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -18,6 +18,7 @@ import React, { createRef, SyntheticEvent, MouseEvent } from "react"; import ReactDOM from "react-dom"; import highlight from "highlight.js"; import { MsgType } from "matrix-js-sdk/src/@types/event"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import * as HtmlUtils from "../../../HtmlUtils"; import { formatDate } from "../../../DateUtils"; @@ -51,7 +52,6 @@ import { IEventTileOps } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { decryptFile } from '../../../utils/DecryptFile'; import { mediaFromMxc } from '../../../customisations/Media'; -import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; const MAX_HIGHLIGHT_LENGTH = 4096; const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index acb8df7a808..4483e39cd01 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React, { createRef } from "react"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -23,7 +24,6 @@ import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; import { mediaFromMxc } from '../../../customisations/Media'; -import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import SettingsFieldset from "../settings/SettingsFieldset"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") @@ -440,8 +440,7 @@ export default class RoomEmoteSettings extends React.Component { multiple /> { emoteSettingsButtons } - { - { disabled={!this.state.canAddEmote} /> - } { uploadedEmotes } { existingEmotes diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index 9011c428950..f6367df8bc2 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -16,13 +16,13 @@ limitations under the License. import classNames from "classnames"; import React, { useContext } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../../languageHandler"; import ContextMenu, { aboveLeftOf, MenuProps, useContextMenu } from "../../structures/ContextMenu"; import EmojiPicker from "../emojipicker/EmojiPicker"; import { CollapsibleButton } from "./CollapsibleButton"; import { OverflowMenuContext } from "./MessageComposerButtons"; -import { Room } from "matrix-js-sdk/src/models/room"; interface IEmojiButtonProps { addEmoji: (unicode: string) => boolean; @@ -52,7 +52,7 @@ export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiB }} managed={false} > - + ); } From 3aca4b33e156dd44a1e2b7ae9ce3ea7d364344ee Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:34:07 -0400 Subject: [PATCH 121/176] type fixes --- src/HtmlUtils.tsx | 4 +- src/autocomplete/EmojiProvider.tsx | 23 ++--- .../views/emojipicker/EmojiPicker.tsx | 17 ++-- src/components/views/messages/TextualBody.tsx | 4 +- .../views/room_settings/RoomEmoteSettings.tsx | 83 ++++++++++--------- .../views/rooms/SendMessageComposer.tsx | 8 +- 6 files changed, 71 insertions(+), 68 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 1faf02c8a21..6e34339be75 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -553,7 +553,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function(safeText) { return highlighter.applyHighlights(safeText, safeHighlights).join('') - .replace(CUSTOM_EMOTES_REGEX, m => opts.emotes[m] ? opts.emotes[m] : m); + .replace(CUSTOM_EMOTES_REGEX, m => opts.emotes.get(m) ? opts.emotes.get(m) : m); }; } @@ -620,7 +620,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "mx_EventTile_bigEmoji": emojiBody, "markdown-body": isHtmlMessage && !emojiBody, }); - const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, m => opts.emotes[m] ? opts.emotes[m] : m); + const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, m => opts.emotes.get(m) ? opts.emotes.get(m) : m); if (tmp !== strippedBody) { safeBody=tmp; } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 87759702ac3..a9a2dd44f08 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -35,6 +35,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext"; import * as recent from "../emojipicker/recent"; import { decryptFile } from "../utils/DecryptFile"; import { mediaFromMxc } from "../customisations/Media"; +import { IEncryptedFile } from "../customisations/models/IMediaEventContent"; const LIMIT = 20; @@ -82,14 +83,14 @@ export default class EmojiProvider extends AutocompleteProvider { public matcher: QueryMatcher; public nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - private emotes: Map; + private emotes: Map=new Map(); private emotesPromise: Promise>; constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; if(room){ - this.emotesPromise = this.decryptEmotes(rawEmotes, room?.roomId); + this.emotesPromise = this.decryptEmotes(rawEmotes as Map, room?.roomId); } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], @@ -105,18 +106,18 @@ export default class EmojiProvider extends AutocompleteProvider { this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); } - private async decryptEmotes(emotes: Object, roomId: string) { + private async decryptEmotes(emotes: Map, roomId: string) { const decryptedEmoteMap=new Map(); let decryptedurl = ""; const isEnc=MatrixClientPeg.get()?.isRoomEncrypted(roomId); - for (const shortcode in emotes) { + for (const shortcode in emotes.keys()) { if (isEnc) { - const blob = await decryptFile(emotes[shortcode]); + const blob = await decryptFile((emotes as Map).get(shortcode)); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(emotes[shortcode]).srcHttp; + decryptedurl = mediaFromMxc((emotes as Map).get(shortcode)).srcHttp; } - decryptedEmoteMap[":"+shortcode+":"] = decryptedurl + decryptedEmoteMap.set(":"+shortcode+":", decryptedurl) } return decryptedEmoteMap; } @@ -133,7 +134,7 @@ export default class EmojiProvider extends AutocompleteProvider { this.emotes = await this.emotesPromise; const emojisAndEmotes=[...SORTED_EMOJI]; - for (const key in this.emotes) { + for (const key in this.emotes.keys()) { emojisAndEmotes.push({ emoji: { label: key, shortcodes: [key], @@ -201,10 +202,10 @@ export default class EmojiProvider extends AutocompleteProvider { completions = uniqBy(completions, "emoji"); return completions.map(c => ({ - completion: this.emotes[c.emoji.hexcode]? c.emoji.hexcode:c.emoji.unicode, + completion: this.emotes.get(c.emoji.hexcode)? c.emoji.hexcode:c.emoji.unicode, component: ( - - { this.emotes[c.emoji.hexcode]? c.emoji.hexcode:c.emoji.unicode } + + { this.emotes.get(c.emoji.hexcode)? c.emoji.hexcode:c.emoji.unicode } ), range: range!, diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 1889599b84f..4b995ec4776 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -42,6 +42,7 @@ import { Ref } from "../../../accessibility/roving/types"; import { mediaFromMxc } from '../../../customisations/Media'; import { decryptFile } from '../../../utils/DecryptFile'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; export const CATEGORY_HEADER_HEIGHT = 20; @@ -88,7 +89,7 @@ class EmojiPicker extends React.Component { const emotesEvent = props.room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; if(props.room){ - this.emotesPromise = this.decryptEmotes(rawEmotes, props.room?.roomId); + this.emotesPromise = this.decryptEmotes(rawEmotes as Map, props.room?.roomId); } this.finalEmotes=[]; @@ -175,14 +176,14 @@ class EmojiPicker extends React.Component { private async loadEmotes(){ this.emotes=await this.emotesPromise - for (const key in this.emotes) { + for (const key in this.emotes.keys()) { this.finalEmotes.push( { label: key, shortcodes: [key], hexcode: key, unicode: ":"+key+":", customLabel:key, - customComponent:this.emotes[key] + customComponent:this.emotes.get(key) } ); this.finalEmotesMap.set((":"+key+":").trim(),{ @@ -191,7 +192,7 @@ class EmojiPicker extends React.Component { hexcode: key, unicode: ":"+key+":", customLabel:key, - customComponent:this.emotes[key] + customComponent:this.emotes.get(key) }); } @@ -218,16 +219,16 @@ class EmojiPicker extends React.Component { this.onScroll(); } - private async decryptEmotes(emotes: Object, roomId: string) { + private async decryptEmotes(emotes: Map, roomId: string) { const decryptedemotes=new Map(); let decryptedurl = ""; const isEnc=MatrixClientPeg.get()?.isRoomEncrypted(roomId); - for (const shortcode in emotes) { + for (const shortcode in emotes.keys()) { if (isEnc) { - const blob = await decryptFile(emotes[shortcode]); + const blob = await decryptFile(emotes.get(shortcode) as IEncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(emotes[shortcode])?.srcHttp; + decryptedurl = mediaFromMxc(emotes.get(shortcode) as string)?.srcHttp; } decryptedemotes[shortcode] = ; } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 10b3429beea..e3990f080db 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -591,8 +591,8 @@ export default class TextualBody extends React.Component { decryptedurl = mediaFromMxc(rawEmotes[shortcode])?.srcHttp; } - decryptedemotes[":" + shortcode + ":"] = ""; + decryptedemotes.set(":" + shortcode + ":", ""); } this.setState({ finalEmotes: decryptedemotes, diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 4483e39cd01..9bca84ddb24 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -26,6 +26,7 @@ import { decryptFile } from "../../../utils/DecryptFile"; import { mediaFromMxc } from '../../../customisations/Media'; import SettingsFieldset from "../settings/SettingsFieldset"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; +import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") const COMPAT_STATE=new UnstableValue("org.matrix.msc3892.clientemote_compatibility","m.room.clientemote_compatibility") const EMOTES_COMP=new UnstableValue("im.ponies.room_emotes","m.room.room_emotes") @@ -35,8 +36,8 @@ interface IProps { } interface IState { - emotes: Dictionary; - decryptedemotes: Dictionary; + emotes: Map; + decryptedemotes: Map; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; newEmoteCodeAdded: boolean; @@ -44,8 +45,8 @@ interface IState { newEmoteFile: Array; canAddEmote: boolean; deleted: boolean; - deletedItems: Dictionary; - value: Dictionary; + deletedItems: Map; + value: Map; compatibility: boolean; } @@ -63,9 +64,9 @@ export default class RoomEmoteSettings extends React.Component { const emotesEvent = room.currentState.getStateEvents(EMOTES_STATE.name, ""); const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const value = {}; - for (const emote in emotes) { - value[emote] = emote; + const value = new Map(); + for (const emote in emotes.keys()) { + value.set(emote,emote); } const compatEvent = room.currentState.getStateEvents(COMPAT_STATE.name, ""); @@ -78,15 +79,15 @@ export default class RoomEmoteSettings extends React.Component { } this.state = { - emotes: emotes, - decryptedemotes: {}, + emotes: emotes as Map, + decryptedemotes: new Map(), EmoteFieldsTouched: {}, newEmoteFileAdded: false, newEmoteCodeAdded: false, newEmoteCode: [""], newEmoteFile: [], deleted: false, - deletedItems: {}, + deletedItems: new Map(), canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE.name, client.getUserId()), value: value, compatibility:compat @@ -111,15 +112,15 @@ export default class RoomEmoteSettings extends React.Component { private cancelEmoteChanges = async (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); - const value = {}; + const value = new Map(); if (this.state.deleted) { for (const key in this.state.deletedItems) { - this.state.emotes[key] = this.state.deletedItems[key]; - value[key] = key; + this.state.emotes.set(key,this.state.deletedItems.get(key)); + value.set(key, key); } } document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { - value[(field as HTMLInputElement).id] = (field as HTMLInputElement).id; + value.set((field as HTMLInputElement).id, (field as HTMLInputElement).id) }); if (!this.isSaveEnabled()) return; this.setState({ @@ -129,7 +130,7 @@ export default class RoomEmoteSettings extends React.Component { newEmoteCode: [""], newEmoteFile: [], deleted: false, - deletedItems: {}, + deletedItems: new Map(), value: value, }); @@ -137,16 +138,16 @@ export default class RoomEmoteSettings extends React.Component { private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); - const cleanemotes = {}; + const cleanemotes = new Map(); const deletedItems = this.state.deletedItems; - const value = {}; + const value = new Map(); const id = e.currentTarget.getAttribute("id"); - for (const emote in this.state.emotes) { + for (const emote in this.state.emotes.keys()) { if (emote != id) { - cleanemotes[emote] = this.state.emotes[emote]; - value[emote] = emote; + cleanemotes.set(emote,this.state.emotes.get(emote)); + value.set(emote,emote); } else { - deletedItems[emote] = this.state.emotes[emote]; + deletedItems.set(emote, this.state.emotes.get(emote)); } } @@ -160,8 +161,8 @@ export default class RoomEmoteSettings extends React.Component { if (!this.isSaveEnabled()) return; const client = MatrixClientPeg.get(); const newState: Partial = {}; - const emotesMxcs = {}; - const value = {}; + const emotesMxcs = new Map(); + const value = new Map(); const newPack={"images":new Map()} if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { @@ -169,17 +170,17 @@ export default class RoomEmoteSettings extends React.Component { for (let i = 0; i < this.state.newEmoteCode.length; i++) { const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); if (client.isRoomEncrypted(this.props.roomId)) { - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + emotesMxcs.set(this.state.newEmoteCode[i], newEmote.file); } else { - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url; + emotesMxcs.set(this.state.newEmoteCode[i], newEmote.url); } value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; if(this.state.compatibility){ if (client.isRoomEncrypted(this.props.roomId)) { const compatNewEmote=await client.uploadContent(this.state.newEmoteFile[i]) - newPack["images"][this.state.newEmoteCode[i]] = {"url":compatNewEmote.content_uri}; + newPack["images"].set(this.state.newEmoteCode[i], {"url":compatNewEmote.content_uri}); } else { - newPack["images"][this.state.newEmoteCode[i]] = {"url":newEmote.url}; + newPack["images"].set(this.state.newEmoteCode[i], {"url":newEmote.url}); } } } @@ -194,14 +195,14 @@ export default class RoomEmoteSettings extends React.Component { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; if (this.imagePack["images"][shortcode]) { - newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { "url": this.imagePack["images"][shortcode]["url"] }; + newPack["images"].set(this.state.EmoteFieldsTouched[shortcode], { "url": this.imagePack["images"][shortcode]["url"] }); } } else { - emotesMxcs[shortcode] = this.state.emotes[shortcode]; - value[shortcode] = shortcode; - if (this.imagePack["images"][shortcode]) { - newPack["images"][shortcode] = { "url": this.imagePack["images"][shortcode]["url"] } + emotesMxcs.set(shortcode,this.state.emotes.get(shortcode)); + value.set(shortcode,shortcode); + if (this.imagePack["images"].get(shortcode)) { + newPack["images"].set(shortcode,{ "url": this.imagePack["images"][shortcode]["url"] }) } } } @@ -216,7 +217,7 @@ export default class RoomEmoteSettings extends React.Component { newState.EmoteFieldsTouched = {}; newState.emotes = emotesMxcs; newState.deleted = false; - newState.deletedItems = {}; + newState.deletedItems = new Map(); newState.newEmoteCode = [""]; newState.newEmoteFile =[]; } @@ -291,9 +292,9 @@ export default class RoomEmoteSettings extends React.Component { if (client.isRoomEncrypted(this.props.roomId)) { const blob = await decryptFile(this.state.emotes[shortcode]); const uploadedEmote = await client.uploadContent(blob) - this.imagePack["images"][shortcode] = { "url": uploadedEmote.content_uri }; + this.imagePack["images"].set(shortcode, { "url": uploadedEmote.content_uri }); } else { - this.imagePack["images"][shortcode] = { "url": this.state.emotes[shortcode] }; + this.imagePack["images"].set(shortcode, { "url": this.state.emotes[shortcode] }); } } } @@ -308,17 +309,17 @@ export default class RoomEmoteSettings extends React.Component { } private async decryptEmotes(isEnc: boolean) { - const decryptede = {}; - for (const shortcode in this.state.emotes) { + let decryptedemotes = new Map(); + for (const shortcode in this.state.emotes.keys()) { if (isEnc) { - const blob = await decryptFile(this.state.emotes[shortcode]); - decryptede[shortcode] = URL.createObjectURL(blob); + const blob = await decryptFile(this.state.emotes.get(shortcode) as IEncryptedFile); + decryptedemotes.set(shortcode, URL.createObjectURL(blob)); } else { - decryptede[shortcode] = mediaFromMxc(this.state.emotes[shortcode]).srcHttp; + decryptedemotes.set(shortcode, mediaFromMxc(this.state.emotes.get(shortcode) as string).srcHttp); } } this.setState({ - decryptedemotes: decryptede, + decryptedemotes: decryptedemotes, }); } public render(): JSX.Element { diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 7c692ef888a..1ae089316b2 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -198,7 +198,7 @@ export function createMessageContent( let emoteBody; if (compat) { - emoteBody = body.replace(/:[\w+-]+:/g, m => emotes[m] ? emotes[m] : m) + emoteBody = body.replace(/:[\w+-]+:/g, m => emotes.get(m) ? emotes.get(m) : m) } const content: IContent = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, @@ -210,7 +210,7 @@ export function createMessageContent( }); if (formattedBody) { if(compat){ - formattedBody=formattedBody.replace(/:[\w+-]+:/g, m => emotes[m] ? emotes[m] : m) + formattedBody=formattedBody.replace(/:[\w+-]+:/g, m => emotes.get(m) ? emotes.get(m) : m) } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; @@ -313,8 +313,8 @@ export class SendMessageComposer extends React.Component()}) : {"images":new Map()}; this.emotes=new Map() - for (const shortcode in this.imagePack["images"]){ - this.emotes[":"+shortcode+":"]=""+":"+shortcode+":" + for (const shortcode in this.imagePack["images"].keys()){ + this.emotes.set(":"+shortcode.replace(/[^a-zA-Z0-9_]/g, "")+":",""+":"+shortcode.replace(/[^a-zA-Z0-9_]/g, "")+":") } } From 56899a2d80fb8cf2f041e73b30887e7e2c233cab Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 15 Jun 2023 12:55:40 -0400 Subject: [PATCH 122/176] Type and lint fixes --- .../views/dialogs/_RoomSettingsDialog.pcss | 2 +- res/css/views/settings/_EmoteSettings.pcss | 2 +- src/HtmlUtils.tsx | 29 +- src/autocomplete/Components.tsx | 11 +- src/autocomplete/EmojiProvider.tsx | 55 ++-- src/components/structures/MessagePanel.tsx | 7 +- .../views/dialogs/RoomSettingsDialog.tsx | 19 +- src/components/views/emojipicker/Category.tsx | 2 +- src/components/views/emojipicker/Emoji.tsx | 4 +- .../views/emojipicker/EmojiPicker.tsx | 278 ++++++++++-------- src/components/views/emojipicker/Preview.tsx | 13 +- src/components/views/messages/TextualBody.tsx | 35 ++- .../views/room_settings/RoomEmoteSettings.tsx | 267 ++++++++--------- src/components/views/rooms/EmojiButton.tsx | 4 +- .../views/rooms/MessageComposerButtons.tsx | 4 +- .../views/rooms/SendMessageComposer.tsx | 61 ++-- .../settings/tabs/room/EmoteSettingsTab.tsx | 12 +- src/emoji.ts | 4 +- 18 files changed, 429 insertions(+), 380 deletions(-) diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index 76b256d62bc..3c2cfa3e1a9 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -50,7 +50,7 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/room/settings/advanced.svg"); } .mx_RoomSettingsDialog_emotesIcon::before { - mask-image: url('$(res)/img/element-icons/room/message-bar/emoji.svg'); + mask-image: url("$(res)/img/element-icons/room/message-bar/emoji.svg"); } .mx_RoomSettingsDialog .mx_Dialog_title { diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index f2ea722bd73..d9b9b4fa507 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -31,7 +31,7 @@ limitations under the License. } .mx_EmoteSettings_uploadedEmoteImage { height: 30px; - width: var(--emote-image-width) *30/var(--emote-image-height); // Sets emote height to 30px and scales the width accordingly + width: var(--emote-image-width) * 30 / var(--emote-image-height); // Sets emote height to 30px and scales the width accordingly margin-left: 30px; align-self: center; } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 6e34339be75..2d4b7fe2b5a 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -58,7 +58,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, "i"); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -const CUSTOM_EMOTES_REGEX =/:[\w+-]+:/g; +const CUSTOM_EMOTES_REGEX = /:[\w+-]+:/g; export const PERMITTED_URL_SCHEMES = [ "bitcoin", @@ -496,7 +496,6 @@ export function formatEmojis(message: string | undefined, isHtmlMessage: boolean return result; } - /* turn a matrix event body into html * * content: 'content' of the MatrixEvent @@ -551,9 +550,14 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // are interrupted by HTML tags (not that we did before) - e.g. foobar won't get highlighted // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. - sanitizeParams.textFilter = function(safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join('') - .replace(CUSTOM_EMOTES_REGEX, m => opts.emotes.get(m) ? opts.emotes.get(m) : m); + sanitizeParams.textFilter = function (safeText) { + if (opts.emotes) { + return highlighter + .applyHighlights(safeText, safeHighlights) + .join("") + .replace(CUSTOM_EMOTES_REGEX, (m) => (opts.emotes.get(m) ? opts.emotes.get(m) : m)); + } + return highlighter.applyHighlights(safeText, safeHighlights).join(""); }; } @@ -620,17 +624,18 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "mx_EventTile_bigEmoji": emojiBody, "markdown-body": isHtmlMessage && !emojiBody, }); - const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, m => opts.emotes.get(m) ? opts.emotes.get(m) : m); - if (tmp !== strippedBody) { - safeBody=tmp; + if (opts.emotes) { + const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, (m) => (opts.emotes.get(m) ? opts.emotes.get(m) : m)); + if (tmp !== strippedBody) { + safeBody = tmp; + } } - let emojiBodyElements: JSX.Element[]; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } - return safeBody ? + return safeBody ? ( , op dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> - : + ) : ( {emojiBodyElements || strippedBody} - ; + ); } /** diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 84ef85dd98f..40c8c96def7 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -51,7 +51,6 @@ interface IPillCompletionProps extends ITextualCompletionProps { children?: React.ReactNode; } - export const PillCompletion = forwardRef((props, ref) => { const { title, @@ -71,10 +70,12 @@ export const PillCompletion = forwardRef((props, ref) aria-selected={ariaSelectedAttribute} ref={ref} > - { children } - { isEmote?:title } - { subtitle } - { description } + {children} + + {isEmote ? : title} + + {subtitle} + {description} ); }); diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index a9a2dd44f08..767626057e7 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -83,14 +83,18 @@ export default class EmojiProvider extends AutocompleteProvider { public matcher: QueryMatcher; public nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; - private emotes: Map=new Map(); - private emotesPromise: Promise>; - constructor(room: Room, renderingType?: TimelineRenderingType) { + private emotes: Map = new Map(); + private emotesPromise: Promise>; + public constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); - const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - if(room){ - this.emotesPromise = this.decryptEmotes(rawEmotes as Map, room?.roomId); + const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; + const emotesMap = new Map(); + for (const shortcode in rawEmotes) { + emotesMap.set(shortcode, rawEmotes[shortcode]); + } + if (room) { + this.emotesPromise = this.decryptEmotes(emotesMap, room?.roomId); } this.matcher = new QueryMatcher(SORTED_EMOJI, { keys: [], @@ -106,23 +110,23 @@ export default class EmojiProvider extends AutocompleteProvider { this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); } - private async decryptEmotes(emotes: Map, roomId: string) { - const decryptedEmoteMap=new Map(); + private async decryptEmotes(emotes: Map, roomId: string): Promise> { + const decryptedEmoteMap = new Map(); let decryptedurl = ""; - const isEnc=MatrixClientPeg.get()?.isRoomEncrypted(roomId); - for (const shortcode in emotes.keys()) { + const isEnc = MatrixClientPeg.get()?.isRoomEncrypted(roomId); + for (const [shortcode, val] of emotes) { if (isEnc) { - const blob = await decryptFile((emotes as Map).get(shortcode)); + const blob = await decryptFile(val as IEncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc((emotes as Map).get(shortcode)).srcHttp; + decryptedurl = mediaFromMxc(val as string).srcHttp; } - decryptedEmoteMap.set(":"+shortcode+":", decryptedurl) + decryptedEmoteMap.set(":" + shortcode + ":", decryptedurl); } return decryptedEmoteMap; } - async getCompletions( + public async getCompletions( query: string, selection: ISelectionRange, force?: boolean, @@ -133,15 +137,10 @@ export default class EmojiProvider extends AutocompleteProvider { } this.emotes = await this.emotesPromise; - const emojisAndEmotes=[...SORTED_EMOJI]; - for (const key in this.emotes.keys()) { + const emojisAndEmotes = [...SORTED_EMOJI]; + for (const [key, val] of this.emotes) { emojisAndEmotes.push({ - emoji: { label: key, - shortcodes: [key], - hexcode: key, - unicode: this.emotes[key], - - }, + emoji: { label: key, shortcodes: [key], hexcode: key, unicode: val as string }, _orderBy: 0, }); } @@ -201,11 +200,15 @@ export default class EmojiProvider extends AutocompleteProvider { completions = recentlyUsedAutocomplete.concat(completions); completions = uniqBy(completions, "emoji"); - return completions.map(c => ({ - completion: this.emotes.get(c.emoji.hexcode)? c.emoji.hexcode:c.emoji.unicode, + return completions.map((c) => ({ + completion: this.emotes.get(c.emoji.hexcode) ? c.emoji.hexcode : c.emoji.unicode, component: ( - - { this.emotes.get(c.emoji.hexcode)? c.emoji.hexcode:c.emoji.unicode } + + {this.emotes.get(c.emoji.hexcode) ? c.emoji.hexcode : c.emoji.unicode} ), range: range!, diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 9045acf5e15..002f35afc3b 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -284,8 +284,11 @@ export default class MessagePanel extends React.Component { // RoomContext, however we still need a fallback for roomless MessagePanels. this._showHiddenEvents = SettingsStore.getValue("showHiddenEventsInTimeline"); - this.showTypingNotificationsWatcherRef = - SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting( + "showTypingNotifications", + null, + this.onShowTypingNotificationsChange, + ); } public componentDidMount(): void { diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index af28abb5448..8a4be2cb9ca 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -41,7 +41,6 @@ import { NonEmptyArray } from "../../../@types/common"; import { PollHistoryTab } from "../settings/tabs/room/PollHistoryTab"; import ErrorBoundary from "../elements/ErrorBoundary"; - export const enum RoomSettingsTab { General = "ROOM_GENERAL_TAB", Voip = "ROOM_VOIP_TAB", @@ -178,14 +177,16 @@ class RoomSettingsDialog extends React.Component { ), ); - tabs.push(new Tab( - RoomSettingsTab.Emotes, - _td("Emotes"), - "mx_RoomSettingsDialog_emotesIcon", - , - "RoomSettingsNotifications", - )); - + tabs.push( + new Tab( + RoomSettingsTab.Emotes, + _td("Emotes"), + "mx_RoomSettingsDialog_emotesIcon", + , + "RoomSettingsNotifications", + ), + ); + if (SettingsStore.getValue("feature_bridge_state")) { tabs.push( new Tab( diff --git a/src/components/views/emojipicker/Category.tsx b/src/components/views/emojipicker/Category.tsx index e3fa6f7ba3c..f8994da359a 100644 --- a/src/components/views/emojipicker/Category.tsx +++ b/src/components/views/emojipicker/Category.tsx @@ -25,7 +25,7 @@ import { ButtonEvent } from "../elements/AccessibleButton"; const OVERFLOW_ROWS = 3; -export type CategoryKey = (keyof typeof DATA_BY_CATEGORY) | "recent" | "custom"; +export type CategoryKey = keyof typeof DATA_BY_CATEGORY | "recent" | "custom"; export interface ICategory { id: CategoryKey; diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index 52e7e7e4e3b..32f495e7187 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -47,8 +47,8 @@ class Emoji extends React.PureComponent { role={this.props.role} focusOnMouseOver > -
    - { emoji.customComponent?emoji.customComponent:emoji.unicode } +
    + {emoji.customComponent ? emoji.customComponent : emoji.unicode}
    ); diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 4b995ec4776..adf19791467 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React, { Dispatch } from "react"; -import { Room } from 'matrix-js-sdk/src/models/room'; +import { Room } from "matrix-js-sdk/src/models/room"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { _t } from "../../../languageHandler"; @@ -39,26 +39,25 @@ import { Key } from "../../../Keyboard"; import { clamp } from "../../../utils/numbers"; import { ButtonEvent } from "../elements/AccessibleButton"; import { Ref } from "../../../accessibility/roving/types"; -import { mediaFromMxc } from '../../../customisations/Media'; -import { decryptFile } from '../../../utils/DecryptFile'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { mediaFromMxc } from "../../../customisations/Media"; +import { decryptFile } from "../../../utils/DecryptFile"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; - export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; export const EMOJIS_PER_ROW = 8; const ZERO_WIDTH_JOINER = "\u200D"; -const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") +const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); interface IProps { selectedEmojis?: Set; onChoose(unicode: string): boolean; onFinished(): void; isEmojiDisabled?: (unicode: string) => boolean; - room?:Room; + room?: Room; } interface IState { @@ -81,21 +80,24 @@ class EmojiPicker extends React.Component { private emotes: Map; private emotesPromise: Promise>; private finalEmotes: IEmoji[]; - private finalEmotesMap:Map; - constructor(props: IProps) { + private finalEmotesMap: Map; + public constructor(props: IProps) { super(props); - const emotesEvent = props.room?.currentState.getStateEvents(EMOTES_STATE.name, ""); - const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - if(props.room){ - this.emotesPromise = this.decryptEmotes(rawEmotes as Map, props.room?.roomId); + const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; + const emotesMap = new Map(); + for (const shortcode in rawEmotes) { + emotesMap.set(shortcode, rawEmotes[shortcode]); } - - this.finalEmotes=[]; - this.finalEmotesMap=new Map(); - if(props.room){ - this.loadEmotes() + if (props.room) { + this.emotesPromise = this.decryptEmotes(emotesMap, props.room?.roomId); + } + + this.finalEmotes = []; + this.finalEmotesMap = new Map(); + if (props.room) { + this.loadEmotes(); } this.state = { @@ -111,131 +113,156 @@ class EmojiPicker extends React.Component { custom: this.finalEmotes, ...DATA_BY_CATEGORY, }; - this.categories = [{ - id: "recent", - name: _t("Frequently Used"), - enabled: this.recentlyUsed.length > 0, - visible: this.recentlyUsed.length > 0, - ref: React.createRef(), - },{ - id: "custom", - name: _t("Custom"), - enabled: true, - visible: true, - ref: React.createRef(), - },{ - id: "people", - name: _t("Smileys & People"), - enabled: true, - visible: true, - ref: React.createRef(), - }, { - id: "nature", - name: _t("Animals & Nature"), - enabled: true, - visible: false, - ref: React.createRef(), - }, { - id: "foods", - name: _t("Food & Drink"), - enabled: true, - visible: false, - ref: React.createRef(), - }, { - id: "activity", - name: _t("Activities"), - enabled: true, - visible: false, - ref: React.createRef(), - }, { - id: "places", - name: _t("Travel & Places"), - enabled: true, - visible: false, - ref: React.createRef(), - }, { - id: "objects", - name: _t("Objects"), - enabled: true, - visible: false, - ref: React.createRef(), - }, { - id: "symbols", - name: _t("Symbols"), - enabled: true, - visible: false, - ref: React.createRef(), - }, { - id: "flags", - name: _t("Flags"), - enabled: true, - visible: false, - ref: React.createRef(), - }]; + this.categories = [ + { + id: "recent", + name: _t("Frequently Used"), + enabled: this.recentlyUsed.length > 0, + visible: this.recentlyUsed.length > 0, + ref: React.createRef(), + }, + { + id: "custom", + name: _t("Custom"), + enabled: true, + visible: true, + ref: React.createRef(), + }, + { + id: "people", + name: _t("Smileys & People"), + enabled: true, + visible: true, + ref: React.createRef(), + }, + { + id: "nature", + name: _t("Animals & Nature"), + enabled: true, + visible: false, + ref: React.createRef(), + }, + { + id: "foods", + name: _t("Food & Drink"), + enabled: true, + visible: false, + ref: React.createRef(), + }, + { + id: "activity", + name: _t("Activities"), + enabled: true, + visible: false, + ref: React.createRef(), + }, + { + id: "places", + name: _t("Travel & Places"), + enabled: true, + visible: false, + ref: React.createRef(), + }, + { + id: "objects", + name: _t("Objects"), + enabled: true, + visible: false, + ref: React.createRef(), + }, + { + id: "symbols", + name: _t("Symbols"), + enabled: true, + visible: false, + ref: React.createRef(), + }, + { + id: "flags", + name: _t("Flags"), + enabled: true, + visible: false, + ref: React.createRef(), + }, + ]; } - private async loadEmotes(){ - this.emotes=await this.emotesPromise - for (const key in this.emotes.keys()) { - this.finalEmotes.push( - { label: key, - shortcodes: [key], - hexcode: key, - unicode: ":"+key+":", - customLabel:key, - customComponent:this.emotes.get(key) - } - ); - this.finalEmotesMap.set((":"+key+":").trim(),{ + private async loadEmotes(): Promise { + this.emotes = await this.emotesPromise; + for (const [key, val] of this.emotes) { + this.finalEmotes.push({ + label: key, + shortcodes: [key], + hexcode: key, + unicode: ":" + key + ":", + customLabel: key, + customComponent: val, + }); + this.finalEmotesMap.set((":" + key + ":").trim(), { label: key, shortcodes: [key], hexcode: key, - unicode: ":"+key+":", - customLabel:key, - customComponent:this.emotes.get(key) + unicode: ":" + key + ":", + customLabel: key, + customComponent: val, }); } - const rec=Array.from(new Set(filterBoolean(recent.get().map(x=>getEmojiFromUnicode(x)?getEmojiFromUnicode(x):this.finalEmotesMap.get(x as string)))));//Array.from(new Set(recent.get())); - rec.forEach((v,i)=>{ - if(this.finalEmotesMap.get(v.unicode)){ - if(i>=this.recentlyUsed.length){ - this.recentlyUsed.push(this.finalEmotesMap.get(v.unicode)) - } - else{ - this.recentlyUsed[i]=this.finalEmotesMap.get(v.unicode) + const rec = Array.from( + new Set( + filterBoolean( + recent + .get() + .map((x) => + getEmojiFromUnicode(x) ? getEmojiFromUnicode(x) : this.finalEmotesMap.get(x as string), + ), + ), + ), + ); //Array.from(new Set(recent.get())); + rec.forEach((v, i) => { + if (this.finalEmotesMap.get(v.unicode)) { + if (i >= this.recentlyUsed.length) { + this.recentlyUsed.push(this.finalEmotesMap.get(v.unicode)); + } else { + this.recentlyUsed[i] = this.finalEmotesMap.get(v.unicode); } - - } else if(getEmojiFromUnicode(v.unicode)){ - if(i>=this.recentlyUsed.length){ - this.recentlyUsed.push(getEmojiFromUnicode(v.unicode)) + } else if (getEmojiFromUnicode(v.unicode)) { + if (i >= this.recentlyUsed.length) { + this.recentlyUsed.push(getEmojiFromUnicode(v.unicode)); + } else { + this.recentlyUsed[i] = getEmojiFromUnicode(v.unicode); } - else{ - this.recentlyUsed[i]=getEmojiFromUnicode(v.unicode) - } } - }) + }); - this.onScroll(); + this.onScroll(); } - private async decryptEmotes(emotes: Map, roomId: string) { - const decryptedemotes=new Map(); + private async decryptEmotes(emotes: Map, roomId: string): Promise> { + const decryptedemotes = new Map(); let decryptedurl = ""; - const isEnc=MatrixClientPeg.get()?.isRoomEncrypted(roomId); - for (const shortcode in emotes.keys()) { + const isEnc = MatrixClientPeg.get()?.isRoomEncrypted(roomId); + for (const [shortcode, val] of emotes) { if (isEnc) { - const blob = await decryptFile(emotes.get(shortcode) as IEncryptedFile); + const blob = await decryptFile(val as IEncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(emotes.get(shortcode) as string)?.srcHttp; + decryptedurl = mediaFromMxc(val as string)?.srcHttp; } - decryptedemotes[shortcode] = ; + decryptedemotes.set( + shortcode, + {":", + ); } return decryptedemotes; } - - private onScroll = () => { + + private onScroll = (): void => { const body = this.scrollRef.current?.containerRef.current; if (!body) return; this.setState({ @@ -350,10 +377,9 @@ class EmojiPicker extends React.Component { if (lcFilter.includes(this.state.filter)) { emojis = this.memoizedDataByCategory[cat.id]; } else { - emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id]; - if(cat.id==="custom"){ - emojis=this.finalEmotes + if (cat.id === "custom") { + emojis = this.finalEmotes; } } @@ -382,8 +408,8 @@ class EmojiPicker extends React.Component { this.memoizedDataByCategory[cat.id] = emojis; cat.enabled = emojis.length > 0; - if(cat.id=="custom"){ - cat.enabled = true + if (cat.id == "custom") { + cat.enabled = true; } // The setState below doesn't re-render the header and we already have the refs for updateVisibility, so... if (cat.ref.current) { diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index ea9f780e1f7..9cca63329ef 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -24,14 +24,17 @@ interface IProps { } class Preview extends React.PureComponent { - render() { - const { unicode, label, shortcodes: [shortcode], customComponent } = this.props.emoji; + public render(): React.ReactNode { + const { + unicode, + label, + shortcodes: [shortcode], + customComponent, + } = this.props.emoji; return (
    -
    - { customComponent?customComponent:unicode } -
    +
    {customComponent ? customComponent : unicode}
    {label}
    {shortcode}
    diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index e3990f080db..138bb581083 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -50,11 +50,11 @@ import { getParentEventId } from "../../../utils/Reply"; import { EditWysiwygComposer } from "../rooms/wysiwyg_composer"; import { IEventTileOps } from "../rooms/EventTile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { decryptFile } from '../../../utils/DecryptFile'; -import { mediaFromMxc } from '../../../customisations/Media'; +import { decryptFile } from "../../../utils/DecryptFile"; +import { mediaFromMxc } from "../../../customisations/Media"; const MAX_HIGHLIGHT_LENGTH = 4096; -const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") +const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. @@ -92,7 +92,7 @@ export default class TextualBody extends React.Component { } private applyFormatting(): void { - if(MatrixClientPeg.get()?.getRoom(this.props.mxEvent.getRoomId())){ + if (MatrixClientPeg.get()?.getRoom(this.props.mxEvent.getRoomId())) { this.decryptEmotes(); } const content = this.contentRef.current!; @@ -572,17 +572,14 @@ export default class TextualBody extends React.Component { } return {`(${text})`}; } - private async decryptEmotes() { + private async decryptEmotes(): Promise { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.mxEvent.getRoomId()); const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE.name, ""); - const rawEmotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; - const decryptedemotes=new Map; - let decryptedurl=""; - if(!room){ - return - } - const isEnc=client?.isRoomEncrypted(this.props.mxEvent.getRoomId()); + const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; + const decryptedemotes = new Map(); + let decryptedurl = ""; + const isEnc = client?.isRoomEncrypted(this.props.mxEvent.getRoomId()); for (const shortcode in rawEmotes) { if (isEnc) { const blob = await decryptFile(rawEmotes[shortcode]); @@ -591,15 +588,21 @@ export default class TextualBody extends React.Component { decryptedurl = mediaFromMxc(rawEmotes[shortcode])?.srcHttp; } - decryptedemotes.set(":" + shortcode + ":", ""); + decryptedemotes.set( + ":" + shortcode + ":", + "", + ); } this.setState({ finalEmotes: decryptedemotes, }); this.forceUpdate(); } - render() { + public render(): React.ReactNode { if (this.props.editState) { const isWysiwygComposerEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); return isWysiwygComposerEnabled ? ( @@ -622,7 +625,7 @@ export default class TextualBody extends React.Component { stripReplyFallback: stripReply, ref: this.contentRef, returnString: false, - emotes:this.state.finalEmotes + emotes: this.state.finalEmotes, }); if (this.props.replacingEventId) { body = ( diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 9bca84ddb24..2b553bdeab1 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -23,21 +23,24 @@ import AccessibleButton from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; -import { mediaFromMxc } from '../../../customisations/Media'; +import { mediaFromMxc } from "../../../customisations/Media"; import SettingsFieldset from "../settings/SettingsFieldset"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; -const EMOTES_STATE=new UnstableValue("org.matrix.msc3892.emotes","m.room.emotes") -const COMPAT_STATE=new UnstableValue("org.matrix.msc3892.clientemote_compatibility","m.room.clientemote_compatibility") -const EMOTES_COMP=new UnstableValue("im.ponies.room_emotes","m.room.room_emotes") -const SHORTCODE_REGEX=/[^a-zA-Z0-9_]/g; +const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); +const COMPAT_STATE = new UnstableValue( + "org.matrix.msc3892.clientemote_compatibility", + "m.room.clientemote_compatibility", +); +const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); +const SHORTCODE_REGEX = /[^a-zA-Z0-9_]/g; interface IProps { roomId: string; } interface IState { - emotes: Map; - decryptedemotes: Map; + emotes: Map; + decryptedemotes: Map; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; newEmoteCodeAdded: boolean; @@ -45,8 +48,8 @@ interface IState { newEmoteFile: Array; canAddEmote: boolean; deleted: boolean; - deletedItems: Map; - value: Map; + deletedItems: Map; + value: Map; compatibility: boolean; } @@ -54,32 +57,36 @@ export default class RoomEmoteSettings extends React.Component { private emoteUpload = createRef(); private emoteCodeUpload = createRef(); private emoteUploadImage = createRef(); - private imagePack:object; - constructor(props: IProps) { + private imagePack: object; + public constructor(props: IProps) { super(props); const client = MatrixClientPeg.get(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); - + const emotesEvent = room.currentState.getStateEvents(EMOTES_STATE.name, ""); - const emotes = emotesEvent ? (emotesEvent.getContent() || {}) : {}; + const emotes = emotesEvent ? emotesEvent.getContent() || {} : {}; const value = new Map(); - for (const emote in emotes.keys()) { - value.set(emote,emote); + const emotesMap = new Map(); + for (const shortcode in emotes) { + value.set(shortcode, shortcode); + emotesMap.set(shortcode, emotes[shortcode]); } const compatEvent = room.currentState.getStateEvents(COMPAT_STATE.name, ""); - const compat = compatEvent ? (compatEvent.getContent().isCompat || false) : false; + const compat = compatEvent ? compatEvent.getContent().isCompat || false : false; const imagePackEvent = room.currentState.getStateEvents(EMOTES_COMP.name, ""); - this.imagePack = imagePackEvent ? (imagePackEvent.getContent() || {"images":new Map()}) : {"images":new Map()}; - if(!this.imagePack["images"]){ - this.imagePack={"images":new Map()} + this.imagePack = imagePackEvent + ? imagePackEvent.getContent() || { images: new Map() } + : { images: new Map() }; + if (!this.imagePack["images"]) { + this.imagePack = { images: new Map() }; } this.state = { - emotes: emotes as Map, + emotes: emotesMap, decryptedemotes: new Map(), EmoteFieldsTouched: {}, newEmoteFileAdded: false, @@ -90,11 +97,11 @@ export default class RoomEmoteSettings extends React.Component { deletedItems: new Map(), canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE.name, client.getUserId()), value: value, - compatibility:compat + compatibility: compat, }; this.decryptEmotes(client.isRoomEncrypted(props.roomId)); } - componentDidMount() { + public componentDidMount(): void { const client = MatrixClientPeg.get(); this.decryptEmotes(client.isRoomEncrypted(this.props.roomId)); } @@ -103,10 +110,12 @@ export default class RoomEmoteSettings extends React.Component { this.emoteUpload.current.click(); }; - private isSaveEnabled = () => { - return Boolean(Object.values(this.state.EmoteFieldsTouched).length) || - (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || - (this.state.deleted); + private isSaveEnabled = (): boolean => { + return ( + Boolean(Object.values(this.state.EmoteFieldsTouched).length) || + (this.state.newEmoteCodeAdded && this.state.newEmoteFileAdded) || + this.state.deleted + ); }; private cancelEmoteChanges = async (e: React.MouseEvent): Promise => { @@ -115,12 +124,12 @@ export default class RoomEmoteSettings extends React.Component { const value = new Map(); if (this.state.deleted) { for (const key in this.state.deletedItems) { - this.state.emotes.set(key,this.state.deletedItems.get(key)); + this.state.emotes.set(key, this.state.deletedItems.get(key)); value.set(key, key); } } - document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach(field => { - value.set((field as HTMLInputElement).id, (field as HTMLInputElement).id) + document.querySelectorAll(".mx_EmoteSettings_existingEmoteCode").forEach((field) => { + value.set((field as HTMLInputElement).id, (field as HTMLInputElement).id); }); if (!this.isSaveEnabled()) return; this.setState({ @@ -133,7 +142,6 @@ export default class RoomEmoteSettings extends React.Component { deletedItems: new Map(), value: value, }); - }; private deleteEmote = (e: React.MouseEvent): Promise => { e.stopPropagation(); @@ -142,10 +150,10 @@ export default class RoomEmoteSettings extends React.Component { const deletedItems = this.state.deletedItems; const value = new Map(); const id = e.currentTarget.getAttribute("id"); - for (const emote in this.state.emotes.keys()) { + for (const emote in this.state.emotes) { if (emote != id) { - cleanemotes.set(emote,this.state.emotes.get(emote)); - value.set(emote,emote); + cleanemotes.set(emote, this.state.emotes.get(emote)); + value.set(emote, emote); } else { deletedItems.set(emote, this.state.emotes.get(emote)); } @@ -161,65 +169,72 @@ export default class RoomEmoteSettings extends React.Component { if (!this.isSaveEnabled()) return; const client = MatrixClientPeg.get(); const newState: Partial = {}; - const emotesMxcs = new Map(); + const emotesMxcs = {}; const value = new Map(); - const newPack={"images":new Map()} + const newPack = { images: new Map() }; if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { for (let i = 0; i < this.state.newEmoteCode.length; i++) { const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); if (client.isRoomEncrypted(this.props.roomId)) { - emotesMxcs.set(this.state.newEmoteCode[i], newEmote.file); + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; } else { - emotesMxcs.set(this.state.newEmoteCode[i], newEmote.url); + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url; } value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i]; - if(this.state.compatibility){ - if (client.isRoomEncrypted(this.props.roomId)) { - const compatNewEmote=await client.uploadContent(this.state.newEmoteFile[i]) - newPack["images"].set(this.state.newEmoteCode[i], {"url":compatNewEmote.content_uri}); + if (this.state.compatibility) { + if (client.isRoomEncrypted(this.props.roomId)) { + const compatNewEmote = await client.uploadContent(this.state.newEmoteFile[i]); + newPack["images"].set(this.state.newEmoteCode[i], { url: compatNewEmote.content_uri }); } else { - newPack["images"].set(this.state.newEmoteCode[i], {"url":newEmote.url}); + newPack["images"].set(this.state.newEmoteCode[i], { url: newEmote.url }); } } } } if (this.state.emotes) { - for (const shortcode in this.state.emotes) { - if ((this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) - && (shortcode in this.state.newEmoteCode)) { + for (const [shortcode, val] of this.state.emotes) { + if ( + this.state.newEmoteFileAdded && + this.state.newEmoteCodeAdded && + shortcode in this.state.newEmoteCode + ) { continue; } if (this.state.EmoteFieldsTouched.hasOwnProperty(shortcode)) { - emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = this.state.emotes[shortcode]; - value[this.state.EmoteFieldsTouched[shortcode]] = this.state.EmoteFieldsTouched[shortcode]; + emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = val; + value.set(this.state.EmoteFieldsTouched[shortcode], this.state.EmoteFieldsTouched[shortcode]); if (this.imagePack["images"][shortcode]) { - newPack["images"].set(this.state.EmoteFieldsTouched[shortcode], { "url": this.imagePack["images"][shortcode]["url"] }); + newPack["images"].set(this.state.EmoteFieldsTouched[shortcode], { + url: this.imagePack["images"][shortcode]["url"], + }); } - } else { - emotesMxcs.set(shortcode,this.state.emotes.get(shortcode)); - value.set(shortcode,shortcode); + emotesMxcs[shortcode] = val; + value.set(shortcode, shortcode); if (this.imagePack["images"].get(shortcode)) { - newPack["images"].set(shortcode,{ "url": this.imagePack["images"][shortcode]["url"] }) + newPack["images"].set(shortcode, { url: this.imagePack["images"][shortcode]["url"] }); } } } } newState.value = value; await client.sendStateEvent(this.props.roomId, EMOTES_STATE.name, emotesMxcs, ""); - this.imagePack=newPack + this.imagePack = newPack; await client.sendStateEvent(this.props.roomId, EMOTES_COMP.name, this.imagePack, ""); - + newState.newEmoteFileAdded = false; newState.newEmoteCodeAdded = false; newState.EmoteFieldsTouched = {}; - newState.emotes = emotesMxcs; + newState.emotes = new Map(); + for (const shortcode in emotesMxcs) { + newState.emotes.set(shortcode, emotesMxcs[shortcode]); + } newState.deleted = false; newState.deletedItems = new Map(); newState.newEmoteCode = [""]; - newState.newEmoteFile =[]; + newState.newEmoteFile = []; } this.setState(newState as IState); this.decryptEmotes(client.isRoomEncrypted(this.props.roomId)); @@ -228,8 +243,11 @@ export default class RoomEmoteSettings extends React.Component { private onEmoteChange = (e: React.ChangeEvent): void => { const id = e.target.getAttribute("id"); const b = this.state.value; - b[id] = e.target.value.replace(SHORTCODE_REGEX, ""); - this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value.replace(SHORTCODE_REGEX, "") } }); + b.set(id, e.target.value.replace(SHORTCODE_REGEX, "")); + this.setState({ + value: b, + EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value.replace(SHORTCODE_REGEX, "") }, + }); }; private onEmoteFileAdd = (e: React.ChangeEvent): void => { @@ -243,10 +261,10 @@ export default class RoomEmoteSettings extends React.Component { return; } - const uploadedFiles=[]; - const newCodes=[]; + const uploadedFiles = []; + const newCodes = []; for (const file of e.target.files) { - const fileName = file.name.replace(/\.[^.]*$/, ''); + const fileName = file.name.replace(/\.[^.]*$/, ""); uploadedFiles.push(file); newCodes.push(fileName); } @@ -263,7 +281,7 @@ export default class RoomEmoteSettings extends React.Component { private onEmoteCodeAdd = (e: React.ChangeEvent): void => { if (e.target.value.replace(SHORTCODE_REGEX, "").length > 0) { const updatedCode = this.state.newEmoteCode; - updatedCode[e.target.getAttribute("data-index")]=e.target.value.replace(SHORTCODE_REGEX, ""); + updatedCode[e.target.getAttribute("data-index")] = e.target.value.replace(SHORTCODE_REGEX, ""); this.setState({ newEmoteCodeAdded: true, newEmoteCode: updatedCode, @@ -272,8 +290,8 @@ export default class RoomEmoteSettings extends React.Component { }, }); } else { - const updatedCode=this.state.newEmoteCode; - updatedCode[e.target.getAttribute("data-index")]=e.target.value.replace(SHORTCODE_REGEX, ""); + const updatedCode = this.state.newEmoteCode; + updatedCode[e.target.getAttribute("data-index")] = e.target.value.replace(SHORTCODE_REGEX, ""); this.setState({ newEmoteCodeAdded: false, newEmoteCode: updatedCode, @@ -281,20 +299,19 @@ export default class RoomEmoteSettings extends React.Component { } }; - private onCompatChange = async (allowed: boolean) => { - + private onCompatChange = async (allowed: boolean): Promise => { const client = MatrixClientPeg.get(); await client.sendStateEvent(this.props.roomId, COMPAT_STATE.name, { isCompat: allowed }, ""); - + if (allowed) { - for (const shortcode in this.state.emotes) { - if(!this.imagePack["images"][shortcode]){ + for (const [shortcode, val] of this.state.emotes) { + if (!this.imagePack["images"][shortcode]) { if (client.isRoomEncrypted(this.props.roomId)) { - const blob = await decryptFile(this.state.emotes[shortcode]); - const uploadedEmote = await client.uploadContent(blob) - this.imagePack["images"].set(shortcode, { "url": uploadedEmote.content_uri }); + const blob = await decryptFile(val as IEncryptedFile); + const uploadedEmote = await client.uploadContent(blob); + this.imagePack["images"].set(shortcode, { url: uploadedEmote.content_uri }); } else { - this.imagePack["images"].set(shortcode, { "url": this.state.emotes[shortcode] }); + this.imagePack["images"].set(shortcode, { url: val }); } } } @@ -304,18 +321,16 @@ export default class RoomEmoteSettings extends React.Component { this.setState({ compatibility: allowed, - } - ) - - } - private async decryptEmotes(isEnc: boolean) { - let decryptedemotes = new Map(); - for (const shortcode in this.state.emotes.keys()) { + }); + }; + private async decryptEmotes(isEnc: boolean): Promise { + const decryptedemotes = new Map(); + for (const [shortcode, val] of this.state.emotes) { if (isEnc) { - const blob = await decryptFile(this.state.emotes.get(shortcode) as IEncryptedFile); + const blob = await decryptFile(val as IEncryptedFile); decryptedemotes.set(shortcode, URL.createObjectURL(blob)); } else { - decryptedemotes.set(shortcode, mediaFromMxc(this.state.emotes.get(shortcode) as string).srcHttp); + decryptedemotes.set(shortcode, mediaFromMxc(val as string).srcHttp); } } this.setState({ @@ -324,24 +339,14 @@ export default class RoomEmoteSettings extends React.Component { } public render(): JSX.Element { let emoteSettingsButtons; - if ( - this.state.canAddEmote - ) { + if (this.state.canAddEmote) { emoteSettingsButtons = (
    - - { _t("Cancel") } + + {_t("Cancel")} - - { _t("Save") } + + {_t("Save")}
    ); @@ -349,21 +354,17 @@ export default class RoomEmoteSettings extends React.Component { const existingEmotes = []; if (this.state.emotes) { - for (const emotecode in this.state.emotes) { + for (const [emotecode, val] of this.state.decryptedemotes) { existingEmotes.push( -
  • +
  • - + {":"
    { aria-label="Close" id={emotecode} > - { _t("Delete") } + {_t("Delete")}
  • , @@ -384,11 +385,8 @@ export default class RoomEmoteSettings extends React.Component { if (this.state.canAddEmote) { emoteUploadButton = (
    - - { _t("Upload Emote") } + + {_t("Upload Emote")}
    ); @@ -398,39 +396,33 @@ export default class RoomEmoteSettings extends React.Component { for (let i = 0; i < this.state.newEmoteCode.length; i++) { const fileUrl = this.state.newEmoteFile[i] ? URL.createObjectURL(this.state.newEmoteFile[i]) : ""; uploadedEmotes.push( -
  • +
  • - { - this.state.newEmoteFileAdded ? - : null - } + {this.state.newEmoteFileAdded ? ( + {":" + ) : null} - { i == 0 ? emoteUploadButton : null } + {i == 0 ? emoteUploadButton : null}
  • , ); } - const isCompat = this.state.compatibility + const isCompat = this.state.compatibility; return ( - - + { accept="image/*" multiple /> - { emoteSettingsButtons } + {emoteSettingsButtons} { disabled={!this.state.canAddEmote} /> - { uploadedEmotes } - { - existingEmotes - } + {uploadedEmotes} + {existingEmotes} ); } diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index f6367df8bc2..18db619416c 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -28,10 +28,10 @@ interface IEmojiButtonProps { addEmoji: (unicode: string) => boolean; menuPosition?: MenuProps; className?: string; - room?: Room, + room?: Room; } -export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiButtonProps) { +export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps): JSX.Element { const overflowMenuCloser = useContext(OverflowMenuContext); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 9170f8e7d97..f9a70cc1e86 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -86,7 +86,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { onClick={props.onComposerModeClick} /> ) : ( - emojiButton(props,room) + emojiButton(props, room) ), ]; moreButtons = [ @@ -106,7 +106,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { onClick={props.onComposerModeClick} /> ) : ( - emojiButton(props,room) + emojiButton(props, room) ), uploadButton(), // props passed via UploadButtonContext ]; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 1ae089316b2..4b33600c55b 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -78,8 +78,11 @@ import { getBlobSafeMimeType } from "../../../utils/blobs"; * @param editedContent - The content of the parent event being edited. */ -const COMPAT_STATE=new UnstableValue("org.matrix.msc3892.clientemote_compatibility","m.room.clientemote_compatibility") -const EMOTES_COMP=new UnstableValue("im.ponies.room_emotes","m.room.room_emotes") +const COMPAT_STATE = new UnstableValue( + "org.matrix.msc3892.clientemote_compatibility", + "m.room.clientemote_compatibility", +); +const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); export function attachMentions( sender: string, @@ -182,8 +185,8 @@ export function createMessageContent( relation: IEventRelation | undefined, permalinkCreator?: RoomPermalinkCreator, includeReplyLegacyFallback = true, - emotes?:Map, - compat?:boolean, + emotes?: Map, + compat?: boolean, ): IContent { const isEmote = containsEmote(model); if (isEmote) { @@ -197,8 +200,8 @@ export function createMessageContent( const body = textSerialize(model); let emoteBody; - if (compat) { - emoteBody = body.replace(/:[\w+-]+:/g, m => emotes.get(m) ? emotes.get(m) : m) + if (compat && emotes) { + emoteBody = body.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m) : m)); } const content: IContent = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, @@ -209,16 +212,15 @@ export function createMessageContent( useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { - if(compat){ - formattedBody=formattedBody.replace(/:[\w+-]+:/g, m => emotes.get(m) ? emotes.get(m) : m) + if (compat && emotes) { + formattedBody = formattedBody.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m) : m)); } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; - } - else if (compat){ - if(body!=emoteBody){ - content.format="org.matrix.custom.html" - content.formatted_body = emoteBody + } else if (compat) { + if (body != emoteBody) { + content.format = "org.matrix.custom.html"; + content.formatted_body = emoteBody; } } @@ -276,9 +278,9 @@ export class SendMessageComposer extends React.Component; + private emotes: Map; private compat: boolean; - static defaultProps = { + public static defaultProps = { includeReplyLegacyFallback: true, }; @@ -304,17 +306,32 @@ export class SendMessageComposer extends React.Component()}) : {"images":new Map()}; - this.emotes=new Map() - - for (const shortcode in this.imagePack["images"].keys()){ - this.emotes.set(":"+shortcode.replace(/[^a-zA-Z0-9_]/g, "")+":",""+":"+shortcode.replace(/[^a-zA-Z0-9_]/g, "")+":") + this.imagePack = imagePackEvent + ? imagePackEvent.getContent() || { images: new Map() } + : { images: new Map() }; + this.emotes = new Map(); + + for (const [shortcode, val] of this.imagePack["images"]) { + this.emotes.set( + ":" + shortcode.replace(/[^a-zA-Z0-9_]/g, "") + ":", + "" +
+                    ":" +
+                    shortcode.replace(/[^a-zA-Z0-9_]/g, "") +
+                    ":", + ); } } diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx index d8aba88defb..4f08607beaa 100644 --- a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ContextType } from 'react'; +import React, { ContextType } from "react"; import { _t } from "../../../../../languageHandler"; import RoomEmoteSettings from "../../../room_settings/RoomEmoteSettings"; @@ -30,9 +30,9 @@ interface IState { export default class EmoteRoomSettingsTab extends React.Component { public static contextType = MatrixClientContext; - context: ContextType; + public context!: ContextType; - constructor(props: IProps, context: ContextType) { + public constructor(props: IProps, context: ContextType) { super(props, context); this.state = { @@ -43,12 +43,10 @@ export default class EmoteRoomSettingsTab extends React.Component - -
    { _t("Emotes") }
    -
    +
    {_t("Emotes")}
    +
    -
    ); } diff --git a/src/emoji.ts b/src/emoji.ts index 577d92a33fb..75b1d7a5417 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -28,8 +28,8 @@ export interface IEmoji { unicode: string; skins?: Omit[]; // Currently unused emoticon?: string | string[]; - customLabel?:string; - customComponent?:React.Component; + customLabel?: string; + customComponent?: React.Component; } // The unicode is stored without the variant selector From 600c499951bcae181e80195e26d7b0d4d8e19a13 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:07:22 -0400 Subject: [PATCH 123/176] Restore removed code in emojibutton --- src/components/views/rooms/EmojiButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index 18db619416c..0a8024c0400 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -31,7 +31,7 @@ interface IEmojiButtonProps { room?: Room; } -export function EmojiButton({ addEmoji, menuPosition, className }: IEmojiButtonProps): JSX.Element { +export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiButtonProps): JSX.Element { const overflowMenuCloser = useContext(OverflowMenuContext); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); From 533ce9ba498a5f738997ee6e973cf144d124e175 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:10:14 -0400 Subject: [PATCH 124/176] i18n update --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5f5208eeb6c..f8f05df48d8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2186,7 +2186,7 @@ "Show more": "Show more", "Upload Emote": "Upload Emote", "Emote Compatibility on Other Clients": "Emote Compatibility on Other Clients", - "This will allow emotes sent to be compatible with non-Element clients that have custom emotes. This uses a different spec and emote images will be stored on the server unencrypted. Emotes sent before this setting is enabled will not work on the other clients. The room will have to be reloaded for this change to take effect. NOTE: The first time you turn this setting on in an encrypted room it may take some time so please be patient and wait until the toggle switch shows that it is on.": "This will allow emotes sent to be compatible with non-Element clients that have custom emotes. This uses a different spec and emote images will be stored on the server unencrypted. Emotes sent before this setting is enabled will not work on the other clients. The room will have to be reloaded for this change to take effect. NOTE: The first time you turn this setting on in an encrypted room it may take some time so please be patient and wait until the toggle switch shows that it is on.", + "This will allow emotes sent to be compatible with non-Element clients that have custom emotes. \nThis uses a different spec and emote images will be stored on the server unencrypted. Emotes sent before this setting is enabled will not work on the other clients. \nThe room will have to be reloaded for this change to take effect. \nNOTE: The first time you turn this setting on in an encrypted room it may take some time so please be patient and wait until the toggle switch shows that it is on.": "This will allow emotes sent to be compatible with non-Element clients that have custom emotes. \nThis uses a different spec and emote images will be stored on the server unencrypted. Emotes sent before this setting is enabled will not work on the other clients. \nThe room will have to be reloaded for this change to take effect. \nNOTE: The first time you turn this setting on in an encrypted room it may take some time so please be patient and wait until the toggle switch shows that it is on.", "Compatibility": "Compatibility", "Room Name": "Room Name", "Room Topic": "Room Topic", From 44ee16007708d33e1f8b54de3aab98f7ec9b0cb2 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:35:10 -0400 Subject: [PATCH 125/176] Update SendMessageComposer.tsx --- src/components/views/rooms/SendMessageComposer.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 4b33600c55b..3bd76796db8 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -316,12 +316,14 @@ export class SendMessageComposer extends React.Component() } : { images: new Map() }; this.emotes = new Map(); - - for (const [shortcode, val] of this.imagePack["images"]) { + if (!this.imagePack["images"]) { + this.imagePack = { images: new Map() }; + } + for (const shortcode in this.imagePack["images"]) { this.emotes.set( ":" + shortcode.replace(/[^a-zA-Z0-9_]/g, "") + ":", "" +
                     ":" +
                     shortcode.replace(/[^a-zA-Z0-9_]/g, "") +

From f58703985acce495d318ccc986099954c70ad67e Mon Sep 17 00:00:00 2001
From: nmscode <105442557+nmscode@users.noreply.github.com>
Date: Thu, 15 Jun 2023 13:44:13 -0400
Subject: [PATCH 126/176] Compatibility mode bug fix

---
 .../views/room_settings/RoomEmoteSettings.tsx      | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx
index 2b553bdeab1..3a3c8124126 100644
--- a/src/components/views/room_settings/RoomEmoteSettings.tsx
+++ b/src/components/views/room_settings/RoomEmoteSettings.tsx
@@ -186,9 +186,9 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
                     if (this.state.compatibility) {
                         if (client.isRoomEncrypted(this.props.roomId)) {
                             const compatNewEmote = await client.uploadContent(this.state.newEmoteFile[i]);
-                            newPack["images"].set(this.state.newEmoteCode[i], { url: compatNewEmote.content_uri });
+                            newPack["images"][this.state.newEmoteCode[i]] = { url: compatNewEmote.content_uri };
                         } else {
-                            newPack["images"].set(this.state.newEmoteCode[i], { url: newEmote.url });
+                            newPack["images"][this.state.newEmoteCode[i]] = { url: newEmote.url };
                         }
                     }
                 }
@@ -206,15 +206,15 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
                         emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = val;
                         value.set(this.state.EmoteFieldsTouched[shortcode], this.state.EmoteFieldsTouched[shortcode]);
                         if (this.imagePack["images"][shortcode]) {
-                            newPack["images"].set(this.state.EmoteFieldsTouched[shortcode], {
+                            newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = {
                                 url: this.imagePack["images"][shortcode]["url"],
-                            });
+                            };
                         }
                     } else {
                         emotesMxcs[shortcode] = val;
                         value.set(shortcode, shortcode);
                         if (this.imagePack["images"].get(shortcode)) {
-                            newPack["images"].set(shortcode, { url: this.imagePack["images"][shortcode]["url"] });
+                            newPack["images"][shortcode] = { url: this.imagePack["images"][shortcode]["url"] };
                         }
                     }
                 }
@@ -309,9 +309,9 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
                     if (client.isRoomEncrypted(this.props.roomId)) {
                         const blob = await decryptFile(val as IEncryptedFile);
                         const uploadedEmote = await client.uploadContent(blob);
-                        this.imagePack["images"].set(shortcode, { url: uploadedEmote.content_uri });
+                        this.imagePack["images"][shortcode] = { url: uploadedEmote.content_uri };
                     } else {
-                        this.imagePack["images"].set(shortcode, { url: val });
+                        this.imagePack["images"][shortcode] = { url: val };
                     }
                 }
             }

From 3dba10c463a0ff5e4c968db643d7cb2097714d0a Mon Sep 17 00:00:00 2001
From: nmscode <105442557+nmscode@users.noreply.github.com>
Date: Thu, 15 Jun 2023 17:13:02 -0400
Subject: [PATCH 127/176] Remove Object type for more specific typing

---
 src/autocomplete/EmojiProvider.tsx                       | 9 ++++++---
 src/components/views/emojipicker/EmojiPicker.tsx         | 5 ++++-
 src/components/views/room_settings/RoomEmoteSettings.tsx | 4 ++--
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index 767626057e7..e01704a1a64 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -83,8 +83,8 @@ export default class EmojiProvider extends AutocompleteProvider {
     public matcher: QueryMatcher<ISortedEmoji>;
     public nameMatcher: QueryMatcher<ISortedEmoji>;
     private readonly recentlyUsed: IEmoji[];
-    private emotes: Map<string, Object> = new Map();
-    private emotesPromise: Promise<Map<string, Object>>;
+    private emotes: Map<string, string> = new Map();
+    private emotesPromise: Promise<Map<string, string>>;
     public constructor(room: Room, renderingType?: TimelineRenderingType) {
         super({ commandRegex: EMOJI_REGEX, renderingType });
         const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", "");
@@ -110,7 +110,10 @@ export default class EmojiProvider extends AutocompleteProvider {
         this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean)));
     }
 
-    private async decryptEmotes(emotes: Map<string, Object>, roomId: string): Promise<Map<string, string>> {
+    private async decryptEmotes(
+        emotes: Map<string, string | IEncryptedFile>,
+        roomId: string,
+    ): Promise<Map<string, string>> {
         const decryptedEmoteMap = new Map<string, string>();
         let decryptedurl = "";
         const isEnc = MatrixClientPeg.get()?.isRoomEncrypted(roomId);
diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index adf19791467..1cbd7db5138 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -238,7 +238,10 @@ class EmojiPicker extends React.Component<IProps, IState> {
         this.onScroll();
     }
 
-    private async decryptEmotes(emotes: Map<string, Object>, roomId: string): Promise<Map<string, React.Component>> {
+    private async decryptEmotes(
+        emotes: Map<string, string | IEncryptedFile>,
+        roomId: string,
+    ): Promise<Map<string, React.Component>> {
         const decryptedemotes = new Map<string, React.Component>();
         let decryptedurl = "";
         const isEnc = MatrixClientPeg.get()?.isRoomEncrypted(roomId);
diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx
index 3a3c8124126..66dcaa6353c 100644
--- a/src/components/views/room_settings/RoomEmoteSettings.tsx
+++ b/src/components/views/room_settings/RoomEmoteSettings.tsx
@@ -39,7 +39,7 @@ interface IProps {
 }
 
 interface IState {
-    emotes: Map<string, Object>;
+    emotes: Map<string, string | IEncryptedFile>;
     decryptedemotes: Map<string, string>;
     EmoteFieldsTouched: Record<string, string>;
     newEmoteFileAdded: boolean;
@@ -48,7 +48,7 @@ interface IState {
     newEmoteFile: Array<File>;
     canAddEmote: boolean;
     deleted: boolean;
-    deletedItems: Map<string, Object>;
+    deletedItems: Map<string, string | IEncryptedFile>;
     value: Map<string, string>;
     compatibility: boolean;
 }

From 19569d6ed7567c896fb3bb4aa2cc1702d472b19a Mon Sep 17 00:00:00 2001
From: nmscode <105442557+nmscode@users.noreply.github.com>
Date: Thu, 15 Jun 2023 19:25:58 -0400
Subject: [PATCH 128/176] lint fix

---
 src/components/views/emojipicker/EmojiPicker.tsx | 8 ++++----
 src/emoji.ts                                     | 3 +--
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx
index 1cbd7db5138..ff6f7ce0f6f 100644
--- a/src/components/views/emojipicker/EmojiPicker.tsx
+++ b/src/components/views/emojipicker/EmojiPicker.tsx
@@ -77,8 +77,8 @@ class EmojiPicker extends React.Component<IProps, IState> {
 
     private scrollRef = React.createRef<AutoHideScrollbar<"div">>();
 
-    private emotes: Map<string, React.Component>;
-    private emotesPromise: Promise<Map<string, React.Component>>;
+    private emotes: Map<string, JSX.Element>;
+    private emotesPromise: Promise<Map<string, JSX.Element>>;
     private finalEmotes: IEmoji[];
     private finalEmotesMap: Map<string, IEmoji>;
     public constructor(props: IProps) {
@@ -241,8 +241,8 @@ class EmojiPicker extends React.Component<IProps, IState> {
     private async decryptEmotes(
         emotes: Map<string, string | IEncryptedFile>,
         roomId: string,
-    ): Promise<Map<string, React.Component>> {
-        const decryptedemotes = new Map<string, React.Component>();
+    ): Promise<Map<string, JSX.Element>> {
+        const decryptedemotes = new Map<string, JSX.Element>();
         let decryptedurl = "";
         const isEnc = MatrixClientPeg.get()?.isRoomEncrypted(roomId);
         for (const [shortcode, val] of emotes) {
diff --git a/src/emoji.ts b/src/emoji.ts
index 75b1d7a5417..994ae757a1f 100644
--- a/src/emoji.ts
+++ b/src/emoji.ts
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from "react";
 import EMOJIBASE from "emojibase-data/en/compact.json";
 import SHORTCODES from "emojibase-data/en/shortcodes/iamcal.json";
 
@@ -29,7 +28,7 @@ export interface IEmoji {
     skins?: Omit<IEmoji, "shortcodes" | "tags">[]; // Currently unused
     emoticon?: string | string[];
     customLabel?: string;
-    customComponent?: React.Component;
+    customComponent?: JSX.Element;
 }
 
 // The unicode is stored without the variant selector

From 2b2554341765364198a7b9ce98097d71cad80020 Mon Sep 17 00:00:00 2001
From: JiffyCat <135999094+JiffyCat@users.noreply.github.com>
Date: Thu, 15 Jun 2023 20:27:34 -0400
Subject: [PATCH 129/176] fix: keyboardevent lint error

---
 src/components/structures/MessagePanel.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 9ab76b46184..687ab1f56a1 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from "react";
+import React, { createRef, ReactNode, TransitionEvent } from "react";
 import ReactDOM from "react-dom";
 import classNames from "classnames";
 import { Room } from "matrix-js-sdk/src/models/room";

From bafd0344a4398ec9231779ea1b5e3cf1e3129c7e Mon Sep 17 00:00:00 2001
From: JiffyCat <135999094+JiffyCat@users.noreply.github.com>
Date: Thu, 15 Jun 2023 21:45:44 -0400
Subject: [PATCH 130/176] fix: revert filterboolean

---
 src/autocomplete/EmojiProvider.tsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index e01704a1a64..cc8c91ce2ea 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -33,6 +33,7 @@ import SettingsStore from "../settings/SettingsStore";
 import { EMOJI, IEmoji, getEmojiFromUnicode } from "../emoji";
 import { TimelineRenderingType } from "../contexts/RoomContext";
 import * as recent from "../emojipicker/recent";
+import { filterBoolean } from "../utils/arrays";
 import { decryptFile } from "../utils/DecryptFile";
 import { mediaFromMxc } from "../customisations/Media";
 import { IEncryptedFile } from "../customisations/models/IMediaEventContent";
@@ -107,7 +108,8 @@ export default class EmojiProvider extends AutocompleteProvider {
             // For removing punctuation
             shouldMatchWordsOnly: true,
         });
-        this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean)));
+
+        this.recentlyUsed = Array.from(new Set(filterBoolean(recent.get().map(getEmojiFromUnicode))));
     }
 
     private async decryptEmotes(

From 5005bbec58c3f865cf7e567972984f7baa457c16 Mon Sep 17 00:00:00 2001
From: JiffyCat <135999094+JiffyCat@users.noreply.github.com>
Date: Thu, 15 Jun 2023 23:35:14 -0400
Subject: [PATCH 131/176] fix: test errors

---
 res/css/_components.pcss                                 | 2 +-
 src/components/views/messages/TextualBody.tsx            | 2 +-
 test/components/views/messages/TextualBody-test.tsx      | 4 ++++
 test/components/views/rooms/EditMessageComposer-test.tsx | 1 +
 test/components/views/rooms/PinnedEventTile-test.tsx     | 1 +
 test/test-utils/test-utils.ts                            | 2 +-
 6 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/res/css/_components.pcss b/res/css/_components.pcss
index 9dac8933228..47d4e9fbcc0 100644
--- a/res/css/_components.pcss
+++ b/res/css/_components.pcss
@@ -317,6 +317,7 @@
 @import "./views/settings/_AvatarSetting.pcss";
 @import "./views/settings/_CrossSigningPanel.pcss";
 @import "./views/settings/_CryptographyPanel.pcss";
+@import "./views/settings/_EmoteSettings.pcss";
 @import "./views/settings/_FontScalingPanel.pcss";
 @import "./views/settings/_ImageSizePanel.pcss";
 @import "./views/settings/_IntegrationManager.pcss";
@@ -326,7 +327,6 @@
 @import "./views/settings/_Notifications.pcss";
 @import "./views/settings/_PhoneNumbers.pcss";
 @import "./views/settings/_ProfileSettings.pcss";
-@import "./views/settings/_EmoteSettings.pcss";
 @import "./views/settings/_SecureBackupPanel.pcss";
 @import "./views/settings/_SetIdServer.pcss";
 @import "./views/settings/_SetIntegrationManager.pcss";
diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx
index 81ee27d5536..87d842abf70 100644
--- a/src/components/views/messages/TextualBody.tsx
+++ b/src/components/views/messages/TextualBody.tsx
@@ -573,7 +573,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
         return <span className="mx_EventTile_pendingModeration">{`(${text})`}</span>;
     }
     private async decryptEmotes(): Promise<void> {
-        const client = MatrixClientPeg.get();
+        const client = MatrixClientPeg.safeGet();
         const room = client.getRoom(this.props.mxEvent.getRoomId());
         const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE.name, "");
         const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {};
diff --git a/test/components/views/messages/TextualBody-test.tsx b/test/components/views/messages/TextualBody-test.tsx
index 46418816268..a62bc4588ae 100644
--- a/test/components/views/messages/TextualBody-test.tsx
+++ b/test/components/views/messages/TextualBody-test.tsx
@@ -89,6 +89,7 @@ describe("<TextualBody />", () => {
             getAccountData: (): MatrixEvent | undefined => undefined,
             isGuest: () => false,
             mxcUrlToHttp: (s: string) => s,
+            isRoomEncrypted: (roomId: string) => false,
             getUserId: () => "@user:example.com",
             fetchRoomEvent: () => {
                 throw new Error("MockClient event not found");
@@ -108,6 +109,7 @@ describe("<TextualBody />", () => {
         highlightLink: "",
         onMessageAllowed: jest.fn(),
         onHeightChanged: jest.fn(),
+        isRoomEncrypted: jest.fn().mockReturnValue(false),
         permalinkCreator: new RoomPermalinkCreator(defaultRoom),
         mediaEventHelper: {} as MediaEventHelper,
     };
@@ -262,6 +264,7 @@ describe("<TextualBody />", () => {
                 on: (): void => undefined,
                 removeListener: (): void => undefined,
                 isGuest: () => false,
+                isRoomEncrypted: (roomId: string) => false,
                 mxcUrlToHttp: (s: string) => s,
             });
             DMRoomMap.makeShared(defaultMatrixClient);
@@ -406,6 +409,7 @@ describe("<TextualBody />", () => {
             getAccountData: (): MatrixClient | undefined => undefined,
             getUrlPreview: (url: string) => new Promise(() => {}),
             isGuest: () => false,
+            isRoomEncrypted: (roomId: string) => false,
             mxcUrlToHttp: (s: string) => s,
         });
         DMRoomMap.makeShared(defaultMatrixClient);
diff --git a/test/components/views/rooms/EditMessageComposer-test.tsx b/test/components/views/rooms/EditMessageComposer-test.tsx
index 64425d8f914..8a6769e618e 100644
--- a/test/components/views/rooms/EditMessageComposer-test.tsx
+++ b/test/components/views/rooms/EditMessageComposer-test.tsx
@@ -48,6 +48,7 @@ describe("<EditMessageComposer/>", () => {
         ...mockClientMethodsUser(userId),
         getRoom: jest.fn(),
         sendMessage: jest.fn(),
+        isRoomEncrypted: jest.fn(),
     });
     const room = new Room(roomId, mockClient, userId);
 
diff --git a/test/components/views/rooms/PinnedEventTile-test.tsx b/test/components/views/rooms/PinnedEventTile-test.tsx
index 7febe0b4bd3..ccaf341c2b3 100644
--- a/test/components/views/rooms/PinnedEventTile-test.tsx
+++ b/test/components/views/rooms/PinnedEventTile-test.tsx
@@ -27,6 +27,7 @@ describe("<PinnedEventTile />", () => {
     const roomId = "!room:server.org";
     const mockClient = getMockClientWithEventEmitter({
         getRoom: jest.fn(),
+        isRoomEncrypted: jest.fn(),
     });
     const room = new Room(roomId, mockClient, userId);
     const permalinkCreator = new RoomPermalinkCreator(room);
diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts
index 811290be17d..9a98224dacc 100644
--- a/test/test-utils/test-utils.ts
+++ b/test/test-utils/test-utils.ts
@@ -156,7 +156,7 @@ export function createTestClient(): MatrixClient {
                 content: {},
             });
         }),
-        mxcUrlToHttp: (mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`,
+        mxcUrlToHttp: (mxc: string) => `http://this.is.a.url/${mxc.toString().substring(6)}`,
         setAccountData: jest.fn(),
         setRoomAccountData: jest.fn(),
         setRoomTopic: jest.fn(),

From 92c150d3c5dfc34a10346745e9ace958ec6636a5 Mon Sep 17 00:00:00 2001
From: nmscode <105442557+nmscode@users.noreply.github.com>
Date: Fri, 16 Jun 2023 00:14:10 -0400
Subject: [PATCH 132/176] Bug fix in roomemptesettings

---
 .../views/room_settings/RoomEmoteSettings.tsx | 22 +++++++++++--------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx
index 66dcaa6353c..bf327088eee 100644
--- a/src/components/views/room_settings/RoomEmoteSettings.tsx
+++ b/src/components/views/room_settings/RoomEmoteSettings.tsx
@@ -123,8 +123,8 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
         e.preventDefault();
         const value = new Map();
         if (this.state.deleted) {
-            for (const key in this.state.deletedItems) {
-                this.state.emotes.set(key, this.state.deletedItems.get(key));
+            for (const [key, val] of this.state.deletedItems) {
+                this.state.emotes.set(key, val);
                 value.set(key, key);
             }
         }
@@ -150,12 +150,12 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
         const deletedItems = this.state.deletedItems;
         const value = new Map();
         const id = e.currentTarget.getAttribute("id");
-        for (const emote in this.state.emotes) {
-            if (emote != id) {
-                cleanemotes.set(emote, this.state.emotes.get(emote));
-                value.set(emote, emote);
+        for (const [shortcode, val] of this.state.emotes) {
+            if (shortcode != id) {
+                cleanemotes.set(shortcode, val);
+                value.set(shortcode, shortcode);
             } else {
-                deletedItems.set(emote, this.state.emotes.get(emote));
+                deletedItems.set(shortcode, val);
             }
         }
 
@@ -354,7 +354,7 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
 
         const existingEmotes = [];
         if (this.state.emotes) {
-            for (const [emotecode, val] of this.state.decryptedemotes) {
+            for (const emotecode of Array.from(this.state.emotes.keys()).sort()) {
                 existingEmotes.push(
                     <li className="mx_EmoteSettings_addEmoteField">
                         <input
@@ -364,7 +364,11 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
                             onChange={this.onEmoteChange}
                             className="mx_EmoteSettings_existingEmoteCode"
                         />
-                        <img alt={":" + emotecode + ":"} className="mx_EmoteSettings_uploadedEmoteImage" src={val} />
+                        <img
+                            alt={":" + emotecode + ":"}
+                            className="mx_EmoteSettings_uploadedEmoteImage"
+                            src={this.state.decryptedemotes.get(emotecode)}
+                        />
                         <div className="mx_EmoteSettings_uploadButton">
                             <AccessibleButton
                                 onClick={this.deleteEmote}

From 026e41169a6a07834128cdcd5c58d770c7c13fea Mon Sep 17 00:00:00 2001
From: nmscode <105442557+nmscode@users.noreply.github.com>
Date: Fri, 16 Jun 2023 00:26:46 -0400
Subject: [PATCH 133/176] RoomEmoteSettings bug fix 2

---
 src/components/views/room_settings/RoomEmoteSettings.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx
index bf327088eee..b087e1f6e4a 100644
--- a/src/components/views/room_settings/RoomEmoteSettings.tsx
+++ b/src/components/views/room_settings/RoomEmoteSettings.tsx
@@ -182,7 +182,7 @@ export default class RoomEmoteSettings extends React.Component<IProps, IState> {
                     } else {
                         emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url;
                     }
-                    value[this.state.newEmoteCode[i]] = this.state.newEmoteCode[i];
+                    value.set(this.state.newEmoteCode[i], this.state.newEmoteCode[i]);
                     if (this.state.compatibility) {
                         if (client.isRoomEncrypted(this.props.roomId)) {
                             const compatNewEmote = await client.uploadContent(this.state.newEmoteFile[i]);

From 6a6ed02a99781ed495a4e73ec51a179a3c9b0196 Mon Sep 17 00:00:00 2001
From: JiffyCat <135999094+JiffyCat@users.noreply.github.com>
Date: Fri, 16 Jun 2023 01:05:03 -0400
Subject: [PATCH 134/176] fix: add test snapshots

---
 .../RoomSettingsDialog-test.tsx.snap          | 18 +++++++++
 .../MessageDiffUtils-test.tsx.snap            | 37 ++++++++-----------
 2 files changed, 33 insertions(+), 22 deletions(-)

diff --git a/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap
index 6c148400d88..8faccd84b8f 100644
--- a/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap
+++ b/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap
@@ -74,6 +74,24 @@ NodeList [
       Notifications
     </span>
   </li>,
+  <li
+    aria-controls="mx_tabpanel_ROOM_EMOTES_TAB"
+    aria-selected="false"
+    class="mx_AccessibleButton mx_TabbedView_tabLabel"
+    data-testid="settings-tab-ROOM_EMOTES_TAB"
+    role="tab"
+    tabindex="-1"
+  >
+    <span
+      class="mx_TabbedView_maskedIcon mx_RoomSettingsDialog_emotesIcon"
+    />
+    <span
+      class="mx_TabbedView_tabLabel_text"
+      id="mx_tabpanel_ROOM_EMOTES_TAB_label"
+    >
+      Emotes
+    </span>
+  </li>,
   <li
     aria-controls="mx_tabpanel_ROOM_POLL_HISTORY_TAB"
     aria-selected="false"
diff --git a/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap b/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap
index 655fc8a8077..a7325dc29aa 100644
--- a/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap
+++ b/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap
@@ -38,36 +38,29 @@ exports[`editBodyDiffToHtml handles complex transformations 1`] = `
       <span
         class="mx_EditHistoryMessage_deletion"
       >
-        <span
-          data-mx-maths="{<span class=☃️}^\\infty" - > - - { - - ☃️ - - }^\\infty - + + { + + ☃️ + + + 😃 + + }^\\infty - { - - ☃️ - - }^\\infty + {☃️}^\\infty From ecc19e62fd6a4f01a90b3b9c6007a9083c4e47e4 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:23:40 -0400 Subject: [PATCH 135/176] Strict type fixes --- src/HtmlUtils.tsx | 10 +-- src/autocomplete/EmojiProvider.tsx | 4 +- .../views/emojipicker/EmojiPicker.tsx | 14 ++--- src/components/views/messages/TextualBody.tsx | 4 +- .../views/room_settings/RoomEmoteSettings.tsx | 63 ++++++++++--------- .../views/rooms/SendMessageComposer.tsx | 13 ++-- 6 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 2d4b7fe2b5a..677eab32f11 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -553,11 +553,11 @@ export function bodyToHtml(content: IContent, highlights: Optional, op sanitizeParams.textFilter = function (safeText) { if (opts.emotes) { return highlighter - .applyHighlights(safeText, safeHighlights) + .applyHighlights(safeText, safeHighlights!) .join("") - .replace(CUSTOM_EMOTES_REGEX, (m) => (opts.emotes.get(m) ? opts.emotes.get(m) : m)); + .replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts?.emotes?.get(m)! : m)); } - return highlighter.applyHighlights(safeText, safeHighlights).join(""); + return highlighter.applyHighlights(safeText, safeHighlights!).join(""); }; } @@ -625,7 +625,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "markdown-body": isHtmlMessage && !emojiBody, }); if (opts.emotes) { - const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, (m) => (opts.emotes.get(m) ? opts.emotes.get(m) : m)); + const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts?.emotes?.get(m)! : m)); if (tmp !== strippedBody) { safeBody = tmp; } @@ -645,7 +645,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op /> ) : ( - {emojiBodyElements || strippedBody} + {emojiBodyElements! || strippedBody} ); } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index cc8c91ce2ea..bff682f56f4 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -85,7 +85,7 @@ export default class EmojiProvider extends AutocompleteProvider { public nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; private emotes: Map = new Map(); - private emotesPromise: Promise>; + private emotesPromise!: Promise>; public constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); @@ -124,7 +124,7 @@ export default class EmojiProvider extends AutocompleteProvider { const blob = await decryptFile(val as IEncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(val as string).srcHttp; + decryptedurl = mediaFromMxc(val as string)?.srcHttp!; } decryptedEmoteMap.set(":" + shortcode + ":", decryptedurl); } diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index ff6f7ce0f6f..f8e85915261 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -77,8 +77,8 @@ class EmojiPicker extends React.Component { private scrollRef = React.createRef>(); - private emotes: Map; - private emotesPromise: Promise>; + private emotes!: Map; + private emotesPromise!: Promise>; private finalEmotes: IEmoji[]; private finalEmotesMap: Map; public constructor(props: IProps) { @@ -222,15 +222,15 @@ class EmojiPicker extends React.Component { rec.forEach((v, i) => { if (this.finalEmotesMap.get(v.unicode)) { if (i >= this.recentlyUsed.length) { - this.recentlyUsed.push(this.finalEmotesMap.get(v.unicode)); + this.recentlyUsed.push(this.finalEmotesMap.get(v.unicode)!); } else { - this.recentlyUsed[i] = this.finalEmotesMap.get(v.unicode); + this.recentlyUsed[i] = this.finalEmotesMap.get(v.unicode)!; } } else if (getEmojiFromUnicode(v.unicode)) { if (i >= this.recentlyUsed.length) { - this.recentlyUsed.push(getEmojiFromUnicode(v.unicode)); + this.recentlyUsed.push(getEmojiFromUnicode(v.unicode)!); } else { - this.recentlyUsed[i] = getEmojiFromUnicode(v.unicode); + this.recentlyUsed[i] = getEmojiFromUnicode(v.unicode)!; } } }); @@ -250,7 +250,7 @@ class EmojiPicker extends React.Component { const blob = await decryptFile(val as IEncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(val as string)?.srcHttp; + decryptedurl = mediaFromMxc(val as string)?.srcHttp!; } decryptedemotes.set( shortcode, diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 87d842abf70..5b2bdbf53ae 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -579,13 +579,13 @@ export default class TextualBody extends React.Component { const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; const decryptedemotes = new Map(); let decryptedurl = ""; - const isEnc = client?.isRoomEncrypted(this.props.mxEvent.getRoomId()); + const isEnc = client?.isRoomEncrypted(this.props.mxEvent.getRoomId()!); for (const shortcode in rawEmotes) { if (isEnc) { const blob = await decryptFile(rawEmotes[shortcode]); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(rawEmotes[shortcode])?.srcHttp; + decryptedurl = mediaFromMxc(rawEmotes[shortcode])?.srcHttp!; } decryptedemotes.set( diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index b087e1f6e4a..dbb2747b997 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -19,7 +19,7 @@ import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; @@ -57,7 +57,7 @@ export default class RoomEmoteSettings extends React.Component { private emoteUpload = createRef(); private emoteCodeUpload = createRef(); private emoteUploadImage = createRef(); - private imagePack: object; + private imagePack: Record>>; public constructor(props: IProps) { super(props); @@ -79,10 +79,10 @@ export default class RoomEmoteSettings extends React.Component { const imagePackEvent = room.currentState.getStateEvents(EMOTES_COMP.name, ""); this.imagePack = imagePackEvent - ? imagePackEvent.getContent() || { images: new Map() } - : { images: new Map() }; + ? imagePackEvent.getContent() || { "images": {} } + : { "images": {} }; if (!this.imagePack["images"]) { - this.imagePack = { images: new Map() }; + this.imagePack["images"]={}; } this.state = { @@ -95,7 +95,7 @@ export default class RoomEmoteSettings extends React.Component { newEmoteFile: [], deleted: false, deletedItems: new Map(), - canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE.name, client.getUserId()), + canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE.name, client.getUserId()!), value: value, compatibility: compat, }; @@ -107,7 +107,7 @@ export default class RoomEmoteSettings extends React.Component { } private uploadEmoteClick = (): void => { - this.emoteUpload.current.click(); + this.emoteUpload.current?.click(); }; private isSaveEnabled = (): boolean => { @@ -118,7 +118,7 @@ export default class RoomEmoteSettings extends React.Component { ); }; - private cancelEmoteChanges = async (e: React.MouseEvent): Promise => { + private cancelEmoteChanges = async (e: ButtonEvent): Promise => { e.stopPropagation(); e.preventDefault(); const value = new Map(); @@ -143,7 +143,7 @@ export default class RoomEmoteSettings extends React.Component { value: value, }); }; - private deleteEmote = (e: React.MouseEvent): Promise => { + private deleteEmote = (e: ButtonEvent): void => { e.stopPropagation(); e.preventDefault(); const cleanemotes = new Map(); @@ -160,7 +160,6 @@ export default class RoomEmoteSettings extends React.Component { } this.setState({ deleted: true, emotes: cleanemotes, deletedItems: deletedItems, value: value }); - return; }; private saveEmote = async (e: React.FormEvent): Promise => { e.stopPropagation(); @@ -169,26 +168,26 @@ export default class RoomEmoteSettings extends React.Component { if (!this.isSaveEnabled()) return; const client = MatrixClientPeg.get(); const newState: Partial = {}; - const emotesMxcs = {}; + const emotesMxcs : { [key: string]:IEncryptedFile|string } = {}; const value = new Map(); - const newPack = { images: new Map() }; + const newPack: Map> = new Map(); if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { for (let i = 0; i < this.state.newEmoteCode.length; i++) { const newEmote = await uploadFile(client, this.props.roomId, this.state.newEmoteFile[i]); if (client.isRoomEncrypted(this.props.roomId)) { - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file; + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.file!; } else { - emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url; + emotesMxcs[this.state.newEmoteCode[i]] = newEmote.url!; } value.set(this.state.newEmoteCode[i], this.state.newEmoteCode[i]); if (this.state.compatibility) { if (client.isRoomEncrypted(this.props.roomId)) { const compatNewEmote = await client.uploadContent(this.state.newEmoteFile[i]); - newPack["images"][this.state.newEmoteCode[i]] = { url: compatNewEmote.content_uri }; + newPack.set(this.state.newEmoteCode[i], { url: compatNewEmote.content_uri }); } else { - newPack["images"][this.state.newEmoteCode[i]] = { url: newEmote.url }; + newPack.set(this.state.newEmoteCode[i], { url: newEmote.url! }); } } } @@ -206,22 +205,26 @@ export default class RoomEmoteSettings extends React.Component { emotesMxcs[this.state.EmoteFieldsTouched[shortcode]] = val; value.set(this.state.EmoteFieldsTouched[shortcode], this.state.EmoteFieldsTouched[shortcode]); if (this.imagePack["images"][shortcode]) { - newPack["images"][this.state.EmoteFieldsTouched[shortcode]] = { + newPack.set(this.state.EmoteFieldsTouched[shortcode], { url: this.imagePack["images"][shortcode]["url"], - }; + }); } } else { emotesMxcs[shortcode] = val; value.set(shortcode, shortcode); - if (this.imagePack["images"].get(shortcode)) { - newPack["images"][shortcode] = { url: this.imagePack["images"][shortcode]["url"] }; + if (this.imagePack["images"][shortcode]) { + newPack.set(shortcode, { url: this.imagePack["images"][shortcode]["url"] }); } } } } newState.value = value; await client.sendStateEvent(this.props.roomId, EMOTES_STATE.name, emotesMxcs, ""); - this.imagePack = newPack; + + for (const [key,val] of newPack){ + this.imagePack["images"][key]=val + } + await client.sendStateEvent(this.props.roomId, EMOTES_COMP.name, this.imagePack, ""); newState.newEmoteFileAdded = false; @@ -241,9 +244,9 @@ export default class RoomEmoteSettings extends React.Component { }; private onEmoteChange = (e: React.ChangeEvent): void => { - const id = e.target.getAttribute("id"); + const id = e.target.getAttribute("id")!; const b = this.state.value; - b.set(id, e.target.value.replace(SHORTCODE_REGEX, "")); + b.set(id, e.target?.value?.replace(SHORTCODE_REGEX, "")); this.setState({ value: b, EmoteFieldsTouched: { ...this.state.EmoteFieldsTouched, [id]: e.target.value.replace(SHORTCODE_REGEX, "") }, @@ -261,8 +264,8 @@ export default class RoomEmoteSettings extends React.Component { return; } - const uploadedFiles = []; - const newCodes = []; + const uploadedFiles: Array = []; + const newCodes: string[] = []; for (const file of e.target.files) { const fileName = file.name.replace(/\.[^.]*$/, ""); uploadedFiles.push(file); @@ -281,7 +284,7 @@ export default class RoomEmoteSettings extends React.Component { private onEmoteCodeAdd = (e: React.ChangeEvent): void => { if (e.target.value.replace(SHORTCODE_REGEX, "").length > 0) { const updatedCode = this.state.newEmoteCode; - updatedCode[e.target.getAttribute("data-index")] = e.target.value.replace(SHORTCODE_REGEX, ""); + updatedCode[parseInt(e.target.getAttribute("data-index")!)] = e.target.value.replace(SHORTCODE_REGEX, ""); this.setState({ newEmoteCodeAdded: true, newEmoteCode: updatedCode, @@ -291,7 +294,7 @@ export default class RoomEmoteSettings extends React.Component { }); } else { const updatedCode = this.state.newEmoteCode; - updatedCode[e.target.getAttribute("data-index")] = e.target.value.replace(SHORTCODE_REGEX, ""); + updatedCode[parseInt(e.target.getAttribute("data-index")!)] = e.target.value.replace(SHORTCODE_REGEX, ""); this.setState({ newEmoteCodeAdded: false, newEmoteCode: updatedCode, @@ -311,7 +314,7 @@ export default class RoomEmoteSettings extends React.Component { const uploadedEmote = await client.uploadContent(blob); this.imagePack["images"][shortcode] = { url: uploadedEmote.content_uri }; } else { - this.imagePack["images"][shortcode] = { url: val }; + this.imagePack["images"][shortcode] = { url: val as string }; } } } @@ -352,7 +355,7 @@ export default class RoomEmoteSettings extends React.Component { ); } - const existingEmotes = []; + const existingEmotes: Array = []; if (this.state.emotes) { for (const emotecode of Array.from(this.state.emotes.keys()).sort()) { existingEmotes.push( @@ -396,7 +399,7 @@ export default class RoomEmoteSettings extends React.Component { ); } - const uploadedEmotes = []; + const uploadedEmotes: Array = []; for (let i = 0; i < this.state.newEmoteCode.length; i++) { const fileUrl = this.state.newEmoteFile[i] ? URL.createObjectURL(this.state.newEmoteFile[i]) : ""; uploadedEmotes.push( diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index ef852641151..cb89aabcc89 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -201,7 +201,7 @@ export function createMessageContent( let emoteBody; if (compat && emotes) { - emoteBody = body.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m) : m)); + emoteBody = body.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m)! : m)); } const content: IContent = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, @@ -213,7 +213,7 @@ export function createMessageContent( }); if (formattedBody) { if (compat && emotes) { - formattedBody = formattedBody.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m) : m)); + formattedBody = formattedBody.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m)! : m)); } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; @@ -277,7 +277,7 @@ export class SendMessageComposer extends React.Component>>;; private emotes: Map; private compat: boolean; public static defaultProps = { @@ -313,12 +313,13 @@ export class SendMessageComposer extends React.Component() } - : { images: new Map() }; + ? imagePackEvent.getContent() || { "images": {} } + : { "images": {} }; this.emotes = new Map(); if (!this.imagePack["images"]) { - this.imagePack = { images: new Map() }; + this.imagePack["images"]={}; } + for (const shortcode in this.imagePack["images"]) { this.emotes.set( ":" + shortcode.replace(/[^a-zA-Z0-9_]/g, "") + ":", From 18cfc7e822d33f99a6920c2478a08680e708b6dc Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:34:48 -0400 Subject: [PATCH 136/176] Lint fixes --- src/HtmlUtils.tsx | 4 ++-- src/autocomplete/EmojiProvider.tsx | 2 +- .../views/emojipicker/EmojiPicker.tsx | 2 +- src/components/views/messages/TextualBody.tsx | 2 +- .../views/room_settings/RoomEmoteSettings.tsx | 18 ++++++++---------- .../views/rooms/SendMessageComposer.tsx | 10 ++++------ 6 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 677eab32f11..876e14c133c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -555,7 +555,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op return highlighter .applyHighlights(safeText, safeHighlights!) .join("") - .replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts?.emotes?.get(m)! : m)); + .replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts.emotes.get(m)! : m)); } return highlighter.applyHighlights(safeText, safeHighlights!).join(""); }; @@ -625,7 +625,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "markdown-body": isHtmlMessage && !emojiBody, }); if (opts.emotes) { - const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts?.emotes?.get(m)! : m)); + const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts.emotes.get(m)! : m)); if (tmp !== strippedBody) { safeBody = tmp; } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index bff682f56f4..7c03b26c9aa 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -124,7 +124,7 @@ export default class EmojiProvider extends AutocompleteProvider { const blob = await decryptFile(val as IEncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(val as string)?.srcHttp!; + decryptedurl = mediaFromMxc(val as string).srcHttp!; } decryptedEmoteMap.set(":" + shortcode + ":", decryptedurl); } diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index f8e85915261..8cd2e4b5899 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -250,7 +250,7 @@ class EmojiPicker extends React.Component { const blob = await decryptFile(val as IEncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(val as string)?.srcHttp!; + decryptedurl = mediaFromMxc(val as string).srcHttp!; } decryptedemotes.set( shortcode, diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 5b2bdbf53ae..3a9b237cbcb 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -585,7 +585,7 @@ export default class TextualBody extends React.Component { const blob = await decryptFile(rawEmotes[shortcode]); decryptedurl = URL.createObjectURL(blob); } else { - decryptedurl = mediaFromMxc(rawEmotes[shortcode])?.srcHttp!; + decryptedurl = mediaFromMxc(rawEmotes[shortcode]).srcHttp!; } decryptedemotes.set( diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index dbb2747b997..fba8f138f98 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -19,7 +19,7 @@ import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { uploadFile } from "../../../ContentMessages"; import { decryptFile } from "../../../utils/DecryptFile"; @@ -57,7 +57,7 @@ export default class RoomEmoteSettings extends React.Component { private emoteUpload = createRef(); private emoteCodeUpload = createRef(); private emoteUploadImage = createRef(); - private imagePack: Record>>; + private imagePack: Record>>; public constructor(props: IProps) { super(props); @@ -78,11 +78,9 @@ export default class RoomEmoteSettings extends React.Component { const compat = compatEvent ? compatEvent.getContent().isCompat || false : false; const imagePackEvent = room.currentState.getStateEvents(EMOTES_COMP.name, ""); - this.imagePack = imagePackEvent - ? imagePackEvent.getContent() || { "images": {} } - : { "images": {} }; + this.imagePack = imagePackEvent ? imagePackEvent.getContent() || { images: {} } : { images: {} }; if (!this.imagePack["images"]) { - this.imagePack["images"]={}; + this.imagePack["images"] = {}; } this.state = { @@ -168,9 +166,9 @@ export default class RoomEmoteSettings extends React.Component { if (!this.isSaveEnabled()) return; const client = MatrixClientPeg.get(); const newState: Partial = {}; - const emotesMxcs : { [key: string]:IEncryptedFile|string } = {}; + const emotesMxcs: { [key: string]: IEncryptedFile | string } = {}; const value = new Map(); - const newPack: Map> = new Map(); + const newPack: Map> = new Map(); if (this.state.emotes || (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded)) { if (this.state.newEmoteFileAdded && this.state.newEmoteCodeAdded) { @@ -221,8 +219,8 @@ export default class RoomEmoteSettings extends React.Component { newState.value = value; await client.sendStateEvent(this.props.roomId, EMOTES_STATE.name, emotesMxcs, ""); - for (const [key,val] of newPack){ - this.imagePack["images"][key]=val + for (const [key, val] of newPack) { + this.imagePack["images"][key] = val; } await client.sendStateEvent(this.props.roomId, EMOTES_COMP.name, this.imagePack, ""); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index cb89aabcc89..d057024f938 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -277,7 +277,7 @@ export class SendMessageComposer extends React.Component>>;; + private imagePack: Record>>; private emotes: Map; private compat: boolean; public static defaultProps = { @@ -312,14 +312,12 @@ export class SendMessageComposer extends React.Component(); if (!this.imagePack["images"]) { - this.imagePack["images"]={}; + this.imagePack["images"] = {}; } - + for (const shortcode in this.imagePack["images"]) { this.emotes.set( ":" + shortcode.replace(/[^a-zA-Z0-9_]/g, "") + ":", From 8063c02895cccdb591df6c7b6cbbd43fdb4f0bbe Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 19 Jun 2023 12:49:30 -0400 Subject: [PATCH 137/176] removed incorrect null safe assertions and used interface for imagePack --- src/HtmlUtils.tsx | 6 +++--- src/autocomplete/EmojiProvider.tsx | 9 +++++++-- src/components/views/emojipicker/EmojiPicker.tsx | 7 +++++-- .../views/room_settings/RoomEmoteSettings.tsx | 16 ++++++++++++---- .../views/rooms/SendMessageComposer.tsx | 10 +++++++++- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 876e14c133c..36b4c3322a8 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -555,7 +555,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op return highlighter .applyHighlights(safeText, safeHighlights!) .join("") - .replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts.emotes.get(m)! : m)); + .replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.has(m) ? opts.emotes.get(m)! : m)); } return highlighter.applyHighlights(safeText, safeHighlights!).join(""); }; @@ -625,7 +625,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "markdown-body": isHtmlMessage && !emojiBody, }); if (opts.emotes) { - const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.get(m) ? opts.emotes.get(m)! : m)); + const tmp = strippedBody?.replace(CUSTOM_EMOTES_REGEX, (m) => (opts?.emotes?.has(m) ? opts.emotes.get(m)! : m)); if (tmp !== strippedBody) { safeBody = tmp; } @@ -645,7 +645,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op /> ) : ( - {emojiBodyElements! || strippedBody} + {emojiBodyElements || strippedBody} ); } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 7c03b26c9aa..365aed076bb 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -85,7 +85,7 @@ export default class EmojiProvider extends AutocompleteProvider { public nameMatcher: QueryMatcher; private readonly recentlyUsed: IEmoji[]; private emotes: Map = new Map(); - private emotesPromise!: Promise>; + private emotesPromise?: Promise>; public constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); @@ -141,7 +141,12 @@ export default class EmojiProvider extends AutocompleteProvider { return []; // don't give any suggestions if the user doesn't want them } - this.emotes = await this.emotesPromise; + const returnedEmotes = await this.emotesPromise; + if (!returnedEmotes) { + this.emotes = new Map(); + } else { + this.emotes = returnedEmotes; + } const emojisAndEmotes = [...SORTED_EMOJI]; for (const [key, val] of this.emotes) { emojisAndEmotes.push({ diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 8cd2e4b5899..294a8379aaf 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -77,8 +77,8 @@ class EmojiPicker extends React.Component { private scrollRef = React.createRef>(); - private emotes!: Map; - private emotesPromise!: Promise>; + private emotes?: Map; + private emotesPromise?: Promise>; private finalEmotes: IEmoji[]; private finalEmotesMap: Map; public constructor(props: IProps) { @@ -189,6 +189,9 @@ class EmojiPicker extends React.Component { private async loadEmotes(): Promise { this.emotes = await this.emotesPromise; + if (!this.emotes) { + return; + } for (const [key, val] of this.emotes) { this.finalEmotes.push({ label: key, diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index fba8f138f98..92e569cfe73 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -53,11 +53,19 @@ interface IState { compatibility: boolean; } +interface compatibilityImagePack { + images: { + [key: string]: { + url?: string; + }; + }; +} + export default class RoomEmoteSettings extends React.Component { private emoteUpload = createRef(); private emoteCodeUpload = createRef(); private emoteUploadImage = createRef(); - private imagePack: Record>>; + private imagePack: compatibilityImagePack; public constructor(props: IProps) { super(props); @@ -93,7 +101,7 @@ export default class RoomEmoteSettings extends React.Component { newEmoteFile: [], deleted: false, deletedItems: new Map(), - canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE.name, client.getUserId()!), + canAddEmote: room.currentState.maySendStateEvent(EMOTES_STATE.name, client.getSafeUserId()), value: value, compatibility: compat, }; @@ -204,14 +212,14 @@ export default class RoomEmoteSettings extends React.Component { value.set(this.state.EmoteFieldsTouched[shortcode], this.state.EmoteFieldsTouched[shortcode]); if (this.imagePack["images"][shortcode]) { newPack.set(this.state.EmoteFieldsTouched[shortcode], { - url: this.imagePack["images"][shortcode]["url"], + url: this.imagePack["images"][shortcode]["url"]!, }); } } else { emotesMxcs[shortcode] = val; value.set(shortcode, shortcode); if (this.imagePack["images"][shortcode]) { - newPack.set(shortcode, { url: this.imagePack["images"][shortcode]["url"] }); + newPack.set(shortcode, { url: this.imagePack["images"][shortcode]["url"]! }); } } } diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index d057024f938..11fa66727b1 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -267,6 +267,14 @@ interface ISendMessageComposerProps extends MatrixClientProps { toggleStickerPickerOpen: () => void; } +interface compatibilityImagePack { + images: { + [key: string]: { + url: string; + }; + }; +} + export class SendMessageComposer extends React.Component { public static contextType = RoomContext; public context!: React.ContextType; @@ -277,7 +285,7 @@ export class SendMessageComposer extends React.Component>>; + private imagePack: compatibilityImagePack; private emotes: Map; private compat: boolean; public static defaultProps = { From 4e67a6b614503c600b553df9733693cd30dd1785 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Mon, 19 Jun 2023 18:03:17 +0000 Subject: [PATCH 138/176] fix: strict type check for emojiBodyElements --- src/HtmlUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 36b4c3322a8..9bcf109e660 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -630,7 +630,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op safeBody = tmp; } } - let emojiBodyElements: JSX.Element[]; + let emojiBodyElements: JSX.Element[] | undefined; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } From 2ad5f2c937940f6817cc11eb7d86f01f3cefb582 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 19 Jun 2023 20:34:10 -0400 Subject: [PATCH 139/176] fix for final strict type issues in emoji.ts and emojiprovider --- src/autocomplete/EmojiProvider.tsx | 2 +- src/components/views/emojipicker/EmojiPicker.tsx | 4 ++++ src/emoji.ts | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 365aed076bb..88ec27ab6dd 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -150,7 +150,7 @@ export default class EmojiProvider extends AutocompleteProvider { const emojisAndEmotes = [...SORTED_EMOJI]; for (const [key, val] of this.emotes) { emojisAndEmotes.push({ - emoji: { label: key, shortcodes: [key], hexcode: key, unicode: val as string }, + emoji: { label: key, shortcodes: [key], hexcode: key, unicode: val as string, order: 0, group: 0 }, _orderBy: 0, }); } diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 294a8379aaf..f956dc62a27 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -200,6 +200,8 @@ class EmojiPicker extends React.Component { unicode: ":" + key + ":", customLabel: key, customComponent: val, + group: 0, + order: 0, }); this.finalEmotesMap.set((":" + key + ":").trim(), { label: key, @@ -208,6 +210,8 @@ class EmojiPicker extends React.Component { unicode: ":" + key + ":", customLabel: key, customComponent: val, + group: 0, + order: 0, }); } diff --git a/src/emoji.ts b/src/emoji.ts index 994ae757a1f..2f4ab84fa94 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -19,9 +19,9 @@ import SHORTCODES from "emojibase-data/en/shortcodes/iamcal.json"; export interface IEmoji { label: string; - group?: number; + group: number; hexcode: string; - order?: number; + order: number; shortcodes: string[]; tags?: string[]; unicode: string; From 30303daa4a7444116fe05543371fe85c1d68b0e7 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Tue, 20 Jun 2023 14:29:42 -0400 Subject: [PATCH 140/176] Added tests --- res/css/views/settings/_EmoteSettings.pcss | 4 + .../views/room_settings/RoomEmoteSettings.tsx | 3 +- .../tabs/room/EmoteSettingsTab-test.tsx | 158 ++++++++++++++++++ 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index d9b9b4fa507..a034851a1f6 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -29,6 +29,10 @@ limitations under the License. margin-left: auto; align-self: center; } + .mx_EmoteSettings_deleteButton { + margin-left: auto; + align-self: center; + } .mx_EmoteSettings_uploadedEmoteImage { height: 30px; width: var(--emote-image-width) * 30 / var(--emote-image-height); // Sets emote height to 30px and scales the width accordingly diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 92e569cfe73..f3aa0f4fab2 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -378,13 +378,14 @@ export default class RoomEmoteSettings extends React.Component { className="mx_EmoteSettings_uploadedEmoteImage" src={this.state.decryptedemotes.get(emotecode)} /> -
    +
    {_t("Delete")} diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx new file mode 100644 index 00000000000..809fd195dbb --- /dev/null +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -0,0 +1,158 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { fireEvent, render, RenderResult, screen } from "@testing-library/react"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { mocked } from "jest-mock"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; + +import EmoteRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/EmoteSettingsTab"; +import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../test-utils"; +import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; + +describe("EmoteSettingsTab", () => { + const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); + const roomId = "!room:example.com"; + let cli: MatrixClient; + let room: Room; + + const renderTab = (propRoom: Room = room): RenderResult => { + return render(, withClientContextRenderOptions(cli)); + }; + + beforeEach(() => { + stubClient(); + cli = MatrixClientPeg.safeGet(); + room = mkStubRoom(roomId, "test room", cli); + }); + + it("should allow an Admin to upload emotes", () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(room.currentState.maySendStateEvent).mockReturnValue(true); + const tab = renderTab(); + + const editEmotesButton = tab.container.querySelector("div.mx_EmoteSettings_uploadButton"); + if (!editEmotesButton) { + throw new Error("upload emote button does not exist."); + } + }); + + it("should not let non-admin upload emotes", () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(room.currentState.maySendStateEvent).mockReturnValue(false); + const tab = renderTab(); + const editEmotesButton = tab.container.querySelector("div.mx_EmoteSettings_uploadButton"); + if (editEmotesButton) { + throw new Error("upload emote button exists for non-permissioned user."); + } + }); + + it("should load emotes", () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + const tab = renderTab(); + const emotefield = tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode"); + if (!emotefield) { + throw new Error("emote isn't loading"); + } + }); + + it("should delete when delete is clicked and restore emotes when cancel is clicked", () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + const tab = renderTab(); + + fireEvent.click(screen.getByText("Delete")); + let emotefield = tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode"); + if (emotefield) { + throw new Error("not deleting"); + } + fireEvent.click(screen.getByText("Cancel")); + emotefield = tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode"); + if (!emotefield) { + throw new Error("not restoring when cancel is clicked"); + } + }); + + it("should save edits to emotes", () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + + const tab = renderTab(); + + fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode"), { + target: { value: "changed" }, + }); + fireEvent.click(screen.getByText("Save")); + expect(cli.sendStateEvent).toHaveBeenCalledWith( + roomId, + EMOTES_STATE.name, + { changed: "http://this.is.a.url/server/custom-emote-123.png" }, + "", + ); + }); +}); From f9c346f74e10cbc8541b62f5208a8c46b80a42ab Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 21 Jun 2023 11:18:47 -0400 Subject: [PATCH 141/176] Add more test coverage and remove sonar and strict type check bug --- .../views/room_settings/RoomEmoteSettings.tsx | 6 +- .../tabs/room/EmoteSettingsTab-test.tsx | 178 +++++++++++++++++- 2 files changed, 179 insertions(+), 5 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index f3aa0f4fab2..f54f50b8eba 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -226,7 +226,7 @@ export default class RoomEmoteSettings extends React.Component { } newState.value = value; await client.sendStateEvent(this.props.roomId, EMOTES_STATE.name, emotesMxcs, ""); - + this.imagePack = { images: {} }; for (const [key, val] of newPack) { this.imagePack["images"][key] = val; } @@ -363,7 +363,9 @@ export default class RoomEmoteSettings extends React.Component { const existingEmotes: Array = []; if (this.state.emotes) { - for (const emotecode of Array.from(this.state.emotes.keys()).sort()) { + for (const emotecode of Array.from(this.state.emotes.keys()).sort(function (a, b) { + return a.localeCompare(b); + })) { existingEmotes.push(
  • ({ + uploadFile: jest.fn(), +})); describe("EmoteSettingsTab", () => { const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); + const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); const roomId = "!room:example.com"; let cli: MatrixClient; let room: Room; - + let newemotefile: File; const renderTab = (propRoom: Room = room): RenderResult => { return render(, withClientContextRenderOptions(cli)); }; @@ -40,6 +46,10 @@ describe("EmoteSettingsTab", () => { stubClient(); cli = MatrixClientPeg.safeGet(); room = mkStubRoom(roomId, "test room", cli); + newemotefile = new File(["(⌐□_□)"], "coolnewemote.png", { type: "image/png" }); + mocked(uploadFile).mockResolvedValue({ + url: "http://this.is.a.url/server/custom-emote-123.png", + }); }); it("should allow an Admin to upload emotes", () => { @@ -144,7 +154,7 @@ describe("EmoteSettingsTab", () => { const tab = renderTab(); - fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode"), { + fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode")!, { target: { value: "changed" }, }); fireEvent.click(screen.getByText("Save")); @@ -155,4 +165,166 @@ describe("EmoteSettingsTab", () => { "", ); }); + + it("should save emote deletion", () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + renderTab(); + fireEvent.click(screen.getByText("Delete")); + fireEvent.click(screen.getByText("Save")); + expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EMOTES_STATE.name, {}, ""); + }); + + it("should save new emotes", async () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + const tab = renderTab(); + + fireEvent.click(screen.getByText("Upload Emote")); + await waitFor(() => + fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_emoteUpload")!, { + target: { files: [newemotefile] }, + }), + ); + fireEvent.click(screen.getByText("Save")); + await new Promise(process.nextTick); + expect(cli.sendStateEvent).toHaveBeenCalledWith( + roomId, + EMOTES_STATE.name, + { coolnewemote: "http://this.is.a.url/server/custom-emote-123.png" }, + "", + ); + }); + + it("should enable compatibility", async () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + + const tab = renderTab(); + fireEvent.click(tab.container.querySelector("div.mx_ToggleSwitch")!); + await new Promise(process.nextTick); + expect(cli.sendStateEvent).toHaveBeenCalledWith( + roomId, + EMOTES_COMP.name, + { + images: { + testEmote: { + url: "http://this.is.a.url/server/custom-emote-123.png", + }, + }, + }, + "", + ); + }); + + it("should save edits to emotes in compatibility", async () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + if (type === EMOTES_COMP.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + images: { + testEmote: { + url: "http://this.is.a.url/server/custom-emote-123.png", + }, + }, + }, + }); + } + return null; + }); + + const tab = renderTab(); + fireEvent.click(tab.container.querySelector("div.mx_ToggleSwitch")!); + fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode")!, { + target: { value: "changed" }, + }); + fireEvent.click(screen.getByText("Save")); + await new Promise(process.nextTick); + expect(cli.sendStateEvent).toHaveBeenLastCalledWith( + roomId, + EMOTES_COMP.name, + { + images: { + changed: { + url: "http://this.is.a.url/server/custom-emote-123.png", + }, + }, + }, + "", + ); + }); + + it("should save new emotes in compatibility", async () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + const tab = renderTab(); + fireEvent.click(tab.container.querySelector("div.mx_ToggleSwitch")!); + fireEvent.click(screen.getByText("Upload Emote")); + await waitFor(() => + fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_emoteUpload")!, { + target: { files: [newemotefile] }, + }), + ); + fireEvent.click(screen.getByText("Save")); + await new Promise(process.nextTick); + await new Promise(process.nextTick); + expect(cli.sendStateEvent).toHaveBeenLastCalledWith( + roomId, + EMOTES_COMP.name, + { images: { coolnewemote: { url: "http://this.is.a.url/server/custom-emote-123.png" } } }, + "", + ); + }); }); From 556b1d6a1c0b3c9e64613190b8db7b567672c053 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 21 Jun 2023 15:16:35 -0400 Subject: [PATCH 142/176] Adding some more tests to increase coverage --- .../views/emojipicker/EmojiPicker-test.tsx | 46 ++++++++- .../views/rooms/SendMessageComposer-test.tsx | 99 ++++++++++++++++++- .../tabs/room/EmoteSettingsTab-test.tsx | 30 +++++- 3 files changed, 170 insertions(+), 5 deletions(-) diff --git a/test/components/views/emojipicker/EmojiPicker-test.tsx b/test/components/views/emojipicker/EmojiPicker-test.tsx index de2469ced69..9a3b02edee2 100644 --- a/test/components/views/emojipicker/EmojiPicker-test.tsx +++ b/test/components/views/emojipicker/EmojiPicker-test.tsx @@ -17,10 +17,18 @@ limitations under the License. import React, { createRef } from "react"; import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { mocked } from "jest-mock"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { mkStubRoom, stubClient } from "../../../test-utils"; import EmojiPicker from "../../../../src/components/views/emojipicker/EmojiPicker"; -import { stubClient } from "../../../test-utils"; +import { Media, mediaFromMxc } from "../../../../src/customisations/Media"; +jest.mock("../../../../src/customisations/Media", () => ({ + mediaFromMxc: jest.fn(), +})); +const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); describe("EmojiPicker", function () { stubClient(); @@ -98,4 +106,40 @@ describe("EmojiPicker", function () { expect(onChoose).toHaveBeenCalledWith("📫️"); expect(onFinished).toHaveBeenCalled(); }); + it("should load custom emotes", async () => { + const cli = stubClient(); + const room = mkStubRoom("!roomId:server", "Room", cli); + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + mocked(mediaFromMxc).mockReturnValue({ + srcHttp: "http://this.is.a.url/server/custom-emote-123.png", + } as Media); + const ref = createRef(); + + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: room.roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + const { container } = render( + false} onFinished={jest.fn()} room={room} />, + ); + await new Promise(process.nextTick); + + const customCategory = container.querySelector("#mx_EmojiPicker_category_custom"); + if (!customCategory) { + throw new Error("custom emote not in emojipicker"); + } + }); }); diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index bf9fb0e1851..a935d0cc645 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -16,9 +16,10 @@ limitations under the License. import React from "react"; import { fireEvent, render, waitFor } from "@testing-library/react"; -import { IContent, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix"; +import { IContent, MatrixClient, MatrixEvent, MsgType } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import userEvent from "@testing-library/user-event"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import SendMessageComposer, { attachMentions, @@ -45,7 +46,11 @@ import SettingsStore from "../../../../src/settings/SettingsStore"; jest.mock("../../../../src/utils/local-room", () => ({ doMaybeLocalRoomAction: jest.fn(), })); - +const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); +const COMPAT_STATE = new UnstableValue( + "org.matrix.msc3892.clientemote_compatibility", + "m.room.clientemote_compatibility", +); describe("", () => { const defaultRoomContext: IRoomState = { roomLoading: true, @@ -601,4 +606,94 @@ describe("", () => { await userEvent.type(composer, "Hello"); expect(cli.prepareToEncrypt).toHaveBeenCalled(); }); + + it("should load compatible emotes and replace them in messages when compatibility is on", async () => { + const cli = stubClient(); + const room = mkStubRoom("!roomId:server", "Room", cli); + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_COMP.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: room.roomId, + type: EMOTES_COMP.name, + state_key: "", + content: { + images: { + testEmote: { + url: "http://this.is.a.url/server/custom-emote-123.png", + }, + }, + }, + }); + } + if (type === COMPAT_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: room.roomId, + type: EMOTES_COMP.name, + state_key: "", + content: { + isCompat: true, + }, + }); + } + + return null; + }); + + render( + + + , + ); + + const permalinkCreator = jest.fn() as any; + const model = new EditorModel([], createPartCreator()); + const documentOffset = new DocumentOffset(30, true); + model.update(":testEmote: and some text", "insertText", documentOffset); + + const emoteMap = new Map(); + emoteMap.set( + ":testEmote:", + ":testEmote:", + ); + + let content = createMessageContent( + "@alice:test", + model, + undefined, + undefined, + permalinkCreator, + undefined, + emoteMap, + true, + ); + + expect(content).toEqual({ + body: ":testEmote: and some text", + msgtype: "m.text", + format: "org.matrix.custom.html", + formatted_body: + ":testEmote: and some text", + }); + + content = createMessageContent( + "@alice:test", + model, + undefined, + undefined, + permalinkCreator, + undefined, + emoteMap, + false, + ); + expect(content).toEqual({ + body: ":testEmote: and some text", + msgtype: "m.text", + }); + }); }); diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index 9c818e54bbd..d7e59e19323 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -194,6 +194,23 @@ describe("EmoteSettingsTab", () => { it("should save new emotes", async () => { mocked(cli.getRoom).mockReturnValue(room); mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + oldEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + const tab = renderTab(); fireEvent.click(screen.getByText("Upload Emote")); @@ -207,7 +224,10 @@ describe("EmoteSettingsTab", () => { expect(cli.sendStateEvent).toHaveBeenCalledWith( roomId, EMOTES_STATE.name, - { coolnewemote: "http://this.is.a.url/server/custom-emote-123.png" }, + { + coolnewemote: "http://this.is.a.url/server/custom-emote-123.png", + oldEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, "", ); }); @@ -317,13 +337,19 @@ describe("EmoteSettingsTab", () => { target: { files: [newemotefile] }, }), ); + fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_emoteField")!, { + target: { value: "" }, + }); + fireEvent.change(tab.container.querySelector("input.mx_EmoteSettings_emoteField")!, { + target: { value: "coolnewemotecustomname" }, + }); fireEvent.click(screen.getByText("Save")); await new Promise(process.nextTick); await new Promise(process.nextTick); expect(cli.sendStateEvent).toHaveBeenLastCalledWith( roomId, EMOTES_COMP.name, - { images: { coolnewemote: { url: "http://this.is.a.url/server/custom-emote-123.png" } } }, + { images: { coolnewemotecustomname: { url: "http://this.is.a.url/server/custom-emote-123.png" } } }, "", ); }); From 4c1298c3b9f8cf0f89b37b608283801c02427a4d Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 22 Jun 2023 09:55:05 -0400 Subject: [PATCH 143/176] final test push --- src/autocomplete/EmojiProvider.tsx | 4 ++- test/autocomplete/EmojiProvider-test.ts | 31 +++++++++++++++++++ .../tabs/room/EmoteSettingsTab-test.tsx | 11 ++++--- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 88ec27ab6dd..00ff5356c03 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -22,6 +22,7 @@ import React from "react"; import { uniq, sortBy, uniqBy, ListIteratee } from "lodash"; import EMOTICON_REGEX from "emojibase-regex/emoticon"; import { Room } from "matrix-js-sdk/src/models/room"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; @@ -43,6 +44,7 @@ const LIMIT = 20; // Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase // anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs const EMOJI_REGEX = new RegExp("(" + EMOTICON_REGEX.source + "|(?:^|\\s):[+-\\w]*:?)$", "g"); +const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); interface ISortedEmoji { emoji: IEmoji; @@ -88,7 +90,7 @@ export default class EmojiProvider extends AutocompleteProvider { private emotesPromise?: Promise>; public constructor(room: Room, renderingType?: TimelineRenderingType) { super({ commandRegex: EMOJI_REGEX, renderingType }); - const emotesEvent = room?.currentState.getStateEvents("m.room.emotes", ""); + const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; const emotesMap = new Map(); for (const shortcode in rawEmotes) { diff --git a/test/autocomplete/EmojiProvider-test.ts b/test/autocomplete/EmojiProvider-test.ts index a18951ce7cc..1912d9c8c0a 100644 --- a/test/autocomplete/EmojiProvider-test.ts +++ b/test/autocomplete/EmojiProvider-test.ts @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { mocked } from "jest-mock"; +import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; + import EmojiProvider from "../../src/autocomplete/EmojiProvider"; import { mkStubRoom } from "../test-utils/test-utils"; import { add } from "../../src/emojipicker/recent"; @@ -40,6 +44,7 @@ const EMOJI_SHORTCODES = [ // This means that we cannot compare their autocompletion before and after the ending `:` and have // to simply assert that the final completion with the colon is the exact emoji. const TOO_SHORT_EMOJI_SHORTCODE = [{ emojiShortcode: ":o", expectedEmoji: "⭕️" }]; +const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); describe("EmojiProvider", function () { const testRoom = mkStubRoom(undefined, undefined, undefined); @@ -95,4 +100,30 @@ describe("EmojiProvider", function () { expect(completionsList[1]?.component?.props.title).toEqual(":heartpulse:"); expect(completionsList[2]?.component?.props.title).toEqual(":heart_eyes:"); }); + + it("loads and returns custom emotes", async function () { + const cli = stubClient(); + mocked(cli.getRoom).mockReturnValue(testRoom); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(testRoom.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: testRoom.roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "abcde/custom-emote-123.png", + }, + }); + } + return null; + }); + + const ep = new EmojiProvider(testRoom); + const completionsList = await ep.getCompletions(":testEmote", { beginning: true, start: 0, end: 6 }); + expect(completionsList[0]?.component?.props.title).toEqual("http://this.is.a.url/custom-emote-123.png"); + }); }); diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index d7e59e19323..df8648a3e21 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -113,6 +113,7 @@ describe("EmoteSettingsTab", () => { state_key: "", content: { testEmote: "http://this.is.a.url/server/custom-emote-123.png", + anotherEmote: "http://this.is.a.url/server/custom-emote-123.png", }, }); } @@ -120,14 +121,14 @@ describe("EmoteSettingsTab", () => { }); const tab = renderTab(); - fireEvent.click(screen.getByText("Delete")); - let emotefield = tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode"); - if (emotefield) { + fireEvent.click(screen.getAllByText("Delete")[0]); + let emotefieldnum = tab.container.querySelectorAll("input.mx_EmoteSettings_existingEmoteCode").length; + if (emotefieldnum > 1) { throw new Error("not deleting"); } fireEvent.click(screen.getByText("Cancel")); - emotefield = tab.container.querySelector("input.mx_EmoteSettings_existingEmoteCode"); - if (!emotefield) { + emotefieldnum = tab.container.querySelectorAll("input.mx_EmoteSettings_existingEmoteCode").length; + if (emotefieldnum < 2) { throw new Error("not restoring when cancel is clicked"); } }); From 38efe85824de1cbbc6a49e32fa39b2f9836d80ca Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:10:00 -0400 Subject: [PATCH 144/176] Fix for new safeGet check --- src/components/views/room_settings/RoomEmoteSettings.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index f54f50b8eba..d3416d03031 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -69,7 +69,7 @@ export default class RoomEmoteSettings extends React.Component { public constructor(props: IProps) { super(props); - const client = MatrixClientPeg.get(); + const client = MatrixClientPeg.safeGet(); const room = client.getRoom(props.roomId); if (!room) throw new Error(`Expected a room for ID: ${props.roomId}`); @@ -108,7 +108,7 @@ export default class RoomEmoteSettings extends React.Component { this.decryptEmotes(client.isRoomEncrypted(props.roomId)); } public componentDidMount(): void { - const client = MatrixClientPeg.get(); + const client = MatrixClientPeg.safeGet(); this.decryptEmotes(client.isRoomEncrypted(this.props.roomId)); } @@ -172,7 +172,7 @@ export default class RoomEmoteSettings extends React.Component { e.preventDefault(); if (!this.isSaveEnabled()) return; - const client = MatrixClientPeg.get(); + const client = MatrixClientPeg.safeGet(); const newState: Partial = {}; const emotesMxcs: { [key: string]: IEncryptedFile | string } = {}; const value = new Map(); @@ -309,7 +309,7 @@ export default class RoomEmoteSettings extends React.Component { }; private onCompatChange = async (allowed: boolean): Promise => { - const client = MatrixClientPeg.get(); + const client = MatrixClientPeg.safeGet(); await client.sendStateEvent(this.props.roomId, COMPAT_STATE.name, { isCompat: allowed }, ""); if (allowed) { From 22c47b76bba0d7d5dc066765f934b2468cef93b2 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Tue, 27 Jun 2023 11:19:35 -0400 Subject: [PATCH 145/176] Make custom emotes a lab setting --- src/autocomplete/EmojiProvider.tsx | 3 ++- .../views/dialogs/RoomSettingsDialog.tsx | 20 ++++++++++--------- .../views/emojipicker/EmojiPicker.tsx | 6 ++++-- src/components/views/messages/TextualBody.tsx | 5 ++++- .../views/rooms/SendMessageComposer.tsx | 7 ++++--- src/settings/Settings.tsx | 7 +++++++ test/autocomplete/EmojiProvider-test.ts | 3 +++ .../views/emojipicker/EmojiPicker-test.tsx | 3 +++ .../views/rooms/SendMessageComposer-test.tsx | 5 +++++ 9 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 00ff5356c03..5b747be6179 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -93,10 +93,11 @@ export default class EmojiProvider extends AutocompleteProvider { const emotesEvent = room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; const emotesMap = new Map(); + const customEmotesEnabled = SettingsStore.getValue("feature_custom_emotes"); for (const shortcode in rawEmotes) { emotesMap.set(shortcode, rawEmotes[shortcode]); } - if (room) { + if (room && customEmotesEnabled) { this.emotesPromise = this.decryptEmotes(emotesMap, room?.roomId); } this.matcher = new QueryMatcher(SORTED_EMOJI, { diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 6ed551fbd89..840136c488d 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -177,15 +177,17 @@ class RoomSettingsDialog extends React.Component { ), ); - tabs.push( - new Tab( - RoomSettingsTab.Emotes, - _td("Emotes"), - "mx_RoomSettingsDialog_emotesIcon", - , - "RoomSettingsNotifications", - ), - ); + if (SettingsStore.getValue("feature_custom_emotes")) { + tabs.push( + new Tab( + RoomSettingsTab.Emotes, + _td("Emotes"), + "mx_RoomSettingsDialog_emotesIcon", + , + "RoomSettingsNotifications", + ), + ); + } if (SettingsStore.getValue("feature_bridge_state")) { tabs.push( diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index f956dc62a27..ebb29bf880e 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -43,6 +43,7 @@ import { mediaFromMxc } from "../../../customisations/Media"; import { decryptFile } from "../../../utils/DecryptFile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; +import SettingsStore from "../../../settings/SettingsStore"; export const CATEGORY_HEADER_HEIGHT = 20; export const EMOJI_HEIGHT = 35; @@ -87,16 +88,17 @@ class EmojiPicker extends React.Component { const emotesEvent = props.room?.currentState.getStateEvents(EMOTES_STATE.name, ""); const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; const emotesMap = new Map(); + const customEmotesEnabled = SettingsStore.getValue("feature_custom_emotes"); for (const shortcode in rawEmotes) { emotesMap.set(shortcode, rawEmotes[shortcode]); } - if (props.room) { + if (props.room && customEmotesEnabled) { this.emotesPromise = this.decryptEmotes(emotesMap, props.room?.roomId); } this.finalEmotes = []; this.finalEmotesMap = new Map(); - if (props.room) { + if (props.room && customEmotesEnabled) { this.loadEmotes(); } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 3a9b237cbcb..464b1d1c1dd 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -92,7 +92,10 @@ export default class TextualBody extends React.Component { } private applyFormatting(): void { - if (MatrixClientPeg.get()?.getRoom(this.props.mxEvent.getRoomId())) { + if ( + MatrixClientPeg.get()?.getRoom(this.props.mxEvent.getRoomId()) && + SettingsStore.getValue("feature_custom_emotes") + ) { this.decryptEmotes(); } const content = this.contentRef.current!; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 11fa66727b1..941b276dfd5 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -200,7 +200,8 @@ export function createMessageContent( const body = textSerialize(model); let emoteBody; - if (compat && emotes) { + const customEmotesEnabled = SettingsStore.getValue("feature_custom_emotes"); + if (compat && emotes && customEmotesEnabled) { emoteBody = body.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m)! : m)); } const content: IContent = { @@ -212,12 +213,12 @@ export function createMessageContent( useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), }); if (formattedBody) { - if (compat && emotes) { + if (compat && emotes && customEmotesEnabled) { formattedBody = formattedBody.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m)! : m)); } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; - } else if (compat) { + } else if (compat && customEmotesEnabled) { if (body != emoteBody) { content.format = "org.matrix.custom.html"; content.formatted_body = emoteBody; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 52bcac185ad..319d0067742 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -253,6 +253,13 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_custom_emotes": { + isFeature: true, + labsGroup: LabGroup.Messaging, + displayName: _td("Support custom emotes."), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_latex_maths": { isFeature: true, labsGroup: LabGroup.Messaging, diff --git a/test/autocomplete/EmojiProvider-test.ts b/test/autocomplete/EmojiProvider-test.ts index 1912d9c8c0a..d9a113a11d2 100644 --- a/test/autocomplete/EmojiProvider-test.ts +++ b/test/autocomplete/EmojiProvider-test.ts @@ -23,6 +23,7 @@ import { mkStubRoom } from "../test-utils/test-utils"; import { add } from "../../src/emojipicker/recent"; import { stubClient } from "../test-utils"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; +import SettingsStore from "../../src/settings/SettingsStore"; const EMOJI_SHORTCODES = [ ":+1", @@ -103,8 +104,10 @@ describe("EmojiProvider", function () { it("loads and returns custom emotes", async function () { const cli = stubClient(); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); mocked(cli.getRoom).mockReturnValue(testRoom); mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly mocked(testRoom.currentState.getStateEvents).mockImplementation((type, key) => { if (key === undefined) return [] as MatrixEvent[]; diff --git a/test/components/views/emojipicker/EmojiPicker-test.tsx b/test/components/views/emojipicker/EmojiPicker-test.tsx index 9a3b02edee2..54542daf81d 100644 --- a/test/components/views/emojipicker/EmojiPicker-test.tsx +++ b/test/components/views/emojipicker/EmojiPicker-test.tsx @@ -24,6 +24,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { mkStubRoom, stubClient } from "../../../test-utils"; import EmojiPicker from "../../../../src/components/views/emojipicker/EmojiPicker"; import { Media, mediaFromMxc } from "../../../../src/customisations/Media"; +import SettingsStore from "../../../../src/settings/SettingsStore"; jest.mock("../../../../src/customisations/Media", () => ({ mediaFromMxc: jest.fn(), @@ -109,11 +110,13 @@ describe("EmojiPicker", function () { it("should load custom emotes", async () => { const cli = stubClient(); const room = mkStubRoom("!roomId:server", "Room", cli); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); mocked(cli.getRoom).mockReturnValue(room); mocked(cli.isRoomEncrypted).mockReturnValue(false); mocked(mediaFromMxc).mockReturnValue({ srcHttp: "http://this.is.a.url/server/custom-emote-123.png", } as Media); + const ref = createRef(); // @ts-ignore - mocked doesn't support overloads properly diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index a935d0cc645..71f62d4dcfb 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -610,6 +610,11 @@ describe("", () => { it("should load compatible emotes and replace them in messages when compatibility is on", async () => { const cli = stubClient(); const room = mkStubRoom("!roomId:server", "Room", cli); + jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => { + if (settingName === "feature_custom_emotes") { + return true; + } + }); mocked(cli.getRoom).mockReturnValue(room); mocked(cli.isRoomEncrypted).mockReturnValue(false); // @ts-ignore - mocked doesn't support overloads properly From 6193d097c60eca5cb6d1d01f687b9f29e63f5695 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Tue, 27 Jun 2023 11:32:58 -0400 Subject: [PATCH 146/176] i18n and a fix --- src/components/views/emojipicker/EmojiPicker.tsx | 6 +++--- src/i18n/strings/en_EN.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index ebb29bf880e..2fea743f107 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -126,8 +126,8 @@ class EmojiPicker extends React.Component { { id: "custom", name: _t("Custom"), - enabled: true, - visible: true, + enabled: customEmotesEnabled, + visible: customEmotesEnabled, ref: React.createRef(), }, { @@ -421,7 +421,7 @@ class EmojiPicker extends React.Component { this.memoizedDataByCategory[cat.id] = emojis; cat.enabled = emojis.length > 0; if (cat.id == "custom") { - cat.enabled = true; + cat.enabled = SettingsStore.getValue("feature_custom_emotes"); } // The setState below doesn't re-render the header and we already have the refs for updateVisibility, so... if (cat.ref.current) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 00323028a92..be849290fd9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -958,6 +958,7 @@ "Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.", "Report to moderators": "Report to moderators", "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.", + "Support custom emotes.": "Support custom emotes.", "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Message Pinning": "Message Pinning", "Rich text editor": "Rich text editor", From 22471f07d9cedf7e9e6ae1ea94c19a6f8ad6b4a7 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Tue, 27 Jun 2023 15:47:37 +0000 Subject: [PATCH 147/176] test: add test snapshot for RoomSettingsDialog --- .../RoomSettingsDialog-test.tsx.snap | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap b/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap index 8faccd84b8f..6c148400d88 100644 --- a/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap +++ b/test/components/views/dialogs/__snapshots__/RoomSettingsDialog-test.tsx.snap @@ -74,24 +74,6 @@ NodeList [ Notifications
  • , - ,
  • Date: Tue, 27 Jun 2023 20:13:39 +0000 Subject: [PATCH 148/176] fix: snapshot with HtmlUtils --- src/HtmlUtils.tsx | 7 +--- .../MessageDiffUtils-test.tsx.snap | 37 +++++++++++-------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index f63fa538a28..6c237d7e20c 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -582,12 +582,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } finally { delete sanitizeParams.textFilter; } - - const contentBody = safeBody ?? strippedBody; - if (opts.returnString) { - return contentBody; - } - + let emojiBody = false; if (!opts.disableBigEmoji && bodyHasEmoji) { const contentBody = safeBody ?? strippedBody; diff --git a/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap b/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap index a7325dc29aa..655fc8a8077 100644 --- a/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap +++ b/test/utils/__snapshots__/MessageDiffUtils-test.tsx.snap @@ -38,29 +38,36 @@ exports[`editBodyDiffToHtml handles complex transformations 1`] = ` - - { - - ☃️ - - - 😃 - - }^\\infty + + + { + + ☃️ + + }^\\infty + - {☃️}^\\infty + { + + ☃️ + + }^\\infty From f373fbe2b3c1590fb22642023f5f16445ecf6d95 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Tue, 27 Jun 2023 16:16:49 -0400 Subject: [PATCH 149/176] lint fix htmlutils --- src/HtmlUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 6c237d7e20c..9b6da72b508 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -582,7 +582,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } finally { delete sanitizeParams.textFilter; } - + let emojiBody = false; if (!opts.disableBigEmoji && bodyHasEmoji) { const contentBody = safeBody ?? strippedBody; From fe0e17ba77345d351ab20ecdfaf4b707cc3ef7c8 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Wed, 28 Jun 2023 12:44:48 -0400 Subject: [PATCH 150/176] fix: batch apply suggestions from code review Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/emojipicker/Emoji.tsx | 2 +- src/components/views/emojipicker/EmojiPicker.tsx | 4 ++-- src/components/views/emojipicker/Preview.tsx | 2 +- src/components/views/rooms/SendMessageComposer.tsx | 4 ++-- src/settings/Settings.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index 32f495e7187..20690ffdcba 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -48,7 +48,7 @@ class Emoji extends React.PureComponent { focusOnMouseOver >
    - {emoji.customComponent ? emoji.customComponent : emoji.unicode} + {emoji.customComponent ?? emoji.unicode}
    ); diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 2fea743f107..4052e61488a 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -86,7 +86,7 @@ class EmojiPicker extends React.Component { super(props); const emotesEvent = props.room?.currentState.getStateEvents(EMOTES_STATE.name, ""); - const rawEmotes = emotesEvent ? emotesEvent.getContent() || {} : {}; + const rawEmotes = emotesEvent?.getContent() ?? {}; const emotesMap = new Map(); const customEmotesEnabled = SettingsStore.getValue("feature_custom_emotes"); for (const shortcode in rawEmotes) { @@ -223,7 +223,7 @@ class EmojiPicker extends React.Component { recent .get() .map((x) => - getEmojiFromUnicode(x) ? getEmojiFromUnicode(x) : this.finalEmotesMap.get(x as string), + getEmojiFromUnicode(x) ?? this.finalEmotesMap.get(x as string), ), ), ), diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index 9cca63329ef..f309dae9106 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -34,7 +34,7 @@ class Preview extends React.PureComponent { return (
    -
    {customComponent ? customComponent : unicode}
    +
    {customComponent ?? unicode}
    {label}
    {shortcode}
    diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index d6d727d0cc6..1d6c7488cc9 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -214,7 +214,7 @@ export function createMessageContent( }); if (formattedBody) { if (compat && emotes && customEmotesEnabled) { - formattedBody = formattedBody.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m)! : m)); + formattedBody = formattedBody.replace(/:[\w+-]+:/g, (m) => (emotes.has(m) ? emotes.get(m)! : m)); } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; @@ -321,7 +321,7 @@ export class SendMessageComposer extends React.Component(); if (!this.imagePack["images"]) { this.imagePack["images"] = {}; diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 319d0067742..6c94f55164f 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -256,7 +256,7 @@ export const SETTINGS: { [setting: string]: ISetting } = { "feature_custom_emotes": { isFeature: true, labsGroup: LabGroup.Messaging, - displayName: _td("Support custom emotes."), + displayName: _td("Support custom emotes"), supportedLevels: LEVELS_FEATURE, default: false, }, From 89e92383d7d2105a084b0340109a9be7271e3437 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Wed, 28 Jun 2023 16:55:58 +0000 Subject: [PATCH 151/176] fix: cleanup comments and unused code --- src/components/views/emojipicker/EmojiPicker.tsx | 10 ++-------- .../views/settings/tabs/room/EmoteSettingsTab.tsx | 9 ++------- .../views/settings/tabs/room/EmoteSettingsTab-test.tsx | 2 +- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 4052e61488a..abdf5a6014d 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -219,15 +219,9 @@ class EmojiPicker extends React.Component { const rec = Array.from( new Set( - filterBoolean( - recent - .get() - .map((x) => - getEmojiFromUnicode(x) ?? this.finalEmotesMap.get(x as string), - ), - ), + filterBoolean(recent.get().map((x) => getEmojiFromUnicode(x) ?? this.finalEmotesMap.get(x as string))), ), - ); //Array.from(new Set(recent.get())); + ); rec.forEach((v, i) => { if (this.finalEmotesMap.get(v.unicode)) { if (i >= this.recentlyUsed.length) { diff --git a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx index 4f08607beaa..bab0c24e116 100644 --- a/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/EmoteSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2023 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. @@ -24,14 +24,9 @@ interface IProps { roomId: string; } -interface IState { - isRoomPublished: boolean; -} +interface IState {} export default class EmoteRoomSettingsTab extends React.Component { - public static contextType = MatrixClientContext; - public context!: ContextType; - public constructor(props: IProps, context: ContextType) { super(props, context); diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index df8648a3e21..aa003f2c96b 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2023 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. From 3a68e2bd9d8efba83b0a8be88b6ddae654cb6ff9 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Wed, 28 Jun 2023 16:59:31 +0000 Subject: [PATCH 152/176] fix: EmoteSettings copyright --- res/css/views/settings/_EmoteSettings.pcss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/settings/_EmoteSettings.pcss b/res/css/views/settings/_EmoteSettings.pcss index a034851a1f6..e5bbe0b0eaa 100644 --- a/res/css/views/settings/_EmoteSettings.pcss +++ b/res/css/views/settings/_EmoteSettings.pcss @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2023 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. From 875491ba4f26180b5272d33dd0d8072fbf1c3a43 Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Wed, 28 Jun 2023 17:32:20 +0000 Subject: [PATCH 153/176] revert: test-utils changes --- test/test-utils/test-utils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 31ba9e86224..3451f17a59b 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -156,8 +156,7 @@ export function createTestClient(): MatrixClient { content: {}, }); }), - - mxcUrlToHttp: (mxc: string) => `http://this.is.a.url/${mxc.toString().substring(6)}`, + mxcUrlToHttp: (mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`, scheduleAllGroupSessionsForBackup: jest.fn().mockResolvedValue(undefined), setAccountData: jest.fn(), setRoomAccountData: jest.fn(), From 7db0bfc6f8d6721526952a8ace458fdb0c13d0d7 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:30:41 -0400 Subject: [PATCH 154/176] Review fixes --- src/autocomplete/Components.tsx | 3 ++- src/autocomplete/EmojiProvider.tsx | 3 ++- .../views/emojipicker/EmojiPicker.tsx | 5 ++-- src/components/views/rooms/EmojiButton.tsx | 9 +------ .../views/rooms/SendMessageComposer.tsx | 26 ++++++++----------- src/emoji.ts | 4 +-- 6 files changed, 20 insertions(+), 30 deletions(-) diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 40c8c96def7..4bb2d548754 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -60,6 +60,7 @@ export const PillCompletion = forwardRef((props, ref) children, "aria-selected": ariaSelectedAttribute, isEmote, + titleComponent, ...restProps } = props; return ( @@ -72,7 +73,7 @@ export const PillCompletion = forwardRef((props, ref) > {children} - {isEmote ? : title} + {isEmote ? : title} {subtitle} {description} diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 5b747be6179..eb38acd678d 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -218,7 +218,8 @@ export default class EmojiProvider extends AutocompleteProvider { component: ( {this.emotes.get(c.emoji.hexcode) ? c.emoji.hexcode : c.emoji.unicode} diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index abdf5a6014d..267d2b590da 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -382,11 +382,10 @@ class EmojiPicker extends React.Component { // If the new filter string includes the old filter string, we don't have to re-filter the whole dataset. if (lcFilter.includes(this.state.filter)) { emojis = this.memoizedDataByCategory[cat.id]; + } else if (cat.id === "custom") { + emojis = this.finalEmotes; } else { emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id]; - if (cat.id === "custom") { - emojis = this.finalEmotes; - } } if (lcFilter !== "") { diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index 0a8024c0400..63607898896 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -44,14 +44,7 @@ export function EmojiButton({ addEmoji, menuPosition, className, room }: IEmojiB }; contextMenu = ( - { - closeMenu(); - overflowMenuCloser?.(); - }} - managed={false} - > + ); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 1d6c7488cc9..d01eab69a6d 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -83,7 +83,8 @@ const COMPAT_STATE = new UnstableValue( "m.room.clientemote_compatibility", ); const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); - +const EMOTES_REGEX = /:[\w+-]+:/g; +const SHORTCODE_REGEX = /[^a-zA-Z0-9_]/g; export function attachMentions( sender: string, content: IContent, @@ -202,7 +203,7 @@ export function createMessageContent( let emoteBody; const customEmotesEnabled = SettingsStore.getValue("feature_custom_emotes"); if (compat && emotes && customEmotesEnabled) { - emoteBody = body.replace(/:[\w+-]+:/g, (m) => (emotes.get(m) ? emotes.get(m)! : m)); + emoteBody = body.replace(EMOTES_REGEX, (m) => (emotes.get(m) ? emotes.get(m)! : m)); } const content: IContent = { msgtype: isEmote ? MsgType.Emote : MsgType.Text, @@ -214,7 +215,7 @@ export function createMessageContent( }); if (formattedBody) { if (compat && emotes && customEmotesEnabled) { - formattedBody = formattedBody.replace(/:[\w+-]+:/g, (m) => (emotes.has(m) ? emotes.get(m)! : m)); + formattedBody = formattedBody.replace(EMOTES_REGEX, (m) => (emotes.has(m) ? emotes.get(m)! : m)); } content.format = "org.matrix.custom.html"; content.formatted_body = formattedBody; @@ -329,18 +330,13 @@ export class SendMessageComposer extends React.Component", + `:${shortcode.replace(SHORTCODE_REGEX, "")}:`, + `:${shortcode.replace(SHORTCODE_REGEX, "")}:`, ); } } diff --git a/src/emoji.ts b/src/emoji.ts index 2f4ab84fa94..d7de45d5e47 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -27,8 +27,8 @@ export interface IEmoji { unicode: string; skins?: Omit[]; // Currently unused emoticon?: string | string[]; - customLabel?: string; - customComponent?: JSX.Element; + customLabel?: string; // Custom label for custom emotes in emojipicker + customComponent?: JSX.Element; // Custom react component for rendering custom emotes in emojipicker } // The unicode is stored without the variant selector From 47bab13b65983f28e851afd424210e45f208e589 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:45:39 -0400 Subject: [PATCH 155/176] Review fixes 2 --- src/components/views/dialogs/RoomSettingsDialog.tsx | 2 +- src/components/views/emojipicker/EmojiPicker.tsx | 4 ++-- src/components/views/rooms/SendMessageComposer.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 840136c488d..ff2854cc22f 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -184,7 +184,7 @@ class RoomSettingsDialog extends React.Component { _td("Emotes"), "mx_RoomSettingsDialog_emotesIcon", , - "RoomSettingsNotifications", + null, ), ); } diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 267d2b590da..2b5d9a93eb8 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -217,12 +217,12 @@ class EmojiPicker extends React.Component { }); } - const rec = Array.from( + const recentEmotes = Array.from( new Set( filterBoolean(recent.get().map((x) => getEmojiFromUnicode(x) ?? this.finalEmotesMap.get(x as string))), ), ); - rec.forEach((v, i) => { + recentEmotes.forEach((v, i) => { if (this.finalEmotesMap.get(v.unicode)) { if (i >= this.recentlyUsed.length) { this.recentlyUsed.push(this.finalEmotesMap.get(v.unicode)!); diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index d01eab69a6d..bd958461e4d 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -200,7 +200,7 @@ export function createMessageContent( const body = textSerialize(model); - let emoteBody; + let emoteBody: string; const customEmotesEnabled = SettingsStore.getValue("feature_custom_emotes"); if (compat && emotes && customEmotesEnabled) { emoteBody = body.replace(EMOTES_REGEX, (m) => (emotes.get(m) ? emotes.get(m)! : m)); From 499638c0926a59a4b02021fd0766ca83b9dc0470 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 28 Jun 2023 14:56:55 -0400 Subject: [PATCH 156/176] Update _EventTile.pcss --- res/css/views/rooms/_EventTile.pcss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 82b0897d97e..3fd95b64233 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -827,9 +827,6 @@ $left-gutter: 64px; } } -.mx_Emote { - height: 30px; -} .mx_EventTile_e2eIcon { position: relative; width: 14px; @@ -944,6 +941,9 @@ $left-gutter: 64px; width: 17px; } } + .mx_Emote { + height: 32px; + } } .mx_EventTile_lineNumbers { From dbf12825a2aefb8405a34caec06c711aa3c2a601 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 28 Jun 2023 15:10:43 -0400 Subject: [PATCH 157/176] Emotes css fix --- res/css/_components.pcss | 1 + res/css/views/rooms/_Emotes.pcss | 3 +++ res/css/views/rooms/_EventTile.pcss | 3 --- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 res/css/views/rooms/_Emotes.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 530f9739316..0532c1afc97 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -270,6 +270,7 @@ @import "./views/rooms/_E2EIcon.pcss"; @import "./views/rooms/_EditMessageComposer.pcss"; @import "./views/rooms/_EmojiButton.pcss"; +@import "./views/rooms/_Emotes.pcss"; @import "./views/rooms/_EntityTile.pcss"; @import "./views/rooms/_EventBubbleTile.pcss"; @import "./views/rooms/_EventTile.pcss"; diff --git a/res/css/views/rooms/_Emotes.pcss b/res/css/views/rooms/_Emotes.pcss new file mode 100644 index 00000000000..25b9b810c62 --- /dev/null +++ b/res/css/views/rooms/_Emotes.pcss @@ -0,0 +1,3 @@ +.mx_Emote { + height: 32px; +} diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 3fd95b64233..849609acf93 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -941,9 +941,6 @@ $left-gutter: 64px; width: 17px; } } - .mx_Emote { - height: 32px; - } } .mx_EventTile_lineNumbers { From f71b528ddd9ae77ecf7a22902e7af004a751654e Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 28 Jun 2023 19:02:07 -0400 Subject: [PATCH 158/176] Fix strings --- src/components/views/room_settings/RoomEmoteSettings.tsx | 5 ++++- src/i18n/strings/en_EN.json | 4 ++-- src/settings/Settings.tsx | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index d3416d03031..f7cb51aefb1 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -452,7 +452,10 @@ export default class RoomEmoteSettings extends React.Component { Date: Wed, 28 Jun 2023 23:39:59 +0000 Subject: [PATCH 159/176] fix: unstable value and compat mode --- src/autocomplete/EmojiProvider.tsx | 2 +- src/components/views/emojipicker/EmojiPicker.tsx | 2 +- src/components/views/messages/TextualBody.tsx | 2 +- src/components/views/room_settings/RoomEmoteSettings.tsx | 8 ++++---- src/components/views/rooms/SendMessageComposer.tsx | 4 ++-- test/autocomplete/EmojiProvider-test.ts | 6 ++++-- test/components/views/emojipicker/EmojiPicker-test.tsx | 2 +- test/components/views/rooms/SendMessageComposer-test.tsx | 4 ++-- .../views/settings/tabs/room/EmoteSettingsTab-test.tsx | 4 ++-- 9 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index eb38acd678d..a293dca5f6d 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -44,7 +44,7 @@ const LIMIT = 20; // Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase // anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs const EMOJI_REGEX = new RegExp("(" + EMOTICON_REGEX.source + "|(?:^|\\s):[+-\\w]*:?)$", "g"); -const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); +const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); interface ISortedEmoji { emoji: IEmoji; diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 2b5d9a93eb8..12193b3885e 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -51,7 +51,7 @@ export const EMOJIS_PER_ROW = 8; const ZERO_WIDTH_JOINER = "\u200D"; -const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); +const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); interface IProps { selectedEmojis?: Set; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 464b1d1c1dd..e1aec2d6f9d 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -54,7 +54,7 @@ import { decryptFile } from "../../../utils/DecryptFile"; import { mediaFromMxc } from "../../../customisations/Media"; const MAX_HIGHLIGHT_LENGTH = 4096; -const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); +const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); interface IState { // the URLs (if any) to be previewed with a LinkPreviewWidget inside this TextualBody. diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index f7cb51aefb1..1d6b21ee3b8 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2023 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. @@ -27,12 +27,12 @@ import { mediaFromMxc } from "../../../customisations/Media"; import SettingsFieldset from "../settings/SettingsFieldset"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; -const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); +const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); const COMPAT_STATE = new UnstableValue( - "org.matrix.msc3892.clientemote_compatibility", "m.room.clientemote_compatibility", + "org.matrix.msc3892.clientemote_compatibility", ); -const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); +const EMOTES_COMP = new UnstableValue("m.room.room_emotes", "im.ponies.room_emotes"); const SHORTCODE_REGEX = /[^a-zA-Z0-9_]/g; interface IProps { roomId: string; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index bd958461e4d..da220d6ce87 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -79,10 +79,10 @@ import { getBlobSafeMimeType } from "../../../utils/blobs"; */ const COMPAT_STATE = new UnstableValue( - "org.matrix.msc3892.clientemote_compatibility", "m.room.clientemote_compatibility", + "org.matrix.msc3892.clientemote_compatibility", ); -const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); +const EMOTES_COMP = new UnstableValue("m.room.room_emotes", "im.ponies.room_emotes"); const EMOTES_REGEX = /:[\w+-]+:/g; const SHORTCODE_REGEX = /[^a-zA-Z0-9_]/g; export function attachMentions( diff --git a/test/autocomplete/EmojiProvider-test.ts b/test/autocomplete/EmojiProvider-test.ts index d9a113a11d2..af423d1890d 100644 --- a/test/autocomplete/EmojiProvider-test.ts +++ b/test/autocomplete/EmojiProvider-test.ts @@ -45,7 +45,7 @@ const EMOJI_SHORTCODES = [ // This means that we cannot compare their autocompletion before and after the ending `:` and have // to simply assert that the final completion with the colon is the exact emoji. const TOO_SHORT_EMOJI_SHORTCODE = [{ emojiShortcode: ":o", expectedEmoji: "⭕️" }]; -const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); +const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); describe("EmojiProvider", function () { const testRoom = mkStubRoom(undefined, undefined, undefined); @@ -127,6 +127,8 @@ describe("EmojiProvider", function () { const ep = new EmojiProvider(testRoom); const completionsList = await ep.getCompletions(":testEmote", { beginning: true, start: 0, end: 6 }); - expect(completionsList[0]?.component?.props.title).toEqual("http://this.is.a.url/custom-emote-123.png"); + expect(completionsList[0]?.component?.props.titleComponent).toEqual( + "http://this.is.a.url/custom-emote-123.png", + ); }); }); diff --git a/test/components/views/emojipicker/EmojiPicker-test.tsx b/test/components/views/emojipicker/EmojiPicker-test.tsx index 54542daf81d..d9bb1bab3e4 100644 --- a/test/components/views/emojipicker/EmojiPicker-test.tsx +++ b/test/components/views/emojipicker/EmojiPicker-test.tsx @@ -29,7 +29,7 @@ import SettingsStore from "../../../../src/settings/SettingsStore"; jest.mock("../../../../src/customisations/Media", () => ({ mediaFromMxc: jest.fn(), })); -const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); +const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); describe("EmojiPicker", function () { stubClient(); diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index 71f62d4dcfb..bf43f172989 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -46,10 +46,10 @@ import SettingsStore from "../../../../src/settings/SettingsStore"; jest.mock("../../../../src/utils/local-room", () => ({ doMaybeLocalRoomAction: jest.fn(), })); -const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); +const EMOTES_COMP = new UnstableValue("m.room.room_emotes", "im.ponies.room_emotes"); const COMPAT_STATE = new UnstableValue( - "org.matrix.msc3892.clientemote_compatibility", "m.room.clientemote_compatibility", + "org.matrix.msc3892.clientemote_compatibility", ); describe("", () => { const defaultRoomContext: IRoomState = { diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index aa003f2c96b..186eb3c4541 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -32,8 +32,8 @@ jest.mock("../../../../../../src/ContentMessages", () => ({ })); describe("EmoteSettingsTab", () => { - const EMOTES_STATE = new UnstableValue("org.matrix.msc3892.emotes", "m.room.emotes"); - const EMOTES_COMP = new UnstableValue("im.ponies.room_emotes", "m.room.room_emotes"); + const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); + const EMOTES_COMP = new UnstableValue("m.room.room_emotes", "im.ponies.room_emotes"); const roomId = "!room:example.com"; let cli: MatrixClient; let room: Room; From 594102242017dd4cb6b940026598e21107f1204a Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 29 Jun 2023 04:41:21 -0400 Subject: [PATCH 160/176] strict type fix --- src/components/views/dialogs/RoomSettingsDialog.tsx | 1 - src/components/views/rooms/SendMessageComposer.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index ff2854cc22f..85457530bdb 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -184,7 +184,6 @@ class RoomSettingsDialog extends React.Component { _td("Emotes"), "mx_RoomSettingsDialog_emotesIcon", , - null, ), ); } diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index da220d6ce87..935662cf9d9 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -200,7 +200,7 @@ export function createMessageContent( const body = textSerialize(model); - let emoteBody: string; + let emoteBody: string | undefined; const customEmotesEnabled = SettingsStore.getValue("feature_custom_emotes"); if (compat && emotes && customEmotesEnabled) { emoteBody = body.replace(EMOTES_REGEX, (m) => (emotes.get(m) ? emotes.get(m)! : m)); From b8623eb56758bf81530de15d997dab89403e9642 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:03:28 -0400 Subject: [PATCH 161/176] sonar coverage --- .../views/emojipicker/EmojiPicker-test.tsx | 4 +-- .../views/messages/TextualBody-test.tsx | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/test/components/views/emojipicker/EmojiPicker-test.tsx b/test/components/views/emojipicker/EmojiPicker-test.tsx index d9bb1bab3e4..7bd8b56fbe8 100644 --- a/test/components/views/emojipicker/EmojiPicker-test.tsx +++ b/test/components/views/emojipicker/EmojiPicker-test.tsx @@ -25,7 +25,7 @@ import { mkStubRoom, stubClient } from "../../../test-utils"; import EmojiPicker from "../../../../src/components/views/emojipicker/EmojiPicker"; import { Media, mediaFromMxc } from "../../../../src/customisations/Media"; import SettingsStore from "../../../../src/settings/SettingsStore"; - +import * as recent from "../../../../src/emojipicker/recent"; jest.mock("../../../../src/customisations/Media", () => ({ mediaFromMxc: jest.fn(), })); @@ -116,7 +116,7 @@ describe("EmojiPicker", function () { mocked(mediaFromMxc).mockReturnValue({ srcHttp: "http://this.is.a.url/server/custom-emote-123.png", } as Media); - + jest.spyOn(recent, "get").mockReturnValue([":testEmote:", "😀"]); const ref = createRef(); // @ts-ignore - mocked doesn't support overloads properly diff --git a/test/components/views/messages/TextualBody-test.tsx b/test/components/views/messages/TextualBody-test.tsx index a62bc4588ae..bc38388ce1e 100644 --- a/test/components/views/messages/TextualBody-test.tsx +++ b/test/components/views/messages/TextualBody-test.tsx @@ -19,8 +19,9 @@ import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { mocked, MockedObject } from "jest-mock"; import { render } from "@testing-library/react"; import * as prettier from "prettier"; +import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; -import { getMockClientWithEventEmitter, mkEvent, mkMessage, mkStubRoom } from "../../../test-utils"; +import { getMockClientWithEventEmitter, mkEvent, mkMessage, mkStubRoom, stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import * as languageHandler from "../../../../src/languageHandler"; import DMRoomMap from "../../../../src/utils/DMRoomMap"; @@ -28,10 +29,12 @@ import TextualBody from "../../../../src/components/views/messages/TextualBody"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; +import SettingsStore from "../../../../src/settings/SettingsStore"; const room1Id = "!room1:example.com"; const room2Id = "!room2:example.com"; const room2Name = "Room 2"; +const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); interface MkRoomTextMessageOpts { roomId?: string; @@ -399,6 +402,35 @@ describe("", () => { '' + "escaped *markdown*" + "", ); }); + it("renders custom emote", () => { + const cli = stubClient(); + const room = mkStubRoom("!roomId:server", "Room", cli); + jest.spyOn(SettingsStore, "getValue").mockReturnValue(true); + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: room.roomId, + type: EMOTES_STATE.name, + state_key: "", + content: { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + }); + } + return null; + }); + const ev = mkRoomTextMessage("this is a plaintext message with a :testEmote:"); + const { container } = getComponent({ mxEvent: ev }); + const emote = container.querySelector("img.mx_Emote"); + if (!emote) { + throw new Error("custom emote not rendering"); + } + }); }); it("renders url previews correctly", () => { From 83a522ef23cb78e3ebafb4df76f0bd4b2010d287 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Sat, 1 Jul 2023 15:09:05 -0400 Subject: [PATCH 162/176] compatibility fix --- src/components/views/room_settings/RoomEmoteSettings.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 1d6b21ee3b8..4e431a500d0 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -342,6 +342,13 @@ export default class RoomEmoteSettings extends React.Component { decryptedemotes.set(shortcode, mediaFromMxc(val as string).srcHttp); } } + if (this.state.compatibility) { + for (const shortcode in this.imagePack["images"]) { + if (!decryptedemotes.has(shortcode)) { + decryptedemotes.set(shortcode, mediaFromMxc(this.imagePack["images"][shortcode] as string).srcHttp); + } + } + } this.setState({ decryptedemotes: decryptedemotes, }); From 77f06b529be0f65b65c87eb70f221ca4e85db62b Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Sat, 1 Jul 2023 16:42:23 -0400 Subject: [PATCH 163/176] compatibility fix 2 --- .../views/room_settings/RoomEmoteSettings.tsx | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 4e431a500d0..1c85d7f58a7 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -343,10 +343,33 @@ export default class RoomEmoteSettings extends React.Component { } } if (this.state.compatibility) { + const client = MatrixClientPeg.safeGet(); + let newCompatUploaded = false; for (const shortcode in this.imagePack["images"]) { if (!decryptedemotes.has(shortcode)) { - decryptedemotes.set(shortcode, mediaFromMxc(this.imagePack["images"][shortcode] as string).srcHttp); + newCompatUploaded = true; + this.state.value.set(shortcode, shortcode); + decryptedemotes.set( + shortcode, + mediaFromMxc(this.imagePack["images"][shortcode]["url"] as string).srcHttp, + ); + if (isEnc) { + const blob = await mediaFromMxc(this.imagePack["images"][shortcode]["url"]) + .downloadSource() + .then((r) => r.blob()); + const uploadedEmoteFile = await uploadFile(client, this.props.roomId, blob); + this.state.emotes.set(shortcode, uploadedEmoteFile.file); + } else { + this.state.emotes.set(shortcode, this.imagePack["images"][shortcode]["url"]); + } + } + } + if (newCompatUploaded) { + const emotesMxcs: { [key: string]: IEncryptedFile | string } = {}; + for (const [shortcode, val] of this.state.emotes) { + emotesMxcs[shortcode] = val; } + client.sendStateEvent(this.props.roomId, EMOTES_STATE.name, emotesMxcs, ""); } } this.setState({ From 0a6d184fd7885221cb4d30d8118e99c2ca301a50 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Sat, 1 Jul 2023 20:24:39 -0400 Subject: [PATCH 164/176] Add test for loading emotes uploaded from other clients when compat mode is on --- .../views/room_settings/RoomEmoteSettings.tsx | 4 +- .../tabs/room/EmoteSettingsTab-test.tsx | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 1c85d7f58a7..946422147c9 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -358,9 +358,9 @@ export default class RoomEmoteSettings extends React.Component { .downloadSource() .then((r) => r.blob()); const uploadedEmoteFile = await uploadFile(client, this.props.roomId, blob); - this.state.emotes.set(shortcode, uploadedEmoteFile.file); + this.state.emotes.set(shortcode, uploadedEmoteFile.file!); } else { - this.state.emotes.set(shortcode, this.imagePack["images"][shortcode]["url"]); + this.state.emotes.set(shortcode, this.imagePack["images"][shortcode]["url"]!); } } } diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index 186eb3c4541..845eb229ed1 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -34,6 +34,10 @@ jest.mock("../../../../../../src/ContentMessages", () => ({ describe("EmoteSettingsTab", () => { const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); const EMOTES_COMP = new UnstableValue("m.room.room_emotes", "im.ponies.room_emotes"); + const COMPAT_STATE = new UnstableValue( + "m.room.clientemote_compatibility", + "org.matrix.msc3892.clientemote_compatibility", + ); const roomId = "!room:example.com"; let cli: MatrixClient; let room: Room; @@ -354,4 +358,60 @@ describe("EmoteSettingsTab", () => { "", ); }); + + it("should load emotes uploaded from other clients in compatibility mode", async () => { + mocked(cli.getRoom).mockReturnValue(room); + mocked(cli.isRoomEncrypted).mockReturnValue(false); + // @ts-ignore - mocked doesn't support overloads properly + mocked(room.currentState.getStateEvents).mockImplementation((type, key) => { + if (key === undefined) return [] as MatrixEvent[]; + if (type === EMOTES_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_STATE.name, + state_key: "", + content: {}, + }); + } + if (type === EMOTES_COMP.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_COMP.name, + state_key: "", + content: { + images: { + testEmote: { + url: "http://this.is.a.url/server/custom-emote-123.png", + }, + }, + }, + }); + } + if (type === COMPAT_STATE.name) { + return new MatrixEvent({ + sender: "@sender:server", + room_id: roomId, + type: EMOTES_COMP.name, + state_key: "", + content: { + isCompat: true, + }, + }); + } + return null; + }); + + renderTab(); + + expect(cli.sendStateEvent).toHaveBeenCalledWith( + roomId, + EMOTES_STATE.name, + { + testEmote: "http://this.is.a.url/server/custom-emote-123.png", + }, + "", + ); + }); }); From aa1ab71c9466935a8bbc874974470ea4cbe7ed64 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 5 Jul 2023 22:37:01 -0400 Subject: [PATCH 165/176] Use readable state key instead of blank --- .../views/room_settings/RoomEmoteSettings.tsx | 16 +++++++++++++--- .../views/rooms/SendMessageComposer.tsx | 2 +- .../views/rooms/SendMessageComposer-test.tsx | 2 +- .../settings/tabs/room/EmoteSettingsTab-test.tsx | 12 ++++++------ 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 946422147c9..daea6bf8938 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -85,7 +85,7 @@ export default class RoomEmoteSettings extends React.Component { const compatEvent = room.currentState.getStateEvents(COMPAT_STATE.name, ""); const compat = compatEvent ? compatEvent.getContent().isCompat || false : false; - const imagePackEvent = room.currentState.getStateEvents(EMOTES_COMP.name, ""); + const imagePackEvent = room.currentState.getStateEvents(EMOTES_COMP.name, "Element Compatible Emotes"); this.imagePack = imagePackEvent ? imagePackEvent.getContent() || { images: {} } : { images: {} }; if (!this.imagePack["images"]) { this.imagePack["images"] = {}; @@ -231,7 +231,12 @@ export default class RoomEmoteSettings extends React.Component { this.imagePack["images"][key] = val; } - await client.sendStateEvent(this.props.roomId, EMOTES_COMP.name, this.imagePack, ""); + await client.sendStateEvent( + this.props.roomId, + EMOTES_COMP.name, + this.imagePack, + "Element Compatible Emotes", + ); newState.newEmoteFileAdded = false; newState.newEmoteCodeAdded = false; @@ -325,7 +330,12 @@ export default class RoomEmoteSettings extends React.Component { } } - await client.sendStateEvent(this.props.roomId, EMOTES_COMP.name, this.imagePack, ""); + await client.sendStateEvent( + this.props.roomId, + EMOTES_COMP.name, + this.imagePack, + "Element Compatible Emotes", + ); } this.setState({ diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 935662cf9d9..b76691d361d 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -321,7 +321,7 @@ export class SendMessageComposer extends React.Component(); if (!this.imagePack["images"]) { diff --git a/test/components/views/rooms/SendMessageComposer-test.tsx b/test/components/views/rooms/SendMessageComposer-test.tsx index bf43f172989..23752b1c038 100644 --- a/test/components/views/rooms/SendMessageComposer-test.tsx +++ b/test/components/views/rooms/SendMessageComposer-test.tsx @@ -625,7 +625,7 @@ describe("", () => { sender: "@sender:server", room_id: room.roomId, type: EMOTES_COMP.name, - state_key: "", + state_key: "Element Compatible Emotes", content: { images: { testEmote: { diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index 845eb229ed1..2426518ecf7 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -270,7 +270,7 @@ describe("EmoteSettingsTab", () => { }, }, }, - "", + "Element Compatible Emotes", ); }); @@ -296,7 +296,7 @@ describe("EmoteSettingsTab", () => { sender: "@sender:server", room_id: roomId, type: EMOTES_STATE.name, - state_key: "", + state_key: "Element Compatible Emotes", content: { images: { testEmote: { @@ -326,7 +326,7 @@ describe("EmoteSettingsTab", () => { }, }, }, - "", + "Element Compatible Emotes", ); }); @@ -355,7 +355,7 @@ describe("EmoteSettingsTab", () => { roomId, EMOTES_COMP.name, { images: { coolnewemotecustomname: { url: "http://this.is.a.url/server/custom-emote-123.png" } } }, - "", + "Element Compatible Emotes", ); }); @@ -379,7 +379,7 @@ describe("EmoteSettingsTab", () => { sender: "@sender:server", room_id: roomId, type: EMOTES_COMP.name, - state_key: "", + state_key: "Element Compatible Emotes", content: { images: { testEmote: { @@ -393,7 +393,7 @@ describe("EmoteSettingsTab", () => { return new MatrixEvent({ sender: "@sender:server", room_id: roomId, - type: EMOTES_COMP.name, + type: COMPAT_STATE.name, state_key: "", content: { isCompat: true, From 4c86ce1d6cd2fed9b2984da49dba07524e2c801d Mon Sep 17 00:00:00 2001 From: JiffyCat <135999094+JiffyCat@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:23:21 +0000 Subject: [PATCH 166/176] fix: add copyright to Emotes css --- res/css/views/rooms/_Emotes.pcss | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/res/css/views/rooms/_Emotes.pcss b/res/css/views/rooms/_Emotes.pcss index 25b9b810c62..248e8dd7828 100644 --- a/res/css/views/rooms/_Emotes.pcss +++ b/res/css/views/rooms/_Emotes.pcss @@ -1,3 +1,19 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + .mx_Emote { height: 32px; } From b11bb36bfdf128528dd81a2844c51fac11968013 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 10 Jul 2023 11:08:10 -0400 Subject: [PATCH 167/176] Add a pack name for the element compatible pack --- .../views/room_settings/RoomEmoteSettings.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index daea6bf8938..39e6f5168ef 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -59,6 +59,9 @@ interface compatibilityImagePack { url?: string; }; }; + pack?: { + display_name: string; + }; } export default class RoomEmoteSettings extends React.Component { @@ -86,11 +89,17 @@ export default class RoomEmoteSettings extends React.Component { const compat = compatEvent ? compatEvent.getContent().isCompat || false : false; const imagePackEvent = room.currentState.getStateEvents(EMOTES_COMP.name, "Element Compatible Emotes"); - this.imagePack = imagePackEvent ? imagePackEvent.getContent() || { images: {} } : { images: {} }; + this.imagePack = imagePackEvent + ? imagePackEvent.getContent() || { images: {}, pack: { display_name: "Element Compatible Emotes" } } + : { images: {}, pack: { display_name: "Element Compatible Emotes" } }; if (!this.imagePack["images"]) { this.imagePack["images"] = {}; } + if (!this.imagePack["pack"]) { + this.imagePack["pack"] = { display_name: "Element Compatible Emotes" }; + } + this.state = { emotes: emotesMap, decryptedemotes: new Map(), @@ -226,7 +235,7 @@ export default class RoomEmoteSettings extends React.Component { } newState.value = value; await client.sendStateEvent(this.props.roomId, EMOTES_STATE.name, emotesMxcs, ""); - this.imagePack = { images: {} }; + this.imagePack = { images: {}, pack: this.imagePack["pack"] }; for (const [key, val] of newPack) { this.imagePack["images"][key] = val; } From 3ad3e36ceb21cd770a2157c8e4b71ca94ac988f0 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 19 Jul 2023 12:09:34 -0400 Subject: [PATCH 168/176] type and jest test fix --- src/autocomplete/EmojiProvider.tsx | 6 +++--- .../views/emojipicker/EmojiPicker.tsx | 6 +++--- .../views/room_settings/RoomEmoteSettings.tsx | 14 +++++++------- .../tabs/room/EmoteSettingsTab-test.tsx | 18 ++++++++++++++++-- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 1e5eabe8a7b..58f709cb9e5 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -37,7 +37,7 @@ import * as recent from "../emojipicker/recent"; import { filterBoolean } from "../utils/arrays"; import { decryptFile } from "../utils/DecryptFile"; import { mediaFromMxc } from "../customisations/Media"; -import { IEncryptedFile } from "../customisations/models/IMediaEventContent"; +import { EncryptedFile } from "../customisations/models/IMediaEventContent"; const LIMIT = 20; @@ -116,7 +116,7 @@ export default class EmojiProvider extends AutocompleteProvider { } private async decryptEmotes( - emotes: Map, + emotes: Map, roomId: string, ): Promise> { const decryptedEmoteMap = new Map(); @@ -124,7 +124,7 @@ export default class EmojiProvider extends AutocompleteProvider { const isEnc = MatrixClientPeg.get()?.isRoomEncrypted(roomId); for (const [shortcode, val] of emotes) { if (isEnc) { - const blob = await decryptFile(val as IEncryptedFile); + const blob = await decryptFile(val as EncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { decryptedurl = mediaFromMxc(val as string).srcHttp!; diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 373c733ab34..831043a5906 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -42,7 +42,7 @@ import { Ref } from "../../../accessibility/roving/types"; import { mediaFromMxc } from "../../../customisations/Media"; import { decryptFile } from "../../../utils/DecryptFile"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; +import { EncryptedFile } from "../../../customisations/models/IMediaEventContent"; import SettingsStore from "../../../settings/SettingsStore"; export const CATEGORY_HEADER_HEIGHT = 20; @@ -242,7 +242,7 @@ class EmojiPicker extends React.Component { } private async decryptEmotes( - emotes: Map, + emotes: Map, roomId: string, ): Promise> { const decryptedemotes = new Map(); @@ -250,7 +250,7 @@ class EmojiPicker extends React.Component { const isEnc = MatrixClientPeg.get()?.isRoomEncrypted(roomId); for (const [shortcode, val] of emotes) { if (isEnc) { - const blob = await decryptFile(val as IEncryptedFile); + const blob = await decryptFile(val as EncryptedFile); decryptedurl = URL.createObjectURL(blob); } else { decryptedurl = mediaFromMxc(val as string).srcHttp!; diff --git a/src/components/views/room_settings/RoomEmoteSettings.tsx b/src/components/views/room_settings/RoomEmoteSettings.tsx index 39e6f5168ef..60c6ca1ada4 100644 --- a/src/components/views/room_settings/RoomEmoteSettings.tsx +++ b/src/components/views/room_settings/RoomEmoteSettings.tsx @@ -26,7 +26,7 @@ import { decryptFile } from "../../../utils/DecryptFile"; import { mediaFromMxc } from "../../../customisations/Media"; import SettingsFieldset from "../settings/SettingsFieldset"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; -import { IEncryptedFile } from "../../../customisations/models/IMediaEventContent"; +import { EncryptedFile } from "../../../customisations/models/IMediaEventContent"; const EMOTES_STATE = new UnstableValue("m.room.emotes", "org.matrix.msc3892.emotes"); const COMPAT_STATE = new UnstableValue( "m.room.clientemote_compatibility", @@ -39,7 +39,7 @@ interface IProps { } interface IState { - emotes: Map; + emotes: Map; decryptedemotes: Map; EmoteFieldsTouched: Record; newEmoteFileAdded: boolean; @@ -48,7 +48,7 @@ interface IState { newEmoteFile: Array; canAddEmote: boolean; deleted: boolean; - deletedItems: Map; + deletedItems: Map; value: Map; compatibility: boolean; } @@ -183,7 +183,7 @@ export default class RoomEmoteSettings extends React.Component { if (!this.isSaveEnabled()) return; const client = MatrixClientPeg.safeGet(); const newState: Partial = {}; - const emotesMxcs: { [key: string]: IEncryptedFile | string } = {}; + const emotesMxcs: { [key: string]: EncryptedFile | string } = {}; const value = new Map(); const newPack: Map> = new Map(); @@ -330,7 +330,7 @@ export default class RoomEmoteSettings extends React.Component { for (const [shortcode, val] of this.state.emotes) { if (!this.imagePack["images"][shortcode]) { if (client.isRoomEncrypted(this.props.roomId)) { - const blob = await decryptFile(val as IEncryptedFile); + const blob = await decryptFile(val as EncryptedFile); const uploadedEmote = await client.uploadContent(blob); this.imagePack["images"][shortcode] = { url: uploadedEmote.content_uri }; } else { @@ -355,7 +355,7 @@ export default class RoomEmoteSettings extends React.Component { const decryptedemotes = new Map(); for (const [shortcode, val] of this.state.emotes) { if (isEnc) { - const blob = await decryptFile(val as IEncryptedFile); + const blob = await decryptFile(val as EncryptedFile); decryptedemotes.set(shortcode, URL.createObjectURL(blob)); } else { decryptedemotes.set(shortcode, mediaFromMxc(val as string).srcHttp); @@ -384,7 +384,7 @@ export default class RoomEmoteSettings extends React.Component { } } if (newCompatUploaded) { - const emotesMxcs: { [key: string]: IEncryptedFile | string } = {}; + const emotesMxcs: { [key: string]: EncryptedFile | string } = {}; for (const [shortcode, val] of this.state.emotes) { emotesMxcs[shortcode] = val; } diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index 2426518ecf7..90f4cfa933a 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -269,6 +269,9 @@ describe("EmoteSettingsTab", () => { url: "http://this.is.a.url/server/custom-emote-123.png", }, }, + pack: { + display_name: "Element Compatible Emotes", + }, }, "Element Compatible Emotes", ); @@ -295,7 +298,7 @@ describe("EmoteSettingsTab", () => { return new MatrixEvent({ sender: "@sender:server", room_id: roomId, - type: EMOTES_STATE.name, + type: EMOTES_COMP.name, state_key: "Element Compatible Emotes", content: { images: { @@ -303,6 +306,9 @@ describe("EmoteSettingsTab", () => { url: "http://this.is.a.url/server/custom-emote-123.png", }, }, + pack: { + display_name: "Element Compatible Emotes", + }, }, }); } @@ -325,6 +331,9 @@ describe("EmoteSettingsTab", () => { url: "http://this.is.a.url/server/custom-emote-123.png", }, }, + pack: { + display_name: "Element Compatible Emotes", + }, }, "Element Compatible Emotes", ); @@ -354,7 +363,12 @@ describe("EmoteSettingsTab", () => { expect(cli.sendStateEvent).toHaveBeenLastCalledWith( roomId, EMOTES_COMP.name, - { images: { coolnewemotecustomname: { url: "http://this.is.a.url/server/custom-emote-123.png" } } }, + { + images: { coolnewemotecustomname: { url: "http://this.is.a.url/server/custom-emote-123.png" } }, + pack: { + display_name: "Element Compatible Emotes", + }, + }, "Element Compatible Emotes", ); }); From 183ec449dd29a2ed31d793c79fe98f02cdc5a0cb Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Fri, 4 Aug 2023 09:21:08 -0400 Subject: [PATCH 169/176] lint fixes --- src/autocomplete/EmojiProvider.tsx | 1 - src/components/views/emojipicker/EmojiPicker.tsx | 2 +- src/components/views/rooms/EmojiButton.tsx | 2 +- .../views/settings/tabs/room/EmoteSettingsTab-test.tsx | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 881a29c0f9b..eb50aa7e395 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -24,7 +24,6 @@ import EMOTICON_REGEX from "emojibase-regex/emoticon"; import { Room } from "matrix-js-sdk/src/matrix"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; - import { MatrixClientPeg } from "../MatrixClientPeg"; import { _t } from "../languageHandler"; import AutocompleteProvider from "./AutocompleteProvider"; diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 831043a5906..96ebffd0443 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React, { Dispatch } from "react"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/matrix"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; import { _t } from "../../../languageHandler"; diff --git a/src/components/views/rooms/EmojiButton.tsx b/src/components/views/rooms/EmojiButton.tsx index 63607898896..a5d3e58058a 100644 --- a/src/components/views/rooms/EmojiButton.tsx +++ b/src/components/views/rooms/EmojiButton.tsx @@ -16,7 +16,7 @@ limitations under the License. import classNames from "classnames"; import React, { useContext } from "react"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import ContextMenu, { aboveLeftOf, MenuProps, useContextMenu } from "../../structures/ContextMenu"; diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index 90f4cfa933a..6d131d821b2 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -18,7 +18,7 @@ import React from "react"; import { fireEvent, render, RenderResult, screen, waitFor } from "@testing-library/react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { Room } from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; From 5e01cfc9cd9eb57ce3fb5c9b41a55505be66df50 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 7 Aug 2023 08:46:59 -0400 Subject: [PATCH 170/176] new js sdk import lint fix --- .../views/settings/tabs/room/EmoteSettingsTab-test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index 6d131d821b2..5e37345fd54 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -17,8 +17,7 @@ limitations under the License. import React from "react"; import { fireEvent, render, RenderResult, screen, waitFor } from "@testing-library/react"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { Room } from "matrix-js-sdk/src/matrix"; +import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; From ff2ed60d42d4ef3512362882ba969c1bd593d90a Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:48:14 -0400 Subject: [PATCH 171/176] lint fix --- src/components/views/messages/TextualBody.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index f85b10bcfb5..085a726f7a3 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { createRef, SyntheticEvent, MouseEvent } from "react"; import ReactDOM from "react-dom"; import highlight from "highlight.js"; - import { MsgType } from "matrix-js-sdk/src/matrix"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; From 63b23424da81a0b1b8a2a7bc9be6c98957e98563 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Wed, 9 Aug 2023 10:31:55 -0400 Subject: [PATCH 172/176] another import lint fix --- .../views/settings/tabs/room/EmoteSettingsTab-test.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx index 5e37345fd54..5c908051174 100644 --- a/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/room/EmoteSettingsTab-test.tsx @@ -16,8 +16,7 @@ limitations under the License. import React from "react"; import { fireEvent, render, RenderResult, screen, waitFor } from "@testing-library/react"; -import { MatrixClient } from "matrix-js-sdk/src/client"; -import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; From fc731cfe0603dd72e0ba3110a743cd912f9814e1 Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:41:31 -0400 Subject: [PATCH 173/176] lint fix --- src/components/views/rooms/SendMessageComposer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0faefd6bf29..ddba0cfeedc 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -31,6 +31,7 @@ import { DebouncedFunc, throttle } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; import { Composer as ComposerEvent } from "@matrix-org/analytics-events/types/typescript/Composer"; import { UnstableValue } from "matrix-js-sdk/src/NamespacedValue"; + import dis from "../../../dispatcher/dispatcher"; import EditorModel from "../../../editor/model"; import { From 9494f77250cbb074af06b48e57fe917435c95ced Mon Sep 17 00:00:00 2001 From: nmscode <105442557+nmscode@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:32:54 -0400 Subject: [PATCH 174/176] revert --- .eslintrc.js | 187 +-- .github/workflows/cypress.yaml | 2 +- .github/workflows/i18n_check.yml | 6 +- __mocks__/languages.json | 10 +- cypress/e2e/audio-player/audio-player.spec.ts | 10 +- cypress/e2e/editing/editing.spec.ts | 4 +- cypress/e2e/login/login.spec.ts | 30 +- cypress/e2e/login/soft_logout.spec.ts | 143 ++ cypress/e2e/login/utils.ts | 49 + cypress/e2e/read-receipts/high-level.spec.ts | 292 +++- .../e2e/read-receipts/read-receipts.spec.ts | 3 +- cypress/e2e/register/email.spec.ts | 93 ++ .../general-user-settings-tab.spec.ts | 8 +- cypress/e2e/spotlight/spotlight.spec.ts | 12 +- cypress/e2e/threads/threads.spec.ts | 8 +- cypress/e2e/timeline/timeline.spec.ts | 22 +- cypress/e2e/widgets/stickers.spec.ts | 8 +- cypress/global.d.ts | 1 + cypress/plugins/docker/index.ts | 8 + cypress/plugins/index.ts | 2 + cypress/plugins/mailhog/index.ts | 91 ++ cypress/plugins/synapsedocker/index.ts | 69 +- .../templates/default/homeserver.yaml | 7 +- .../synapsedocker/templates/email/README.md | 1 + .../templates/email/homeserver.yaml | 44 + .../synapsedocker/templates/email/log.config | 50 + cypress/support/client.ts | 2 +- cypress/support/e2e.ts | 2 + cypress/support/homeserver.ts | 12 +- cypress/support/mailhog.ts | 54 + cypress/support/promise.ts | 58 + cypress/support/views.ts | 26 +- docs/settings.md | 2 +- jest.config.ts | 7 +- package.json | 37 +- res/css/_components.pcss | 8 +- res/css/components/views/utils/_Box.pcss | 27 + res/css/components/views/utils/_Flex.pcss | 23 + res/css/structures/_RightPanel.pcss | 4 - res/css/structures/_RoomStatusBar.pcss | 10 - res/css/structures/_SpaceHierarchy.pcss | 6 - res/css/structures/_SpacePanel.pcss | 18 +- res/css/structures/_SpaceRoomView.pcss | 4 - .../auth/_ConfirmSessionLockTheftView.pcss} | 20 +- .../auth/_SessionLockStolenView.pcss | 30 + res/css/views/avatars/_BaseAvatar.pcss | 50 +- .../views/avatars/_DecoratedRoomAvatar.pcss | 8 +- .../dialogs/_AddExistingToSpaceDialog.pcss | 14 +- res/css/views/dialogs/_CompoundDialog.pcss | 3 +- .../_ManageRestrictedJoinRuleDialog.pcss | 9 - .../views/dialogs/_RoomSettingsDialog.pcss | 4 + res/css/views/dialogs/_SpotlightDialog.pcss | 13 +- res/css/views/elements/_AccessibleButton.pcss | 12 +- res/css/views/elements/_FacePile.pcss | 6 +- .../elements/_GenericEventListSummary.pcss | 5 + res/css/views/elements/_LanguageDropdown.pcss | 21 + .../views/elements/_MiniAvatarUploader.pcss | 2 + res/css/views/elements/_TooltipButton.pcss | 51 - res/css/views/messages/_DateSeparator.pcss | 1 + .../views/right_panel/_RoomSummaryCard.pcss | 5 - res/css/views/right_panel/_UserInfo.pcss | 53 +- res/css/views/rooms/_EntityTile.pcss | 1 + res/css/views/rooms/_EventTile.pcss | 1 + res/css/views/rooms/_LegacyRoomHeader.pcss | 4 - res/css/views/rooms/_MemberInfo.pcss | 2 +- res/css/views/rooms/_RoomHeader.pcss | 11 +- res/css/views/rooms/_RoomPreviewCard.pcss | 7 - res/css/views/rooms/_WhoIsTypingTile.pcss | 4 - res/css/views/settings/_JoinRuleSettings.pcss | 5 - .../tabs/room/_NotificationSettingsTab.pcss | 4 + .../tabs/room/_PeopleRoomSettingsTab.pcss | 56 + res/img/element-icons/spaces.svg | 3 + res/img/feather-customised/check.svg | 2 +- res/img/feather-customised/x.svg | 2 +- .../legacy-light/css/_legacy-light.pcss | 16 +- .../css/_light-high-contrast.pcss | 16 +- res/themes/light/css/_light.pcss | 16 +- scripts/check-i18n.pl | 192 --- scripts/fix-i18n.pl | 114 -- scripts/fixup-imports.pl | 27 - sonar-project.properties | 4 +- src/@types/common.ts | 43 +- src/@types/global.d.ts | 1 + src/AddThreepid.ts | 6 +- src/AsyncWrapper.tsx | 4 +- src/ContentMessages.ts | 2 +- src/DateUtils.ts | 266 +-- src/HtmlUtils.tsx | 2 +- src/IdentityAuthClient.tsx | 6 +- src/LegacyCallHandler.tsx | 14 +- src/Lifecycle.ts | 60 +- src/Login.ts | 10 +- src/Markdown.ts | 23 +- src/MatrixClientPeg.ts | 4 +- src/Notifier.ts | 5 +- src/PosthogTrackers.ts | 2 + src/Registration.tsx | 4 +- src/RoomInvite.tsx | 3 +- src/SecurityManager.ts | 4 +- src/SlashCommands.tsx | 22 +- src/TextForEvent.tsx | 34 +- src/Unread.ts | 3 +- src/Views.ts | 6 + src/accessibility/KeyboardShortcutUtils.ts | 6 +- src/accessibility/KeyboardShortcuts.ts | 36 +- .../eventindex/DisableEventIndexDialog.tsx | 2 +- .../eventindex/ManageEventIndexDialog.tsx | 4 +- .../security/CreateKeyBackupDialog.tsx | 4 +- .../security/CreateSecretStorageDialog.tsx | 50 +- .../dialogs/security/ExportE2eKeysDialog.tsx | 15 +- .../dialogs/security/ImportE2eKeysDialog.tsx | 10 +- .../security/NewRecoveryMethodDialog.tsx | 7 +- .../security/RecoveryMethodRemovedDialog.tsx | 12 +- src/autocomplete/EmojiProvider.tsx | 6 +- src/autocomplete/NotifProvider.tsx | 2 +- src/autocomplete/RoomProvider.tsx | 2 +- src/autocomplete/UserProvider.tsx | 2 +- src/boundThreepids.ts | 3 +- src/components/structures/EmbeddedPage.tsx | 4 +- src/components/structures/HomePage.tsx | 8 +- src/components/structures/InteractiveAuth.tsx | 20 +- src/components/structures/MatrixChat.tsx | 109 +- src/components/structures/MessagePanel.tsx | 11 +- src/components/structures/RoomSearch.tsx | 9 +- src/components/structures/RoomSearchView.tsx | 4 +- src/components/structures/RoomStatusBar.tsx | 12 +- src/components/structures/RoomView.tsx | 2 +- src/components/structures/SpaceHierarchy.tsx | 32 +- src/components/structures/SpaceRoomView.tsx | 19 +- src/components/structures/TabbedView.tsx | 4 +- src/components/structures/ThreadPanel.tsx | 5 +- src/components/structures/ThreadView.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 5 +- src/components/structures/UploadBar.tsx | 17 +- src/components/structures/UserMenu.tsx | 12 +- src/components/structures/ViewSource.tsx | 4 +- .../WaitingForThirdPartyRoomView.tsx | 3 +- .../auth/ConfirmSessionLockTheftView.tsx | 51 + .../structures/auth/ForgotPassword.tsx | 12 +- src/components/structures/auth/Login.tsx | 8 +- .../structures/auth/Registration.tsx | 42 +- .../structures/auth/SessionLockStolenView.tsx | 35 + .../structures/auth/SetupEncryptionBody.tsx | 25 +- src/components/structures/auth/SoftLogout.tsx | 55 +- .../auth/forgot-password/CheckEmail.tsx | 4 +- .../auth/forgot-password/VerifyEmailModal.tsx | 5 +- src/components/utils/Box.tsx | 103 ++ src/components/utils/Flex.tsx | 86 + src/components/views/auth/CountryDropdown.tsx | 54 +- src/components/views/auth/EmailField.tsx | 14 +- .../auth/InteractiveAuthEntryComponents.tsx | 17 +- src/components/views/auth/LoginWithQRFlow.tsx | 8 +- .../views/auth/PassphraseConfirmField.tsx | 8 +- src/components/views/auth/PassphraseField.tsx | 12 +- src/components/views/auth/PasswordLogin.tsx | 10 +- .../views/auth/RegistrationForm.tsx | 4 +- src/components/views/avatars/BaseAvatar.tsx | 150 +- .../views/avatars/DecoratedRoomAvatar.tsx | 7 +- src/components/views/avatars/MemberAvatar.tsx | 15 +- src/components/views/avatars/RoomAvatar.tsx | 17 +- .../views/avatars/SearchResultAvatar.tsx | 7 +- src/components/views/avatars/WidgetAvatar.tsx | 10 +- .../views/beacon/BeaconListItem.tsx | 5 +- src/components/views/beacon/BeaconMarker.tsx | 3 +- .../views/beacon/BeaconStatusTooltip.tsx | 3 +- .../views/beacon/BeaconViewDialog.tsx | 2 +- .../views/beacon/DialogOwnBeaconStatus.tsx | 10 +- .../views/beacon/OwnBeaconStatus.tsx | 6 +- .../views/beacon/RoomCallBanner.tsx | 2 +- .../views/beacon/RoomLiveShareWarning.tsx | 2 +- .../views/beacon/ShareLatestLocation.tsx | 4 +- src/components/views/beacon/displayStatus.ts | 4 +- src/components/views/beta/BetaCard.tsx | 4 +- .../views/context_menus/DeviceContextMenu.tsx | 4 +- .../context_menus/MessageContextMenu.tsx | 22 +- .../views/context_menus/RoomContextMenu.tsx | 10 +- .../context_menus/RoomGeneralContextMenu.tsx | 6 +- .../views/context_menus/SpaceContextMenu.tsx | 8 +- .../views/context_menus/WidgetContextMenu.tsx | 7 +- .../dialogs/AddExistingToSpaceDialog.tsx | 22 +- .../dialogs/AnalyticsLearnMoreDialog.tsx | 4 +- .../views/dialogs/AskInviteAnywayDialog.tsx | 2 +- .../views/dialogs/BugReportDialog.tsx | 10 +- .../views/dialogs/BulkRedactDialog.tsx | 10 +- .../CantStartVoiceMessageBroadcastDialog.tsx | 3 +- .../views/dialogs/ChangelogDialog.tsx | 2 +- .../dialogs/ConfirmAndWaitRedactDialog.tsx | 2 +- .../views/dialogs/ConfirmRedactDialog.tsx | 4 +- .../views/dialogs/ConfirmUserActionDialog.tsx | 2 +- .../views/dialogs/ConfirmWipeDeviceDialog.tsx | 5 +- .../views/dialogs/CreateRoomDialog.tsx | 13 +- .../views/dialogs/CreateSubspaceDialog.tsx | 4 +- .../views/dialogs/CryptoStoreTooNewDialog.tsx | 14 +- .../views/dialogs/DeactivateAccountDialog.tsx | 4 +- .../views/dialogs/DevtoolsDialog.tsx | 10 +- .../views/dialogs/EndPollDialog.tsx | 4 +- src/components/views/dialogs/ErrorDialog.tsx | 4 +- src/components/views/dialogs/ExportDialog.tsx | 8 +- .../views/dialogs/FeedbackDialog.tsx | 10 +- .../views/dialogs/ForwardDialog.tsx | 33 +- .../dialogs/GenericFeatureFeedbackDialog.tsx | 2 +- .../views/dialogs/IncomingSasDialog.tsx | 23 +- src/components/views/dialogs/InfoDialog.tsx | 2 +- .../dialogs/IntegrationsDisabledDialog.tsx | 4 +- .../dialogs/IntegrationsImpossibleDialog.tsx | 5 +- .../views/dialogs/InteractiveAuthDialog.tsx | 9 +- src/components/views/dialogs/InviteDialog.tsx | 43 +- .../KeySignatureUploadFailedDialog.tsx | 2 +- .../dialogs/LazyLoadingDisabledDialog.tsx | 9 +- .../views/dialogs/LazyLoadingResyncDialog.tsx | 6 +- .../views/dialogs/LeaveSpaceDialog.tsx | 7 +- src/components/views/dialogs/LogoutDialog.tsx | 12 +- .../ManageRestrictedJoinRuleDialog.tsx | 15 +- .../views/dialogs/ModuleUiDialog.tsx | 38 +- .../views/dialogs/QuestionDialog.tsx | 2 +- .../dialogs/RegistrationEmailPromptDialog.tsx | 5 +- .../views/dialogs/ReportEventDialog.tsx | 36 +- .../views/dialogs/RoomSettingsDialog.tsx | 20 +- .../views/dialogs/RoomUpgradeDialog.tsx | 10 +- .../dialogs/RoomUpgradeWarningDialog.tsx | 16 +- .../views/dialogs/ScrollableBaseModal.tsx | 3 +- .../views/dialogs/ServerOfflineDialog.tsx | 7 +- .../views/dialogs/ServerPickerDialog.tsx | 6 +- .../views/dialogs/SeshatResetDialog.tsx | 6 +- .../dialogs/SessionRestoreErrorDialog.tsx | 11 +- .../views/dialogs/SetEmailDialog.tsx | 16 +- .../dialogs/SlidingSyncOptionsDialog.tsx | 2 +- .../views/dialogs/SpacePreferencesDialog.tsx | 7 +- .../views/dialogs/StorageEvictedDialog.tsx | 6 +- src/components/views/dialogs/TermsDialog.tsx | 4 +- .../views/dialogs/TextInputDialog.tsx | 6 +- .../views/dialogs/UntrustedDeviceDialog.tsx | 2 +- .../views/dialogs/UploadConfirmDialog.tsx | 2 +- .../views/dialogs/UploadFailureDialog.tsx | 11 +- .../views/dialogs/UserSettingsDialog.tsx | 6 +- .../dialogs/WidgetOpenIDPermissionsDialog.tsx | 2 +- .../views/dialogs/devtools/BaseTool.tsx | 2 +- .../views/dialogs/devtools/Event.tsx | 6 +- .../dialogs/devtools/RoomNotifications.tsx | 33 +- .../views/dialogs/devtools/RoomState.tsx | 7 +- .../dialogs/devtools/VerificationExplorer.tsx | 6 +- .../views/dialogs/oidc/OidcLogoutDialog.tsx | 74 + .../security/AccessSecretStorageDialog.tsx | 16 +- .../ConfirmDestroyCrossSigningDialog.tsx | 7 +- .../security/CreateCrossSigningDialog.tsx | 7 +- .../security/RestoreKeyBackupDialog.tsx | 31 +- .../views/dialogs/spotlight/Filter.ts | 21 + .../dialogs/spotlight/SpotlightDialog.tsx | 143 +- .../views/directory/NetworkDropdown.tsx | 2 +- .../views/elements/AccessibleButton.tsx | 4 +- .../views/elements/AppPermission.tsx | 6 +- src/components/views/elements/AppTile.tsx | 6 +- .../views/elements/CopyableText.tsx | 2 +- .../elements/DesktopCapturerSourcePicker.tsx | 10 +- .../views/elements/DialogButtons.tsx | 2 +- src/components/views/elements/Dropdown.tsx | 2 +- .../views/elements/EditableItemList.tsx | 13 +- .../views/elements/ErrorBoundary.tsx | 12 +- src/components/views/elements/FacePile.tsx | 14 +- .../elements/GenericEventListSummary.tsx | 4 +- src/components/views/elements/ImageView.tsx | 15 +- .../views/elements/InlineSpinner.tsx | 2 +- .../views/elements/LanguageDropdown.tsx | 31 +- src/components/views/elements/LearnMore.tsx | 4 +- .../views/elements/MiniAvatarUploader.tsx | 2 +- src/components/views/elements/Pill.tsx | 10 +- .../views/elements/PollCreateDialog.tsx | 13 +- src/components/views/elements/ReplyChain.tsx | 3 +- .../views/elements/RoomFacePile.tsx | 8 +- src/components/views/elements/SSOButtons.tsx | 14 +- .../views/elements/ServerPicker.tsx | 16 +- .../views/elements/SettingsFlag.tsx | 9 +- .../elements/SpellCheckLanguagesDropdown.tsx | 13 +- src/components/views/elements/Spinner.tsx | 2 +- src/components/views/elements/TagComposer.tsx | 2 +- .../views/elements/TooltipButton.tsx | 43 - .../views/elements/UseCaseSelection.tsx | 2 +- src/components/views/emojipicker/Category.tsx | 2 +- src/components/views/emojipicker/Emoji.tsx | 9 +- .../views/emojipicker/EmojiPicker.tsx | 13 +- src/components/views/emojipicker/Preview.tsx | 5 +- .../views/emojipicker/QuickReactions.tsx | 2 +- src/components/views/emojipicker/Search.tsx | 2 +- .../views/location/EnableLiveShare.tsx | 7 +- src/components/views/location/MapError.tsx | 2 +- src/components/views/location/Marker.tsx | 3 +- .../views/location/ShareDialogButtons.tsx | 4 +- src/components/views/location/ShareType.tsx | 8 +- src/components/views/location/ZoomButtons.tsx | 4 +- .../views/location/shareLocation.ts | 28 +- src/components/views/messages/CallEvent.tsx | 15 +- .../views/messages/DateSeparator.tsx | 28 +- .../views/messages/DisambiguatedProfile.tsx | 2 +- .../views/messages/DownloadActionButton.tsx | 6 +- .../views/messages/EditHistoryMessage.tsx | 4 +- .../views/messages/EncryptionEvent.tsx | 10 +- .../views/messages/LegacyCallEvent.tsx | 8 +- src/components/views/messages/MBeaconBody.tsx | 6 +- src/components/views/messages/MFileBody.tsx | 10 +- src/components/views/messages/MImageBody.tsx | 8 +- .../messages/MKeyVerificationRequest.tsx | 4 +- src/components/views/messages/MPollBody.tsx | 12 +- .../views/messages/MPollEndBody.tsx | 3 +- .../views/messages/MessageActionBar.tsx | 22 +- .../views/messages/MessageEvent.tsx | 13 +- .../views/messages/ReactionsRow.tsx | 2 +- .../views/messages/RoomAvatarEvent.tsx | 2 +- .../views/messages/RoomPredecessorTile.tsx | 7 +- src/components/views/messages/TextualBody.tsx | 6 +- .../views/messages/TileErrorBoundary.tsx | 2 +- src/components/views/pips/WidgetPip.tsx | 6 +- .../views/polls/pollHistory/fetchPastPolls.ts | 2 +- src/components/views/right_panel/BaseCard.tsx | 4 +- .../views/right_panel/EncryptionInfo.tsx | 6 +- .../right_panel/LegacyRoomHeaderButtons.tsx | 2 +- .../views/right_panel/PinnedMessagesCard.tsx | 3 +- .../views/right_panel/RoomSummaryCard.tsx | 14 +- src/components/views/right_panel/UserInfo.tsx | 50 +- .../views/right_panel/VerificationPanel.tsx | 16 +- .../views/right_panel/WidgetCard.tsx | 2 +- .../views/room_settings/AliasSettings.tsx | 20 +- .../room_settings/RoomProfileSettings.tsx | 4 +- .../room_settings/UrlPreviewSettings.tsx | 7 +- .../views/rooms/BasicMessageComposer.tsx | 2 +- src/components/views/rooms/E2EIcon.tsx | 8 +- .../views/rooms/EditMessageComposer.tsx | 4 +- src/components/views/rooms/EntityTile.tsx | 10 +- src/components/views/rooms/EventTile.tsx | 21 +- .../views/rooms/LegacyRoomHeader.tsx | 6 +- .../views/rooms/LinkPreviewGroup.tsx | 2 +- .../views/rooms/LiveContentSummary.tsx | 2 +- src/components/views/rooms/MemberList.tsx | 9 +- src/components/views/rooms/MemberTile.tsx | 2 +- .../views/rooms/MessageComposerButtons.tsx | 7 +- .../views/rooms/MessageComposerFormatBar.tsx | 2 +- src/components/views/rooms/NewRoomIntro.tsx | 15 +- .../views/rooms/PinnedEventTile.tsx | 7 +- src/components/views/rooms/PresenceLabel.tsx | 2 +- .../views/rooms/ReadReceiptGroup.tsx | 3 +- .../views/rooms/ReadReceiptMarker.tsx | 4 +- src/components/views/rooms/ReplyTile.tsx | 2 +- .../views/rooms/RoomBreadcrumbs.tsx | 2 +- src/components/views/rooms/RoomHeader.tsx | 141 +- src/components/views/rooms/RoomList.tsx | 18 +- src/components/views/rooms/RoomListHeader.tsx | 4 +- src/components/views/rooms/RoomPreviewBar.tsx | 20 +- .../views/rooms/RoomPreviewCard.tsx | 16 +- src/components/views/rooms/RoomSublist.tsx | 2 +- src/components/views/rooms/RoomTile.tsx | 2 +- .../views/rooms/RoomTileCallSummary.tsx | 2 +- .../views/rooms/RoomUpgradeWarningBar.tsx | 10 +- src/components/views/rooms/SearchBar.tsx | 4 +- .../views/rooms/SendMessageComposer.tsx | 5 +- src/components/views/rooms/Stickerpicker.tsx | 4 +- .../views/rooms/ThirdPartyMemberInfo.tsx | 11 +- src/components/views/rooms/ThreadSummary.tsx | 3 +- .../views/rooms/VoiceRecordComposerTile.tsx | 2 +- .../views/rooms/WhoIsTypingTile.tsx | 3 +- .../DynamicImportWysiwygComposer.tsx | 2 +- .../components/EditionButtons.tsx | 4 +- .../components/FormattingButtons.tsx | 2 +- .../wysiwyg_composer/components/LinkModal.tsx | 4 +- .../components/WysiwygAutocomplete.tsx | 13 +- .../wysiwyg_composer/hooks/useEditing.ts | 2 +- .../wysiwyg_composer/hooks/useSuggestion.ts | 18 +- .../views/settings/AddPrivilegedUsers.tsx | 6 +- .../views/settings/AvatarSetting.tsx | 4 +- src/components/views/settings/BridgeTile.tsx | 4 +- .../views/settings/CrossSigningPanel.tsx | 5 +- .../views/settings/EventIndexPanel.tsx | 16 +- .../views/settings/JoinRuleSettings.tsx | 11 +- .../views/settings/LayoutSwitcher.tsx | 4 +- .../views/settings/Notifications.tsx | 13 +- .../views/settings/ProfileSettings.tsx | 4 +- .../views/settings/SecureBackupPanel.tsx | 13 +- src/components/views/settings/SetIdServer.tsx | 36 +- .../views/settings/SetIntegrationManager.tsx | 5 +- .../views/settings/SpellCheckSettings.tsx | 4 +- .../views/settings/ThemeChoicePanel.tsx | 2 +- .../views/settings/account/EmailAddresses.tsx | 36 +- .../views/settings/account/PhoneNumbers.tsx | 48 +- .../settings/devices/CurrentDeviceSection.tsx | 6 +- .../settings/devices/DeviceDetailHeading.tsx | 10 +- .../views/settings/devices/DeviceDetails.tsx | 5 +- .../devices/DeviceSecurityLearnMore.tsx | 9 +- .../settings/devices/FilteredDeviceList.tsx | 86 +- .../devices/FilteredDeviceListHeader.tsx | 24 +- .../settings/devices/LoginWithQRSection.tsx | 3 +- .../devices/OtherSessionsSectionHeading.tsx | 36 +- .../devices/SecurityRecommendations.tsx | 6 +- .../views/settings/devices/deleteDevices.tsx | 2 +- .../views/settings/devices/useOwnDevices.ts | 2 +- .../settings/discovery/EmailAddresses.tsx | 8 +- .../views/settings/discovery/PhoneNumbers.tsx | 8 +- .../NotificationPusherSettings.tsx | 3 +- .../notifications/NotificationSettings2.tsx | 6 +- .../tabs/room/AdvancedRoomSettingsTab.tsx | 4 +- .../settings/tabs/room/BridgeSettingsTab.tsx | 4 +- .../tabs/room/GeneralRoomSettingsTab.tsx | 4 +- .../tabs/room/NotificationSettingsTab.tsx | 7 +- .../tabs/room/PeopleRoomSettingsTab.tsx | 173 ++ .../tabs/room/RolesRoomSettingsTab.tsx | 19 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 27 +- .../tabs/room/VoipRoomSettingsTab.tsx | 7 +- .../tabs/user/GeneralUserSettingsTab.tsx | 49 +- .../tabs/user/HelpUserSettingsTab.tsx | 28 +- .../tabs/user/LabsUserSettingsTab.tsx | 10 +- .../tabs/user/MjolnirUserSettingsTab.tsx | 19 +- .../tabs/user/SecurityUserSettingsTab.tsx | 9 +- .../settings/tabs/user/SessionManagerTab.tsx | 78 +- .../tabs/user/SidebarUserSettingsTab.tsx | 9 +- .../views/spaces/QuickSettingsButton.tsx | 6 +- .../views/spaces/QuickThemeSwitcher.tsx | 2 +- .../views/spaces/SpaceBasicSettings.tsx | 8 +- .../views/spaces/SpaceChildrenPicker.tsx | 2 +- .../views/spaces/SpaceCreateMenu.tsx | 53 +- src/components/views/spaces/SpacePanel.tsx | 19 +- .../views/spaces/SpaceSettingsGeneralTab.tsx | 2 +- .../spaces/SpaceSettingsVisibilityTab.tsx | 4 +- .../views/spaces/SpaceTreeLevel.tsx | 12 +- .../views/terms/InlineTermsAgreement.tsx | 4 +- .../views/toasts/VerificationRequestToast.tsx | 4 +- .../user-onboarding/UserOnboardingHeader.tsx | 12 +- .../verification/VerificationCancelled.tsx | 6 +- .../verification/VerificationComplete.tsx | 3 +- .../verification/VerificationShowSas.tsx | 134 +- src/components/views/voip/CallView.tsx | 6 +- src/components/views/voip/LegacyCallView.tsx | 4 +- .../LegacyCallView/LegacyCallViewHeader.tsx | 8 +- src/components/views/voip/VideoFeed.tsx | 8 +- src/createRoom.ts | 3 +- src/dispatcher/actions.ts | 5 + src/dispatcher/payloads/JoinRoomPayload.ts | 2 +- .../payloads/OpenSpotlightPayload.ts | 26 + .../payloads/SubmitAskToJoinPayload.ts | 2 +- src/editor/commands.tsx | 3 +- src/effects/effect.ts | 6 +- src/emoji.ts | 122 -- src/events/EventTileFactory.tsx | 12 +- src/events/forward/getForwardableEvent.ts | 4 +- .../location/getShareableLocationEvent.ts | 3 +- src/hooks/room/useRoomCallStatus.ts | 161 ++ src/hooks/room/useRoomThreadNotifications.ts | 67 + src/hooks/room/useTopic.ts | 17 +- src/hooks/spotlight/useDebouncedCallback.ts | 2 +- src/hooks/useGlobalNotificationState.ts | 44 + src/hooks/usePermalink.ts | 4 +- src/hooks/usePublicRoomDirectory.ts | 3 +- src/hooks/useRoomMembers.ts | 33 +- src/hooks/useSpaceResults.ts | 7 +- src/hooks/useThreepids.ts | 3 +- src/i18n/strings/ar.json | 227 +-- src/i18n/strings/az.json | 56 +- src/i18n/strings/be.json | 24 +- src/i18n/strings/bg.json | 473 +++--- src/i18n/strings/bs.json | 8 +- src/i18n/strings/ca.json | 279 ++-- src/i18n/strings/cs.json | 945 ++++++----- src/i18n/strings/cy.json | 8 +- src/i18n/strings/da.json | 126 +- src/i18n/strings/de_DE.json | 951 ++++++----- src/i18n/strings/el.json | 782 +++++---- src/i18n/strings/en_EN.json | 1207 ++++++-------- src/i18n/strings/en_US.json | 127 +- src/i18n/strings/eo.json | 663 +++++--- src/i18n/strings/es.json | 888 ++++++---- src/i18n/strings/et.json | 938 ++++++----- src/i18n/strings/eu.json | 437 +++-- src/i18n/strings/fa.json | 547 ++++--- src/i18n/strings/fi.json | 867 ++++++---- src/i18n/strings/fr.json | 940 ++++++----- src/i18n/strings/ga.json | 238 +-- src/i18n/strings/gl.json | 824 ++++++---- src/i18n/strings/he.json | 609 ++++--- src/i18n/strings/hi.json | 86 +- src/i18n/strings/hr.json | 22 +- src/i18n/strings/hu.json | 914 ++++++----- src/i18n/strings/id.json | 945 ++++++----- src/i18n/strings/is.json | 816 ++++++---- src/i18n/strings/it.json | 945 ++++++----- src/i18n/strings/ja.json | 870 ++++++---- src/i18n/strings/jbo.json | 116 +- src/i18n/strings/ka.json | 49 +- src/i18n/strings/kab.json | 449 +++--- src/i18n/strings/ko.json | 379 +++-- src/i18n/strings/lo.json | 771 +++++---- src/i18n/strings/lt.json | 633 +++++--- src/i18n/strings/lv.json | 476 +++--- src/i18n/strings/ml.json | 46 +- src/i18n/strings/mn.json | 6 +- src/i18n/strings/nb_NO.json | 427 +++-- src/i18n/strings/ne.json | 6 +- src/i18n/strings/nl.json | 832 ++++++---- src/i18n/strings/nn.json | 360 +++-- src/i18n/strings/oc.json | 161 +- src/i18n/strings/pl.json | 932 ++++++----- src/i18n/strings/pt.json | 145 +- src/i18n/strings/pt_BR.json | 680 +++++--- src/i18n/strings/ro.json | 18 +- src/i18n/strings/ru.json | 866 ++++++---- src/i18n/strings/si.json | 10 +- src/i18n/strings/sk.json | 948 ++++++----- src/i18n/strings/sl.json | 20 +- src/i18n/strings/sq.json | 908 ++++++----- src/i18n/strings/sr.json | 342 ++-- src/i18n/strings/sr_Latn.json | 18 +- src/i18n/strings/sv.json | 937 ++++++----- src/i18n/strings/ta.json | 48 +- src/i18n/strings/te.json | 40 +- src/i18n/strings/th.json | 179 ++- src/i18n/strings/tr.json | 453 +++--- src/i18n/strings/tzm.json | 75 +- src/i18n/strings/uk.json | 982 +++++++----- src/i18n/strings/vi.json | 903 +++++++---- src/i18n/strings/vls.json | 321 ++-- src/i18n/strings/zh_Hans.json | 854 ++++++---- src/i18n/strings/zh_Hant.json | 940 ++++++----- src/languageHandler.tsx | 76 +- src/modules/ProxiedModuleApi.ts | 19 +- .../VectorPushRulesDefinitions.ts | 6 +- src/phonenumber.ts | 253 +-- src/settings/Settings.tsx | 114 +- src/settings/SettingsStore.ts | 2 +- src/slash-commands/command.ts | 6 +- src/stores/OwnBeaconStore.ts | 14 +- src/stores/OwnProfileStore.ts | 1 + src/stores/RoomViewStore.tsx | 4 +- .../right-panel/RightPanelStorePhases.ts | 2 +- src/stores/room-list/MessagePreviewStore.ts | 3 +- .../previews/PollStartEventPreview.ts | 3 +- src/stores/spaces/SpaceStore.ts | 2 +- src/stores/spaces/index.ts | 11 +- src/theme.ts | 4 +- src/toasts/AnalyticsToast.tsx | 13 +- src/toasts/DesktopNotificationsToast.ts | 4 +- src/toasts/IncomingCallToast.tsx | 10 +- src/toasts/IncomingLegacyCallToast.tsx | 6 +- src/toasts/MobileGuideToast.ts | 5 +- src/toasts/ServerLimitToast.tsx | 2 +- src/toasts/SetupEncryptionToast.ts | 6 +- src/toasts/UnverifiedSessionToast.tsx | 2 +- src/toasts/UpdateToast.tsx | 6 +- src/utils/AutoDiscoveryUtils.tsx | 29 +- src/utils/ErrorUtils.tsx | 11 +- src/utils/EventRenderingUtils.ts | 13 +- src/utils/EventUtils.ts | 7 +- src/utils/FileUtils.ts | 2 +- src/utils/FormattingUtils.ts | 28 +- src/utils/MultiInviter.ts | 3 +- src/utils/PasswordScorer.ts | 7 +- src/utils/PinningUtils.ts | 3 +- src/utils/Reply.ts | 13 +- src/utils/SessionLock.ts | 261 +++ src/utils/UserInteractiveAuth.ts | 2 +- src/utils/ValidatedServerConfig.ts | 3 +- src/utils/beacon/duration.ts | 5 +- src/utils/beacon/timeline.ts | 3 +- src/utils/exportUtils/Exporter.ts | 99 +- src/utils/exportUtils/HtmlExport.tsx | 9 +- src/utils/i18n-helpers.ts | 12 +- src/utils/leave-behaviour.ts | 3 +- src/utils/location/LocationShareErrors.ts | 3 +- src/utils/location/isSelfLocation.ts | 2 +- src/utils/location/locationEventGeoUri.ts | 3 +- src/utils/location/map.ts | 7 +- src/utils/location/positionFailureMessage.ts | 3 +- src/utils/notifications.ts | 4 +- src/utils/oidc/authorize.ts | 2 +- src/utils/oidc/getDelegatedAuthAccountUrl.ts | 27 + src/utils/oidc/getOidcLogoutUrl.ts | 28 + .../components/atoms/VoiceBroadcastHeader.tsx | 2 +- .../ConfirmListenBroadcastStopCurrent.tsx | 5 +- .../hooks/useVoiceBroadcastRecording.tsx | 3 +- .../checkVoiceBroadcastPreConditions.tsx | 9 +- .../utils/showCantStartACallDialog.tsx | 3 +- src/widgets/CapabilityText.tsx | 9 +- test/DeviceListener-test.ts | 19 +- test/Reply-test.ts | 13 +- test/TextForEvent-test.ts | 45 +- test/Unread-test.ts | 3 +- test/audio/VoiceRecording-test.ts | 2 + .../components/structures/MatrixChat-test.tsx | 266 ++- .../structures/MessagePanel-test.tsx | 3 +- .../structures/PipContainer-test.tsx | 5 +- test/components/structures/RoomView-test.tsx | 2 +- .../structures/SpaceHierarchy-test.tsx | 45 +- .../components/structures/TabbedView-test.tsx | 17 +- .../structures/TimelinePanel-test.tsx | 18 +- test/components/structures/UploadBar-test.tsx | 52 + .../components/structures/ViewSource-test.tsx | 28 +- .../__snapshots__/MatrixChat-test.tsx.snap | 177 +- .../__snapshots__/MessagePanel-test.tsx.snap | 31 +- .../__snapshots__/RoomView-test.tsx.snap | 176 +- .../SpaceHierarchy-test.tsx.snap | 92 +- .../__snapshots__/UserMenu-test.tsx.snap | 23 +- .../components/structures/auth/Login-test.tsx | 10 +- .../structures/auth/Registration-test.tsx | 1 - .../views/VerificationShowSas-test.tsx | 31 + .../views/auth/CountryDropdown-test.tsx | 17 +- .../views/avatars/MemberAvatar-test.tsx | 2 +- .../views/avatars/RoomAvatar-test.tsx | 3 - .../__snapshots__/RoomAvatar-test.tsx.snap | 69 +- .../views/beacon/BeaconListItem-test.tsx | 3 +- .../__snapshots__/BeaconMarker-test.tsx.snap | 27 +- .../BeaconViewDialog-test.tsx.snap | 25 +- .../__snapshots__/DialogSidebar-test.tsx.snap | 15 +- test/components/views/beta/BetaCard-test.tsx | 11 +- .../context_menus/MessageContextMenu-test.tsx | 2 +- .../context_menus/RoomContextMenu-test.tsx | 13 + .../RoomGeneralContextMenu-test.tsx | 3 +- .../context_menus/WidgetContextMenu-test.tsx | 8 +- .../dialogs/ConfirmUserActionDialog-test.tsx | 35 + .../views/dialogs/ForwardDialog-test.tsx | 14 +- .../dialogs/MessageEditHistoryDialog-test.tsx | 2 +- .../views/dialogs/RoomSettingsDialog-test.tsx | 58 +- .../views/dialogs/SpotlightDialog-test.tsx | 25 +- .../ConfirmUserActionDialog-test.tsx.snap | 95 ++ ...nageRestrictedJoinRuleDialog-test.tsx.snap | 23 +- .../MessageEditHistoryDialog-test.tsx.snap | 14 +- .../views/elements/FacePile-test.tsx | 31 + .../views/elements/PollCreateDialog-test.tsx | 11 +- .../SpellCheckLanguagesDropdown-test.tsx | 38 + .../__snapshots__/AppTile-test.tsx.snap | 104 +- .../__snapshots__/FacePile-test.tsx.snap | 26 + .../elements/__snapshots__/Pill-test.tsx.snap | 186 +-- .../SpellCheckLanguagesDropdown-test.tsx.snap | 32 + .../views/location/LocationShareMenu-test.tsx | 3 +- .../location/LocationViewDialog-test.tsx | 5 +- .../components/views/location/Marker-test.tsx | 2 +- .../views/location/shareLocation-test.ts | 14 +- .../views/messages/CallEvent-test.tsx | 8 +- .../views/messages/DateSeparator-test.tsx | 8 +- .../views/messages/MBeaconBody-test.tsx | 2 +- .../views/messages/MImageBody-test.tsx | 26 + .../views/messages/MLocationBody-test.tsx | 3 +- .../views/messages/MPollBody-test.tsx | 7 +- .../views/messages/MPollEndBody-test.tsx | 3 +- .../views/messages/TextualBody-test.tsx | 4 +- .../__snapshots__/DateSeparator-test.tsx.snap | 8 +- .../__snapshots__/MLocationBody-test.tsx.snap | 25 +- .../__snapshots__/TextualBody-test.tsx.snap | 164 +- .../polls/pollHistory/PollHistory-test.tsx | 15 +- .../pollHistory/PollListItemEnded-test.tsx | 3 +- .../LegacyRoomHeaderButtons-test.tsx | 2 +- .../right_panel/PinnedMessagesCard-test.tsx | 2 +- .../RoomSummaryCard-test.tsx.snap | 23 +- .../__snapshots__/UserInfo-test.tsx.snap | 28 +- .../views/rooms/LegacyRoomHeader-test.tsx | 40 +- .../UnreadNotificationBadge-test.tsx | 2 +- .../views/rooms/PresenceLabel-test.tsx | 35 + .../views/rooms/RoomHeader-test.tsx | 229 ++- test/components/views/rooms/RoomTile-test.tsx | 4 +- .../views/rooms/SearchResultTile-test.tsx | 2 +- .../PinnedEventTile-test.tsx.snap | 25 +- .../RoomPreviewBar-test.tsx.snap | 115 +- .../__snapshots__/RoomTile-test.tsx.snap | 92 +- .../components/FormattingButtons-test.tsx | 4 + .../views/settings/Notifications-test.tsx | 3 +- .../settings/devices/LoginWithQR-test.tsx | 2 +- .../discovery/EmailAddresses-test.tsx | 7 +- .../settings/discovery/PhoneNumbers-test.tsx | 2 +- .../notifications/Notifications2-test.tsx | 11 +- .../tabs/room/PeopleRoomSettingsTab-test.tsx | 217 +++ .../PeopleRoomSettingsTab-test.tsx.snap | 163 ++ .../tabs/user/GeneralUserSettingsTab-test.tsx | 203 ++- .../tabs/user/LabsUserSettingsTab-test.tsx | 8 +- .../tabs/user/SessionManagerTab-test.tsx | 175 +- .../tabs/user/SidebarUserSettingsTab-test.tsx | 2 +- .../GeneralUserSettingsTab-test.tsx.snap | 175 ++ .../SessionManagerTab-test.tsx.snap | 15 + .../spaces/AddExistingToSpaceDialog-test.tsx | 23 +- .../views/spaces/QuickSettingsButton-test.tsx | 5 + .../views/spaces/SpacePanel-test.tsx | 7 +- .../views/spaces/SpaceTreeLevel-test.tsx | 19 +- .../AddExistingToSpaceDialog-test.tsx.snap | 27 +- .../QuickSettingsButton-test.tsx.snap | 15 + .../UserOnboardingPage-test.tsx | 4 +- test/components/views/voip/CallView-test.tsx | 10 +- test/editor/serialize-test.ts | 23 + test/i18n/languages.json | 5 +- test/languageHandler-test.ts | 121 -- test/{i18n-test => }/languageHandler-test.tsx | 169 +- test/modules/ProxiedModuleApi-test.ts | 105 -- test/modules/ProxiedModuleApi-test.tsx | 257 +++ ...erSupportUnstableFeatureController-test.ts | 3 +- test/setup/setupLanguage.ts | 60 +- test/setup/setupManualMocks.ts | 13 +- test/stores/OwnBeaconStore-test.ts | 18 +- .../room-list/algorithms/Algorithm-test.ts | 9 +- test/test-utils/beacon.ts | 19 +- test/test-utils/location.ts | 8 +- test/test-utils/oidc.ts | 2 +- test/test-utils/poll.ts | 8 +- test/test-utils/test-utils.ts | 3 +- test/test-utils/utilities.ts | 69 + test/utils/AutoDiscoveryUtils-test.tsx | 25 +- test/utils/DateUtils-test.ts | 244 ++- test/utils/EventUtils-test.ts | 2 +- test/utils/SessionLock-test.ts | 252 +++ .../AutoDiscoveryUtils-test.tsx.snap | 26 + test/utils/beacon/duration-test.ts | 3 +- test/utils/exportUtils/HTMLExport-test.ts | 68 +- .../utils/exportUtils/PlainTextExport-test.ts | 4 +- .../__snapshots__/HTMLExport-test.ts.snap | 8 +- test/utils/i18n-helpers-test.ts | 62 + test/utils/location/isSelfLocation-test.ts | 10 +- .../media/requestMediaPermissions-test.tsx | 3 +- test/utils/notifications-test.ts | 3 +- .../oidc/getDelegatedAuthAccountUrl-test.ts | 61 + test/utils/threepids-test.ts | 3 +- yarn.lock | 1422 ++++++++++------- 711 files changed, 29268 insertions(+), 18785 deletions(-) create mode 100644 cypress/e2e/login/soft_logout.spec.ts create mode 100644 cypress/e2e/login/utils.ts create mode 100644 cypress/e2e/register/email.spec.ts create mode 100644 cypress/plugins/mailhog/index.ts create mode 100644 cypress/plugins/synapsedocker/templates/email/README.md create mode 100644 cypress/plugins/synapsedocker/templates/email/homeserver.yaml create mode 100644 cypress/plugins/synapsedocker/templates/email/log.config create mode 100644 cypress/support/mailhog.ts create mode 100644 cypress/support/promise.ts create mode 100644 res/css/components/views/utils/_Box.pcss create mode 100644 res/css/components/views/utils/_Flex.pcss rename res/css/{voice-broadcast/atoms/_PlaybackControlButton.pcss => structures/auth/_ConfirmSessionLockTheftView.pcss} (70%) create mode 100644 res/css/structures/auth/_SessionLockStolenView.pcss create mode 100644 res/css/views/elements/_LanguageDropdown.pcss delete mode 100644 res/css/views/elements/_TooltipButton.pcss create mode 100644 res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss create mode 100644 res/img/element-icons/spaces.svg delete mode 100755 scripts/check-i18n.pl delete mode 100755 scripts/fix-i18n.pl delete mode 100755 scripts/fixup-imports.pl create mode 100644 src/components/structures/auth/ConfirmSessionLockTheftView.tsx create mode 100644 src/components/structures/auth/SessionLockStolenView.tsx create mode 100644 src/components/utils/Box.tsx create mode 100644 src/components/utils/Flex.tsx create mode 100644 src/components/views/dialogs/oidc/OidcLogoutDialog.tsx create mode 100644 src/components/views/dialogs/spotlight/Filter.ts delete mode 100644 src/components/views/elements/TooltipButton.tsx create mode 100644 src/components/views/settings/tabs/room/PeopleRoomSettingsTab.tsx create mode 100644 src/dispatcher/payloads/OpenSpotlightPayload.ts delete mode 100644 src/emoji.ts create mode 100644 src/hooks/room/useRoomCallStatus.ts create mode 100644 src/hooks/room/useRoomThreadNotifications.ts create mode 100644 src/hooks/useGlobalNotificationState.ts create mode 100644 src/utils/SessionLock.ts create mode 100644 src/utils/oidc/getDelegatedAuthAccountUrl.ts create mode 100644 src/utils/oidc/getOidcLogoutUrl.ts create mode 100644 test/components/structures/UploadBar-test.tsx create mode 100644 test/components/views/VerificationShowSas-test.tsx create mode 100644 test/components/views/dialogs/ConfirmUserActionDialog-test.tsx create mode 100644 test/components/views/dialogs/__snapshots__/ConfirmUserActionDialog-test.tsx.snap create mode 100644 test/components/views/elements/FacePile-test.tsx create mode 100644 test/components/views/elements/SpellCheckLanguagesDropdown-test.tsx create mode 100644 test/components/views/elements/__snapshots__/FacePile-test.tsx.snap create mode 100644 test/components/views/elements/__snapshots__/SpellCheckLanguagesDropdown-test.tsx.snap create mode 100644 test/components/views/rooms/PresenceLabel-test.tsx create mode 100644 test/components/views/settings/tabs/room/PeopleRoomSettingsTab-test.tsx create mode 100644 test/components/views/settings/tabs/room/__snapshots__/PeopleRoomSettingsTab-test.tsx.snap create mode 100644 test/components/views/spaces/__snapshots__/QuickSettingsButton-test.tsx.snap delete mode 100644 test/languageHandler-test.ts rename test/{i18n-test => }/languageHandler-test.tsx (60%) delete mode 100644 test/modules/ProxiedModuleApi-test.ts create mode 100644 test/modules/ProxiedModuleApi-test.tsx create mode 100644 test/utils/SessionLock-test.ts create mode 100644 test/utils/__snapshots__/AutoDiscoveryUtils-test.tsx.snap create mode 100644 test/utils/i18n-helpers-test.ts create mode 100644 test/utils/oidc/getDelegatedAuthAccountUrl-test.ts diff --git a/.eslintrc.js b/.eslintrc.js index f3a6e364934..4434aecfdfe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -62,118 +62,6 @@ module.exports = { name: "matrix-js-sdk/src/index", message: "Please use matrix-js-sdk/src/matrix instead", }, - { - name: "matrix-js-sdk/src/models/typed-event-emitter", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/room", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/room-member", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/room-state", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event-status", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/user", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/device", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event-timeline", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/event-timeline-set", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/partials", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/event", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/client", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/search-result", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/poll", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/relations", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/http-api", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/PushRules", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/@types/search", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/filter", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/webrtc/groupCall", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/service-types", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/sync", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/timeline-window", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/store/indexeddb", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/crypto/store/localStorage-crypto-store", - message: "Please use matrix-js-sdk/src/matrix instead", - }, - { - name: "matrix-js-sdk/src/models/thread", - message: "Please use matrix-js-sdk/src/matrix instead", - }, { name: "matrix-react-sdk", message: "Please use matrix-react-sdk/src/index instead", @@ -185,8 +73,79 @@ module.exports = { ], patterns: [ { - group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"], - message: "Please use matrix-js-sdk/src/* instead", + group: [ + "matrix-js-sdk/src/**", + "!matrix-js-sdk/src/matrix", + "matrix-js-sdk/lib", + "matrix-js-sdk/lib/", + "matrix-js-sdk/lib/**", + // XXX: Temporarily allow these as they are not available via the main export + "!matrix-js-sdk/src/logger", + "!matrix-js-sdk/src/errors", + "!matrix-js-sdk/src/utils", + "!matrix-js-sdk/src/version-support", + "!matrix-js-sdk/src/randomstring", + "!matrix-js-sdk/src/sliding-sync", + "!matrix-js-sdk/src/browser-index", + "!matrix-js-sdk/src/feature", + "!matrix-js-sdk/src/NamespacedValue", + "!matrix-js-sdk/src/ReEmitter", + "!matrix-js-sdk/src/event-mapper", + "!matrix-js-sdk/src/interactive-auth", + "!matrix-js-sdk/src/secret-storage", + "!matrix-js-sdk/src/room-hierarchy", + "!matrix-js-sdk/src/rendezvous", + "!matrix-js-sdk/src/rendezvous/transports", + "!matrix-js-sdk/src/rendezvous/channels", + "!matrix-js-sdk/src/indexeddb-worker", + "!matrix-js-sdk/src/pushprocessor", + "!matrix-js-sdk/src/extensible_events_v1", + "!matrix-js-sdk/src/extensible_events_v1/PollStartEvent", + "!matrix-js-sdk/src/extensible_events_v1/PollResponseEvent", + "!matrix-js-sdk/src/extensible_events_v1/PollEndEvent", + "!matrix-js-sdk/src/extensible_events_v1/InvalidEventError", + "!matrix-js-sdk/src/crypto-api", + "!matrix-js-sdk/src/crypto-api/verification", + "!matrix-js-sdk/src/crypto", + "!matrix-js-sdk/src/crypto/algorithms", + "!matrix-js-sdk/src/crypto/api", + "!matrix-js-sdk/src/crypto/aes", + "!matrix-js-sdk/src/crypto/backup", + "!matrix-js-sdk/src/crypto/olmlib", + "!matrix-js-sdk/src/crypto/crypto", + "!matrix-js-sdk/src/crypto/keybackup", + "!matrix-js-sdk/src/crypto/RoomList", + "!matrix-js-sdk/src/crypto/deviceinfo", + "!matrix-js-sdk/src/crypto/key_passphrase", + "!matrix-js-sdk/src/crypto/CrossSigning", + "!matrix-js-sdk/src/crypto/recoverykey", + "!matrix-js-sdk/src/crypto/dehydration", + "!matrix-js-sdk/src/crypto/verification", + "!matrix-js-sdk/src/crypto/verification/SAS", + "!matrix-js-sdk/src/crypto/verification/QRCode", + "!matrix-js-sdk/src/crypto/verification/request", + "!matrix-js-sdk/src/crypto/verification/request/VerificationRequest", + "!matrix-js-sdk/src/common-crypto", + "!matrix-js-sdk/src/common-crypto/CryptoBackend", + "!matrix-js-sdk/src/oidc", + "!matrix-js-sdk/src/oidc/discovery", + "!matrix-js-sdk/src/oidc/authorize", + "!matrix-js-sdk/src/oidc/validate", + "!matrix-js-sdk/src/oidc/error", + "!matrix-js-sdk/src/oidc/register", + "!matrix-js-sdk/src/webrtc", + "!matrix-js-sdk/src/webrtc/call", + "!matrix-js-sdk/src/webrtc/callFeed", + "!matrix-js-sdk/src/webrtc/mediaHandler", + "!matrix-js-sdk/src/webrtc/callEventTypes", + "!matrix-js-sdk/src/webrtc/callEventHandler", + "!matrix-js-sdk/src/webrtc/groupCallEventHandler", + "!matrix-js-sdk/src/models", + "!matrix-js-sdk/src/models/read-receipt", + "!matrix-js-sdk/src/models/relations-container", + "!matrix-js-sdk/src/models/related-relations", + ], + message: "Please use matrix-js-sdk/src/matrix instead", }, ], }, diff --git a/.github/workflows/cypress.yaml b/.github/workflows/cypress.yaml index bf1a30b87c0..070dee5b5ea 100644 --- a/.github/workflows/cypress.yaml +++ b/.github/workflows/cypress.yaml @@ -163,7 +163,7 @@ jobs: echo "CYPRESS_RUST_CRYPTO=1" >> "$GITHUB_ENV" - name: Run Cypress tests - uses: cypress-io/github-action@90dff940a41c08c7c344310eac7e57eda636326a + uses: cypress-io/github-action@fa88e4afe551e64c8827a4b9e379afc63d8f691a with: working-directory: matrix-react-sdk # The built-in Electron runner seems to grind to a halt trying to run the tests, so use chrome. diff --git a/.github/workflows/i18n_check.yml b/.github/workflows/i18n_check.yml index bb8e0188826..e72f8ca7b62 100644 --- a/.github/workflows/i18n_check.yml +++ b/.github/workflows/i18n_check.yml @@ -11,8 +11,8 @@ jobs: - name: "Get modified files" id: changed_files - if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' - uses: tj-actions/changed-files@87697c0dca7dd44e37a2b79a79489332556ff1f3 # v37 + if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot' && github.event.pull_request.user.login != 't3chguy' + uses: tj-actions/changed-files@1c26215f3fbd51eba03bc199e5cbabdfc3584ce3 # v38 with: files: | src/i18n/strings/* @@ -25,8 +25,8 @@ jobs: github.event.pull_request.user.login != 'RiotTranslateBot' && steps.changed_files.outputs.any_modified == 'true' run: | - echo "Only translation files modified by `yarn i18n` can be committed - other translation files will confuse weblate in unrecoverable ways." exit 1 + echo "Only translation files modified by 'yarn i18n' can be committed - other translation files will confuse weblate in unrecoverable ways." - uses: actions/setup-node@v3 with: diff --git a/__mocks__/languages.json b/__mocks__/languages.json index 36ec89561b2..35a400808b8 100644 --- a/__mocks__/languages.json +++ b/__mocks__/languages.json @@ -1,10 +1,4 @@ { - "en": { - "fileName": "en_EN.json", - "label": "English" - }, - "en-us": { - "fileName": "en_US.json", - "label": "English (US)" - } + "en": "en_EN.json", + "en-us": "en_US.json" } diff --git a/cypress/e2e/audio-player/audio-player.spec.ts b/cypress/e2e/audio-player/audio-player.spec.ts index 8f1ef8054c6..30470716c91 100644 --- a/cypress/e2e/audio-player/audio-player.spec.ts +++ b/cypress/e2e/audio-player/audio-player.spec.ts @@ -168,7 +168,8 @@ describe("Audio player", () => { it("should be correctly rendered - light theme with monospace font", () => { uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg"); - takeSnapshots("Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace + // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881 + //takeSnapshots("Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace }); it("should be correctly rendered - high contrast theme", () => { @@ -186,7 +187,8 @@ describe("Audio player", () => { uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg"); - takeSnapshots("Selected EventTile of audio player (high contrast)"); + // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881 + //takeSnapshots("Selected EventTile of audio player (high contrast)"); }); it("should be correctly rendered - dark theme", () => { @@ -254,8 +256,8 @@ describe("Audio player", () => { }); }); - // Take snapshots - takeSnapshots("Selected EventTile of audio player with a reply"); + // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881 + //takeSnapshots("Selected EventTile of audio player with a reply"); }); it("should support creating a reply chain with multiple audio files", () => { diff --git a/cypress/e2e/editing/editing.spec.ts b/cypress/e2e/editing/editing.spec.ts index dafe15c885f..b7dacf86034 100644 --- a/cypress/e2e/editing/editing.spec.ts +++ b/cypress/e2e/editing/editing.spec.ts @@ -119,7 +119,7 @@ describe("Editing", () => { // Assert that the date separator is rendered at the top cy.get("li:nth-child(1) .mx_DateSeparator").within(() => { cy.get("h2").within(() => { - cy.findByText("Today"); + cy.findByText("today").should("have.css", "text-transform", "capitalize"); }); }); @@ -184,7 +184,7 @@ describe("Editing", () => { // Assert that the date is rendered cy.get("li:nth-child(1) .mx_DateSeparator").within(() => { cy.get("h2").within(() => { - cy.findByText("Today"); + cy.findByText("today").should("have.css", "text-transform", "capitalize"); }); }); diff --git a/cypress/e2e/login/login.spec.ts b/cypress/e2e/login/login.spec.ts index 29c1f6f16bd..2bb7d4c6a00 100644 --- a/cypress/e2e/login/login.spec.ts +++ b/cypress/e2e/login/login.spec.ts @@ -17,6 +17,7 @@ limitations under the License. /// import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { doTokenRegistration } from "./utils"; describe("Login", () => { let homeserver: HomeserverInstance; @@ -93,7 +94,6 @@ describe("Login", () => { }) .then((data) => { homeserver = data; - cy.visit("/#/login"); }); }); @@ -108,33 +108,7 @@ describe("Login", () => { // If you are using ufw, try something like: // sudo ufw allow in on docker0 // - cy.findByRole("button", { name: "Edit" }).click(); - cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserver.baseUrl); - cy.findByRole("button", { name: "Continue" }).click(); - // wait for the dialog to go away - cy.get(".mx_ServerPickerDialog").should("not.exist"); - - // click on "Continue with OAuth test" - cy.findByRole("button", { name: "Continue with OAuth test" }).click(); - - // wait for the Test OAuth Page to load - cy.findByText("Test OAuth page"); - - // click the submit button - cy.findByRole("button", { name: "Submit" }).click(); - - // Synapse prompts us to pick a user ID - cy.findByRole("heading", { name: "Create your account" }); - cy.findByRole("textbox", { name: "Username (required)" }).type("alice"); - - // wait for username validation to start, and complete - cy.wait(50); - cy.get("#field-username-output").should("have.value", ""); - cy.findByRole("button", { name: "Continue" }).click(); - - // Synapse prompts us to grant permission to Element - cy.findByRole("heading", { name: "Continue to your account" }); - cy.findByRole("link", { name: "Continue" }).click(); + doTokenRegistration(homeserver.baseUrl); // Eventually, we should end up at the home screen. cy.url().should("contain", "/#/home", { timeout: 30000 }); diff --git a/cypress/e2e/login/soft_logout.spec.ts b/cypress/e2e/login/soft_logout.spec.ts new file mode 100644 index 00000000000..959197b7ebe --- /dev/null +++ b/cypress/e2e/login/soft_logout.spec.ts @@ -0,0 +1,143 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { UserCredentials } from "../../support/login"; +import { doTokenRegistration } from "./utils"; + +describe("Soft logout", () => { + let homeserver: HomeserverInstance; + + beforeEach(() => { + cy.task("startOAuthServer") + .then((oAuthServerPort: number) => { + return cy.startHomeserver({ template: "default", oAuthServerPort }); + }) + .then((data) => { + homeserver = data; + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + }); + + describe("with password user", () => { + let testUserCreds: UserCredentials; + + beforeEach(() => { + cy.initTestUser(homeserver, "Alice").then((creds) => { + testUserCreds = creds; + }); + }); + + it("shows the soft-logout page when a request fails, and allows a re-login", () => { + interceptRequestsWithSoftLogout(); + cy.findByText("You're signed out"); + cy.findByPlaceholderText("Password").type(testUserCreds.password).type("{enter}"); + + // back to the welcome page + cy.url().should("contain", "/#/home", { timeout: 30000 }); + cy.findByRole("heading", { name: "Welcome Alice" }); + }); + + it("still shows the soft-logout page when the page is reloaded after a soft-logout", () => { + interceptRequestsWithSoftLogout(); + cy.findByText("You're signed out"); + cy.reload(); + cy.findByText("You're signed out"); + }); + }); + + describe("with SSO user", () => { + beforeEach(() => { + doTokenRegistration(homeserver.baseUrl); + + // Eventually, we should end up at the home screen. + cy.url().should("contain", "/#/home", { timeout: 30000 }); + cy.findByRole("heading", { name: "Welcome Alice" }); + }); + + it("shows the soft-logout page when a request fails, and allows a re-login", () => { + // there is a bug in Element which means this only actually works if there is an app reload between + // the token login and the soft-logout: https://github.com/vector-im/element-web/issues/25957 + cy.reload(); + cy.findByRole("heading", { name: "Welcome Alice" }); + + interceptRequestsWithSoftLogout(); + + cy.findByText("You're signed out"); + cy.findByRole("button", { name: "Continue with OAuth test" }).click(); + + // click the submit button + cy.findByRole("button", { name: "Submit" }).click(); + + // Synapse prompts us to grant permission to Element + cy.findByRole("heading", { name: "Continue to your account" }); + cy.findByRole("link", { name: "Continue" }).click(); + + // back to the welcome page + cy.url().should("contain", "/#/home", { timeout: 30000 }); + cy.findByRole("heading", { name: "Welcome Alice" }); + }); + }); +}); + +/** + * Intercept calls to /sync and have them fail with a soft-logout + * + * Any further requests to /sync with the same access token are blocked. + */ +function interceptRequestsWithSoftLogout(): void { + let expiredAccessToken: string | null = null; + cy.intercept( + { + pathname: "/_matrix/client/*/sync", + }, + (req) => { + const accessToken = req.headers["authorization"] as string; + + // on the first request, record the access token + if (!expiredAccessToken) { + console.log(`Soft-logout on access token ${accessToken}`); + expiredAccessToken = accessToken; + } + + // now, if the access token on this request matches the expired one, block it + if (expiredAccessToken && accessToken === expiredAccessToken) { + console.log(`Intercepting request with soft-logged-out access token`); + req.reply({ + statusCode: 401, + body: { + errcode: "M_UNKNOWN_TOKEN", + error: "Soft logout", + soft_logout: true, + }, + }); + return; + } + + // otherwise, pass through as normal + req.continue(); + }, + ); + + // do something to make the active /sync return: create a new room + cy.getClient().then((client) => { + // don't wait for this to complete: it probably won't, because of the broken sync + return client.createRoom({}); + }); +} diff --git a/cypress/e2e/login/utils.ts b/cypress/e2e/login/utils.ts new file mode 100644 index 00000000000..39d87b42756 --- /dev/null +++ b/cypress/e2e/login/utils.ts @@ -0,0 +1,49 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element + */ +export function doTokenRegistration(homeserverUrl: string) { + cy.visit("/#/login"); + + cy.findByRole("button", { name: "Edit" }).click(); + cy.findByRole("textbox", { name: "Other homeserver" }).type(homeserverUrl); + cy.findByRole("button", { name: "Continue" }).click(); + // wait for the dialog to go away + cy.get(".mx_ServerPickerDialog").should("not.exist"); + + // click on "Continue with OAuth test" + cy.findByRole("button", { name: "Continue with OAuth test" }).click(); + + // wait for the Test OAuth Page to load + cy.findByText("Test OAuth page"); + + // click the submit button + cy.findByRole("button", { name: "Submit" }).click(); + + // Synapse prompts us to pick a user ID + cy.findByRole("heading", { name: "Create your account" }); + cy.findByRole("textbox", { name: "Username (required)" }).type("alice"); + + // wait for username validation to start, and complete + cy.wait(50); + cy.get("#field-username-output").should("have.value", ""); + cy.findByRole("button", { name: "Continue" }).click(); + + // Synapse prompts us to grant permission to Element + cy.findByRole("heading", { name: "Continue to your account" }); + cy.findByRole("link", { name: "Continue" }).click(); +} diff --git a/cypress/e2e/read-receipts/high-level.spec.ts b/cypress/e2e/read-receipts/high-level.spec.ts index 7af0daeb9e4..d7b27242c85 100644 --- a/cypress/e2e/read-receipts/high-level.spec.ts +++ b/cypress/e2e/read-receipts/high-level.spec.ts @@ -68,11 +68,15 @@ describe("Read receipts", () => { cy.stopHomeserver(homeserver); }); - abstract class MessageSpec { + abstract class MessageContentSpec { public abstract getContent(room: Room): Promise>; } - type Message = string | MessageSpec; + abstract class BotActionSpec { + public abstract performAction(cli: MatrixClient, room: Room): Promise; + } + + type Message = string | MessageContentSpec | BotActionSpec; function goTo(room: string) { cy.viewRoomByName(room); @@ -95,24 +99,39 @@ describe("Read receipts", () => { cy.get(".mx_ThreadView_timelinePanelWrapper", { log: false }).should("have.length", 1); } - /** - * Sends messages into given room as a bot - * @param room - the name of the room to send messages into - * @param messages - the list of messages to send, these can be strings or implementations of MessageSpace like `editOf` - */ - function receiveMessages(room: string, messages: Message[]) { + function sendMessageAsClient(cli: MatrixClient, room: string, messages: Message[]) { findRoomByName(room).then(async ({ roomId }) => { - const room = bot.getRoom(roomId); + const room = cli.getRoom(roomId); for (const message of messages) { if (typeof message === "string") { - await bot.sendTextMessage(roomId, message); + await cli.sendTextMessage(roomId, message); + } else if (message instanceof MessageContentSpec) { + await cli.sendMessage(roomId, await message.getContent(room)); } else { - await bot.sendMessage(roomId, await message.getContent(room)); + await message.performAction(cli, room); } } }); } + /** + * Sends messages into given room as a bot + * @param room - the name of the room to send messages into + * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` + */ + function receiveMessages(room: string, messages: Message[]) { + sendMessageAsClient(bot, room, messages); + } + + /** + * Sends messages into given room as the currently logged-in user + * @param room - the name of the room to send messages into + * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` + */ + function sendMessages(room: string, messages: Message[]) { + cy.getClient().then((cli) => sendMessageAsClient(cli, room, messages)); + } + /** * Utility to find a MatrixEvent by its body content * @param room - the room to search for the event in @@ -140,12 +159,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send an edit into a room + * MessageContentSpec to send an edit into a room * @param originalMessage - the body of the message to edit * @param newMessage - the message body to send in the edit */ - function editOf(originalMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function editOf(originalMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, originalMessage, true); @@ -163,12 +182,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send a reply into a room + * MessageContentSpec to send a reply into a room * @param targetMessage - the body of the message to reply to * @param newMessage - the message body to send into the reply */ - function replyTo(targetMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function replyTo(targetMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, targetMessage); @@ -186,12 +205,12 @@ describe("Read receipts", () => { } /** - * MessageSpec to send a threaded response into a room + * MessageContentSpec to send a threaded response into a room * @param rootMessage - the body of the thread root message to send a response to * @param newMessage - the message body to send into the thread response */ - function threadedOff(rootMessage: string, newMessage: string): MessageSpec { - return new (class extends MessageSpec { + function threadedOff(rootMessage: string, newMessage: string): MessageContentSpec { + return new (class extends MessageContentSpec { public async getContent(room: Room): Promise> { const ev = await getMessage(room, rootMessage); @@ -208,6 +227,53 @@ describe("Read receipts", () => { })(); } + /** + * BotActionSpec to send a reaction to an existing event into a room + * @param targetMessage - the body of the message to send a reaction to + * @param reaction - the key of the reaction to send into the room + */ + function reactionTo(targetMessage: string, reaction: string): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + const ev = await getMessage(room, targetMessage, true); + const threadId = !ev.isThreadRoot ? ev.threadRootId : undefined; + await cli.sendEvent(room.roomId, threadId ?? null, "m.reaction", { + "m.relates_to": { + rel_type: "m.annotation", + event_id: ev.getId(), + key: reaction, + }, + }); + } + })(); + } + + /** + * BotActionSpec to send a custom event + * @param eventType - the type of the event to send + * @param content - the event content to send + */ + function customEvent(eventType: string, content: Record): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + await cli.sendEvent(room.roomId, null, eventType, content); + } + })(); + } + + /** + * BotActionSpec to send a redaction into a room + * @param targetMessage - the body of the message to send a redaction to + */ + function redactionOf(targetMessage: string): BotActionSpec { + return new (class extends BotActionSpec { + public async performAction(cli: MatrixClient, room: Room): Promise { + const ev = await getMessage(room, targetMessage, true); + await cli.redactEvent(room.roomId, ev.threadRootId, ev.getId()); + } + })(); + } + function getRoomListTile(room: string) { return cy.findByRole("treeitem", { name: new RegExp("^" + room), log: false }); } @@ -246,7 +312,7 @@ describe("Read receipts", () => { cy.log("Open thread list"); cy.findByTestId("threadsButton", { log: false }).then(($button) => { if ($button?.attr("aria-current") !== "true") { - $button.trigger("click"); + cy.findByTestId("threadsButton", { log: false }).click(); } }); @@ -296,7 +362,7 @@ describe("Read receipts", () => { describe("new messages", () => { describe("in the main timeline", () => { - it("Sending a message makes a room unread", () => { + it("Receiving a message makes a room unread", () => { goTo(room1); assertRead(room2); @@ -323,7 +389,7 @@ describe("Read receipts", () => { markAsRead(room2); assertRead(room2); }); - it("Sending a new message after marking as read makes it unread", () => { + it("Receiving a new message after marking as read makes it unread", () => { goTo(room1); assertRead(room2); receiveMessages(room2, ["Msg1"]); @@ -356,6 +422,16 @@ describe("Read receipts", () => { saveAndReload(); assertRead(room2); }); + it.skip("Sending a message from a different client marks room as read", () => { + goTo(room1); + assertRead(room2); + + receiveMessages(room2, ["Msg1"]); + assertUnread(room2, 1); + + sendMessages(room2, ["Msg2"]); + assertRead(room2); + }); }); describe("in threads", () => { @@ -376,7 +452,7 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -386,7 +462,7 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 2); // (Sanity) + assertUnread(room2, 3); // (Sanity) // When I read the main timeline goTo(room2); @@ -405,7 +481,7 @@ describe("Read receipts", () => { receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), "Msg2", threadedOff("Msg2", "Resp2")]); assertUnread(room2, 4); goTo(room2); - assertUnread(room2, 4); + assertUnread(room2, 2); openThread("Msg1"); assertUnread(room2, 1); @@ -486,7 +562,7 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), threadedOff("Msg1", "Resp2")]); - assertUnread(room2, 2); // (Sanity) + assertUnread(room2, 3); // (Sanity) // When I read the main timeline goTo(room2); @@ -527,13 +603,13 @@ describe("Read receipts", () => { // Given a thread exists goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); // (Sanity) + assertUnread(room2, 2); // (Sanity) // When I read the main timeline goTo(room2); // Then room does appear unread - assertUnread(room2, 2); + assertUnread(room2, 1); assertUnreadThread("Msg1"); }); it.skip("Reading a thread root within the thread view marks it as read in the main timeline", () => {}); @@ -625,7 +701,7 @@ describe("Read receipts", () => { goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); assertRead(room2); @@ -642,7 +718,7 @@ describe("Read receipts", () => { goTo(room1); receiveMessages(room2, ["Msg1", replyTo("Msg1", "Reply1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); markAsRead(room2); // When an edit appears in the room @@ -693,7 +769,7 @@ describe("Read receipts", () => { it("An edit of a threaded message makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -706,7 +782,7 @@ describe("Read receipts", () => { it("Reading an edit of a threaded message makes the room read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -723,7 +799,7 @@ describe("Read receipts", () => { it("Marking a room as read after an edit in a thread makes it read", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1"), editOf("Resp1", "Edit1")]); - assertUnread(room2, 2); + assertUnread(room2, 3); // TODO: the edit counts as a message! markAsRead(room2); assertRead(room2); @@ -731,13 +807,13 @@ describe("Read receipts", () => { it.skip("Editing a thread message after marking as read makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); markAsRead(room2); assertRead(room2); receiveMessages(room2, [editOf("Resp1", "Edit1")]); - assertUnread(room2, 1); + assertUnread(room2, 1); // TODO: should this edit make us unread? }); it("A room with an edited threaded message is still unread after restart", () => { goTo(room1); @@ -764,7 +840,7 @@ describe("Read receipts", () => { it("An edit of a thread root makes the room unread", () => { goTo(room1); receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Resp1")]); - assertUnread(room2, 1); + assertUnread(room2, 2); goTo(room2); openThread("Msg1"); @@ -801,24 +877,78 @@ describe("Read receipts", () => { }); describe("reactions", () => { - // Justification for this section: edits an reactions are similar, so we - // might choose to miss this section, but I have included it because - // edits replace the content of the original event in our code and - // reactions don't, so it seems possible that bugs could creep in that - // affect only one or the other. - describe("in the main timeline", () => { - it.skip("Reacting to a message makes a room unread", () => {}); - it.skip("Reading a reaction makes the room read", () => {}); - it.skip("Marking a room as read after a reaction makes it read", () => {}); - it.skip("Reacting to a message after marking as read makes the room unread", () => {}); - it.skip("A room with a reaction is still unread after restart", () => {}); - it.skip("A room where all reactions are read is still read after restart", () => {}); + it("Receiving a reaction to a message does not make a room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + }); + it("Reacting to a message after marking as read does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + }); + it("A room with an unread reaction is still read after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + receiveMessages(room2, [reactionTo("Msg2", "🪿")]); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); + it("A room where all reactions are read is still read after restart", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2", reactionTo("Msg2", "🪿")]); + assertUnread(room2, 2); + + markAsRead(room2); + assertRead(room2); + + saveAndReload(); + assertRead(room2); + }); }); describe("in threads", () => { - it.skip("A reaction to a threaded message makes the room unread", () => {}); - it.skip("Reading a reaction to a threaded message makes the room read", () => {}); + it("A reaction to a threaded message does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Reply1", "🪿")]); + + assertRead(room2); + }); it.skip("Marking a room as read after a reaction in a thread makes it read", () => {}); it.skip("Reacting to a thread message after marking as read makes the room unread", () => {}); it.skip("A room with a reaction to a threaded message is still unread after restart", () => {}); @@ -826,7 +956,21 @@ describe("Read receipts", () => { }); describe("thread roots", () => { - it.skip("A reaction to a thread root makes the room unread", () => {}); + it("A reaction to a thread root does not make the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", threadedOff("Msg1", "Reply1")]); + assertUnread(room2, 2); + + goTo(room2); + openThread("Msg1"); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [reactionTo("Msg1", "🪿")]); + + assertRead(room2); + }); it.skip("Reading a reaction to a thread root makes the room read", () => {}); it.skip("Marking a room as read after a reaction to a thread root makes it read", () => {}); it.skip("Reacting to a thread root after marking as read makes the room unread but not the thread", () => {}); @@ -835,9 +979,20 @@ describe("Read receipts", () => { describe("redactions", () => { describe("in the main timeline", () => { - // One of the following two must be right: - it.skip("Redacting the message pointed to by my receipt leaves the room read", () => {}); - it.skip("Redacting a message after it was read makes the room unread", () => {}); + it("Redacting the message pointed to by my receipt leaves the room read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + // When I read the main timeline + goTo(room2); + assertRead(room2); + + goTo(room1); + receiveMessages(room2, [redactionOf("Msg2")]); + assertRead(room2); + }); it.skip("Reading an unread room after a redaction of the latest message makes it read", () => {}); it.skip("Reading an unread room after a redaction of an older message makes it read", () => {}); @@ -949,8 +1104,31 @@ describe("Read receipts", () => { }); describe("Ignored events", () => { - it.skip("If all events after receipt are unimportant, the room is read", () => {}); - it.skip("Sending an important event after unimportant ones makes the room unread", () => {}); + it("If all events after receipt are unimportant, the room is read", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + + receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]); + assertRead(room2); + }); + it("Sending an important event after unimportant ones makes the room unread", () => { + goTo(room1); + assertRead(room2); + receiveMessages(room2, ["Msg1", "Msg2"]); + assertUnread(room2, 2); + + markAsRead(room2); + + receiveMessages(room2, [customEvent("org.custom.event", { body: "foobar" })]); + assertRead(room2); + + receiveMessages(room2, ["Hello"]); + assertUnread(room2, 1); + }); it.skip("A receipt for the last unimportant event makes the room read, even if all are unimportant", () => {}); }); diff --git a/cypress/e2e/read-receipts/read-receipts.spec.ts b/cypress/e2e/read-receipts/read-receipts.spec.ts index a08862d4409..e298f7fa987 100644 --- a/cypress/e2e/read-receipts/read-receipts.spec.ts +++ b/cypress/e2e/read-receipts/read-receipts.spec.ts @@ -16,8 +16,7 @@ limitations under the License. /// -import type { MatrixClient, MatrixEvent, ISendEventResponse } from "matrix-js-sdk/src/matrix"; -import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; +import type { MatrixClient, MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix"; import { HomeserverInstance } from "../../plugins/utils/homeserver"; describe("Read receipts", () => { diff --git a/cypress/e2e/register/email.spec.ts b/cypress/e2e/register/email.spec.ts new file mode 100644 index 00000000000..988cee9ff21 --- /dev/null +++ b/cypress/e2e/register/email.spec.ts @@ -0,0 +1,93 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import { HomeserverInstance } from "../../plugins/utils/homeserver"; +import { Mailhog } from "../../support/mailhog"; + +describe("Email Registration", () => { + let homeserver: HomeserverInstance; + let mailhog: Mailhog; + + beforeEach(() => { + cy.startMailhog().then((_mailhog) => { + mailhog = _mailhog; + cy.startHomeserver({ + template: "email", + variables: { + SMTP_HOST: "{{HOST_DOCKER_INTERNAL}}", // This will get replaced in synapseStart + SMTP_PORT: _mailhog.instance.smtpPort, + }, + }).then((_homeserver) => { + homeserver = _homeserver; + + cy.intercept( + { method: "GET", pathname: "/config.json" }, + { + body: { + default_server_config: { + "m.homeserver": { + base_url: homeserver.baseUrl, + }, + "m.identity_server": { + base_url: "https://server.invalid", + }, + }, + }, + }, + ); + cy.visit("/#/register"); + cy.injectAxe(); + }); + }); + }); + + afterEach(() => { + cy.stopHomeserver(homeserver); + cy.stopMailhog(mailhog); + }); + + it("registers an account and lands on the use case selection screen", () => { + cy.findByRole("textbox", { name: "Username" }).should("be.visible"); + // Hide the server text as it contains the randomly allocated Homeserver port + const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }"; + + cy.findByRole("textbox", { name: "Username" }).type("alice"); + cy.findByPlaceholderText("Password").type("totally a great password"); + cy.findByPlaceholderText("Confirm password").type("totally a great password"); + cy.findByPlaceholderText("Email").type("alice@email.com"); + cy.findByRole("button", { name: "Register" }).click(); + + cy.findByText("Check your email to continue").should("be.visible"); + cy.percySnapshot("Registration check your email", { percyCSS }); + cy.checkA11y(); + + cy.findByText("An error was encountered when sending the email").should("not.exist"); + + cy.waitForPromise(async () => { + const messages = await mailhog.api.messages(); + expect(messages.items).to.have.length(1); + expect(messages.items[0].to).to.eq("alice@email.com"); + const [link] = messages.items[0].text.match(/http.+/); + return link; + }).as("emailLink"); + + cy.get("@emailLink").then((link) => cy.request(link)); + + cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist"); + }); +}); diff --git a/cypress/e2e/settings/general-user-settings-tab.spec.ts b/cypress/e2e/settings/general-user-settings-tab.spec.ts index 2879d6d9301..725caf2038e 100644 --- a/cypress/e2e/settings/general-user-settings-tab.spec.ts +++ b/cypress/e2e/settings/general-user-settings-tab.spec.ts @@ -133,10 +133,12 @@ describe("General user settings tab", () => { cy.findByRole("button", { name: "Language Dropdown" }).click(); // Assert that the default option is rendered and highlighted - cy.findByRole("option", { name: /Bahasa Indonesia/ }) + cy.findByRole("option", { name: /Albanian/ }) .should("be.visible") .should("have.class", "mx_Dropdown_option_highlight"); + cy.findByRole("option", { name: /Deutsch/ }).should("be.visible"); + // Click again to close the dropdown cy.findByRole("button", { name: "Language Dropdown" }).click(); @@ -230,7 +232,7 @@ describe("General user settings tab", () => { cy.closeDialog(); // Assert the avatar's initial characters are set - cy.get(".mx_UserMenu .mx_BaseAvatar_initial").findByText("A").should("exist"); // Alice - cy.get(".mx_RoomView_wrapper .mx_BaseAvatar_initial").findByText("A").should("exist"); // Alice + cy.get(".mx_UserMenu .mx_BaseAvatar").findByText("A").should("exist"); // Alice + cy.get(".mx_RoomView_wrapper .mx_BaseAvatar").findByText("A").should("exist"); // Alice }); }); diff --git a/cypress/e2e/spotlight/spotlight.spec.ts b/cypress/e2e/spotlight/spotlight.spec.ts index b85e1103984..d7ce14eff9e 100644 --- a/cypress/e2e/spotlight/spotlight.spec.ts +++ b/cypress/e2e/spotlight/spotlight.spec.ts @@ -218,20 +218,20 @@ describe("Spotlight", () => { cy.openSpotlightDialog().within(() => { cy.wait(1000); // wait for the dialog to settle, otherwise our keypresses might race with an update - // initially, publicrooms should be highlighted (because there are no other suggestions) - cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true"); + // initially, public spaces should be highlighted (because there are no other suggestions) + cy.get("#mx_SpotlightDialog_button_explorePublicSpaces").should("have.attr", "aria-selected", "true"); - // hitting enter should enable the publicrooms filter + // hitting enter should enable the public rooms filter cy.spotlightSearch().type("{enter}"); - cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms"); + cy.get(".mx_SpotlightDialog_filter").should("contain", "Public spaces"); cy.spotlightSearch().type("{backspace}"); cy.get(".mx_SpotlightDialog_filter").should("not.exist"); cy.spotlightSearch().type("{downArrow}"); cy.spotlightSearch().type("{downArrow}"); - cy.get("#mx_SpotlightDialog_button_startChat").should("have.attr", "aria-selected", "true"); + cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true"); cy.spotlightSearch().type("{enter}"); - cy.get(".mx_SpotlightDialog_filter").should("contain", "People"); + cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms"); cy.spotlightSearch().type("{backspace}"); cy.get(".mx_SpotlightDialog_filter").should("not.exist"); }); diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts index 35b6410d5b8..78fc6c63272 100644 --- a/cypress/e2e/threads/threads.spec.ts +++ b/cypress/e2e/threads/threads.spec.ts @@ -98,7 +98,7 @@ describe("Threads", () => { // Wait until the both messages are read cy.get(".mx_ThreadView .mx_EventTile_last[data-layout=group]").within(() => { cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); // Make sure the CSS style for spacing is applied to mx_EventTile_line on group/modern layout cy.get(".mx_EventTile_line").should("have.css", "padding-inline-start", ThreadViewGroupSpacingStart); @@ -118,7 +118,7 @@ describe("Threads", () => { cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); // Make sure the avatar inside ReadReceiptGroup is visible on the group layout - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); }); // Enable the bubble layout @@ -127,12 +127,12 @@ describe("Threads", () => { cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble'].mx_EventTile_last").within(() => { // TODO: remove this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout // See: https://github.com/vector-im/element-web/issues/23569 - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("exist"); + cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("exist"); // Make sure the avatar inside ReadReceiptGroup is visible on bubble layout // TODO: enable this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout // See: https://github.com/vector-im/element-web/issues/23569 - // cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar_image").should("be.visible"); + // cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); }); // Re-enable the group layout diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts index 42e23a2bc49..3d83a61b5ac 100644 --- a/cypress/e2e/timeline/timeline.spec.ts +++ b/cypress/e2e/timeline/timeline.spec.ts @@ -45,7 +45,7 @@ const expectDisplayName = (e: JQuery, displayName: string): void => const expectAvatar = (e: JQuery, avatarUrl: string): void => { cy.all([cy.window({ log: false }), cy.getClient()]).then(([win, cli]) => { const size = AVATAR_SIZE * win.devicePixelRatio; - expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal( + expect(e.find(".mx_BaseAvatar img").attr("src")).to.equal( // eslint-disable-next-line no-restricted-properties cli.mxcUrlToHttp(avatarUrl, size, size, AVATAR_RESIZE_METHOD), ); @@ -197,10 +197,10 @@ describe("Timeline", () => { cy.get(".mx_GenericEventListSummary").within(() => { // Click "expand" link button - cy.findByRole("button", { name: "expand" }).click(); + cy.findByRole("button", { name: "Expand" }).click(); // Assert that the "expand" link button worked - cy.findByRole("button", { name: "collapse" }).should("exist"); + cy.findByRole("button", { name: "Collapse" }).should("exist"); }); cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on IRC layout", { percyCSS }); @@ -224,10 +224,10 @@ describe("Timeline", () => { cy.get(".mx_GenericEventListSummary").within(() => { // Click "expand" link button - cy.findByRole("button", { name: "expand" }).click(); + cy.findByRole("button", { name: "Expand" }).click(); // Assert that the "expand" link button worked - cy.findByRole("button", { name: "collapse" }).should("exist"); + cy.findByRole("button", { name: "Collapse" }).should("exist"); }); cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on modern layout", { percyCSS }); @@ -247,10 +247,10 @@ describe("Timeline", () => { cy.get(".mx_GenericEventListSummary").within(() => { // Click "expand" link button - cy.findByRole("button", { name: "expand" }).click(); + cy.findByRole("button", { name: "Expand" }).click(); // Assert that the "expand" link button worked - cy.findByRole("button", { name: "collapse" }).should("exist"); + cy.findByRole("button", { name: "Collapse" }).should("exist"); }); // Make sure spacer is not visible on bubble layout @@ -270,10 +270,10 @@ describe("Timeline", () => { .realHover() .findByRole("toolbar", { name: "Message Actions" }) .should("be.visible"); - cy.findByRole("button", { name: "collapse" }).click(); + cy.findByRole("button", { name: "Collapse" }).click(); // Assert that "collapse" link button worked - cy.findByRole("button", { name: "expand" }).should("exist"); + cy.findByRole("button", { name: "Expand" }).should("exist"); }); // Save snapshot of collapsed generic event list summary on bubble layout @@ -292,7 +292,7 @@ describe("Timeline", () => { }); // Click "expand" link button - cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "expand" }).click(); + cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "Expand" }).click(); // Check the event line has margin instead of inset property // cf. _EventTile.pcss @@ -388,7 +388,7 @@ describe("Timeline", () => { // 2. Alignment of expanded GELS and messages // Click "expand" link button - cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "expand" }).click(); + cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "Expand" }).click(); // Check inline start spacing of info line on expanded GELS cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line") // See: _EventTile.pcss diff --git a/cypress/e2e/widgets/stickers.spec.ts b/cypress/e2e/widgets/stickers.spec.ts index 1a172055f97..d3e08f8405c 100644 --- a/cypress/e2e/widgets/stickers.spec.ts +++ b/cypress/e2e/widgets/stickers.spec.ts @@ -86,10 +86,10 @@ function expectTimelineSticker(roomId: string) { // Make sure it's in the right room cy.get(".mx_EventTile_sticker > a").should("have.attr", "href").and("include", `/${roomId}/`); - // Make sure the image points at the sticker image - cy.get(`img[alt="${STICKER_NAME}"]`) - .should("have.attr", "src") - .and("match", /thumbnail\/somewhere\?/); + // Make sure the image points at the sticker image. We will briefly show it + // using the thumbnail URL, but as soon as that fails, we will switch to the + // download URL. + cy.get(`img[alt="${STICKER_NAME}"][src*="download/somewhere"]`).should("exist"); } describe("Stickers", () => { diff --git a/cypress/global.d.ts b/cypress/global.d.ts index da5b3b8cd71..f8caad1f89e 100644 --- a/cypress/global.d.ts +++ b/cypress/global.d.ts @@ -17,6 +17,7 @@ limitations under the License. import "../src/@types/global"; import "../src/@types/svg"; import "../src/@types/raw-loader"; +// eslint-disable-next-line no-restricted-imports import "matrix-js-sdk/src/@types/global"; import type { MatrixClient, diff --git a/cypress/plugins/docker/index.ts b/cypress/plugins/docker/index.ts index 66bab0b8532..4c2da5f6457 100644 --- a/cypress/plugins/docker/index.ts +++ b/cypress/plugins/docker/index.ts @@ -156,6 +156,14 @@ export function isPodman(): Promise { }); } +/** + * Supply the right hostname to use to talk to the host machine. On Docker this + * is "host.docker.internal" and on Podman this is "host.containers.internal". + */ +export async function hostContainerName() { + return (await isPodman()) ? "host.containers.internal" : "host.docker.internal"; +} + /** * @type {Cypress.PluginConfig} */ diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index cb6d819dcd3..412057cf544 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -26,6 +26,7 @@ import { webserver } from "./webserver"; import { docker } from "./docker"; import { log } from "./log"; import { oAuthServer } from "./oauth_server"; +import { mailhogDocker } from "./mailhog"; /** * @type {Cypress.PluginConfig} @@ -41,4 +42,5 @@ export default function (on: PluginEvents, config: PluginConfigOptions) { installLogsPrinter(on, { // printLogsToConsole: "always", }); + mailhogDocker(on, config); } diff --git a/cypress/plugins/mailhog/index.ts b/cypress/plugins/mailhog/index.ts new file mode 100644 index 00000000000..a156e939818 --- /dev/null +++ b/cypress/plugins/mailhog/index.ts @@ -0,0 +1,91 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import PluginEvents = Cypress.PluginEvents; +import PluginConfigOptions = Cypress.PluginConfigOptions; +import { getFreePort } from "../utils/port"; +import { dockerIp, dockerRun, dockerStop } from "../docker"; + +// A cypress plugins to add command to manage an instance of Mailhog in Docker + +export interface Instance { + host: string; + smtpPort: number; + httpPort: number; + containerId: string; +} + +const instances = new Map(); + +// Start a synapse instance: the template must be the name of +// one of the templates in the cypress/plugins/synapsedocker/templates +// directory +async function mailhogStart(): Promise { + const smtpPort = await getFreePort(); + const httpPort = await getFreePort(); + + console.log(`Starting mailhog...`); + + const containerId = await dockerRun({ + image: "mailhog/mailhog:latest", + containerName: `react-sdk-cypress-mailhog`, + params: ["--rm", "-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`], + }); + + console.log(`Started mailhog on ports smtp=${smtpPort} http=${httpPort}.`); + + const host = await dockerIp({ containerId }); + const instance: Instance = { smtpPort, httpPort, containerId, host }; + instances.set(containerId, instance); + return instance; +} + +async function mailhogStop(id: string): Promise { + const synCfg = instances.get(id); + + if (!synCfg) throw new Error("Unknown mailhog ID"); + + await dockerStop({ + containerId: id, + }); + + instances.delete(id); + + console.log(`Stopped mailhog id ${id}.`); + // cypress deliberately fails if you return 'undefined', so + // return null to signal all is well, and we've handled the task. + return null; +} + +/** + * @type {Cypress.PluginConfig} + */ +export function mailhogDocker(on: PluginEvents, config: PluginConfigOptions) { + on("task", { + mailhogStart, + mailhogStop, + }); + + on("after:spec", async (spec) => { + // Cleans up any remaining instances after a spec run + for (const synId of instances.keys()) { + console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); + await mailhogStop(synId); + } + }); +} diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts index 9773849e030..7c278610cc1 100644 --- a/cypress/plugins/synapsedocker/index.ts +++ b/cypress/plugins/synapsedocker/index.ts @@ -24,7 +24,7 @@ import * as fse from "fs-extra"; import PluginEvents = Cypress.PluginEvents; import PluginConfigOptions = Cypress.PluginConfigOptions; import { getFreePort } from "../utils/port"; -import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker"; +import { dockerExec, dockerLogs, dockerRun, dockerStop, hostContainerName, isPodman } from "../docker"; import { HomeserverConfig, HomeserverInstance } from "../utils/homeserver"; import { StartHomeserverOpts } from "../../support/homeserver"; @@ -58,21 +58,41 @@ async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise ${outputHomeserver}`); + let hsYaml = await fse.readFile(templateHomeserver, "utf8"); hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret); hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl); hsYaml = hsYaml.replace(/{{OAUTH_SERVER_PORT}}/g, opts.oAuthServerPort?.toString()); - await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); + hsYaml = hsYaml.replace(/{{HOST_DOCKER_INTERNAL}}/g, await hostContainerName()); + if (opts.variables) { + let fetchedHostContainer = null; + for (const key in opts.variables) { + let value = String(opts.variables[key]); + + if (value === "{{HOST_DOCKER_INTERNAL}}") { + if (!fetchedHostContainer) { + fetchedHostContainer = await hostContainerName(); + } + value = fetchedHostContainer; + } + + hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), value); + } + } + + await fse.writeFile(outputHomeserver, hsYaml); // now generate a signing key (we could use synapse's config generation for // this, or we could just do this...) // NB. This assumes the homeserver.yaml specifies the key in this location const signingKey = randB64Bytes(32); - console.log(`Gen ${path.join(templateDir, "localhost.signing.key")}`); - await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`); + const outputSigningKey = path.join(tempDir, "localhost.signing.key"); + console.log(`Gen -> ${outputSigningKey}`); + await fse.writeFile(outputSigningKey, `ed25519 x ${signingKey}`); return { port, @@ -82,27 +102,38 @@ async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise { const synCfg = await cfgDirFromTemplate(opts); console.log(`Starting synapse with config dir ${synCfg.configDir}...`); + const dockerSynapseParams = ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`]; + + if (await isPodman()) { + // Make host.containers.internal work to allow Synapse to talk to the + // test OIDC server. + dockerSynapseParams.push("--network"); + dockerSynapseParams.push("slirp4netns:allow_host_loopback=true"); + } else { + // Make host.docker.internal work to allow Synapse to talk to the test + // OIDC server. + dockerSynapseParams.push("--add-host"); + dockerSynapseParams.push("host.docker.internal:host-gateway"); + } + const synapseId = await dockerRun({ image: "matrixdotorg/synapse:develop", containerName: `react-sdk-cypress-synapse`, - params: [ - "--rm", - "-v", - `${synCfg.configDir}:/data`, - "-p", - `${synCfg.port}:8008/tcp`, - // make host.docker.internal work to allow Synapse to talk to the test OIDC server - "--add-host", - "host.docker.internal:host-gateway", - ], + params: dockerSynapseParams, cmd: ["run"], }); diff --git a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml index a866d4b5be6..e51ac1918ff 100644 --- a/cypress/plugins/synapsedocker/templates/default/homeserver.yaml +++ b/cypress/plugins/synapsedocker/templates/default/homeserver.yaml @@ -81,9 +81,10 @@ oidc_providers: issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth" authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html" # the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container. - # Hence, host.docker.internal rather than localhost. - token_endpoint: "http://host.docker.internal:{{OAUTH_SERVER_PORT}}/oauth/token" - userinfo_endpoint: "http://host.docker.internal:{{OAUTH_SERVER_PORT}}/oauth/userinfo" + # Hence, HOST_DOCKER_INTERNAL rather than localhost. This is set to + # host.docker.internal on Docker and host.containers.internal on Podman. + token_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/token" + userinfo_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/userinfo" client_id: "synapse" discover: false scopes: ["profile"] diff --git a/cypress/plugins/synapsedocker/templates/email/README.md b/cypress/plugins/synapsedocker/templates/email/README.md new file mode 100644 index 00000000000..40c23ba0be4 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/README.md @@ -0,0 +1 @@ +A synapse configured to require an email for registration diff --git a/cypress/plugins/synapsedocker/templates/email/homeserver.yaml b/cypress/plugins/synapsedocker/templates/email/homeserver.yaml new file mode 100644 index 00000000000..fc20641ab40 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/homeserver.yaml @@ -0,0 +1,44 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: "{{PUBLIC_BASEURL}}" +listeners: + - port: 8008 + tls: false + bind_addresses: ["::"] + type: http + x_forwarded: true + + resources: + - names: [client] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +registrations_require_3pid: + - email +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true + +ui_auth: + session_timeout: "300s" + +email: + smtp_host: "%SMTP_HOST%" + smtp_port: %SMTP_PORT% + notif_from: "Your Friendly %(app)s homeserver " + app_name: my_branded_matrix_server diff --git a/cypress/plugins/synapsedocker/templates/email/log.config b/cypress/plugins/synapsedocker/templates/email/log.config new file mode 100644 index 00000000000..ac232762da3 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/email/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/support/client.ts b/cypress/support/client.ts index ad3c2e239f0..44bc1487af3 100644 --- a/cypress/support/client.ts +++ b/cypress/support/client.ts @@ -26,8 +26,8 @@ import type { UploadOpts, ICreateRoomOpts, ISendEventResponse, + ReceiptType, } from "matrix-js-sdk/src/matrix"; -import type { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; import Chainable = Cypress.Chainable; import { UserCredentials } from "./login"; diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 2ff0197ba65..11eae401f9e 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -40,6 +40,8 @@ import "./network"; import "./composer"; import "./proxy"; import "./axe"; +import "./mailhog"; +import "./promise"; installLogsCollector({ // specify the types of logs to collect (and report to the node console at the end of the test) diff --git a/cypress/support/homeserver.ts b/cypress/support/homeserver.ts index 15fce60350b..2e8a309a65e 100644 --- a/cypress/support/homeserver.ts +++ b/cypress/support/homeserver.ts @@ -28,6 +28,9 @@ export interface StartHomeserverOpts { /** Port of an OAuth server to configure the homeserver to use */ oAuthServerPort?: number; + + /** Additional variables to inject into the configuration template **/ + variables?: Record; } declare global { @@ -36,15 +39,22 @@ declare global { interface Chainable { /** * Start a homeserver instance with a given config template. + * * @param opts: either the template path (within cypress/plugins/{homeserver}docker/template/), or * an options object + * + * If any of opts.variables has the special value + * '{{HOST_DOCKER_INTERNAL}}', it will be replaced by + * 'host.docker.interal' if we are on Docker, or + * 'host.containers.internal' on Podman. */ startHomeserver(opts: string | StartHomeserverOpts): Chainable; /** * Custom command wrapping task:{homeserver}Stop whilst preventing uncaught exceptions * for if Homeserver stopping races with the app's background sync loop. - * @param homeserver the homeserver instance returned by start{Homeserver} + * + * @param homeserver the homeserver instance returned by {homeserver}Start (e.g. synapseStart). */ stopHomeserver(homeserver: HomeserverInstance): Chainable; diff --git a/cypress/support/mailhog.ts b/cypress/support/mailhog.ts new file mode 100644 index 00000000000..86efc5e0f66 --- /dev/null +++ b/cypress/support/mailhog.ts @@ -0,0 +1,54 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import mailhog from "mailhog"; + +import Chainable = Cypress.Chainable; +import { Instance } from "../plugins/mailhog"; + +export interface Mailhog { + api: mailhog.API; + instance: Instance; +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + startMailhog(): Chainable; + stopMailhog(instance: Mailhog): Chainable; + } + } +} + +Cypress.Commands.add("startMailhog", (): Chainable => { + return cy.task("mailhogStart", { log: false }).then((x) => { + Cypress.log({ name: "startHomeserver", message: `Started mailhog instance ${x.containerId}` }); + return { + api: mailhog({ + host: "localhost", + port: x.httpPort, + }), + instance: x, + }; + }); +}); + +Cypress.Commands.add("stopMailhog", (mailhog: Mailhog): Chainable => { + return cy.task("mailhogStop", mailhog.instance.containerId); +}); diff --git a/cypress/support/promise.ts b/cypress/support/promise.ts new file mode 100644 index 00000000000..4baaf75e8ea --- /dev/null +++ b/cypress/support/promise.ts @@ -0,0 +1,58 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import Chainable = Cypress.Chainable; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + /** + * Utility wrapper around promises to help control flow in tests + * Calls `fn` function `tries` times, with a sleep of `interval` between calls. + * Ensure you do not rely on any effects of calling any `cy.*` functions within the body of `fn` + * as the calls will not happen until after waitForPromise returns. + * @param fn the function to retry + * @param tries the number of tries to call it + * @param interval the time interval between tries + */ + waitForPromise(fn: () => Promise, tries?: number, interval?: number): Chainable; + } + } +} + +function waitForPromise(fn: () => Promise, tries = 10, interval = 1000): Chainable { + return cy.then( + () => + new Cypress.Promise(async (resolve, reject) => { + for (let i = 0; i < tries; i++) { + try { + const v = await fn(); + resolve(v); + } catch { + await new Cypress.Promise((resolve) => setTimeout(resolve, interval)); + } + } + reject(); + }), + ); +} + +Cypress.Commands.add("waitForPromise", waitForPromise); + +export {}; diff --git a/cypress/support/views.ts b/cypress/support/views.ts index f31c2d7bf2a..c610af5f8b7 100644 --- a/cypress/support/views.ts +++ b/cypress/support/views.ts @@ -23,11 +23,11 @@ declare global { namespace Cypress { interface Chainable { /** - * Opens the given room by name. The room must be visible in the room list. - * It uses a start-anchored regexp to accommodate for room tiles for unread rooms containing additional - * context in their aria labels, e.g. "Room name 3 unread messages." + * Opens the given room by name. The room must be visible in the + * room list, but the room list may be folded horizontally, and the + * room may contain unread messages. * - * @param name The room name to find and click on/open. + * @param name The exact room name to find and click on/open. */ viewRoomByName(name: string): Chainable>; @@ -65,10 +65,20 @@ declare global { } Cypress.Commands.add("viewRoomByName", (name: string): Chainable> => { - return cy - .findByRole("treeitem", { name: new RegExp("^" + name) }) - .should("have.class", "mx_RoomTile") - .click(); + // We look for the room inside the room list, which is a tree called Rooms. + // + // There are 3 cases: + // - the room list is folded: + // then the aria-label on the room tile is the name (with nothing extra) + // - the room list is unfolder and the room has messages: + // then the aria-label contains the unread count, but the title of the + // div inside the titleContainer equals the room name + // - the room list is unfolded and the room has no messages: + // then the aria-label is the name and so is the title of a div + // + // So by matching EITHER title=name OR aria-label=name we find this exact + // room in all three cases. + return cy.findByRole("tree", { name: "Rooms" }).find(`[title="${name}"],[aria-label="${name}"]`).first().click(); }); Cypress.Commands.add("viewRoomById", (id: string): void => { diff --git a/docs/settings.md b/docs/settings.md index b8a575ff5ae..3f0636d3801 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -174,7 +174,7 @@ An example of a watcher in action would be: class MyComponent extends React.Component { settingWatcherRef = null; - componentWillMount() { + componentDidMount() { const callback = (settingName, roomId, level, newValAtLevel, newVal) => { this.setState({ color: newVal }); }; diff --git a/jest.config.ts b/jest.config.ts index d7f00f194eb..58bec7684e8 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -36,7 +36,12 @@ const config: Config = { "RecorderWorklet": "/__mocks__/empty.js", }, transformIgnorePatterns: ["/node_modules/(?!matrix-js-sdk).+$"], - collectCoverageFrom: ["/src/**/*.{js,ts,tsx}"], + collectCoverageFrom: [ + "/src/**/*.{js,ts,tsx}", + // getSessionLock is piped into a different JS context via stringification, and the coverage functionality is + // not available in that contest. So, turn off coverage instrumentation for it. + "!/src/utils/SessionLock.ts", + ], coverageReporters: ["text-summary", "lcov"], testResultsProcessor: "@casualbot/jest-sonar-reporter", }; diff --git a/package.json b/package.json index 9dfbdbf53bf..4ba9c8bb149 100644 --- a/package.json +++ b/package.json @@ -55,18 +55,20 @@ "coverage": "yarn test --coverage" }, "resolutions": { - "@types/react-dom": "17.0.19", - "@types/react": "17.0.58" + "@types/react-dom": "17.0.20", + "@types/react": "17.0.65" }, "dependencies": { "@babel/runtime": "^7.12.5", - "@matrix-org/analytics-events": "^0.6.0", + "@matrix-org/analytics-events": "^0.7.0", + "@matrix-org/emojibase-bindings": "^1.1.2", "@matrix-org/matrix-wysiwyg": "^2.4.1", - "@matrix-org/react-sdk-module-api": "^1.0.0", + "@matrix-org/react-sdk-module-api": "^2.1.0", + "@matrix-org/spec": "^1.7.0", "@sentry/browser": "^7.0.0", "@sentry/tracing": "^7.0.0", "@testing-library/react-hooks": "^8.0.1", - "@vector-im/compound-design-tokens": "^0.0.3", + "@vector-im/compound-design-tokens": "^0.0.4", "@vector-im/compound-web": "^0.2.3", "await-lock": "^2.1.0", "blurhash": "^1.1.3", @@ -75,8 +77,6 @@ "counterpart": "^0.18.6", "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase": "15.0.0", - "emojibase-data": "15.0.0", "emojibase-regex": "15.0.0", "escape-html": "^1.0.3", "file-saver": "^2.0.5", @@ -121,6 +121,7 @@ "sanitize-html": "2.11.0", "tar-js": "^0.3.0", "ua-parser-js": "^1.0.2", + "uuid": "^9.0.0", "what-input": "^5.2.10", "zxcvbn": "^4.4.2" }, @@ -146,7 +147,7 @@ "@percy/cli": "^1.11.0", "@percy/cypress": "^3.1.2", "@testing-library/cypress": "^9.0.0", - "@testing-library/jest-dom": "^5.16.5", + "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.4.3", "@types/commonmark": "^0.27.4", @@ -158,17 +159,18 @@ "@types/fs-extra": "^11.0.0", "@types/geojson": "^7946.0.8", "@types/glob-to-regexp": "^0.4.1", - "@types/jest": "29.2.6", + "@types/jest": "29.5.3", "@types/katex": "^0.16.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", "@types/node": "^16", "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", + "@types/prettier": "^2.7.0", "@types/qrcode": "^1.3.5", - "@types/react": "17.0.58", + "@types/react": "17.0.65", "@types/react-beautiful-dnd": "^13.0.0", - "@types/react-dom": "17.0.19", + "@types/react-dom": "17.0.20", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "2.9.0", "@types/sdp-transform": "^2.4.6", @@ -190,7 +192,7 @@ "cypress-terminal-report": "^5.3.2", "eslint": "8.45.0", "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^8.5.0", + "eslint-config-prettier": "^9.0.0", "eslint-plugin-deprecate": "^0.7.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.2.1", @@ -202,14 +204,15 @@ "express": "^4.18.2", "fetch-mock-jest": "^1.5.1", "fs-extra": "^11.0.0", - "jest": "29.3.1", - "jest-canvas-mock": "2.4.0", - "jest-environment-jsdom": "^29.2.2", - "jest-mock": "^29.2.2", + "jest": "^29.6.2", + "jest-canvas-mock": "^2.5.2", + "jest-environment-jsdom": "^29.6.2", + "jest-mock": "^29.6.2", "jest-raw-loader": "^1.0.1", "jsqr": "^1.4.0", + "mailhog": "^4.16.0", "matrix-mock-request": "^2.5.0", - "matrix-web-i18n": "^1.4.0", + "matrix-web-i18n": "^2.1.0", "mocha-junit-reporter": "^2.2.0", "node-fetch": "2", "postcss-scss": "^4.0.4", diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 37ce247a366..3b46ee5fda9 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -52,6 +52,8 @@ @import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; @import "./components/views/typography/_Caption.pcss"; +@import "./components/views/utils/_Box.pcss"; +@import "./components/views/utils/_Flex.pcss"; @import "./compound/_Icon.pcss"; @import "./compound/_SuccessDialog.pcss"; @import "./structures/_AutoHideScrollbar.pcss"; @@ -88,8 +90,10 @@ @import "./structures/_UserMenu.pcss"; @import "./structures/_ViewSource.pcss"; @import "./structures/auth/_CompleteSecurity.pcss"; +@import "./structures/auth/_ConfirmSessionLockTheftView.pcss"; @import "./structures/auth/_Login.pcss"; @import "./structures/auth/_Registration.pcss"; +@import "./structures/auth/_SessionLockStolenView.pcss"; @import "./structures/auth/_SetupEncryptionBody.pcss"; @import "./views/audio_messages/_AudioPlayer.pcss"; @import "./views/audio_messages/_PlayPauseButton.pcss"; @@ -188,6 +192,7 @@ @import "./views/elements/_InteractiveTooltip.pcss"; @import "./views/elements/_InviteReason.pcss"; @import "./views/elements/_LabelledCheckbox.pcss"; +@import "./views/elements/_LanguageDropdown.pcss"; @import "./views/elements/_MiniAvatarUploader.pcss"; @import "./views/elements/_Pill.pcss"; @import "./views/elements/_PowerSelector.pcss"; @@ -210,7 +215,6 @@ @import "./views/elements/_TextWithTooltip.pcss"; @import "./views/elements/_ToggleSwitch.pcss"; @import "./views/elements/_Tooltip.pcss"; -@import "./views/elements/_TooltipButton.pcss"; @import "./views/elements/_UseCaseSelection.pcss"; @import "./views/elements/_UseCaseSelectionButton.pcss"; @import "./views/elements/_Validation.pcss"; @@ -341,6 +345,7 @@ @import "./views/settings/tabs/_SettingsSection.pcss"; @import "./views/settings/tabs/_SettingsTab.pcss"; @import "./views/settings/tabs/room/_NotificationSettingsTab.pcss"; +@import "./views/settings/tabs/room/_PeopleRoomSettingsTab.pcss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.pcss"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.pcss"; @import "./views/settings/tabs/user/_AppearanceUserSettingsTab.pcss"; @@ -379,7 +384,6 @@ @import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_VideoFeed.pcss"; @import "./voice-broadcast/atoms/_LiveBadge.pcss"; -@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss"; @import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss"; diff --git a/res/css/components/views/utils/_Box.pcss b/res/css/components/views/utils/_Box.pcss new file mode 100644 index 00000000000..a8ab7e94557 --- /dev/null +++ b/res/css/components/views/utils/_Box.pcss @@ -0,0 +1,27 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_Box--flex { + flex: var(--mx-box-flex, unset); +} + +.mx_Box--shrink { + flex-shrink: var(--mx-box-shrink, unset); +} + +.mx_Box--grow { + flex-grow: var(--mx-box-grow, unset); +} diff --git a/res/css/components/views/utils/_Flex.pcss b/res/css/components/views/utils/_Flex.pcss new file mode 100644 index 00000000000..f9cdc7e3cc4 --- /dev/null +++ b/res/css/components/views/utils/_Flex.pcss @@ -0,0 +1,23 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_Flex { + display: var(--mx-flex-display, unset); + flex-direction: var(--mx-flex-direction, unset); + align-items: var(--mx-flex-align, unset); + justify-content: var(--mx-flex-justify, unset); + gap: var(--mx-flex-gap, unset); +} diff --git a/res/css/structures/_RightPanel.pcss b/res/css/structures/_RightPanel.pcss index 7d39968c11a..7649ce25721 100644 --- a/res/css/structures/_RightPanel.pcss +++ b/res/css/structures/_RightPanel.pcss @@ -111,8 +111,4 @@ limitations under the License. margin-right: 8px; vertical-align: middle; } - - .mx_BaseAvatar_image { - border-radius: 8px; - } } diff --git a/res/css/structures/_RoomStatusBar.pcss b/res/css/structures/_RoomStatusBar.pcss index e046e5f7fd7..d0bfa9f7f5d 100644 --- a/res/css/structures/_RoomStatusBar.pcss +++ b/res/css/structures/_RoomStatusBar.pcss @@ -25,16 +25,6 @@ limitations under the License. text-align: left; } -.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_image { - margin-right: -12px; - border: 1px solid $background; -} - -.mx_RoomStatusBar_typingIndicatorAvatars .mx_BaseAvatar_initial { - padding-left: 1px; - padding-top: 1px; -} - .mx_RoomStatusBar_typingIndicatorRemaining { display: inline-block; color: #acacac; diff --git a/res/css/structures/_SpaceHierarchy.pcss b/res/css/structures/_SpaceHierarchy.pcss index 81498af176a..ca129fdbac9 100644 --- a/res/css/structures/_SpaceHierarchy.pcss +++ b/res/css/structures/_SpaceHierarchy.pcss @@ -108,12 +108,6 @@ limitations under the License. } } - .mx_SpaceHierarchy_subspace { - .mx_BaseAvatar_image { - border-radius: 8px; - } - } - .mx_SpaceHierarchy_subspace_toggle { position: absolute; left: -1px; diff --git a/res/css/structures/_SpacePanel.pcss b/res/css/structures/_SpacePanel.pcss index 76c328fa568..02f6f50363d 100644 --- a/res/css/structures/_SpacePanel.pcss +++ b/res/css/structures/_SpacePanel.pcss @@ -232,10 +232,6 @@ limitations under the License. transform: rotate(45deg); } - .mx_BaseAvatar_image { - border-radius: 8px; - } - .mx_SpaceButton_menuButton { width: 20px; min-width: 20px; /* yay flex */ @@ -269,19 +265,6 @@ limitations under the License. min-width: 0; flex-grow: 1; - .mx_BaseAvatar:not(.mx_UserMenu_userAvatar_BaseAvatar) .mx_BaseAvatar_initial { - color: $secondary-content; - border-radius: 8px; - background-color: $panel-actions; - font-size: $font-15px !important; /* override inline style */ - font-weight: var(--cpd-font-weight-semibold); - line-height: $font-18px; - - & + .mx_BaseAvatar_image { - visibility: hidden; - } - } - .mx_SpaceTreeLevel { // Indent subspaces padding-left: 16px; @@ -290,6 +273,7 @@ limitations under the License. .mx_SpaceButton_avatarWrapper { position: relative; + line-height: 0; } .mx_SpacePanel_badgeContainer { diff --git a/res/css/structures/_SpaceRoomView.pcss b/res/css/structures/_SpaceRoomView.pcss index f1bf0cf2141..dff60c3fb5d 100644 --- a/res/css/structures/_SpaceRoomView.pcss +++ b/res/css/structures/_SpaceRoomView.pcss @@ -143,10 +143,6 @@ limitations under the License. .mx_BaseAvatar { width: 80px; - - .mx_BaseAvatar_image { - border-radius: 12px; - } } } diff --git a/res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss b/res/css/structures/auth/_ConfirmSessionLockTheftView.pcss similarity index 70% rename from res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss rename to res/css/structures/auth/_ConfirmSessionLockTheftView.pcss index fd2c3ad73c5..14b92daaa81 100644 --- a/res/css/voice-broadcast/atoms/_PlaybackControlButton.pcss +++ b/res/css/structures/auth/_ConfirmSessionLockTheftView.pcss @@ -1,5 +1,5 @@ /* -Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2019-2023 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. @@ -14,13 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BroadcastPlaybackControlButton { - align-items: center; - background-color: $background; - border-radius: 50%; +.mx_ConfirmSessionLockTheftView { + width: 100%; + height: 100%; display: flex; - height: 32px; + align-items: center; justify-content: center; - margin-bottom: $spacing-8; - width: 32px; +} + +.mx_ConfirmSessionLockTheftView_body { + display: flex; + flex-direction: column; + max-width: 400px; + align-items: center; } diff --git a/res/css/structures/auth/_SessionLockStolenView.pcss b/res/css/structures/auth/_SessionLockStolenView.pcss new file mode 100644 index 00000000000..e9ab0d95ffa --- /dev/null +++ b/res/css/structures/auth/_SessionLockStolenView.pcss @@ -0,0 +1,30 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SessionLockStolenView { + h1 { + font-weight: var(--cpd-font-weight-semibold); + font-size: $font-32px; + text-align: center; + } + + h2 { + margin: 0; + font-weight: 500; + font-size: $font-24px; + text-align: center; + } +} diff --git a/res/css/views/avatars/_BaseAvatar.pcss b/res/css/views/avatars/_BaseAvatar.pcss index 70c41d0b251..52fd8452d3e 100644 --- a/res/css/views/avatars/_BaseAvatar.pcss +++ b/res/css/views/avatars/_BaseAvatar.pcss @@ -14,57 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_BaseAvatar { - position: relative; - /* In at least Firefox, the case of relative positioned inline elements */ - /* (such as mx_BaseAvatar) with absolute positioned children (such as */ - /* mx_BaseAvatar_initial) is a dark corner full of spider webs. It will give */ - /* different results during full reflow of the page vs. incremental reflow */ - /* of small portions. While that's surely a browser bug, we can avoid it by */ - /* using `inline-block` instead of the default `inline`. */ - /* https://github.com/vector-im/element-web/issues/5594 */ - /* https://bugzilla.mozilla.org/show_bug.cgi?id=1535053 */ - /* https://bugzilla.mozilla.org/show_bug.cgi?id=255139 */ - display: inline-block; - user-select: none; - - &.mx_RoomAvatar_isSpaceRoom { - &.mx_BaseAvatar_image, - .mx_BaseAvatar_image { - border-radius: 8px; - } - } -} - -.mx_BaseAvatar_initial { - position: absolute; - left: 0; - color: $avatar-initial-color; - text-align: center; - speak: none; - pointer-events: none; - font-weight: normal; -} - -.mx_BaseAvatar_image { - object-fit: cover; - aspect-ratio: 1; - border-radius: 125px; - vertical-align: top; - background-color: $background; -} - /* Percy screenshot test specific CSS */ @media only percy { /* Stick the default room avatar colour, so it doesn't cause a false diff on the screenshot */ - .mx_BaseAvatar_initial { + .mx_BaseAvatar { background-color: var(--percy-color-avatar) !important; - border-radius: 125px; - } - .mx_RoomAvatar_isSpaceRoom .mx_BaseAvatar_initial { - border-radius: 8px; - } - .mx_BaseAvatar_initial + .mx_BaseAvatar_image { - visibility: hidden; + color: white !important; } } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.pcss b/res/css/views/avatars/_DecoratedRoomAvatar.pcss index a0770c3ca0b..2d430981a35 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.pcss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.pcss @@ -18,6 +18,7 @@ limitations under the License. .mx_ExtraTile { position: relative; contain: content; + line-height: 0; &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { mask-image: url("$(res)/img/element-icons/roomlist/decorated-avatar-mask.svg"); @@ -29,10 +30,9 @@ limitations under the License. .mx_DecoratedRoomAvatar_icon { position: absolute; /* the following percentage based sizings are to match the scalable svg mask for the cutout */ - bottom: -6.25%; - right: -6.25%; - margin: 12.5%; - width: 25%; + bottom: 6.25%; // 2px for a 32x32 avatar + right: 6.25%; + width: 25%; // 8px for a 32x32 avatar height: 25%; border-radius: 50%; } diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss index 7866bac1c11..25e75911670 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.pcss @@ -157,12 +157,6 @@ limitations under the License. .mx_SubspaceSelector { display: flex; - .mx_BaseAvatar_image { - border-radius: 8px; - margin: 0; - vertical-align: unset; - } - .mx_BaseAvatar { display: inline-flex; margin: auto 16px auto 5px; @@ -228,16 +222,10 @@ limitations under the License. display: flex; margin-top: 12px; - .mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ - .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom { + .mx_DecoratedRoomAvatar, /* we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling */ { margin-right: 12px; } - img.mx_RoomAvatar_isSpaceRoom, - .mx_RoomAvatar_isSpaceRoom img { - border-radius: 8px; - } - .mx_AddExistingToSpace_entry_name { font-size: $font-15px; line-height: 30px; diff --git a/res/css/views/dialogs/_CompoundDialog.pcss b/res/css/views/dialogs/_CompoundDialog.pcss index 6777b4f81d4..70ba1f8c10c 100644 --- a/res/css/views/dialogs/_CompoundDialog.pcss +++ b/res/css/views/dialogs/_CompoundDialog.pcss @@ -58,12 +58,13 @@ limitations under the License. display: flex; flex-direction: column; min-height: 0; - max-height: 100%; + flex: 1; } .mx_CompoundDialog_content { overflow: auto; padding: 8px 32px; + flex: 1; } .mx_CompoundDialog_footer { diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss index 1082e500055..501d7a2aaf6 100644 --- a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.pcss @@ -66,11 +66,6 @@ limitations under the License. flex-grow: 1; } - img.mx_RoomAvatar_isSpaceRoom, - .mx_RoomAvatar_isSpaceRoom img { - border-radius: 4px; - } - .mx_ManageRestrictedJoinRuleDialog_entry_name { margin: 0 8px; font-size: $font-15px; @@ -98,10 +93,6 @@ limitations under the License. .mx_BaseAvatar { margin-right: 12px; } - - .mx_BaseAvatar_image { - border-radius: 8px; - } } .mx_ManageRestrictedJoinRuleDialog_section_info { diff --git a/res/css/views/dialogs/_RoomSettingsDialog.pcss b/res/css/views/dialogs/_RoomSettingsDialog.pcss index 3c2cfa3e1a9..78feacce114 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.pcss +++ b/res/css/views/dialogs/_RoomSettingsDialog.pcss @@ -53,6 +53,10 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/room/message-bar/emoji.svg"); } +.mx_RoomSettingsDialog_peopleIcon::before { + mask-image: url("$(res)/img/element-icons/group-members.svg"); +} + .mx_RoomSettingsDialog .mx_Dialog_title { -ms-text-overflow: ellipsis; text-overflow: ellipsis; diff --git a/res/css/views/dialogs/_SpotlightDialog.pcss b/res/css/views/dialogs/_SpotlightDialog.pcss index 4e811ecef10..5c2f5b67174 100644 --- a/res/css/views/dialogs/_SpotlightDialog.pcss +++ b/res/css/views/dialogs/_SpotlightDialog.pcss @@ -97,7 +97,11 @@ limitations under the License. } &.mx_SpotlightDialog_filterPublicRooms::before { - mask-image: url("$(res)/img/element-icons/roomlist/explore.svg"); + mask-image: url("$(res)/img/element-icons/roomlist/hash-circle.svg"); + } + + &.mx_SpotlightDialog_filterPublicSpaces::before { + mask-image: url("$(res)/img/element-icons/spaces.svg"); } .mx_SpotlightDialog_filter--close { @@ -408,6 +412,7 @@ limitations under the License. .mx_SpotlightDialog_startChat, .mx_SpotlightDialog_joinRoomAlias, .mx_SpotlightDialog_explorePublicRooms, + .mx_SpotlightDialog_explorePublicSpaces, .mx_SpotlightDialog_startGroupChat { padding-left: $spacing-32; position: relative; @@ -436,7 +441,11 @@ limitations under the License. } .mx_SpotlightDialog_explorePublicRooms::before { - mask-image: url("$(res)/img/element-icons/roomlist/explore.svg"); + mask-image: url("$(res)/img/element-icons/roomlist/hash-circle.svg"); + } + + .mx_SpotlightDialog_explorePublicSpaces::before { + mask-image: url("$(res)/img/element-icons/spaces.svg"); } .mx_SpotlightDialog_startGroupChat::before { diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index 35a5287fa9e..172d8fc053f 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -20,6 +20,8 @@ limitations under the License. &.mx_AccessibleButton_disabled { cursor: not-allowed; + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline, &.mx_AccessibleButton_kind_primary_sm, @@ -80,29 +82,37 @@ limitations under the License. } } - &.mx_AccessibleButton_kind_icon { + &.mx_AccessibleButton_kind_icon, + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline { padding: 0; height: 32px; width: 32px; } } + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline, &.mx_AccessibleButton_kind_secondary { font-weight: var(--cpd-font-weight-semibold); } + &.mx_AccessibleButton_kind_icon_primary, + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary, &.mx_AccessibleButton_kind_primary_outline { border: 1px solid $accent; } + &.mx_AccessibleButton_kind_icon_primary, &.mx_AccessibleButton_kind_primary { color: $button-primary-fg-color; background-color: $accent; } + &.mx_AccessibleButton_kind_icon_primary_outline, &.mx_AccessibleButton_kind_primary_outline { color: $accent; } diff --git a/res/css/views/elements/_FacePile.pcss b/res/css/views/elements/_FacePile.pcss index 03b736a73e8..2976873b1aa 100644 --- a/res/css/views/elements/_FacePile.pcss +++ b/res/css/views/elements/_FacePile.pcss @@ -29,14 +29,10 @@ limitations under the License. margin-right: -8px; } - .mx_BaseAvatar_image { + .mx_BaseAvatar { border: 1px solid var(--facepile-background, $background); } - .mx_BaseAvatar_initial { - margin: 1px; /* to offset the border on the image */ - } - .mx_FacePile_more { position: relative; border-radius: 100%; diff --git a/res/css/views/elements/_GenericEventListSummary.pcss b/res/css/views/elements/_GenericEventListSummary.pcss index cfb62d0d37b..f05a15b44d2 100644 --- a/res/css/views/elements/_GenericEventListSummary.pcss +++ b/res/css/views/elements/_GenericEventListSummary.pcss @@ -31,6 +31,11 @@ limitations under the License. } } + .mx_GenericEventListSummary_toggle { + // We reuse a title cased translation + text-transform: lowercase; + } + &[data-layout="irc"], &[data-layout="group"] { .mx_GenericEventListSummary_toggle { diff --git a/res/css/views/elements/_LanguageDropdown.pcss b/res/css/views/elements/_LanguageDropdown.pcss new file mode 100644 index 00000000000..60f406af73a --- /dev/null +++ b/res/css/views/elements/_LanguageDropdown.pcss @@ -0,0 +1,21 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_LanguageDropdown { + .mx_Dropdown_option > div { + text-transform: capitalize; + } +} diff --git a/res/css/views/elements/_MiniAvatarUploader.pcss b/res/css/views/elements/_MiniAvatarUploader.pcss index b28886a103b..01616cd3dde 100644 --- a/res/css/views/elements/_MiniAvatarUploader.pcss +++ b/res/css/views/elements/_MiniAvatarUploader.pcss @@ -43,6 +43,8 @@ limitations under the License. border-radius: 50%; z-index: 1; + line-height: 0; + .mx_MiniAvatarUploader_cameraIcon { height: 100%; width: 100%; diff --git a/res/css/views/elements/_TooltipButton.pcss b/res/css/views/elements/_TooltipButton.pcss deleted file mode 100644 index cc91c6d112c..00000000000 --- a/res/css/views/elements/_TooltipButton.pcss +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2017 New Vector Ltd. -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_TooltipButton { - display: inline-block; - width: 11px; - height: 11px; - margin-left: 5px; - - border: 2px solid $neutral-badge-color; - border-radius: 20px; - color: $neutral-badge-color; - - transition: opacity 0.2s ease-in; - opacity: 0.6; - - line-height: $font-11px; - text-align: center; - - cursor: pointer; -} - -.mx_TooltipButton:hover { - opacity: 1; -} - -.mx_TooltipButton_container { - position: relative; - top: -18px; - left: 4px; -} - -.mx_TooltipButton_helpText { - width: 400px; - text-align: start; - line-height: 17px !important; -} diff --git a/res/css/views/messages/_DateSeparator.pcss b/res/css/views/messages/_DateSeparator.pcss index 0a25cccaafd..52d263f6888 100644 --- a/res/css/views/messages/_DateSeparator.pcss +++ b/res/css/views/messages/_DateSeparator.pcss @@ -40,6 +40,7 @@ limitations under the License. font-size: inherit; font-weight: inherit; color: inherit; + text-transform: capitalize; } .mx_DateSeparator_jumpToDateMenu { diff --git a/res/css/views/right_panel/_RoomSummaryCard.pcss b/res/css/views/right_panel/_RoomSummaryCard.pcss index 4bd5f2ee0ff..0c11cab73b4 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.pcss +++ b/res/css/views/right_panel/_RoomSummaryCard.pcss @@ -123,11 +123,6 @@ limitations under the License. text-overflow: ellipsis; overflow: hidden; - .mx_BaseAvatar_image { - vertical-align: top; - margin-right: 12px; - } - span { color: $primary-content; } diff --git a/res/css/views/right_panel/_UserInfo.pcss b/res/css/views/right_panel/_UserInfo.pcss index 12d74915a2d..c0b8f16c6b6 100644 --- a/res/css/views/right_panel/_UserInfo.pcss +++ b/res/css/views/right_panel/_UserInfo.pcss @@ -95,49 +95,14 @@ limitations under the License. .mx_UserInfo_avatar_transition { max-width: 30vh; + aspect-ratio: 1 / 1; margin: 0 auto; transition: 0.5s; - .mx_UserInfo_avatar_transition_child { - /* use padding-top instead of height to make this element square, - as the % in padding is a % of the width (including margin, - that's why we had to put the margin to center on a parent div), - and not a % of the parent height. */ - padding-top: 100%; - position: relative; - - .mx_BaseAvatar, - .mx_BaseAvatar_initial, - .mx_BaseAvatar_image { - border-radius: 100%; - position: absolute; - top: 0; - left: 0; - width: 100% !important; - height: 100% !important; - } - - .mx_BaseAvatar { - &.mx_BaseAvatar_image { - cursor: zoom-in; - } - - .mx_BaseAvatar_initial { - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - - /* override the calculated sizes so that the letter isn't HUGE */ - font-size: 6rem !important; - width: 100% !important; - transition: font-size 0.5s; - - & + .mx_BaseAvatar_image { - cursor: default; - } - } - } + .mx_BaseAvatar, + .mx_BaseAvatar img { + width: 100%; + height: 100%; } } } @@ -285,14 +250,6 @@ limitations under the License. max-width: 72px; margin: 0 auto; } - - .mx_UserInfo_avatar_transition_child { - .mx_BaseAvatar { - .mx_BaseAvatar_initial { - font-size: 40px !important; /* override the other override because here the avatar is smaller */ - } - } - } } } } diff --git a/res/css/views/rooms/_EntityTile.pcss b/res/css/views/rooms/_EntityTile.pcss index a2ce91037d9..9632946bd59 100644 --- a/res/css/views/rooms/_EntityTile.pcss +++ b/res/css/views/rooms/_EntityTile.pcss @@ -67,6 +67,7 @@ limitations under the License. padding-top: 4px; padding-bottom: 4px; position: relative; + line-height: 0; } .mx_EntityTile_name { diff --git a/res/css/views/rooms/_EventTile.pcss b/res/css/views/rooms/_EventTile.pcss index 7d68dc98d9e..8b8d370ac75 100644 --- a/res/css/views/rooms/_EventTile.pcss +++ b/res/css/views/rooms/_EventTile.pcss @@ -571,6 +571,7 @@ $left-gutter: 64px; .mx_EventTile_avatar, .mx_EventTile_e2eIcon { + line-height: 1; margin: $spacing-block-start 0 $spacing-block-end; } diff --git a/res/css/views/rooms/_LegacyRoomHeader.pcss b/res/css/views/rooms/_LegacyRoomHeader.pcss index 17f1dfec912..1e36e0887b7 100644 --- a/res/css/views/rooms/_LegacyRoomHeader.pcss +++ b/res/css/views/rooms/_LegacyRoomHeader.pcss @@ -163,10 +163,6 @@ limitations under the License. cursor: pointer; } -.mx_LegacyRoomHeader_avatar .mx_BaseAvatar_image { - object-fit: cover; -} - .mx_LegacyRoomHeader_button { cursor: pointer; flex: 0 0 auto; diff --git a/res/css/views/rooms/_MemberInfo.pcss b/res/css/views/rooms/_MemberInfo.pcss index c963c7ed28d..f309d37e6d9 100644 --- a/res/css/views/rooms/_MemberInfo.pcss +++ b/res/css/views/rooms/_MemberInfo.pcss @@ -96,7 +96,7 @@ limitations under the License. display: block; } - .mx_BaseAvatar.mx_BaseAvatar_image { + .mx_BaseAvatar img { cursor: zoom-in; } } diff --git a/res/css/views/rooms/_RoomHeader.pcss b/res/css/views/rooms/_RoomHeader.pcss index 3937e10584d..e5456494642 100644 --- a/res/css/views/rooms/_RoomHeader.pcss +++ b/res/css/views/rooms/_RoomHeader.pcss @@ -15,17 +15,14 @@ limitations under the License. */ .mx_RoomHeader { - display: flex; - align-items: center; height: 64px; - gap: var(--cpd-space-3x); padding: 0 var(--cpd-space-3x); border-bottom: 1px solid $separator; background-color: $background; +} - &:hover { - cursor: pointer; - } +.mx_RoomHeader_info { + cursor: pointer; } .mx_RoomHeader_topic { @@ -39,7 +36,7 @@ limitations under the License. word-break: break-all; text-overflow: ellipsis; - transition: all var(--transition-standard) ease; + transition: all var(--transition-standard) ease 0.1s; } .mx_RoomHeader:hover .mx_RoomHeader_topic { diff --git a/res/css/views/rooms/_RoomPreviewCard.pcss b/res/css/views/rooms/_RoomPreviewCard.pcss index 087e71bdd73..241b6f37ff1 100644 --- a/res/css/views/rooms/_RoomPreviewCard.pcss +++ b/res/css/views/rooms/_RoomPreviewCard.pcss @@ -70,13 +70,6 @@ limitations under the License. display: flex; align-items: center; - .mx_RoomAvatar_isSpaceRoom { - &.mx_BaseAvatar_image, - .mx_BaseAvatar_image { - border-radius: 12px; - } - } - .mx_RoomPreviewCard_video { width: 50px; height: 50px; diff --git a/res/css/views/rooms/_WhoIsTypingTile.pcss b/res/css/views/rooms/_WhoIsTypingTile.pcss index be07862f75c..b981526e58f 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.pcss +++ b/res/css/views/rooms/_WhoIsTypingTile.pcss @@ -31,10 +31,6 @@ limitations under the License. margin-left: -12px; } -.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_initial { - padding-top: 1px; -} - .mx_WhoIsTypingTile_avatars .mx_BaseAvatar { border: 1px solid $background; border-radius: 40px; diff --git a/res/css/views/settings/_JoinRuleSettings.pcss b/res/css/views/settings/_JoinRuleSettings.pcss index de06580ea7e..62debe28a13 100644 --- a/res/css/views/settings/_JoinRuleSettings.pcss +++ b/res/css/views/settings/_JoinRuleSettings.pcss @@ -39,11 +39,6 @@ limitations under the License. color: $secondary-content; display: inline-block; - img.mx_RoomAvatar_isSpaceRoom, - .mx_RoomAvatar_isSpaceRoom img { - border-radius: 8px; - } - .mx_BaseAvatar { margin-right: 8px; } diff --git a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss index fead9e430a1..92fc9d599ed 100644 --- a/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss +++ b/res/css/views/settings/tabs/room/_NotificationSettingsTab.pcss @@ -69,3 +69,7 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/roomlist/notifications-off.svg"); } } + +input[type="file"].mx_NotificationSound_soundUpload { + display: none; +} diff --git a/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss b/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss new file mode 100644 index 00000000000..0b9c5c00a28 --- /dev/null +++ b/res/css/views/settings/tabs/room/_PeopleRoomSettingsTab.pcss @@ -0,0 +1,56 @@ +/* +Copyright 2023 Nordeck IT + Consulting GmbH + +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. +*/ + +.mx_PeopleRoomSettingsTab_knock { + display: flex; + margin-top: var(--cpd-space-2x); +} + +.mx_PeopleRoomSettingsTab_content { + flex-grow: 1; + margin: 0 var(--cpd-space-4x); +} + +.mx_PeopleRoomSettingsTab_name { + font-weight: var(--cpd-font-weight-semibold); +} + +.mx_PeopleRoomSettingsTab_timestamp { + color: $secondary-content; + margin-left: var(--cpd-space-1x); +} + +.mx_PeopleRoomSettingsTab_userId { + color: $secondary-content; + display: block; + font-size: var(--cpd-font-size-body-sm); +} + +.mx_PeopleRoomSettingsTab_seeMoreOrLess { + margin: var(--cpd-space-3x) 0 0; +} + +.mx_PeopleRoomSettingsTab_action { + flex-shrink: 0; + + + .mx_PeopleRoomSettingsTab_action { + margin-left: var(--cpd-space-3x); + } +} + +.mx_PeopleRoomSettingsTab_paragraph { + margin: 0; +} diff --git a/res/img/element-icons/spaces.svg b/res/img/element-icons/spaces.svg new file mode 100644 index 00000000000..7183b4eca9a --- /dev/null +++ b/res/img/element-icons/spaces.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/feather-customised/check.svg b/res/img/feather-customised/check.svg index 5c600f8649e..85cd1965117 100644 --- a/res/img/feather-customised/check.svg +++ b/res/img/feather-customised/check.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/feather-customised/x.svg b/res/img/feather-customised/x.svg index 5468caa8aa1..a4f6c4a81a7 100644 --- a/res/img/feather-customised/x.svg +++ b/res/img/feather-customised/x.svg @@ -1,4 +1,4 @@ - + diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index ba18532d737..a48301009c3 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -186,14 +186,14 @@ $call-background: #15191e; $call-primary-content: #ffffff; $call-light-quaternary-content: #c1c6cd; -$username-variant1-color: var(--cpd-color-blue-900); -$username-variant2-color: var(--cpd-color-fuchsia-900); -$username-variant3-color: var(--cpd-color-green-900); -$username-variant4-color: var(--cpd-color-pink-900); -$username-variant5-color: var(--cpd-color-orange-900); -$username-variant6-color: var(--cpd-color-cyan-900); -$username-variant7-color: var(--cpd-color-purple-900); -$username-variant8-color: var(--cpd-color-lime-900); +$username-variant1-color: var(--cpd-color-blue-1200); +$username-variant2-color: var(--cpd-color-fuchsia-1200); +$username-variant3-color: var(--cpd-color-green-1200); +$username-variant4-color: var(--cpd-color-pink-1200); +$username-variant5-color: var(--cpd-color-orange-1200); +$username-variant6-color: var(--cpd-color-cyan-1200); +$username-variant7-color: var(--cpd-color-purple-1200); +$username-variant8-color: var(--cpd-color-lime-1200); /** * Creating a `semantic` color scale. This will not be needed with the new diff --git a/res/themes/light-high-contrast/css/_light-high-contrast.pcss b/res/themes/light-high-contrast/css/_light-high-contrast.pcss index f82284089ba..1562db5eab5 100644 --- a/res/themes/light-high-contrast/css/_light-high-contrast.pcss +++ b/res/themes/light-high-contrast/css/_light-high-contrast.pcss @@ -8,14 +8,14 @@ $tertiary-content: var(--cpd-color-gray-800); $quaternary-content: var(--cpd-color-gray-600); $quinary-content: var(--cpd-color-gray-400); -$username-variant1-color: var(--cpd-color-blue-900); -$username-variant2-color: var(--cpd-color-fuchsia-900); -$username-variant3-color: var(--cpd-color-green-900); -$username-variant4-color: var(--cpd-color-pink-900); -$username-variant5-color: var(--cpd-color-orange-900); -$username-variant6-color: var(--cpd-color-cyan-900); -$username-variant7-color: var(--cpd-color-purple-900); -$username-variant8-color: var(--cpd-color-lime-900); +$username-variant1-color: var(--cpd-color-blue-1200); +$username-variant2-color: var(--cpd-color-fuchsia-1200); +$username-variant3-color: var(--cpd-color-green-1200); +$username-variant4-color: var(--cpd-color-pink-1200); +$username-variant5-color: var(--cpd-color-orange-1200); +$username-variant6-color: var(--cpd-color-cyan-1200); +$username-variant7-color: var(--cpd-color-purple-1200); +$username-variant8-color: var(--cpd-color-lime-1200); $accent-alt: $links; $input-border-color: $secondary-content; diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index f40a56ddbb9..e4428ac1810 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -36,14 +36,14 @@ $alert: var(--cpd-color-text-critical-primary); $links: var(--cpd-color-text-link-external); $link-external: var(--cpd-color-text-link-external); -$username-variant1-color: var(--cpd-color-blue-900); -$username-variant2-color: var(--cpd-color-fuchsia-900); -$username-variant3-color: var(--cpd-color-green-900); -$username-variant4-color: var(--cpd-color-pink-900); -$username-variant5-color: var(--cpd-color-orange-900); -$username-variant6-color: var(--cpd-color-cyan-900); -$username-variant7-color: var(--cpd-color-purple-900); -$username-variant8-color: var(--cpd-color-lime-900); +$username-variant1-color: var(--cpd-color-blue-1200); +$username-variant2-color: var(--cpd-color-fuchsia-1200); +$username-variant3-color: var(--cpd-color-green-1200); +$username-variant4-color: var(--cpd-color-pink-1200); +$username-variant5-color: var(--cpd-color-orange-1200); +$username-variant6-color: var(--cpd-color-cyan-1200); +$username-variant7-color: var(--cpd-color-purple-1200); +$username-variant8-color: var(--cpd-color-lime-1200); /* ******************** */ /** diff --git a/scripts/check-i18n.pl b/scripts/check-i18n.pl deleted file mode 100755 index fa11bc52924..00000000000 --- a/scripts/check-i18n.pl +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; -use Cwd 'abs_path'; - -# script which checks how out of sync the i18ns are drifting - -# example i18n format: -# "%(oneUser)sleft": "%(oneUser)sleft", - -$|=1; - -$0 =~ /^(.*\/)/; -my $i18ndir = abs_path($1."/../src/i18n/strings"); -my $srcdir = abs_path($1."/../src"); - -my $en = read_i18n($i18ndir."/en_EN.json"); - -my $src_strings = read_src_strings($srcdir); -my $src = {}; - -print "Checking strings in src\n"; -foreach my $tuple (@$src_strings) { - my ($s, $file) = (@$tuple); - $src->{$s} = $file; - if (!$en->{$s}) { - if ($en->{$s . '.'}) { - printf ("%50s %24s\t%s\n", $file, "en_EN has fullstop!", $s); - } - else { - $s =~ /^(.*)\.?$/; - if ($en->{$1}) { - printf ("%50s %24s\t%s\n", $file, "en_EN lacks fullstop!", $s); - } - else { - printf ("%50s %24s\t%s\n", $file, "Translation missing!", $s); - } - } - } -} - -print "\nChecking en_EN\n"; -my $count = 0; -my $remaining_src = {}; -foreach (keys %$src) { $remaining_src->{$_}++ }; - -foreach my $k (sort keys %$en) { - # crappy heuristic to ignore country codes for now... - next if ($k =~ /^(..|..-..)$/); - - if ($en->{$k} ne $k) { - printf ("%50s %24s\t%s\n", "en_EN", "en_EN is not symmetrical", $k); - } - - if (!$src->{$k}) { - if ($src->{$k. '.'}) { - printf ("%50s %24s\t%s\n", $src->{$k. '.'}, "src has fullstop!", $k); - } - else { - $k =~ /^(.*)\.?$/; - if ($src->{$1}) { - printf ("%50s %24s\t%s\n", $src->{$1}, "src lacks fullstop!", $k); - } - else { - printf ("%50s %24s\t%s\n", '???', "Not present in src?", $k); - } - } - } - else { - $count++; - delete $remaining_src->{$k}; - } -} -printf ("$count/" . (scalar keys %$src) . " strings found in src are present in en_EN\n"); -foreach (keys %$remaining_src) { - print "missing: $_\n"; -} - -opendir(DIR, $i18ndir) || die $!; -my @files = readdir(DIR); -closedir(DIR); -foreach my $lang (grep { -f "$i18ndir/$_" && !/(basefile|en_EN)\.json/ } @files) { - print "\nChecking $lang\n"; - - my $map = read_i18n($i18ndir."/".$lang); - my $count = 0; - - my $remaining_en = {}; - foreach (keys %$en) { $remaining_en->{$_}++ }; - - foreach my $k (sort keys %$map) { - { - no warnings 'uninitialized'; - my $vars = {}; - while ($k =~ /%\((.*?)\)s/g) { - $vars->{$1}++; - } - while ($map->{$k} =~ /%\((.*?)\)s/g) { - $vars->{$1}--; - } - foreach my $var (keys %$vars) { - if ($vars->{$var} != 0) { - printf ("%10s %24s\t%s\n", $lang, "Broken var ($var)s", $k); - } - } - } - - if ($en->{$k}) { - if ($map->{$k} eq $k) { - printf ("%10s %24s\t%s\n", $lang, "Untranslated string?", $k); - } - $count++; - delete $remaining_en->{$k}; - } - else { - if ($en->{$k . "."}) { - printf ("%10s %24s\t%s\n", $lang, "en_EN has fullstop!", $k); - next; - } - - $k =~ /^(.*)\.?$/; - if ($en->{$1}) { - printf ("%10s %24s\t%s\n", $lang, "en_EN lacks fullstop!", $k); - next; - } - - printf ("%10s %24s\t%s\n", $lang, "Not present in en_EN", $k); - } - } - - if (scalar keys %$remaining_en < 100) { - foreach (keys %$remaining_en) { - printf ("%10s %24s\t%s\n", $lang, "Not yet translated", $_); - } - } - - printf ("$count/" . (scalar keys %$en) . " strings translated\n"); -} - -sub read_i18n { - my $path = shift; - my $map = {}; - $path =~ /.*\/(.*)$/; - my $lang = $1; - - open(FILE, "<", $path) || die $!; - while() { - if ($_ =~ m/^(\s+)"(.*?)"(: *)"(.*?)"(,?)$/) { - my ($indent, $src, $colon, $dst, $comma) = ($1, $2, $3, $4, $5); - $src =~ s/\\"/"/g; - $dst =~ s/\\"/"/g; - - if ($map->{$src}) { - printf ("%10s %24s\t%s\n", $lang, "Duplicate translation!", $src); - } - $map->{$src} = $dst; - } - } - close(FILE); - - return $map; -} - -sub read_src_strings { - my $path = shift; - - use File::Find; - use File::Slurp; - - my $strings = []; - - my @files; - find( sub { push @files, $File::Find::name if (-f $_ && /\.jsx?$/) }, $path ); - foreach my $file (@files) { - my $src = read_file($file); - $src =~ s/'\s*\+\s*'//g; - $src =~ s/"\s*\+\s*"//g; - - $file =~ s/^.*\/src/src/; - while ($src =~ /_t(?:Jsx)?\(\s*'(.*?[^\\])'/sg) { - my $s = $1; - $s =~ s/\\'/'/g; - push @$strings, [$s, $file]; - } - while ($src =~ /_t(?:Jsx)?\(\s*"(.*?[^\\])"/sg) { - push @$strings, [$1, $file]; - } - } - - return $strings; -} \ No newline at end of file diff --git a/scripts/fix-i18n.pl b/scripts/fix-i18n.pl deleted file mode 100755 index def352463d4..00000000000 --- a/scripts/fix-i18n.pl +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/perl -ni - -use strict; -use warnings; - -# script which synchronises i18n strings to include punctuation. -# i've cherry-picked ones which seem to have diverged between the different translations -# from TextForEvent, causing missing events all over the place - -BEGIN { -$::fixups = [split(/\n/, < 0 ? ('../' x $depth) : './'; - -s/= require\(['"]matrix-react-sdk\/lib\/(.*?)['"]\)/= require('$prefix$1')/; -s/= require\(['"]matrix-react-sdk['"]\)/= require('${prefix}index')/; - -s/^(import .* from )['"]matrix-react-sdk\/lib\/(.*?)['"]/$1'$prefix$2'/; -s/^(import .* from )['"]matrix-react-sdk['"]/$1'${prefix}index'/; diff --git a/sonar-project.properties b/sonar-project.properties index a8d8f0cf860..b127fee1e98 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,7 +8,9 @@ sonar.sources=src,res sonar.tests=test,cypress sonar.exclusions=__mocks__,docs +sonar.cpd.exclusions=src/i18n/strings/*.json sonar.typescript.tsconfigPath=./tsconfig.json sonar.javascript.lcov.reportPaths=coverage/lcov.info -sonar.coverage.exclusions=test/**/*,cypress/**/*,src/components/views/dialogs/devtools/**/* +# instrumentation is disabled on SessionLock +sonar.coverage.exclusions=test/**/*,cypress/**/*,src/components/views/dialogs/devtools/**/*,src/utils/SessionLock.ts sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml diff --git a/src/@types/common.ts b/src/@types/common.ts index 4ea9cff802c..dd42c2078f2 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -23,22 +23,41 @@ export type Writeable = { -readonly [P in keyof T]: T[P] }; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; -// Utility type for string dot notation for accessing nested object properties -// Based on https://stackoverflow.com/a/58436959 -type Join = K extends string | number +/** + * Utility type for string dot notation for accessing nested object properties. + * Based on https://stackoverflow.com/a/58436959 + * @example + * { + * "a": { + * "b": { + * "c": "value" + * }, + * "d": "foobar" + * } + * } + * will yield a type of `"a.b.c" | "a.d"` with Separator="." + * @typeParam Target the target type to generate leaf keys for + * @typeParam Separator the separator to use between key segments when accessing nested objects + * @typeParam LeafType the type which leaves of this object extend, used to determine when to stop recursion + * @typeParam MaxDepth the maximum depth to recurse to + * @returns a union type representing all dot (Separator) string notation keys which can access a Leaf (of LeafType) + */ +export type Leaves = [ + MaxDepth, +] extends [never] + ? never + : Target extends LeafType + ? "" + : { + [K in keyof Target]-?: Join, Separator>; + }[keyof Target]; +type Prev = [never, 0, 1, 2, 3, ...0[]]; +type Join = K extends string | number ? P extends string | number - ? `${K}${"" extends P ? "" : "."}${P}` + ? `${K}${"" extends P ? "" : S}${P}` : never : never; -type Prev = [never, 0, 1, 2, 3, ...0[]]; - -export type Leaves = [D] extends [never] - ? never - : T extends object - ? { [K in keyof T]-?: Join> }[keyof T] - : ""; - export type RecursivePartial = { [P in keyof T]?: T[P] extends (infer U)[] ? RecursivePartial[] diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index d8f01cd4be5..39e1eacbc87 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// eslint-disable-next-line no-restricted-imports import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "@types/modernizr"; diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index bc9958cb6df..0d527ada5a7 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -24,8 +24,8 @@ import { MatrixClient, MatrixError, HTTPError, + IThreepid, } from "matrix-js-sdk/src/matrix"; -import { IThreepid } from "matrix-js-sdk/src/@types/threepids"; import Modal from "./Modal"; import { _t, UserFriendlyError } from "./languageHandler"; @@ -226,7 +226,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_POSTAUTH]: { title: _t("Confirm adding email"), body: _t("Click the button below to confirm adding this email address."), - continueText: _t("Confirm"), + continueText: _t("action|confirm"), continueKind: "primary", }, }; @@ -329,7 +329,7 @@ export default class AddThreepid { [SSOAuthEntry.PHASE_POSTAUTH]: { title: _t("Confirm adding phone number"), body: _t("Click the button below to confirm adding this phone number."), - continueText: _t("Confirm"), + continueText: _t("action|confirm"), continueKind: "primary", }, }; diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 1173ca3fd06..901699a3597 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -77,10 +77,10 @@ export default class AsyncWrapper extends React.Component { return ; } else if (this.state.error) { return ( - + {_t("Unable to load! Check your network connectivity and try again.")} diff --git a/src/ContentMessages.ts b/src/ContentMessages.ts index 14208e0d9e6..a5a5ffb137f 100644 --- a/src/ContentMessages.ts +++ b/src/ContentMessages.ts @@ -536,7 +536,7 @@ export default class ContentMessages { replyToEvent: MatrixEvent | undefined, promBefore?: Promise, ): Promise { - const fileName = file.name || _t("Attachment"); + const fileName = file.name || _t("common|attachment"); const content: Omit & { info: Partial } = { body: fileName, info: { diff --git a/src/DateUtils.ts b/src/DateUtils.ts index e743b3feead..78b390a4aa9 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -18,95 +18,121 @@ limitations under the License. import { Optional } from "matrix-events-sdk"; -import { _t } from "./languageHandler"; +import { _t, getUserLanguage } from "./languageHandler"; -function getDaysArray(): string[] { - return [_t("Sun"), _t("Mon"), _t("Tue"), _t("Wed"), _t("Thu"), _t("Fri"), _t("Sat")]; -} +export const MINUTE_MS = 60000; +export const HOUR_MS = MINUTE_MS * 60; +export const DAY_MS = HOUR_MS * 24; -function getMonthsArray(): string[] { - return [ - _t("Jan"), - _t("Feb"), - _t("Mar"), - _t("Apr"), - _t("May"), - _t("Jun"), - _t("Jul"), - _t("Aug"), - _t("Sep"), - _t("Oct"), - _t("Nov"), - _t("Dec"), - ]; +/** + * Returns array of 7 weekday names, from Sunday to Saturday, internationalised to the user's language. + * @param weekday - format desired "short" | "long" | "narrow" + */ +export function getDaysArray(weekday: Intl.DateTimeFormatOptions["weekday"] = "short"): string[] { + const sunday = 1672574400000; // 2023-01-01 12:00 UTC + const { format } = new Intl.DateTimeFormat(getUserLanguage(), { weekday, timeZone: "UTC" }); + return [...Array(7).keys()].map((day) => format(sunday + day * DAY_MS)); } -function pad(n: number): string { - return (n < 10 ? "0" : "") + n; +/** + * Returns array of 12 month names, from January to December, internationalised to the user's language. + * @param month - format desired "numeric" | "2-digit" | "long" | "short" | "narrow" + */ +export function getMonthsArray(month: Intl.DateTimeFormatOptions["month"] = "short"): string[] { + const { format } = new Intl.DateTimeFormat(getUserLanguage(), { month, timeZone: "UTC" }); + return [...Array(12).keys()].map((m) => format(Date.UTC(2021, m))); } -function twelveHourTime(date: Date, showSeconds = false): string { - let hours = date.getHours() % 12; - const minutes = pad(date.getMinutes()); - const ampm = date.getHours() >= 12 ? _t("PM") : _t("AM"); - hours = hours ? hours : 12; // convert 0 -> 12 - if (showSeconds) { - const seconds = pad(date.getSeconds()); - return `${hours}:${minutes}:${seconds}${ampm}`; - } - return `${hours}:${minutes}${ampm}`; +// XXX: Ideally we could just specify `hour12: boolean` but it has issues on Chrome in the `en` locale +// https://support.google.com/chrome/thread/29828561?hl=en +function getTwelveHourOptions(showTwelveHour: boolean): Intl.DateTimeFormatOptions { + return { + hourCycle: showTwelveHour ? "h12" : "h23", + }; } -export function formatDate(date: Date, showTwelveHour = false): string { +/** + * Formats a given date to a date & time string. + * + * The output format depends on how far away the given date is from now. + * Will use the browser's default time zone. + * If the date is today it will return a time string excluding seconds. See {@formatTime}. + * If the date is within the last 6 days it will return the name of the weekday along with the time string excluding seconds. + * If the date is within the same year then it will return the weekday, month and day of the month along with the time string excluding seconds. + * Otherwise, it will return a string representing the full date & time in a human friendly manner. See {@formatFullDate}. + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatDate(date: Date, showTwelveHour = false, locale?: string): string { + const _locale = locale ?? getUserLanguage(); const now = new Date(); - const days = getDaysArray(); - const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) { - return formatTime(date, showTwelveHour); - } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - // TODO: use standard date localize function provided in counterpart - return _t("%(weekDayName)s %(time)s", { - weekDayName: days[date.getDay()], - time: formatTime(date, showTwelveHour), - }); + return formatTime(date, showTwelveHour, _locale); + } else if (now.getTime() - date.getTime() < 6 * DAY_MS) { + // Time is within the last 6 days (or in the future) + return new Intl.DateTimeFormat(_locale, { + ...getTwelveHourOptions(showTwelveHour), + weekday: "short", + hour: "numeric", + minute: "2-digit", + }).format(date); } else if (now.getFullYear() === date.getFullYear()) { - // TODO: use standard date localize function provided in counterpart - return _t("%(weekDayName)s, %(monthName)s %(day)s %(time)s", { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - time: formatTime(date, showTwelveHour), - }); + return new Intl.DateTimeFormat(_locale, { + ...getTwelveHourOptions(showTwelveHour), + weekday: "short", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }).format(date); } - return formatFullDate(date, showTwelveHour); + return formatFullDate(date, showTwelveHour, false, _locale); } -export function formatFullDateNoTime(date: Date): string { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - fullYear: date.getFullYear(), - }); +/** + * Formats a given date to a human-friendly string with short weekday. + * Will use the browser's default time zone. + * @example "Thu, 17 Nov 2022" in en-GB locale + * @param date - date object to format + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullDateNoTime(date: Date, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", + }).format(date); } -export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string { - const days = getDaysArray(); - const months = getMonthsArray(); - return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", { - weekDayName: days[date.getDay()], - monthName: months[date.getMonth()], - day: date.getDate(), - fullYear: date.getFullYear(), - time: showSeconds ? formatFullTime(date, showTwelveHour) : formatTime(date, showTwelveHour), - }); +/** + * Formats a given date to a date & time string, optionally including seconds. + * Will use the browser's default time zone. + * @example "Thu, 17 Nov 2022, 4:58:32 pm" in en-GB locale with showTwelveHour=true and showSeconds=true + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param showSeconds - whether to include seconds in the time portion of the string + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + ...getTwelveHourOptions(showTwelveHour), + weekday: "short", + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", + second: showSeconds ? "2-digit" : undefined, + }).format(date); } /** * Formats dates to be compatible with attributes of a ``. Dates - * should be formatted like "2020-06-23" (formatted according to ISO8601) + * should be formatted like "2020-06-23" (formatted according to ISO8601). * * @param date The date to format. * @returns The date string in ISO8601 format ready to be used with an `` @@ -115,22 +141,44 @@ export function formatDateForInput(date: Date): string { const year = `${date.getFullYear()}`.padStart(4, "0"); const month = `${date.getMonth() + 1}`.padStart(2, "0"); const day = `${date.getDate()}`.padStart(2, "0"); - const dateInputValue = `${year}-${month}-${day}`; - return dateInputValue; + return `${year}-${month}-${day}`; } -export function formatFullTime(date: Date, showTwelveHour = false): string { - if (showTwelveHour) { - return twelveHourTime(date, true); - } - return pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()); +/** + * Formats a given date to a time string including seconds. + * Will use the browser's default time zone. + * @example "4:58:32 PM" in en-GB locale with showTwelveHour=true + * @example "16:58:32" in en-GB locale with showTwelveHour=false + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullTime(date: Date, showTwelveHour = false, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + ...getTwelveHourOptions(showTwelveHour), + hour: "numeric", + minute: "2-digit", + second: "2-digit", + }).format(date); } -export function formatTime(date: Date, showTwelveHour = false): string { - if (showTwelveHour) { - return twelveHourTime(date); - } - return pad(date.getHours()) + ":" + pad(date.getMinutes()); +/** + * Formats a given date to a time string excluding seconds. + * Will use the browser's default time zone. + * @example "4:58 PM" in en-GB locale with showTwelveHour=true + * @example "16:58" in en-GB locale with showTwelveHour=false + * @param date - date object to format + * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode). + * Overrides the default from the locale, whether `true` or `false`. + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatTime(date: Date, showTwelveHour = false, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + ...getTwelveHourOptions(showTwelveHour), + hour: "numeric", + minute: "2-digit", + }).format(date); } export function formatSeconds(inSeconds: number): string { @@ -183,9 +231,8 @@ export function formatTimeLeft(inSeconds: number): string { }); } -const MILLIS_IN_DAY = 86400000; function withinPast24Hours(prevDate: Date, nextDate: Date): boolean { - return Math.abs(prevDate.getTime() - nextDate.getTime()) <= MILLIS_IN_DAY; + return Math.abs(prevDate.getTime() - nextDate.getTime()) <= DAY_MS; } function withinCurrentDay(prevDate: Date, nextDate: Date): boolean { @@ -210,15 +257,15 @@ export function wantsDateSeparator(prevEventDate: Optional, nextEventDate: } export function formatFullDateNoDay(date: Date): string { + const locale = getUserLanguage(); return _t("%(date)s at %(time)s", { - date: date.toLocaleDateString().replace(/\//g, "-"), - time: date.toLocaleTimeString().replace(/:/g, "-"), + date: date.toLocaleDateString(locale).replace(/\//g, "-"), + time: date.toLocaleTimeString(locale).replace(/:/g, "-"), }); } /** - * Returns an ISO date string without textual description of the date (ie: no "Wednesday" or - * similar) + * Returns an ISO date string without textual description of the date (ie: no "Wednesday" or similar) * @param date The date to format. * @returns The date string in ISO format. */ @@ -226,12 +273,23 @@ export function formatFullDateNoDayISO(date: Date): string { return date.toISOString(); } -export function formatFullDateNoDayNoTime(date: Date): string { - return date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate()); +/** + * Formats a given date to a string. + * Will use the browser's default time zone. + * @example 17/11/2022 in en-GB locale + * @param date - date object to format + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale + */ +export function formatFullDateNoDayNoTime(date: Date, locale?: string): string { + return new Intl.DateTimeFormat(locale ?? getUserLanguage(), { + year: "numeric", + month: "numeric", + day: "numeric", + }).format(date); } export function formatRelativeTime(date: Date, showTwelveHour = false): string { - const now = new Date(Date.now()); + const now = new Date(); if (withinCurrentDay(date, now)) { return formatTime(date, showTwelveHour); } else { @@ -245,15 +303,11 @@ export function formatRelativeTime(date: Date, showTwelveHour = false): string { } } -const MINUTE_MS = 60000; -const HOUR_MS = MINUTE_MS * 60; -const DAY_MS = HOUR_MS * 24; - /** - * Formats duration in ms to human readable string - * Returns value in biggest possible unit (day, hour, min, second) + * Formats duration in ms to human-readable string + * Returns value in the biggest possible unit (day, hour, min, second) * Rounds values up until unit threshold - * ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d + * i.e. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d */ export function formatDuration(durationMs: number): string { if (durationMs >= DAY_MS) { @@ -269,9 +323,9 @@ export function formatDuration(durationMs: number): string { } /** - * Formats duration in ms to human readable string + * Formats duration in ms to human-readable string * Returns precise value down to the nearest second - * ie. 23:13:57 -> 23h 13m 57s, 44:56:56 -> 1d 20h 56m 56s + * i.e. 23:13:57 -> 23h 13m 57s, 44:56:56 -> 1d 20h 56m 56s */ export function formatPreciseDuration(durationMs: number): string { const days = Math.floor(durationMs / DAY_MS); @@ -293,13 +347,13 @@ export function formatPreciseDuration(durationMs: number): string { /** * Formats a timestamp to a short date - * (eg 25/12/22 in uk locale) - * localised by system locale + * Similar to {@formatFullDateNoDayNoTime} but with 2-digit on day, month, year. + * @example 25/12/22 in en-GB locale * @param timestamp - epoch timestamp + * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale * @returns {string} formattedDate */ -export const formatLocalDateShort = (timestamp: number): string => - new Intl.DateTimeFormat( - undefined, // locales - { day: "2-digit", month: "2-digit", year: "2-digit" }, - ).format(timestamp); +export const formatLocalDateShort = (timestamp: number, locale?: string): string => + new Intl.DateTimeFormat(locale ?? getUserLanguage(), { day: "2-digit", month: "2-digit", year: "2-digit" }).format( + timestamp, + ); diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 04c79bc44ae..538c626b839 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -29,6 +29,7 @@ import { Optional } from "matrix-events-sdk"; import _Linkify from "linkify-react"; import escapeHtml from "escape-html"; import GraphemeSplitter from "graphemer"; +import { getEmojiFromUnicode } from "@matrix-org/emojibase-bindings"; import { _linkifyElement, @@ -39,7 +40,6 @@ import { import { IExtendedSanitizeOptions } from "./@types/sanitize-html"; import SettingsStore from "./settings/SettingsStore"; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; -import { getEmojiFromUnicode } from "./emoji"; import { mediaFromMxc } from "./customisations/Media"; import { stripHTMLReply, stripPlainReply } from "./utils/Reply"; import { PERMITTED_URL_SCHEMES } from "./utils/UrlUtils"; diff --git a/src/IdentityAuthClient.tsx b/src/IdentityAuthClient.tsx index 44f0aaf2e6b..a596156addf 100644 --- a/src/IdentityAuthClient.tsx +++ b/src/IdentityAuthClient.tsx @@ -141,9 +141,7 @@ export default class IdentityAuthClient {

    {_t( - "This action requires accessing the default identity server " + - " to validate an email address or phone number, " + - "but the server does not have any terms of service.", + "This action requires accessing the default identity server to validate an email address or phone number, but the server does not have any terms of service.", {}, { server: () => {abbreviateUrl(identityServerUrl)}, @@ -153,7 +151,7 @@ export default class IdentityAuthClient {

    {_t("Only continue if you trust the owner of the server.")}

    ), - button: _t("Trust"), + button: _t("action|trust"), }); const [confirmed] = await finished; if (confirmed) { diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 18ae6b8b8e6..675756b462d 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -823,19 +823,14 @@ export default class LegacyCallHandler extends EventEmitter {

    {_t( - "Please ask the administrator of your homeserver " + - "(%(homeserverDomain)s) to configure a TURN server in " + - "order for calls to work reliably.", + "Please ask the administrator of your homeserver (%(homeserverDomain)s) to configure a TURN server in order for calls to work reliably.", { homeserverDomain: cli.getDomain() }, { code: (sub: string) => {sub} }, )}

    {_t( - "Alternatively, you can try to use the public server at " + - ", but this will not be as reliable, and " + - "it will share your IP address with that server. You can also manage " + - "this in Settings.", + "Alternatively, you can try to use the public server at , but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.", undefined, { server: () => {new URL(FALLBACK_ICE_SERVER).pathname} }, )} @@ -845,7 +840,7 @@ export default class LegacyCallHandler extends EventEmitter { button: _t("Try using %(server)s", { server: new URL(FALLBACK_ICE_SERVER).pathname, }), - cancelButton: _t("OK"), + cancelButton: _t("action|ok"), onFinished: (allow) => { SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); cli.setFallbackICEServerAllowed(!!allow); @@ -865,8 +860,7 @@ export default class LegacyCallHandler extends EventEmitter { description = (

    {_t( - "Call failed because microphone could not be accessed. " + - "Check that a microphone is plugged in and set up correctly.", + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.", )}
    ); diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 719b0a45fe0..34c27c6a21a 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -18,12 +18,11 @@ limitations under the License. */ import { ReactNode } from "react"; -import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { createClient, MatrixClient, SSOAction } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; import { QueryDict } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { SSOAction } from "matrix-js-sdk/src/@types/auth"; import { MINIMUM_MATRIX_VERSION } from "matrix-js-sdk/src/version-support"; import { IMatrixClientCreds, MatrixClientPeg } from "./MatrixClientPeg"; @@ -83,6 +82,41 @@ dis.register((payload) => { } }); +/** + * This is set to true by {@link #onSessionLockStolen}. + * + * It is used in various of the async functions to prevent races where we initialise a client after the lock is stolen. + */ +let sessionLockStolen = false; + +// this is exposed solely for unit tests. +// ts-prune-ignore-next +export function setSessionLockNotStolen(): void { + sessionLockStolen = false; +} + +/** + * Handle the session lock being stolen. Stops any active Matrix Client, and aborts any ongoing client initialisation. + */ +export async function onSessionLockStolen(): Promise { + sessionLockStolen = true; + stopMatrixClient(); +} + +/** + * Check if we still hold the session lock. + * + * If not, raises a {@link SessionLockStolenError}. + */ +function checkSessionLock(): void { + if (sessionLockStolen) { + throw new SessionLockStolenError("session lock has been released"); + } +} + +/** Error type raised by various functions in the Lifecycle workflow if session lock is stolen during execution */ +class SessionLockStolenError extends Error {} + interface ILoadSessionOpts { enableGuest?: boolean; guestHsUrl?: string; @@ -154,6 +188,9 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise if (success) { return true; } + if (sessionLockStolen) { + return false; + } if (enableGuest && guestHsUrl) { return registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName); @@ -167,6 +204,12 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise // need to show the general failure dialog. Instead, just go back to welcome. return false; } + + // likewise, if the session lock has been stolen while we've been trying to start + if (sessionLockStolen) { + return false; + } + return handleLoadSessionFailure(e); } } @@ -306,8 +349,7 @@ export function attemptTokenLogin( logger.warn("Cannot log in with token: can't determine HS URL to use"); onFailedDelegatedAuthLogin( _t( - "We asked the browser to remember which homeserver you use to let you sign in, " + - "but unfortunately your browser has forgotten it. Go to the sign in page and try again.", + "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.", ), ); return Promise.resolve(false); @@ -366,7 +408,7 @@ async function onFailedDelegatedAuthLogin(description: string | ReactNode, tryAg Modal.createDialog(ErrorDialog, { title: _t("We couldn't log you in"), description, - button: _t("Try again"), + button: _t("action|try_again"), // if we have a tryAgain callback, call it the primary 'try again' button was clicked in the dialog onFinished: tryAgain ? (shouldTryAgain?: boolean) => shouldTryAgain && tryAgain() : undefined, }); @@ -614,7 +656,7 @@ async function checkServerVersions(): Promise { brand: SdkConfig.get().brand, }, ), - acceptLabel: _t("OK"), + acceptLabel: _t("action|ok"), onAccept: () => { ToastStore.sharedInstance().dismissToast(toastKey); }, @@ -721,6 +763,7 @@ export async function hydrateSession(credentials: IMatrixClientCreds): Promise { + checkSessionLock(); credentials.guest = Boolean(credentials.guest); const softLogout = isSoftLogout(); @@ -751,6 +794,8 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable await abortLogin(); } + // check the session lock just before creating the new client + checkSessionLock(); MatrixClientPeg.replaceUsingCreds(credentials); const client = MatrixClientPeg.safeGet(); @@ -783,6 +828,7 @@ async function doSetLoggedIn(credentials: IMatrixClientCreds, clearStorageEnable } else { logger.warn("No local storage available: can't persist session!"); } + checkSessionLock(); dis.fire(Action.OnLoggedIn); await startMatrixClient(client, /*startSyncing=*/ !softLogout); @@ -978,6 +1024,8 @@ async function startMatrixClient(client: MatrixClient, startSyncing = true): Pro await MatrixClientPeg.assign(); } + checkSessionLock(); + // Run the migrations after the MatrixClientPeg has been assigned SettingsStore.runMigrations(); diff --git a/src/Login.ts b/src/Login.ts index d918aebc4b2..5ca4e9e5a25 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -15,9 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix"; +import { + createClient, + MatrixClient, + LoginFlow, + DELEGATED_OIDC_COMPATIBILITY, + ILoginFlow, + LoginRequest, +} from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { DELEGATED_OIDC_COMPATIBILITY, ILoginFlow, LoginFlow, LoginRequest } from "matrix-js-sdk/src/@types/auth"; import { IMatrixClientCreds } from "./MatrixClientPeg"; import SecurityCustomisations from "./customisations/Security"; diff --git a/src/Markdown.ts b/src/Markdown.ts index 709c34f31b8..0d33e58b6f0 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -112,6 +112,10 @@ const innerNodeLiteral = (node: commonmark.Node): string => { return literal; }; +const emptyItemWithNoSiblings = (node: commonmark.Node): boolean => { + return !node.prev && !node.next && !node.firstChild; +}; + /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -242,13 +246,30 @@ export default class Markdown { public isPlainText(): boolean { const walker = this.parsed.walker(); - let ev: commonmark.NodeWalkingStep | null; + while ((ev = walker.next())) { const node = ev.node; + if (TEXT_NODES.indexOf(node.type) > -1) { // definitely text continue; + } else if (node.type == "list" || node.type == "item") { + // Special handling for inputs like `+`, `*`, `-` and `2021.` which + // would otherwise be treated as a list of a single empty item. + // See https://github.com/vector-im/element-web/issues/7631 + if (node.type == "list" && node.firstChild && emptyItemWithNoSiblings(node.firstChild)) { + // A list with a single empty item is treated as plain text. + continue; + } + + if (node.type == "item" && emptyItemWithNoSiblings(node)) { + // An empty list item with no sibling items is treated as plain text. + continue; + } + + // Everything else is actual lists and therefore not plaintext. + return false; } else if (node.type == "html_inline" || node.type == "html_block") { // if it's an allowed html tag, we need to render it and therefore // we will need to use HTML. If it's not allowed, it's not HTML since diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 22c2dcb45c9..517d264597e 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -26,8 +26,8 @@ import { EventTimelineSet, IStartClientOpts, MatrixClient, + MemoryStore, } from "matrix-js-sdk/src/matrix"; -import { MemoryStore } from "matrix-js-sdk/src/store/memory"; import * as utils from "matrix-js-sdk/src/utils"; import { verificationMethods } from "matrix-js-sdk/src/crypto"; import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode"; @@ -212,7 +212,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { description: _t( "This may be caused by having the app open in multiple tabs or due to clearing browser data.", ), - button: _t("Reload"), + button: _t("action|reload"), }); const [reload] = await finished; if (!reload) return; diff --git a/src/Notifier.ts b/src/Notifier.ts index 839be8c83a6..2a7e7daf6ab 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -27,9 +27,9 @@ import { SyncState, SyncStateData, IRoomTimelineData, + M_LOCATION, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { M_LOCATION } from "matrix-js-sdk/src/@types/location"; import { PermissionChanged as PermissionChangedEvent } from "@matrix-org/analytics-events/types/typescript/PermissionChanged"; import { MatrixClientPeg } from "./MatrixClientPeg"; @@ -299,8 +299,7 @@ class NotifierClass { const description = result === "denied" ? _t( - "%(brand)s does not have permission to send you notifications - " + - "please check your browser settings", + "%(brand)s does not have permission to send you notifications - please check your browser settings", { brand }, ) : _t("%(brand)s was not given permission to send notifications - please try again", { diff --git a/src/PosthogTrackers.ts b/src/PosthogTrackers.ts index c03a20216c1..14bd8426de6 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -27,6 +27,7 @@ export type InteractionName = InteractionEvent["name"]; const notLoggedInMap: Record, ScreenName> = { [Views.LOADING]: "Loading", + [Views.CONFIRM_LOCK_THEFT]: "ConfirmStartup", [Views.WELCOME]: "Welcome", [Views.LOGIN]: "Login", [Views.REGISTER]: "Register", @@ -35,6 +36,7 @@ const notLoggedInMap: Record, ScreenName> = { [Views.COMPLETE_SECURITY]: "CompleteSecurity", [Views.E2E_SETUP]: "E2ESetup", [Views.SOFT_LOGOUT]: "SoftLogout", + [Views.LOCK_STOLEN]: "SessionLockStolen", }; const loggedInPageTypeMap: Record = { diff --git a/src/Registration.tsx b/src/Registration.tsx index f96aef22519..b9cebf35a93 100644 --- a/src/Registration.tsx +++ b/src/Registration.tsx @@ -53,11 +53,11 @@ export async function startAnyRegistrationFlow( const modal = Modal.createDialog(QuestionDialog, { hasCancelButton: true, quitOnly: true, - title: SettingsStore.getValue(UIFeature.Registration) ? _t("Sign In or Create Account") : _t("Sign In"), + title: SettingsStore.getValue(UIFeature.Registration) ? _t("Sign In or Create Account") : _t("action|sign_in"), description: SettingsStore.getValue(UIFeature.Registration) ? _t("Use your account or create a new one to continue.") : _t("Use your account to continue."), - button: _t("Sign In"), + button: _t("action|sign_in"), extraButtons: SettingsStore.getValue(UIFeature.Registration) ? [
    diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 16cd300b7a5..4283840d7e1 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -75,8 +75,8 @@ async function confirmToDismiss(): Promise { title: _t("Cancel entering passphrase?"), description: _t("Are you sure you want to cancel entering passphrase?"), danger: false, - button: _t("Go Back"), - cancelButton: _t("Cancel"), + button: _t("action|go_back"), + cancelButton: _t("action|cancel"), }).finished; return !sure; } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 0a3d2ba8b17..8cb2f781d02 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -18,10 +18,8 @@ limitations under the License. */ import * as React from "react"; -import { User, IContent, Direction } from "matrix-js-sdk/src/matrix"; -import * as ContentHelpers from "matrix-js-sdk/src/content-helpers"; +import { User, IContent, Direction, ContentHelpers, MRoomTopicEventContent } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { MRoomTopicEventContent } from "matrix-js-sdk/src/@types/topic"; import dis from "./dispatcher/dispatcher"; import { _t, _td, UserFriendlyError } from "./languageHandler"; @@ -193,8 +191,7 @@ export const Commands = [ const unixTimestamp = Date.parse(args); if (!unixTimestamp) { throw new UserFriendlyError( - "We were unable to understand the given date (%(inputDate)s). " + - "Try using the format YYYY-MM-DD.", + "We were unable to understand the given date (%(inputDate)s). Try using the format YYYY-MM-DD.", { inputDate: args, cause: undefined }, ); } @@ -402,16 +399,14 @@ export const Commands = [ description: (

    {_t( - "Use an identity server to invite by email. " + - "Click continue to use the default identity server " + - "(%(defaultIdentityServerName)s) or manage in Settings.", + "Use an identity server to invite by email. Click continue to use the default identity server (%(defaultIdentityServerName)s) or manage in Settings.", { defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), }, )}

    ), - button: _t("Continue"), + button: _t("action|continue"), }); prom = finished.then(([useDefault]) => { @@ -461,7 +456,7 @@ export const Commands = [ new Command({ command: "part", args: "[]", - description: _td("Leave room"), + description: _td("action|leave_room"), analyticsName: "Part", isEnabled: (cli) => !isCurrentLocalRoom(cli), runFn: function (cli, roomId, threadId, args) { @@ -718,9 +713,7 @@ export const Commands = [ if (device.getFingerprint() !== fingerprint) { const fprint = device.getFingerprint(); throw new UserFriendlyError( - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session" + - ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + - '"%(fingerprint)s". This could mean your communications are being intercepted!', + 'WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is "%(fprint)s" which does not match the provided key "%(fingerprint)s". This could mean your communications are being intercepted!', { fprint, userId, @@ -740,8 +733,7 @@ export const Commands = [

    {_t( - "The signing key you provided matches the signing key you received " + - "from %(userId)s's session %(deviceId)s. Session marked as verified.", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.", { userId, deviceId }, )}

    diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 93896cd0aa5..a1876839144 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -23,10 +23,11 @@ import { JoinRule, EventType, MsgType, + M_POLL_START, + M_POLL_END, } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils"; -import { M_POLL_START, M_POLL_END } from "matrix-js-sdk/src/@types/polls"; import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent"; import { _t } from "./languageHandler"; @@ -482,17 +483,14 @@ function textForHistoryVisibilityEvent(event: MatrixEvent): (() => string) | nul case HistoryVisibility.Invited: return () => _t( - "%(senderName)s made future room history visible to all room members, " + - "from the point they are invited.", + "%(senderName)s made future room history visible to all room members, from the point they are invited.", { senderName }, ); case HistoryVisibility.Joined: return () => - _t( - "%(senderName)s made future room history visible to all room members, " + - "from the point they joined.", - { senderName }, - ); + _t("%(senderName)s made future room history visible to all room members, from the point they joined.", { + senderName, + }); case HistoryVisibility.Shared: return () => _t("%(senderName)s made future room history visible to all room members.", { senderName }); case HistoryVisibility.WorldReadable: @@ -810,33 +808,31 @@ function textForMjolnirEvent(event: MatrixEvent): (() => string) | null { if (USER_RULE_TYPES.includes(event.getType())) { return () => _t( - "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + - "%(newGlob)s for %(reason)s", + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason }, ); } else if (ROOM_RULE_TYPES.includes(event.getType())) { return () => _t( - "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + - "%(newGlob)s for %(reason)s", + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason }, ); } else if (SERVER_RULE_TYPES.includes(event.getType())) { return () => _t( - "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + - "%(newGlob)s for %(reason)s", + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason }, ); } // Unknown type. We'll say something but we shouldn't end up here. return () => - _t( - "%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " + - "for %(reason)s", - { senderName, oldGlob: prevEntity, newGlob: entity, reason }, - ); + _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s for %(reason)s", { + senderName, + oldGlob: prevEntity, + newGlob: entity, + reason, + }); } export function textForLocationEvent(event: MatrixEvent): () => string { diff --git a/src/Unread.ts b/src/Unread.ts index 6185407a9b4..45f4f1fb820 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; +import { M_BEACON, Room, Thread, MatrixEvent, EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { Room, Thread, MatrixEvent, EventType, MatrixClient } from "matrix-js-sdk/src/matrix"; import shouldHideEvent from "./shouldHideEvent"; import { haveRendererForEvent } from "./events/EventTileFactory"; diff --git a/src/Views.ts b/src/Views.ts index 447dbeab84d..4c7f002d507 100644 --- a/src/Views.ts +++ b/src/Views.ts @@ -20,6 +20,9 @@ enum Views { // trying to re-animate a matrix client or register as a guest. LOADING, + // Another tab holds the lock. + CONFIRM_LOCK_THEFT, + // we are showing the welcome view WELCOME, @@ -48,6 +51,9 @@ enum Views { // We are logged out (invalid token) but have our local state again. The user // should log back in to rehydrate the client. SOFT_LOGOUT, + + // Another instance of the application has started up. We just show an error page. + LOCK_STOLEN, } export default Views; diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts index acbf14d7565..8b6ac184f97 100644 --- a/src/accessibility/KeyboardShortcutUtils.ts +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -25,9 +25,9 @@ import { IKeyboardShortcuts, KeyBindingAction, KEYBOARD_SHORTCUTS, + KeyboardShortcutSetting, MAC_ONLY_SHORTCUTS, } from "./KeyboardShortcuts"; -import { IBaseSetting } from "../settings/Settings"; /** * This function gets the keyboard shortcuts that should be presented in the UI @@ -115,7 +115,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => { export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())] as [ KeyBindingAction, - IBaseSetting, + KeyboardShortcutSetting, ][]; return entries.reduce((acc, [key, value]) => { @@ -130,5 +130,5 @@ export const getKeyboardShortcutValue = (name: KeyBindingAction): KeyCombo | und export const getKeyboardShortcutDisplayName = (name: KeyBindingAction): string | undefined => { const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName; - return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName as string); + return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); }; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index bcd720ee21b..db13ea79eb6 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { _td } from "../languageHandler"; +import { _td, TranslationKey } from "../languageHandler"; import { IS_MAC, Key } from "../Keyboard"; import { IBaseSetting } from "../settings/Settings"; import { KeyCombo } from "../KeyBindingsManager"; @@ -154,13 +154,15 @@ export enum KeyBindingAction { ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility", } -type KeyboardShortcutSetting = Omit, "supportedLevels">; +export type KeyboardShortcutSetting = Omit, "supportedLevels" | "displayName"> & { + displayName?: TranslationKey; +}; // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager export type IKeyboardShortcuts = Partial>; export interface ICategory { - categoryLabel?: string; + categoryLabel?: TranslationKey; // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager settingNames: KeyBindingAction[]; } @@ -179,18 +181,18 @@ export enum CategoryName { // Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts export const DIGITS = "digits"; -export const ALTERNATE_KEY_NAME: Record = { - [Key.PAGE_UP]: _td("Page Up"), - [Key.PAGE_DOWN]: _td("Page Down"), - [Key.ESCAPE]: _td("Esc"), - [Key.ENTER]: _td("Enter"), - [Key.SPACE]: _td("Space"), - [Key.HOME]: _td("Home"), - [Key.END]: _td("End"), - [Key.ALT]: _td("Alt"), - [Key.CONTROL]: _td("Ctrl"), - [Key.SHIFT]: _td("Shift"), - [DIGITS]: _td("[number]"), +export const ALTERNATE_KEY_NAME: Record = { + [Key.PAGE_UP]: _td("keyboard|page_up"), + [Key.PAGE_DOWN]: _td("keyboard|page_down"), + [Key.ESCAPE]: _td("keyboard|escape"), + [Key.ENTER]: _td("keyboard|enter"), + [Key.SPACE]: _td("keyboard|space"), + [Key.HOME]: _td("keyboard|home"), + [Key.END]: _td("keyboard|end"), + [Key.ALT]: _td("keyboard|alt"), + [Key.CONTROL]: _td("keyboard|control"), + [Key.SHIFT]: _td("keyboard|shift"), + [DIGITS]: _td("keyboard|number"), }; export const KEY_ICON: Record = { [Key.ARROW_UP]: "↑", @@ -231,7 +233,7 @@ export const CATEGORIES: Record = { settingNames: [KeyBindingAction.ToggleMicInCall, KeyBindingAction.ToggleWebcamInCall], }, [CategoryName.ROOM]: { - categoryLabel: _td("Room"), + categoryLabel: _td("common|room"), settingNames: [ KeyBindingAction.SearchInRoom, KeyBindingAction.UploadFile, @@ -301,7 +303,7 @@ export const CATEGORIES: Record = { ], }, [CategoryName.LABS]: { - categoryLabel: _td("Labs"), + categoryLabel: _td("common|labs"), settingNames: [KeyBindingAction.ToggleHiddenEventVisibility], }, }; diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx index a1e9485e027..dbedac0db04 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx @@ -62,7 +62,7 @@ export default class DisableEventIndexDialog extends React.Component :
    } {eventIndexingSettings} diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index 2a02945621f..88e69032a26 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -131,7 +131,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent

    {_t("Your keys are being backed up (the first backup could take a few minutes).")}

    - +
    ); } @@ -154,7 +154,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent

    {_t("Unable to create key backup")}

    {_t( - "Safeguard against losing access to encrypted messages & data by " + - "backing up encryption keys on your server.", + "Safeguard against losing access to encrypted messages & data by backing up encryption keys on your server.", )}

    @@ -569,7 +567,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent @@ -588,7 +586,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

    {_t( - "Upgrade this session to allow it to verify other sessions, " + - "granting them access to encrypted messages and marking them " + - "as trusted for other users.", + "Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.", )}

    {authPrompt}
    @@ -625,7 +621,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent
    @@ -637,8 +633,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

    {_t( - "Enter a Security Phrase only you know, as it's used to safeguard your data. " + - "To be secure, you shouldn't re-use your account password.", + "Enter a Security Phrase only you know, as it's used to safeguard your data. To be secure, you shouldn't re-use your account password.", )}

    @@ -659,13 +654,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent @@ -717,13 +712,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent{passPhraseMatch}
    @@ -735,7 +730,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

    {_t( - "Store your Security Key somewhere safe, like a password manager or a safe, " + - "as it's used to safeguard your encrypted data.", + "Store your Security Key somewhere safe, like a password manager or a safe, as it's used to safeguard your encrypted data.", )}

    @@ -769,7 +763,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent - {_t("Download")} + {_t("action|download")} {_t("%(downloadButton)s or %(copyButton)s", { @@ -783,7 +777,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent - {this.state.copied ? _t("Copied!") : _t("Copy")} + {this.state.copied ? _t("Copied!") : _t("action|copy")}
    @@ -806,7 +800,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent

    {_t("Your keys are now being backed up from this device.")}

    this.props.onFinished(true)} hasCancel={false} /> @@ -820,7 +814,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent{_t("Unable to query secret storage status")}

    {_t("You can also set up Secure Backup & manage your keys in Settings.")}

    @@ -895,7 +889,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent{_t("Unable to set up secret storage")}

    {_t( - "This process allows you to export the keys for messages " + - "you have received in encrypted rooms to a local file. You " + - "will then be able to import the file into another Matrix " + - "client in the future, so that client will also be able to " + - "decrypt these messages.", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", )}

    {_t( - "The exported file will allow anyone who can read it to decrypt " + - "any encrypted messages that you can see, so you should be " + - "careful to keep it secure. To help with this, you should enter " + - "a unique passphrase below, which will only be used to encrypt the " + - "exported data. " + - "It will only be possible to import the data by using the same passphrase.", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a unique passphrase below, which will only be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", )}

    {this.state.errStr}
    @@ -229,7 +220,7 @@ export default class ExportE2eKeysDialog extends React.Component disabled={disableForm} />
    diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index e9bf268cbae..b65e104170d 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -146,16 +146,12 @@ export default class ImportE2eKeysDialog extends React.Component

    {_t( - "This process allows you to import encryption keys " + - "that you had previously exported from another Matrix " + - "client. You will then be able to decrypt any " + - "messages that the other client could decrypt.", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", )}

    {_t( - "The export file will be protected with a passphrase. " + - "You should enter the passphrase here, to decrypt the file.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", )}

    {this.state.errStr}
    @@ -195,7 +191,7 @@ export default class ImportE2eKeysDialog extends React.Component disabled={!this.state.enableSubmit || disableForm} />
    diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index a7aa464b8d1..db18b4e54d9 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -62,10 +62,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent const hackWarning = (

    {_t( - "If you didn't set the new recovery method, an " + - "attacker may be trying to access your account. " + - "Change your account password and set a new recovery " + - "method immediately in Settings.", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", )}

    ); @@ -78,7 +75,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent

    {_t("This session is encrypting history using the new recovery method.")}

    {hackWarning}

    {_t( - "This session has detected that your Security Phrase and key " + - "for Secure Messages have been removed.", + "This session has detected that your Security Phrase and key for Secure Messages have been removed.", )}

    {_t( - "If you did this accidentally, you can setup Secure Messages on " + - "this session which will re-encrypt this session's message " + - "history with a new recovery method.", + "If you did this accidentally, you can setup Secure Messages on this session which will re-encrypt this session's message history with a new recovery method.", )}

    {_t( - "If you didn't remove the recovery method, an " + - "attacker may be trying to access your account. " + - "Change your account password and set a new recovery " + - "method immediately in Settings.", + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", )}

    ; public nameMatcher: QueryMatcher; - private readonly recentlyUsed: IEmoji[]; + private readonly recentlyUsed: Emoji[]; private emotes: Map = new Map(); private emotesPromise?: Promise>; public constructor(room: Room, renderingType?: TimelineRenderingType) { diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 6d23c3694dd..8bad2c87187 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -56,7 +56,7 @@ export default class NotifProvider extends AutocompleteProvider { suffix: " ", component: ( - + ), range: range!, diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index c60c901f7c1..3d7a8ba1c1b 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -122,7 +122,7 @@ export default class RoomProvider extends AutocompleteProvider { href: makeRoomPermalink(this.room.client, room.displayedAlias), component: ( - + ), range: range!, diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index a934e9309a7..519c65344e3 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -135,7 +135,7 @@ export default class UserProvider extends AutocompleteProvider { href: makeUserPermalink(user.userId), component: ( - + ), range: range!, diff --git a/src/boundThreepids.ts b/src/boundThreepids.ts index 42a81876fb2..e8fee803bec 100644 --- a/src/boundThreepids.ts +++ b/src/boundThreepids.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IThreepid, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; -import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; +import { IThreepid, ThreepidMedium, MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; import IdentityAuthClient from "./IdentityAuthClient"; diff --git a/src/components/structures/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index 7ee30cb3ec6..6b063db9985 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -21,7 +21,7 @@ import sanitizeHtml from "sanitize-html"; import classnames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t } from "../../languageHandler"; +import { _t, TranslationKey } from "../../languageHandler"; import dis from "../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import MatrixClientContext from "../../contexts/MatrixClientContext"; @@ -56,7 +56,7 @@ export default class EmbeddedPage extends React.PureComponent { }; } - private translate(s: string): string { + private translate(s: TranslationKey): string { return sanitizeHtml(_t(s)); } diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 115da051188..f16f4113361 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -59,7 +59,7 @@ const getOwnProfile = ( avatarUrl?: string; } => ({ displayName: OwnProfileStore.instance.displayName || userId, - avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE) ?? undefined, + avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(parseInt(AVATAR_SIZE, 10)) ?? undefined, }); const UserWelcomeTop: React.FC = () => { @@ -84,9 +84,7 @@ const UserWelcomeTop: React.FC = () => { idName={userId} name={ownProfile.displayName} url={ownProfile.avatarUrl} - width={AVATAR_SIZE} - height={AVATAR_SIZE} - resizeMethod="crop" + size={AVATAR_SIZE + "px"} /> @@ -106,7 +104,7 @@ const HomePage: React.FC = ({ justRegistered = false }) => { } let introSection: JSX.Element; - if (justRegistered || !OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE)) { + if (justRegistered || !OwnProfileStore.instance.getHttpAvatarUrl(parseInt(AVATAR_SIZE, 10))) { introSection = ; } else { const brandingConfig = SdkConfig.getObject("branding"); diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 67c765760d5..d35d403e5f0 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -35,8 +35,8 @@ type InteractiveAuthCallbackSuccess = ( success: true, response: T, extra?: { emailSid?: string; clientSecret?: string }, -) => void; -type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void; +) => Promise; +type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => Promise; export type InteractiveAuthCallback = InteractiveAuthCallbackSuccess & InteractiveAuthCallbackFailure; export interface InteractiveAuthProps { @@ -141,15 +141,15 @@ export default class InteractiveAuthComponent extends React.Component { + .then(async (result) => { const extra = { emailSid: this.authLogic.getEmailSid(), clientSecret: this.authLogic.getClientSecret(), }; - this.props.onAuthFinished(true, result, extra); + await this.props.onAuthFinished(true, result, extra); }) - .catch((error) => { - this.props.onAuthFinished(false, error); + .catch(async (error) => { + await this.props.onAuthFinished(false, error); logger.error("Error during user-interactive auth:", error); if (this.unmounted) { return; @@ -251,12 +251,12 @@ export default class InteractiveAuthComponent extends React.Component { - this.props.onAuthFinished(false, ERROR_USER_CANCELLED); + private onStageCancel = async (): Promise => { + await this.props.onAuthFinished(false, ERROR_USER_CANCELLED); }; - private onAuthStageFailed = (e: Error): void => { - this.props.onAuthFinished(false, e); + private onAuthStageFailed = async (e: Error): Promise => { + await this.props.onAuthFinished(false, e); }; private setEmailSid = (sid: string): void => { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 49981b781a9..401dfc712fa 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -140,12 +140,16 @@ import { SdkContextClass, SDKContext } from "../../contexts/SDKContext"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast"; import GenericToast from "../views/toasts/GenericToast"; -import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog"; +import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import { findDMForUser } from "../../utils/dm/findDMForUser"; import { Linkify } from "../../HtmlUtils"; import { NotificationColor } from "../../stores/notifications/NotificationColor"; import { UserTab } from "../views/dialogs/UserTab"; import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption"; +import { Filter } from "../views/dialogs/spotlight/Filter"; +import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock"; +import { SessionLockStolenView } from "./auth/SessionLockStolenView"; +import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView"; // legacy export export { default as Views } from "../../Views"; @@ -176,8 +180,6 @@ interface IProps { initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. defaultDeviceDisplayName?: string; - // A function that makes a registration URL - makeRegistrationUrl: (params: QueryDict) => string; } interface IState { @@ -308,11 +310,23 @@ export default class MatrixChat extends React.PureComponent { initSentry(SdkConfig.get("sentry")); + if (!checkSessionLockFree()) { + // another instance holds the lock; confirm its theft before proceeding + setTimeout(() => this.setState({ view: Views.CONFIRM_LOCK_THEFT }), 0); + } else { + this.startInitSession(); + } + } + + /** + * Kick off a call to {@link initSession}, and handle any errors + */ + private startInitSession = (): void => { this.initSession().catch((err) => { // TODO: show an error screen, rather than a spinner of doom logger.error("Error initialising Matrix session", err); }); - } + }; /** * Do what we can to establish a Matrix session. @@ -325,6 +339,13 @@ export default class MatrixChat extends React.PureComponent { * * If all else fails, present a login screen. */ private async initSession(): Promise { + // The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so + // make sure we are the only Element instance in town (on this browser/domain). + if (!(await getSessionLock(() => this.onSessionLockStolen()))) { + // we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do. + return; + } + // If the user was soft-logged-out, we want to make the SoftLogout component responsible for doing any // token auth (rather than Lifecycle.attemptDelegatedAuthLogin), since SoftLogout knows about submitting the // device ID and preserving the session. @@ -379,6 +400,18 @@ export default class MatrixChat extends React.PureComponent { } } + private async onSessionLockStolen(): Promise { + // switch to the LockStolenView. We deliberately do this immediately, rather than going through the dispatcher, + // because there can be a substantial queue in the dispatcher, and some of the events in it might require an + // active MatrixClient. + await new Promise((resolve) => { + this.setState({ view: Views.LOCK_STOLEN }, resolve); + }); + + // now we can tell the Lifecycle routines to abort any active startup, and to stop the active client. + await Lifecycle.onSessionLockStolen(); + } + private async postLoginSetup(): Promise { const cli = MatrixClientPeg.safeGet(); const cryptoEnabled = cli.isCryptoEnabled(); @@ -483,12 +516,10 @@ export default class MatrixChat extends React.PureComponent { const waitText = _t("Wait!"); const scamText = _t( - "If someone told you to copy/paste something here, " + "there is a high likelihood you're being scammed!", + "If someone told you to copy/paste something here, there is a high likelihood you're being scammed!", ); const devText = _t( - "If you know what you're doing, Element is open-source, " + - "be sure to check out our GitHub (https://github.com/vector-im/element-web/) " + - "and contribute!", + "If you know what you're doing, Element is open-source, be sure to check out our GitHub (https://github.com/vector-im/element-web/) and contribute!", ); global.mx_rage_logger.bypassRageshake( @@ -577,6 +608,11 @@ export default class MatrixChat extends React.PureComponent { } private onAction = (payload: ActionPayload): void => { + // once the session lock has been stolen, don't try to do anything. + if (this.state.view === Views.LOCK_STOLEN) { + return; + } + // Start the onboarding process for certain actions if (MatrixClientPeg.get()?.isGuest() && ONBOARDING_FLOW_STARTERS.includes(payload.action)) { // This will cause `payload` to be dispatched later, once a @@ -900,6 +936,18 @@ export default class MatrixChat extends React.PureComponent { break; } + case Action.OpenSpotlight: + Modal.createDialog( + RovingSpotlightDialog, + { + initialText: payload.initialText, + initialFilter: payload.initialFilter, + }, + "mx_SpotlightDialog_wrapper", + false, + true, + ); + break; } }; @@ -1155,8 +1203,7 @@ export default class MatrixChat extends React.PureComponent { {" " /* Whitespace, otherwise the sentences get smashed together */} {_t( - "You are the only person here. " + - "If you leave, no one will be able to join in the future, including you.", + "You are the only person here. If you leave, no one will be able to join in the future, including you.", )} , ); @@ -1188,7 +1235,7 @@ export default class MatrixChat extends React.PureComponent { const isSpace = roomToLeave?.isSpaceRoom(); Modal.createDialog(QuestionDialog, { - title: isSpace ? _t("Leave space") : _t("Leave room"), + title: isSpace ? _t("Leave space") : _t("action|leave_room"), description: ( {isSpace @@ -1201,7 +1248,7 @@ export default class MatrixChat extends React.PureComponent { {warnings} ), - button: _t("Leave"), + button: _t("action|leave"), onFinished: async (shouldLeave) => { if (shouldLeave) { await leaveRoomBehaviour(cli, roomId); @@ -1390,7 +1437,7 @@ export default class MatrixChat extends React.PureComponent { title: userNotice.title, props: { description: {userNotice.description}, - acceptLabel: _t("OK"), + acceptLabel: _t("action|ok"), onAccept: () => { ToastStore.sharedInstance().dismissToast(key); localStorage.setItem(key, "1"); @@ -1582,15 +1629,14 @@ export default class MatrixChat extends React.PureComponent {

    {" "} {_t( - "To continue using the %(homeserverDomain)s homeserver " + - "you must review and agree to our terms and conditions.", + "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.", { homeserverDomain: cli.getDomain() }, )}

    ), button: _t("Review terms and conditions"), - cancelButton: _t("Dismiss"), + cancelButton: _t("action|dismiss"), onFinished: (confirmed) => { if (confirmed) { const wnd = window.open(consentUri, "_blank")!; @@ -1632,13 +1678,7 @@ export default class MatrixChat extends React.PureComponent { Modal.createDialog(ErrorDialog, { title: _t("Old cryptography data detected"), description: _t( - "Data from an older version of %(brand)s has been detected. " + - "This will have caused end-to-end cryptography to malfunction " + - "in the older version. End-to-end encrypted messages exchanged " + - "recently whilst using the older version may not be decryptable " + - "in this version. This may also cause messages exchanged with this " + - "version to fail. If you experience problems, log out and back in " + - "again. To retain message history, export and re-import your keys.", + "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", { brand: SdkConfig.get().brand }, ), }); @@ -1989,7 +2029,7 @@ export default class MatrixChat extends React.PureComponent { this.subTitleStatus = ""; if (state === SyncState.Error) { - this.subTitleStatus += `[${_t("Offline")}] `; + this.subTitleStatus += `[${_t("common|offline")}] `; } if (numUnreadRooms > 0) { this.subTitleStatus += `[${numUnreadRooms}]`; @@ -2004,13 +2044,6 @@ export default class MatrixChat extends React.PureComponent { this.setState({ serverConfig }); }; - private makeRegistrationUrl = (params: QueryDict): string => { - if (this.props.startingFragmentQueryParams?.referrer) { - params.referrer = this.props.startingFragmentQueryParams.referrer; - } - return this.props.makeRegistrationUrl(params); - }; - /** * After registration or login, we run various post-auth steps before entering the app * proper, such setting up cross-signing or verifying the new session. @@ -2057,6 +2090,15 @@ export default class MatrixChat extends React.PureComponent {
    ); + } else if (this.state.view === Views.CONFIRM_LOCK_THEFT) { + view = ( + { + this.setState({ view: Views.LOADING }); + this.startInitSession(); + }} + /> + ); } else if (this.state.view === Views.COMPLETE_SECURITY) { view = ; } else if (this.state.view === Views.E2E_SETUP) { @@ -2104,7 +2146,7 @@ export default class MatrixChat extends React.PureComponent {
    - {_t("Logout")} + {_t("action|logout")}
    @@ -2121,7 +2163,6 @@ export default class MatrixChat extends React.PureComponent { idSid={this.state.register_id_sid} email={email} brand={this.props.config.brand} - makeRegistrationUrl={this.makeRegistrationUrl} onLoggedIn={this.onRegisterFlowComplete} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} @@ -2164,6 +2205,8 @@ export default class MatrixChat extends React.PureComponent { ); } else if (this.state.view === Views.USE_CASE_SELECTION) { view = => this.onShowPostLoginScreen(useCase)} />; + } else if (this.state.view === Views.LOCK_STOLEN) { + view = ; } else { logger.error(`Unknown view ${this.state.view}`); return null; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 957377f4776..0760d316b75 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -17,9 +17,16 @@ limitations under the License. import React, { createRef, ReactNode, TransitionEvent } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; -import { Room, MatrixClient, RoomStateEvent, EventStatus, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix"; +import { + Room, + MatrixClient, + RoomStateEvent, + EventStatus, + MatrixEvent, + EventType, + M_BEACON_INFO, +} from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; import { Optional } from "matrix-events-sdk"; diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index a387a2e0d54..33644676410 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -22,9 +22,8 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import { ActionPayload } from "../../dispatcher/payloads"; import { IS_MAC, Key } from "../../Keyboard"; import { _t } from "../../languageHandler"; -import Modal from "../../Modal"; -import SpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import AccessibleButton from "../views/elements/AccessibleButton"; +import { Action } from "../../dispatcher/actions"; interface IProps { isMinimized: boolean; @@ -44,7 +43,7 @@ export default class RoomSearch extends React.PureComponent { } private openSpotlight(): void { - Modal.createDialog(SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true); + defaultDispatcher.fire(Action.OpenSpotlight); } private onAction = (payload: ActionPayload): void => { @@ -73,7 +72,9 @@ export default class RoomSearch extends React.PureComponent { return ( {icon} - {!this.props.isMinimized &&
    {_t("Search")}
    } + {!this.props.isMinimized && ( +
    {_t("action|search")}
    + )} {shortcutPrompt}
    ); diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 1b3d8651948..bb701ed5754 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -199,7 +199,7 @@ export const RoomSearchView = forwardRef( if (!results?.results?.length) { ret.push(
  • -

    {_t("No results")}

    +

    {_t("common|no_results")}

  • , ); } else { @@ -256,7 +256,7 @@ export const RoomSearchView = forwardRef( ret.push(
  • - {_t("Room")}: {room.name} + {_t("common|room")}: {room.name}

  • , ); diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index 98039b1abcf..58c8225ab98 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -207,8 +207,7 @@ export default class RoomStatusBar extends React.PureComponent { } if (consentError) { title = _t( - "You can't send any messages until you review and agree to " + - "our terms and conditions.", + "You can't send any messages until you review and agree to our terms and conditions.", {}, { consentLink: (sub) => ( @@ -224,16 +223,13 @@ export default class RoomStatusBar extends React.PureComponent { resourceLimitError.data.admin_contact, { "monthly_active_user": _td( - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + - "Please contact your service administrator to continue using the service.", + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", ), "hs_disabled": _td( - "Your message wasn't sent because this homeserver has been blocked by its administrator. " + - "Please contact your service administrator to continue using the service.", + "Your message wasn't sent because this homeserver has been blocked by its administrator. Please contact your service administrator to continue using the service.", ), "": _td( - "Your message wasn't sent because this homeserver has exceeded a resource limit. " + - "Please contact your service administrator to continue using the service.", + "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", ), }, ); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fe4d0c957c8..e8521aee24a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -286,7 +286,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { if (room.isError) { const buttons = ( - {_t("Retry")} + {_t("action|retry")} ); diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 4640ee9295f..b2524063d1e 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -38,9 +38,10 @@ import { RoomType, GuestAccess, HistoryVisibility, + HierarchyRelation, + HierarchyRoom, } from "matrix-js-sdk/src/matrix"; import { RoomHierarchy } from "matrix-js-sdk/src/room-hierarchy"; -import { IHierarchyRelation, IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; import classNames from "classnames"; import { sortBy, uniqBy } from "lodash"; import { logger } from "matrix-js-sdk/src/logger"; @@ -85,7 +86,7 @@ interface IProps { } interface ITileProps { - room: IHierarchyRoom; + room: HierarchyRoom; suggested?: boolean; selected?: boolean; numChildRooms?: number; @@ -162,13 +163,13 @@ const Tile: React.FC = ({ onFocus={onFocus} tabIndex={isActive ? 0 : -1} > - {_t("View")} + {_t("action|view")} ); } else { button = ( - {_t("Join")} + {_t("action|join")} ); } @@ -193,15 +194,14 @@ const Tile: React.FC = ({ let avatar: ReactElement; if (joinedRoom) { - avatar = ; + avatar = ; } else { avatar = ( ); } @@ -429,8 +429,8 @@ export const joinRoom = async (cli: MatrixClient, hierarchy: RoomHierarchy, room }; interface IHierarchyLevelProps { - root: IHierarchyRoom; - roomSet: Set; + root: HierarchyRoom; + roomSet: Set; hierarchy: RoomHierarchy; parents: Set; selectedMap?: Map>; @@ -439,7 +439,7 @@ interface IHierarchyLevelProps { onToggleClick?(parentId: string, childId: string): void; } -export const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: RoomHierarchy): IHierarchyRoom => { +export const toLocalRoom = (cli: MatrixClient, room: HierarchyRoom, hierarchy: RoomHierarchy): HierarchyRoom => { const history = cli.getRoomUpgradeHistory( room.room_id, true, @@ -497,14 +497,14 @@ export const HierarchyLevel: React.FC = ({ }); const [subspaces, childRooms] = sortedChildren.reduce( - (result, ev: IHierarchyRelation) => { + (result, ev: HierarchyRelation) => { const room = hierarchy.roomMap.get(ev.state_key); if (room && roomSet.has(room)) { result[room.room_type === RoomType.Space ? 0 : 1].push(toLocalRoom(cli, room, hierarchy)); } return result; }, - [[] as IHierarchyRoom[], [] as IHierarchyRoom[]], + [[] as HierarchyRoom[], [] as HierarchyRoom[]], ); const newParents = new Set(parents).add(root.room_id); @@ -564,12 +564,12 @@ export const useRoomHierarchy = ( space: Room, ): { loading: boolean; - rooms?: IHierarchyRoom[]; + rooms?: HierarchyRoom[]; hierarchy?: RoomHierarchy; error?: Error; loadMore(pageSize?: number): Promise; } => { - const [rooms, setRooms] = useState([]); + const [rooms, setRooms] = useState([]); const [hierarchy, setHierarchy] = useState(); const [error, setError] = useState(); @@ -715,7 +715,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set kind="danger_outline" disabled={disabled} > - {removing ? _t("Removing…") : _t("Remove")} + {removing ? _t("Removing…") : _t("action|remove")} +
    )} diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index 8b1fe716f9d..418199d5d91 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -75,8 +75,7 @@ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resize className="mx_cryptoEvent mx_cryptoEvent_icon" title={_t("Waiting for users to join %(brand)s", { brand })} subtitle={_t( - "Once invited users have joined %(brand)s, " + - "you will be able to chat and the room will be end-to-end encrypted", + "Once invited users have joined %(brand)s, you will be able to chat and the room will be end-to-end encrypted", { brand }, )} /> diff --git a/src/components/structures/auth/ConfirmSessionLockTheftView.tsx b/src/components/structures/auth/ConfirmSessionLockTheftView.tsx new file mode 100644 index 00000000000..36d612d21bf --- /dev/null +++ b/src/components/structures/auth/ConfirmSessionLockTheftView.tsx @@ -0,0 +1,51 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import { _t } from "../../../languageHandler"; +import SdkConfig from "../../../SdkConfig"; +import AccessibleButton from "../../views/elements/AccessibleButton"; + +interface Props { + /** Callback which the view will call if the user confirms they want to use this window */ + onConfirm: () => void; +} + +/** + * Component shown by {@link MatrixChat} when another session is already active in the same browser and we need to + * confirm if we should steal its lock + */ +export function ConfirmSessionLockTheftView(props: Props): JSX.Element { + const brand = SdkConfig.get().brand; + + return ( +
    +
    +

    + {_t( + '%(brand)s is open in another window. Click "%(label)s" to use %(brand)s here and disconnect the other window.', + { brand, label: _t("action|continue") }, + )} +

    + + + {_t("action|continue")} + +
    +
    + ); +} diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 14d58f372fa..f3026bbec61 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -354,19 +354,17 @@ export default class ForgotPassword extends React.Component {

    {_t( - "Signing out your devices will delete the message encryption keys stored on them, " + - "making encrypted chat history unreadable.", + "Signing out your devices will delete the message encryption keys stored on them, making encrypted chat history unreadable.", )}

    {_t( - "If you want to retain access to your chat history in encrypted rooms, set up Key Backup " + - "or export your message keys from one of your other devices before proceeding.", + "If you want to retain access to your chat history in encrypted rooms, set up Key Backup or export your message keys from one of your other devices before proceeding.", )}

    ), - button: _t("Continue"), + button: _t("action|continue"), }); const [confirmed] = await finished; return !!confirmed; @@ -443,9 +441,7 @@ export default class ForgotPassword extends React.Component { {this.state.logoutDevices ? (

    {_t( - "You have been logged out of all devices and will no longer receive " + - "push notifications. To re-enable notifications, sign in again on each " + - "device.", + "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device.", )}

    ) : null} diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 241c2dcc719..7f3a326b58a 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; +import { SSOFlow, SSOAction } from "matrix-js-sdk/src/matrix"; import { _t, _td, UserFriendlyError } from "../../../languageHandler"; import Login, { ClientLoginFlow, OidcNativeFlow } from "../../../Login"; @@ -481,13 +481,13 @@ export default class LoginComponent extends React.PureComponent ); }} > - {_t("Continue")} + {_t("action|continue")} ); }; private renderSsoStep = (loginType: "cas" | "sso"): JSX.Element => { - const flow = this.state.flows?.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; + const flow = this.state.flows?.find((flow) => flow.type === "m.login." + loginType) as SSOFlow; return (

    - {_t("Sign in")} + {_t("action|sign_in")} {loader}

    {errorTextSection} diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 2a8f89a5cdb..88725be2662 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -24,12 +24,13 @@ import { IRegisterRequestParams, IRequestTokenResponse, MatrixClient, + SSOFlow, + SSOAction, + RegisterResponse, } from "matrix-js-sdk/src/matrix"; import React, { Fragment, ReactNode } from "react"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; -import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; -import { RegisterResponse } from "matrix-js-sdk/src/@types/registration"; import { _t } from "../../../languageHandler"; import { adminContactStrings, messageForResourceLimitError, resourceLimitStrings } from "../../../utils/ErrorUtils"; @@ -73,15 +74,7 @@ interface IProps { // - The user's password, if available and applicable (may be cached in memory // for a short time so the user is not required to re-enter their password // for operations like uploading cross-signing keys). - onLoggedIn(params: IMatrixClientCreds, password: string): void; - makeRegistrationUrl(params: { - /* eslint-disable camelcase */ - client_secret: string; - hs_url: string; - is_url?: string; - session_id: string; - /* eslint-enable camelcase */ - }): string; + onLoggedIn(params: IMatrixClientCreds, password: string): Promise; // registration shouldn't know or care how login is done. onLoginClick(): void; onServerConfigChange(config: ValidatedServerConfig): void; @@ -129,7 +122,7 @@ interface IState { differentLoggedInUserId?: string; // the SSO flow definition, this is fetched from /login as that's the only // place it is exposed. - ssoFlow?: ISSOFlow; + ssoFlow?: SSOFlow; } export default class Registration extends React.Component { @@ -227,11 +220,11 @@ export default class Registration extends React.Component { this.loginLogic.setHomeserverUrl(hsUrl); this.loginLogic.setIdentityServerUrl(isUrl); - let ssoFlow: ISSOFlow | undefined; + let ssoFlow: SSOFlow | undefined; try { const loginFlows = await this.loginLogic.getFlows(); if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us - ssoFlow = loginFlows.find((f) => f.type === "m.login.sso" || f.type === "m.login.cas") as ISSOFlow; + ssoFlow = loginFlows.find((f) => f.type === "m.login.sso" || f.type === "m.login.cas") as SSOFlow; } catch (e) { if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us logger.error("Failed to get login flows to check for SSO support", e); @@ -302,17 +295,7 @@ export default class Registration extends React.Component { sessionId: string, ): Promise => { if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); - return this.state.matrixClient.requestRegisterEmailToken( - emailAddress, - clientSecret, - sendAttempt, - this.props.makeRegistrationUrl({ - client_secret: clientSecret, - hs_url: this.state.matrixClient.getHomeserverUrl(), - is_url: this.state.matrixClient.getIdentityServerUrl(), - session_id: sessionId, - }), - ); + return this.state.matrixClient.requestRegisterEmailToken(emailAddress, clientSecret, sendAttempt); }; private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { @@ -401,9 +384,7 @@ export default class Registration extends React.Component { const hasAccessToken = Boolean(accessToken); debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken }); // don’t log in if we found a session for a different user - if (!hasEmail && hasAccessToken && !newState.differentLoggedInUserId) { - // we'll only try logging in if we either have no email to verify at all or we're the client that verified - // the email, not the client that started the registration flow + if (hasAccessToken && !newState.differentLoggedInUserId) { await this.props.onLoggedIn( { userId, @@ -628,7 +609,7 @@ export default class Registration extends React.Component { if (this.state.doingUIAuth) { goBack = ( - {_t("Go back")} + {_t("action|go_back")} ); } @@ -641,8 +622,7 @@ export default class Registration extends React.Component {

    {_t( - "Your new account (%(newAccountId)s) is registered, but you're already " + - "logged into a different account (%(loggedInUserId)s).", + "Your new account (%(newAccountId)s) is registered, but you're already logged into a different account (%(loggedInUserId)s).", { newAccountId: this.state.registeredUsername, loggedInUserId: this.state.differentLoggedInUserId, diff --git a/src/components/structures/auth/SessionLockStolenView.tsx b/src/components/structures/auth/SessionLockStolenView.tsx new file mode 100644 index 00000000000..0c0bc018499 --- /dev/null +++ b/src/components/structures/auth/SessionLockStolenView.tsx @@ -0,0 +1,35 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import SplashPage from "../SplashPage"; +import { _t } from "../../../languageHandler"; +import SdkConfig from "../../../SdkConfig"; + +/** + * Component shown by {@link MatrixChat} when another session is started in the same browser. + */ +export function SessionLockStolenView(): JSX.Element { + const brand = SdkConfig.get().brand; + + return ( + +

    {_t("common|error")}

    +

    {_t("%(brand)s has been opened in another tab.", { brand })}

    + + ); +} diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 1de7cc7d83f..f8781cdb0f5 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -161,10 +161,7 @@ export default class SetupEncryptionBody extends React.Component

    {_t( - "It looks like you don't have a Security Key or any other devices you can " + - "verify against. This device will not be able to access old encrypted messages. " + - "In order to verify your identity on this device, you'll need to reset " + - "your verification keys.", + "It looks like you don't have a Security Key or any other devices you can verify against. This device will not be able to access old encrypted messages. In order to verify your identity on this device, you'll need to reset your verification keys.", )}

    @@ -234,8 +231,7 @@ export default class SetupEncryptionBody extends React.Component message = (

    {_t( - "Your new device is now verified. It has access to your " + - "encrypted messages, and other users will see it as trusted.", + "Your new device is now verified. It has access to your encrypted messages, and other users will see it as trusted.", )}

    ); @@ -248,7 +244,7 @@ export default class SetupEncryptionBody extends React.Component {message}
    - {_t("Done")} + {_t("action|done")}
    @@ -258,8 +254,7 @@ export default class SetupEncryptionBody extends React.Component

    {_t( - "Without verifying, you won't have access to all your messages " + - "and may appear as untrusted to others.", + "Without verifying, you won't have access to all your messages and may appear as untrusted to others.", )}

    @@ -267,7 +262,7 @@ export default class SetupEncryptionBody extends React.Component {_t("I'll verify later")} - {_t("Go Back")} + {_t("action|go_back")}
    @@ -277,16 +272,12 @@ export default class SetupEncryptionBody extends React.Component

    {_t( - "Resetting your verification keys cannot be undone. After resetting, " + - "you won't have access to old encrypted messages, and any friends who " + - "have previously verified you will see security warnings until you " + - "re-verify with them.", + "Resetting your verification keys cannot be undone. After resetting, you won't have access to old encrypted messages, and any friends who have previously verified you will see security warnings until you re-verify with them.", )}

    {_t( - "Please only proceed if you're sure you've lost all of your other " + - "devices and your Security Key.", + "Please only proceed if you're sure you've lost all of your other devices and your Security Key.", )}

    @@ -295,7 +286,7 @@ export default class SetupEncryptionBody extends React.Component {_t("Proceed with reset")} - {_t("Go Back")} + {_t("action|go_back")}
    diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 4ff18492ef7..57a39c74426 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -17,8 +17,7 @@ limitations under the License. import React, { ChangeEvent, SyntheticEvent } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; -import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; -import { MatrixError } from "matrix-js-sdk/src/matrix"; +import { LoginFlow, MatrixError, SSOAction, SSOFlow } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -64,7 +63,6 @@ interface IProps { interface IState { loginView: LoginView; - keyBackupNeeded: boolean; busy: boolean; password: string; errorText: string; @@ -77,7 +75,6 @@ export default class SoftLogout extends React.Component { this.state = { loginView: LoginView.Loading, - keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) busy: false, password: "", errorText: "", @@ -93,13 +90,6 @@ export default class SoftLogout extends React.Component { } this.initLogin(); - - const cli = MatrixClientPeg.safeGet(); - if (cli.isCryptoEnabled()) { - cli.countSessionsNeedingBackup().then((remaining) => { - this.setState({ keyBackupNeeded: remaining > 0 }); - }); - } } private onClearAll = (): void => { @@ -235,7 +225,7 @@ export default class SoftLogout extends React.Component { {error} { type="submit" disabled={this.state.busy} > - {_t("Sign In")} + {_t("action|sign_in")} {_t("Forgotten your password?")} @@ -257,7 +247,7 @@ export default class SoftLogout extends React.Component { private renderSsoForm(introText: Optional): JSX.Element { const loginType = this.state.loginView === LoginView.CAS ? "cas" : "sso"; - const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; + const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as SSOFlow; return (
    @@ -279,42 +269,22 @@ export default class SoftLogout extends React.Component { return ; } - let introText: string | null = null; // null is translated to something area specific in this function - if (this.state.keyBackupNeeded) { - introText = _t( - "Regain access to your account and recover encryption keys stored in this session. " + - "Without them, you won't be able to read all of your secure messages in any session.", - ); - } - if (this.state.loginView === LoginView.Password) { - if (!introText) { - introText = _t("Enter your password to sign in and regain access to your account."); - } // else we already have a message and should use it (key backup warning) - - return this.renderPasswordForm(introText); + return this.renderPasswordForm(_t("Enter your password to sign in and regain access to your account.")); } if (this.state.loginView === LoginView.SSO || this.state.loginView === LoginView.CAS) { - if (!introText) { - introText = _t("Sign in and regain access to your account."); - } // else we already have a message and should use it (key backup warning) - - return this.renderSsoForm(introText); + return this.renderSsoForm(_t("Sign in and regain access to your account.")); } if (this.state.loginView === LoginView.PasswordWithSocialSignOn) { - if (!introText) { - introText = _t("Sign in and regain access to your account."); - } - // We render both forms with no intro/error to ensure the layout looks reasonably // okay enough. // // Note: "mx_AuthBody_centered" text taken from registration page. return ( <> -

    {introText}

    +

    {_t("Sign in and regain access to your account.")}

    {this.renderSsoForm(null)}

    {_t("%(ssoButtons)s Or %(usernamePassword)s", { @@ -330,10 +300,7 @@ export default class SoftLogout extends React.Component { // Default: assume unsupported/error return (

    - {_t( - "You cannot sign in to your account. Please contact your " + - "homeserver admin for more information.", - )} + {_t("You cannot sign in to your account. Please contact your homeserver admin for more information.")}

    ); } @@ -345,15 +312,13 @@ export default class SoftLogout extends React.Component {

    {_t("You're signed out")}

    -

    {_t("Sign in")}

    +

    {_t("action|sign_in")}

    {this.renderSignInSection()}

    {_t("Clear personal data")}

    {_t( - "Warning: your personal data (including encryption keys) is still stored " + - "in this session. Clear it if you're finished using this session, or want to sign " + - "in to another account.", + "Warning: your personal data (including encryption keys) is still stored in this session. Clear it if you're finished using this session, or want to sign in to another account.", )}

    diff --git a/src/components/structures/auth/forgot-password/CheckEmail.tsx b/src/components/structures/auth/forgot-password/CheckEmail.tsx index 7e4679a1dcf..086a71d4c8b 100644 --- a/src/components/structures/auth/forgot-password/CheckEmail.tsx +++ b/src/components/structures/auth/forgot-password/CheckEmail.tsx @@ -66,7 +66,7 @@ export const CheckEmail: React.FC = ({

    {errorText && } - +
    {_t("Did not receive it?")} = ({ aria-describedby={tooltipVisible ? tooltipId : undefined} > - {_t("Resend")} + {_t("action|resend")} = ({

    {_t("Verify your email to continue")}

    {_t( - "We need to know it’s you before resetting your password. " + - "Click the link in the email we just sent to %(email)s", + "We need to know it’s you before resetting your password. Click the link in the email we just sent to %(email)s", { email, }, @@ -74,7 +73,7 @@ export const VerifyEmailModal: React.FC = ({ aria-describedby={tooltipVisible ? tooltipId : undefined} > - {_t("Resend")} + {_t("action|resend")} void; + /** + * The flex space to use + * @default null + */ + flex?: string | null; + /** + * The flex shrink factor + * @default null + */ + shrink?: string | null; + /** + * The flex grow factor + * @default null + */ + grow?: string | null; +}; + +/** + * Set or remove a CSS property + * @param ref the reference + * @param name the CSS property name + * @param value the CSS property value + */ +function addOrRemoveProperty( + ref: React.MutableRefObject, + name: string, + value?: string | null, +): void { + const style = ref.current!.style; + if (value) { + style.setProperty(name, value); + } else { + style.removeProperty(name); + } +} + +/** + * A flex child helper + */ +export function Box({ + as = "div", + flex = null, + shrink = null, + grow = null, + className, + children, + ...props +}: React.PropsWithChildren): JSX.Element { + const ref = useRef(); + + useEffect(() => { + addOrRemoveProperty(ref, `--mx-box-flex`, flex); + addOrRemoveProperty(ref, `--mx-box-shrink`, shrink); + addOrRemoveProperty(ref, `--mx-box-grow`, grow); + }, [flex, grow, shrink]); + + return React.createElement( + as, + { + ...props, + className: classNames("mx_Box", className, { + "mx_Box--flex": !!flex, + "mx_Box--shrink": !!shrink, + "mx_Box--grow": !!grow, + }), + ref, + }, + children, + ); +} diff --git a/src/components/utils/Flex.tsx b/src/components/utils/Flex.tsx new file mode 100644 index 00000000000..20802f4f022 --- /dev/null +++ b/src/components/utils/Flex.tsx @@ -0,0 +1,86 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import classNames from "classnames"; +import React, { useEffect, useRef } from "react"; + +type FlexProps = { + /** + * The type of the HTML element + * @default div + */ + as?: string; + /** + * The CSS class name. + */ + className?: string; + /** + * The type of flex container + * @default flex + */ + display?: "flex" | "inline-flex"; + /** + * The flow direction of the flex children + * @default row + */ + direction?: "row" | "column" | "row-reverse" | "column-reverse"; + /** + * The alingment of the flex children + * @default start + */ + align?: "start" | "center" | "end" | "baseline" | "stretch"; + /** + * The justification of the flex children + * @default start + */ + justify?: "start" | "center" | "end" | "between"; + /** + * The spacing between the flex children, expressed with the CSS unit + * @default 0 + */ + gap?: string; + /** + * the on click event callback + */ + onClick?: (e: React.MouseEvent) => void; +}; + +/** + * A flexbox container helper + */ +export function Flex({ + as = "div", + display = "flex", + direction = "row", + align = "start", + justify = "start", + gap = "0", + className, + children, + ...props +}: React.PropsWithChildren): JSX.Element { + const ref = useRef(); + + useEffect(() => { + ref.current!.style.setProperty(`--mx-flex-display`, display); + ref.current!.style.setProperty(`--mx-flex-direction`, direction); + ref.current!.style.setProperty(`--mx-flex-align`, align); + ref.current!.style.setProperty(`--mx-flex-justify`, justify); + ref.current!.style.setProperty(`--mx-flex-gap`, gap); + }, [align, direction, display, gap, justify]); + + return React.createElement(as, { ...props, className: classNames("mx_Flex", className), ref }, children); +} diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index a1b428de680..9946e72dad4 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -18,16 +18,15 @@ import React, { ReactElement } from "react"; import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from "../../../phonenumber"; import SdkConfig from "../../../SdkConfig"; -import { _t } from "../../../languageHandler"; +import { _t, getUserLanguage } from "../../../languageHandler"; import Dropdown from "../elements/Dropdown"; import { NonEmptyArray } from "../../../@types/common"; -const COUNTRIES_BY_ISO2: Record = {}; -for (const c of COUNTRIES) { - COUNTRIES_BY_ISO2[c.iso2] = c; +interface InternationalisedCountry extends PhoneNumberCountryDefinition { + name: string; // already translated to the user's locale } -function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean { +function countryMatchesSearchQuery(query: string, country: InternationalisedCountry): boolean { // Remove '+' if present (when searching for a prefix) if (query[0] === "+") { query = query.slice(1); @@ -41,7 +40,7 @@ function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDef interface IProps { value?: string; - onOptionChange: (country: PhoneNumberCountryDefinition) => void; + onOptionChange: (country: InternationalisedCountry) => void; isSmall: boolean; // if isSmall, show +44 in the selected value showPrefix: boolean; className?: string; @@ -53,15 +52,25 @@ interface IState { } export default class CountryDropdown extends React.Component { - private readonly defaultCountry: PhoneNumberCountryDefinition; + private readonly defaultCountry: InternationalisedCountry; + private readonly countries: InternationalisedCountry[]; + private readonly countryMap: Map; public constructor(props: IProps) { super(props); - let defaultCountry: PhoneNumberCountryDefinition | undefined; + const displayNames = new Intl.DisplayNames([getUserLanguage()], { type: "region" }); + + this.countries = COUNTRIES.map((c) => ({ + name: displayNames.of(c.iso2) ?? c.iso2, + ...c, + })); + this.countryMap = new Map(this.countries.map((c) => [c.iso2, c])); + + let defaultCountry: InternationalisedCountry | undefined; const defaultCountryCode = SdkConfig.get("default_country_code"); if (defaultCountryCode) { - const country = COUNTRIES.find((c) => c.iso2 === defaultCountryCode.toUpperCase()); + const country = this.countries.find((c) => c.iso2 === defaultCountryCode.toUpperCase()); if (country) defaultCountry = country; } @@ -69,9 +78,8 @@ export default class CountryDropdown extends React.Component { try { const locale = new Intl.Locale(navigator.language ?? navigator.languages[0]); const code = locale.region ?? locale.language ?? locale.baseName; - const displayNames = new Intl.DisplayNames(["en"], { type: "region" }); - const displayName = displayNames.of(code)?.toUpperCase(); - defaultCountry = COUNTRIES.find( + const displayName = displayNames.of(code)!.toUpperCase(); + defaultCountry = this.countries.find( (c) => c.iso2 === code.toUpperCase() || c.name.toUpperCase() === displayName, ); } catch (e) { @@ -79,7 +87,7 @@ export default class CountryDropdown extends React.Component { } } - this.defaultCountry = defaultCountry ?? COUNTRIES[0]; + this.defaultCountry = defaultCountry ?? this.countries[0]; this.state = { searchQuery: "", }; @@ -101,7 +109,7 @@ export default class CountryDropdown extends React.Component { }; private onOptionChange = (iso2: string): void => { - this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]); + this.props.onOptionChange(this.countryMap.get(iso2)!); }; private flagImgForIso2(iso2: string): React.ReactNode { @@ -112,9 +120,9 @@ export default class CountryDropdown extends React.Component { if (!this.props.isSmall) { return undefined; } - let countryPrefix; + let countryPrefix: string | undefined; if (this.props.showPrefix) { - countryPrefix = "+" + COUNTRIES_BY_ISO2[iso2].prefix; + countryPrefix = "+" + this.countryMap.get(iso2)!.prefix; } return ( @@ -125,26 +133,28 @@ export default class CountryDropdown extends React.Component { }; public render(): React.ReactNode { - let displayedCountries; + let displayedCountries: InternationalisedCountry[]; if (this.state.searchQuery) { - displayedCountries = COUNTRIES.filter(countryMatchesSearchQuery.bind(this, this.state.searchQuery)); - if (this.state.searchQuery.length == 2 && COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]) { + displayedCountries = this.countries.filter((country) => + countryMatchesSearchQuery(this.state.searchQuery, country), + ); + if (this.state.searchQuery.length == 2 && this.countryMap.has(this.state.searchQuery.toUpperCase())) { // exact ISO2 country name match: make the first result the matches ISO2 - const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]; + const matched = this.countryMap.get(this.state.searchQuery.toUpperCase())!; displayedCountries = displayedCountries.filter((c) => { return c.iso2 != matched.iso2; }); displayedCountries.unshift(matched); } } else { - displayedCountries = COUNTRIES; + displayedCountries = this.countries; } const options = displayedCountries.map((country) => { return (

    {this.flagImgForIso2(country.iso2)} - {_t(country.name)} (+{country.prefix}) + {country.name} (+{country.prefix})
    ); }) as NonEmptyArray; diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index 849f38fea7b..16fa73771ce 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { PureComponent, RefCallback, RefObject } from "react"; import Field, { IInputProps } from "../elements/Field"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import * as Email from "../../../email"; @@ -27,9 +27,9 @@ interface IProps extends Omit { value: string; autoFocus?: boolean; - label?: string; - labelRequired?: string; - labelInvalid?: string; + label: TranslationKey; + labelRequired: TranslationKey; + labelInvalid: TranslationKey; // When present, completely overrides the default validation rules. validationRules?: (fieldState: IFieldState) => Promise; @@ -50,12 +50,12 @@ class EmailField extends PureComponent { { key: "required", test: ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t(this.props.labelRequired!), + invalid: () => _t(this.props.labelRequired), }, { key: "email", test: ({ value }) => !value || Email.looksValid(value), - invalid: () => _t(this.props.labelInvalid!), + invalid: () => _t(this.props.labelInvalid), }, ], }); @@ -80,7 +80,7 @@ class EmailField extends PureComponent { id={this.props.id} ref={this.props.fieldRef} type="text" - label={_t(this.props.label!)} + label={_t(this.props.label)} value={this.props.value} autoFocus={this.props.autoFocus} onChange={this.props.onChange} diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index add3638fcb4..50af045d5f3 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -153,7 +153,7 @@ export class PasswordAuthEntry extends React.Component ); } @@ -175,7 +175,7 @@ export class PasswordAuthEntry extends React.Component - {_t("Accept")} + {_t("action|accept")}
    ); } @@ -509,7 +508,7 @@ export class EmailIdentityAuthEntry extends React.Component< a: (text: string) => ( - {_t("Continue")} + {_t("action|continue")} ); } @@ -869,7 +868,7 @@ export class SSOAuthEntry extends React.Component - {_t("Cancel")} + {_t("action|cancel")} ); if (this.state.phase === SSOAuthEntry.PHASE_PREAUTH) { @@ -881,7 +880,7 @@ export class SSOAuthEntry extends React.Component - {this.props.continueText || _t("Confirm")} + {this.props.continueText || _t("action|confirm")} ); } diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index e5d1d94a32e..acb2fc5a2f8 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -54,7 +54,7 @@ export default class LoginWithQRFlow extends React.Component { private cancelButton = (): JSX.Element => ( - {_t("Cancel")} + {_t("action|cancel")} ); @@ -124,7 +124,7 @@ export default class LoginWithQRFlow extends React.Component { kind="primary" onClick={this.handleClick(Click.TryAgain)} > - {_t("Try again")} + {_t("action|try_again")} {this.cancelButton()} @@ -156,7 +156,7 @@ export default class LoginWithQRFlow extends React.Component { kind="primary_outline" onClick={this.handleClick(Click.Decline)} > - {_t("Cancel")} + {_t("action|cancel")} { buttons = this.cancelButton(); break; case Phase.Verifying: - title = _t("Success"); + title = _t("common|success"); centreTitle = true; main = this.simpleSpinner(_t("Completing set up of your new device")); break; diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index b314c3f838a..ebb273ff550 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -18,7 +18,7 @@ import React, { PureComponent, RefCallback, RefObject } from "react"; import Field, { IInputProps } from "../elements/Field"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; interface IProps extends Omit { id?: string; @@ -27,9 +27,9 @@ interface IProps extends Omit { value: string; password: string; // The password we're confirming - label: string; - labelRequired: string; - labelInvalid: string; + label: TranslationKey; + labelRequired: TranslationKey; + labelInvalid: TranslationKey; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index a40ba0bb07d..4efd06e4e43 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -20,7 +20,7 @@ import zxcvbn from "zxcvbn"; import SdkConfig from "../../../SdkConfig"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import Field, { IInputProps } from "../elements/Field"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -34,10 +34,10 @@ interface IProps extends Omit { // Additional strings such as a username used to catch bad passwords userInputs?: string[]; - label: string; - labelEnterPassword: string; - labelStrongPassword: string; - labelAllowedButUnsafe: string; + label: TranslationKey; + labelEnterPassword: TranslationKey; + labelStrongPassword: TranslationKey; + labelAllowedButUnsafe: TranslationKey; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; @@ -45,7 +45,7 @@ interface IProps extends Omit { class PassphraseField extends PureComponent { public static defaultProps = { - label: _td("Password"), + label: _td("common|password"), labelEnterPassword: _td("Enter password"), labelStrongPassword: _td("Nice, strong password!"), labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index bbab2242f49..33c62a8df02 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -308,8 +308,8 @@ export default class PasswordLogin extends React.PureComponent { autoComplete="username" key="username_input" type="text" - label={_t("Username")} - placeholder={_t("Username").toLocaleLowerCase()} + label={_t("common|username")} + placeholder={_t("common|username").toLocaleLowerCase()} value={this.props.username} onChange={this.onUsernameChanged} onBlur={this.onUsernameBlur} @@ -404,7 +404,7 @@ export default class PasswordLogin extends React.PureComponent { disabled={this.props.busy} >
    ); diff --git a/src/components/views/beacon/RoomLiveShareWarning.tsx b/src/components/views/beacon/RoomLiveShareWarning.tsx index ace873cc1ad..541e2329d0c 100644 --- a/src/components/views/beacon/RoomLiveShareWarning.tsx +++ b/src/components/views/beacon/RoomLiveShareWarning.tsx @@ -111,7 +111,7 @@ const RoomLiveShareWarningInner: React.FC = ({ l element="button" disabled={stoppingInProgress} > - {hasError ? _t("Retry") : _t("Stop")} + {hasError ? _t("action|retry") : _t("action|stop")}
    {hasLocationPublishError && ( = ({ latestLocationState }) => { diff --git a/src/components/views/beacon/displayStatus.ts b/src/components/views/beacon/displayStatus.ts index 48260cb672f..a2cd4b662bb 100644 --- a/src/components/views/beacon/displayStatus.ts +++ b/src/components/views/beacon/displayStatus.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { BeaconLocationState } from "matrix-js-sdk/src/content-helpers"; +import { ContentHelpers } from "matrix-js-sdk/src/matrix"; export enum BeaconDisplayStatus { Loading = "Loading", @@ -24,7 +24,7 @@ export enum BeaconDisplayStatus { } export const getBeaconDisplayStatus = ( isLive: boolean, - latestLocationState?: BeaconLocationState, + latestLocationState?: ContentHelpers.BeaconLocationState, error?: Error, waitingToStart?: boolean, ): BeaconDisplayStatus => { diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 7fbf68b2908..a604aee1ec5 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -61,12 +61,12 @@ export const BetaPill: React.FC = ({ } onClick={onClick} > - {_t("Beta")} + {_t("common|beta")} ); } - return {_t("Beta")}; + return {_t("common|beta")}; }; const BetaCard: React.FC = ({ title: titleOverride, featureId }) => { diff --git a/src/components/views/context_menus/DeviceContextMenu.tsx b/src/components/views/context_menus/DeviceContextMenu.tsx index 16ac988f6fb..81c33b56efa 100644 --- a/src/components/views/context_menus/DeviceContextMenu.tsx +++ b/src/components/views/context_menus/DeviceContextMenu.tsx @@ -19,9 +19,9 @@ import React, { useEffect, useState } from "react"; import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../MediaDeviceHandler"; import IconizedContextMenu, { IconizedContextMenuOptionList, IconizedContextMenuRadio } from "./IconizedContextMenu"; import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; -const SECTION_NAMES: Record = { +const SECTION_NAMES: Record = { [MediaDeviceKindEnum.AudioInput]: _td("Input devices"), [MediaDeviceKindEnum.AudioOutput]: _td("Output devices"), [MediaDeviceKindEnum.VideoInput]: _td("Cameras"), diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 012119c3c6a..f92f66ceff6 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -26,8 +26,8 @@ import { RelationType, Relations, Thread, + M_POLL_START, } from "matrix-js-sdk/src/matrix"; -import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; @@ -425,7 +425,7 @@ export default class MessageContextMenu extends React.Component redactButton = ( ); @@ -456,7 +456,7 @@ export default class MessageContextMenu extends React.Component forwardButton = ( ); @@ -467,7 +467,7 @@ export default class MessageContextMenu extends React.Component pinButton = ( ); @@ -499,7 +499,7 @@ export default class MessageContextMenu extends React.Component quoteButton = ( ); @@ -600,7 +600,7 @@ export default class MessageContextMenu extends React.Component copyButton = ( @@ -631,7 +631,7 @@ export default class MessageContextMenu extends React.Component editButton = ( ); @@ -642,7 +642,7 @@ export default class MessageContextMenu extends React.Component replyButton = ( ); @@ -664,7 +664,7 @@ export default class MessageContextMenu extends React.Component reactButton = ( diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 883b86f3dae..cf3704ffa06 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -105,7 +105,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { leaveOption = ( @@ -136,7 +136,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { inviteOption = ( ); @@ -186,7 +186,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { iconClassName = "mx_RoomTile_iconNotificationsMentionsKeywords"; break; case RoomNotifState.Mute: - notificationLabel = _t("Mute"); + notificationLabel = _t("common|mute"); iconClassName = "mx_RoomTile_iconNotificationsNone"; break; } @@ -228,7 +228,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { onFinished(); PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuPeopleItem", ev); }} - label={_t("People")} + label={_t("common|people")} iconClassName="mx_RoomTile_iconPeople" > {room.getJoinedMemberCount()} @@ -390,7 +390,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { onFinished(); PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuSettingsItem", ev); }} - label={_t("Settings")} + label={_t("common|settings")} iconClassName="mx_RoomTile_iconSettings" /> diff --git a/src/components/views/context_menus/RoomGeneralContextMenu.tsx b/src/components/views/context_menus/RoomGeneralContextMenu.tsx index 965a34c8983..eba32eca06a 100644 --- a/src/components/views/context_menus/RoomGeneralContextMenu.tsx +++ b/src/components/views/context_menus/RoomGeneralContextMenu.tsx @@ -138,7 +138,7 @@ export const RoomGeneralContextMenu: React.FC = ({ }), onPostInviteClick, )} - label={_t("Invite")} + label={_t("action|invite")} iconClassName="mx_RoomGeneralContextMenu_iconInvite" /> ); @@ -172,7 +172,7 @@ export const RoomGeneralContextMenu: React.FC = ({ }), onPostSettingsClick, )} - label={_t("Settings")} + label={_t("common|settings")} iconClassName="mx_RoomGeneralContextMenu_iconSettings" /> ); @@ -205,7 +205,7 @@ export const RoomGeneralContextMenu: React.FC = ({ }), onPostLeaveClick, )} - label={_t("Leave")} + label={_t("action|leave")} className="mx_IconizedContextMenu_option_red" iconClassName="mx_RoomGeneralContextMenu_iconSignOut" /> diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx index 811a88ca209..d9262270200 100644 --- a/src/components/views/context_menus/SpaceContextMenu.tsx +++ b/src/components/views/context_menus/SpaceContextMenu.tsx @@ -69,7 +69,7 @@ const SpaceContextMenu: React.FC = ({ space, hideHeader, onFinished, ... data-testid="invite-option" className="mx_SpacePanel_contextMenu_inviteButton" iconClassName="mx_SpacePanel_iconInvite" - label={_t("Invite")} + label={_t("action|invite")} onClick={onInviteClick} /> ); @@ -90,7 +90,7 @@ const SpaceContextMenu: React.FC = ({ space, hideHeader, onFinished, ... ); @@ -173,13 +173,13 @@ const SpaceContextMenu: React.FC = ({ space, hideHeader, onFinished, ... newRoomSection = ( <>
    - {_t("Add")} + {_t("action|add")}
    {canAddRooms && ( )} diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 83fde0722cd..2525f3d2b28 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -159,7 +159,7 @@ export const WidgetContextMenu: React.FC = ({ onFinished(); }; - editButton = ; + editButton = ; } let snapshotButton: JSX.Element | undefined; @@ -192,8 +192,7 @@ export const WidgetContextMenu: React.FC = ({ Modal.createDialog(QuestionDialog, { title: _t("Delete Widget"), description: _t( - "Deleting a widget removes it for all users in this room." + - " Are you sure you want to delete this widget?", + "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", ), button: _t("Delete widget"), onFinished: (confirmed) => { @@ -209,7 +208,7 @@ export const WidgetContextMenu: React.FC = ({ deleteButton = ( ); } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index e2d4481e0b2..7d6c10839db 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -20,7 +20,7 @@ import { Room, EventType } from "matrix-js-sdk/src/matrix"; import { sleep } from "matrix-js-sdk/src/utils"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; @@ -62,9 +62,9 @@ export const Entry: React.FC<{ return (
    @@ -181,17 +178,14 @@ export default class IncomingSasDialog extends React.Component { const userDetailText = [

    {_t( - "Verify this user to mark them as trusted. " + - "Trusting users gives you extra peace of mind when using " + - "end-to-end encrypted messages.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", )}

    ,

    {_t( // NB. Below wording adjusted to singular 'session' until we have // cross-signing - "Verifying this user will mark their session as trusted, and " + - "also mark your session as trusted to them.", + "Verifying this user will mark their session as trusted, and also mark your session as trusted to them.", )}

    , ]; @@ -199,15 +193,12 @@ export default class IncomingSasDialog extends React.Component { const selfDetailText = [

    {_t( - "Verify this device to mark it as trusted. " + - "Trusting this device gives you and other users extra peace of mind when using " + - "end-to-end encrypted messages.", + "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.", )}

    ,

    {_t( - "Verifying this device will mark it as trusted, and users who have verified with " + - "you will trust this device.", + "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.", )}

    , ]; @@ -217,7 +208,7 @@ export default class IncomingSasDialog extends React.Component { {profile} {isSelf ? selfDetailText : userDetailText} {
    {this.props.button !== false && ( diff --git a/src/components/views/dialogs/IntegrationsDisabledDialog.tsx b/src/components/views/dialogs/IntegrationsDisabledDialog.tsx index f8098f3d419..184bf6a3568 100644 --- a/src/components/views/dialogs/IntegrationsDisabledDialog.tsx +++ b/src/components/views/dialogs/IntegrationsDisabledDialog.tsx @@ -52,9 +52,9 @@ export default class IntegrationsDisabledDialog extends React.Component

    diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx index 5b158402e87..6eee4e73c8b 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.tsx @@ -43,14 +43,13 @@ export default class IntegrationsImpossibleDialog extends React.Component

    {_t( - "Your %(brand)s doesn't allow you to use an integration manager to do this. " + - "Please contact an admin.", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.", { brand }, )}

    diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index 4fbb3456f65..fbb57148939 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -17,9 +17,8 @@ limitations under the License. */ import React from "react"; -import { MatrixClient } from "matrix-js-sdk/src/matrix"; +import { MatrixClient, UIAResponse } from "matrix-js-sdk/src/matrix"; import { AuthType } from "matrix-js-sdk/src/interactive-auth"; -import { UIAResponse } from "matrix-js-sdk/src/@types/uia"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; @@ -106,7 +105,7 @@ export default class InteractiveAuthDialog extends React.Component extends React.Component = (success, result): void => { + private onAuthFinished: InteractiveAuthCallback = async (success, result): Promise => { if (success) { this.props.onFinished(true, result); } else { @@ -172,7 +171,7 @@ export default class InteractiveAuthDialog extends React.Component{this.state.authError.message || this.state.authError.toString()}
    - {_t("Dismiss")} + {_t("action|dismiss")} ); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 67818a308ad..d37c834f50d 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -126,7 +126,7 @@ class DMUserTile extends React.PureComponent { }; public render(): React.ReactNode { - const avatarSize = 20; + const avatarSize = "20px"; const avatar = ; let closeButton; @@ -135,7 +135,7 @@ class DMUserTile extends React.PureComponent { {_t("Remove")} @@ -233,20 +233,21 @@ class DMRoomTile extends React.PureComponent { timestamp = {humanTs}; } - const avatarSize = 36; + const avatarSize = "36px"; const avatar = (this.props.member as ThreepidMember).isEmail ? ( ) : ( ); @@ -944,7 +945,7 @@ export default class InviteDialog extends React.PureComponent (kind === "recents" ? m.lastActive : undefined); - let sectionName = kind === "recents" ? _t("Recent Conversations") : _t("Suggestions"); + let sectionName = kind === "recents" ? _t("Recent Conversations") : _t("common|suggestions"); if (this.props.kind === InviteKind.Invite) { - sectionName = kind === "recents" ? _t("Recently Direct Messaged") : _t("Suggestions"); + sectionName = kind === "recents" ? _t("Recently Direct Messaged") : _t("common|suggestions"); } // Mix in the server results if we have any, but only if we're searching. We track the additional @@ -1038,7 +1039,7 @@ export default class InviteDialog extends React.PureComponent

    {sectionName}

    -

    {_t("No results")}

    +

    {_t("common|no_results")}

    ); } @@ -1107,7 +1108,7 @@ export default class InviteDialog extends React.PureComponent 0) } autoComplete="off" - placeholder={hasPlaceholder ? _t("Search") : undefined} + placeholder={hasPlaceholder ? _t("action|search") : undefined} data-testid="invite-dialog-input" /> ); @@ -1133,9 +1134,7 @@ export default class InviteDialog extends React.PureComponent {_t( - "Use an identity server to invite by email. " + - "Use the default (%(defaultIdentityServerName)s) " + - "or manage in Settings.", + "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.", { defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl), }, @@ -1158,7 +1157,7 @@ export default class InviteDialog extends React.PureComponent {_t( - "Use an identity server to invite by email. " + "Manage in Settings.", + "Use an identity server to invite by email. Manage in Settings.", {}, { settings: (sub) => ( @@ -1349,23 +1348,21 @@ export default class InviteDialog extends React.PureComponent) or share this space.", + "Invite someone using their name, email address, username (like ) or share this space.", ); } else { helpTextUntranslated = _td( - "Invite someone using their name, username " + "(like ) or share this space.", + "Invite someone using their name, username (like ) or share this space.", ); } } else { if (identityServersEnabled) { helpTextUntranslated = _td( - "Invite someone using their name, email address, username " + - "(like ) or share this room.", + "Invite someone using their name, email address, username (like ) or share this room.", ); } else { helpTextUntranslated = _td( - "Invite someone using their name, username " + "(like ) or share this room.", + "Invite someone using their name, username (like ) or share this room.", ); } } @@ -1392,7 +1389,7 @@ export default class InviteDialog extends React.PureComponent - {_t("Cancel")} + {_t("action|cancel")}
    = ({ failures, source, co body = (
    {text} - +
    ); } diff --git a/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx index e1d5374fb3e..cd69a6ce72f 100644 --- a/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx +++ b/src/components/views/dialogs/LazyLoadingDisabledDialog.tsx @@ -29,19 +29,14 @@ interface IProps { const LazyLoadingDisabledDialog: React.FC = (props) => { const brand = SdkConfig.get().brand; const description1 = _t( - "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. " + - "In this version lazy loading is disabled. " + - "As the local cache is not compatible between these two settings, " + - "%(brand)s needs to resync your account.", + "You've previously used %(brand)s on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, %(brand)s needs to resync your account.", { brand, host: props.host, }, ); const description2 = _t( - "If the other version of %(brand)s is still open in another tab, " + - "please close it as using %(brand)s on the same host with both " + - "lazy loading enabled and disabled simultaneously will cause issues.", + "If the other version of %(brand)s is still open in another tab, please close it as using %(brand)s on the same host with both lazy loading enabled and disabled simultaneously will cause issues.", { brand, }, diff --git a/src/components/views/dialogs/LazyLoadingResyncDialog.tsx b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx index c5bd2e0227c..2caa4e6fce8 100644 --- a/src/components/views/dialogs/LazyLoadingResyncDialog.tsx +++ b/src/components/views/dialogs/LazyLoadingResyncDialog.tsx @@ -28,9 +28,7 @@ interface IProps { const LazyLoadingResyncDialog: React.FC = (props) => { const brand = SdkConfig.get().brand; const description = _t( - "%(brand)s now uses 3-5x less memory, by only loading information " + - "about other users when needed. Please wait whilst we resynchronise " + - "with the server!", + "%(brand)s now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!", { brand }, ); @@ -39,7 +37,7 @@ const LazyLoadingResyncDialog: React.FC = (props) => { hasCancelButton={false} title={_t("Updating %(brand)s", { brand })} description={
    {description}
    } - button={_t("OK")} + button={_t("action|ok")} onFinished={props.onFinished} /> ); diff --git a/src/components/views/dialogs/LeaveSpaceDialog.tsx b/src/components/views/dialogs/LeaveSpaceDialog.tsx index cc31183414b..a8a9aca4d8e 100644 --- a/src/components/views/dialogs/LeaveSpaceDialog.tsx +++ b/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -63,15 +63,12 @@ const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { let onlyAdminWarning; if (isOnlyAdmin(space)) { - onlyAdminWarning = _t( - "You're the only admin of this space. " + "Leaving it will mean no one has control over it.", - ); + onlyAdminWarning = _t("You're the only admin of this space. Leaving it will mean no one has control over it."); } else { const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length; if (numChildrenOnlyAdminIn > 0) { onlyAdminWarning = _t( - "You're the only admin of some of the rooms or spaces you wish to leave. " + - "Leaving them will leave them without any admins.", + "You're the only admin of some of the rooms or spaces you wish to leave. Leaving them will leave them without any admins.", ); } } diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 9830cf59922..2086a33b8af 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -138,16 +138,12 @@ export default class LogoutDialog extends React.Component {

    {_t( - "Encrypted messages are secured with end-to-end encryption. " + - "Only you and the recipient(s) have the keys to read these messages.", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", )}

    {_t( - "When you sign out, these keys will be deleted from this device, " + - "which means you won't be able to read encrypted messages unless you " + - "have the keys for them on your other devices, or backed them up to the " + - "server.", + "When you sign out, these keys will be deleted from this device, which means you won't be able to read encrypted messages unless you have the keys for them on your other devices, or backed them up to the server.", )}

    {_t("Back up your keys before signing out to avoid losing them.")}

    @@ -206,9 +202,9 @@ export default class LogoutDialog extends React.Component { return ( ); diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index d29b9e8bafb..f01ff6d362a 100644 --- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -54,11 +54,7 @@ const Entry: React.FC<{
    { let buttons; if (this.props.totalFiles === 1 && this.props.badFiles.length === 1) { message = _t( - "This file is too large to upload. " + - "The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", { limit: fileSize(this.props.contentMessages.getUploadLimit()), sizeOfThisFile: fileSize(this.props.badFiles[0].size), @@ -61,7 +60,7 @@ export default class UploadFailureDialog extends React.Component { ); buttons = ( { ); } else if (this.props.totalFiles === this.props.badFiles.length) { message = _t( - "These files are too large to upload. " + "The file size limit is %(limit)s.", + "These files are too large to upload. The file size limit is %(limit)s.", { limit: fileSize(this.props.contentMessages.getUploadLimit()), }, @@ -79,7 +78,7 @@ export default class UploadFailureDialog extends React.Component { ); buttons = ( { ); } else { message = _t( - "Some files are too large to be uploaded. " + "The file size limit is %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.", { limit: fileSize(this.props.contentMessages.getUploadLimit()), }, diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index c185da1f2df..e35bed48395 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -86,7 +86,7 @@ export default class UserSettingsDialog extends React.Component tabs.push( new Tab( UserTab.Appearance, - _td("Appearance"), + _td("common|appearance"), "mx_UserSettingsDialog_appearanceIcon", , "UserSettingsAppearance", @@ -168,7 +168,7 @@ export default class UserSettingsDialog extends React.Component tabs.push( new Tab( UserTab.Labs, - _td("Labs"), + _td("common|labs"), "mx_UserSettingsDialog_labsIcon", , "UserSettingsLabs", @@ -205,7 +205,7 @@ export default class UserSettingsDialog extends React.Component className="mx_UserSettingsDialog" hasCancel={true} onFinished={this.props.onFinished} - title={_t("Settings")} + title={_t("common|settings")} >
    > = ({
    {children}
    {extraButton} - + {actionButton}
    diff --git a/src/components/views/dialogs/devtools/Event.tsx b/src/components/views/dialogs/devtools/Event.tsx index 16d76752eb1..f0a89fd6a4d 100644 --- a/src/components/views/dialogs/devtools/Event.tsx +++ b/src/components/views/dialogs/devtools/Event.tsx @@ -18,7 +18,7 @@ limitations under the License. import React, { ChangeEvent, ReactNode, useContext, useMemo, useRef, useState } from "react"; import { IContent, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { _t, _td } from "../../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../../languageHandler"; import Field from "../../elements/Field"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; @@ -37,7 +37,7 @@ interface IEventEditorProps extends Pick { interface IFieldDef { id: string; - label: string; // _td + label: TranslationKey; default?: string; } @@ -161,7 +161,7 @@ export const EventViewer: React.FC = ({ mxEvent, onBack, Editor, e }; return ( - + {stringify(mxEvent.event)} ); diff --git a/src/components/views/dialogs/devtools/RoomNotifications.tsx b/src/components/views/dialogs/devtools/RoomNotifications.tsx index cbb0f3c0a6a..34e9a29c6b0 100644 --- a/src/components/views/dialogs/devtools/RoomNotifications.tsx +++ b/src/components/views/dialogs/devtools/RoomNotifications.tsx @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { NotificationCountType, Room, Thread } from "matrix-js-sdk/src/matrix"; +import { NotificationCountType, Room, Thread, ReceiptType } from "matrix-js-sdk/src/matrix"; import React, { useContext } from "react"; -import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts"; import { ReadReceipt } from "matrix-js-sdk/src/models/read-receipt"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; @@ -76,16 +75,26 @@ export default function RoomNotifications({ onBack }: IDevtoolsProps): JSX.Eleme

    {_t("Room status")}

    • - {_t( - "Room unread status: %(status)s, count: %(count)s", - { - status: humanReadableNotificationColor(color), - count, - }, - { - strong: (sub) => {sub}, - }, - )} + {count > 0 + ? _t( + "Room unread status: %(status)s, count: %(count)s", + { + status: humanReadableNotificationColor(color), + count, + }, + { + strong: (sub) => {sub}, + }, + ) + : _t( + "Room unread status: %(status)s", + { + status: humanReadableNotificationColor(color), + }, + { + strong: (sub) => {sub}, + }, + )}
    • {_t( diff --git a/src/components/views/dialogs/devtools/RoomState.tsx b/src/components/views/dialogs/devtools/RoomState.tsx index e84799c513c..9dbbf89fbce 100644 --- a/src/components/views/dialogs/devtools/RoomState.tsx +++ b/src/components/views/dialogs/devtools/RoomState.tsx @@ -95,6 +95,11 @@ const RoomStateHistory: React.FC<{ const StateEventButton: React.FC = ({ label, onClick }) => { const trimmed = label.trim(); + let content = label; + if (!trimmed) { + content = label.length > 0 ? _t("<%(count)s spaces>", { count: label.length }) : _t(""); + } + return ( ); }; diff --git a/src/components/views/dialogs/devtools/VerificationExplorer.tsx b/src/components/views/dialogs/devtools/VerificationExplorer.tsx index aaaf02dea4d..171bfd01769 100644 --- a/src/components/views/dialogs/devtools/VerificationExplorer.tsx +++ b/src/components/views/dialogs/devtools/VerificationExplorer.tsx @@ -21,16 +21,16 @@ import { VerificationPhase as Phase, VerificationRequestEvent } from "matrix-js- import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { useTypedEventEmitter, useTypedEventEmitterState } from "../../../../hooks/useEventEmitter"; -import { _t, _td } from "../../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../../languageHandler"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool"; import { Tool } from "../DevtoolsDialog"; -const PHASE_MAP: Record = { +const PHASE_MAP: Record = { [Phase.Unsent]: _td("Unsent"), [Phase.Requested]: _td("Requested"), [Phase.Ready]: _td("Ready"), - [Phase.Done]: _td("Done"), + [Phase.Done]: _td("action|done"), [Phase.Started]: _td("Started"), [Phase.Cancelled]: _td("Cancelled"), }; diff --git a/src/components/views/dialogs/oidc/OidcLogoutDialog.tsx b/src/components/views/dialogs/oidc/OidcLogoutDialog.tsx new file mode 100644 index 00000000000..b15051ba52b --- /dev/null +++ b/src/components/views/dialogs/oidc/OidcLogoutDialog.tsx @@ -0,0 +1,74 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { useState } from "react"; + +import { _t } from "../../../../languageHandler"; +import BaseDialog from "../BaseDialog"; +import { getOidcLogoutUrl } from "../../../../utils/oidc/getOidcLogoutUrl"; +import AccessibleButton from "../../elements/AccessibleButton"; + +export interface OidcLogoutDialogProps { + delegatedAuthAccountUrl: string; + deviceId: string; + onFinished(ok?: boolean): void; +} + +/** + * Handle logout of OIDC sessions other than the current session + * - ask for user confirmation to open the delegated auth provider + * - open the auth provider in a new tab + * - wait for the user to return and close the modal, we assume the user has completed sign out of the session in auth provider UI + * and trigger a refresh of the session list + */ +export const OidcLogoutDialog: React.FC = ({ + delegatedAuthAccountUrl, + deviceId, + onFinished, +}) => { + const [hasOpenedLogoutLink, setHasOpenedLogoutLink] = useState(false); + const logoutUrl = getOidcLogoutUrl(delegatedAuthAccountUrl, deviceId); + + return ( + +
      + {_t("You will be redirected to your server's authentication provider to complete sign out.")} +
      +
      + {hasOpenedLogoutLink ? ( + onFinished(true)}> + {_t("action|close")} + + ) : ( + <> + onFinished(false)}> + {_t("action|cancel")} + + setHasOpenedLogoutLink(true)} + kind="primary" + href={logoutUrl} + target="_blank" + > + {_t("action|continue")} + + + )} +
      +
      + ); +}; diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index 2ee052d2a2a..0bead58e7df 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -305,12 +305,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent{_t("Only do this if you have no other device to complete verification with.")}

      {_t( - "If you reset everything, you will restart with no trusted sessions, no trusted users, and " + - "might not be able to see past messages.", + "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.", )}

      {"\uD83D\uDC4E "} {_t( - "Unable to access secret storage. " + - "Please verify that you entered the correct Security Phrase.", + "Unable to access secret storage. Please verify that you entered the correct Security Phrase.", )} ); @@ -368,7 +366,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {keyStatus} - {_t("Upload")} + {_t("action|upload")} {recoveryKeyFeedback}

      {_t( - "Deleting cross-signing keys is permanent. " + - "Anyone you have verified with will see security alerts. " + - "You almost certainly don't want to do this, unless " + - "you've lost every device you can cross-sign from.", + "Deleting cross-signing keys is permanent. Anyone you have verified with will see security alerts. You almost certainly don't want to do this, unless you've lost every device you can cross-sign from.", )}

      @@ -55,7 +52,7 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index ba54bc28f5d..0a55fc6de4e 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -16,9 +16,8 @@ limitations under the License. */ import React from "react"; -import { CrossSigningKeys, AuthDict, MatrixError, UIAFlow } from "matrix-js-sdk/src/matrix"; +import { CrossSigningKeys, AuthDict, MatrixError, UIAFlow, UIAResponse } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { UIAResponse } from "matrix-js-sdk/src/@types/uia"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t } from "../../../../languageHandler"; @@ -121,7 +120,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent{_t("Unable to set up keys")}

      diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index fdf558e8ea8..34399a25bbf 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -338,7 +338,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent ); } else if (this.state.loadError) { - title = _t("Error"); + title = _t("common|error"); content = _t("Unable to load backup status"); } else if (this.state.restoreError) { if ( @@ -351,8 +351,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

      {_t( - "Backup could not be decrypted with this Security Key: " + - "please verify that you entered the correct Security Key.", + "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.", )}

      @@ -363,19 +362,18 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

      {_t( - "Backup could not be decrypted with this Security Phrase: " + - "please verify that you entered the correct Security Phrase.", + "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.", )}

      ); } } else { - title = _t("Error"); + title = _t("common|error"); content = _t("Unable to restore backup"); } } else if (this.state.backupInfo === null) { - title = _t("Error"); + title = _t("common|error"); content = _t("No backup found!"); } else if (this.state.recoverInfo) { title = _t("Keys restored"); @@ -398,7 +396,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {failedToDecrypt}

      {_t( - "Access your secure message history and set up secure " + - "messaging by entering your Security Phrase.", + "Access your secure message history and set up secure messaging by entering your Security Phrase.", )}

      @@ -432,7 +429,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {_t( - "If you've forgotten your Security Phrase you can " + - "use your Security Key or " + - "set up new recovery options", + "If you've forgotten your Security Phrase you can use your Security Key or set up new recovery options", {}, { button1: (s) => ( @@ -493,8 +488,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

      {_t( - "Access your secure message history and set up secure " + - "messaging by entering your Security Key.", + "Access your secure message history and set up secure messaging by entering your Security Key.", )}

      @@ -507,7 +501,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {keyStatus} {_t( - "If you've forgotten your Security Key you can " + - "", + "If you've forgotten your Security Key you can ", {}, { button: (s) => ( diff --git a/src/components/views/dialogs/spotlight/Filter.ts b/src/components/views/dialogs/spotlight/Filter.ts new file mode 100644 index 00000000000..11cdb6dc6ba --- /dev/null +++ b/src/components/views/dialogs/spotlight/Filter.ts @@ -0,0 +1,21 @@ +/* +Copyright 2021 - 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export enum Filter { + People, + PublicRooms, + PublicSpaces, +} diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 8ccffaf1b84..758ff320924 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -17,8 +17,14 @@ limitations under the License. import { WebSearch as WebSearchEvent } from "@matrix-org/analytics-events/types/typescript/WebSearch"; import classNames from "classnames"; import { capitalize, sum } from "lodash"; -import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces"; -import { IPublicRoomsChunkRoom, MatrixClient, RoomMember, RoomType, Room } from "matrix-js-sdk/src/matrix"; +import { + IPublicRoomsChunkRoom, + MatrixClient, + RoomMember, + RoomType, + Room, + HierarchyRoom, +} from "matrix-js-sdk/src/matrix"; import { normalize } from "matrix-js-sdk/src/utils"; import React, { ChangeEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import sanitizeHtml from "sanitize-html"; @@ -65,7 +71,6 @@ import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar"; import { SearchResultAvatar } from "../../avatars/SearchResultAvatar"; import { NetworkDropdown } from "../../directory/NetworkDropdown"; import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; -import LabelledCheckbox from "../../elements/LabelledCheckbox"; import Spinner from "../../elements/Spinner"; import NotificationBadge from "../../rooms/NotificationBadge"; import BaseDialog from "../BaseDialog"; @@ -79,10 +84,11 @@ import RoomAvatar from "../../avatars/RoomAvatar"; import { useFeatureEnabled } from "../../../../hooks/useSettings"; import { filterBoolean } from "../../../../utils/arrays"; import { transformSearchTerm } from "../../../../utils/SearchInput"; +import { Filter } from "./Filter"; const MAX_RECENT_SEARCHES = 10; const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons -const AVATAR_SIZE = 24; +const AVATAR_SIZE = "24px"; interface IProps { initialText?: string; @@ -94,11 +100,11 @@ function refIsForRecentlyViewed(ref?: RefObject): boolean { return ref?.current?.id?.startsWith("mx_SpotlightDialog_button_recentlyViewed_") === true; } -function getRoomTypes(showRooms: boolean, showSpaces: boolean): Set { +function getRoomTypes(filter: Filter | null): Set { const roomTypes = new Set(); - if (showRooms) roomTypes.add(null); - if (showSpaces) roomTypes.add(RoomType.Space); + if (filter === Filter.PublicRooms) roomTypes.add(null); + if (filter === Filter.PublicSpaces) roomTypes.add(RoomType.Space); return roomTypes; } @@ -108,20 +114,17 @@ enum Section { Rooms, Spaces, Suggestions, - PublicRooms, -} - -export enum Filter { - People, - PublicRooms, + PublicRoomsAndSpaces, } function filterToLabel(filter: Filter): string { switch (filter) { case Filter.People: - return _t("People"); + return _t("common|people"); case Filter.PublicRooms: return _t("Public rooms"); + case Filter.PublicSpaces: + return _t("Public spaces"); } } @@ -162,8 +165,8 @@ const isMemberResult = (result: any): result is IMemberResult => !!result?.membe const toPublicRoomResult = (publicRoom: IPublicRoomsChunkRoom): IPublicRoomResult => ({ publicRoom, - section: Section.PublicRooms, - filter: [Filter.PublicRooms], + section: Section.PublicRoomsAndSpaces, + filter: [Filter.PublicRooms, Filter.PublicSpaces], query: filterBoolean([ publicRoom.room_id.toLowerCase(), publicRoom.canonical_alias?.toLowerCase(), @@ -302,7 +305,16 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n const [inviteLinkCopied, setInviteLinkCopied] = useState(false); const trimmedQuery = useMemo(() => query.trim(), [query]); - const exploringPublicSpacesEnabled = useFeatureEnabled("feature_exploring_public_spaces"); + const [supportsSpaceFiltering, setSupportsSpaceFiltering] = useState(true); // assume it does until we find out it doesn't + useEffect(() => { + cli.isVersionSupported("v1.4") + .then((supported) => { + return supported || cli.doesServerSupportUnstableFeature("org.matrix.msc3827.stable"); + }) + .then((supported) => { + setSupportsSpaceFiltering(supported); + }); + }, [cli]); const { loading: publicRoomsLoading, @@ -313,21 +325,23 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n search: searchPublicRooms, error: publicRoomsError, } = usePublicRoomDirectory(); - const [showRooms, setShowRooms] = useState(true); - const [showSpaces, setShowSpaces] = useState(false); const { loading: peopleLoading, users: userDirectorySearchResults, search: searchPeople } = useUserDirectory(); const { loading: profileLoading, profile, search: searchProfileInfo } = useProfileInfo(); const searchParams: [IDirectoryOpts] = useMemo( () => [ { query: trimmedQuery, - roomTypes: getRoomTypes(showRooms, showSpaces), + roomTypes: getRoomTypes(filter), limit: SECTION_LIMIT, }, ], - [trimmedQuery, showRooms, showSpaces], + [trimmedQuery, filter], + ); + useDebouncedCallback( + filter === Filter.PublicRooms || filter === Filter.PublicSpaces, + searchPublicRooms, + searchParams, ); - useDebouncedCallback(filter === Filter.PublicRooms, searchPublicRooms, searchParams); useDebouncedCallback(filter === Filter.People, searchPeople, searchParams); useDebouncedCallback(filter === Filter.People, searchProfileInfo, searchParams); @@ -397,7 +411,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n [Section.Rooms]: [], [Section.Spaces]: [], [Section.Suggestions]: [], - [Section.PublicRooms]: [], + [Section.PublicRoomsAndSpaces]: [], }; // Group results in their respective sections @@ -430,7 +444,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n results[entry.section].push(entry); }); - } else if (filter === Filter.PublicRooms) { + } else if (filter === Filter.PublicRooms || filter === Filter.PublicSpaces) { // return all results for public rooms if no query is given possibleResults.forEach((entry) => { if (isPublicRoomResult(entry)) { @@ -543,6 +557,15 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n {trimmedQuery ? _t('Use "%(query)s" to search', { query }) : _t("Search for")}
      + {filter !== Filter.PublicSpaces && supportsSpaceFiltering && ( + + )} {filter !== Filter.PublicRooms && (
      ); @@ -763,22 +781,18 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n } let publicRoomsSection: JSX.Element | undefined; - if (filter === Filter.PublicRooms) { + if (filter === Filter.PublicRooms || filter === Filter.PublicSpaces) { let content: JSX.Element | JSX.Element[]; - if (!showRooms && !showSpaces) { + if (publicRoomsError) { content = (
      - {_t("You cannot search for rooms that are neither a room nor a space")} -
      - ); - } else if (publicRoomsError) { - content = ( -
      - {_t("Failed to query public rooms")} + {filter === Filter.PublicRooms + ? _t("Failed to query public rooms") + : _t("Failed to query public spaces")}
      ); } else { - content = results[Section.PublicRooms].slice(0, SECTION_LIMIT).map(resultMapper); + content = results[Section.PublicRoomsAndSpaces].slice(0, SECTION_LIMIT).map(resultMapper); } publicRoomsSection = ( @@ -788,22 +802,8 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n aria-labelledby="mx_SpotlightDialog_section_publicRooms" >
      -

      {_t("Suggestions")}

      +

      {_t("common|suggestions")}

      - {exploringPublicSpacesEnabled && ( - <> - - - - )}
      @@ -825,7 +825,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n
      {spaceResults.slice(0, SECTION_LIMIT).map( - (room: IHierarchyRoom): JSX.Element => ( + (room: HierarchyRoom): JSX.Element => (
      ); - } else if (trimmedQuery && filter === Filter.PublicRooms) { + } else if (trimmedQuery && (filter === Filter.PublicRooms || filter === Filter.PublicSpaces)) { hiddenResultsSection = (

      {_t("Some results may be hidden")}

      - {_t( - "If you can't find the room you're looking for, " + - "ask for an invite or create a new room.", - )} + {_t("If you can't find the room you're looking for, ask for an invite or create a new room.")}
      - {_t("Continue")} + {_t("action|continue")}
      diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 7badebd7051..a25fc464292 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -526,7 +526,7 @@ export default class AppTile extends React.Component { return ( - + {name} {title ? titleSpacer : ""} @@ -620,7 +620,7 @@ export default class AppTile extends React.Component { const loadingElement = (
      - +
      ); @@ -785,7 +785,7 @@ export default class AppTile extends React.Component { {this.state.hasContextMenuOptions && ( = ({ children, getTextToCopy, border = true
      {children} { + private getTab(type: TabId, label: TranslationKey): Tab { const sources = this.state.sources .filter((source) => source.id.startsWith(type)) .map((source) => { @@ -154,8 +154,8 @@ export default class DesktopCapturerSourcePicker extends React.Component> = [ - this.getTab("screen", _t("Share entire screen")), - this.getTab("window", _t("Application window")), + this.getTab("screen", _td("Share entire screen")), + this.getTab("window", _td("Application window")), ]; return ( @@ -166,7 +166,7 @@ export default class DesktopCapturerSourcePicker extends React.Component { className={this.props.cancelButtonClass} disabled={this.props.disabled} > - {this.props.cancelButton || _t("Cancel")} + {this.props.cancelButton || _t("action|cancel")} ); } diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index 305cee51967..0a5786a1cb2 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -320,7 +320,7 @@ export default class Dropdown extends React.Component { if (!options?.length) { return [
    • - {_t("No results")} + {_t("common|no_results")}
    • , ]; } diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx index 87576afcf89..de30fb3f91f 100644 --- a/src/components/views/elements/EditableItemList.tsx +++ b/src/components/views/elements/EditableItemList.tsx @@ -67,14 +67,14 @@ export class EditableItem extends React.Component { kind="primary_sm" className="mx_EditableItem_confirmBtn" > - {_t("Yes")} + {_t("action|yes")} - {_t("No")} + {_t("action|no")} ); @@ -82,7 +82,12 @@ export class EditableItem extends React.Component { return (
      -
      +
      {this.props.value}
      ); @@ -142,7 +147,7 @@ export default class EditableItemList

      extends React.PureComponent - {_t("Add")} + {_t("action|add")} ); diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index dca7d4562f6..4ecb8a17ee1 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -85,8 +85,7 @@ export default class ErrorBoundary extends React.PureComponent {

      {_t( - "Please create a new issue " + - "on GitHub so that we can investigate this bug.", + "Please create a new issue on GitHub so that we can investigate this bug.", {}, { newIssueLink: (sub) => { @@ -101,15 +100,10 @@ export default class ErrorBoundary extends React.PureComponent {

      {_t( - "If you've submitted a bug via GitHub, debug logs can help " + - "us track down the problem. ", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. ", )} {_t( - "Debug logs contain application " + - "usage data including your username, the IDs or aliases of " + - "the rooms you have visited, which UI elements you " + - "last interacted with, and the usernames of other users. " + - "They do not contain messages.", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms you have visited, which UI elements you last interacted with, and the usernames of other users. They do not contain messages.", )}

      diff --git a/src/components/views/elements/FacePile.tsx b/src/components/views/elements/FacePile.tsx index 6a03f7cbbe8..6c31614c06e 100644 --- a/src/components/views/elements/FacePile.tsx +++ b/src/components/views/elements/FacePile.tsx @@ -23,25 +23,19 @@ import TextWithTooltip from "./TextWithTooltip"; interface IProps extends HTMLAttributes { members: RoomMember[]; - faceSize: number; + size: string; overflow: boolean; tooltip?: ReactNode; children?: ReactNode; } -const FacePile: FC = ({ members, faceSize, overflow, tooltip, children, ...props }) => { +const FacePile: FC = ({ members, size, overflow, tooltip, children, ...props }) => { const faces = members.map( tooltip - ? (m) => + ? (m) => : (m) => ( - + ), ); diff --git a/src/components/views/elements/GenericEventListSummary.tsx b/src/components/views/elements/GenericEventListSummary.tsx index f9668c2ca46..79dda584169 100644 --- a/src/components/views/elements/GenericEventListSummary.tsx +++ b/src/components/views/elements/GenericEventListSummary.tsx @@ -103,7 +103,7 @@ const GenericEventListSummary: React.FC = ({ }), (member) => member.getMxcAvatarUrl(), ); - const avatars = uniqueMembers.map((m) => ); + const avatars = uniqueMembers.map((m) => ); body = (
      @@ -130,7 +130,7 @@ const GenericEventListSummary: React.FC = ({ onClick={toggleExpanded} aria-expanded={expanded} > - {expanded ? _t("collapse") : _t("expand")} + {expanded ? _t("action|collapse") : _t("action|expand")} {body} diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 7162d7ad804..d66014b7ed6 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -477,8 +477,7 @@ export default class ImageView extends React.Component { ); @@ -504,7 +503,7 @@ export default class ImageView extends React.Component { contextMenuButton = ( { const zoomOutButton = ( ); const zoomInButton = ( ); @@ -531,7 +530,7 @@ export default class ImageView extends React.Component { if (this.props.mxEvent?.getContent()) { title = (
      - {presentableTextForFile(this.props.mxEvent?.getContent(), _t("Image"), true)} + {presentableTextForFile(this.props.mxEvent?.getContent(), _t("common|image"), true)}
      ); } @@ -565,13 +564,13 @@ export default class ImageView extends React.Component { /> {contextMenuButton} {this.renderContextMenu()} diff --git a/src/components/views/elements/InlineSpinner.tsx b/src/components/views/elements/InlineSpinner.tsx index 144463c3245..866fc4b1d86 100644 --- a/src/components/views/elements/InlineSpinner.tsx +++ b/src/components/views/elements/InlineSpinner.tsx @@ -36,7 +36,7 @@ export default class InlineSpinner extends React.PureComponent {
      {this.props.children}
      diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index 5de1ffe7853..ff27b112834 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import React, { ReactElement } from "react"; +import classNames from "classnames"; import * as languageHandler from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; @@ -24,9 +25,10 @@ import Spinner from "./Spinner"; import Dropdown from "./Dropdown"; import { NonEmptyArray } from "../../../@types/common"; -type Languages = Awaited>; +type Languages = Awaited>; function languageMatchesSearchQuery(query: string, language: Languages[0]): boolean { + if (language.labelInTargetLanguage.toUpperCase().includes(query.toUpperCase())) return true; if (language.label.toUpperCase().includes(query.toUpperCase())) return true; if (language.value.toUpperCase() === query.toUpperCase()) return true; return false; @@ -56,23 +58,30 @@ export default class LanguageDropdown extends React.Component { public componentDidMount(): void { languageHandler - .getAllLanguagesFromJson() + .getAllLanguagesWithLabels() .then((langs) => { langs.sort(function (a, b) { - if (a.label < b.label) return -1; - if (a.label > b.label) return 1; + if (a.labelInTargetLanguage < b.labelInTargetLanguage) return -1; + if (a.labelInTargetLanguage > b.labelInTargetLanguage) return 1; return 0; }); this.setState({ langs }); }) .catch(() => { - this.setState({ langs: [{ value: "en", label: "English" }] }); + this.setState({ + langs: [ + { + value: "en", + label: "English", + labelInTargetLanguage: "English", + }, + ], + }); }); if (!this.props.value) { - // If no value is given, we start with the first - // country selected, but our parent component - // doesn't know this, therefore we do this. + // If no value is given, we start with the first country selected, + // but our parent component doesn't know this, therefore we do this. const language = languageHandler.getUserLanguage(); this.props.onOptionChange(language); } @@ -89,7 +98,7 @@ export default class LanguageDropdown extends React.Component { return ; } - let displayedLanguages: Awaited>; + let displayedLanguages: Awaited>; if (this.state.searchQuery) { displayedLanguages = this.state.langs.filter((lang) => { return languageMatchesSearchQuery(this.state.searchQuery, lang); @@ -99,7 +108,7 @@ export default class LanguageDropdown extends React.Component { } const options = displayedLanguages.map((language) => { - return
      {language.label}
      ; + return
      {language.labelInTargetLanguage}
      ; }) as NonEmptyArray; // default value here too, otherwise we need to handle null / undefined @@ -116,7 +125,7 @@ export default class LanguageDropdown extends React.Component { return ( = ({ title, description, ...rest }) => Modal.createDialog(InfoDialog, { title, description, - button: _t("Got it"), + button: _t("action|got_it"), hasCloseButton: true, }); }; return ( - {_t("Learn more")} + {_t("action|learn_more")} ); }; diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index dd02d3238d6..35414c87824 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -26,7 +26,7 @@ import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import AccessibleButton from "./AccessibleButton"; import Spinner from "./Spinner"; -export const AVATAR_SIZE = 52; +export const AVATAR_SIZE = "52px"; interface IProps { hasAvatar: boolean; diff --git a/src/components/views/elements/Pill.tsx b/src/components/views/elements/Pill.tsx index 7e96a21b6f0..5770145b788 100644 --- a/src/components/views/elements/Pill.tsx +++ b/src/components/views/elements/Pill.tsx @@ -44,7 +44,7 @@ export const pillRoomNotifLen = (): number => { return "@room".length; }; -const linkIcon = ; +const linkIcon = ; const PillRoomAvatar: React.FC<{ shouldShowPillAvatar: boolean; @@ -55,7 +55,7 @@ const PillRoomAvatar: React.FC<{ } if (room) { - return
      diff --git a/src/components/views/location/Marker.tsx b/src/components/views/location/Marker.tsx index c38f43b0d37..5045d87a03b 100644 --- a/src/components/views/location/Marker.tsx +++ b/src/components/views/location/Marker.tsx @@ -78,8 +78,7 @@ const Marker = React.forwardRef(({ id, roomMember, useMem {roomMember ? ( = ({ onBack, onCancel, displayBack }) @@ -44,7 +44,7 @@ const ShareDialogButtons: React.FC = ({ onBack, onCancel, displayBack }) diff --git a/src/components/views/location/ShareType.tsx b/src/components/views/location/ShareType.tsx index d17bcdc8ce3..4e99c286227 100644 --- a/src/components/views/location/ShareType.tsx +++ b/src/components/views/location/ShareType.tsx @@ -31,8 +31,8 @@ const UserAvatar: React.FC = () => { const userId = matrixClient.getSafeUserId(); const displayName = OwnProfileStore.instance.displayName ?? undefined; // 40 - 2px border - const avatarSize = 36; - const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize) ?? undefined; + const avatarSize = "36px"; + const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(parseInt(avatarSize, 10)) ?? undefined; return (
      @@ -40,9 +40,7 @@ const UserAvatar: React.FC = () => { idName={userId} name={displayName} url={avatarUrl} - width={avatarSize} - height={avatarSize} - resizeMethod="crop" + size={avatarSize} className="mx_UserMenu_userAvatar_BaseAvatar" />
      diff --git a/src/components/views/location/ZoomButtons.tsx b/src/components/views/location/ZoomButtons.tsx index 461cdad3cdb..970835cffd4 100644 --- a/src/components/views/location/ZoomButtons.tsx +++ b/src/components/views/location/ZoomButtons.tsx @@ -40,7 +40,7 @@ const ZoomButtons: React.FC = ({ map }) => { @@ -48,7 +48,7 @@ const ZoomButtons: React.FC = ({ map }) => { diff --git a/src/components/views/location/shareLocation.ts b/src/components/views/location/shareLocation.ts index a0c7b6febc3..dd3a0ce9d0a 100644 --- a/src/components/views/location/shareLocation.ts +++ b/src/components/views/location/shareLocation.ts @@ -14,10 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, IContent, IEventRelation, MatrixError, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix"; -import { makeLocationContent, makeBeaconInfoContent } from "matrix-js-sdk/src/content-helpers"; +import { + MatrixClient, + IContent, + IEventRelation, + MatrixError, + THREAD_RELATION_TYPE, + ContentHelpers, + LocationAssetType, +} from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { LocationAssetType } from "matrix-js-sdk/src/@types/location"; import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; @@ -57,7 +63,7 @@ const getPermissionsErrorParams = ( const modalParams = { title: _t("You don't have permission to share locations"), description: _t("You need to have the right permissions in order to share locations in this room."), - button: _t("OK"), + button: _t("action|ok"), hasCancelButton: false, onFinished: () => {}, // NOOP }; @@ -80,8 +86,8 @@ const getDefaultErrorParams = ( description: _t("%(brand)s could not send your location. Please try again later.", { brand: SdkConfig.get().brand, }), - button: _t("Try again"), - cancelButton: _t("Cancel"), + button: _t("action|try_again"), + cancelButton: _t("action|cancel"), onFinished: (tryAgain: boolean) => { if (tryAgain) { openMenu(); @@ -109,7 +115,7 @@ export const shareLiveLocation = try { await OwnBeaconStore.instance.createLiveBeacon( roomId, - makeBeaconInfoContent( + ContentHelpers.makeBeaconInfoContent( timeout ?? DEFAULT_LIVE_DURATION, true /* isLive */, description, @@ -134,7 +140,13 @@ export const shareLocation = try { const threadId = (relation?.rel_type === THREAD_RELATION_TYPE.name && relation?.event_id) || null; const assetType = shareType === LocationShareType.Pin ? LocationAssetType.Pin : LocationAssetType.Self; - const content = makeLocationContent(undefined, uri, timestamp, undefined, assetType) as IContent; + const content = ContentHelpers.makeLocationContent( + undefined, + uri, + timestamp, + undefined, + assetType, + ) as IContent; await doMaybeLocalRoomAction( roomId, (actualRoomId: string) => client.sendMessage(actualRoomId, threadId, content), diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index 067bffce30f..575f19c3a24 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -62,8 +62,7 @@ const ActiveCallEvent = forwardRef( member={mxEvent.sender} fallbackUserId={mxEvent.getSender()} viewUserOnClick - width={24} - height={24} + size="24px" />
      @@ -76,7 +75,7 @@ const ActiveCallEvent = forwardRef( active={false} participantCount={participatingMembers.length} /> - +
      {call && } (({ mxE const [buttonText, buttonKind, onButtonClick] = useMemo(() => { switch (connectionState) { case ConnectionState.Disconnected: - return [_t("Join"), "primary", connect]; + return [_t("action|join"), "primary", connect]; case ConnectionState.Connecting: - return [_t("Join"), "primary", null]; + return [_t("action|join"), "primary", null]; case ConnectionState.Connected: - return [_t("Leave"), "danger", disconnect]; + return [_t("action|leave"), "danger", disconnect]; case ConnectionState.Disconnecting: - return [_t("Leave"), "danger", null]; + return [_t("action|leave"), "danger", null]; } }, [connectionState, connect, disconnect]); @@ -189,7 +188,7 @@ export const CallEvent = forwardRef(({ mxEvent }, ref) => { mxEvent={mxEvent} call={null} participatingMembers={[]} - buttonText={_t("Join")} + buttonText={_t("action|join")} buttonKind="primary" onButtonClick={null} /> diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 0e6815ed36a..4175b7d0218 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -19,8 +19,8 @@ import React from "react"; import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t } from "../../../languageHandler"; -import { formatFullDateNoDay, formatFullDateNoTime } from "../../../DateUtils"; +import { _t, getUserLanguage } from "../../../languageHandler"; +import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; @@ -40,10 +40,6 @@ import JumpToDatePicker from "./JumpToDatePicker"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { SdkContextClass } from "../../../contexts/SDKContext"; -function getDaysArray(): string[] { - return [_t("Sunday"), _t("Monday"), _t("Tuesday"), _t("Wednesday"), _t("Thursday"), _t("Friday"), _t("Saturday")]; -} - interface IProps { roomId: string; ts: number; @@ -105,15 +101,16 @@ export default class DateSeparator extends React.Component { const today = new Date(); const yesterday = new Date(); - const days = getDaysArray(); + const days = getDaysArray("long"); yesterday.setDate(today.getDate() - 1); + const relativeTimeFormat = new Intl.RelativeTimeFormat(getUserLanguage(), { style: "long", numeric: "auto" }); if (date.toDateString() === today.toDateString()) { - return _t("Today"); + return relativeTimeFormat.format(0, "day"); // Today } else if (date.toDateString() === yesterday.toDateString()) { - return _t("Yesterday"); + return relativeTimeFormat.format(-1, "day"); // Yesterday } else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { - return days[date.getDay()]; + return days[date.getDay()]; // Sunday-Saturday } else { return formatFullDateNoTime(date); } @@ -170,16 +167,12 @@ export default class DateSeparator extends React.Component { let submitDebugLogsContent: JSX.Element = <>; if (err instanceof ConnectionError) { friendlyErrorMessage = _t( - "A network error occurred while trying to find and jump to the given date. " + - "Your homeserver might be down or there was just a temporary problem with " + - "your internet connection. Please try again. If this continues, please " + - "contact your homeserver administrator.", + "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", ); } else if (err instanceof MatrixError) { if (err?.errcode === "M_NOT_FOUND") { friendlyErrorMessage = _t( - "We were unable to find an event looking forwards from %(dateString)s. " + - "Try choosing an earlier date.", + "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.", { dateString: formatFullDateNoDay(new Date(unixTimestamp)) }, ); } else { @@ -195,8 +188,7 @@ export default class DateSeparator extends React.Component { submitDebugLogsContent = (

      {_t( - "Please submit debug logs to help us " + - "track down the problem.", + "Please submit debug logs to help us track down the problem.", {}, { debugLogsLink: (sub) => ( diff --git a/src/components/views/messages/DisambiguatedProfile.tsx b/src/components/views/messages/DisambiguatedProfile.tsx index 20788b71cbf..6810723c6e3 100644 --- a/src/components/views/messages/DisambiguatedProfile.tsx +++ b/src/components/views/messages/DisambiguatedProfile.tsx @@ -40,7 +40,7 @@ export default class DisambiguatedProfile extends React.Component { let colorClass: string | undefined; if (colored) { - colorClass = getUserNameColorClass(fallbackName); + colorClass = getUserNameColorClass(mxid ?? ""); } let mxidElement; diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 8e974c1e15e..5123ed63f2a 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -22,7 +22,7 @@ import { Icon as DownloadIcon } from "../../../../res/img/download.svg"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Spinner from "../elements/Spinner"; -import { _t, _td } from "../../../languageHandler"; +import { _t, _td, TranslationKey } from "../../../languageHandler"; import { FileDownloader } from "../../../utils/FileDownloader"; interface IProps { @@ -37,7 +37,7 @@ interface IProps { interface IState { loading: boolean; blob?: Blob; - tooltip: string; + tooltip: TranslationKey; } export default class DownloadActionButton extends React.PureComponent { @@ -95,7 +95,7 @@ export default class DownloadActionButton extends React.PureComponent diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index f49db88d7c1..49e0f1f7de4 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -137,13 +137,13 @@ export default class EditHistoryMessage extends React.PureComponent{_t("Remove")}; + redactButton = {_t("action|remove")}; } let viewSourceButton: JSX.Element | undefined; if (SettingsStore.getValue("developerMode")) { viewSourceButton = ( - {_t("View Source")} + {_t("action|view_source")} ); } diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index bcd6136ec90..b9566f65306 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -53,23 +53,21 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp } else if (dmPartner) { const displayName = room?.getMember(dmPartner)?.rawDisplayName || dmPartner; subtitle = _t( - "Messages here are end-to-end encrypted. " + - "Verify %(displayName)s in their profile - tap on their profile picture.", + "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their profile picture.", { displayName }, ); } else if (room && isLocalRoom(room)) { subtitle = _t("Messages in this chat will be end-to-end encrypted."); } else { subtitle = _t( - "Messages in this room are end-to-end encrypted. " + - "When people join, you can verify them in their profile, just tap on their profile picture.", + "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their profile picture.", ); } return ( @@ -80,7 +78,7 @@ const EncryptionEvent = forwardRef(({ mxEvent, timestamp return ( diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx index 9516d035779..c56d424d950 100644 --- a/src/components/views/messages/LegacyCallEvent.tsx +++ b/src/components/views/messages/LegacyCallEvent.tsx @@ -137,14 +137,14 @@ export default class LegacyCallEvent extends React.PureComponent onClick={this.props.callEventGrouper.rejectCall} kind="danger" > - {_t("Decline")} + {_t("action|decline")} - {_t("Accept")} + {_t("action|accept")} {this.props.timestamp}

      @@ -234,7 +234,7 @@ export default class LegacyCallEvent extends React.PureComponent kind={InfoTooltipKind.Warning} /> {_t("Connection failed")} - {this.renderCallBackButton(_t("Retry"))} + {this.renderCallBackButton(_t("action|retry"))} {this.props.timestamp}
      ); @@ -290,7 +290,7 @@ export default class LegacyCallEvent extends React.PureComponent
      {silenceIcon}
      - +
      {sender}
      diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index c8e7f3b17e3..eeed20d5673 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -23,10 +23,10 @@ import { MatrixClient, RelationType, IRedactOpts, + ContentHelpers, + M_BEACON, } from "matrix-js-sdk/src/matrix"; -import { BeaconLocationState } from "matrix-js-sdk/src/content-helpers"; import { randomString } from "matrix-js-sdk/src/randomstring"; -import { M_BEACON } from "matrix-js-sdk/src/@types/beacon"; import classNames from "classnames"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -51,7 +51,7 @@ const useBeaconState = ( ): { beacon?: Beacon; description?: string; - latestLocationState?: BeaconLocationState; + latestLocationState?: ContentHelpers.BeaconLocationState; isLive?: boolean; waitingToStart?: boolean; } => { diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index 1fc0004fa1c..f430b0f9bdd 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -133,7 +133,7 @@ export default class MFileBody extends React.Component { } private get fileName(): string { - return this.content.body && this.content.body.length > 0 ? this.content.body : _t("Attachment"); + return this.content.body && this.content.body.length > 0 ? this.content.body : _t("common|attachment"); } private get linkText(): string { @@ -173,7 +173,7 @@ export default class MFileBody extends React.Component { } catch (err) { logger.warn("Unable to decrypt attachment: ", err); Modal.createDialog(ErrorDialog, { - title: _t("Error"), + title: _t("common|error"), description: _t("Error decrypting attachment"), }); } @@ -205,9 +205,9 @@ export default class MFileBody extends React.Component { placeholder = ( - + - {presentableTextForFile(this.content, _t("Attachment"), true, true)} + {presentableTextForFile(this.content, _t("common|attachment"), true, true)} @@ -284,7 +284,7 @@ export default class MFileBody extends React.Component { */}