diff --git a/package.json b/package.json index bb8db64d28..b32e5f0501 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", + "focus-trap-react": "^3.0.5", "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", diff --git a/src/Analytics.js b/src/Analytics.js index 5c39b48a35..2ef058b11b 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -92,6 +92,10 @@ class Analytics { */ disable() { this.trackEvent('Analytics', 'opt-out'); + // disableHeartBeatTimer is undocumented but exists in the piwik code + // the _paq.push method will result in an error being printed in the console + // if an unknown method signature is passed + this._paq.push(['disableHeartBeatTimer']); this.disabled = true; } @@ -143,7 +147,10 @@ class Analytics { return true; } - trackPageChange() { + trackPageChange(generationTimeMs) { + if (typeof generationTimeMs !== 'number') { + throw new Error('Analytics.trackPageChange: expected generationTimeMs to be a number'); + } if (this.disabled) return; if (this.firstPage) { // De-duplicate first page @@ -152,6 +159,7 @@ class Analytics { return; } this._paq.push(['setCustomUrl', getRedactedUrl()]); + this._paq.push(['setGenerationTimeMs', generationTimeMs]); this._paq.push(['trackPageView']); } diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index ef9010cbf2..c45a335ab6 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -22,28 +22,30 @@ import MatrixClientPeg from './MatrixClientPeg'; import GroupStoreCache from './stores/GroupStoreCache'; export function showGroupInviteDialog(groupId) { - const description =
-
{ _t("Who would you like to add to this community?") }
-
- { _t( - "Warning: any person you add to a community will be publicly "+ - "visible to anyone who knows the community ID", - ) } -
-
; + return new Promise((resolve, reject) => { + const description =
+
{ _t("Who would you like to add to this community?") }
+
+ { _t( + "Warning: any person you add to a community will be publicly "+ + "visible to anyone who knows the community ID", + ) } +
+
; - const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); - Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { - title: _t("Invite new community members"), - description: description, - placeholder: _t("Name or matrix ID"), - button: _t("Invite to Community"), - validAddressTypes: ['mx-user-id'], - onFinished: (success, addrs) => { - if (!success) return; + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); + Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { + title: _t("Invite new community members"), + description: description, + placeholder: _t("Name or matrix ID"), + button: _t("Invite to Community"), + validAddressTypes: ['mx-user-id'], + onFinished: (success, addrs) => { + if (!success) return; - _onGroupInviteFinished(groupId, addrs); - }, + _onGroupInviteFinished(groupId, addrs).then(resolve, reject); + }, + }); }); } @@ -87,7 +89,7 @@ function _onGroupInviteFinished(groupId, addrs) { const addrTexts = addrs.map((addr) => addr.address); - multiInviter.invite(addrTexts).then((completionStates) => { + return multiInviter.invite(addrTexts).then((completionStates) => { // Show user any errors const errorList = []; for (const addr of Object.keys(completionStates)) { diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 5c6cbd6c1b..e3b7ba47f5 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -410,8 +410,7 @@ class TextHighlighter extends BaseHighlighter { * opts.disableBigEmoji: optional argument to disable the big emoji class. */ export function bodyToHtml(content, highlights, opts={}) { - const isHtml = (content.format === "org.matrix.custom.html"); - const body = isHtml ? content.formatted_body : escape(content.body); + let isHtml = (content.format === "org.matrix.custom.html"); let bodyHasEmoji = false; @@ -431,9 +430,27 @@ export function bodyToHtml(content, highlights, opts={}) { return highlighter.applyHighlights(safeText, safeHighlights).join(''); }; } - safeBody = sanitizeHtml(body, sanitizeHtmlParams); - bodyHasEmoji = containsEmoji(body); - if (bodyHasEmoji) safeBody = unicodeToImage(safeBody); + + bodyHasEmoji = containsEmoji(isHtml ? content.formatted_body : content.body); + + // Only generate safeBody if the message was sent as org.matrix.custom.html + if (isHtml) { + safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); + } else { + // ... or if there are emoji, which we insert as HTML alongside the + // escaped plaintext body. + if (bodyHasEmoji) { + isHtml = true; + safeBody = sanitizeHtml(escape(content.body), sanitizeHtmlParams); + } + } + + // An HTML message with emoji + // or a plaintext message with emoji that was escaped and sanitized into + // HTML. + if (bodyHasEmoji) { + safeBody = unicodeToImage(safeBody); + } } finally { delete sanitizeHtmlParams.textFilter; } @@ -451,7 +468,10 @@ export function bodyToHtml(content, highlights, opts={}) { 'mx_EventTile_bigEmoji': emojiBody, 'markdown-body': isHtml, }); - return ; + + return isHtml ? + : + { content.body }; } export function emojifyText(text) { diff --git a/src/Modal.js b/src/Modal.js index c9f08772e7..2565d5c73b 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -22,6 +22,7 @@ const ReactDOM = require('react-dom'); import PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; +import dis from './dispatcher'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -188,10 +189,22 @@ class ModalManager { _reRender() { if (this._modals.length == 0) { + // If there is no modal to render, make all of Riot available + // to screen reader users again + dis.dispatch({ + action: 'aria_unhide_main_app', + }); ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); return; } + // Hide the content outside the modal to screen reader users + // so they won't be able to navigate into it and act on it using + // screen reader specific features + dis.dispatch({ + action: 'aria_hide_main_app', + }); + const modal = this._modals[0]; const dialog = (
diff --git a/src/Notifier.js b/src/Notifier.js index e69bdf4461..b823c4df05 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -256,6 +256,10 @@ const Notifier = { }, onEventDecrypted: function(ev) { + // 'decrypted' means the decryption process has finished: it may have failed, + // in which case it might decrypt soon if the keys arrive + if (ev.isDecryptionFailure()) return; + const idx = this.pendingEncryptedEventIds.indexOf(ev.getId()); if (idx === -1) return; diff --git a/src/Presence.js b/src/Presence.js index 2652c64c96..fd9bcf516d 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,7 +32,7 @@ class Presence { this.running = true; if (undefined === this.state) { this._resetTimer(); - this.dispatcherRef = dis.register(this._onUserActivity.bind(this)); + this.dispatcherRef = dis.register(this._onAction.bind(this)); } } @@ -125,9 +126,10 @@ class Presence { this.setState("unavailable"); } - _onUserActivity(payload) { - if (payload.action === "sync_state" || payload.action === "self_presence_updated") return; - this._resetTimer(); + _onAction(payload) { + if (payload.action === "user_activity") { + this._resetTimer(); + } } /** diff --git a/src/Tinter.js b/src/Tinter.js index c7402c15be..7667e6d912 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -252,7 +252,6 @@ class Tinter { setTheme(theme) { - console.trace("setTheme " + theme); this.theme = theme; // update keyRgb from the current theme CSS itself, if it defines it diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index a92bd1ebaf..e5911c4e32 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -18,7 +18,7 @@ import { asyncAction } from './actionCreators'; import RoomListStore from '../stores/RoomListStore'; import Modal from '../Modal'; -import Rooms from '../Rooms'; +import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; import sdk from '../index'; diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index d47f1a161a..e33fa7861f 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -105,6 +105,11 @@ const COMMANDS = [ args: '', description: _td('Stops ignoring a user, showing their messages going forward'), }, + { + command: '/devtools', + args: '', + description: _td('Opens the Developer Tools dialog'), + }, // Omitting `/markdown` as it only seems to apply to OldComposer ]; diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index bceec3f144..e636f95751 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -44,6 +44,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, + shouldMatchWordsOnly: false }); this._onRoomTimelineBound = this._onRoomTimeline.bind(this); @@ -72,6 +73,7 @@ export default class UserProvider extends AutocompleteProvider { // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; + // TODO: lazyload if we have no ev.sender room member? this.onUserSpoke(ev.sender); } @@ -147,6 +149,7 @@ export default class UserProvider extends AutocompleteProvider { onUserSpoke(user: RoomMember) { if (this.users === null) return; + if (!user) return; if (user.userId === MatrixClientPeg.get().credentials.userId) return; // Move the user that spoke to the front of the array diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 4a28faaac4..2d1581da6b 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -31,7 +31,6 @@ import GroupStoreCache from '../../stores/GroupStoreCache'; import GroupStore from '../../stores/GroupStore'; import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to"; const LONG_DESC_PLACEHOLDER = _td( @@ -671,6 +670,20 @@ export default React.createClass({ }); }, + _onJoinClick: function() { + this.setState({membershipBusy: true}); + this._matrixClient.joinGroup(this.props.groupId).then(() => { + // don't reset membershipBusy here: wait for the membership change to come down the sync + }).catch((e) => { + this.setState({membershipBusy: false}); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Error joining room', '', ErrorDialog, { + title: _t("Error"), + description: _t("Unable to join community"), + }); + }); + }, + _onLeaveClick: function() { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); Modal.createTrackedDialog('Leave Group', '', QuestionDialog, { @@ -687,9 +700,9 @@ export default React.createClass({ }).catch((e) => { this.setState({membershipBusy: false}); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Error leaving room', '', ErrorDialog, { + Modal.createTrackedDialog('Error leaving community', '', ErrorDialog, { title: _t("Error"), - description: _t("Unable to leave room"), + description: _t("Unable to leave community"), }); }); }, @@ -707,8 +720,21 @@ export default React.createClass({ }); const header = this.state.editing ?

{ _t('Community Settings') }

:
; + const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ? +
+ { _t( + 'Changes made to your community name and avatar ' + + 'might not be seen by other users for up to 30 minutes.', + {}, + { + 'bold1': (sub) => { sub } , + 'bold2': (sub) => { sub } , + }, + ) } +
:
; return
{ header } + { changeDelayWarning } { this._getLongDescriptionNode() } { this._getRoomsNode() }
; @@ -847,9 +873,8 @@ export default React.createClass({ const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const group = this._matrixClient.getGroup(this.props.groupId); - if (!group) return null; - if (group.myMembership === 'invite') { + if (group && group.myMembership === 'invite') { if (this.state.membershipBusy || this.state.inviterProfileBusy) { return
@@ -890,33 +915,72 @@ export default React.createClass({
; - } else if (group.myMembership === 'join' && this.state.editing) { - const leaveButtonTooltip = this.state.isUserPrivileged ? + } + + let membershipContainerExtraClasses; + let membershipButtonExtraClasses; + let membershipButtonTooltip; + let membershipButtonText; + let membershipButtonOnClick; + + // User is not in the group + if ((!group || group.myMembership === 'leave') && + this.state.summary && + this.state.summary.profile && + Boolean(this.state.summary.profile.is_joinable) + ) { + membershipButtonText = _t("Join this community"); + membershipButtonOnClick = this._onJoinClick; + + membershipButtonExtraClasses = 'mx_GroupView_joinButton'; + membershipContainerExtraClasses = 'mx_GroupView_membershipSection_leave'; + } else if ( + group && + group.myMembership === 'join' && + this.state.editing + ) { + membershipButtonText = _t("Leave this community"); + membershipButtonOnClick = this._onLeaveClick; + membershipButtonTooltip = this.state.isUserPrivileged ? _t("You are an administrator of this community") : _t("You are a member of this community"); - const leaveButtonClasses = classnames({ - "mx_RoomHeader_textButton": true, - "mx_GroupView_textButton": true, - "mx_GroupView_leaveButton": true, - "mx_RoomHeader_textButton_danger": this.state.isUserPrivileged, - }); - return
-
- { /* Empty div for flex alignment */ } -
-
- - { _t("Leave") } - -
-
-
; + + membershipButtonExtraClasses = { + 'mx_GroupView_leaveButton': true, + 'mx_RoomHeader_textButton_danger': this.state.isUserPrivileged, + }; + membershipContainerExtraClasses = 'mx_GroupView_membershipSection_joined'; + } else { + return null; } - return null; + + const membershipButtonClasses = classnames([ + 'mx_RoomHeader_textButton', + 'mx_GroupView_textButton', + ], + membershipButtonExtraClasses, + ); + + const membershipContainerClasses = classnames( + 'mx_GroupView_membershipSection', + membershipContainerExtraClasses, + ); + + return
+
+ { /* Empty div for flex alignment */ } +
+
+ + { membershipButtonText } + +
+
+
; }, _getLongDescriptionNode: function() { @@ -962,6 +1026,7 @@ export default React.createClass({ const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const Spinner = sdk.getComponent("elements.Spinner"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); if (this.state.summaryLoading && this.state.error === null || this.state.saving) { return ; @@ -1112,9 +1177,9 @@ export default React.createClass({ { rightButtons }
- + { bodyNodes } - +
); } else if (this.state.error) { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index f6bbfd247b..d9ac9de693 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -374,7 +374,7 @@ const LoggedInView = React.createClass({ } return ( -
+
{ topBar }
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b37da0144f..92baecb787 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -171,6 +171,10 @@ export default React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, + + // When showing Modal dialogs we need to set aria-hidden on the root app element + // and disable it when there are no dialogs + hideToSRUsers: false, }; return s; }, @@ -287,6 +291,8 @@ export default React.createClass({ this.handleResize(); window.addEventListener('resize', this.handleResize); + this._pageChanging = false; + // check we have the right tint applied for this theme. // N.B. we don't call the whole of setTheme() here as we may be // racing with the theme CSS download finishing from index.js @@ -364,13 +370,58 @@ export default React.createClass({ window.removeEventListener('resize', this.handleResize); }, - componentDidUpdate: function() { + componentWillUpdate: function(props, state) { + if (this.shouldTrackPageChange(this.state, state)) { + this.startPageChangeTimer(); + } + }, + + componentDidUpdate: function(prevProps, prevState) { + if (this.shouldTrackPageChange(prevState, this.state)) { + const durationMs = this.stopPageChangeTimer(); + Analytics.trackPageChange(durationMs); + } if (this.focusComposer) { dis.dispatch({action: 'focus_composer'}); this.focusComposer = false; } }, + startPageChangeTimer() { + // This shouldn't happen because componentWillUpdate and componentDidUpdate + // are used. + if (this._pageChanging) { + console.warn('MatrixChat.startPageChangeTimer: timer already started'); + return; + } + this._pageChanging = true; + performance.mark('riot_MatrixChat_page_change_start'); + }, + + stopPageChangeTimer() { + if (!this._pageChanging) { + console.warn('MatrixChat.stopPageChangeTimer: timer not started'); + return; + } + this._pageChanging = false; + performance.mark('riot_MatrixChat_page_change_stop'); + performance.measure( + 'riot_MatrixChat_page_change_delta', + 'riot_MatrixChat_page_change_start', + 'riot_MatrixChat_page_change_stop', + ); + performance.clearMarks('riot_MatrixChat_page_change_start'); + performance.clearMarks('riot_MatrixChat_page_change_stop'); + const measurement = performance.getEntriesByName('riot_MatrixChat_page_change_delta').pop(); + return measurement.duration; + }, + + shouldTrackPageChange(prevState, state) { + return prevState.currentRoomId !== state.currentRoomId || + prevState.view !== state.view || + prevState.page_type !== state.page_type; + }, + setStateForNewView: function(state) { if (state.view === undefined) { throw new Error("setStateForNewView with no view!"); @@ -608,6 +659,16 @@ export default React.createClass({ case 'send_event': this.onSendEvent(payload.room_id, payload.event); break; + case 'aria_hide_main_app': + this.setState({ + hideToSRUsers: true, + }); + break; + case 'aria_unhide_main_app': + this.setState({ + hideToSRUsers: false, + }); + break; } }, @@ -1171,18 +1232,6 @@ export default React.createClass({ cli.on("crypto.warning", (type) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); switch (type) { - case 'CRYPTO_WARNING_ACCOUNT_MIGRATED': - Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { - title: _t('Cryptography data migrated'), - description: _t( - "A one-off migration of cryptography data has been performed. "+ - "End-to-end encryption will not work if you go back to an older "+ - "version of Riot. If you need to use end-to-end cryptography on "+ - "an older version, log out of Riot first. To retain message history, "+ - "export and re-import your keys.", - ), - }); - break; case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { title: _t('Old cryptography data detected'), @@ -1339,7 +1388,6 @@ export default React.createClass({ if (this.props.onNewScreen) { this.props.onNewScreen(screen); } - Analytics.trackPageChange(); }, onAliasClick: function(event, alias) { diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index da7bebd16a..7a93cfb886 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import sdk from '../../index'; import { _t } from '../../languageHandler'; import dis from '../../dispatcher'; @@ -63,6 +62,8 @@ export default withMatrixClient(React.createClass({ const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); const GroupTile = sdk.getComponent("groups.GroupTile"); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); + let content; let contentHeader; @@ -73,7 +74,7 @@ export default withMatrixClient(React.createClass({ }); contentHeader = groupNodes.length > 0 ?

{ _t('Your Communities') }

:
; content = groupNodes.length > 0 ? - +

{ _t( @@ -92,7 +93,7 @@ export default withMatrixClient(React.createClass({

{ groupNodes }
- : + :
{ _t( "You're not currently a member of any communities.", diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index cbb6001d5f..0fdbc9a349 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -17,9 +17,9 @@ limitations under the License. const React = require("react"); const ReactDOM = require("react-dom"); import PropTypes from 'prop-types'; -const GeminiScrollbar = require('react-gemini-scrollbar'); import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; +import sdk from '../../index.js'; const DEBUG_SCROLL = false; // var DEBUG_SCROLL = true; @@ -224,7 +224,7 @@ module.exports = React.createClass({ onResize: function() { this.props.onResize(); this.checkScroll(); - this.refs.geminiPanel.forceUpdate(); + if (this._gemScroll) this._gemScroll.forceUpdate(); }, // after an update to the contents of the panel, check that the scroll is @@ -665,14 +665,25 @@ module.exports = React.createClass({ throw new Error("ScrollPanel._getScrollNode called when unmounted"); } - return this.refs.geminiPanel.scrollbar.getViewElement(); + if (!this._gemScroll) { + // Likewise, we should have the ref by this point, but if not + // turn the NPE into something meaningful. + throw new Error("ScrollPanel._getScrollNode called before gemini ref collected"); + } + + return this._gemScroll.scrollbar.getViewElement(); + }, + + _collectGeminiScroll: function(gemScroll) { + this._gemScroll = gemScroll; }, render: function() { + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); // TODO: the classnames on the div and ol could do with being updated to // reflect the fact that we don't necessarily contain a list of messages. // it's not obvious why we have a separate div and ol anyway. - return (
@@ -680,7 +691,7 @@ module.exports = React.createClass({ { this.props.children }
-
+ ); }, }); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 4bfebc59b8..790c497a67 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; @@ -101,6 +100,9 @@ const TagPanel = React.createClass({ const GroupsButton = sdk.getComponent('elements.GroupsButton'); const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const TintableSvg = sdk.getComponent('elements.TintableSvg'); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); + const tags = this.state.orderedTags.map((tag, index) => { return 0 ? - {_t("Clear : + /> :
; return
@@ -125,7 +125,7 @@ const TagPanel = React.createClass({ { clearButton }
- ) } - +
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 12f745146e..1a03b5d994 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -624,6 +624,7 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', + roomId: this.props.timelineSet.room.roomId, }); } } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 51b6ff5bc1..85223c4eef 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -30,7 +30,6 @@ import Promise from 'bluebird'; const packageJson = require('../../../package.json'); const UserSettingsStore = require('../../UserSettingsStore'); const CallMediaHandler = require('../../CallMediaHandler'); -const GeminiScrollbar = require('react-gemini-scrollbar'); const Email = require('../../email'); const AddThreepid = require('../../AddThreepid'); const SdkConfig = require('../../SdkConfig'); @@ -795,11 +794,18 @@ module.exports = React.createClass({ } return (
-

{ _t("Bug Report") }

+

{ _t("Debug Logs Submission") }

-

{ _t("Found a bug?") }

+

{ + _t( "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contian messages.", + ) + }

@@ -1111,6 +1117,7 @@ module.exports = React.createClass({ const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const Notifications = sdk.getComponent("settings.Notifications"); const EditableText = sdk.getComponent('elements.EditableText'); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const avatarUrl = ( this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null @@ -1206,8 +1213,9 @@ module.exports = React.createClass({ onCancelClick={this.props.onClose} /> - +

{ _t("Profile") }

@@ -1320,7 +1328,7 @@ module.exports = React.createClass({ { this._renderDeactivateAccount() } -
+
); }, diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 5735a99125..6fb86c9cd8 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -82,7 +83,7 @@ module.exports = React.createClass({ } }, - onClientSync(syncState, prevState) { + onClientSync: function(syncState, prevState) { if (this.unmounted) return; // Consider the client reconnected if there is no error with syncing. diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index cae02ac408..e547cf0fa7 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -48,12 +48,33 @@ module.exports = React.createClass({ }; }, + componentWillMount: function() { + MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); + }, + + componentWillUnmount: function() { + const cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener("RoomState.events", this.onRoomStateEvents); + } + }, + componentWillReceiveProps: function(newProps) { this.setState({ urls: this.getImageUrls(newProps), }); }, + onRoomStateEvents: function(ev) { + if (ev.getRoomId() !== this.props.room.roomId || + ev.getType() !== 'm.room.avatar' + ) return; + + this.setState({ + urls: this.getImageUrls(this.props), + }); + }, + getImageUrls: function(props) { return [ ContentRepo.getHttpUriForMxc( diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 66e5fcb0c0..21a2477c37 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import FocusTrap from 'focus-trap-react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; @@ -37,9 +38,6 @@ export default React.createClass({ // onFinished callback to call when Escape is pressed onFinished: PropTypes.func.isRequired, - // callback to call when Enter is pressed - onEnterPressed: PropTypes.func, - // called when a key is pressed onKeyDown: PropTypes.func, @@ -52,6 +50,10 @@ export default React.createClass({ // children should be the content of the dialog children: PropTypes.node, + + // Id of content element + // If provided, this is used to add a aria-describedby attribute + contentId: React.PropTypes.string, }, childContextTypes: { @@ -76,12 +78,6 @@ export default React.createClass({ e.stopPropagation(); e.preventDefault(); this.props.onFinished(); - } else if (e.keyCode === KeyCode.ENTER) { - if (this.props.onEnterPressed) { - e.stopPropagation(); - e.preventDefault(); - this.props.onEnterPressed(e); - } } }, @@ -93,17 +89,28 @@ export default React.createClass({ const TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( -
+ -
+
{ this.props.title }
{ this.props.children } -
+
); }, }); diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index dc4f3f77db..e2387064cf 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -59,6 +59,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { ); tiles.push(
{ _t("Start new chat") }
; - content =
+ content =
{ _t('You already have existing direct chats with this user:') }
{ this.state.tiles } @@ -146,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { if (this.state.busyProfile) { profile = ; } else if (this.state.profileError) { - profile =
+ profile =
Unable to load profile information for { this.props.userId }
; } else { @@ -162,14 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
; } content =
-
+

{ _t('Click on the button below to start chatting!') }

{ profile }
+ onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
; } @@ -178,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { { content } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index f347261470..b65d98d78d 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -114,10 +114,10 @@ export default React.createClass({ return ( -
+
{ avatar }
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 86a2b2498c..04f99a0e15 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -112,7 +112,7 @@ export default React.createClass({ // XXX: We should catch errcodes and give sensible i18ned messages for them, // rather than displaying what the server gives us, but synapse doesn't give // any yet. - createErrorNode =
+ createErrorNode =
{ _t('Something went wrong whilst creating your community') }
{ this.state.createError.message }
; @@ -120,7 +120,6 @@ export default React.createClass({ return (
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index d9287d23da..51693a19c9 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -45,30 +45,31 @@ export default React.createClass({ const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( -
-
- -
-
- -
-
- -
- { _t('Advanced options') } -
- - + +
+
+
-
-
+
+ +
+
+ +
+ { _t('Advanced options') } +
+ + +
+
+
+ diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 2af2d6214f..a055f07629 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -52,22 +52,18 @@ export default React.createClass({ }; }, - componentDidMount: function() { - if (this.props.focus) { - this.refs.button.focus(); - } - }, - render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( -
+ title={this.props.title || _t('Error')} + contentId='mx_Dialog_content' + > +
{ this.props.description || _t('An error has occurred.') }
-
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index a47702305c..b682156072 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -73,11 +73,12 @@ export default React.createClass({ let content; if (this.state.authError) { content = ( -
-
{ this.state.authError.message || this.state.authError.toString() }
+
+
{ this.state.authError.message || this.state.authError.toString() }

{ _t("Dismiss") } @@ -85,7 +86,7 @@ export default React.createClass({ ); } else { content = ( -
+
{ content } diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index 00bcc942a1..b9b64a69d2 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -126,11 +126,11 @@ export default React.createClass({ text = _t(text, {displayName: displayName}); return ( -
+

{ text }

-