diff --git a/package.json b/package.json index 7ef14e6635..a1ebc6602d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", - "react-focus-lock": "^2.2.1", "focus-visible": "^5.0.2", "fuse.js": "^2.2.0", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", @@ -82,6 +81,7 @@ "glob": "^5.0.14", "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", + "humanize": "^0.0.9", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", @@ -99,6 +99,7 @@ "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", + "react-focus-lock": "^2.2.1", "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", diff --git a/res/css/_components.scss b/res/css/_components.scss index 7b8ca77739..7a9ebfdf26 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -56,6 +56,7 @@ @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_DMInviteDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss new file mode 100644 index 0000000000..1153ecb0d4 --- /dev/null +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -0,0 +1,81 @@ +/* +Copyright 2019, 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_DMInviteDialog_addressBar { + display: flex; + flex-direction: row; + + .mx_DMInviteDialog_editor { + flex: 1; + width: 100%; // Needed to make the Field inside grow + } + + .mx_Field { + margin: 0; + } + + .mx_DMInviteDialog_goButton { + width: 48px; + margin-left: 10px; + } +} + +.mx_DMInviteDialog_section { + padding-bottom: 10px; + + h3 { + font-size: 12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } +} + +.mx_DMInviteDialog_roomTile { + cursor: pointer; + padding: 5px 10px; + + &:hover { + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + } + + * { + vertical-align: middle; + } + + .mx_DMInviteDialog_roomTile_name { + font-weight: 600; + font-size: 14px; + color: $primary-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_userId { + font-size: 12px; + color: $muted-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_time { + text-align: right; + font-size: 12px; + color: $muted-fg-color; + float: right; + line-height: 36px; // Height of the avatar to keep the time vertically aligned + } +} + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index eadde4c672..d28efbb11f 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -16,6 +16,7 @@ $room-highlight-color: #343a46; // typical text (dark-on-white in light skin) $primary-fg-color: $text-primary-color; $primary-bg-color: $bg-color; +$muted-fg-color: $header-panel-text-primary-color; // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -172,6 +173,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #272c35; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 0a3ef812b8..ac9cb261d3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) $primary-fg-color: #2e2f32; $primary-bg-color: #ffffff; +$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text // used for dialog box text $light-fg-color: #747474; @@ -293,6 +294,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #e8eef5; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 48baad5d9f..ba9fe1f541 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -25,6 +25,7 @@ import sdk from './'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; +import SettingsStore from "./settings/SettingsStore"; /** * Invites multiple addresses to a room @@ -41,6 +42,18 @@ function inviteMultipleToRoom(roomId, addrs) { } export function showStartChatInviteDialog() { + if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { + const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog"); + Modal.createTrackedDialog('Start DM', '', DMInviteDialog, { + onFinished: (inviteIds) => { + // TODO: Replace _onStartDmFinished with less hacks + if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i}))); + // else ignore and just do nothing + }, + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + return; + } + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { @@ -99,7 +112,7 @@ export function isValid3pidInvite(event) { return true; } -// TODO: Immutable DMs replaces this +// TODO: Canonical DMs replaces this function _onStartDmFinished(shouldInvite, addrs) { if (!shouldInvite) return; diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js new file mode 100644 index 0000000000..ff498e3e75 --- /dev/null +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -0,0 +1,217 @@ +/* +Copyright 2019, 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 from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import {makeUserPermalink} from "../../../utils/permalinks/Permalinks"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {RoomMember} from "matrix-js-sdk/lib/matrix"; +import * as humanize from "humanize"; + +// TODO: [TravisR] Make this generic for all kinds of invites + +const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first +const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked + +class DMRoomTile extends React.PureComponent { + static propTypes = { + member: PropTypes.object.isRequired, + lastActiveTs: PropTypes.number, + onToggle: PropTypes.func.isRequired, + }; + + constructor() { + super(); + } + + _onClick = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + this.props.onToggle(this.props.member.userId); + }; + + render() { + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + + let timestamp = null; + if (this.props.lastActiveTs) { + // TODO: [TravisR] Figure out how to i18n this + // `humanize` wants seconds for a timestamp, so divide by 1000 + const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000); + timestamp = {humanTs}; + } + + return ( +
+ {_t( + "If you can't find someone, ask them for their username, or share your " + + "username (%(userId)s) or profile link.", + {userId}, + {a: (sub) => {sub}}, + )} +
+ {targets} +