From a732c55797599446c131e46ccc937859978ce5c6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 21 Aug 2020 18:22:05 +0100 Subject: [PATCH 01/34] Add secret storage readiness checks This visits all places that were checking for cross-siging readiness and adapts them to also check for secret storage readiness if needed. Part of https://github.com/vector-im/element-web/issues/13895 --- src/DeviceListener.ts | 6 +++++- .../views/settings/CrossSigningPanel.js | 16 ++++++++++++---- src/i18n/strings/en_EN.json | 3 ++- src/rageshake/submit-rageshake.ts | 1 + 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts index 6b667ae54d..b05f0fcd68 100644 --- a/src/DeviceListener.ts +++ b/src/DeviceListener.ts @@ -207,9 +207,13 @@ export default class DeviceListener { // (we add a listener on sync to do once check after the initial sync is done) if (!cli.isInitialSyncComplete()) return; + // JRS: This will change again in the next PR which moves secret storage + // later in the process. const crossSigningReady = await cli.isCrossSigningReady(); + const secretStorageReady = await cli.isSecretStorageReady(); + const allSystemsReady = crossSigningReady && secretStorageReady; - if (this.dismissedThisDeviceToast || crossSigningReady) { + if (this.dismissedThisDeviceToast || allSystemsReady) { hideSetupEncryptionToast(); } else if (this.shouldShowSetupEncryptionToast()) { // make sure our keys are finished downloading diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 1c6baee9af..847bcf3da3 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -89,6 +89,7 @@ export default class CrossSigningPanel extends React.PureComponent { const homeserverSupportsCrossSigning = await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"); const crossSigningReady = await cli.isCrossSigningReady(); + const secretStorageReady = await cli.isSecretStorageReady(); this.setState({ crossSigningPublicKeysOnDevice, @@ -101,6 +102,7 @@ export default class CrossSigningPanel extends React.PureComponent { secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, + secretStorageReady, }); } @@ -151,6 +153,7 @@ export default class CrossSigningPanel extends React.PureComponent { secretStorageKeyInAccount, homeserverSupportsCrossSigning, crossSigningReady, + secretStorageReady, } = this.state; let errorSection; @@ -166,14 +169,19 @@ export default class CrossSigningPanel extends React.PureComponent { summarisedStatus =

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

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

✅ {_t( - "Cross-signing and secret storage are enabled.", + "Cross-signing and secret storage are ready for use.", + )}

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

✅ {_t( + "Cross-signing is ready for use, but secret storage is " + + "currently not being used to backup your keys.", )}

; } else if (crossSigningPrivateKeysInStorage) { summarisedStatus =

{_t( - "Your account has a cross-signing identity in secret storage, but it " + - "is not yet trusted by this session.", + "Your account has a cross-signing identity in secret storage, " + + "but it is not yet trusted by this session.", )}

; } else { summarisedStatus =

{_t( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c12b57c033..dc2669e20f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -645,7 +645,8 @@ "Confirm password": "Confirm password", "Change Password": "Change Password", "Your homeserver does not support cross-signing.": "Your homeserver does not support cross-signing.", - "Cross-signing and secret storage are enabled.": "Cross-signing and secret storage are enabled.", + "Cross-signing and secret storage are ready for use.": "Cross-signing and secret storage are ready for use.", + "Cross-signing is ready for use, but secret storage is currently not being used to backup your keys.": "Cross-signing is ready for use, but secret storage is currently not being used to backup your keys.", "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.", "Cross-signing and secret storage are not yet set up.": "Cross-signing and secret storage are not yet set up.", "Reset cross-signing and secret storage": "Reset cross-signing and secret storage", diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 74292749b9..448562b68a 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -115,6 +115,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append("cross_signing_supported_by_hs", String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))); body.append("cross_signing_ready", String(await client.isCrossSigningReady())); + body.append("secret_storage_ready", String(await client.isSecretStorageReady())); } } From 2d4ac548d0e709b594c08d0358fbea0d364f044f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 24 Aug 2020 19:19:28 -0600 Subject: [PATCH 02/34] Override invite metadata if the server wants a group profile --- src/components/views/rooms/RoomPreviewBar.js | 28 +++++- src/components/views/rooms/RoomTile.tsx | 28 ++++-- src/stores/CommunityPrototypeStore.ts | 100 +++++++++++++++++++ 3 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 src/stores/CommunityPrototypeStore.ts diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index d52bbbb0d0..dc3893785d 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -26,6 +26,8 @@ import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import SdkConfig from "../../../SdkConfig"; import IdentityAuthClient from '../../../IdentityAuthClient'; +import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; const MessageCase = Object.freeze({ NotLoggedIn: "NotLoggedIn", @@ -100,6 +102,7 @@ export default createReactClass({ componentDidMount: function() { this._checkInvitedEmail(); + CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate); }, componentDidUpdate: function(prevProps, prevState) { @@ -108,6 +111,10 @@ export default createReactClass({ } }, + componentWillUnmount: function() { + CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate); + }, + _checkInvitedEmail: async function() { // If this is an invite and we've been told what email address was // invited, fetch the user's account emails and discovery bindings so we @@ -143,6 +150,13 @@ export default createReactClass({ } }, + _onCommunityUpdate: function (roomId) { + if (this.props.room && this.props.room.roomId !== roomId) { + return; + } + this.forceUpdate(); // we have nothing to update + }, + _getMessageCase() { const isGuest = MatrixClientPeg.get().isGuest(); @@ -219,8 +233,15 @@ export default createReactClass({ } }, + _communityProfile: function() { + if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId); + return {displayName: null, avatarMxc: null}; + }, + _roomName: function(atStart = false) { - const name = this.props.room ? this.props.room.name : this.props.roomAlias; + let name = this.props.room ? this.props.room.name : this.props.roomAlias; + const profile = this._communityProfile(); + if (profile.displayName) name = profile.displayName; if (name) { return name; } else if (atStart) { @@ -439,7 +460,10 @@ export default createReactClass({ } case MessageCase.Invite: { const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar"); - const avatar = ; + const oobData = Object.assign({}, this.props.oobData, { + avatarUrl: this._communityProfile().avatarMxc, + }); + const avatar = ; const inviteMember = this._getInviteMember(); let inviterElement; diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 0c99b98e1a..a09853d762 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -27,7 +27,7 @@ import defaultDispatcher from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; import { _t } from "../../../languageHandler"; -import { ChevronFace, ContextMenuTooltipButton, MenuItemRadio } from "../../structures/ContextMenu"; +import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; @@ -47,8 +47,11 @@ import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber" import IconizedContextMenu, { IconizedContextMenuCheckbox, IconizedContextMenuOption, - IconizedContextMenuOptionList, IconizedContextMenuRadio + IconizedContextMenuOptionList, + IconizedContextMenuRadio } from "../context_menus/IconizedContextMenu"; +import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; interface IProps { room: Room; @@ -101,6 +104,7 @@ export default class RoomTile extends React.PureComponent { this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); this.roomProps = EchoChamber.forRoom(this.props.room); this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate); + CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate); } private onNotificationUpdate = () => { @@ -140,6 +144,7 @@ export default class RoomTile extends React.PureComponent { defaultDispatcher.unregister(this.dispatcherRef); MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged); this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate); } private onAction = (payload: ActionPayload) => { @@ -150,6 +155,11 @@ export default class RoomTile extends React.PureComponent { } }; + private onCommunityUpdate = (roomId: string) => { + if (roomId !== this.props.room.roomId) return; + this.forceUpdate(); // we don't have anything to actually update + }; + private onRoomPreviewChanged = (room: Room) => { if (this.props.room && room.roomId === this.props.room.roomId) { // generatePreview() will return nothing if the user has previews disabled @@ -461,11 +471,21 @@ export default class RoomTile extends React.PureComponent { 'mx_RoomTile_minimized': this.props.isMinimized, }); + let roomProfile: IRoomProfile = {displayName: null, avatarMxc: null}; + if (this.props.tag === DefaultTagID.Invite) { + roomProfile = CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId); + } + + let name = roomProfile.displayName || this.props.room.name; + if (typeof name !== 'string') name = ''; + name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon + const roomAvatar = ; let badge: React.ReactNode; @@ -482,10 +502,6 @@ export default class RoomTile extends React.PureComponent { ); } - let name = this.props.room.name; - if (typeof name !== 'string') name = ''; - name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - let messagePreview = null; if (this.showMessagePreview && this.state.messagePreview) { messagePreview = ( diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts new file mode 100644 index 0000000000..581f8a97c8 --- /dev/null +++ b/src/stores/CommunityPrototypeStore.ts @@ -0,0 +1,100 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { ActionPayload } from "../dispatcher/payloads"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { EffectiveMembership, getEffectiveMembership } from "../utils/membership"; +import SettingsStore from "../settings/SettingsStore"; +import * as utils from "matrix-js-sdk/src/utils"; +import { UPDATE_EVENT } from "./AsyncStore"; + +interface IState { + // nothing of value - we use account data +} + +export interface IRoomProfile { + displayName: string; + avatarMxc: string; +} + +export class CommunityPrototypeStore extends AsyncStoreWithClient { + private static internalInstance = new CommunityPrototypeStore(); + + private constructor() { + super(defaultDispatcher, {}); + } + + public static get instance(): CommunityPrototypeStore { + return CommunityPrototypeStore.internalInstance; + } + + protected async onAction(payload: ActionPayload): Promise { + if (!this.matrixClient || !SettingsStore.getValue("feature_communities_v2_prototypes")) { + return; + } + + if (payload.action === "MatrixActions.Room.myMembership") { + const room: Room = payload.room; + const membership = getEffectiveMembership(payload.membership); + const oldMembership = getEffectiveMembership(payload.oldMembership); + if (membership === oldMembership) return; + + if (membership === EffectiveMembership.Invite) { + try { + const path = utils.encodeUri("/rooms/$roomId/group_info", {$roomId: room.roomId}); + const profile = await this.matrixClient._http.authedRequest( + undefined, "GET", path, + undefined, undefined, + {prefix: "/_matrix/client/unstable/im.vector.custom"}); + // we use global account data because per-room account data on invites is unreliable + await this.matrixClient.setAccountData("im.vector.group_info." + room.roomId, profile); + } catch (e) { + console.warn("Non-fatal error getting group information for invite:", e); + } + } + } else if (payload.action === "MatrixActions.accountData") { + if (payload.event_type.startsWith("im.vector.group_info.")) { + this.emit(UPDATE_EVENT, payload.event_type.substring("im.vector.group_info.".length)); + } + } + } + + public getInviteProfile(roomId: string): IRoomProfile { + if (!this.matrixClient) return {displayName: null, avatarMxc: null}; + const room = this.matrixClient.getRoom(roomId); + if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId); + if (data && data.getContent()) { + return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url}; + } + } + return {displayName: room.name, avatarMxc: room.avatar_url}; + } + + protected async onReady(): Promise { + for (const room of this.matrixClient.getRooms()) { + const myMember = room.currentState.getMembers().find(m => m.userId === this.matrixClient.getUserId()); + if (!myMember) continue; + if (getEffectiveMembership(myMember.membership) === EffectiveMembership.Invite) { + // Fake an update for anything that might have started listening before the invite + // data was available (eg: RoomPreviewBar after a refresh) + this.emit(UPDATE_EVENT, room.roomId); + } + } + } +} From 65fe562cbf54db4b8da8ef67d15a659dea225ef8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 24 Aug 2020 22:38:24 -0600 Subject: [PATCH 03/34] Select new tag after creating the group --- src/components/views/dialogs/CreateGroupDialog.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index d8a8b96961..2b22054947 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -88,6 +88,13 @@ export default createReactClass({ action: 'view_room', room_id: result.room_id, }); + + // Ensure the tag gets selected now that we've created it + dis.dispatch({action: 'deselect_tags'}, true); + dis.dispatch({ + action: 'select_tag', + tag: result.group_id, + }); } else { dis.dispatch({ action: 'view_group', From 8feda74156263680e0f39c46cffb6a271503ab35 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 24 Aug 2020 22:40:43 -0600 Subject: [PATCH 04/34] Wire up TagPanel's create button to the dialog --- src/components/structures/TagPanel.js | 24 +++++++++++++++++++----- src/i18n/strings/en_EN.json | 1 + 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 3acec417f2..4acbc49d4d 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -146,6 +146,24 @@ const TagPanel = createReactClass({ mx_TagPanel_items_selected: itemsSelected, }); + let createButton = ( + + ); + + if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + createButton = ( + + ); + } + return

{ clearButton } @@ -168,11 +186,7 @@ const TagPanel = createReactClass({ { this.renderGlobalIcon() } { tags }
- + {createButton}
{ provided.placeholder }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c12b57c033..d052ba5a51 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2091,6 +2091,7 @@ "Click to mute video": "Click to mute video", "Click to unmute audio": "Click to unmute audio", "Click to mute audio": "Click to mute audio", + "Create community": "Create community", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", From 7c1a9993e3c47c90abb6ec7557578908d5f96901 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Aug 2020 13:58:15 -0600 Subject: [PATCH 05/34] Add new create group dialog --- res/css/_components.scss | 2 + .../dialogs/_PrototypeCreateGroupDialog.scss | 102 ++++++++ res/css/views/elements/_InfoTooltip.scss | 34 +++ res/img/element-icons/add-photo.svg | 5 + res/img/element-icons/info.svg | 4 + src/components/structures/MatrixChat.tsx | 6 +- src/components/views/dialogs/IDialogProps.ts | 19 ++ .../dialogs/PrototypeCreateGroupDialog.tsx | 217 ++++++++++++++++++ .../views/dialogs/ServerOfflineDialog.tsx | 4 +- src/components/views/dialogs/ShareDialog.tsx | 4 +- src/components/views/elements/InfoTooltip.tsx | 73 ++++++ src/i18n/strings/en_EN.json | 9 + 12 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 res/css/views/dialogs/_PrototypeCreateGroupDialog.scss create mode 100644 res/css/views/elements/_InfoTooltip.scss create mode 100644 res/img/element-icons/add-photo.svg create mode 100644 res/img/element-icons/info.svg create mode 100644 src/components/views/dialogs/IDialogProps.ts create mode 100644 src/components/views/dialogs/PrototypeCreateGroupDialog.tsx create mode 100644 src/components/views/elements/InfoTooltip.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 5145133127..60725ff5d4 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -72,6 +72,7 @@ @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; +@import "./views/dialogs/_PrototypeCreateGroupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; @@ -106,6 +107,7 @@ @import "./views/elements/_FormButton.scss"; @import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; +@import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_PowerSelector.scss"; diff --git a/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss b/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss new file mode 100644 index 0000000000..a0f505b1ff --- /dev/null +++ b/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss @@ -0,0 +1,102 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_PrototypeCreateGroupDialog { + .mx_Dialog_content { + display: flex; + flex-direction: row; + margin-bottom: 12px; + + .mx_PrototypeCreateGroupDialog_colName { + flex-basis: 66.66%; + padding-right: 100px; + + .mx_Field input { + font-size: $font-16px; + line-height: $font-20px; + } + + .mx_PrototypeCreateGroupDialog_subtext { + display: block; + color: $muted-fg-color; + margin-bottom: 16px; + + &:last-child { + margin-top: 16px; + } + + &.mx_PrototypeCreateGroupDialog_subtext_error { + color: $warning-color; + } + } + + .mx_PrototypeCreateGroupDialog_communityId { + position: relative; + + .mx_InfoTooltip { + float: right; + } + } + + .mx_AccessibleButton { + display: block; + height: 32px; + font-size: $font-16px; + line-height: 32px; + } + } + + .mx_PrototypeCreateGroupDialog_colAvatar { + flex-basis: 33.33%; + + .mx_PrototypeCreateGroupDialog_avatarContainer { + margin-top: 12px; + margin-bottom: 20px; + + .mx_PrototypeCreateGroupDialog_avatar, + .mx_PrototypeCreateGroupDialog_placeholderAvatar { + width: 96px; + height: 96px; + border-radius: 96px; + } + + .mx_PrototypeCreateGroupDialog_placeholderAvatar { + background-color: #368BD6; // hardcoded for both themes + + &::before { + display: inline-block; + background-color: #fff; // hardcoded because the background is + mask-repeat: no-repeat; + mask-size: 96px; + width: 96px; + height: 96px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/add-photo.svg'); + } + } + } + + .mx_PrototypeCreateGroupDialog_tip { + &>b, &>span { + display: block; + color: $muted-fg-color; + } + } + } + } +} diff --git a/res/css/views/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss new file mode 100644 index 0000000000..5858a60629 --- /dev/null +++ b/res/css/views/elements/_InfoTooltip.scss @@ -0,0 +1,34 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_InfoTooltip_icon { + width: 16px; + height: 16px; + display: inline-block; +} + +.mx_InfoTooltip_icon::before { + display: inline-block; + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + mask-position: center; + content: ''; + vertical-align: middle; + mask-image: url('$(res)/img/element-icons/info.svg'); +} diff --git a/res/img/element-icons/add-photo.svg b/res/img/element-icons/add-photo.svg new file mode 100644 index 0000000000..bde5253bea --- /dev/null +++ b/res/img/element-icons/add-photo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/res/img/element-icons/info.svg b/res/img/element-icons/info.svg new file mode 100644 index 0000000000..b5769074ab --- /dev/null +++ b/res/img/element-icons/info.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a10af429b9..2b764d00c9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -77,6 +77,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { SettingLevel } from "../../settings/SettingLevel"; import { leaveRoomBehaviour } from "../../utils/membership"; +import PrototypeCreateGroupDialog from "../views/dialogs/PrototypeCreateGroupDialog"; /** constants for MatrixChat.state.view */ export enum Views { @@ -620,7 +621,10 @@ export default class MatrixChat extends React.PureComponent { this.createRoom(payload.public); break; case 'view_create_group': { - const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); + let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog") + if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + CreateGroupDialog = PrototypeCreateGroupDialog; + } Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); break; } diff --git a/src/components/views/dialogs/IDialogProps.ts b/src/components/views/dialogs/IDialogProps.ts new file mode 100644 index 0000000000..1027ca7607 --- /dev/null +++ b/src/components/views/dialogs/IDialogProps.ts @@ -0,0 +1,19 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export interface IDialogProps { + onFinished: (bool) => void; +} diff --git a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx new file mode 100644 index 0000000000..7427b2737c --- /dev/null +++ b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx @@ -0,0 +1,217 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { ChangeEvent } from 'react'; +import BaseDialog from "./BaseDialog"; +import { _t } from "../../../languageHandler"; +import { IDialogProps } from "./IDialogProps"; +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import InfoTooltip from "../elements/InfoTooltip"; +import dis from "../../../dispatcher/dispatcher"; + +interface IProps extends IDialogProps { +} + +interface IState { + name: string; + localpart: string; + error: string; + busy: boolean; + avatarFile: File; + avatarPreview: string; +} + +export default class PrototypeCreateGroupDialog extends React.PureComponent { + private avatarUploadRef: React.RefObject = React.createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + name: "", + localpart: "", + error: null, + busy: false, + avatarFile: null, + avatarPreview: null, + }; + } + + private onNameChange = (ev: ChangeEvent) => { + const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-'); + this.setState({name: ev.target.value, localpart}); + }; + + private onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (this.state.busy) return; + + // We'll create the community now to see if it's taken, leaving it active in + // the background for the user to look at while they invite people. + this.setState({busy: true}); + try { + let avatarUrl = null; + if (this.state.avatarFile) { + avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile); + } + + const result = await MatrixClientPeg.get().createGroup({ + localpart: this.state.localpart, + profile: { + name: this.state.name, + avatar_url: avatarUrl, + }, + }); + + // Ensure the tag gets selected now that we've created it + dis.dispatch({action: 'deselect_tags'}, true); + dis.dispatch({ + action: 'select_tag', + tag: result.group_id, + }); + + if (result.room_id) { + dis.dispatch({ + action: 'view_room', + room_id: result.room_id, + }); + } else { + dis.dispatch({ + action: 'view_group', + group_id: result.group_id, + group_is_new: true, + }); + } + + // TODO: Show invite dialog + } catch (e) { + console.error(e); + this.setState({ + busy: false, + error: _t( + "There was an error creating your community. The name may be taken or the " + + "server is unable to process your request.", + ), + }); + } + }; + + private onAvatarChanged = (e: ChangeEvent) => { + if (!e.target.files || !e.target.files.length) { + this.setState({avatarFile: null}); + } else { + this.setState({busy: true}); + const file = e.target.files[0]; + const reader = new FileReader(); + reader.onload = (ev: ProgressEvent) => { + this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string}); + }; + reader.readAsDataURL(file); + } + }; + + private onChangeAvatar = () => { + if (this.avatarUploadRef.current) this.avatarUploadRef.current.click(); + }; + + public render() { + let communityId = null; + if (this.state.localpart) { + communityId = ( + + {_t("Community ID: +:%(domain)s", { + domain: MatrixClientPeg.getHomeserverName(), + }, { + localpart: () => {this.state.localpart}, + })} + + + ); + } + + let helpText = ( + + {_t("You can change this later if needed.")} + + ); + if (this.state.error) { + helpText = ( + + {this.state.error} + + ); + } + + let preview = ; + if (!this.state.avatarPreview) { + preview =
+ } + + return ( + +
+
+
+ + {helpText} + + {/*nbsp is to reserve the height of this element when there's nothing*/} +  {communityId} + + + {_t("Create")} + +
+
+ + + {preview} + +
+ {_t("PRO TIP")} + + {_t("An image will help people identify your community.")} + +
+
+
+
+
+ ); + } +} diff --git a/src/components/views/dialogs/ServerOfflineDialog.tsx b/src/components/views/dialogs/ServerOfflineDialog.tsx index f6767dcb8d..81f628343b 100644 --- a/src/components/views/dialogs/ServerOfflineDialog.tsx +++ b/src/components/views/dialogs/ServerOfflineDialog.tsx @@ -27,9 +27,9 @@ import Spinner from "../elements/Spinner"; import AccessibleButton from "../elements/AccessibleButton"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IDialogProps } from "./IDialogProps"; -interface IProps { - onFinished: (bool) => void; +interface IProps extends IDialogProps { } export default class ServerOfflineDialog extends React.PureComponent { diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index dc2a987f13..22f83d391c 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -31,6 +31,7 @@ import {toRightOf} from "../../structures/ContextMenu"; import {copyPlaintext, selectText} from "../../../utils/strings"; import StyledCheckbox from '../elements/StyledCheckbox'; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; +import { IDialogProps } from "./IDialogProps"; const socials = [ { @@ -60,8 +61,7 @@ const socials = [ }, ]; -interface IProps { - onFinished: () => void; +interface IProps extends IDialogProps { target: Room | User | Group | RoomMember | MatrixEvent; permalinkCreator: RoomPermalinkCreator; } diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx new file mode 100644 index 0000000000..645951aab9 --- /dev/null +++ b/src/components/views/elements/InfoTooltip.tsx @@ -0,0 +1,73 @@ +/* +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +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. +*/ + +import React from 'react'; +import classNames from 'classnames'; + +import AccessibleButton from "./AccessibleButton"; +import Tooltip from './Tooltip'; +import { _t } from "../../../languageHandler"; + +interface ITooltipProps { + tooltip?: React.ReactNode; + tooltipClassName?: string; +} + +interface IState { + hover: boolean; +} + +export default class InfoTooltip extends React.PureComponent { + constructor(props: ITooltipProps) { + super(props); + this.state = { + hover: false, + }; + } + + onMouseOver = () => { + this.setState({ + hover: true, + }); + }; + + onMouseLeave = () => { + this.setState({ + hover: false, + }); + }; + + render() { + const {tooltip, children, tooltipClassName} = this.props; + const title = _t("Information"); + + // Tooltip are forced on the right for a more natural feel to them on info icons + const tip = this.state.hover ? :
; + return ( +
+ + {children} + {tip} +
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d052ba5a51..9a246a421d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1484,6 +1484,7 @@ "Rotate Right": "Rotate Right", "Rotate clockwise": "Rotate clockwise", "Download this file": "Download this file", + "Information": "Information", "Language Dropdown": "Language Dropdown", "Manage Integrations": "Manage Integrations", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", @@ -1728,6 +1729,14 @@ "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", "This wasn't me": "This wasn't me", + "There was an error creating your community. The name may be taken or the server is unable to process your request.": "There was an error creating your community. The name may be taken or the server is unable to process your request.", + "Community ID: +:%(domain)s": "Community ID: +:%(domain)s", + "Use this when referencing your community to others. The community ID cannot be changed.": "Use this when referencing your community to others. The community ID cannot be changed.", + "You can change this later if needed.": "You can change this later if needed.", + "What's the name of your community or team?": "What's the name of your community or team?", + "Enter name": "Enter name", + "PRO TIP": "PRO TIP", + "An image will help people identify your community.": "An image will help people identify your community.", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", From 56c7f8698330569367de839f45f36c944eff6f16 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Aug 2020 21:02:32 -0600 Subject: [PATCH 06/34] Add an invite users to community step to dialog flow --- res/css/_components.scss | 1 + .../_PrototypeCommunityInviteDialog.scss | 88 ++++++ src/RoomInvite.js | 16 +- src/components/views/dialogs/InviteDialog.js | 4 +- .../PrototypeCommunityInviteDialog.tsx | 252 ++++++++++++++++++ .../dialogs/PrototypeCreateGroupDialog.tsx | 9 +- src/i18n/strings/en_EN.json | 12 +- 7 files changed, 371 insertions(+), 11 deletions(-) create mode 100644 res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss create mode 100644 src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 60725ff5d4..88fbbb5c3e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -72,6 +72,7 @@ @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; +@import "./views/dialogs/_PrototypeCommunityInviteDialog.scss"; @import "./views/dialogs/_PrototypeCreateGroupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; diff --git a/res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss b/res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss new file mode 100644 index 0000000000..8d2ff598d8 --- /dev/null +++ b/res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss @@ -0,0 +1,88 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_PrototypeCommunityInviteDialog { + &.mx_Dialog_fixedWidth { + width: 360px; + } + + .mx_Dialog_content { + margin-bottom: 0; + + .mx_PrototypeCommunityInviteDialog_people { + position: relative; + margin-bottom: 4px; + + .mx_AccessibleButton { + display: inline-block; + background-color: $focus-bg-color; // XXX: Abuse of variables + border-radius: 4px; + padding: 3px 5px; + font-size: $font-12px; + float: right; + } + } + + .mx_PrototypeCommunityInviteDialog_morePeople { + margin-top: 8px; + } + + .mx_PrototypeCommunityInviteDialog_person { + position: relative; + margin-top: 4px; + + & > * { + vertical-align: middle; + } + + .mx_Checkbox { + position: absolute; + right: 0; + top: calc(50% - 8px); // checkbox is 16px high + width: 16px; // to force a square + } + + .mx_PrototypeCommunityInviteDialog_personIdentifiers { + display: inline-block; + + & > * { + display: block; + } + + .mx_PrototypeCommunityInviteDialog_personName { + font-weight: 600; + font-size: $font-14px; + color: $primary-fg-color; + margin-left: 7px; + } + + .mx_PrototypeCommunityInviteDialog_personId { + font-size: $font-12px; + color: $muted-fg-color; + margin-left: 7px; + } + } + } + + .mx_PrototypeCommunityInviteDialog_primaryButton { + display: block; + font-size: $font-13px; + line-height: 20px; + height: 20px; + margin-top: 24px; + } + } +} diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 839d677069..3347a8288d 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -23,6 +23,7 @@ import Modal from './Modal'; import * as sdk from './'; import { _t } from './languageHandler'; import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; +import PrototypeCommunityInviteDialog from "./components/views/dialogs/PrototypeCommunityInviteDialog"; /** * Invites multiple addresses to a room @@ -56,6 +57,13 @@ export function showRoomInviteDialog(roomId) { ); } +export function showCommunityRoomInviteDialog(roomId, communityName) { + Modal.createTrackedDialog( + 'Invite Users to Community', '', PrototypeCommunityInviteDialog, {communityName, roomId}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, + ); +} + /** * Checks if the given MatrixEvent is a valid 3rd party user invite. * @param {MatrixEvent} event The event to check @@ -77,7 +85,7 @@ export function isValid3pidInvite(event) { export function inviteUsersToRoom(roomId, userIds) { return inviteMultipleToRoom(roomId, userIds).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); - return _showAnyInviteErrors(result.states, room, result.inviter); + showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { console.error(err.stack); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -88,7 +96,7 @@ export function inviteUsersToRoom(roomId, userIds) { }); } -function _showAnyInviteErrors(addrs, room, inviter) { +export function showAnyInviteErrors(addrs, room, inviter) { // Show user any errors const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); if (failedUsers.length === 1 && inviter.fatal) { @@ -100,6 +108,7 @@ function _showAnyInviteErrors(addrs, room, inviter) { title: _t("Failed to invite users to the room:", {roomName: room.name}), description: inviter.getErrorText(failedUsers[0]), }); + return false; } else { const errorList = []; for (const addr of failedUsers) { @@ -118,8 +127,9 @@ function _showAnyInviteErrors(addrs, room, inviter) { title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), description, }); + return false; } } - return addrs; + return true; } diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index c90811ed5a..6cd0b22505 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -327,7 +327,7 @@ export default class InviteDialog extends React.PureComponent { this.state = { targets: [], // array of Member objects (see interface above) filterText: "", - recents: this._buildRecents(alreadyInvited), + recents: InviteDialog.buildRecents(alreadyInvited), numRecentsShown: INITIAL_ROOMS_SHOWN, suggestions: this._buildSuggestions(alreadyInvited), numSuggestionsShown: INITIAL_ROOMS_SHOWN, @@ -344,7 +344,7 @@ export default class InviteDialog extends React.PureComponent { this._editorRef = createRef(); } - _buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { + static buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the diff --git a/src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx b/src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx new file mode 100644 index 0000000000..08d3f0208a --- /dev/null +++ b/src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx @@ -0,0 +1,252 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, { ChangeEvent, FormEvent } from 'react'; +import BaseDialog from "./BaseDialog"; +import { _t } from "../../../languageHandler"; +import { IDialogProps } from "./IDialogProps"; +import Field from "../elements/Field"; +import AccessibleButton from "../elements/AccessibleButton"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import InfoTooltip from "../elements/InfoTooltip"; +import dis from "../../../dispatcher/dispatcher"; +import {showCommunityRoomInviteDialog} from "../../../RoomInvite"; +import { arrayFastClone } from "../../../utils/arrays"; +import SdkConfig from "../../../SdkConfig"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import InviteDialog from "./InviteDialog"; +import BaseAvatar from "../avatars/BaseAvatar"; +import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; +import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite"; +import {humanizeTime} from "../../../utils/humanize"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import Modal from "../../../Modal"; +import ErrorDialog from "./ErrorDialog"; + +interface IProps extends IDialogProps { + roomId: string; + communityName: string; +} + +interface IPerson { + userId: string; + user: RoomMember; + lastActive: number; +} + +interface IState { + emailTargets: string[]; + userTargets: string[]; + showPeople: boolean; + people: IPerson[]; + numPeople: number; + busy: boolean; +} + +export default class PrototypeCommunityInviteDialog extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.state = { + emailTargets: [], + userTargets: [], + showPeople: false, + people: this.buildSuggestions(), + numPeople: 5, // arbitrary default + busy: false, + }; + } + + private buildSuggestions(): IPerson[] { + const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]); + if (this.props.roomId) { + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + if (!room) throw new Error("Room ID given to InviteDialog does not look like a room"); + room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId)); + room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId)); + // add banned users, so we don't try to invite them + room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId)); + } + + return InviteDialog.buildRecents(alreadyInvited); + } + + private onSubmit = async (ev: FormEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({busy: true}); + try { + const targets = [...this.state.emailTargets, ...this.state.userTargets]; + const result = await inviteMultipleToRoom(this.props.roomId, targets); + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + const success = showAnyInviteErrors(result.states, room, result.inviter); + if (success) { + this.props.onFinished(true); + } else { + this.setState({busy: false}); + } + } catch (e) { + this.setState({busy: false}); + console.error(e); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t("Failed to invite"), + description: ((e && e.message) ? e.message : _t("Operation failed")), + }); + } + }; + + private onAddressChange = (ev: ChangeEvent, index: number) => { + const targets = arrayFastClone(this.state.emailTargets); + if (index >= targets.length) { + targets.push(ev.target.value); + } else { + targets[index] = ev.target.value; + } + this.setState({emailTargets: targets}); + }; + + private onAddressBlur = (index: number) => { + const targets = arrayFastClone(this.state.emailTargets); + if (index >= targets.length) return; // not important + if (targets[index].trim() === "") { + targets.splice(index, 1); + this.setState({emailTargets: targets}); + } + }; + + private onShowPeopleClick = () => { + this.setState({showPeople: !this.state.showPeople}); + }; + + private setPersonToggle = (person: IPerson, selected: boolean) => { + const targets = arrayFastClone(this.state.userTargets); + if (selected && !targets.includes(person.userId)) { + targets.push(person.userId); + } else if (!selected && targets.includes(person.userId)) { + targets.splice(targets.indexOf(person.userId), 1); + } + this.setState({userTargets: targets}); + }; + + private renderPerson(person: IPerson, key: any) { + const avatarSize = 36; + return ( +
+ +
+ {person.user.name} + {person.userId} +
+ this.setPersonToggle(person, e.target.checked)} /> +
+ ); + } + + private onShowMorePeople = () => { + this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase + }; + + public render() { + const emailAddresses = []; + this.state.emailTargets.forEach((address, i) => { + emailAddresses.push( + this.onAddressChange(e, i)} + label={_t("Email address")} + placeholder={_t("Email address")} + onBlur={() => this.onAddressBlur(i)} + /> + ); + }); + + // Push a clean input + emailAddresses.push( + this.onAddressChange(e, emailAddresses.length)} + label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} + placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} + /> + ); + + let peopleIntro = null; + let people = []; + if (this.state.showPeople) { + const humansToPresent = this.state.people.slice(0, this.state.numPeople); + humansToPresent.forEach((person, i) => { + people.push(this.renderPerson(person, i)); + }); + if (humansToPresent.length < this.state.people.length) { + people.push( + {_t("Show more")} + ); + } + } + if (this.state.people.length > 0) { + peopleIntro = ( +
+ {_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})} + + {this.state.showPeople ? _t("Hide") : _t("Show")} + +
+ ); + } + + let buttonText = _t("Skip"); + const targetCount = this.state.userTargets.length + this.state.emailTargets.length; + if (targetCount > 0) { + buttonText = _t("Send %(count)s invites", {count: targetCount}); + } + + return ( + +
+
+ {emailAddresses} + {peopleIntro} + {people} + {buttonText} +
+
+
+ ); + } +} diff --git a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx index 7427b2737c..8978b9cf0d 100644 --- a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx +++ b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx @@ -23,6 +23,7 @@ import AccessibleButton from "../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import InfoTooltip from "../elements/InfoTooltip"; import dis from "../../../dispatcher/dispatcher"; +import {showCommunityRoomInviteDialog} from "../../../RoomInvite"; interface IProps extends IDialogProps { } @@ -67,7 +68,7 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent:%(domain)s": "Community ID: +:%(domain)s", "Use this when referencing your community to others. The community ID cannot be changed.": "Use this when referencing your community to others. The community ID cannot be changed.", @@ -1783,9 +1792,7 @@ "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.", "Verification Pending": "Verification Pending", "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.", - "Email address": "Email address", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", - "Skip": "Skip", "A username can only contain lower case letters, numbers and '=_-./'": "A username can only contain lower case letters, numbers and '=_-./'", "Username not available": "Username not available", "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s", @@ -1898,7 +1905,6 @@ "Set status": "Set status", "Set a new status...": "Set a new status...", "View Community": "View Community", - "Hide": "Hide", "Reload": "Reload", "Take picture": "Take picture", "Remove for everyone": "Remove for everyone", From 5ea11e90b2d5289619b42390b439605832067fee Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Aug 2020 21:17:35 -0600 Subject: [PATCH 07/34] Appease the style linter --- res/css/views/dialogs/_PrototypeCreateGroupDialog.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss b/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss index a0f505b1ff..3235575a88 100644 --- a/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss +++ b/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss @@ -74,7 +74,7 @@ limitations under the License. } .mx_PrototypeCreateGroupDialog_placeholderAvatar { - background-color: #368BD6; // hardcoded for both themes + background-color: #368bd6; // hardcoded for both themes &::before { display: inline-block; @@ -92,7 +92,7 @@ limitations under the License. } .mx_PrototypeCreateGroupDialog_tip { - &>b, &>span { + & > b, & > span { display: block; color: $muted-fg-color; } From c9e967f05cb3abd9884064bcfb5f7dabaae8f0b7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Aug 2020 14:23:18 -0600 Subject: [PATCH 08/34] Don't override UserMenu with group changes --- src/components/structures/UserMenu.tsx | 43 ++------------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 839d4bccda..30be71abcb 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -42,9 +42,6 @@ import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../views/context_menus/IconizedContextMenu"; -import TagOrderStore from "../../stores/TagOrderStore"; -import * as fbEmitter from "fbemitter"; -import FlairStore from "../../stores/FlairStore"; interface IProps { isMinimized: boolean; @@ -55,16 +52,11 @@ type PartialDOMRect = Pick; interface IState { contextMenuPosition: PartialDOMRect; isDarkTheme: boolean; - selectedCommunityProfile: { - displayName: string; - avatarMxc: string; - }; } export default class UserMenu extends React.Component { private dispatcherRef: string; private themeWatcherRef: string; - private tagStoreRef: fbEmitter.EventSubscription; private buttonRef: React.RefObject = createRef(); constructor(props: IProps) { @@ -73,7 +65,6 @@ export default class UserMenu extends React.Component { this.state = { contextMenuPosition: null, isDarkTheme: this.isUserOnDarkTheme(), - selectedCommunityProfile: null, }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); @@ -86,7 +77,6 @@ export default class UserMenu extends React.Component { public componentDidMount() { this.dispatcherRef = defaultDispatcher.register(this.onAction); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); - this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate); } public componentWillUnmount() { @@ -103,25 +93,6 @@ export default class UserMenu extends React.Component { return theme === "dark"; } - private onTagStoreUpdate = async () => { - if (!SettingsStore.getValue("feature_communities_v2_prototypes")) { - return; - } - - const selectedId = TagOrderStore.getSelectedTags()[0]; - if (!selectedId) { - this.setState({selectedCommunityProfile: null}); - return; - } - - // For some reason the group's profile info isn't on the js-sdk Group object but - // is in the flair store, so get it from there. - const profile = await FlairStore.getGroupProfileCached(MatrixClientPeg.get(), selectedId); - const displayName = profile.name || selectedId; - const avatarMxc = profile.avatarUrl; - this.setState({selectedCommunityProfile: {displayName, avatarMxc}}); - }; - private onProfileUpdate = async () => { // the store triggered an update, so force a layout update. We don't // have any state to store here for that to magically happen. @@ -324,18 +295,8 @@ export default class UserMenu extends React.Component { public render() { const avatarSize = 32; // should match border-radius of the avatar - let displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId(); - let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); - - if (this.state.selectedCommunityProfile) { - displayName = this.state.selectedCommunityProfile.displayName - const mxc = this.state.selectedCommunityProfile.avatarMxc; - if (mxc) { - avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(mxc, avatarSize, avatarSize); - } else { - avatarUrl = null; - } - } + const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId(); + const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); let name = {displayName}; let buttons = ( From 20c562c2086fed80fd4dfa111c4e7e7f06507e85 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Aug 2020 21:45:38 -0600 Subject: [PATCH 09/34] Change user avatar to a home icon --- res/css/structures/_TagPanel.scss | 44 +++++++++++++++++-- res/img/element-icons/home.svg | 3 ++ src/components/views/elements/TagTile.js | 4 +- src/components/views/elements/UserTagTile.tsx | 31 ++++--------- 4 files changed, 54 insertions(+), 28 deletions(-) create mode 100644 res/img/element-icons/home.svg diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 6c85341aaf..e7f67c8ace 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -75,6 +75,7 @@ limitations under the License. .mx_TagPanel .mx_TagTile { // opacity: 0.5; position: relative; + padding: 3px; } .mx_TagPanel .mx_TagTile:focus, .mx_TagPanel .mx_TagTile:hover, @@ -82,6 +83,45 @@ limitations under the License. // opacity: 1; } +.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype { + background-color: $primary-bg-color; + border-radius: 6px; +} + +.mx_TagTile_selected_prototype { + .mx_TagTile_homeIcon::before { + background-color: $primary-fg-color; // dark-on-light + } +} + +.mx_TagTile:not(.mx_TagTile_selected_prototype) .mx_TagTile_homeIcon { + background-color: $icon-button-color; // XXX: Variable abuse + border-radius: 48px; + + &::before { + background-color: $primary-bg-color; // light-on-grey + } +} + +.mx_TagTile_homeIcon { + width: 32px; + height: 32px; + position: relative; + + &::before { + mask-image: url('$(res)/img/element-icons/home.svg'); + mask-position: center; + mask-repeat: no-repeat; + width: 21px; + height: 21px; + content: ''; + display: inline-block; + position: absolute; + top: calc(50% - 10.5px); + left: calc(50% - 10.5px); + } +} + .mx_TagPanel .mx_TagTile_plus { margin-bottom: 12px; height: 32px; @@ -116,10 +156,6 @@ limitations under the License. border-radius: 0 3px 3px 0; } -.mx_TagPanel .mx_TagTile.mx_TagTile_large.mx_TagTile_selected::before { - left: -10px; -} - .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { filter: none; } diff --git a/res/img/element-icons/home.svg b/res/img/element-icons/home.svg new file mode 100644 index 0000000000..d65812cafd --- /dev/null +++ b/res/img/element-icons/home.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index 49b336a577..562a478976 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -141,9 +141,11 @@ export default createReactClass({ profile.avatarUrl, avatarHeight, avatarHeight, "crop", ) : null; + const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes"); const className = classNames({ mx_TagTile: true, - mx_TagTile_selected: this.props.selected, + mx_TagTile_selected: this.props.selected && !isPrototype, + mx_TagTile_selected_prototype: this.props.selected && isPrototype, }); const badge = TagOrderStore.getGroupBadge(this.props.tag); diff --git a/src/components/views/elements/UserTagTile.tsx b/src/components/views/elements/UserTagTile.tsx index c652423753..635c537324 100644 --- a/src/components/views/elements/UserTagTile.tsx +++ b/src/components/views/elements/UserTagTile.tsx @@ -16,16 +16,14 @@ limitations under the License. import React from "react"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { OwnProfileStore } from "../../../stores/OwnProfileStore"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import * as fbEmitter from "fbemitter"; import TagOrderStore from "../../../stores/TagOrderStore"; import AccessibleTooltipButton from "./AccessibleTooltipButton"; -import BaseAvatar from "../avatars/BaseAvatar"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import classNames from "classnames"; +import { _t } from "../../../languageHandler"; -interface IProps{} +interface IProps { +} interface IState { selected: boolean; @@ -43,18 +41,13 @@ export default class UserTagTile extends React.PureComponent { } public componentDidMount() { - OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate); } public componentWillUnmount() { - OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); + this.tagStoreRef.remove(); } - private onProfileUpdate = () => { - this.forceUpdate(); - }; - private onTagStoreUpdate = () => { const selected = TagOrderStore.getSelectedTags().length === 0; this.setState({selected}); @@ -71,27 +64,19 @@ export default class UserTagTile extends React.PureComponent { public render() { // XXX: We reuse TagTile classes for ease of demonstration - we should probably generify // TagTile instead if we continue to use this component. - const avatarHeight = 36; - const name = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId(); const className = classNames({ mx_TagTile: true, - mx_TagTile_selected: this.state.selected, - mx_TagTile_large: true, + mx_TagTile_selected_prototype: this.state.selected, + mx_TagTile_home: true, }); return (
- +
); From 55001f319327d16acbbed92ecdd1da023bd3c52b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Aug 2020 21:52:48 -0600 Subject: [PATCH 10/34] Select the general chat for a community when viewing it --- src/stores/TagOrderStore.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 2b72a963b0..f02fce0769 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -166,6 +166,25 @@ class TagOrderStore extends Store { selectedTags: newTags, }); + if (!allowMultiple && newTags.length === 1) { + // We're in prototype behaviour: select the general chat for the community + const rooms = GroupStore.getGroupRooms(newTags[0]) + .map(r => MatrixClientPeg.get().getRoom(r.roomId)) + .filter(r => !!r); + let chat = rooms.find(r => { + const idState = r.currentState.getStateEvents("im.vector.general_chat", ""); + if (!idState || idState.getContent()['groupId'] !== newTags[0]) return false; + return true; + }); + if (!chat) chat = rooms[0]; + if (chat) { + dis.dispatch({ + action: 'view_room', + room_id: chat.roomId, + }); + } + } + Analytics.trackEvent('FilterStore', 'select_tag'); } break; From c68636bd32c4d2bae47d8efc5d7020542232f48a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Aug 2020 21:59:09 -0600 Subject: [PATCH 11/34] i18n updates --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c12b57c033..1fed9e0619 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1555,6 +1555,7 @@ "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", + "Home": "Home", "Enter a server name": "Enter a server name", "Looks good": "Looks good", "Can't find this server or its room list": "Can't find this server or its room list", @@ -2097,7 +2098,6 @@ "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", - "Home": "Home", "Switch to light mode": "Switch to light mode", "Switch to dark mode": "Switch to dark mode", "Switch theme": "Switch theme", From 0c67a42b0f16283bb7354b5868246240d308feeb Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Aug 2020 08:42:26 -0600 Subject: [PATCH 12/34] Make padding only on the prototype tag panel --- res/css/structures/_TagPanel.scss | 4 ++++ src/components/views/elements/TagTile.js | 1 + src/components/views/elements/UserTagTile.tsx | 1 + 3 files changed, 6 insertions(+) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index e7f67c8ace..2683a32dae 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -75,8 +75,12 @@ limitations under the License. .mx_TagPanel .mx_TagTile { // opacity: 0.5; position: relative; +} + +.mx_TagPanel .mx_TagTile.mx_TagTile_prototype { padding: 3px; } + .mx_TagPanel .mx_TagTile:focus, .mx_TagPanel .mx_TagTile:hover, .mx_TagPanel .mx_TagTile.mx_TagTile_selected { diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index 562a478976..db5eedc274 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -144,6 +144,7 @@ export default createReactClass({ const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes"); const className = classNames({ mx_TagTile: true, + mx_TagTile_prototype: isPrototype, mx_TagTile_selected: this.props.selected && !isPrototype, mx_TagTile_selected_prototype: this.props.selected && isPrototype, }); diff --git a/src/components/views/elements/UserTagTile.tsx b/src/components/views/elements/UserTagTile.tsx index 635c537324..912f54edc7 100644 --- a/src/components/views/elements/UserTagTile.tsx +++ b/src/components/views/elements/UserTagTile.tsx @@ -66,6 +66,7 @@ export default class UserTagTile extends React.PureComponent { // TagTile instead if we continue to use this component. const className = classNames({ mx_TagTile: true, + mx_TagTile_prototype: true, mx_TagTile_selected_prototype: this.state.selected, mx_TagTile_home: true, }); From 3e7d82b42137febf3961a87d9df3e3def3ee9760 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Aug 2020 08:48:01 -0600 Subject: [PATCH 13/34] Change tip copy --- src/components/views/dialogs/PrototypeCreateGroupDialog.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx index 8978b9cf0d..d37301b573 100644 --- a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx +++ b/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx @@ -206,7 +206,7 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent
- {_t("PRO TIP")} + {_t("Add image (optional)")} {_t("An image will help people identify your community.")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b42a5fadd3..c24a5537a3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1744,7 +1744,7 @@ "You can change this later if needed.": "You can change this later if needed.", "What's the name of your community or team?": "What's the name of your community or team?", "Enter name": "Enter name", - "PRO TIP": "PRO TIP", + "Add image (optional)": "Add image (optional)", "An image will help people identify your community.": "An image will help people identify your community.", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", From 82b015bd5f84b2c8ecaee14d572c6f1b8a3b7657 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Aug 2020 08:50:32 -0600 Subject: [PATCH 14/34] Rename components to match prior convention --- res/css/_components.scss | 4 +-- ...s => _CommunityPrototypeInviteDialog.scss} | 16 ++++----- ...s => _CreateCommunityPrototypeDialog.scss} | 22 ++++++------ src/RoomInvite.js | 4 +-- src/components/structures/MatrixChat.tsx | 4 +-- ...tsx => CommunityPrototypeInviteDialog.tsx} | 18 +++++----- ...tsx => CreateCommunityPrototypeDialog.tsx} | 24 ++++++------- src/i18n/strings/en_EN.json | 36 +++++++++---------- 8 files changed, 64 insertions(+), 64 deletions(-) rename res/css/views/dialogs/{_PrototypeCommunityInviteDialog.scss => _CommunityPrototypeInviteDialog.scss} (82%) rename res/css/views/dialogs/{_PrototypeCreateGroupDialog.scss => _CreateCommunityPrototypeDialog.scss} (79%) rename src/components/views/dialogs/{PrototypeCommunityInviteDialog.tsx => CommunityPrototypeInviteDialog.tsx} (94%) rename src/components/views/dialogs/{PrototypeCreateGroupDialog.tsx => CreateCommunityPrototypeDialog.tsx} (88%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 88fbbb5c3e..24d2ffa2b0 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -61,7 +61,9 @@ @import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; +@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; +@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @@ -72,8 +74,6 @@ @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; -@import "./views/dialogs/_PrototypeCommunityInviteDialog.scss"; -@import "./views/dialogs/_PrototypeCreateGroupDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; diff --git a/res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss similarity index 82% rename from res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss rename to res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss index 8d2ff598d8..beae03f00f 100644 --- a/res/css/views/dialogs/_PrototypeCommunityInviteDialog.scss +++ b/res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_PrototypeCommunityInviteDialog { +.mx_CommunityPrototypeInviteDialog { &.mx_Dialog_fixedWidth { width: 360px; } @@ -22,7 +22,7 @@ limitations under the License. .mx_Dialog_content { margin-bottom: 0; - .mx_PrototypeCommunityInviteDialog_people { + .mx_CommunityPrototypeInviteDialog_people { position: relative; margin-bottom: 4px; @@ -36,11 +36,11 @@ limitations under the License. } } - .mx_PrototypeCommunityInviteDialog_morePeople { + .mx_CommunityPrototypeInviteDialog_morePeople { margin-top: 8px; } - .mx_PrototypeCommunityInviteDialog_person { + .mx_CommunityPrototypeInviteDialog_person { position: relative; margin-top: 4px; @@ -55,21 +55,21 @@ limitations under the License. width: 16px; // to force a square } - .mx_PrototypeCommunityInviteDialog_personIdentifiers { + .mx_CommunityPrototypeInviteDialog_personIdentifiers { display: inline-block; & > * { display: block; } - .mx_PrototypeCommunityInviteDialog_personName { + .mx_CommunityPrototypeInviteDialog_personName { font-weight: 600; font-size: $font-14px; color: $primary-fg-color; margin-left: 7px; } - .mx_PrototypeCommunityInviteDialog_personId { + .mx_CommunityPrototypeInviteDialog_personId { font-size: $font-12px; color: $muted-fg-color; margin-left: 7px; @@ -77,7 +77,7 @@ limitations under the License. } } - .mx_PrototypeCommunityInviteDialog_primaryButton { + .mx_CommunityPrototypeInviteDialog_primaryButton { display: block; font-size: $font-13px; line-height: 20px; diff --git a/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss similarity index 79% rename from res/css/views/dialogs/_PrototypeCreateGroupDialog.scss rename to res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss index 3235575a88..81babc4c38 100644 --- a/res/css/views/dialogs/_PrototypeCreateGroupDialog.scss +++ b/res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_PrototypeCreateGroupDialog { +.mx_CreateCommunityPrototypeDialog { .mx_Dialog_content { display: flex; flex-direction: row; margin-bottom: 12px; - .mx_PrototypeCreateGroupDialog_colName { + .mx_CreateCommunityPrototypeDialog_colName { flex-basis: 66.66%; padding-right: 100px; @@ -29,7 +29,7 @@ limitations under the License. line-height: $font-20px; } - .mx_PrototypeCreateGroupDialog_subtext { + .mx_CreateCommunityPrototypeDialog_subtext { display: block; color: $muted-fg-color; margin-bottom: 16px; @@ -38,12 +38,12 @@ limitations under the License. margin-top: 16px; } - &.mx_PrototypeCreateGroupDialog_subtext_error { + &.mx_CreateCommunityPrototypeDialog_subtext_error { color: $warning-color; } } - .mx_PrototypeCreateGroupDialog_communityId { + .mx_CreateCommunityPrototypeDialog_communityId { position: relative; .mx_InfoTooltip { @@ -59,21 +59,21 @@ limitations under the License. } } - .mx_PrototypeCreateGroupDialog_colAvatar { + .mx_CreateCommunityPrototypeDialog_colAvatar { flex-basis: 33.33%; - .mx_PrototypeCreateGroupDialog_avatarContainer { + .mx_CreateCommunityPrototypeDialog_avatarContainer { margin-top: 12px; margin-bottom: 20px; - .mx_PrototypeCreateGroupDialog_avatar, - .mx_PrototypeCreateGroupDialog_placeholderAvatar { + .mx_CreateCommunityPrototypeDialog_avatar, + .mx_CreateCommunityPrototypeDialog_placeholderAvatar { width: 96px; height: 96px; border-radius: 96px; } - .mx_PrototypeCreateGroupDialog_placeholderAvatar { + .mx_CreateCommunityPrototypeDialog_placeholderAvatar { background-color: #368bd6; // hardcoded for both themes &::before { @@ -91,7 +91,7 @@ limitations under the License. } } - .mx_PrototypeCreateGroupDialog_tip { + .mx_CreateCommunityPrototypeDialog_tip { & > b, & > span { display: block; color: $muted-fg-color; diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 3347a8288d..420561ea41 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -23,7 +23,7 @@ import Modal from './Modal'; import * as sdk from './'; import { _t } from './languageHandler'; import {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; -import PrototypeCommunityInviteDialog from "./components/views/dialogs/PrototypeCommunityInviteDialog"; +import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; /** * Invites multiple addresses to a room @@ -59,7 +59,7 @@ export function showRoomInviteDialog(roomId) { export function showCommunityRoomInviteDialog(roomId, communityName) { Modal.createTrackedDialog( - 'Invite Users to Community', '', PrototypeCommunityInviteDialog, {communityName, roomId}, + 'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, ); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 2b764d00c9..9d51062b7d 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -77,7 +77,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { SettingLevel } from "../../settings/SettingLevel"; import { leaveRoomBehaviour } from "../../utils/membership"; -import PrototypeCreateGroupDialog from "../views/dialogs/PrototypeCreateGroupDialog"; +import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog"; /** constants for MatrixChat.state.view */ export enum Views { @@ -623,7 +623,7 @@ export default class MatrixChat extends React.PureComponent { case 'view_create_group': { let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog") if (SettingsStore.getValue("feature_communities_v2_prototypes")) { - CreateGroupDialog = PrototypeCreateGroupDialog; + CreateGroupDialog = CreateCommunityPrototypeDialog; } Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); break; diff --git a/src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx similarity index 94% rename from src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx rename to src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index 08d3f0208a..7a500cd053 100644 --- a/src/components/views/dialogs/PrototypeCommunityInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -56,7 +56,7 @@ interface IState { busy: boolean; } -export default class PrototypeCommunityInviteDialog extends React.PureComponent { +export default class CommunityPrototypeInviteDialog extends React.PureComponent { constructor(props: IProps) { super(props); @@ -145,7 +145,7 @@ export default class PrototypeCommunityInviteDialog extends React.PureComponent< private renderPerson(person: IPerson, key: any) { const avatarSize = 36; return ( -
+
-
- {person.user.name} - {person.userId} +
+ {person.user.name} + {person.userId}
this.setPersonToggle(person, e.target.checked)} />
@@ -206,14 +206,14 @@ export default class PrototypeCommunityInviteDialog extends React.PureComponent< {_t("Show more")} ); } } if (this.state.people.length > 0) { peopleIntro = ( -
+
{_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})} {this.state.showPeople ? _t("Hide") : _t("Show")} @@ -230,7 +230,7 @@ export default class PrototypeCommunityInviteDialog extends React.PureComponent< return ( @@ -242,7 +242,7 @@ export default class PrototypeCommunityInviteDialog extends React.PureComponent< {buttonText}
diff --git a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx similarity index 88% rename from src/components/views/dialogs/PrototypeCreateGroupDialog.tsx rename to src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx index d37301b573..5f8321fd7d 100644 --- a/src/components/views/dialogs/PrototypeCreateGroupDialog.tsx +++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx @@ -37,7 +37,7 @@ interface IState { avatarPreview: string; } -export default class PrototypeCreateGroupDialog extends React.PureComponent { +export default class CreateCommunityPrototypeDialog extends React.PureComponent { private avatarUploadRef: React.RefObject = React.createRef(); constructor(props: IProps) { @@ -138,7 +138,7 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent + {_t("Community ID: +:%(domain)s", { domain: MatrixClientPeg.getHomeserverName(), }, { @@ -155,32 +155,32 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent + {_t("You can change this later if needed.")} ); if (this.state.error) { helpText = ( - + {this.state.error} ); } - let preview = ; + let preview = ; if (!this.state.avatarPreview) { - preview =
+ preview =
} return (
-
+
{helpText} - + {/*nbsp is to reserve the height of this element when there's nothing*/}  {communityId} @@ -196,16 +196,16 @@ export default class PrototypeCreateGroupDialog extends React.PureComponent
-
+
- + {preview} -
+
{_t("Add image (optional)")} {_t("An image will help people identify your community.")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c24a5537a3..f3cd1d80b7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1598,6 +1598,15 @@ "Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s", "Unavailable": "Unavailable", "Changelog": "Changelog", + "Email address": "Email address", + "Add another email": "Add another email", + "People you know on %(brand)s": "People you know on %(brand)s", + "Hide": "Hide", + "Show": "Show", + "Skip": "Skip", + "Send %(count)s invites|other": "Send %(count)s invites", + "Send %(count)s invites|one": "Send %(count)s invite", + "Invite people to join %(communityName)s": "Invite people to join %(communityName)s", "You cannot delete this message. (%(code)s)": "You cannot delete this message. (%(code)s)", "Removing…": "Removing…", "Destroy cross-signing keys?": "Destroy cross-signing keys?", @@ -1608,6 +1617,15 @@ "Clear all data in this session?": "Clear all data in this session?", "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.": "Clearing all data from this session is permanent. Encrypted messages will be lost unless their keys have been backed up.", "Clear all data": "Clear all data", + "There was an error creating your community. The name may be taken or the server is unable to process your request.": "There was an error creating your community. The name may be taken or the server is unable to process your request.", + "Community ID: +:%(domain)s": "Community ID: +:%(domain)s", + "Use this when referencing your community to others. The community ID cannot be changed.": "Use this when referencing your community to others. The community ID cannot be changed.", + "You can change this later if needed.": "You can change this later if needed.", + "What's the name of your community or team?": "What's the name of your community or team?", + "Enter name": "Enter name", + "Create": "Create", + "Add image (optional)": "Add image (optional)", + "An image will help people identify your community.": "An image will help people identify your community.", "Community IDs cannot be empty.": "Community IDs cannot be empty.", "Community IDs may only contain characters a-z, 0-9, or '=_-./'": "Community IDs may only contain characters a-z, 0-9, or '=_-./'", "Something went wrong whilst creating your community": "Something went wrong whilst creating your community", @@ -1616,7 +1634,6 @@ "Example": "Example", "Community ID": "Community ID", "example": "example", - "Create": "Create", "Please enter a name for the room": "Please enter a name for the room", "Set a room address to easily share your room with other people.": "Set a room address to easily share your room with other people.", "This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.", @@ -1729,23 +1746,6 @@ "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", "This wasn't me": "This wasn't me", - "Email address": "Email address", - "Add another email": "Add another email", - "People you know on %(brand)s": "People you know on %(brand)s", - "Hide": "Hide", - "Show": "Show", - "Skip": "Skip", - "Send %(count)s invites|other": "Send %(count)s invites", - "Send %(count)s invites|one": "Send %(count)s invite", - "Invite people to join %(communityName)s": "Invite people to join %(communityName)s", - "There was an error creating your community. The name may be taken or the server is unable to process your request.": "There was an error creating your community. The name may be taken or the server is unable to process your request.", - "Community ID: +:%(domain)s": "Community ID: +:%(domain)s", - "Use this when referencing your community to others. The community ID cannot be changed.": "Use this when referencing your community to others. The community ID cannot be changed.", - "You can change this later if needed.": "You can change this later if needed.", - "What's the name of your community or team?": "What's the name of your community or team?", - "Enter name": "Enter name", - "Add image (optional)": "Add image (optional)", - "An image will help people identify your community.": "An image will help people identify your community.", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback", From fd71bca7c0af97ab496a3d320900498256af0dc4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Aug 2020 10:33:05 -0600 Subject: [PATCH 15/34] Change menu label if in a community --- src/components/views/rooms/RoomList.tsx | 5 ++++- src/i18n/strings/en_EN.json | 1 + src/stores/TagOrderStore.js | 7 +++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 3274e0e49f..92c5982276 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -45,6 +45,7 @@ import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays"; import { objectShallowClone, objectWithOnly } from "../../../utils/objects"; import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu"; import AccessibleButton from "../elements/AccessibleButton"; +import TagOrderStore from "../../../stores/TagOrderStore"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -129,7 +130,9 @@ const TAG_AESTHETICS: { }} /> { e.preventDefault(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d6ba736a76..da4e298f0f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1121,6 +1121,7 @@ "Rooms": "Rooms", "Add room": "Add room", "Create new room": "Create new room", + "Explore community rooms": "Explore community rooms", "Explore public rooms": "Explore public rooms", "Low priority": "Low priority", "System Alerts": "System Alerts", diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index f02fce0769..2eb35e6dc2 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -285,6 +285,13 @@ class TagOrderStore extends Store { getSelectedTags() { return this._state.selectedTags; } + + getSelectedPrototypeTag() { + if (SettingsStore.getValue("feature_communities_v2_prototypes")) { + return this.getSelectedTags()[0]; + } + return null; // no selection as far as this function is concerned + } } if (global.singletonTagOrderStore === undefined) { From c28134eb11bb3f1fab540c83cf9a512d1209f3e0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Aug 2020 10:53:06 -0600 Subject: [PATCH 16/34] Associate created rooms with the selected community --- src/components/views/dialogs/CreateRoomDialog.js | 13 ++++++++++++- src/createRoom.ts | 6 ++++++ src/i18n/strings/en_EN.json | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index ce7ac6e59c..9726c44fac 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -25,6 +25,8 @@ import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {Key} from "../../../Keyboard"; import {privateShouldBeEncrypted} from "../../../createRoom"; +import TagOrderStore from "../../../stores/TagOrderStore"; +import GroupStore from "../../../stores/GroupStore"; export default createReactClass({ displayName: 'CreateRoomDialog', @@ -70,6 +72,10 @@ export default createReactClass({ opts.encryption = this.state.isEncrypted; } + if (TagOrderStore.getSelectedPrototypeTag()) { + opts.associatedWithCommunity = TagOrderStore.getSelectedPrototypeTag(); + } + return opts; }, @@ -212,7 +218,12 @@ export default createReactClass({ ; } - const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + if (TagOrderStore.getSelectedPrototypeTag()) { + const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag()); + const name = summary?.profile?.name || TagOrderStore.getSelectedPrototypeTag(); + title = _t("Create a room in %(communityName)s", {communityName: name}); + } return ( { } else { return Promise.resolve(); } + }).then(() => { + if (opts.associatedWithCommunity) { + return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false); + } }).then(function() { // NB createRoom doesn't block on the client seeing the echo that the // room has been created, so we race here with the client knowing that diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index da4e298f0f..0ec12a4d6a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1643,6 +1643,7 @@ "Enable end-to-end encryption": "Enable end-to-end encryption", "Create a public room": "Create a public room", "Create a private room": "Create a private room", + "Create a room in %(communityName)s": "Create a room in %(communityName)s", "Name": "Name", "Topic (optional)": "Topic (optional)", "Make this room public": "Make this room public", From 027f263589a767830120b18b83829d8ca8c42bab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Aug 2020 11:01:58 -0600 Subject: [PATCH 17/34] Remove prototype code from CreateGroupDialog The prototype code paths prevent users from ending up here, so we don't need custom code. --- .../views/dialogs/CreateGroupDialog.js | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 2b22054947..10285ccee0 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -83,25 +83,11 @@ export default createReactClass({ localpart: this.state.groupId, profile: profile, }).then((result) => { - if (result.room_id) { - dis.dispatch({ - action: 'view_room', - room_id: result.room_id, - }); - - // Ensure the tag gets selected now that we've created it - dis.dispatch({action: 'deselect_tags'}, true); - dis.dispatch({ - action: 'select_tag', - tag: result.group_id, - }); - } else { - dis.dispatch({ - action: 'view_group', - group_id: result.group_id, - group_is_new: true, - }); - } + dis.dispatch({ + action: 'view_group', + group_id: result.group_id, + group_is_new: true, + }); this.props.onFinished(true); }).catch((e) => { this.setState({createError: e}); From 4f29770adb8880e451067d58574b6d1f06e02de1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Aug 2020 11:02:14 -0600 Subject: [PATCH 18/34] Force the GroupStore to update after creating a prototype community --- .../views/dialogs/CreateCommunityPrototypeDialog.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx index 5f8321fd7d..58412c23d6 100644 --- a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx @@ -24,6 +24,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg"; import InfoTooltip from "../elements/InfoTooltip"; import dis from "../../../dispatcher/dispatcher"; import {showCommunityRoomInviteDialog} from "../../../RoomInvite"; +import GroupStore from "../../../stores/GroupStore"; interface IProps extends IDialogProps { } @@ -92,6 +93,8 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent< this.props.onFinished(true); if (result.room_id) { + // Force the group store to update as it might have missed the general chat + await GroupStore.refreshGroupRooms(result.group_id); dis.dispatch({ action: 'view_room', room_id: result.room_id, From 4a807f9385fd3e755b46b897761f77cac1107e63 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 27 Aug 2020 13:41:03 +0100 Subject: [PATCH 19/34] Migrate to new, separate APIs for cross-signing and secret storage This migrates to the new JS SDK APIs, which now use separate paths for cross-signing and secret storage setup. There should be no functional change here. Part of https://github.com/vector-im/element-web/issues/13895 --- src/CrossSigningManager.js | 6 ++++-- .../dialogs/secretstorage/CreateSecretStorageDialog.js | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index da09a436e9..a7b494dc26 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -239,7 +239,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f } } else { const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - await cli.bootstrapSecretStorage({ + await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: async (makeRequest) => { const { finished } = Modal.createTrackedDialog( 'Cross-signing keys dialog', '', InteractiveAuthDialog, @@ -254,7 +254,9 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f throw new Error("Cross-signing key upload auth canceled"); } }, - getBackupPassphrase: promptForBackupPassphrase, + }); + await cli.bootstrapSecretStorage({ + getKeyBackupPassphrase: promptForBackupPassphrase, }); } diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 47faa35df4..19c0c79448 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -282,15 +282,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent { try { if (force) { console.log("Forcing secret storage reset"); // log something so we can debug this later - await cli.bootstrapSecretStorage({ + await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + setupNewCrossSigning: true, + }); + await cli.bootstrapSecretStorage({ createSecretStorageKey: async () => this._recoveryKey, setupNewKeyBackup: true, setupNewSecretStorage: true, }); } else { - await cli.bootstrapSecretStorage({ + await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + }); + await cli.bootstrapSecretStorage({ createSecretStorageKey: async () => this._recoveryKey, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo, From 3a98b4b4e948b9993d8f063049c5338e42fefd21 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 27 Aug 2020 13:50:50 +0100 Subject: [PATCH 20/34] Rename reset secret storage prop The bare word `force` has bothered me, so this adds a tiny amount more meaning. --- src/CrossSigningManager.js | 2 +- .../secretstorage/CreateSecretStorageDialog.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index a7b494dc26..b15290b9c3 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -218,7 +218,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '', import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"), { - force: forceReset, + forceReset, }, null, /* priority = */ false, diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 19c0c79448..00216e3765 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -56,12 +56,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent { static propTypes = { hasCancel: PropTypes.bool, accountPassword: PropTypes.string, - force: PropTypes.bool, + forceReset: PropTypes.bool, }; static defaultProps = { hasCancel: true, - force: false, + forceReset: false, }; constructor(props) { @@ -118,8 +118,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo) ); - const { force } = this.props; - const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; + const { forceReset } = this.props; + const phase = (backupInfo && !forceReset) ? PHASE_MIGRATE : PHASE_CHOOSE_KEY_PASSPHRASE; this.setState({ phase, @@ -277,11 +277,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const cli = MatrixClientPeg.get(); - const { force } = this.props; + const { forceReset } = this.props; try { - if (force) { - console.log("Forcing secret storage reset"); // log something so we can debug this later + if (forceReset) { + console.log("Forcing cross-signing and secret storage reset"); await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, setupNewCrossSigning: true, From 13e9f7b9128b99c11970d4ca76ffd0063b085c02 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 27 Aug 2020 13:18:52 -0600 Subject: [PATCH 21/34] Update home icon --- res/css/structures/_TagPanel.scss | 13 +++++++------ res/img/element-icons/home.svg | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 2683a32dae..dc27da7102 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -99,11 +99,11 @@ limitations under the License. } .mx_TagTile:not(.mx_TagTile_selected_prototype) .mx_TagTile_homeIcon { - background-color: $icon-button-color; // XXX: Variable abuse + background-color: $roomheader-addroom-bg-color; border-radius: 48px; &::before { - background-color: $primary-bg-color; // light-on-grey + background-color: $roomheader-addroom-fg-color; } } @@ -116,13 +116,14 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/home.svg'); mask-position: center; mask-repeat: no-repeat; - width: 21px; - height: 21px; + mask-size: 21px; content: ''; display: inline-block; + width: 32px; + height: 32px; position: absolute; - top: calc(50% - 10.5px); - left: calc(50% - 10.5px); + top: calc(50% - 16px); + left: calc(50% - 16px); } } diff --git a/res/img/element-icons/home.svg b/res/img/element-icons/home.svg index d65812cafd..a6c15456ff 100644 --- a/res/img/element-icons/home.svg +++ b/res/img/element-icons/home.svg @@ -1,3 +1,3 @@ - + From be1de1d2952e845542b45c900d0235e2d8039114 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 27 Aug 2020 13:49:40 -0600 Subject: [PATCH 22/34] Update create room dialog copy --- .../views/dialogs/CreateRoomDialog.js | 38 ++++++++++++++++--- src/i18n/strings/en_EN.json | 8 ++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index 9726c44fac..d334438d58 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -184,18 +184,25 @@ export default createReactClass({ const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); - let publicPrivateLabel; let aliasField; if (this.state.isPublic) { - publicPrivateLabel = (

{_t("Set a room address to easily share your room with other people.")}

); const domain = MatrixClientPeg.get().getDomain(); aliasField = (
this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
); - } else { - publicPrivateLabel = (

{_t("This room is private, and can only be joined by invitation.")}

); + } + + let publicPrivateLabel =

{_t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone.", + )}

; + if (TagOrderStore.getSelectedPrototypeTag()) { + publicPrivateLabel =

{_t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone in this community.", + )}

; } let e2eeSection; @@ -218,6 +225,19 @@ export default createReactClass({ ; } + let federateLabel = _t( + "You might enable this if the room will be only be used for collaborating with internal " + + "teams on your homeserver. This setting cannot be changed later.", + ); + if (SdkConfig.get().default_federate === false) { + // We only change the label if the default setting is different to avoid jarring text changes to the + // user. They will have read the implications of turning this off/on, so no need to rephrase for them. + federateLabel = _t( + "You might disable this if the room will be used for collaborating with external " + + "teams who have their own homeserver. This setting cannot be changed later.", + ); + } + let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); if (TagOrderStore.getSelectedPrototypeTag()) { const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag()); @@ -238,7 +258,15 @@ export default createReactClass({ { aliasField }
{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') } - + +

{federateLabel}

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0ec12a4d6a..b92af49fad 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1637,10 +1637,12 @@ "Community ID": "Community ID", "example": "example", "Please enter a name for the room": "Please enter a name for the room", - "Set a room address to easily share your room with other people.": "Set a room address to easily share your room with other people.", - "This room is private, and can only be joined by invitation.": "This room is private, and can only be joined by invitation.", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.", + "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.", "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.", "Enable end-to-end encryption": "Enable end-to-end encryption", + "You might enable this if the room will be only be used for collaborating with internal teams on your homeserver. This setting cannot be changed later.": "You might enable this if the room will be only be used for collaborating with internal teams on your homeserver. This setting cannot be changed later.", + "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This setting cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This setting cannot be changed later.", "Create a public room": "Create a public room", "Create a private room": "Create a private room", "Create a room in %(communityName)s": "Create a room in %(communityName)s", @@ -1649,7 +1651,7 @@ "Make this room public": "Make this room public", "Hide advanced": "Hide advanced", "Show advanced": "Show advanced", - "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)": "Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)", + "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.", "Create Room": "Create Room", "Sign out": "Sign out", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this": "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of %(brand)s to do this", From bbd343f414fec4cd1035d053c49556cf048175a3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 27 Aug 2020 14:17:55 -0600 Subject: [PATCH 23/34] Fix clicking the background of the tag panel not clearing the filter Fixes https://github.com/vector-im/element-web/issues/12988 --- src/components/structures/TagPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 4acbc49d4d..40b5d04a0a 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -164,7 +164,7 @@ const TagPanel = createReactClass({ ); } - return
+ return
{ clearButton }
From 3c176f762ed9d20f8ffe6529ce23c5a200451308 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 27 Aug 2020 14:26:55 -0600 Subject: [PATCH 24/34] Remove disabled clear button from tag panel + dead code --- res/css/structures/_TagPanel.scss | 19 ------------------- src/components/structures/TagPanel.js | 21 --------------------- 2 files changed, 40 deletions(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 2683a32dae..0c8857e1ef 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -30,25 +30,6 @@ limitations under the License. cursor: pointer; } -.mx_TagPanel .mx_TagPanel_clearButton_container { - /* Constant height within flex mx_TagPanel */ - height: 70px; - width: 56px; - - flex: none; - - justify-content: center; - align-items: flex-start; - - display: none; -} - -.mx_TagPanel .mx_TagPanel_clearButton object { - /* Same as .mx_SearchBox padding-top */ - margin-top: 24px; - pointer-events: none; -} - .mx_TagPanel .mx_TagPanel_divider { height: 0px; width: 90%; diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 40b5d04a0a..a714b126ec 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -95,11 +95,6 @@ const TagPanel = createReactClass({ } }, - onCreateGroupClick(ev) { - ev.stopPropagation(); - dis.dispatch({action: 'view_create_group'}); - }, - onClearFilterClick(ev) { dis.dispatch({action: 'deselect_tags'}); }, @@ -117,9 +112,7 @@ const TagPanel = createReactClass({ render() { const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const ActionButton = sdk.getComponent('elements.ActionButton'); - const TintableSvg = sdk.getComponent('elements.TintableSvg'); const tags = this.state.orderedTags.map((tag, index) => { return 0; - - let clearButton; - if (itemsSelected) { - clearButton = - - ; - } - const classes = classNames('mx_TagPanel', { mx_TagPanel_items_selected: itemsSelected, }); @@ -165,9 +147,6 @@ const TagPanel = createReactClass({ } return
-
- { clearButton } -
Date: Fri, 28 Aug 2020 12:10:17 +0100 Subject: [PATCH 25/34] Add secret storage cache callback to avoid prompts This supplies a cache callback to the JS SDK so that we can be notified if a new storage key is created e.g. by resetting secret storage. This allows it to be supplied automatically in case it's needed in the same user operation, as it is when resetting both secret storage and cross-signing together. --- src/CrossSigningManager.js | 27 +++++++++++-------- .../CreateSecretStorageDialog.js | 8 +++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index b15290b9c3..0353bfc5ae 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -69,19 +69,19 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { if (keyInfoEntries.length > 1) { throw new Error("Multiple storage key requests not implemented"); } - const [name, info] = keyInfoEntries[0]; + const [keyId, keyInfo] = keyInfoEntries[0]; // Check the in-memory cache - if (isCachingAllowed() && secretStorageKeys[name]) { - return [name, secretStorageKeys[name]]; + if (isCachingAllowed() && secretStorageKeys[keyId]) { + return [keyId, secretStorageKeys[keyId]]; } const inputToKey = async ({ passphrase, recoveryKey }) => { if (passphrase) { return deriveKey( passphrase, - info.passphrase.salt, - info.passphrase.iterations, + keyInfo.passphrase.salt, + keyInfo.passphrase.iterations, ); } else { return decodeRecoveryKey(recoveryKey); @@ -93,10 +93,10 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { AccessSecretStorageDialog, /* props= */ { - keyInfo: info, + keyInfo, checkPrivateKey: async (input) => { const key = await inputToKey(input); - return await MatrixClientPeg.get().checkSecretStorageKey(key, info); + return await MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo); }, }, /* className= */ null, @@ -118,11 +118,15 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { const key = await inputToKey(input); // Save to cache to avoid future prompts in the current session - if (isCachingAllowed()) { - secretStorageKeys[name] = key; - } + cacheSecretStorageKey(keyId, key); - return [name, key]; + return [keyId, key]; +} + +function cacheSecretStorageKey(keyId, key) { + if (isCachingAllowed()) { + secretStorageKeys[keyId] = key; + } } const onSecretRequested = async function({ @@ -170,6 +174,7 @@ const onSecretRequested = async function({ export const crossSigningCallbacks = { getSecretStorageKey, + cacheSecretStorageKey, onSecretRequested, }; diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 00216e3765..0a1a0b02b3 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -282,15 +282,15 @@ export default class CreateSecretStorageDialog extends React.PureComponent { try { if (forceReset) { console.log("Forcing cross-signing and secret storage reset"); - await cli.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: this._doBootstrapUIAuth, - setupNewCrossSigning: true, - }); await cli.bootstrapSecretStorage({ createSecretStorageKey: async () => this._recoveryKey, setupNewKeyBackup: true, setupNewSecretStorage: true, }); + await cli.bootstrapCrossSigning({ + authUploadDeviceSigningKeys: this._doBootstrapUIAuth, + setupNewCrossSigning: true, + }); } else { await cli.bootstrapCrossSigning({ authUploadDeviceSigningKeys: this._doBootstrapUIAuth, From f038103f97b0b9625a711e7a839420b2c7fc475c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 28 Aug 2020 09:47:36 -0600 Subject: [PATCH 26/34] Fix copy --- src/components/views/dialogs/CreateRoomDialog.js | 6 +++--- src/i18n/strings/en_EN.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index d334438d58..4890626527 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -226,15 +226,15 @@ export default createReactClass({ } let federateLabel = _t( - "You might enable this if the room will be only be used for collaborating with internal " + - "teams on your homeserver. This setting cannot be changed later.", + "You might enable this if the room will only be used for collaborating with internal " + + "teams on your homeserver. This cannot be changed later.", ); if (SdkConfig.get().default_federate === false) { // We only change the label if the default setting is different to avoid jarring text changes to the // user. They will have read the implications of turning this off/on, so no need to rephrase for them. federateLabel = _t( "You might disable this if the room will be used for collaborating with external " + - "teams who have their own homeserver. This setting cannot be changed later.", + "teams who have their own homeserver. This cannot be changed later.", ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b92af49fad..f65e75d2b9 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1641,8 +1641,8 @@ "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.", "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.", "Enable end-to-end encryption": "Enable end-to-end encryption", - "You might enable this if the room will be only be used for collaborating with internal teams on your homeserver. This setting cannot be changed later.": "You might enable this if the room will be only be used for collaborating with internal teams on your homeserver. This setting cannot be changed later.", - "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This setting cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This setting cannot be changed later.", + "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.", + "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.", "Create a public room": "Create a public room", "Create a private room": "Create a private room", "Create a room in %(communityName)s": "Create a room in %(communityName)s", From b9cfa95ceb31d8333c0cb9884212821ebc797336 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 28 Aug 2020 17:06:56 +0100 Subject: [PATCH 27/34] Add display-capture to iframe allow --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 75946f19c1..0813faee54 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -735,7 +735,7 @@ export default class AppTile extends React.Component { // Additional iframe feature pemissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = "microphone; camera; encrypted-media; autoplay;"; + const iframeFeatures = "microphone; camera; encrypted-media; autoplay; display-capture;"; const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); From 9ba33c7f80e3c812825415923994eaf98181524f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 29 Aug 2020 01:11:08 +0100 Subject: [PATCH 28/34] Fix eslint ts override tsx matching and delint --- .eslintrc.js | 2 +- src/ContentMessages.tsx | 20 ++++--- src/HtmlUtils.tsx | 11 +++- src/Modal.tsx | 8 +-- src/SlashCommands.tsx | 14 ++--- src/accessibility/KeyboardShortcuts.tsx | 2 +- src/accessibility/RovingTabIndex.tsx | 2 +- src/accessibility/Toolbar.tsx | 3 +- .../context_menu/ContextMenuTooltipButton.tsx | 2 +- .../roving/RovingAccessibleTooltipButton.tsx | 3 +- .../roving/RovingTabIndexWrapper.tsx | 1 - src/autocomplete/CommandProvider.tsx | 6 ++- src/autocomplete/CommunityProvider.tsx | 12 ++--- src/autocomplete/Components.tsx | 12 ++--- src/autocomplete/EmojiProvider.tsx | 6 ++- src/autocomplete/RoomProvider.tsx | 4 +- src/autocomplete/UserProvider.tsx | 15 ++++-- src/components/structures/ContextMenu.tsx | 3 +- src/components/structures/LeftPanel.tsx | 2 +- src/components/structures/LoggedInView.tsx | 37 +++++++------ src/components/structures/MatrixChat.tsx | 17 +++--- src/components/structures/RoomSearch.tsx | 3 +- src/components/structures/TabbedView.tsx | 1 - src/components/structures/UserMenu.tsx | 12 ++--- src/components/views/avatars/BaseAvatar.tsx | 4 +- .../views/avatars/DecoratedRoomAvatar.tsx | 2 +- src/components/views/avatars/GroupAvatar.tsx | 2 +- src/components/views/avatars/PulsedAvatar.tsx | 2 +- .../CommunityPrototypeInviteDialog.tsx | 52 ++++++++----------- .../CreateCommunityPrototypeDialog.tsx | 10 +++- src/components/views/dialogs/ShareDialog.tsx | 20 +++---- src/components/views/elements/Draggable.tsx | 4 +- .../views/elements/EventTilePreview.tsx | 14 +++-- src/components/views/elements/Field.tsx | 6 +-- .../elements/IRCTimelineProfileResizer.tsx | 7 ++- src/components/views/elements/InfoTooltip.tsx | 1 - src/components/views/elements/QRCode.tsx | 2 +- src/components/views/elements/Slider.tsx | 19 ++++--- .../views/elements/StyledCheckbox.tsx | 7 ++- .../views/right_panel/EncryptionInfo.tsx | 6 ++- .../views/right_panel/HeaderButtons.tsx | 16 +++--- .../views/right_panel/VerificationPanel.tsx | 23 ++++---- .../views/rooms/NotificationBadge.tsx | 1 + src/components/views/rooms/RoomList.tsx | 32 ++++++------ src/components/views/rooms/RoomSublist.tsx | 25 +++++---- src/components/views/rooms/RoomTile.tsx | 6 +-- src/components/views/rooms/TemporaryTile.tsx | 2 - .../views/settings/UpdateCheckButton.tsx | 2 +- .../tabs/user/AppearanceUserSettingsTab.tsx | 4 +- .../views/toasts/GenericExpiringToast.tsx | 10 +++- src/components/views/toasts/GenericToast.tsx | 8 ++- src/components/views/voip/CallView.tsx | 5 +- src/components/views/voip/IncomingCallBox.tsx | 3 +- src/languageHandler.tsx | 6 +-- 54 files changed, 268 insertions(+), 231 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fc82e75ce2..bc2a142c2d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,7 +19,7 @@ module.exports = { }, overrides: [{ - "files": ["src/**/*.{ts, tsx}"], + "files": ["src/**/*.{ts,tsx}"], "extends": ["matrix-org/ts"], "rules": { // We disable this while we're transitioning diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 6f55a75d0c..eb8fff0eb1 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -70,6 +70,7 @@ interface IContent { interface IThumbnail { info: { + // eslint-disable-next-line camelcase thumbnail_info: { w: number; h: number; @@ -104,7 +105,12 @@ interface IAbortablePromise extends Promise { * @return {Promise} A promise that resolves with an object with an info key * and a thumbnail key. */ -function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise { +function createThumbnail( + element: ThumbnailableElement, + inputWidth: number, + inputHeight: number, + mimeType: string, +): Promise { return new Promise((resolve) => { let targetWidth = inputWidth; let targetHeight = inputHeight; @@ -437,11 +443,13 @@ export default class ContentMessages { for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { - const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, { - file, - currentIndex: i, - totalFiles: okFiles.length, - }); + const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', + '', UploadConfirmDialog, { + file, + currentIndex: i, + totalFiles: okFiles.length, + }, + ); const [shouldContinue, shouldUploadAll] = await finished; if (!shouldContinue) break; if (shouldUploadAll) { diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 5d33645bb7..2ce9e40aa6 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -339,6 +339,7 @@ class HtmlHighlighter extends BaseHighlighter { } } +// eslint-disable-next-line @typescript-eslint/no-unused-vars class TextHighlighter extends BaseHighlighter { private key = 0; @@ -366,6 +367,7 @@ class TextHighlighter extends BaseHighlighter { interface IContent { format?: string; + // eslint-disable-next-line camelcase formatted_body?: string; body: string; } @@ -474,8 +476,13 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts }); return isDisplayedWithHtml ? - : - { strippedBody }; + : { strippedBody }; } /** diff --git a/src/Modal.tsx b/src/Modal.tsx index 82ed33b794..0a36813961 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -151,7 +151,7 @@ export class ModalManager { prom: Promise, props?: IProps, className?: string, - options?: IOptions + options?: IOptions, ) { const modal: IModal = { onFinished: props ? props.onFinished : null, @@ -182,7 +182,7 @@ export class ModalManager { private getCloseFn( modal: IModal, - props: IProps + props: IProps, ): [IHandle["close"], IHandle["finished"]] { const deferred = defer(); return [async (...args: T) => { @@ -264,7 +264,7 @@ export class ModalManager { className?: string, isPriorityModal = false, isStaticModal = false, - options: IOptions = {} + options: IOptions = {}, ): IHandle { const {modal, closeDialog, onFinishedProm} = this.buildModal(prom, props, className, options); if (isPriorityModal) { @@ -287,7 +287,7 @@ export class ModalManager { private appendDialogAsync( prom: Promise, props?: IProps, - className?: string + className?: string, ): IHandle { const {modal, closeDialog, onFinishedProm} = this.buildModal(prom, props, className, {}); diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index d674634109..661ab74e6f 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -860,12 +860,12 @@ export const Commands = [ _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session' + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + '"%(fingerprint)s". This could mean your communications are being intercepted!', - { - fprint, - userId, - deviceId, - fingerprint, - })); + { + fprint, + userId, + deviceId, + fingerprint, + })); } await cli.setDeviceVerified(userId, deviceId, true); @@ -879,7 +879,7 @@ export const Commands = [ { _t('The signing key you provided matches the signing key you received ' + 'from %(userId)s\'s session %(deviceId)s. Session marked as verified.', - {userId, deviceId}) + {userId, deviceId}) }

, diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx index f527ab4a14..58d8124122 100644 --- a/src/accessibility/KeyboardShortcuts.tsx +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -168,7 +168,7 @@ const shortcuts: Record = { key: Key.U, }], description: _td("Upload a file"), - } + }, ], [Categories.ROOM_LIST]: [ diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 5a650d4b6e..b1dbb56a01 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -190,7 +190,7 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn ev.preventDefault(); ev.stopPropagation(); } else if (onKeyDown) { - return onKeyDown(ev, state); + return onKeyDown(ev, context.state); } }, [context.state, onKeyDown, handleHomeEnd]); diff --git a/src/accessibility/Toolbar.tsx b/src/accessibility/Toolbar.tsx index 0e968461a8..cc2a1769c7 100644 --- a/src/accessibility/Toolbar.tsx +++ b/src/accessibility/Toolbar.tsx @@ -30,6 +30,7 @@ const Toolbar: React.FC = ({children, ...props}) => { const target = ev.target as HTMLElement; let handled = true; + // HOME and END are handled by RovingTabIndexProvider switch (ev.key) { case Key.ARROW_UP: case Key.ARROW_DOWN: @@ -47,8 +48,6 @@ const Toolbar: React.FC = ({children, ...props}) => { } break; - // HOME and END are handled by RovingTabIndexProvider - default: handled = false; } diff --git a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx index abc5412100..49f57ca7b6 100644 --- a/src/accessibility/context_menu/ContextMenuTooltipButton.tsx +++ b/src/accessibility/context_menu/ContextMenuTooltipButton.tsx @@ -20,7 +20,7 @@ import React from "react"; import AccessibleTooltipButton from "../../components/views/elements/AccessibleTooltipButton"; -interface IProps extends React.ComponentProps { +interface IProps extends React.ComponentProps { // whether or not the context menu is currently open isExpanded: boolean; } diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index cc824fef22..2cb974d60e 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -20,7 +20,8 @@ import AccessibleTooltipButton from "../../components/views/elements/AccessibleT import {useRovingTabIndex} from "../RovingTabIndex"; import {Ref} from "./types"; -interface IProps extends Omit, "onFocus" | "inputRef" | "tabIndex"> { +type ATBProps = React.ComponentProps; +interface IProps extends Omit { inputRef?: Ref; } diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx index c826b74497..5211f30215 100644 --- a/src/accessibility/roving/RovingTabIndexWrapper.tsx +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from "react"; -import AccessibleButton from "../../components/views/elements/AccessibleButton"; import {useRovingTabIndex} from "../RovingTabIndex"; import {FocusHandler, Ref} from "./types"; diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index e7a6f44536..3ff8ff0469 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -89,7 +89,11 @@ export default class CommandProvider extends AutocompleteProvider { renderCompletions(completions: React.ReactNode[]): React.ReactNode { return ( -
+
{ completions }
); diff --git a/src/autocomplete/CommunityProvider.tsx b/src/autocomplete/CommunityProvider.tsx index f34fee890e..031fcd6169 100644 --- a/src/autocomplete/CommunityProvider.tsx +++ b/src/autocomplete/CommunityProvider.tsx @@ -91,15 +91,15 @@ export default class CommunityProvider extends AutocompleteProvider { href: makeGroupPermalink(groupId), component: ( - + ), range, - })) - .slice(0, 4); + })).slice(0, 4); } return completions; } diff --git a/src/autocomplete/Components.tsx b/src/autocomplete/Components.tsx index 6ac2f4db14..4b0d35698d 100644 --- a/src/autocomplete/Components.tsx +++ b/src/autocomplete/Components.tsx @@ -34,9 +34,9 @@ export const TextualCompletion = forwardRef((props const {title, subtitle, description, className, ...restProps} = props; return (
{ title } { subtitle } @@ -53,9 +53,9 @@ export const PillCompletion = forwardRef((props, ref) const {title, subtitle, description, className, children, ...restProps} = props; return (
{ children } { title } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 147d68f5ff..eaca42b0dd 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -139,7 +139,11 @@ export default class EmojiProvider extends AutocompleteProvider { renderCompletions(completions: React.ReactNode[]): React.ReactNode { return ( -
+
{ completions }
); diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index b18b2d132c..defbc8c47f 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -110,9 +110,7 @@ export default class RoomProvider extends AutocompleteProvider { ), range, }; - }) - .filter((completion) => !!completion.completion && completion.completion.length > 0) - .slice(0, 4); + }).filter((completion) => !!completion.completion && completion.completion.length > 0).slice(0, 4); } return completions; } diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index c957b5e597..3bde4b1d07 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -71,8 +71,13 @@ export default class UserProvider extends AutocompleteProvider { } } - private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, - data: IRoomTimelineData) => { + private onRoomTimeline = ( + ev: MatrixEvent, + room: Room, + toStartOfTimeline: boolean, + removed: boolean, + data: IRoomTimelineData, + ) => { if (!room) return; if (removed) return; if (room.roomId !== this.room.roomId) return; @@ -171,7 +176,11 @@ export default class UserProvider extends AutocompleteProvider { renderCompletions(completions: React.ReactNode[]): React.ReactNode { return ( -
+
{ completions }
); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 587ae2cb6b..64e0160d83 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -233,8 +233,7 @@ export class ContextMenu extends React.PureComponent { switch (ev.key) { case Key.TAB: case Key.ESCAPE: - // close on left and right arrows too for when it is a context menu on a - case Key.ARROW_LEFT: + case Key.ARROW_LEFT: // close on left and right arrows too for when it is a context menu on a case Key.ARROW_RIGHT: this.props.onFinished(); break; diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 899dfe222d..1c2295384c 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -377,7 +377,7 @@ export default class LeftPanel extends React.Component { public render(): React.ReactNode { const tagPanel = !this.state.showTagPanel ? null : (
- + {SettingsStore.getValue("feature_custom_tags") ? : null}
); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index d7f2c73a0b..e427eb92cb 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -43,11 +43,11 @@ import PlatformPeg from "../../PlatformPeg"; import { DefaultTagID } from "../../stores/room-list/models"; import { showToast as showSetPasswordToast, - hideToast as hideSetPasswordToast + hideToast as hideSetPasswordToast, } from "../../toasts/SetPasswordToast"; import { showToast as showServerLimitToast, - hideToast as hideServerLimitToast + hideToast as hideServerLimitToast, } from "../../toasts/ServerLimitToast"; import { Action } from "../../dispatcher/actions"; import LeftPanel from "./LeftPanel"; @@ -79,6 +79,7 @@ interface IProps { initialEventPixelOffset: number; leftDisabled: boolean; rightDisabled: boolean; + // eslint-disable-next-line camelcase page_type: string; autoJoin: boolean; thirdPartyInvite?: object; @@ -98,7 +99,9 @@ interface IProps { } interface IUsageLimit { + // eslint-disable-next-line camelcase limit_type: "monthly_active_user" | string; + // eslint-disable-next-line camelcase admin_contact?: string; } @@ -316,10 +319,10 @@ class LoggedInView extends React.Component { } }; - _calculateServerLimitToast(syncErrorData: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { - const error = syncErrorData && syncErrorData.error && syncErrorData.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; + _calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) { + const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED"; if (error) { - usageLimitEventContent = syncErrorData.error.data; + usageLimitEventContent = syncError.error.data; } if (usageLimitEventContent) { @@ -620,18 +623,18 @@ class LoggedInView extends React.Component { switch (this.props.page_type) { case PageTypes.RoomView: pageElement = ; + ref={this._roomView} + autoJoin={this.props.autoJoin} + onRegistered={this.props.onRegistered} + thirdPartyInvite={this.props.thirdPartyInvite} + oobData={this.props.roomOobData} + viaServers={this.props.viaServers} + eventPixelOffset={this.props.initialEventPixelOffset} + key={this.props.currentRoomId || 'roomview'} + disabled={this.props.middleDisabled} + ConferenceHandler={this.props.ConferenceHandler} + resizeNotifier={this.props.resizeNotifier} + />; break; case PageTypes.MyGroups: diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 9d51062b7d..176aaf95a3 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -69,7 +69,7 @@ import { ViewUserPayload } from "../../dispatcher/payloads/ViewUserPayload"; import { Action } from "../../dispatcher/actions"; import { showToast as showAnalyticsToast, - hideToast as hideAnalyticsToast + hideToast as hideAnalyticsToast, } from "../../toasts/AnalyticsToast"; import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; @@ -129,6 +129,7 @@ interface IScreen { params?: object; } +/* eslint-disable camelcase */ interface IRoomInfo { room_id?: string; room_alias?: string; @@ -140,6 +141,7 @@ interface IRoomInfo { oob_data?: object; via_servers?: string[]; } +/* eslint-enable camelcase */ interface IProps { // TODO type things better config: Record; @@ -165,6 +167,7 @@ interface IState { // the master view we are showing. view: Views; // What the LoggedInView would be showing if visible + // eslint-disable-next-line camelcase page_type?: PageTypes; // The ID of the room we're viewing. This is either populated directly // in the case where we view a room by ID or by RoomView when it resolves @@ -180,8 +183,11 @@ interface IState { middleDisabled: boolean; // the right panel's disabled state is tracked in its store. // Parameters used in the registration dance with the IS + // eslint-disable-next-line camelcase register_client_secret?: string; + // eslint-disable-next-line camelcase register_session_id?: string; + // eslint-disable-next-line camelcase register_id_sid?: string; // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs @@ -341,6 +347,7 @@ export default class MatrixChat extends React.PureComponent { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage + // eslint-disable-next-line camelcase UNSAFE_componentWillUpdate(props, state) { if (this.shouldTrackPageChange(this.state, state)) { this.startPageChangeTimer(); @@ -610,8 +617,7 @@ export default class MatrixChat extends React.PureComponent { const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {initialTabId: tabPayload.initialTabId}, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true - ); + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); @@ -1080,7 +1086,7 @@ export default class MatrixChat extends React.PureComponent { title: _t("Leave room"), description: ( - { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) } + { _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) } { warnings } ), @@ -1433,7 +1439,6 @@ export default class MatrixChat extends React.PureComponent { cli.on("crypto.warning", (type) => { switch (type) { case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': - const brand = SdkConfig.get().brand; Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { title: _t('Old cryptography data detected'), description: _t( @@ -1444,7 +1449,7 @@ export default class MatrixChat extends React.PureComponent { "in this version. This may also cause messages exchanged with this " + "version to fail. If you experience problems, log out and back in " + "again. To retain message history, export and re-import your keys.", - { brand }, + { brand: SdkConfig.get().brand }, ), }); break; diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index f6b8d42c30..768bc38d23 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -20,7 +20,6 @@ import classNames from "classnames"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import { ActionPayload } from "../../dispatcher/payloads"; -import { throttle } from 'lodash'; import { Key } from "../../Keyboard"; import AccessibleButton from "../views/elements/AccessibleButton"; import { Action } from "../../dispatcher/actions"; @@ -137,7 +136,7 @@ export default class RoomSearch extends React.PureComponent { }); let icon = ( -
+
); let input = ( { >
- - {OwnProfileStore.instance.displayName} - + + {OwnProfileStore.instance.displayName} + - {MatrixClientPeg.get().getUserId()} - + {MatrixClientPeg.get().getUserId()} +
{ urls, width = 40, height = 40, - resizeMethod = "crop", // eslint-disable-line no-unused-vars + resizeMethod = "crop", // eslint-disable-line @typescript-eslint/no-unused-vars defaultToInitialLetter = true, onClick, inputRef, diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index e6dadf676c..d7e012467b 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -126,7 +126,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent { if (this.isUnmounted) return; - let newIcon = this.getPresenceIcon(); + const newIcon = this.getPresenceIcon(); if (newIcon !== this.state.icon) this.setState({icon: newIcon}); }; diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index e55e2e6fac..51327605c0 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -47,7 +47,7 @@ export default class GroupAvatar extends React.Component { render() { // extract the props we use from props so we can pass any others through // should consider adding this as a global rule in js-sdk? - /*eslint no-unused-vars: ["error", { "ignoreRestSiblings": true }]*/ + /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const {groupId, groupAvatarUrl, groupName, ...otherProps} = this.props; return ( diff --git a/src/components/views/avatars/PulsedAvatar.tsx b/src/components/views/avatars/PulsedAvatar.tsx index 94a6c87687..b4e876b9f6 100644 --- a/src/components/views/avatars/PulsedAvatar.tsx +++ b/src/components/views/avatars/PulsedAvatar.tsx @@ -25,4 +25,4 @@ const PulsedAvatar: React.FC = (props) => {
; }; -export default PulsedAvatar; \ No newline at end of file +export default PulsedAvatar; diff --git a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx index 7a500cd053..4a454c8cbb 100644 --- a/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx +++ b/src/components/views/dialogs/CommunityPrototypeInviteDialog.tsx @@ -21,9 +21,6 @@ import { IDialogProps } from "./IDialogProps"; import Field from "../elements/Field"; import AccessibleButton from "../elements/AccessibleButton"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import InfoTooltip from "../elements/InfoTooltip"; -import dis from "../../../dispatcher/dispatcher"; -import {showCommunityRoomInviteDialog} from "../../../RoomInvite"; import { arrayFastClone } from "../../../utils/arrays"; import SdkConfig from "../../../SdkConfig"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; @@ -31,7 +28,6 @@ import InviteDialog from "./InviteDialog"; import BaseAvatar from "../avatars/BaseAvatar"; import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo"; import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite"; -import {humanizeTime} from "../../../utils/humanize"; import StyledCheckbox from "../elements/StyledCheckbox"; import Modal from "../../../Modal"; import ErrorDialog from "./ErrorDialog"; @@ -171,44 +167,38 @@ export default class CommunityPrototypeInviteDialog extends React.PureComponent< public render() { const emailAddresses = []; this.state.emailTargets.forEach((address, i) => { - emailAddresses.push( - this.onAddressChange(e, i)} - label={_t("Email address")} - placeholder={_t("Email address")} - onBlur={() => this.onAddressBlur(i)} - /> - ); + emailAddresses.push( this.onAddressChange(e, i)} + label={_t("Email address")} + placeholder={_t("Email address")} + onBlur={() => this.onAddressBlur(i)} + />); }); // Push a clean input - emailAddresses.push( - this.onAddressChange(e, emailAddresses.length)} - label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} - placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} - /> - ); + emailAddresses.push( this.onAddressChange(e, emailAddresses.length)} + label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} + placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")} + />); let peopleIntro = null; - let people = []; + const people = []; if (this.state.showPeople) { const humansToPresent = this.state.people.slice(0, this.state.numPeople); humansToPresent.forEach((person, i) => { people.push(this.renderPerson(person, i)); }); if (humansToPresent.length < this.state.people.length) { - people.push( - {_t("Show more")} - ); + people.push({_t("Show more")}); } } if (this.state.people.length > 0) { diff --git a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx index 58412c23d6..dbfc208583 100644 --- a/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx +++ b/src/components/views/dialogs/CreateCommunityPrototypeDialog.tsx @@ -164,7 +164,10 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent< ); if (this.state.error) { helpText = ( - + {this.state.error} ); @@ -205,7 +208,10 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent< ref={this.avatarUploadRef} accept="image/*" onChange={this.onAvatarChanged} /> - + {preview}
diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index 22f83d391c..e849f7efe3 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -186,8 +186,8 @@ export default class ShareDialog extends React.PureComponent { title = _t('Share Room Message'); checkbox =
{ _t('Link to selected message') } @@ -198,16 +198,18 @@ export default class ShareDialog extends React.PureComponent { const encodedUrl = encodeURIComponent(matrixToUrl); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - return
- { matrixToUrl } diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx index 3397fd901c..a6eb8323f3 100644 --- a/src/components/views/elements/Draggable.tsx +++ b/src/components/views/elements/Draggable.tsx @@ -34,7 +34,6 @@ export interface ILocationState { } export default class Draggable extends React.Component { - constructor(props: IProps) { super(props); @@ -77,5 +76,4 @@ export default class Draggable extends React.Component { render() { return
; } - -} \ No newline at end of file +} diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 7d8b774955..98f6850e6b 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -39,6 +39,7 @@ interface IProps { className: string; } +/* eslint-disable camelcase */ interface IState { userId: string; displayname: string; @@ -72,7 +73,6 @@ export default class EventTilePreview extends React.Component { displayname: profileInfo.displayname, avatar_url, }); - } private fakeEvent({userId, displayname, avatar_url}: IState) { @@ -114,16 +114,14 @@ export default class EventTilePreview extends React.Component { public render() { const event = this.fakeEvent(this.state); - let className = classnames( - this.props.className, - { - "mx_IRCLayout": this.props.useIRCLayout, - "mx_GroupLayout": !this.props.useIRCLayout, - } - ); + const className = classnames(this.props.className, { + "mx_IRCLayout": this.props.useIRCLayout, + "mx_GroupLayout": !this.props.useIRCLayout, + }); return
; } } +/* eslint-enable camelcase */ diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index d9fd59dc11..d2869f68c8 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -198,11 +198,9 @@ export default class Field extends React.PureComponent { } } - - public render() { - const { - element, prefixComponent, postfixComponent, className, onValidate, children, + /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ + const { element, prefixComponent, postfixComponent, className, onValidate, children, tooltipContent, forceValidity, tooltipClassName, list, ...inputProps} = this.props; // Set some defaults for the element diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 1098d0293e..ecd63816de 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -78,7 +78,12 @@ export default class IRCTimelineProfileResizer extends React.Component = ({data, className, ...options}) => { return () => { cancelled = true; }; - }, [JSON.stringify(data), options]); + }, [JSON.stringify(data), options]); // eslint-disable-line react-hooks/exhaustive-deps return
{ dataUri ? {_t("QR : } diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index a88c581d07..b7c8e1b533 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -45,7 +45,7 @@ export default class Slider extends React.Component { // non linear slider. private offset(values: number[], value: number): number { // the index of the first number greater than value. - let closest = values.reduce((prev, curr) => { + const closest = values.reduce((prev, curr) => { return (value > curr ? prev + 1 : prev); }, 0); @@ -68,17 +68,16 @@ export default class Slider extends React.Component { const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue); return 100 * (closest - 1 + linearInterpolation) * intervalWidth; - } render(): React.ReactNode { - const dots = this.props.values.map(v => - {} : () => this.props.onSelectionChange(v)} - key={v} - disabled={this.props.disabled} - />); + const dots = this.props.values.map(v => {} : () => this.props.onSelectionChange(v)} + key={v} + disabled={this.props.disabled} + />); let selection = null; @@ -93,7 +92,7 @@ export default class Slider extends React.Component { return
-
{} : this.onClick.bind(this)}/> +
{} : this.onClick.bind(this)} /> { selection }
diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx index be983828ff..f8d2665d07 100644 --- a/src/components/views/elements/StyledCheckbox.tsx +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -17,8 +17,6 @@ limitations under the License. import React from "react"; import { randomString } from "matrix-js-sdk/src/randomstring"; -const CHECK_BOX_SVG = require("../../../../res/img/feather-customised/check.svg"); - interface IProps extends React.InputHTMLAttributes { } @@ -39,13 +37,14 @@ export default class StyledCheckbox extends React.PureComponent } public render() { + /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { children, className, ...otherProps } = this.props; return