diff --git a/package.json b/package.json index b4e1af9f0a..70de250830 100644 --- a/package.json +++ b/package.json @@ -93,10 +93,10 @@ "qrcode-react": "^0.1.16", "qs": "^6.6.0", "querystring": "^0.2.0", - "react": "^15.6.0", - "react-addons-css-transition-group": "15.3.2", + "react": "^16.9.0", + "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", - "react-dom": "^15.6.0", + "react-dom": "^16.9.0", "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#f644523", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", diff --git a/res/css/_common.scss b/res/css/_common.scss index adf4c93290..2b627cce9f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -171,7 +171,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { :not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type=search], .mx_textinput { color: $input-darker-fg-color; - background-color: $input-darker-bg-color; + background-color: $primary-bg-color; border: none; } } @@ -330,7 +330,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { .mx_Dialog_header { position: relative; - margin-bottom: 20px; + margin-bottom: 10px; } .mx_Dialog_title { diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7d10fdb6d6..85fdfa092d 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -125,3 +125,53 @@ limitations under the License. margin-top: 12px; } } + +.mx_LeftPanel_exploreAndFilterRow { + display: flex; + + .mx_SearchBox { + flex: 1 1 0; + min-width: 0; + margin: 4px 9px 1px 9px; + } +} + +.mx_LeftPanel_explore { + flex: 0 0 50%; + overflow: hidden; + transition: flex-basis 0.2s; + box-sizing: border-box; + + &.mx_LeftPanel_explore_hidden { + flex-basis: 0; + } + + .mx_AccessibleButton { + font-size: 14px; + margin: 4px 0 1px 9px; + padding: 9px; + padding-left: 42px; + font-weight: 600; + color: $notice-secondary-color; + position: relative; + border-radius: 4px; + + &:hover { + background-color: $primary-bg-color; + } + + &::before { + cursor: pointer; + mask: url('$(res)/img/explore.svg'); + mask-repeat: no-repeat; + mask-position: center center; + content: ""; + left: 14px; + top: 10px; + width: 16px; + height: 16px; + background-color: $notice-secondary-color; + position: absolute; + } + } +} diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 1df0a61a2b..6b7a4ff0c7 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -17,7 +17,6 @@ limitations under the License. .mx_RoomDirectory_dialogWrapper > .mx_Dialog { max-width: 960px; height: 100%; - padding: 20px; } .mx_RoomDirectory_dialog { @@ -35,17 +34,6 @@ limitations under the License. flex: 1; } -.mx_RoomDirectory_createRoom { - background-color: $button-bg-color; - border-radius: 4px; - padding: 8px; - color: $button-fg-color; - font-weight: 600; - position: absolute; - top: 0; - left: 0; -} - .mx_RoomDirectory_list { flex: 1; display: flex; @@ -84,9 +72,8 @@ limitations under the License. } .mx_RoomDirectory_roomAvatar { - width: 24px; - padding-left: 12px; - padding-right: 24px; + width: 32px; + padding-right: 14px; vertical-align: top; } @@ -94,6 +81,34 @@ limitations under the License. padding-bottom: 16px; } +.mx_RoomDirectory_roomMemberCount { + color: $light-fg-color; + width: 60px; + padding: 0 10px; + text-align: center; + + &::before { + background-color: $light-fg-color; + display: inline-block; + vertical-align: text-top; + margin-right: 2px; + content: ""; + mask: url('$(res)/img/feather-customised/user.svg'); + mask-repeat: no-repeat; + mask-position: center; + // scale it down and make the size slightly bigger (16 instead of 14px) + // to avoid rendering artifacts + mask-size: 80%; + width: 16px; + height: 16px; + } +} + +.mx_RoomDirectory_join, .mx_RoomDirectory_preview { + width: 80px; + text-align: center; +} + .mx_RoomDirectory_name { display: inline-block; font-weight: 600; @@ -103,22 +118,9 @@ limitations under the License. display: inline-block; } -.mx_RoomDirectory_perm { - display: inline; - padding-left: 5px; - padding-right: 5px; - margin-right: 5px; - height: 15px; - border-radius: 11px; - background-color: $plinth-bg-color; - text-transform: uppercase; - font-weight: 600; - font-size: 11px; - color: $accent-color; -} - .mx_RoomDirectory_topic { cursor: initial; + color: $light-fg-color; } .mx_RoomDirectory_alias { @@ -126,13 +128,20 @@ limitations under the License. color: $settings-grey-fg-color; } -.mx_RoomDirectory_roomMemberCount { - text-align: right; - width: 100px; - padding-right: 10px; -} - .mx_RoomDirectory_table tr { padding-bottom: 10px; cursor: pointer; } + +.mx_RoomDirectory .mx_RoomView_MessageList { + padding: 0; +} + +.mx_RoomDirectory p { + font-size: 14px; + margin-top: 0; + + .mx_AccessibleButton { + padding: 0; + } +} diff --git a/res/css/structures/_SearchBox.scss b/res/css/structures/_SearchBox.scss index 9434d93bd2..23ee06f7b3 100644 --- a/res/css/structures/_SearchBox.scss +++ b/res/css/structures/_SearchBox.scss @@ -14,12 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SearchBox_closeButton { - cursor: pointer; - background-image: url('$(res)/img/icons-close.svg'); - background-repeat: no-repeat; - width: 16px; - height: 16px; - background-position: center; - padding: 9px; +.mx_SearchBox { + flex: 1 1 0; + min-width: 0; + + &.mx_SearchBox_blurred:not(:hover) { + background-color: transparent; + } + + .mx_SearchBox_closeButton { + cursor: pointer; + background-image: url('$(res)/img/icons-close.svg'); + background-repeat: no-repeat; + width: 16px; + height: 16px; + background-position: center; + padding: 9px; + } } diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index b51d720e4d..5ed22f997d 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -27,10 +27,6 @@ limitations under the License. position: relative; } -.mx_SearchBox { - flex: none; -} - /* hide resize handles next to collapsed / empty sublists */ .mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle { display: none; diff --git a/res/img/explore.svg b/res/img/explore.svg new file mode 100644 index 0000000000..3956e912ac --- /dev/null +++ b/res/img/explore.svg @@ -0,0 +1,97 @@ + + diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index cd5ecc790d..dfc90841a3 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -46,7 +46,7 @@ export function showGroupInviteDialog(groupId) { _onGroupInviteFinished(groupId, addrs).then(resolve, reject); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }); } @@ -81,7 +81,7 @@ export function showGroupAddRoomDialog(groupId) { _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly).then(resolve, reject); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }); } diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 075ae93709..7cbad074bf 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -65,7 +65,7 @@ export default class IdentityAuthClient { } // Returns a promise that resolves to the access_token string from the IS - async getAccessToken(check=true) { + async getAccessToken({ check = true } = {}) { if (!this.authEnabled) { // The current IS doesn't support authentication return null; diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 19cbef4242..64aab36128 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -57,7 +57,7 @@ export function showStartChatInviteDialog() { validAddressTypes: ['mx-user-id', 'email'], button: _t("Start Chat"), onFinished: _onStartDmFinished, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); } export function showRoomInviteDialog(roomId) { @@ -78,7 +78,7 @@ export function showRoomInviteDialog(roomId) { onFinished: (shouldInvite, addrs) => { _onRoomInviteFinished(roomId, shouldInvite, addrs); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); } /** diff --git a/src/Terms.js b/src/Terms.js index 594f15b522..685a39709c 100644 --- a/src/Terms.js +++ b/src/Terms.js @@ -116,6 +116,7 @@ export async function startTermsFlow( } // if there's anything left to agree to, prompt the user + const numAcceptedBeforeAgreement = agreedUrlSet.size; if (unagreedPoliciesAndServicePairs.length > 0) { const newlyAgreedUrls = await interactionCallback(unagreedPoliciesAndServicePairs, [...agreedUrlSet]); console.log("User has agreed to URLs", newlyAgreedUrls); @@ -125,8 +126,11 @@ export async function startTermsFlow( console.log("User has already agreed to all required policies"); } - const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) }; - await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms); + // We only ever add to the set of URLs, so if anything has changed then we'd see a different length + if (agreedUrlSet.size !== numAcceptedBeforeAgreement) { + const newAcceptedTerms = {accepted: Array.from(agreedUrlSet)}; + await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms); + } const agreePromises = policiesAndServicePairs.map((policiesAndService) => { // filter the agreed URL list for ones that are actually for this service diff --git a/src/boundThreepids.js b/src/boundThreepids.js index 799728f801..3b32815913 100644 --- a/src/boundThreepids.js +++ b/src/boundThreepids.js @@ -16,7 +16,7 @@ limitations under the License. import IdentityAuthClient from './IdentityAuthClient'; -export async function getThreepidBindStatus(client, filterMedium) { +export async function getThreepidsWithBindStatus(client, filterMedium) { const userId = client.getUserId(); let { threepids } = await client.getThreePids(); @@ -24,27 +24,33 @@ export async function getThreepidBindStatus(client, filterMedium) { threepids = threepids.filter((a) => a.medium === filterMedium); } - if (threepids.length > 0) { - // TODO: Handle terms agreement - // See https://github.com/vector-im/riot-web/issues/10522 - const authClient = new IdentityAuthClient(); - const identityAccessToken = await authClient.getAccessToken(); + // Check bind status assuming we have an IS and terms are agreed + if (threepids.length > 0 && !!client.getIdentityServerUrl()) { + try { + const authClient = new IdentityAuthClient(); + const identityAccessToken = await authClient.getAccessToken({ check: false }); - // Restructure for lookup query - const query = threepids.map(({ medium, address }) => [medium, address]); - const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); + // Restructure for lookup query + const query = threepids.map(({ medium, address }) => [medium, address]); + const lookupResults = await client.bulkLookupThreePids(query, identityAccessToken); - // Record which are already bound - for (const [medium, address, mxid] of lookupResults.threepids) { - if (mxid !== userId) { - continue; + // Record which are already bound + for (const [medium, address, mxid] of lookupResults.threepids) { + if (mxid !== userId) { + continue; + } + if (filterMedium && medium !== filterMedium) { + continue; + } + const threepid = threepids.find(e => e.medium === medium && e.address === address); + if (!threepid) continue; + threepid.bound = true; } - if (filterMedium && medium !== filterMedium) { - continue; + } catch (e) { + // Ignore terms errors here and assume other flows handle this + if (!(e.errcode === "M_TERMS_NOT_SIGNED")) { + throw e; } - const threepid = threepids.find(e => e.medium === medium && e.address === address); - if (!threepid) continue; - threepid.bound = true; } } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ed18f0f463..70d8b2e298 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -120,7 +120,7 @@ const CategoryRoomList = createReactClass({ }); }); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, render: function() { @@ -297,7 +297,7 @@ const RoleUserList = createReactClass({ }); }); }, - }); + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }, render: function() { diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f083e5ab38..fd315d2540 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -81,6 +81,9 @@ const LeftPanel = createReactClass({ if (this.state.searchFilter !== nextState.searchFilter) { return true; } + if (this.state.searchExpanded !== nextState.searchExpanded) { + return true; + } return false; }, @@ -203,12 +206,23 @@ const LeftPanel = createReactClass({ if (source === "keyboard") { dis.dispatch({action: 'focus_composer'}); } + this.setState({searchExpanded: false}); }, collectRoomList: function(ref) { this._roomList = ref; }, + _onSearchFocus: function() { + this.setState({searchExpanded: true}); + }, + + _onSearchBlur: function(event) { + if (event.target.value.length === 0) { + this.setState({searchExpanded: false}); + } + }, + render: function() { const RoomList = sdk.getComponent('rooms.RoomList'); const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs'); @@ -217,6 +231,7 @@ const LeftPanel = createReactClass({ const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton'); const SearchBox = sdk.getComponent('structures.SearchBox'); const CallPreview = sdk.getComponent('voip.CallPreview'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel"); let tagPanelContainer; @@ -240,11 +255,23 @@ const LeftPanel = createReactClass({ }, ); + let exploreButton; + if (!this.props.collapsed) { + exploreButton = ( +