diff --git a/CHANGELOG.md b/CHANGELOG.md index 43a1494497..c87f1c62e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,87 @@ +Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0) + + * Upgrade to JS SDK 9.7.0 + * [Release] Use config for host signup branding + [\#5651](https://github.com/matrix-org/matrix-react-sdk/pull/5651) + +Changes in [3.14.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0-rc.1) (2021-02-10) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.1...v3.14.0-rc.1) + + * Upgrade to JS SDK 9.7.0-rc.1 + * Translations update from Weblate + [\#5636](https://github.com/matrix-org/matrix-react-sdk/pull/5636) + * Add host signup modal with iframe + [\#5450](https://github.com/matrix-org/matrix-react-sdk/pull/5450) + * Fix duplication of codeblock elements + [\#5633](https://github.com/matrix-org/matrix-react-sdk/pull/5633) + * Handle undefined call stats + [\#5632](https://github.com/matrix-org/matrix-react-sdk/pull/5632) + * Avoid delayed displaying of sources in source picker + [\#5631](https://github.com/matrix-org/matrix-react-sdk/pull/5631) + * Give breadcrumbs toolbar an accessibility label. + [\#5628](https://github.com/matrix-org/matrix-react-sdk/pull/5628) + * Fix the %s in logs + [\#5627](https://github.com/matrix-org/matrix-react-sdk/pull/5627) + * Fix jumpy notifications settings UI + [\#5625](https://github.com/matrix-org/matrix-react-sdk/pull/5625) + * Improve displaying of code blocks + [\#5559](https://github.com/matrix-org/matrix-react-sdk/pull/5559) + * Fix desktop Matrix screen sharing and add a screen/window picker + [\#5525](https://github.com/matrix-org/matrix-react-sdk/pull/5525) + * Call "MatrixClientPeg.get()" only once in method "findOverrideMuteRule" + [\#5498](https://github.com/matrix-org/matrix-react-sdk/pull/5498) + * Close current modal when session is logged out + [\#5616](https://github.com/matrix-org/matrix-react-sdk/pull/5616) + * Switch room explorer list to CSS grid + [\#5551](https://github.com/matrix-org/matrix-react-sdk/pull/5551) + * Improve SSO login start screen and 3pid invite handling somewhat + [\#5622](https://github.com/matrix-org/matrix-react-sdk/pull/5622) + * Don't jump to bottom on reaction + [\#5621](https://github.com/matrix-org/matrix-react-sdk/pull/5621) + * Fix several profile settings oddities + [\#5620](https://github.com/matrix-org/matrix-react-sdk/pull/5620) + * Add option to hide the stickers button in the composer + [\#5530](https://github.com/matrix-org/matrix-react-sdk/pull/5530) + * Fix confusing right panel button behaviour + [\#5598](https://github.com/matrix-org/matrix-react-sdk/pull/5598) + * Fix jumping timestamp if hovering a message with e2e indicator bar + [\#5601](https://github.com/matrix-org/matrix-react-sdk/pull/5601) + * Fix avatar and trash alignment + [\#5614](https://github.com/matrix-org/matrix-react-sdk/pull/5614) + * Fix z-index of stickerpicker + [\#5617](https://github.com/matrix-org/matrix-react-sdk/pull/5617) + * Fix permalink via parsing for rooms + [\#5615](https://github.com/matrix-org/matrix-react-sdk/pull/5615) + * Fix "Terms and Conditions" checkbox alignment + [\#5613](https://github.com/matrix-org/matrix-react-sdk/pull/5613) + * Fix flair height after accent changes + [\#5611](https://github.com/matrix-org/matrix-react-sdk/pull/5611) + * Iterate Social Logins work around edge cases and branding + [\#5609](https://github.com/matrix-org/matrix-react-sdk/pull/5609) + * Lock widget room ID when added + [\#5607](https://github.com/matrix-org/matrix-react-sdk/pull/5607) + * Better errors for SSO failures + [\#5605](https://github.com/matrix-org/matrix-react-sdk/pull/5605) + * Increase language search bar width + [\#5549](https://github.com/matrix-org/matrix-react-sdk/pull/5549) + * Scroll to bottom on message_sent + [\#5565](https://github.com/matrix-org/matrix-react-sdk/pull/5565) + * Fix new rooms being titled 'Empty Room' + [\#5587](https://github.com/matrix-org/matrix-react-sdk/pull/5587) + * Fix saving the collapsed state of the left panel + [\#5593](https://github.com/matrix-org/matrix-react-sdk/pull/5593) + * Fix app-url hint in the e2e-test run script output + [\#5600](https://github.com/matrix-org/matrix-react-sdk/pull/5600) + * Fix RoomView re-mounting breaking peeking + [\#5602](https://github.com/matrix-org/matrix-react-sdk/pull/5602) + * Tweak a few room ID checks + [\#5592](https://github.com/matrix-org/matrix-react-sdk/pull/5592) + * Remove pills from event permalinks with text + [\#5575](https://github.com/matrix-org/matrix-react-sdk/pull/5575) + Changes in [3.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.1) (2021-02-04) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0...v3.13.1) diff --git a/package.json b/package.json index 2263c7de32..d4f931d811 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.13.1", + "version": "3.14.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -86,7 +86,6 @@ "pako": "^2.0.3", "parse5": "^6.0.1", "png-chunks-extract": "^1.0.0", - "project-name-generator": "^2.1.9", "prop-types": "^15.7.2", "qrcode": "^1.4.4", "qs": "^6.9.6", diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss index f2577938e5..2a4453df70 100644 --- a/res/css/structures/_UserMenu.scss +++ b/res/css/structures/_UserMenu.scss @@ -128,7 +128,7 @@ limitations under the License. } .mx_UserMenu_contextMenu { - width: 247px; + width: 258px; // These override the styles already present on the user menu rather than try to // define a new menu. They are specifically for the stacked menu when a community diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 35cb6bc7ab..8fee740016 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -223,3 +223,54 @@ limitations under the License. content: ":"; } } + +.mx_DevTools_SettingsExplorer { + table { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + + th { + // Colour choice: first one autocomplete gave me. + border-bottom: 1px solid $accent-color; + text-align: left; + } + + td, th { + width: 360px; // "feels right" number + + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + td + td, th + th { + width: auto; + } + + tr:hover { + // Colour choice: first one autocomplete gave me. + background-color: $accent-color-50pct; + } + } + + .mx_DevTools_SettingsExplorer_mutable { + background-color: $accent-color; + } + + .mx_DevTools_SettingsExplorer_immutable { + background-color: $warning-color; + } + + .mx_DevTools_SettingsExplorer_edit { + float: right; + margin-right: 16px; + } + + .mx_DevTools_SettingsExplorer_warning { + border: 2px solid $warning-color; + border-radius: 4px; + padding: 4px; + margin-bottom: 8px; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 42df3211de..5841cf2853 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -257,17 +257,13 @@ $left-gutter: 64px; display: inline-block; width: 14px; height: 14px; - top: 29px; + // This aligns the avatar with the last line of the + // message. We want to move it one line up - 2.2rem + top: -2.2rem; user-select: none; z-index: 1; } -.mx_EventTile_continuation .mx_EventTile_readAvatars, -.mx_EventTile_info .mx_EventTile_readAvatars, -.mx_EventTile_emote .mx_EventTile_readAvatars { - top: 7px; -} - .mx_EventTile_readAvatars .mx_BaseAvatar { position: absolute; display: inline-block; @@ -531,14 +527,14 @@ $left-gutter: 64px; display: inline-block; visibility: hidden; cursor: pointer; - top: 6px; - right: 12px; + top: 8px; + right: 8px; width: 19px; height: 19px; background-color: $message-action-bar-fg-color; } .mx_EventTile_buttonBottom { - top: 31px; + top: 33px; } .mx_EventTile_copyButton { mask-image: url($copy-button-url); diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 71c0db947e..dea1b58741 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -227,18 +227,6 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); } -.mx_MessageComposer_hangup::before { - mask-image: url('$(res)/img/element-icons/call/hangup.svg'); -} - -.mx_MessageComposer_voicecall::before { - mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); -} - -.mx_MessageComposer_videocall::before { - mask-image: url('$(res)/img/element-icons/call/video-call.svg'); -} - .mx_MessageComposer_emoji::before { mask-image: url('$(res)/img/element-icons/room/composer/emoji.svg'); } @@ -247,6 +235,32 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/composer/sticker.svg'); } +.mx_MessageComposer_sendMessage { + cursor: pointer; + position: relative; + margin-right: 6px; + width: 32px; + height: 32px; + border-radius: 100%; + background-color: $button-bg-color; + + &::before { + position: absolute; + height: 16px; + width: 16px; + top: 8px; + left: 9px; + + mask-image: url('$(res)/img/element-icons/send-message.svg'); + mask-repeat: no-repeat; + mask-size: contain; + mask-position: center; + + background-color: $button-fg-color; + content: ''; + } +} + .mx_MessageComposer_formatting { cursor: pointer; margin: 0 11px; diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index a23a44906f..387d1588a3 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -252,6 +252,19 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/search-inset.svg'); } +.mx_RoomHeader_voiceCallButton::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + + // The call button SVG is padded slightly differently, so match it up to the size + // of the other icons + mask-size: 20px; + mask-position: center; +} + +.mx_RoomHeader_videoCallButton::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); +} + .mx_RoomHeader_showPanel { height: 16px; } diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index 27c7c7d0f7..92a475694e 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -197,6 +197,9 @@ limitations under the License. .mx_RoomSublist_resizerHandles { flex: 0 0 4px; + display: flex; + justify-content: center; + width: 100%; } // Class name comes from the ResizableBox component @@ -207,17 +210,12 @@ limitations under the License. border-radius: 3px; // Override styles from library - width: unset !important; + max-width: 64px; height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes // This is positioned directly below the 'show more' button. - position: absolute; + position: relative !important; bottom: 0 !important; // override from library - - // Together, these make the bar 64px wide - // These are also overridden from the library - left: calc(50% - 32px) !important; - right: calc(50% - 32px) !important; } &:hover, &.mx_RoomSublist_hasMenuOpen { diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 931410dba3..3e473a80b2 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -16,6 +16,7 @@ limitations under the License. .mx_VideoFeed_remote { width: 100%; + max-height: 100%; background-color: #000; z-index: 50; } diff --git a/res/img/element-icons/send-message.svg b/res/img/element-icons/send-message.svg new file mode 100644 index 0000000000..ce35bf8bc8 --- /dev/null +++ b/res/img/element-icons/send-message.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 612b8e03bd..a878aa3cdd 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -259,6 +259,11 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); .mx_EventTile_content .markdown-body pre:hover { border-color: #808080 !important; // inverted due to rules below scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // copied from light theme due to inversion below + // the code above works only in Firefox, this is for other browsers + // see https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); // copied from light theme due to inversion below + } } .mx_EventTile_content .markdown-body { pre, code { diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 2a28c8e43f..28f22780a2 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -37,6 +37,7 @@ import CountlyAnalytics from "../CountlyAnalytics"; import UserActivity from "../UserActivity"; import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; +import VoipUserMapper from "../VoipUserMapper"; declare global { interface Window { @@ -66,6 +67,7 @@ declare global { mxCountlyAnalytics: typeof CountlyAnalytics; mxUserActivity: UserActivity; mxModalWidgetStore: ModalWidgetStore; + mxVoipUserMapper: VoipUserMapper; } interface Document { diff --git a/src/Avatar.ts b/src/Avatar.ts index 60bdfdcf75..e2557e21a8 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -165,6 +165,9 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi return explicitRoomAvatar; } + // space rooms cannot be DMs so skip the rest + if (room.isSpaceRoom()) return null; + let otherMember = null; const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); if (otherUserId) { diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 41a5941092..42a38c7a54 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -64,7 +64,6 @@ import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; import SettingsStore from './settings/SettingsStore'; -import {generateHumanReadableId} from "./utils/NamingUtils"; import {Jitsi} from "./widgets/Jitsi"; import {WidgetType} from "./widgets/WidgetType"; import {SettingLevel} from "./settings/SettingLevel"; @@ -84,10 +83,19 @@ import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker" import { Action } from './dispatcher/actions'; -import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper'; +import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; +import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; -const CHECK_PSTN_SUPPORT_ATTEMPTS = 3; +export const PROTOCOL_PSTN = 'm.protocol.pstn'; +export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; +export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native'; +export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual'; + +const CHECK_PROTOCOLS_ATTEMPTS = 3; +// Event type for room account data and room creation content used to mark rooms as virtual rooms +// (and store the ID of their native room) +export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; enum AudioID { Ring = 'ringAudio', @@ -96,6 +104,29 @@ enum AudioID { Busy = 'busyAudio', } +interface ThirdpartyLookupResponseFields { + /* eslint-disable camelcase */ + + // im.vector.sip_native + virtual_mxid?: string; + is_virtual?: boolean; + + // im.vector.sip_virtual + native_mxid?: string; + is_native?: boolean; + + // common + lookup_success?: boolean; + + /* eslint-enable camelcase */ +} + +interface ThirdpartyLookupResponse { + userid: string, + protocol: string, + fields: ThirdpartyLookupResponseFields, +} + // Unlike 'CallType' in js-sdk, this one includes screen sharing // (because a screen sharing call is only a screen sharing call to the caller, // to the callee it's just a video call, at least as far as the current impl @@ -126,7 +157,12 @@ export default class CallHandler { private audioPromises = new Map>(); private dispatcherRef: string = null; private supportsPstnProtocol = null; + private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol + private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser + // For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't. + private invitedRoomsAreVirtual = new Map(); + private invitedRoomCheckInProgress = false; static sharedInstance() { if (!window.mxCallHandler) { @@ -140,9 +176,9 @@ export default class CallHandler { * Gets the user-facing room associated with a call (call.roomId may be the call "virtual room" * if a voip_mxid_translate_pattern is set in the config) */ - public static roomIdForCall(call: MatrixCall) { + public static roomIdForCall(call: MatrixCall): string { if (!call) return null; - return roomForVirtualRoom(call.roomId) || call.roomId; + return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId; } start() { @@ -163,7 +199,7 @@ export default class CallHandler { MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming); } - this.checkForPstnSupport(CHECK_PSTN_SUPPORT_ATTEMPTS); + this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS); } stop() { @@ -177,33 +213,73 @@ export default class CallHandler { } } - private async checkForPstnSupport(maxTries) { + private async checkProtocols(maxTries) { try { const protocols = await MatrixClientPeg.get().getThirdpartyProtocols(); - if (protocols['im.vector.protocol.pstn'] !== undefined) { - this.supportsPstnProtocol = protocols['im.vector.protocol.pstn']; - } else if (protocols['m.protocol.pstn'] !== undefined) { - this.supportsPstnProtocol = protocols['m.protocol.pstn']; + + if (protocols[PROTOCOL_PSTN] !== undefined) { + this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN]); + if (this.supportsPstnProtocol) this.pstnSupportPrefixed = false; + } else if (protocols[PROTOCOL_PSTN_PREFIXED] !== undefined) { + this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN_PREFIXED]); + if (this.supportsPstnProtocol) this.pstnSupportPrefixed = true; } else { this.supportsPstnProtocol = null; } + dis.dispatch({action: Action.PstnSupportUpdated}); + + if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) { + this.supportsSipNativeVirtual = Boolean( + protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL], + ); + } + + dis.dispatch({action: Action.VirtualRoomSupportUpdated}); } catch (e) { if (maxTries === 1) { - console.log("Failed to check for pstn protocol support and no retries remain: assuming no support", e); + console.log("Failed to check for protocol support and no retries remain: assuming no support", e); } else { - console.log("Failed to check for pstn protocol support: will retry", e); + console.log("Failed to check for protocol support: will retry", e); this.pstnSupportCheckTimer = setTimeout(() => { - this.checkForPstnSupport(maxTries - 1); + this.checkProtocols(maxTries - 1); }, 10000); } } } - getSupportsPstnProtocol() { + public getSupportsPstnProtocol() { return this.supportsPstnProtocol; } + public getSupportsVirtualRooms() { + return this.supportsPstnProtocol; + } + + public pstnLookup(phoneNumber: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, { + 'm.id.phone': phoneNumber, + }, + ); + } + + public sipVirtualLookup(nativeMxid: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + PROTOCOL_SIP_VIRTUAL, { + 'native_mxid': nativeMxid, + }, + ); + } + + public sipNativeLookup(virtualMxid: string): Promise { + return MatrixClientPeg.get().getThirdpartyUser( + PROTOCOL_SIP_NATIVE, { + 'virtual_mxid': virtualMxid, + }, + ); + } + private onCallIncoming = (call) => { // we dispatch this synchronously to make sure that the event // handlers on the call are set up immediately (so that if @@ -550,9 +626,11 @@ export default class CallHandler { Analytics.trackEvent('voip', 'placeCall', 'type', type); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); - const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId; + const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId; logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); + const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now(); + console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds"); const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); this.calls.set(roomId, call); @@ -681,6 +759,12 @@ export default class CallHandler { Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); this.calls.set(mappedRoomId, call) this.setCallListeners(call); + + // get ready to send encrypted events in the room, so if the user does answer + // the call, we'll be ready to send. NB. This is the protocol-level room ID not + // the mapped one: that's where we'll send the events. + const cli = MatrixClientPeg.get(); + cli.prepareToEncrypt(cli.getRoom(call.roomId)); } break; case 'hangup': @@ -779,8 +863,9 @@ export default class CallHandler { // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification confId = base32.stringify(Buffer.from(roomId), { pad: false }); } else { - // Create a random human readable conference ID - confId = `JitsiConference${generateHumanReadableId()}`; + // Create a random conference ID + const random = randomUppercaseString(1) + randomLowercaseString(23); + confId = 'Jitsi' + random; } let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth}); @@ -796,6 +881,7 @@ export default class CallHandler { isAudioOnly: type === 'voice', domain: jitsiDomain, auth: jitsiAuth, + roomName: room.name, }; const widgetId = ( diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index bb4be663b6..98ca446532 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -279,6 +279,10 @@ class _MatrixClientPeg implements IMatrixClientPeg { timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), + // Gather up to 20 ICE candidates when a call arrives: this should be more than we'd + // ever normally need, so effectively this should make all the gathering happen when + // the call arrives. + iceCandidatePoolSize: 20, verificationMethods: [ verificationMethods.SAS, SHOW_QR_CODE_METHOD, diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js deleted file mode 100644 index 24dfe61d68..0000000000 --- a/src/ObjectUtils.js +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright 2016 OpenMarket 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. -*/ - -/** - * For two objects of the form { key: [val1, val2, val3] }, work out the added/removed - * values. Entirely new keys will result in the entire value array being added. - * @param {Object} before - * @param {Object} after - * @return {Object[]} An array of objects with the form: - * { key: $KEY, val: $VALUE, place: "add|del" } - */ -export function getKeyValueArrayDiffs(before, after) { - const results = []; - const delta = {}; - Object.keys(before).forEach(function(beforeKey) { - delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially - delta[beforeKey]--; // keys present in the past have -ve values - }); - Object.keys(after).forEach(function(afterKey) { - delta[afterKey] = delta[afterKey] || 0; // init to 0 initially - delta[afterKey]++; // keys present in the future have +ve values - }); - - Object.keys(delta).forEach(function(muxedKey) { - switch (delta[muxedKey]) { - case 1: // A new key in after - after[muxedKey].forEach(function(afterVal) { - results.push({ place: "add", key: muxedKey, val: afterVal }); - }); - break; - case -1: // A before key was removed - before[muxedKey].forEach(function(beforeVal) { - results.push({ place: "del", key: muxedKey, val: beforeVal }); - }); - break; - case 0: {// A mix of added/removed keys - // compare old & new vals - const itemDelta = {}; - before[muxedKey].forEach(function(beforeVal) { - itemDelta[beforeVal] = itemDelta[beforeVal] || 0; - itemDelta[beforeVal]--; - }); - after[muxedKey].forEach(function(afterVal) { - itemDelta[afterVal] = itemDelta[afterVal] || 0; - itemDelta[afterVal]++; - }); - - Object.keys(itemDelta).forEach(function(item) { - if (itemDelta[item] === 1) { - results.push({ place: "add", key: muxedKey, val: item }); - } else if (itemDelta[item] === -1) { - results.push({ place: "del", key: muxedKey, val: item }); - } else { - // itemDelta of 0 means it was unchanged between before/after - } - }); - break; - } - default: - console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!"); - break; - } - }); - - return results; -} - -/** - * Shallow-compare two objects for equality: each key and value must be identical - * @param {Object} objA First object to compare against the second - * @param {Object} objB Second object to compare against the first - * @return {boolean} whether the two objects have same key=values - */ -export function shallowEqual(objA, objB) { - if (objA === objB) { - return true; - } - - if (typeof objA !== 'object' || objA === null || - typeof objB !== 'object' || objB === null) { - return false; - } - - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - - if (keysA.length !== keysB.length) { - return false; - } - - for (let i = 0; i < keysA.length; i++) { - const key = keysA[i]; - if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) { - return false; - } - } - - return true; -} diff --git a/src/PhasedRollOut.js b/src/PhasedRollOut.js deleted file mode 100644 index b17ed37974..0000000000 --- a/src/PhasedRollOut.js +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2018 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 SdkConfig from './SdkConfig'; -import {hashCode} from './utils/FormattingUtils'; - -export function phasedRollOutExpiredForUser(username, feature, now, rollOutConfig = SdkConfig.get().phasedRollOut) { - if (!rollOutConfig) { - console.log(`no phased rollout configuration, so enabling ${feature}`); - return true; - } - const featureConfig = rollOutConfig[feature]; - if (!featureConfig) { - console.log(`${feature} doesn't have phased rollout configured, so enabling`); - return true; - } - if (!Number.isFinite(featureConfig.offset) || !Number.isFinite(featureConfig.period)) { - console.error(`phased rollout of ${feature} is misconfigured, ` + - `offset and/or period are not numbers, so disabling`, featureConfig); - return false; - } - - const hash = hashCode(username); - //ms -> min, enable users at minute granularity - const bucketRatio = 1000 * 60; - const bucketCount = featureConfig.period / bucketRatio; - const userBucket = hash % bucketCount; - const userMs = userBucket * bucketRatio; - const enableAt = featureConfig.offset + userMs; - const result = now >= enableAt; - const bucketStr = `(bucket ${userBucket}/${bucketCount})`; - if (result) { - console.log(`${feature} enabled for ${username} ${bucketStr}`); - } else { - console.log(`${feature} will be enabled for ${username} in ${Math.ceil((enableAt - now)/1000)}s ${bucketStr}`); - } - return result; -} diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 220320470a..03cbe88c22 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -98,11 +98,27 @@ async function getSecretStorageKey( { keys: keyInfos }: { keys: Record }, ssssItemName, ): Promise<[string, Uint8Array]> { - const keyInfoEntries = Object.entries(keyInfos); - if (keyInfoEntries.length > 1) { - throw new Error("Multiple storage key requests not implemented"); + const cli = MatrixClientPeg.get(); + let keyId = await cli.getDefaultSecretStorageKeyId(); + let keyInfo; + if (keyId) { + // use the default SSSS key if set + keyInfo = keyInfos[keyId]; + if (!keyInfo) { + // if the default key is not available, pretend the default key + // isn't set + keyId = undefined; + } + } + if (!keyId) { + // if no default SSSS key is set, fall back to a heuristic of using the + // only available key, if only one key is set + const keyInfoEntries = Object.entries(keyInfos); + if (keyInfoEntries.length > 1) { + throw new Error("Multiple storage key requests not implemented"); + } + [keyId, keyInfo] = keyInfoEntries[0]; } - const [keyId, keyInfo] = keyInfoEntries[0]; // Check the in-memory cache if (isCachingAllowed() && secretStorageKeys[keyId]) { diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index a39ad33b04..6b5f261374 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1040,9 +1040,7 @@ export const Commands = [ return success((async () => { if (isPhoneNumber) { - const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', { - 'm.id.phone': userId, - }); + const results = await CallHandler.sharedInstance().pstnLookup(this.state.value); if (!results || results.length === 0 || !results[0].userid) { throw new Error("Unable to find Matrix ID for phone number"); } @@ -1182,7 +1180,7 @@ export function parseCommandString(input: string) { input = input.replace(/\s+$/, ''); if (input[0] !== '/') return {}; // not a command - const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/); + const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/); let cmd; let args; if (bits) { diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index a4f5822065..d919615349 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -14,66 +14,97 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ensureDMExists, findDMForUser } from './createRoom'; +import { ensureVirtualRoomExists, findDMForUser } from './createRoom'; import { MatrixClientPeg } from "./MatrixClientPeg"; import DMRoomMap from "./utils/DMRoomMap"; -import SdkConfig from "./SdkConfig"; +import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler'; +import { Room } from 'matrix-js-sdk/src/models/room'; -// Functions for mapping users & rooms for the voip_mxid_translate_pattern -// config option +// Functions for mapping virtual users & rooms. Currently the only lookup +// is sip virtual: there could be others in the future. -export function voipUserMapperEnabled(): boolean { - return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined; -} +export default class VoipUserMapper { + private virtualRoomIdCache = new Set(); -// only exported for tests -export function userToVirtualUser(userId: string, templateString?: string): string { - if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern']; - if (!templateString) return null; - return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase()); -} + public static sharedInstance(): VoipUserMapper { + if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper(); + return window.mxVoipUserMapper; + } -// only exported for tests -export function virtualUserToUser(userId: string, templateString?: string): string { - if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern']; - if (!templateString) return null; + private async userToVirtualUser(userId: string): Promise { + const results = await CallHandler.sharedInstance().sipVirtualLookup(userId); + if (results.length === 0) return null; + return results[0].userid; + } - const regexString = templateString.replace('${mxid}', '(.+)'); + public async getOrCreateVirtualRoomForRoom(roomId: string):Promise { + const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); + if (!userId) return null; - const match = userId.match('^' + regexString + '$'); - if (!match) return null; + const virtualUser = await this.userToVirtualUser(userId); + if (!virtualUser) return null; - return decodeURIComponent(match[1].replace(/=/g, '%')); -} + const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId); + MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { + native_room: roomId, + }); -async function getOrCreateVirtualRoomForUser(userId: string):Promise { - const virtualUser = userToVirtualUser(userId); - if (!virtualUser) return null; + return virtualRoomId; + } - return await ensureDMExists(MatrixClientPeg.get(), virtualUser); -} + public nativeRoomForVirtualRoom(roomId: string):string { + const virtualRoom = MatrixClientPeg.get().getRoom(roomId); + if (!virtualRoom) return null; + const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); + if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; + return virtualRoomEvent.getContent()['native_room'] || null; + } -export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise { - const user = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!user) return null; - return getOrCreateVirtualRoomForUser(user); -} + public isVirtualRoom(room: Room):boolean { + if (this.nativeRoomForVirtualRoom(room.roomId)) return true; -export function roomForVirtualRoom(roomId: string):string { - const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!virtualUser) return null; - const realUser = virtualUserToUser(virtualUser); - const room = findDMForUser(MatrixClientPeg.get(), realUser); - if (room) { - return room.roomId; - } else { - return null; + if (this.virtualRoomIdCache.has(room.roomId)) return true; + + // also look in the create event for the claimed native room ID, which is the only + // way we can recognise a virtual room we've created when it first arrives down + // our stream. We don't trust this in general though, as it could be faked by an + // inviter: our main source of truth is the DM state. + const roomCreateEvent = room.currentState.getStateEvents("m.room.create", ""); + if (!roomCreateEvent || !roomCreateEvent.getContent()) return false; + // we only look at this for rooms we created (so inviters can't just cause rooms + // to be invisible) + if (roomCreateEvent.getSender() !== MatrixClientPeg.get().getUserId()) return false; + const claimedNativeRoomId = roomCreateEvent.getContent()[VIRTUAL_ROOM_EVENT_TYPE]; + return Boolean(claimedNativeRoomId); + } + + public async onNewInvitedRoom(invitedRoom: Room) { + const inviterId = invitedRoom.getDMInviter(); + console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); + const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); + if (result.length === 0) { + return true; + } + + if (result[0].fields.is_virtual) { + const nativeUser = result[0].userid; + const nativeRoom = findDMForUser(MatrixClientPeg.get(), nativeUser); + if (nativeRoom) { + // It's a virtual room with a matching native room, so set the room account data. This + // will make sure we know where how to map calls and also allow us know not to display + // it in the future. + MatrixClientPeg.get().setRoomAccountData(invitedRoom.roomId, VIRTUAL_ROOM_EVENT_TYPE, { + native_room: nativeRoom.roomId, + }); + // also auto-join the virtual room if we have a matching native room + // (possibly we should only join if we've also joined the native room, then we'd also have + // to make sure we joined virtual rooms on joining a native one) + MatrixClientPeg.get().joinRoom(invitedRoom.roomId); + } + + // also put this room in the virtual room ID cache so isVirtualRoom return the right answer + // in however long it takes for the echo of setAccountData to come down the sync + this.virtualRoomIdCache.add(invitedRoom.roomId); + } } } - -export function isVirtualRoom(roomId: string):boolean { - const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId); - if (!virtualUser) return null; - const realUser = virtualUserToUser(virtualUser); - return Boolean(realUser); -} diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 32eea55b0b..7fc01daef9 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -155,6 +155,7 @@ export default class UserProvider extends AutocompleteProvider { const currentUserId = MatrixClientPeg.get().credentials.userId; this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId); + this.users = this.users.concat(this.room.getMembersWithMembership("invite")); this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index aab7701f26..b5e5966d91 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -299,7 +299,7 @@ export class ContextMenu extends React.PureComponent { // such that it does not leave the (padded) window. if (contextMenuRect) { const padding = 10; - adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height + padding); + adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding); } position.top = adjusted; @@ -390,7 +390,7 @@ export class ContextMenu extends React.PureComponent { } // Placement method for to position context menu to right of elementRect with chevronOffset -export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => { +export const toRightOf = (elementRect: Pick, chevronOffset = 12) => { const left = elementRect.right + window.pageXOffset + 3; let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset; top -= chevronOffset + 8; // where 8 is half the height of the chevron diff --git a/src/components/structures/HostSignupAction.tsx b/src/components/structures/HostSignupAction.tsx index 39185acd05..9cf84a9379 100644 --- a/src/components/structures/HostSignupAction.tsx +++ b/src/components/structures/HostSignupAction.tsx @@ -21,6 +21,7 @@ import { } from "../views/context_menus/IconizedContextMenu"; import { _t } from "../../languageHandler"; import { HostSignupStore } from "../../stores/HostSignupStore"; +import SdkConfig from "../../SdkConfig"; interface IProps {} @@ -32,11 +33,21 @@ export default class HostSignupAction extends React.PureComponent diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index c76cd7cee7..c01214f3f4 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -107,7 +107,9 @@ interface IState { errcode: string; }; }; + usageLimitDismissed: boolean; usageLimitEventContent?: IUsageLimit; + usageLimitEventTs?: number; useCompactLayout: boolean; } @@ -151,6 +153,7 @@ class LoggedInView extends React.Component { syncErrorData: undefined, // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), + usageLimitDismissed: false, }; // stash the MatrixClient in case we log out before we are unmounted @@ -302,14 +305,27 @@ class LoggedInView extends React.Component { } }; + private onUsageLimitDismissed = () => { + this.setState({ + usageLimitDismissed: true, + }); + } + _calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { usageLimitEventContent = syncError.error.data; } - if (usageLimitEventContent) { - showServerLimitToast(usageLimitEventContent.limit_type, usageLimitEventContent.admin_contact, error); + // usageLimitDismissed is true when the user has explicitly hidden the toast + // and it will be reset to false if a *new* usage alert comes in. + if (usageLimitEventContent && this.state.usageLimitDismissed) { + showServerLimitToast( + usageLimitEventContent.limit_type, + this.onUsageLimitDismissed, + usageLimitEventContent.admin_contact, + error, + ); } else { hideServerLimitToast(); } @@ -320,10 +336,12 @@ class LoggedInView extends React.Component { if (!serverNoticeList) return []; const events = []; + let pinnedEventTs = 0; for (const room of serverNoticeList) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; + pinnedEventTs = pinStateEvent.getTs(); const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); for (const eventId of pinnedEventIds) { @@ -333,6 +351,11 @@ class LoggedInView extends React.Component { } } + if (pinnedEventTs && this.state.usageLimitEventTs > pinnedEventTs) { + // We've processed a newer event than this one, so ignore it. + return; + } + const usageLimitEvent = events.find((e) => { return ( e && e.getType() === 'm.room.message' && @@ -341,7 +364,12 @@ class LoggedInView extends React.Component { }); const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent(); this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); - this.setState({ usageLimitEventContent }); + this.setState({ + usageLimitEventContent, + usageLimitEventTs: pinnedEventTs, + // This is a fresh toast, we can show toasts again + usageLimitDismissed: false, + }); }; _onPaste = (ev) => { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 371623642b..161227a139 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -27,6 +27,7 @@ import dis from "../../dispatcher/dispatcher"; import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; +import {Layout, LayoutPropType} from "../../settings/Layout"; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; import {textForEvent} from "../../TextForEvent"; @@ -136,14 +137,13 @@ export default class MessagePanel extends React.Component { // whether to show reactions for an event showReactions: PropTypes.bool, - // whether to use the irc layout - useIRCLayout: PropTypes.bool, + // which layout to use + layout: LayoutPropType, // whether or not to show flair at all enableFlair: PropTypes.bool, }; - // Force props to be loaded for useIRCLayout constructor(props) { super(props); @@ -623,7 +623,7 @@ export default class MessagePanel extends React.Component { isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} showReactions={this.props.showReactions} - useIRCLayout={this.props.useIRCLayout} + layout={this.props.layout} enableFlair={this.props.enableFlair} /> @@ -821,7 +821,7 @@ export default class MessagePanel extends React.Component { } let ircResizer = null; - if (this.props.useIRCLayout) { + if (this.props.layout == Layout.IRC) { ircResizer = { statusBarVisible: false, canReact: false, canReply: false, - useIRCLayout: SettingsStore.getValue("useIRCLayout"), + layout: SettingsStore.getValue("layout"), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), }; @@ -264,7 +265,7 @@ export default class RoomView extends React.Component { this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, this.onReadReceiptsChange); - this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); + this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange); } private onWidgetStoreUpdate = () => { @@ -522,8 +523,7 @@ export default class RoomView extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - return (!ObjectUtils.shallowEqual(this.props, nextProps) || - !ObjectUtils.shallowEqual(this.state, nextState)); + return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState)); } componentDidUpdate() { @@ -638,7 +638,7 @@ export default class RoomView extends React.Component { private onLayoutChange = () => { this.setState({ - useIRCLayout: SettingsStore.getValue("useIRCLayout"), + layout: SettingsStore.getValue("layout"), }); }; @@ -1352,6 +1352,14 @@ export default class RoomView extends React.Component { SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned); }; + private onCallPlaced = (type: PlaceCallType) => { + dis.dispatch({ + action: 'place_call', + type: type, + room_id: this.state.room.roomId, + }); + }; + private onSettingsClick = () => { dis.dispatch({ action: "open_room_settings" }); }; @@ -1945,8 +1953,8 @@ export default class RoomView extends React.Component { const messagePanelClassNames = classNames( "mx_RoomView_messagePanel", { - "mx_IRCLayout": this.state.useIRCLayout, - "mx_GroupLayout": !this.state.useIRCLayout, + "mx_IRCLayout": this.state.layout == Layout.IRC, + "mx_GroupLayout": this.state.layout == Layout.Group, }); // console.info("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); @@ -1969,7 +1977,7 @@ export default class RoomView extends React.Component { permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} resizeNotifier={this.props.resizeNotifier} showReactions={true} - useIRCLayout={this.state.useIRCLayout} + layout={this.state.layout} />); let topUnreadMessagesBar = null; @@ -2031,6 +2039,7 @@ export default class RoomView extends React.Component { e2eStatus={this.state.e2eStatus} onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null} appsShown={this.state.showApps} + onCallPlaced={this.onCallPlaced} />
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 27a384ddb2..6bc1f70ba1 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -18,6 +18,7 @@ limitations under the License. */ import SettingsStore from "../../settings/SettingsStore"; +import {LayoutPropType} from "../../settings/Layout"; import React, {createRef} from 'react'; import ReactDOM from "react-dom"; import PropTypes from 'prop-types'; @@ -25,7 +26,6 @@ import {EventTimeline} from "matrix-js-sdk"; import * as Matrix from "matrix-js-sdk"; import { _t } from '../../languageHandler'; import {MatrixClientPeg} from "../../MatrixClientPeg"; -import * as ObjectUtils from "../../ObjectUtils"; import UserActivity from "../../UserActivity"; import Modal from "../../Modal"; import dis from "../../dispatcher/dispatcher"; @@ -36,6 +36,7 @@ import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; import {haveTileForEvent} from "../views/rooms/EventTile"; import {UIFeature} from "../../settings/UIFeature"; +import {objectHasDiff} from "../../utils/objects"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -111,8 +112,8 @@ class TimelinePanel extends React.Component { // whether to show reactions for an event showReactions: PropTypes.bool, - // whether to use the irc layout - useIRCLayout: PropTypes.bool, + // which layout to use + layout: LayoutPropType, } // a map from room id to read marker event timestamp @@ -260,7 +261,7 @@ class TimelinePanel extends React.Component { } shouldComponentUpdate(nextProps, nextState) { - if (!ObjectUtils.shallowEqual(this.props, nextProps)) { + if (objectHasDiff(this.props, nextProps)) { if (DEBUG) { console.group("Timeline.shouldComponentUpdate: props change"); console.log("props before:", this.props); @@ -270,7 +271,7 @@ class TimelinePanel extends React.Component { return true; } - if (!ObjectUtils.shallowEqual(this.state, nextState)) { + if (objectHasDiff(this.state, nextState)) { if (DEBUG) { console.group("Timeline.shouldComponentUpdate: state change"); console.log("state before:", this.state); @@ -1442,7 +1443,7 @@ class TimelinePanel extends React.Component { getRelationsForEvent={this.getRelationsForEvent} editState={this.state.editState} showReactions={this.props.showReactions} - useIRCLayout={this.props.useIRCLayout} + layout={this.props.layout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} /> ); diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 5b34d36d09..5ed6a00d74 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -103,11 +103,15 @@ export default class UserMenu extends React.Component { }; private isUserOnDarkTheme(): boolean { - const theme = SettingsStore.getValue("theme"); - if (theme.startsWith("custom-")) { - return getCustomTheme(theme.substring("custom-".length)).is_dark; + if (SettingsStore.getValue("use_system_theme")) { + return window.matchMedia("(prefers-color-scheme: dark)").matches; + } else { + const theme = SettingsStore.getValue("theme"); + if (theme.startsWith("custom-")) { + return getCustomTheme(theme.substring("custom-".length)).is_dark; + } + return theme === "dark"; } - return theme === "dark"; } private onProfileUpdate = async () => { @@ -300,7 +304,7 @@ export default class UserMenu extends React.Component { const hostSignupDomains = hostSignupConfig.domains || []; const mxDomain = MatrixClientPeg.get().getDomain(); const validDomains = hostSignupDomains.filter(d => (d === mxDomain || mxDomain.endsWith(`.${d}`))); - if (!hostSignupDomains || validDomains.length > 0) { + if (!hostSignupConfig.domains || validDomains.length > 0) { topSection =
; diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 98d69a63e7..0e16d17da9 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,7 +13,7 @@ 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 React, {ComponentProps} from 'react'; import Room from 'matrix-js-sdk/src/models/room'; import {getHttpUriForMxc} from 'matrix-js-sdk/src/content-repo'; @@ -24,7 +24,7 @@ import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; import {ResizeMethod} from "../../../Avatar"; -interface IProps { +interface IProps extends Omit, "name" | "idName" | "url" | "onClick">{ // Room may be left unset here, but if it is, // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 7d4e3b462f..814378bb51 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -34,6 +34,10 @@ import { } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import WidgetStore from "../../../stores/WidgetStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import {SETTINGS} from "../../../settings/Settings"; +import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore"; +import Modal from "../../../Modal"; +import ErrorDialog from "./ErrorDialog"; class GenericEditor extends React.PureComponent { // static propTypes = {onBack: PropTypes.func.isRequired}; @@ -794,6 +798,286 @@ class WidgetExplorer extends React.Component { } } +class SettingsExplorer extends React.Component { + static getLabel() { + return _t("Settings Explorer"); + } + + constructor(props) { + super(props); + + this.state = { + query: '', + editSetting: null, // set to a setting ID when editing + viewSetting: null, // set to a setting ID when exploring in detail + + explicitValues: null, // stringified JSON for edit view + explicitRoomValues: null, // stringified JSON for edit view + }; + } + + onQueryChange = (ev) => { + this.setState({query: ev.target.value}); + }; + + onExplValuesEdit = (ev) => { + this.setState({explicitValues: ev.target.value}); + }; + + onExplRoomValuesEdit = (ev) => { + this.setState({explicitRoomValues: ev.target.value}); + }; + + onBack = () => { + if (this.state.editSetting) { + this.setState({editSetting: null}); + } else if (this.state.viewSetting) { + this.setState({viewSetting: null}); + } else { + this.props.onBack(); + } + }; + + onViewClick = (ev, settingId) => { + ev.preventDefault(); + this.setState({viewSetting: settingId}); + }; + + onEditClick = (ev, settingId) => { + ev.preventDefault(); + this.setState({ + editSetting: settingId, + explicitValues: this.renderExplicitSettingValues(settingId, null), + explicitRoomValues: this.renderExplicitSettingValues(settingId, this.props.room.roomId), + }); + }; + + onSaveClick = async () => { + try { + const settingId = this.state.editSetting; + const parsedExplicit = JSON.parse(this.state.explicitValues); + const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues); + for (const level of Object.keys(parsedExplicit)) { + console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`); + try { + const val = parsedExplicit[level]; + await SettingsStore.setValue(settingId, null, level, val); + } catch (e) { + console.warn(e); + } + } + const roomId = this.props.room.roomId; + for (const level of Object.keys(parsedExplicit)) { + console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`); + try { + const val = parsedExplicitRoom[level]; + await SettingsStore.setValue(settingId, roomId, level, val); + } catch (e) { + console.warn(e); + } + } + this.setState({ + viewSetting: settingId, + editSetting: null, + }); + } catch (e) { + Modal.createTrackedDialog('Devtools - Failed to save settings', '', ErrorDialog, { + title: _t("Failed to save settings"), + description: e.message, + }); + } + }; + + renderSettingValue(val) { + // Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us + const toStringTypes = ['boolean', 'number']; + if (toStringTypes.includes(typeof(val))) { + return val.toString(); + } else { + return JSON.stringify(val); + } + } + + renderExplicitSettingValues(setting, roomId) { + const vals = {}; + for (const level of LEVEL_ORDER) { + try { + vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true); + if (vals[level] === undefined) { + vals[level] = null; + } + } catch (e) { + console.warn(e); + } + } + return JSON.stringify(vals, null, 4); + } + + renderCanEditLevel(roomId, level) { + const canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level); + const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable'; + return {canEdit.toString()}; + } + + render() { + const room = this.props.room; + + if (!this.state.viewSetting && !this.state.editSetting) { + // view all settings + const allSettings = Object.keys(SETTINGS) + .filter(n => this.state.query ? n.toLowerCase().includes(this.state.query.toLowerCase()) : true); + return ( +
+
+ + + + + + + + + + + {allSettings.map(i => ( + + + + + + ))} + +
{_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
+ this.onViewClick(e, i)}> + {i} + + this.onEditClick(e, i)} + className='mx_DevTools_SettingsExplorer_edit' + > + ✏ + + + {this.renderSettingValue(SettingsStore.getValue(i))} + + + {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} + +
+
+
+ +
+
+ ); + } else if (this.state.editSetting) { + return ( +
+
+

{_t("Setting:")} {this.state.editSetting}

+ +
+ {_t("Caution:")} {_t( + "This UI does NOT check the types of the values. Use at your own risk.", + )} +
+ +
+ {_t("Setting definition:")} +
{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}
+
+ +
+ + + + + + + + + + {LEVEL_ORDER.map(lvl => ( + + + {this.renderCanEditLevel(null, lvl)} + {this.renderCanEditLevel(room.roomId, lvl)} + + ))} + +
{_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
{lvl}
+
+ +
+ +
+ +
+ +
+ +
+
+ + +
+
+ ); + } else if (this.state.viewSetting) { + return ( +
+
+

{_t("Setting:")} {this.state.viewSetting}

+ +
+ {_t("Setting definition:")} +
{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}
+
+ +
+ {_t("Value:")}  + {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))} +
+ +
+ {_t("Value in this room:")}  + {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))} +
+ +
+ {_t("Values at explicit levels:")} +
{this.renderExplicitSettingValues(this.state.viewSetting, null)}
+
+ +
+ {_t("Values at explicit levels in this room:")} +
{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}
+
+ +
+
+ + +
+
+ ); + } + } +} + const Entries = [ SendCustomEvent, RoomStateExplorer, @@ -802,6 +1086,7 @@ const Entries = [ ServersInRoomList, VerificationExplorer, WidgetExplorer, + SettingsExplorer, ]; export default class DevtoolsDialog extends React.PureComponent { diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 20cca35d62..49c97831bc 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -22,6 +22,7 @@ import * as Avatar from '../../../Avatar'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import EventTile from '../rooms/EventTile'; import SettingsStore from "../../../settings/SettingsStore"; +import {Layout} from "../../../settings/Layout"; import {UIFeature} from "../../../settings/UIFeature"; interface IProps { @@ -33,7 +34,7 @@ interface IProps { /** * Whether to use the irc layout or not */ - useIRCLayout: boolean; + layout: Layout; /** * classnames to apply to the wrapper of the preview @@ -121,14 +122,14 @@ export default class EventTilePreview extends React.Component { const event = this.fakeEvent(this.state); const className = classnames(this.props.className, { - "mx_IRCLayout": this.props.useIRCLayout, - "mx_GroupLayout": !this.props.useIRCLayout, + "mx_IRCLayout": this.props.layout == Layout.IRC, + "mx_GroupLayout": this.props.layout == Layout.Group, }); return
; diff --git a/src/components/views/elements/PersistentApp.js b/src/components/views/elements/PersistentApp.js index a1e805c085..7801076c66 100644 --- a/src/components/views/elements/PersistentApp.js +++ b/src/components/views/elements/PersistentApp.js @@ -31,6 +31,7 @@ export default class PersistentApp extends React.Component { componentDidMount() { this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate); + MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership); } componentWillUnmount() { @@ -38,6 +39,9 @@ export default class PersistentApp extends React.Component { this._roomStoreToken.remove(); } ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate); + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership); + } } _onRoomViewStoreUpdate = payload => { @@ -53,16 +57,28 @@ export default class PersistentApp extends React.Component { }); }; + _onMyMembership = async (room, membership) => { + const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); + if (membership !== "join") { + // we're not in the room anymore - delete + if (room.roomId === persistentWidgetInRoomId) { + ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); + } + } + }; + render() { if (this.state.persistentWidgetId) { const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); - if (this.state.roomId !== persistentWidgetInRoomId) { - const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); - // Sanity check the room - the widget may have been destroyed between render cycles, and - // thus no room is associated anymore. - if (!persistentWidgetInRoom) return null; + const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); + // Sanity check the room - the widget may have been destroyed between render cycles, and + // thus no room is associated anymore. + if (!persistentWidgetInRoom) return null; + + const myMembership = persistentWidgetInRoom.getMyMembership(); + if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") { // get the widget data const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 24b49f2b13..27d773b099 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../../DateUtils'; import {MatrixEvent} from 'matrix-js-sdk'; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import SettingsStore from "../../../settings/SettingsStore"; +import {LayoutPropType} from "../../../settings/Layout"; import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {Action} from "../../../dispatcher/actions"; @@ -42,7 +43,7 @@ export default class ReplyThread extends React.Component { onHeightChanged: PropTypes.func.isRequired, permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, // Specifies which layout to use. - useIRCLayout: PropTypes.bool, + layout: LayoutPropType, }; static contextType = MatrixClientContext; @@ -209,7 +210,7 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) { + static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout) { if (!ReplyThread.getParentEventId(parentEv)) { return
; } @@ -218,7 +219,7 @@ export default class ReplyThread extends React.Component { onHeightChanged={onHeightChanged} ref={ref} permalinkCreator={permalinkCreator} - useIRCLayout={useIRCLayout} + layout={layout} />; } @@ -386,7 +387,7 @@ export default class ReplyThread extends React.Component { permalinkCreator={this.props.permalinkCreator} isRedacted={ev.isRedacted()} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} - useIRCLayout={this.props.useIRCLayout} + layout={this.props.layout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} replacingEventId={ev.replacingEventId()} /> diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js deleted file mode 100644 index e9de6f8d15..0000000000 --- a/src/components/views/elements/RoomDirectoryButton.js +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright 2017 Vector Creations 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 from 'react'; -import * as sdk from '../../../index'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import {Action} from "../../../dispatcher/actions"; - -const RoomDirectoryButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -RoomDirectoryButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default RoomDirectoryButton; diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx new file mode 100644 index 0000000000..9178155d19 --- /dev/null +++ b/src/components/views/elements/RoomName.tsx @@ -0,0 +1,40 @@ +/* +Copyright 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. +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 {useEffect, useState} from "react"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {useEventEmitter} from "../../../hooks/useEventEmitter"; + +interface IProps { + room: Room; + children?(name: string): JSX.Element; +} + +const RoomName = ({ room, children }: IProps): JSX.Element => { + const [name, setName] = useState(room?.name); + useEventEmitter(room, "Room.name", () => { + setName(room?.name); + }); + useEffect(() => { + setName(room?.name); + }, [room]); + + if (children) return children(name); + return name || ""; +}; + +export default RoomName; diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx new file mode 100644 index 0000000000..fe8aa5a83d --- /dev/null +++ b/src/components/views/elements/RoomTopic.tsx @@ -0,0 +1,45 @@ +/* +Copyright 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. +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, {useEffect, useState} from "react"; +import {EventType} from "matrix-js-sdk/src/@types/event"; +import {Room} from "matrix-js-sdk/src/models/room"; + +import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import {linkifyElement} from "../../../HtmlUtils"; + +interface IProps { + room?: Room; + children?(topic: string, ref: (element: HTMLElement) => void): JSX.Element; +} + +export const getTopic = room => room?.currentState?.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; + +const RoomTopic = ({ room, children }: IProps): JSX.Element => { + const [topic, setTopic] = useState(getTopic(room)); + useEventEmitter(room.currentState, "RoomState.events", () => { + setTopic(getTopic(room)); + }); + useEffect(() => { + setTopic(getTopic(room)); + }, [room]); + + const ref = e => e && linkifyElement(e); + if (children) return children(topic, ref); + return { topic }; +}; + +export default RoomTopic; diff --git a/src/components/views/elements/StartChatButton.js b/src/components/views/elements/StartChatButton.js deleted file mode 100644 index f828f8ae4d..0000000000 --- a/src/components/views/elements/StartChatButton.js +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2017 Vector Creations 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 from 'react'; -import * as sdk from '../../../index'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; - -const StartChatButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -StartChatButton.propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, -}; - -export default StartChatButton; diff --git a/src/components/views/elements/TintableSvgButton.js b/src/components/views/elements/TintableSvgButton.js deleted file mode 100644 index a3f5b7db5d..0000000000 --- a/src/components/views/elements/TintableSvgButton.js +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2017 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 from 'react'; -import PropTypes from 'prop-types'; -import TintableSvg from './TintableSvg'; -import AccessibleButton from './AccessibleButton'; - -export default class TintableSvgButton extends React.Component { - constructor(props) { - super(props); - } - - render() { - let classes = "mx_TintableSvgButton"; - if (this.props.className) { - classes += " " + this.props.className; - } - return ( - - - - - ); - } -} - -TintableSvgButton.propTypes = { - src: PropTypes.string, - title: PropTypes.string, - className: PropTypes.string, - width: PropTypes.string.isRequired, - height: PropTypes.string.isRequired, - onClick: PropTypes.func, -}; - -TintableSvgButton.defaultProps = { - onClick: function() {}, -}; diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index d17a1c4ce3..770cd4fff3 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -288,7 +288,7 @@ export default class MFileBody extends React.Component {