diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 945de01eba..76183f6931 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -62,6 +62,9 @@ $SpaceRoomViewInnerWidth: 428px; } .mx_SpaceRoomView { + overflow-y: auto; + flex: 1; + .mx_MainSplit > div:first-child { padding: 80px 60px; flex-grow: 1; @@ -248,6 +251,7 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_landing { display: flex; flex-direction: column; + min-width: 0; > .mx_BaseAvatar_image, > .mx_BaseAvatar > .mx_BaseAvatar_image { diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index f87bb571d6..ffe1011f51 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -7,13 +7,15 @@ background: $primary-bg-color; border-bottom: none; border-radius: 8px 8px 0 0; - max-height: 50vh; - overflow: auto; + max-height: 35vh; + overflow: clip; + display: flex; box-shadow: 0px -16px 32px $composer-shadow-color; } .mx_Autocomplete_ProviderSection { border-bottom: 1px solid $primary-hairline-color; + width: 100%; } /* a "block" completion takes up a whole line */ @@ -59,8 +61,8 @@ .mx_Autocomplete_Completion_container_pill { margin: 12px; - display: flex; - flex-direction: column; + height: 100%; + overflow-y: scroll; } .mx_Autocomplete_Completion_container_truncate { diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index f270146606..8c02affbed 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -105,6 +105,8 @@ limitations under the License. .mx_ReplyTile .mx_SenderProfile { display: block; + top: unset; + left: unset; } .mx_ReactionsRow { @@ -188,8 +190,6 @@ limitations under the License. } .mx_ReplyThread { - margin: 0 calc(-1 * var(--gutterSize)); - .mx_EventTile_reply { max-width: 90%; padding: 0; @@ -223,11 +223,6 @@ limitations under the License. margin-left: -9px; } - .mx_ReplyThread { - border-left-width: 2px; - border-left-color: $eventbubble-reply-color; - } - /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { display: grid; diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 0aae8c6372..326651e037 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -67,7 +67,7 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); this.matcher = new QueryMatcher(SORTED_EMOJI, { - keys: ['emoji.emoticon'], + keys: [], funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)], // For matching against ascii equivalents shouldMatchWordsOnly: false, @@ -91,7 +91,8 @@ export default class EmojiProvider extends AutocompleteProvider { let completions = []; const { command, range } = this.getCurrentCommand(query, selection); - if (command) { + + if (command && command[0].length > 2) { const matchedString = command[0]; completions = this.matcher.match(matchedString, limit); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index ba4b2e0b56..2c932e3610 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -628,9 +628,12 @@ class LoggedInView extends React.Component { break; } + const wrapperClasses = classNames({ + 'mx_MatrixChat_wrapper': true, + 'mx_MatrixChat_useCompactLayout': this.state.useCompactLayout, + }); const bodyClasses = classNames({ 'mx_MatrixChat': true, - 'mx_MatrixChat_useCompactLayout': this.state.useCompactLayout, 'mx_MatrixChat--with-avatar': this.state.backgroundImage, }); @@ -645,7 +648,7 @@ class LoggedInView extends React.Component {
diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 8f22c7ca9a..38566cdf04 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -215,7 +215,7 @@ export default class BugReportDialog extends React.Component { { a: (sub) => { sub } , diff --git a/src/components/views/dialogs/FeedbackDialog.js b/src/components/views/dialogs/FeedbackDialog.js index 85171c9bf6..ceb8cb2175 100644 --- a/src/components/views/dialogs/FeedbackDialog.js +++ b/src/components/views/dialogs/FeedbackDialog.js @@ -28,7 +28,7 @@ import StyledRadioGroup from "../elements/StyledRadioGroup"; const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" + "?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc"; -const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; +const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose"; export default (props) => { const [rating, setRating] = useState(""); diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index 03d331bd9f..50ea7d9a56 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -77,7 +77,7 @@ export default class ErrorBoundary extends React.PureComponent<{}, IState> { render() { if (this.state.error) { - const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; + const newIssueUrl = "https://github.com/vector-im/element-web/issues/new/choose"; let bugReportSection; if (SdkConfig.get().bug_report_endpoint_url) { diff --git a/src/components/views/messages/MVoiceOrAudioBody.tsx b/src/components/views/messages/MVoiceOrAudioBody.tsx index 2d78ea192e..5a7e34b8a1 100644 --- a/src/components/views/messages/MVoiceOrAudioBody.tsx +++ b/src/components/views/messages/MVoiceOrAudioBody.tsx @@ -19,14 +19,12 @@ import MAudioBody from "./MAudioBody"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import MVoiceMessageBody from "./MVoiceMessageBody"; import { IBodyProps } from "./IBodyProps"; +import { isVoiceMessage } from "../../../utils/EventUtils"; @replaceableComponent("views.messages.MVoiceOrAudioBody") export default class MVoiceOrAudioBody extends React.PureComponent { public render() { - // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245 - const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'] - || !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice']; - if (isVoiceMessage) { + if (isVoiceMessage(this.props.mxEvent)) { return ; } else { return ; diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 674bcdaec2..8a96b8a9ba 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -36,6 +36,7 @@ import { showSpaceInvite } from "../../../utils/space"; import { privateShouldBeEncrypted } from "../../../createRoom"; import EventTileBubble from "../messages/EventTileBubble"; import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); @@ -191,11 +192,21 @@ const NewRoomIntro = () => { }); } - const sub2 = _t( + const subText = _t( "Your private messages are normally encrypted, but this room isn't. "+ "Usually this is due to an unsupported device or method being used, " + - "like email invites. Enable encryption in settings.", {}, - { a: sub => { sub } }, + "like email invites.", + ); + + let subButton; + if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) { + subButton = ( + { _t("Enable encryption in settings.") } + ); + } + + const subtitle = ( + { subText } { subButton } ); return
@@ -204,7 +215,7 @@ const NewRoomIntro = () => { ) } diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 8c0e09c76c..cf7d1ce945 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -25,8 +25,9 @@ import MImageReplyBody from "../messages/MImageReplyBody"; import * as sdk from '../../../index'; import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; import { replaceableComponent } from '../../../utils/replaceableComponent'; -import { getEventDisplayInfo } from '../../../utils/EventUtils'; +import { getEventDisplayInfo, isVoiceMessage } from '../../../utils/EventUtils'; import MFileBody from "../messages/MFileBody"; +import MVoiceMessageBody from "../messages/MVoiceMessageBody"; interface IProps { mxEvent: MatrixEvent; @@ -95,7 +96,7 @@ export default class ReplyTile extends React.PureComponent { const msgType = mxEvent.getContent().msgtype; const evType = mxEvent.getType() as EventType; - const { tileHandler, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent); + const { tileHandler, isInfoMessage } = getEventDisplayInfo(mxEvent); // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!tileHandler) { @@ -109,14 +110,14 @@ export default class ReplyTile extends React.PureComponent { const EventTileType = sdk.getComponent(tileHandler); const classes = classNames("mx_ReplyTile", { - mx_ReplyTile_info: isInfoMessage && !this.props.mxEvent.isRedacted(), + mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(), mx_ReplyTile_audio: msgType === MsgType.Audio, mx_ReplyTile_video: msgType === MsgType.Video, }); let permalink = "#"; if (this.props.permalinkCreator) { - permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); + permalink = this.props.permalinkCreator.forEvent(mxEvent.getId()); } let sender; @@ -129,7 +130,7 @@ export default class ReplyTile extends React.PureComponent { if (needsSenderProfile) { sender = ; } @@ -137,7 +138,7 @@ export default class ReplyTile extends React.PureComponent { const msgtypeOverrides = { [MsgType.Image]: MImageReplyBody, // Override audio and video body with file body. We also hide the download/decrypt button using CSS - [MsgType.Audio]: MFileBody, + [MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody, [MsgType.Video]: MFileBody, }; const evOverrides = { @@ -151,14 +152,14 @@ export default class ReplyTile extends React.PureComponent { { sender }
diff --git a/src/components/views/settings/CrossSigningPanel.tsx b/src/components/views/settings/CrossSigningPanel.tsx index 21e38a762a..3fd67d6b5d 100644 --- a/src/components/views/settings/CrossSigningPanel.tsx +++ b/src/components/views/settings/CrossSigningPanel.tsx @@ -25,6 +25,8 @@ import InteractiveAuthDialog from '../dialogs/InteractiveAuthDialog'; import ConfirmDestroyCrossSigningDialog from '../dialogs/security/ConfirmDestroyCrossSigningDialog'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { MatrixEvent } from 'matrix-js-sdk/src'; +import SetupEncryptionDialog from '../dialogs/security/SetupEncryptionDialog'; +import { accessSecretStorage } from '../../../SecurityManager'; interface IState { error?: Error; @@ -72,7 +74,16 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> { }; private onBootstrapClick = () => { - this.bootstrapCrossSigning({ forceReset: false }); + if (this.state.crossSigningPrivateKeysInStorage) { + Modal.createTrackedDialog( + "Verify session", "Verify session", SetupEncryptionDialog, + {}, null, /* priority = */ false, /* static = */ true, + ); + } else { + // Trigger the flow to set up secure backup, which is what this will do when in + // the appropriate state. + accessSecretStorage(); + } }; private onStatusChanged = () => { @@ -176,10 +187,14 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> { summarisedStatus =

{ _t( "Your homeserver does not support cross-signing.", ) }

; - } else if (crossSigningReady) { + } else if (crossSigningReady && crossSigningPrivateKeysInStorage) { summarisedStatus =

✅ { _t( "Cross-signing is ready for use.", ) }

; + } else if (crossSigningReady && !crossSigningPrivateKeysInStorage) { + summarisedStatus =

⚠️ { _t( + "Cross-signing is ready but keys are not backed up.", + ) }

; } else if (crossSigningPrivateKeysInStorage) { summarisedStatus =

{ _t( "Your account has a cross-signing identity in secret storage, " + @@ -210,9 +225,13 @@ export default class CrossSigningPanel extends React.PureComponent<{}, IState> { // TODO: determine how better to expose this to users in addition to prompts at login/toast if (!keysExistEverywhere && homeserverSupportsCrossSigning) { + let buttonCaption = _t("Set up Secure Backup"); + if (crossSigningPrivateKeysInStorage) { + buttonCaption = _t("Verify this session"); + } actions.push( - { _t("Set up") } + { buttonCaption } , ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2c44bd5ff8..60e0ef832b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1103,9 +1103,9 @@ "Change Password": "Change Password", "Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.", "Cross-signing is ready for use.": "Cross-signing is ready for use.", + "Cross-signing is ready but keys are not backed up.": "Cross-signing is ready but keys are not backed up.", "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", "Cross-signing is not set up.": "Cross-signing is not set up.", - "Set up": "Set up", "Reset": "Reset", "Cross-signing public keys:": "Cross-signing public keys:", "in memory": "in memory", @@ -1203,6 +1203,7 @@ "Algorithm:": "Algorithm:", "Your keys are not being backed up from this session.": "Your keys are not being backed up from this session.", "Back up your keys before signing out to avoid losing them.": "Back up your keys before signing out to avoid losing them.", + "Set up": "Set up", "well formed": "well formed", "unexpected type": "unexpected type", "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.": "Back up your encryption keys with your account data in case you lose access to your sessions. Your keys will be secured with a unique Security Key.", @@ -1572,7 +1573,8 @@ "Invite to just this room": "Invite to just this room", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "This is the start of .": "This is the start of .", - "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.": "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites.", + "Enable encryption in settings.": "Enable encryption in settings.", "End-to-end encryption isn't enabled": "End-to-end encryption isn't enabled", "Unpin": "Unpin", "View message": "View message", diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index d5a5c416ba..ee8d9bceae 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -144,3 +144,12 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent): { return { tileHandler, isInfoMessage, isBubbleMessage, isLeftAlignedBubbleMessage }; } + +export function isVoiceMessage(mxEvent: MatrixEvent): boolean { + const content = mxEvent.getContent(); + // MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245 + return ( + !!content['org.matrix.msc2516.voice'] || + !!content['org.matrix.msc3245.voice'] + ); +}