diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 8b3a368f4d..2ab635081f 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -186,7 +186,7 @@ module.exports = { * * highlights: optional list of words to highlight, ordered by longest word first * - * opts.highlightLink: optional href to add to highlights + * opts.highlightLink: optional href to add to highlighted words */ bodyToHtml: function(content, highlights, opts) { opts = opts || {}; diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 305994aa0e..39f393b242 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -113,6 +113,35 @@ module.exports = { }); }, + getUrlPreviewsDisabled: function() { + var event = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + return (event && event.getContent().disable); + }, + + setUrlPreviewsDisabled: function(disabled) { + // FIXME: handle errors + MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", { + disable: disabled + }); + }, + + getSyncedSettings: function() { + var event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + return event ? event.getContent() : {}; + }, + + getSyncedSetting: function(type) { + var settings = this.getSyncedSettings(); + return settings[type]; + }, + + setSyncedSetting: function(type, value) { + var settings = this.getSyncedSettings(); + settings[type] = value; + // FIXME: handle errors + MatrixClientPeg.get().setAccountData("im.vector.web.settings", settings); + }, + isFeatureEnabled: function(feature: string): boolean { return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true'; }, diff --git a/src/component-index.js b/src/component-index.js index 4aa0efe21f..5fadb18b6a 100644 --- a/src/component-index.js +++ b/src/component-index.js @@ -74,6 +74,8 @@ module.exports.components['views.messages.TextualEvent'] = require('./components module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); module.exports.components['views.room_settings.AliasSettings'] = require('./components/views/room_settings/AliasSettings'); module.exports.components['views.room_settings.ColorSettings'] = require('./components/views/room_settings/ColorSettings'); +module.exports.components['views.room_settings.UrlPreviewSettings'] = require('./components/views/room_settings/UrlPreviewSettings'); +module.exports.components['views.rooms.Autocomplete'] = require('./components/views/rooms/Autocomplete'); module.exports.components['views.rooms.AuxPanel'] = require('./components/views/rooms/AuxPanel'); module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile'); module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index c8e878118b..53efac6406 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -44,6 +44,9 @@ module.exports = React.createClass({ // ID of an event to highlight. If undefined, no event will be highlighted. highlightedEventId: React.PropTypes.string, + // Should we show URL Previews + showUrlPreview: React.PropTypes.bool, + // event after which we should show a read marker readMarkerEventId: React.PropTypes.string, @@ -365,6 +368,7 @@ module.exports = React.createClass({ onWidgetLoad={this._onWidgetLoad} readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} + showUrlPreview={this.props.showUrlPreview} checkUnmounting={this._isUnmounting} eventSendStatus={mxEv.status} last={last} isSelectedEvent={highlight}/> diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index abcccc48b8..f73b90ae28 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -338,6 +338,10 @@ module.exports = React.createClass({ // ignore events for other rooms if (!this.state.room || room.roomId != this.state.room.roomId) return; + if (ev.getType() === "org.matrix.room.preview_urls") { + this._updatePreviewUrlVisibility(room); + } + // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; @@ -371,6 +375,7 @@ module.exports = React.createClass({ // after a successful peek, or after we join the room). _onRoomLoaded: function(room) { this._calculatePeekRules(room); + this._updatePreviewUrlVisibility(room); }, _calculatePeekRules: function(room) { @@ -389,6 +394,42 @@ module.exports = React.createClass({ } }, + _updatePreviewUrlVisibility: function(room) { + console.log("_updatePreviewUrlVisibility"); + + // check our per-room overrides + var roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls"); + if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) { + this.setState({ + showUrlPreview: !roomPreviewUrls.getContent().disable + }); + return; + } + + // check our global disable override + var userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls"); + if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) { + this.setState({ + showUrlPreview: false + }); + return; + } + + // check the room state event + var roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', ''); + if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) { + this.setState({ + showUrlPreview: false + }); + return; + } + + // otherwise, we assume they're on. + this.setState({ + showUrlPreview: true + }); + }, + onRoom: function(room) { // This event is fired when the room is 'stored' by the JS SDK, which // means it's now a fully-fledged room object ready to be used, so @@ -419,14 +460,17 @@ module.exports = React.createClass({ Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); }, - onRoomAccountData: function(room, event) { - if (room.roomId == this.props.roomId) { - if (event.getType === "org.matrix.room.color_scheme") { + onRoomAccountData: function(event, room) { + if (room.roomId == this.state.roomId) { + if (event.getType() === "org.matrix.room.color_scheme") { var 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") { + this._updatePreviewUrlVisibility(room); + } } }, @@ -1506,6 +1550,8 @@ module.exports = React.createClass({ hideMessagePanel = true; } + console.log("ShowUrlPreview for %s is %s", this.state.room.roomId, this.state.showUrlPreview); + var messagePanel = ( ); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index d804dfd6b9..798ee03e73 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -71,6 +71,9 @@ var TimelinePanel = React.createClass({ // half way down the viewport. eventPixelOffset: React.PropTypes.number, + // Should we show URL Previews + showUrlPreview: React.PropTypes.bool, + // callback which is called when the panel is scrolled. onScroll: React.PropTypes.func, @@ -934,6 +937,7 @@ var TimelinePanel = React.createClass({ readMarkerEventId={ this.state.readMarkerEventId } readMarkerVisible={ this.state.readMarkerVisible } suppressFirstDateSeparator={ this.state.canBackPaginate } + showUrlPreview = { this.props.showUrlPreview } ourUserId={ MatrixClientPeg.get().credentials.userId } stickyBottom={ stickyBottom } onScroll={ this.onMessageListScroll } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index c54a10c7c0..188e140007 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -262,6 +262,63 @@ module.exports = React.createClass({ }); }, + _renderUserInterfaceSettings: function() { + var client = MatrixClientPeg.get(); + + var settingsLabels = [ + /* + { + id: 'alwaysShowTimestamps', + label: 'Always show message timestamps', + }, + { + id: 'showTwelveHourTimestamps', + label: 'Show timestamps in 12 hour format (e.g. 2:30pm)', + }, + { + id: 'useCompactLayout', + label: 'Use compact timeline layout', + }, + { + id: 'useFixedWidthFont', + label: 'Use fixed width font', + }, + */ + ]; + + var syncedSettings = UserSettingsStore.getSyncedSettings(); + + return ( +
+

User Interface

+
+
+ UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) } + /> + +
+
+ { settingsLabels.forEach( setting => { +
+ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + /> + +
+ })} +
+ ); + }, + _renderDeviceInfo: function() { if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) { return null; @@ -379,7 +436,7 @@ module.exports = React.createClass({ this._renderLabs = function () { let features = LABS_FEATURES.map(feature => ( -
+
; }, }); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 310da598fa..8c6cf455dc 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -39,6 +39,9 @@ module.exports = React.createClass({ /* link URL for the highlights */ highlightLink: React.PropTypes.string, + /* should show URL previews for this event */ + showUrlPreview: React.PropTypes.bool, + /* callback for when our widget has loaded */ onWidgetLoad: React.PropTypes.func, }, @@ -56,34 +59,47 @@ module.exports = React.createClass({ componentDidMount: function() { linkifyElement(this.refs.content, linkifyMatrix.options); - - var links = this.findLinks(this.refs.content.children); - if (links.length) { - this.setState({ links: links.map((link)=>{ - return link.getAttribute("href"); - })}); - - // lazy-load the hidden state of the preview widget from localstorage - if (global.localStorage) { - var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId()); - this.setState({ widgetHidden: hidden }); - } - } + this.calculateUrlPreview(); if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); }, + componentDidUpdate: function() { + this.calculateUrlPreview(); + }, + shouldComponentUpdate: function(nextProps, nextState) { + //console.log("shouldComponentUpdate: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + // exploit that events are immutable :) - // ...and that .links is only ever set in componentDidMount and never changes return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || nextProps.highlights !== this.props.highlights || nextProps.highlightLink !== this.props.highlightLink || + nextProps.showUrlPreview !== this.props.showUrlPreview || nextState.links !== this.state.links || nextState.widgetHidden !== this.state.widgetHidden); }, + calculateUrlPreview: function() { + //console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + + if (this.props.showUrlPreview && !this.state.links.length) { + var links = this.findLinks(this.refs.content.children); + if (links.length) { + this.setState({ links: links.map((link)=>{ + return link.getAttribute("href"); + })}); + + // lazy-load the hidden state of the preview widget from localstorage + if (global.localStorage) { + var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId()); + this.setState({ widgetHidden: hidden }); + } + } + } + }, + findLinks: function(nodes) { var links = []; for (var i = 0; i < nodes.length; i++) { @@ -163,12 +179,14 @@ module.exports = React.createClass({ render: function() { var mxEvent = this.props.mxEvent; var content = mxEvent.getContent(); - var body = HtmlUtils.bodyToHtml(content, this.props.highlights, - {highlightLink: this.props.highlightLink}); + var body = HtmlUtils.bodyToHtml(content, this.props.highlights, {}); + if (this.props.highlightLink) { + body = { body }; + } var widgets; - if (this.state.links.length && !this.state.widgetHidden) { + if (this.state.links.length && !this.state.widgetHidden && this.props.showUrlPreview) { var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget'); widgets = this.state.links.map((link)=>{ return + + Disable URL previews by default for participants in this room + + } + else { + disableRoomPreviewUrls = + + } + + return ( +
+

URL Previews

+ + + { disableRoomPreviewUrls } + + +
+ ); + + } +}); \ No newline at end of file diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 70dfe8ac33..77be8226a2 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -101,6 +101,9 @@ module.exports = React.createClass({ /* link URL for the highlights */ highlightLink: React.PropTypes.string, + /* should show URL previews for this event */ + showUrlPreview: React.PropTypes.bool, + /* is this the focused event */ isSelectedEvent: React.PropTypes.bool, @@ -359,6 +362,8 @@ module.exports = React.createClass({ var SenderProfile = sdk.getComponent('messages.SenderProfile'); var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + //console.log("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + var content = this.props.mxEvent.getContent(); var msgtype = content.msgtype; @@ -420,6 +425,7 @@ module.exports = React.createClass({
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index df30b63c75..b12c6e4aa2 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -216,10 +216,13 @@ module.exports = React.createClass({ // color scheme promises.push(this.saveColor()); + // url preview settings + promises.push(this.saveUrlPreviewSettings()); + // encryption promises.push(this.saveEncryption()); - console.log("Performing %s operations", promises.length); + console.log("Performing %s operations: %s", promises.length, JSON.stringify(promises)); return q.allSettled(promises); }, @@ -233,6 +236,11 @@ module.exports = React.createClass({ return this.refs.color_settings.saveSettings(); }, + saveUrlPreviewSettings: function() { + if (!this.refs.url_preview_settings) { return q(); } + return this.refs.url_preview_settings.saveSettings(); + }, + saveEncryption: function () { if (!this.refs.encrypt) { return q(); } @@ -427,6 +435,7 @@ module.exports = React.createClass({ var AliasSettings = sdk.getComponent("room_settings.AliasSettings"); var ColorSettings = sdk.getComponent("room_settings.ColorSettings"); + var UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); var EditableText = sdk.getComponent('elements.EditableText'); var PowerSelector = sdk.getComponent('elements.PowerSelector'); @@ -659,6 +668,8 @@ module.exports = React.createClass({ canonicalAliasEvent={this.props.room.currentState.getStateEvents('m.room.canonical_alias', '')} aliasEvents={this.props.room.currentState.getStateEvents('m.room.aliases')} /> + +

Permissions