diff --git a/package.json b/package.json index 6b369e9c27..bae7b1e4cf 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "12.0.0", - "matrix-widget-api": "^0.1.0-beta.14", + "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", @@ -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/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 14e4c01389..35d6087a1b 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -71,7 +71,7 @@ limitations under the License. &::before { background-color: #ffffff; mask-image: url('$(res)/img/e2e/normal.svg'); - mask-size: 90%; + mask-size: 80%; } &::after { 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/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 4faa4b594f..bcc40f1181 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -21,7 +21,7 @@ limitations under the License. mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } &.mx_cryptoEvent_icon::after { diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index a3473dedec..68ad44cf6a 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -45,7 +45,7 @@ limitations under the License. mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } // transparent-looking border surrounding the shield for when overlain over avatars @@ -59,7 +59,7 @@ limitations under the License. } // shrink the infill of the badge &::before { - mask-size: 65%; + mask-size: 60%; } } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3af266caee..27a83e58f8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -345,7 +345,7 @@ $hover-select-border: 4px; mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } } 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/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index 0c7bff0ce8..483b131bfe 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -23,7 +23,7 @@ limitations under the License. .mx_DialPad_button { width: 40px; height: 40px; - background-color: $theme-button-bg-color; + background-color: $dialpad-button-bg-color; border-radius: 40px; font-size: 18px; font-weight: 600; diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 520f51cf93..31327113cf 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -27,9 +27,22 @@ limitations under the License. } .mx_DialPadContextMenu_dialled { - height: 1em; + height: 1.5em; font-size: 18px; font-weight: 600; + max-width: 150px; + border: none; + margin: 0px; +} +.mx_DialPadContextMenu_dialled input { + font-size: 18px; + font-weight: 600; + overflow: hidden; + max-width: 150px; + text-align: left; + direction: rtl; + padding: 8px 0px; + background-color: rgb(0, 0, 0, 0); } .mx_DialPadContextMenu_dialPad { 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/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 2d0e3d2a8b..8b5fde3bd1 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -118,6 +118,9 @@ $voipcall-plinth-color: #394049; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #6F7882; +; + $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $bg-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index a852ad94e9..eb6dc40599 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -114,6 +114,8 @@ $voipcall-plinth-color: #394049; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #6F7882; +; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 84666bc662..a6b180bab4 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -181,6 +181,8 @@ $voipcall-plinth-color: #F4F6FA; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index c889f43d0b..d8dab9c9c4 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -173,6 +173,8 @@ $voipcall-plinth-color: #F4F6FA; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: #ffffff; 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/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 649c53664e..def0ac2cb8 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; } @@ -466,9 +486,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 { @@ -594,7 +638,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 = { @@ -635,7 +679,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/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}