diff --git a/package.json b/package.json index bc79b791d4..bae7b1e4cf 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "jest": { "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ - "/test/**/*-test.[jt]s" + "/test/**/*-test.[jt]s?(x)" ], "setupFiles": [ "jest-canvas-mock" diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index c7dd678c07..f254ca3226 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -111,6 +111,29 @@ $roomListCollapsedWidth: 68px; } } + .mx_LeftPanel_dialPadButton { + width: 32px; + height: 32px; + border-radius: 8px; + background-color: $roomlist-button-bg-color; + position: relative; + margin-left: 8px; + + &::before { + content: ''; + position: absolute; + top: 8px; + left: 8px; + width: 16px; + height: 16px; + mask-image: url('$(res)/img/element-icons/call/dialpad.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $secondary-fg-color; + } + } + .mx_LeftPanel_exploreButton { width: 32px; height: 32px; @@ -185,6 +208,12 @@ $roomListCollapsedWidth: 68px; flex-direction: column; justify-content: center; + .mx_LeftPanel_dialPadButton { + margin-left: 0; + margin-top: 8px; + background-color: transparent; + } + .mx_LeftPanel_exploreButton { margin-left: 0; margin-top: 8px; diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 8cc00aba0f..de9e049165 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -112,7 +112,7 @@ limitations under the License. .mx_AccessibleButton { padding: 5px 10px; - padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding + padding-left: 30px; // 18px for the icon, 2px margin to text, 10px regular padding display: inline-block; position: relative; @@ -128,13 +128,14 @@ limitations under the License. mask-repeat: no-repeat; mask-position: center; mask-size: contain; + width: 18px; + height: 18px; + top: 50%; // text sizes are dynamic + transform: translateY(-50%); } &.mx_RoomStatusBar_unsentCancelAllBtn::before { mask-image: url('$(res)/img/element-icons/trashcan.svg'); - width: 12px; - height: 16px; - top: calc(50% - 8px); // text sizes are dynamic } &.mx_RoomStatusBar_unsentResendAllBtn { @@ -142,9 +143,6 @@ limitations under the License. &::before { mask-image: url('$(res)/img/element-icons/retry.svg'); - width: 18px; - height: 18px; - top: calc(50% - 9px); // text sizes are dynamic } } } diff --git a/res/css/views/dialogs/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss index 1976a43ab9..95d7ce74c4 100644 --- a/res/css/views/dialogs/_ForwardDialog.scss +++ b/res/css/views/dialogs/_ForwardDialog.scss @@ -34,7 +34,7 @@ limitations under the License. > .mx_ForwardDialog_preview { max-height: 30%; flex-shrink: 0; - overflow: scroll; + overflow-y: auto; div { pointer-events: none; diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 2e48b5d8e9..c01b43c1c4 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -295,6 +295,7 @@ limitations under the License. .mx_InviteDialog_content { overflow: hidden; + height: 100%; } } @@ -316,3 +317,42 @@ limitations under the License. .mx_InviteDialog_helpText .mx_AccessibleButton_kind_link { padding: 0; } + +.mx_InviteDialog_multiInviterError { + > h4 { + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + font-weight: normal; + } + + > div { + .mx_InviteDialog_multiInviterError_entry { + margin-bottom: 24px; + + .mx_InviteDialog_multiInviterError_entry_userProfile { + .mx_InviteDialog_multiInviterError_entry_name { + margin-left: 6px; + font-size: $font-15px; + line-height: $font-24px; + font-weight: $font-semi-bold; + color: $primary-fg-color; + } + + .mx_InviteDialog_multiInviterError_entry_userId { + margin-left: 6px; + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + } + } + + .mx_InviteDialog_multiInviterError_entry_error { + margin-left: 32px; + font-size: $font-15px; + line-height: $font-24px; + color: $notice-primary-color; + } + } + } +} diff --git a/res/css/views/messages/_TextualEvent.scss b/res/css/views/messages/_TextualEvent.scss index be7565b3c5..e87fed90de 100644 --- a/res/css/views/messages/_TextualEvent.scss +++ b/res/css/views/messages/_TextualEvent.scss @@ -17,4 +17,9 @@ limitations under the License. .mx_TextualEvent { opacity: 0.5; overflow-y: hidden; + + a { + color: $accent-color; + cursor: pointer; + } } diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index ae8d22a462..5e61c3b8a3 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -29,6 +29,7 @@ $irc-line-height: $font-18px; // timestamps are links which shouldn't be underlined > a { text-decoration: none; + min-width: 45px; } display: flex; @@ -49,18 +50,6 @@ $irc-line-height: $font-18px; } } - > .mx_SenderProfile { - order: 2; - flex-shrink: 0; - width: var(--name-width); - text-overflow: ellipsis; - text-align: left; - display: flex; - align-items: center; - overflow: visible; - justify-content: flex-end; - } - .mx_EventTile_line, .mx_EventTile_reply { padding: 0; display: flex; @@ -173,27 +162,37 @@ $irc-line-height: $font-18px; border-left: 0; } - .mx_SenderProfile_hover { - background-color: $primary-bg-color; - overflow: hidden; + .mx_SenderProfile { + width: var(--name-width); display: flex; + order: 2; + flex-shrink: 0; + justify-content: flex-start; + align-items: center; > .mx_SenderProfile_displayName { + width: 100%; + text-align: end; overflow: hidden; text-overflow: ellipsis; - min-width: var(--name-width); - text-align: end; + } + + > .mx_SenderProfile_mxid { + visibility: collapse; } } .mx_SenderProfile:hover { - justify-content: flex-start; - } - - .mx_SenderProfile_hover:hover { overflow: visible; - width: max(auto, 100%); z-index: 10; + + > .mx_SenderProfile_displayName { + overflow: visible; + } + + > .mx_SenderProfile_mxid { + visibility: visible; + } } .mx_ReplyThread { @@ -201,16 +200,7 @@ $irc-line-height: $font-18px; .mx_SenderProfile { width: unset; max-width: var(--name-width); - } - - .mx_SenderProfile_hover { background: transparent; - - > span { - > .mx_SenderProfile_displayName { - min-width: inherit; - } - } } .mx_EventTile_emote { diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index a3ee104bd8..5501ab343e 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -36,10 +36,10 @@ limitations under the License. } .mx_VoiceRecordComposerTile_delete { - width: 14px; // w&h are size of icon - height: 18px; + width: 24px; + height: 24px; vertical-align: middle; - margin-right: 11px; // distance from left edge of waveform container (container has some margin too) + margin-right: 8px; // distance from left edge of waveform container (container has some margin too) background-color: $voice-record-icon-color; mask-repeat: no-repeat; mask-size: contain; diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss index 68e8723f11..c73e0715dd 100644 --- a/res/css/views/spaces/_SpaceBasicSettings.scss +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -73,7 +73,7 @@ limitations under the License. } } - .mx_AccessibleButton { + .mx_AccessibleButton_hasKind { padding: 8px 22px; margin-left: auto; display: block; diff --git a/res/img/element-icons/call/dialpad.svg b/res/img/element-icons/call/dialpad.svg new file mode 100644 index 0000000000..a97e80aa0b --- /dev/null +++ b/res/img/element-icons/call/dialpad.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/trashcan.svg b/res/img/element-icons/trashcan.svg index f8fb8b5c46..4106f0bd60 100644 --- a/res/img/element-icons/trashcan.svg +++ b/res/img/element-icons/trashcan.svg @@ -1,3 +1,3 @@ - - + + diff --git a/scripts/ci/Dockerfile b/scripts/ci/Dockerfile index 1d1425c865..6d33987d8c 100644 --- a/scripts/ci/Dockerfile +++ b/scripts/ci/Dockerfile @@ -5,4 +5,4 @@ FROM node:14-buster RUN apt-get update RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime # dependencies for chrome (installed by puppeteer) -RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget +RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm-dev libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget diff --git a/src/@types/diff-dom.ts b/src/@types/diff-dom.ts index 884ee6126d..38ff6432cf 100644 --- a/src/@types/diff-dom.ts +++ b/src/@types/diff-dom.ts @@ -15,20 +15,8 @@ limitations under the License. */ declare module "diff-dom" { - enum Action { - AddElement = "addElement", - AddTextElement = "addTextElement", - RemoveTextElement = "removeTextElement", - RemoveElement = "removeElement", - ReplaceElement = "replaceElement", - ModifyTextElement = "modifyTextElement", - AddAttribute = "addAttribute", - RemoveAttribute = "removeAttribute", - ModifyAttribute = "modifyAttribute", - } - export interface IDiff { - action: Action; + action: string; name: string; text?: string; route: number[]; diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 9042d47243..7a3cf5d50f 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -307,7 +307,7 @@ function readFileAsArrayBuffer(file: File | Blob): Promise { * If the file is unencrypted then the object will have a "url" key. * If the file is encrypted then the object will have a "file" key. */ -function uploadFile( +export function uploadFile( matrixClient: MatrixClient, roomId: string, file: File | Blob, diff --git a/src/Notifier.ts b/src/Notifier.ts index 4f55046e72..2afc7d9901 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -68,7 +68,7 @@ export const Notifier = { // or not pendingEncryptedEventIds: [], - notificationMessageForEvent: function(ev: MatrixEvent) { + notificationMessageForEvent: function(ev: MatrixEvent): string { if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) { return typehandlers[ev.getContent().msgtype](ev); } diff --git a/src/RoomInvite.js b/src/RoomInvite.tsx similarity index 54% rename from src/RoomInvite.js rename to src/RoomInvite.tsx index aa758ecbdc..c86f832b90 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,15 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import {MatrixClientPeg} from './MatrixClientPeg'; -import MultiInviter from './utils/MultiInviter'; +import React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { User } from "matrix-js-sdk/src/models/user"; + +import { MatrixClientPeg } from './MatrixClientPeg'; +import MultiInviter, { CompletionStates } from './utils/MultiInviter'; import Modal from './Modal'; import * as sdk from './'; import { _t } from './languageHandler'; -import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; +import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; -import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; +import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; +import BaseAvatar from "./components/views/avatars/BaseAvatar"; +import { mediaFromMxc } from "./customisations/Media"; + +export interface IInviteResult { + states: CompletionStates; + inviter: MultiInviter; +} /** * Invites multiple addresses to a room @@ -32,15 +41,15 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; * no option to cancel. * * @param {string} roomId The ID of the room to invite to - * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. + * @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -export function inviteMultipleToRoom(roomId, addrs) { +export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise { const inviter = new MultiInviter(roomId); - return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); + return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter })); } -export function showStartChatInviteDialog(initialText) { +export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog( @@ -49,7 +58,7 @@ export function showStartChatInviteDialog(initialText) { ); } -export function showRoomInviteDialog(roomId, initialText = "") { +export function showRoomInviteDialog(roomId: string, initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. Modal.createTrackedDialog( "Invite Users", "", InviteDialog, { @@ -61,14 +70,14 @@ export function showRoomInviteDialog(roomId, initialText = "") { ); } -export function showCommunityRoomInviteDialog(roomId, communityName) { +export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void { Modal.createTrackedDialog( 'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, ); } -export function showCommunityInviteDialog(communityId) { +export function showCommunityInviteDialog(communityId: string): void { const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); if (chat) { const name = CommunityPrototypeStore.instance.getCommunityName(communityId); @@ -83,7 +92,7 @@ export function showCommunityInviteDialog(communityId) { * @param {MatrixEvent} event The event to check * @returns {boolean} True if valid, false otherwise */ -export function isValid3pidInvite(event) { +export function isValid3pidInvite(event: MatrixEvent): boolean { if (!event || event.getType() !== "m.room.third_party_invite") return false; // any events without these keys are not valid 3pid invites, so we ignore them @@ -96,7 +105,7 @@ export function isValid3pidInvite(event) { return true; } -export function inviteUsersToRoom(roomId, userIds) { +export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise { return inviteMultipleToRoom(roomId, userIds).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); showAnyInviteErrors(result.states, room, result.inviter); @@ -110,9 +119,14 @@ export function inviteUsersToRoom(roomId, userIds) { }); } -export function showAnyInviteErrors(addrs, room, inviter) { +export function showAnyInviteErrors( + states: CompletionStates, + room: Room, + inviter: MultiInviter, + userMap?: Map, +): boolean { // Show user any errors - const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); + const failedUsers = Object.keys(states).filter(a => states[a] === 'error'); if (failedUsers.length === 1 && inviter.fatal) { // Just get the first message because there was a fatal problem on the first // user. This usually means that no other users were attempted, making it @@ -126,19 +140,47 @@ export function showAnyInviteErrors(addrs, room, inviter) { } else { const errorList = []; for (const addr of failedUsers) { - if (addrs[addr] === "error") { + if (states[addr] === "error") { const reason = inviter.getErrorText(addr); errorList.push(addr + ": " + reason); } } + const cli = MatrixClientPeg.get(); if (errorList.length > 0) { // React 16 doesn't let us use `errorList.join(
)` anymore, so this is our solution - const description =
{errorList.map(e =>
{e}
)}
; + const description =
+

{ _t("We sent the others, but the below people couldn't be invited to ", {}, { + RoomName: () => { room.name }, + }) }

+
+ { failedUsers.map(addr => { + const user = userMap?.get(addr) || cli.getUser(addr); + const name = (user as Member).name || (user as User).rawDisplayName; + const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl; + return
+
+ + { name } + { user.userId } +
+
+ { inviter.getErrorText(addr) } +
+
; + }) } +
+
; const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { - title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), + Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, { + title: _t("Some invites couldn't be sent"), description, }); return false; diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 09c8d30614..1ba0d6439b 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; +import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; import * as sdk from './index'; @@ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; import SettingsStore from "./settings/SettingsStore"; import SecurityCustomisations from "./customisations/Security"; +import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -244,7 +245,7 @@ async function onSecretRequested( deviceId: string, requestId: string, name: string, - deviceTrust: IDeviceTrustLevel, + deviceTrust: DeviceTrustLevel, ): Promise { console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); const client = MatrixClientPeg.get(); diff --git a/src/TextForEvent.ts b/src/TextForEvent.tsx similarity index 89% rename from src/TextForEvent.ts rename to src/TextForEvent.tsx index e162b09ed4..c5c9a6fb22 100644 --- a/src/TextForEvent.ts +++ b/src/TextForEvent.tsx @@ -13,6 +13,8 @@ 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 {MatrixClientPeg} from './MatrixClientPeg'; import { _t } from './languageHandler'; import * as Roles from './Roles'; @@ -20,6 +22,11 @@ import {isValid3pidInvite} from "./RoomInvite"; import SettingsStore from "./settings/SettingsStore"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore"; +import { RightPanelPhases } from './stores/RightPanelStorePhases'; +import { Action } from './dispatcher/actions'; +import defaultDispatcher from './dispatcher/dispatcher'; +import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; // These functions are frequently used just to check whether an event has // any text to display at all. For this reason they return deferred values @@ -31,76 +38,89 @@ function textForMemberEvent(ev): () => string | null { const targetName = ev.target ? ev.target.name : ev.getStateKey(); const prevContent = ev.getPrevContent(); const content = ev.getContent(); + const reason = content.reason; - const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : ''; switch (content.membership) { case 'invite': { const threePidContent = content.third_party_invite; if (threePidContent) { if (threePidContent.display_name) { - return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', { + return () => _t('%(targetName)s accepted the invitation for %(displayName)s', { targetName, displayName: threePidContent.display_name, }); } else { - return () => _t('%(targetName)s accepted an invitation.', {targetName}); + return () => _t('%(targetName)s accepted an invitation', { targetName }); } } else { - return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s invited %(targetName)s', { senderName, targetName }); } } case 'ban': - return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s banned %(targetName)s: %(reason)s', { senderName, targetName, reason }) + : _t('%(senderName)s banned %(targetName)s', { senderName, targetName }); case 'join': if (prevContent && prevContent.membership === 'join') { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { - return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', { + return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', { oldDisplayName: prevContent.displayname, displayName: content.displayname, }); } else if (!prevContent.displayname && content.displayname) { - return () => _t('%(senderName)s set their display name to %(displayName)s.', { + return () => _t('%(senderName)s set their display name to %(displayName)s', { senderName: ev.getSender(), displayName: content.displayname, }); } else if (prevContent.displayname && !content.displayname) { - return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', { + return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', { senderName, oldDisplayName: prevContent.displayname, }); } else if (prevContent.avatar_url && !content.avatar_url) { - return () => _t('%(senderName)s removed their profile picture.', {senderName}); + return () => _t('%(senderName)s removed their profile picture', { senderName }); } else if (prevContent.avatar_url && content.avatar_url && prevContent.avatar_url !== content.avatar_url) { - return () => _t('%(senderName)s changed their profile picture.', {senderName}); + return () => _t('%(senderName)s changed their profile picture', { senderName }); } else if (!prevContent.avatar_url && content.avatar_url) { - return () => _t('%(senderName)s set a profile picture.', {senderName}); + return () => _t('%(senderName)s set a profile picture', { senderName }); } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { - // This is a null rejoin, it will only be visible if the Labs option is enabled - return () => _t("%(senderName)s made no change.", {senderName}); + // This is a null rejoin, it will only be visible if using 'show hidden events' (labs) + return () => _t("%(senderName)s made no change", { senderName }); } else { return null; } } else { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); - return () => _t('%(targetName)s joined the room.', {targetName}); + return () => _t('%(targetName)s joined the room', { targetName }); } case 'leave': if (ev.getSender() === ev.getStateKey()) { if (prevContent.membership === "invite") { - return () => _t('%(targetName)s rejected the invitation.', {targetName}); + return () => _t('%(targetName)s rejected the invitation', { targetName }); } else { - return () => _t('%(targetName)s left the room.', {targetName}); + return () => reason + ? _t('%(targetName)s left the room: %(reason)s', { targetName, reason }) + : _t('%(targetName)s left the room', { targetName }); } } else if (prevContent.membership === "ban") { - return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s unbanned %(targetName)s', { senderName, targetName }); } else if (prevContent.membership === "invite") { - return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { - senderName, - targetName, - }) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', { + senderName, + targetName, + reason, + }) + : _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName }) } else if (prevContent.membership === "join") { - return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s kicked %(targetName)s: %(reason)s', { + senderName, + targetName, + reason, + }) + : _t('%(senderName)s kicked %(targetName)s', { senderName, targetName }); } else { return null; } @@ -474,9 +494,33 @@ function textForPowerEvent(event): () => string | null { }); } -function textForPinnedEvent(event): () => string | null { +const onPinnedMessagesClick = (): void => { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.PinnedMessages, + allowClose: false, + }); +} + +function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null { + if (!SettingsStore.getValue("feature_pinning")) return null; const senderName = event.sender ? event.sender.name : event.getSender(); - return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName}); + + if (allowJSX) { + return () => ( + + { + _t( + "%(senderName)s changed the pinned messages for the room.", + { senderName }, + { "a": (sub) => { sub } }, + ) + } + + ); + } + + return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName }); } function textForWidgetEvent(event): () => string | null { @@ -602,7 +646,7 @@ function textForMjolnirEvent(event): () => string | null { } interface IHandlers { - [type: string]: (ev: any) => (() => string | null); + [type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null); } const handlers: IHandlers = { @@ -643,7 +687,9 @@ export function hasText(ev): boolean { return Boolean(handler?.(ev)); } -export function textForEvent(ev): string { +export function textForEvent(ev: MatrixEvent): string; +export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element; +export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - return handler?.(ev)?.() || ''; + return handler?.(ev, allowJSX)?.() || ''; } diff --git a/src/Unread.ts b/src/Unread.ts index b733f4175a..72f0bb4642 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -16,7 +16,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; @@ -43,12 +43,6 @@ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { case EventType.RoomCanonicalAlias: case EventType.RoomServerAcl: return false; - - case EventType.RoomMessage: - if (ev.getContent().msgtype === MsgType.Notice) { - return false; - } - break; } if (ev.isRedacted()) return false; diff --git a/src/UserAddress.js b/src/UserAddress.ts similarity index 69% rename from src/UserAddress.js rename to src/UserAddress.ts index e7501a0d91..a2c546deb7 100644 --- a/src/UserAddress.js +++ b/src/UserAddress.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 New Vector Ltd +Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,15 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -const emailRegex = /^\S+@\S+\.\S+$/; +import PropTypes from "prop-types"; +const emailRegex = /^\S+@\S+\.\S+$/; const mxUserIdRegex = /^@\S+:\S+$/; const mxRoomIdRegex = /^!\S+:\S+$/; -import PropTypes from 'prop-types'; -export const addressTypes = [ - 'mx-user-id', 'mx-room-id', 'email', -]; +export const addressTypes = ['mx-user-id', 'mx-room-id', 'email']; + +export enum AddressType { + Email = "email", + MatrixUserId = "mx-user-id", + MatrixRoomId = "mx-room-id", +} // PropType definition for an object describing // an address that can be invited to a room (which @@ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({ isKnown: PropTypes.bool, }); -export function getAddressType(inputText) { - const isEmailAddress = emailRegex.test(inputText); - const isUserId = mxUserIdRegex.test(inputText); - const isRoomId = mxRoomIdRegex.test(inputText); - - // sanity check the input for user IDs - if (isEmailAddress) { - return 'email'; - } else if (isUserId) { - return 'mx-user-id'; - } else if (isRoomId) { - return 'mx-room-id'; +export function getAddressType(inputText: string): AddressType | null { + if (emailRegex.test(inputText)) { + return AddressType.Email; + } else if (mxUserIdRegex.test(inputText)) { + return AddressType.MatrixUserId; + } else if (mxRoomIdRegex.test(inputText)) { + return AddressType.MatrixRoomId; } else { return null; } diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index af22db1350..dec4e4c13e 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -24,6 +24,7 @@ import CustomRoomTagPanel from "./CustomRoomTagPanel"; import dis from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import RoomList from "../views/rooms/RoomList"; +import CallHandler from "../../CallHandler"; import { HEADER_HEIGHT } from "../views/rooms/RoomSublist"; import { Action } from "../../dispatcher/actions"; import UserMenu from "./UserMenu"; @@ -124,6 +125,10 @@ export default class LeftPanel extends React.Component { this.setState({ activeSpace }); }; + private onDialPad = () => { + dis.fire(Action.OpenDialPad); + } + private onExplore = () => { dis.fire(Action.ViewRoomDirectory); }; @@ -397,7 +402,20 @@ export default class LeftPanel extends React.Component { } } - private renderSearchExplore(): React.ReactNode { + private renderSearchDialExplore(): React.ReactNode { + let dialPadButton = null; + + // If we have dialer support, show a button to bring up the dial pad + // to start a new call + if (CallHandler.sharedInstance().getSupportsPstnProtocol()) { + dialPadButton = + ; + } + return (
{ onKeyDown={this.onKeyDown} onSelectRoom={this.selectRoom} /> + + {dialPadButton} + { {leftLeftPanel}