diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index c6f25c2480..430546d281 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -2,7 +2,6 @@ src/autocomplete/AutocompleteProvider.js src/autocomplete/Autocompleter.js -src/autocomplete/EmojiProvider.js src/autocomplete/UserProvider.js src/component-index.js src/components/structures/BottomLeftMenu.js @@ -17,7 +16,6 @@ src/components/structures/MessagePanel.js src/components/structures/NotificationPanel.js src/components/structures/RoomDirectory.js src/components/structures/RoomStatusBar.js -src/components/structures/RoomSubList.js src/components/structures/RoomView.js src/components/structures/ScrollPanel.js src/components/structures/SearchBox.js @@ -29,7 +27,6 @@ src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js src/components/views/dialogs/ChangelogDialog.js -src/components/views/dialogs/ChatCreateOrReuseDialog.js src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/SetPasswordDialog.js src/components/views/dialogs/UnknownDeviceDialog.js @@ -37,7 +34,6 @@ src/components/views/directory/NetworkDropdown.js src/components/views/elements/AddressSelector.js src/components/views/elements/DeviceVerifyButtons.js src/components/views/elements/DirectorySearchBox.js -src/components/views/elements/EditableText.js src/components/views/elements/ImageView.js src/components/views/elements/InlineSpinner.js src/components/views/elements/MemberEventListSummary.js @@ -81,7 +77,6 @@ src/components/views/rooms/TopUnreadMessagesBar.js src/components/views/rooms/UserTile.js src/components/views/settings/AddPhoneNumber.js src/components/views/settings/ChangeAvatar.js -src/components/views/settings/ChangeDisplayName.js src/components/views/settings/ChangePassword.js src/components/views/settings/DevicesPanel.js src/components/views/settings/IntegrationsManager.js diff --git a/package.json b/package.json index 33aa428d4b..74f97bad9c 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "glob": "^5.0.14", "highlight.js": "^9.0.0", "isomorphic-fetch": "^2.2.1", - "linkifyjs": "^2.1.3", + "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", "matrix-js-sdk": "0.10.4", diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 0a708a8edc..f1de103d3b 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -70,6 +70,7 @@ limitations under the License. flex: 1; display: flex; flex-direction: column; + cursor: text; } .mx_MessageComposer_input { diff --git a/src/autocomplete/CommunityProvider.js b/src/autocomplete/CommunityProvider.js index 6b5438e8c8..6bcf1a02fd 100644 --- a/src/autocomplete/CommunityProvider.js +++ b/src/autocomplete/CommunityProvider.js @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,7 +27,7 @@ import {makeGroupPermalink} from "../matrix-to"; import type {Completion, SelectionRange} from "./Autocompleter"; import FlairStore from "../stores/FlairStore"; -const COMMUNITY_REGEX = /(?=\+)(\S*)/g; +const COMMUNITY_REGEX = /\B\+\S*/g; function score(query, space) { const index = space.indexOf(query); diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js index 762b285685..9d4d4d0598 100644 --- a/src/autocomplete/QueryMatcher.js +++ b/src/autocomplete/QueryMatcher.js @@ -1,6 +1,7 @@ //@flow /* Copyright 2017 Aviral Dasgupta +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,6 +28,10 @@ class KeyMap { priorityMap = new Map(); } +function stripDiacritics(str: string): string { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); +} + export default class QueryMatcher { /** * @param {object[]} objects the objects to perform a match on @@ -46,10 +51,11 @@ export default class QueryMatcher { objects.forEach((object, i) => { const keyValues = _at(object, keys); for (const keyValue of keyValues) { - if (!map.hasOwnProperty(keyValue)) { - map[keyValue] = []; + const key = stripDiacritics(keyValue).toLowerCase(); + if (!map.hasOwnProperty(key)) { + map[key] = []; } - map[keyValue].push(object); + map[key].push(object); } keyMap.priorityMap.set(object, i); }); @@ -82,7 +88,7 @@ export default class QueryMatcher { } match(query: String): Array { - query = query.toLowerCase(); + query = stripDiacritics(query).toLowerCase(); if (this.options.shouldMatchWordsOnly) { query = query.replace(/[^\w]/g, ''); } @@ -91,7 +97,7 @@ export default class QueryMatcher { } const results = []; this.keyMap.keys.forEach((key) => { - let resultKey = key.toLowerCase(); + let resultKey = key; if (this.options.shouldMatchWordsOnly) { resultKey = resultKey.replace(/[^\w]/g, ''); } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index c222ae95d4..4e41a4221a 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -2,6 +2,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,7 +29,7 @@ import _sortBy from 'lodash/sortBy'; import {makeRoomPermalink} from "../matrix-to"; import type {Completion, SelectionRange} from "./Autocompleter"; -const ROOM_REGEX = /(?=#)(\S*)/g; +const ROOM_REGEX = /\B#\S*/g; function score(query, space) { const index = space.indexOf(query); diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 85837d5ebb..2806b1dec8 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -3,6 +3,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,11 +27,11 @@ import FuzzyMatcher from './FuzzyMatcher'; import _sortBy from 'lodash/sortBy'; import MatrixClientPeg from '../MatrixClientPeg'; -import type {Room, RoomMember} from 'matrix-js-sdk'; +import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk'; import {makeUserPermalink} from "../matrix-to"; -import type {SelectionRange} from "./Autocompleter"; +import type {Completion, SelectionRange} from "./Autocompleter"; -const USER_REGEX = /@\S*/g; +const USER_REGEX = /\B@\S*/g; export default class UserProvider extends AutocompleteProvider { users: Array = null; @@ -44,7 +45,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, - shouldMatchWordsOnly: false + shouldMatchWordsOnly: false, }); this._onRoomTimelineBound = this._onRoomTimeline.bind(this); @@ -61,7 +62,7 @@ export default class UserProvider extends AutocompleteProvider { } } - _onRoomTimeline(ev, room, toStartOfTimeline, removed, data) { + _onRoomTimeline(ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: Object) { if (!room) return; if (removed) return; if (room.roomId !== this.room.roomId) return; @@ -77,7 +78,7 @@ export default class UserProvider extends AutocompleteProvider { this.onUserSpoke(ev.sender); } - _onRoomStateMember(ev, state, member) { + _onRoomStateMember(ev: MatrixEvent, state: RoomState, member: RoomMember) { // ignore members in other rooms if (member.roomId !== this.room.roomId) { return; @@ -87,7 +88,7 @@ export default class UserProvider extends AutocompleteProvider { this.users = null; } - async getCompletions(query: string, selection: SelectionRange, force?: boolean = false) { + async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); // Disable autocompletions when composing commands because of various issues @@ -128,7 +129,7 @@ export default class UserProvider extends AutocompleteProvider { return completions; } - getName() { + getName(): string { return '👥 ' + _t('Users'); } @@ -141,13 +142,9 @@ export default class UserProvider extends AutocompleteProvider { } const currentUserId = MatrixClientPeg.get().credentials.userId; - this.users = this.room.getJoinedMembers().filter((member) => { - if (member.userId !== currentUserId) return true; - }); + this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId); - this.users = _sortBy(this.users, (member) => - 1E20 - lastSpoken[member.userId] || 1E20, - ); + this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20); this.matcher.setObjects(this.users); } diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js index 91ec312f43..adc8dfd11c 100644 --- a/src/components/structures/ContextualMenu.js +++ b/src/components/structures/ContextualMenu.js @@ -64,7 +64,9 @@ export default class ContextualMenu extends React.Component { // The component to render as the context menu elementClass: PropTypes.element.isRequired, // on resize callback - windowResize: PropTypes.func + windowResize: PropTypes.func, + // method to close menu + closeMenu: PropTypes.func, }; constructor() { @@ -73,6 +75,7 @@ export default class ContextualMenu extends React.Component { contextMenuRect: null, }; + this.onContextMenu = this.onContextMenu.bind(this); this.collectContextMenuRect = this.collectContextMenuRect.bind(this); } @@ -85,6 +88,28 @@ export default class ContextualMenu extends React.Component { }); } + onContextMenu(e) { + if (this.props.closeMenu) { + this.props.closeMenu(); + + e.preventDefault(); + const x = e.clientX; + const y = e.clientY; + + // XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst + // a context menu and its click-guard are up without completely rewriting how the context menus work. + setImmediate(() => { + const clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent( + 'contextmenu', true, true, window, 0, + 0, 0, x, y, false, false, + false, false, 0, null, + ); + document.elementFromPoint(x, y).dispatchEvent(clickEvent); + }); + } + } + render() { const position = {}; let chevronFace = null; @@ -195,7 +220,7 @@ export default class ContextualMenu extends React.Component { { chevron } - { props.hasBackground &&
} + { props.hasBackground &&
}
; } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 8034923158..36f4719abb 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -25,6 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg'; import MemberAvatar from '../views/avatars/MemberAvatar'; import Resend from '../../Resend'; import * as cryptodevices from '../../cryptodevices'; +import dis from '../../dispatcher'; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -157,10 +158,12 @@ module.exports = React.createClass({ _onResendAllClick: function() { Resend.resendUnsentEvents(this.props.room); + dis.dispatch({action: 'focus_composer'}); }, _onCancelAllClick: function() { Resend.cancelUnsentEvents(this.props.room); + dis.dispatch({action: 'focus_composer'}); }, _onShowDevicesClick: function() { diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 33ff17caa4..0b6f7eaa45 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -1,6 +1,7 @@ /* -Copyright 2017 Vector Creations Ltd Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 Vector Creations 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. @@ -15,30 +16,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - -var React = require('react'); -var ReactDOM = require('react-dom'); -var classNames = require('classnames'); -var sdk = require('../../index'); +import React from 'react'; +import classNames from 'classnames'; +import sdk from '../../index'; import { Droppable } from 'react-beautiful-dnd'; import { _t } from '../../languageHandler'; -var dis = require('../../dispatcher'); -var Unread = require('../../Unread'); -var MatrixClientPeg = require('../../MatrixClientPeg'); -var RoomNotifs = require('../../RoomNotifs'); -var FormattingUtils = require('../../utils/FormattingUtils'); -var AccessibleButton = require('../../components/views/elements/AccessibleButton'); -import Modal from '../../Modal'; +import dis from '../../dispatcher'; +import Unread from '../../Unread'; +import * as RoomNotifs from '../../RoomNotifs'; +import * as FormattingUtils from '../../utils/FormattingUtils'; import { KeyCode } from '../../Keyboard'; // turn this on for drop & drag console debugging galore -var debug = false; +const debug = false; const TRUNCATE_AT = 10; -var RoomSubList = React.createClass({ +const RoomSubList = React.createClass({ displayName: 'RoomSubList', debug: debug, @@ -77,8 +72,10 @@ var RoomSubList = React.createClass({ getDefaultProps: function() { return { - onHeaderClick: function() {}, // NOP - onShowMoreRooms: function() {}, // NOP + onHeaderClick: function() { + }, // NOP + onShowMoreRooms: function() { + }, // NOP extraTiles: [], isInvite: false, }; @@ -115,7 +112,7 @@ var RoomSubList = React.createClass({ // The header is collapsable if it is hidden or not stuck // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method isCollapsableOnClick: function() { - var stuck = this.refs.header.dataset.stuck; + const stuck = this.refs.header.dataset.stuck; if (this.state.hidden || stuck === undefined || stuck === "none") { return true; } else { @@ -141,12 +138,12 @@ var RoomSubList = React.createClass({ onClick: function(ev) { if (this.isCollapsableOnClick()) { // The header isCollapsable, so the click is to be interpreted as collapse and truncation logic - var isHidden = !this.state.hidden; - this.setState({ hidden : isHidden }); + const isHidden = !this.state.hidden; + this.setState({hidden: isHidden}); if (isHidden) { // as good a way as any to reset the truncate state - this.setState({ truncateAt : TRUNCATE_AT }); + this.setState({truncateAt: TRUNCATE_AT}); } this.props.onShowMoreRooms(); @@ -161,7 +158,7 @@ var RoomSubList = React.createClass({ dis.dispatch({ action: 'view_room', room_id: roomId, - clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)), + clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)), }); }, @@ -171,17 +168,17 @@ var RoomSubList = React.createClass({ }, _shouldShowMentionBadge: function(roomNotifState) { - return roomNotifState != RoomNotifs.MUTE; + return roomNotifState !== RoomNotifs.MUTE; }, /** * Total up all the notification counts from the rooms * - * @param {Number} If supplied will only total notifications for rooms outside the truncation number + * @param {Number} truncateAt If supplied will only total notifications for rooms outside the truncation number * @returns {Array} The array takes the form [total, highlight] where highlight is a bool */ roomNotificationCount: function(truncateAt) { - var self = this; + const self = this; if (this.props.isInvite) { return [0, true]; @@ -189,9 +186,9 @@ var RoomSubList = React.createClass({ return this.props.list.reduce(function(result, room, index) { if (truncateAt === undefined || index >= truncateAt) { - var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId); - var highlight = room.getUnreadNotificationCount('highlight') > 0; - var notificationCount = room.getUnreadNotificationCount(); + const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId); + const highlight = room.getUnreadNotificationCount('highlight') > 0; + const notificationCount = room.getUnreadNotificationCount(); const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState); const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState); @@ -241,29 +238,27 @@ var RoomSubList = React.createClass({ }, _getHeaderJsx: function() { - var TintableSvg = sdk.getComponent("elements.TintableSvg"); + const subListNotifications = this.roomNotificationCount(); + const subListNotifCount = subListNotifications[0]; + const subListNotifHighlight = subListNotifications[1]; - var subListNotifications = this.roomNotificationCount(); - var subListNotifCount = subListNotifications[0]; - var subListNotifHighlight = subListNotifications[1]; + const totalTiles = this.props.list.length + (this.props.extraTiles || []).length; + const roomCount = totalTiles > 0 ? totalTiles : ''; - var totalTiles = this.props.list.length + (this.props.extraTiles || []).length; - var roomCount = totalTiles > 0 ? totalTiles : ''; - - var chevronClasses = classNames({ + const chevronClasses = classNames({ 'mx_RoomSubList_chevron': true, 'mx_RoomSubList_chevronRight': this.state.hidden, 'mx_RoomSubList_chevronDown': !this.state.hidden, }); - var badgeClasses = classNames({ + const badgeClasses = classNames({ 'mx_RoomSubList_badge': true, 'mx_RoomSubList_badgeHighlight': subListNotifHighlight, }); - var badge; + let badge; if (subListNotifCount > 0) { - badge =
{ FormattingUtils.formatCount(subListNotifCount) }
; + badge =
{FormattingUtils.formatCount(subListNotifCount)}
; } else if (this.props.isInvite) { // no notifications but highlight anyway because this is an invite badge badge =
!
; @@ -271,7 +266,7 @@ var RoomSubList = React.createClass({ // When collapsed, allow a long hover on the header to show user // the full tag name and room count - var title; + let title; if (this.props.collapsed) { title = this.props.label; if (roomCount !== '') { @@ -279,63 +274,66 @@ var RoomSubList = React.createClass({ } } - var incomingCall; + let incomingCall; if (this.props.incomingCall) { - var self = this; + const self = this; // Check if the incoming call is for this section - var incomingCallRoom = this.props.list.filter(function(room) { + const incomingCallRoom = this.props.list.filter(function(room) { return self.props.incomingCall.roomId === room.roomId; }); if (incomingCallRoom.length === 1) { - var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); - incomingCall = ; + const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox"); + incomingCall = + ; } } - var tabindex = this.props.searchFilter === "" ? "0" : "-1"; + const tabindex = this.props.searchFilter === "" ? "0" : "-1"; + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return ( -
- - { this.props.collapsed ? '' : this.props.label } -
{ roomCount }
-
- { badge } - { incomingCall } +
+ + {this.props.collapsed ? '' : this.props.label} +
{roomCount}
+
+ {badge} + {incomingCall}
); }, _createOverflowTile: function(overflowCount, totalCount) { - var content =
; + let content =
; - var overflowNotifications = this.roomNotificationCount(TRUNCATE_AT); - var overflowNotifCount = overflowNotifications[0]; - var overflowNotifHighlight = overflowNotifications[1]; + const overflowNotifications = this.roomNotificationCount(TRUNCATE_AT); + const overflowNotifCount = overflowNotifications[0]; + const overflowNotifHighlight = overflowNotifications[1]; if (overflowNotifCount && !this.props.collapsed) { content = FormattingUtils.formatCount(overflowNotifCount); } - var badgeClasses = classNames({ + const badgeClasses = classNames({ 'mx_RoomSubList_moreBadge': true, 'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed, 'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed, }); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); return ( -
-
{ _t("more") }
-
{ content }
+
+
{_t("more")}
+
{content}
); }, _showFullMemberList: function() { this.setState({ - truncateAt: -1 + truncateAt: -1, }); this.props.onShowMoreRooms(); @@ -343,37 +341,39 @@ var RoomSubList = React.createClass({ }, render: function() { - var connectDropTarget = this.props.connectDropTarget; - var TruncatedList = sdk.getComponent('elements.TruncatedList'); - - var label = this.props.collapsed ? null : this.props.label; + const TruncatedList = sdk.getComponent('elements.TruncatedList'); let content; - if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) { - content = this.props.emptyContent; + if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) { + // if no search filter is applied and there is a placeholder defined then show it, otherwise show nothing + if (!this.props.searchFilter && this.props.emptyContent) { + content = this.props.emptyContent; + } else { + // don't show an empty sublist + return null; + } } else { content = this.makeRoomTiles(); content.push(...this.props.extraTiles); } if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) { - var subList; - var classes = "mx_RoomSubList"; + let subList; + const classes = "mx_RoomSubList"; if (!this.state.hidden) { - subList = - { content } - ; - } - else { - subList = - ; + subList = + {content} + ; + } else { + subList = + ; } const subListContent =
- { this._getHeaderJsx() } - { subList } + {this._getHeaderJsx()} + {subList}
; return this.props.editable ? @@ -381,23 +381,22 @@ var RoomSubList = React.createClass({ droppableId={"room-sub-list-droppable_" + this.props.tagName} type="draggable-RoomTile" > - { (provided, snapshot) => ( + {(provided, snapshot) => (
- { subListContent } + {subListContent}
- ) } + )} : subListContent; - } - else { - var Loader = sdk.getComponent("elements.Spinner"); + } else { + const Loader = sdk.getComponent("elements.Spinner"); return (
- { this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined } - { (this.props.showSpinner && !this.state.hidden) ? : undefined } + {this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined} + {(this.props.showSpinner && !this.state.hidden) ? : undefined}
); } - } + }, }); module.exports = RoomSubList; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 4beafb099c..8f03199a62 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations 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. @@ -618,9 +619,11 @@ module.exports = React.createClass({ } }, - _updatePreviewUrlVisibility: function(room) { + _updatePreviewUrlVisibility: function({roomId}) { + // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit + const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'; this.setState({ - showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId), + showUrlPreview: SettingsStore.getValue(key, roomId), }); }, @@ -645,19 +648,23 @@ module.exports = React.createClass({ }, onAccountData: function(event) { - if (event.getType() === "org.matrix.preview_urls" && this.state.room) { + const type = event.getType(); + if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) { + // non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls` this._updatePreviewUrlVisibility(this.state.room); } }, onRoomAccountData: function(event, room) { if (room.roomId == this.state.roomId) { - if (event.getType() === "org.matrix.room.color_scheme") { + const type = event.getType(); + if (type === "org.matrix.room.color_scheme") { const color_scheme = event.getContent(); // XXX: we should validate the event console.log("Tinter.tint from onRoomAccountData"); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); - } else if (event.getType() === "org.matrix.room.preview_urls") { + } else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") { + // non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls` this._updatePreviewUrlVisibility(room); } } diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 59cdb61fd6..4802244c74 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -15,10 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; +import {EventStatus} from 'matrix-js-sdk'; import MatrixClientPeg from '../../../MatrixClientPeg'; import dis from '../../../dispatcher'; @@ -220,7 +219,10 @@ module.exports = React.createClass({ let replyButton; let collapseReplyThread; - if (eventStatus === 'not_sent') { + // status is SENT before remote-echo, null after + const isSent = !eventStatus || eventStatus === EventStatus.SENT; + + if (eventStatus === EventStatus.NOT_SENT) { resendButton = (
{ _t('Resend') } @@ -228,7 +230,7 @@ module.exports = React.createClass({ ); } - if (!eventStatus && this.state.canRedact) { + if (isSent && this.state.canRedact) { redactButton = (
{ _t('Remove') } @@ -236,7 +238,7 @@ module.exports = React.createClass({ ); } - if (eventStatus === "queued" || eventStatus === "not_sent") { + if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) { cancelButton = (
{ _t('Cancel Sending') } @@ -244,7 +246,7 @@ module.exports = React.createClass({ ); } - if (!eventStatus && this.props.mxEvent.getType() === 'm.room.message') { + if (isSent && this.props.mxEvent.getType() === 'm.room.message') { const content = this.props.mxEvent.getContent(); if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) { forwardButton = ( diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 5ca2fc6ed8..70c9c711fc 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -72,14 +72,12 @@ export default React.createClass({ _updateRelatedGroups() { if (this.unmounted) return; - const relatedGroupsEvent = this.context.matrixClient - .getRoom(this.props.mxEvent.getRoomId()) - .currentState - .getStateEvents('m.room.related_groups', ''); + const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId()); + if (!room) return; + + const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', ''); this.setState({ - relatedGroups: relatedGroupsEvent ? - relatedGroupsEvent.getContent().groups || [] - : [], + relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [], }); }, diff --git a/src/components/views/room_settings/UrlPreviewSettings.js b/src/components/views/room_settings/UrlPreviewSettings.js index ed58d610aa..fe2a2bacf4 100644 --- a/src/components/views/room_settings/UrlPreviewSettings.js +++ b/src/components/views/room_settings/UrlPreviewSettings.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017 Travis Ralston +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. @@ -15,6 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {MatrixClient} from "matrix-js-sdk"; const React = require('react'); import PropTypes from 'prop-types'; const sdk = require("../../../index"); @@ -29,6 +31,10 @@ module.exports = React.createClass({ room: PropTypes.object, }, + contextTypes: { + matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + }, + saveSettings: function() { const promises = []; if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save()); @@ -39,42 +45,58 @@ module.exports = React.createClass({ render: function() { const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); const roomId = this.props.room.roomId; + const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId); let previewsForAccount = null; - if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) { - previewsForAccount = ( - _t("You have enabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) - ); - } else { - previewsForAccount = ( - _t("You have disabled URL previews by default.", {}, { 'a': (sub)=>{ sub } }) - ); - } - let previewsForRoom = null; - if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) { - previewsForRoom = ( - - ); - } else { - let str = _td("URL previews are enabled by default for participants in this room."); - if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) { - str = _td("URL previews are disabled by default for participants in this room."); + + if (!isEncrypted) { + // Only show account setting state and room state setting state in non-e2ee rooms where they apply + const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled"); + if (accountEnabled) { + previewsForAccount = ( + _t("You have enabled URL previews by default.", {}, { + 'a': (sub)=>{ sub }, + }) + ); + } else if (accountEnabled) { + previewsForAccount = ( + _t("You have disabled URL previews by default.", {}, { + 'a': (sub)=>{ sub }, + }) + ); } - previewsForRoom = (); + + if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) { + previewsForRoom = ( + + ); + } else { + let str = _td("URL previews are enabled by default for participants in this room."); + if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) { + str = _td("URL previews are disabled by default for participants in this room."); + } + previewsForRoom = (); + } + } else { + previewsForAccount = ( + _t("In encrypted rooms, like this one, URL previews are disabled by default to ensure that your " + + "homeserver (where the previews are generated) cannot gather information about links you see in " + + "this room.") + ); } - const previewsForRoomAccount = ( - @@ -83,8 +105,13 @@ module.exports = React.createClass({ return (

{ _t("URL Previews") }

- - +
+ { _t('When someone puts a URL in their message, a URL preview can be shown to give more ' + + 'information about that link such as the title, description, and an image from the website.') } +
+
+ { previewsForAccount } +
{ previewsForRoom }
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index aa2f28024e..537545c904 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -621,13 +621,14 @@ module.exports = withMatrixClient(React.createClass({ switch (this.props.tileShape) { case 'notif': { + const EmojiText = sdk.getComponent('elements.EmojiText'); const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); return (
{ avatar } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 8477176cc2..90a14d72fc 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -157,6 +157,7 @@ export default class MessageComposerInput extends React.Component { this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); this.onTextPasted = this.onTextPasted.bind(this); + this.focusComposer = this.focusComposer.bind(this); const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'); @@ -270,13 +271,12 @@ export default class MessageComposerInput extends React.Component { } onAction = (payload) => { - const editor = this.refs.editor; let contentState = this.state.editorState.getCurrentContent(); switch (payload.action) { case 'reply_to_event': case 'focus_composer': - editor.focus(); + this.focusComposer(); break; case 'insert_mention': { // Pretend that we've autocompleted this user because keeping two code @@ -319,7 +319,7 @@ export default class MessageComposerInput extends React.Component { let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); editorState = EditorState.moveSelectionToEnd(editorState); this.onEditorContentChanged(editorState); - editor.focus(); + this.focusComposer(); } } break; @@ -1155,6 +1155,10 @@ export default class MessageComposerInput extends React.Component { this.handleKeyCommand('toggle-mode'); }; + focusComposer() { + this.refs.editor.focus(); + } + render() { const activeEditorState = this.state.originalEditorState || this.state.editorState; @@ -1179,7 +1183,7 @@ export default class MessageComposerInput extends React.Component { activeEditorState.getCurrentContent().getBlocksAsArray()); return ( -
+
disabled URL previews by default.": "You have disabled URL previews by default.", "URL previews are enabled by default for participants in this room.": "URL previews are enabled by default for participants in this room.", "URL previews are disabled by default for participants in this room.": "URL previews are disabled by default for participants in this room.", + "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", "URL Previews": "URL Previews", + "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "Cannot add any more widgets": "Cannot add any more widgets", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "Add a widget": "Add a widget", diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js index d72319948a..132695b9de 100644 --- a/src/linkify-matrix.js +++ b/src/linkify-matrix.js @@ -66,10 +66,13 @@ function matrixLinkify(linkify) { S_HASH_NAME_COLON.on(TT.DOMAIN, S_HASH_NAME_COLON_DOMAIN); S_HASH_NAME_COLON.on(TT.LOCALHOST, S_ROOMALIAS); // accept #foo:localhost + S_HASH_NAME_COLON.on(TT.TLD, S_ROOMALIAS); // accept #foo:com (mostly for (TLD|DOMAIN)+ mixing) S_HASH_NAME_COLON_DOMAIN.on(TT.DOT, S_HASH_NAME_COLON_DOMAIN_DOT); S_HASH_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_HASH_NAME_COLON_DOMAIN); S_HASH_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_ROOMALIAS); + S_ROOMALIAS.on(TT.DOT, S_HASH_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk) + const USERID = function(value) { MultiToken.call(this, value); @@ -107,10 +110,13 @@ function matrixLinkify(linkify) { S_AT_NAME_COLON.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN); S_AT_NAME_COLON.on(TT.LOCALHOST, S_USERID); // accept @foo:localhost + S_AT_NAME_COLON.on(TT.TLD, S_USERID); // accept @foo:com (mostly for (TLD|DOMAIN)+ mixing) S_AT_NAME_COLON_DOMAIN.on(TT.DOT, S_AT_NAME_COLON_DOMAIN_DOT); S_AT_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_AT_NAME_COLON_DOMAIN); S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID); + S_USERID.on(TT.DOT, S_AT_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk) + const GROUPID = function(value) { MultiToken.call(this, value); @@ -148,9 +154,12 @@ function matrixLinkify(linkify) { S_PLUS_NAME_COLON.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN); S_PLUS_NAME_COLON.on(TT.LOCALHOST, S_GROUPID); // accept +foo:localhost + S_PLUS_NAME_COLON.on(TT.TLD, S_GROUPID); // accept +foo:com (mostly for (TLD|DOMAIN)+ mixing) S_PLUS_NAME_COLON_DOMAIN.on(TT.DOT, S_PLUS_NAME_COLON_DOMAIN_DOT); S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.DOMAIN, S_PLUS_NAME_COLON_DOMAIN); S_PLUS_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_GROUPID); + + S_GROUPID.on(TT.DOT, S_PLUS_NAME_COLON_DOMAIN_DOT); // accept repeated TLDs (e.g .org.uk) } // stubs, overwritten in MatrixChat's componentDidMount diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 9f35b0bfec..84178e2673 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -245,6 +245,13 @@ export const SETTINGS = { }, default: true, }, + "urlPreviewsEnabled_e2ee": { + supportedLevels: ['room-device', 'room-account'], + displayName: { + "room-account": _td("Enable URL previews for this room (only affects you)"), + }, + default: false, + }, "roomColor": { supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM, displayName: _td("Room Colour"), diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js index 74dbf9eed0..d0dadc2de7 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.js +++ b/src/settings/handlers/RoomAccountSettingsHandler.js @@ -74,7 +74,7 @@ export default class RoomAccountSettingsHandler extends SettingsHandler { return cli !== undefined && cli !== null; } - _getSettings(roomId, eventType = "im.vector.settings") { + _getSettings(roomId, eventType = "im.vector.web.settings") { const room = MatrixClientPeg.get().getRoom(roomId); if (!room) return null;