-
-
Notifications
You must be signed in to change notification settings - Fork 832
URL previewing support #260
Changes from 13 commits
e6842ea
4d959fc
62d04c3
4388334
a6b6be7
bffb482
2d289b3
f195d2e
f9c914c
e61c99f
0eb7b62
96b0f42
1de4e0d
7884c13
1125c62
1d8b080
4abc5d0
23d6edb
6c372d3
8e48bed
9845e5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
Copyright 2015, 2016 OpenMarket Ltd | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
module.exports = { | ||
thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) { | ||
if (!fullWidth || !fullHeight) { | ||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even | ||
// log this because it's spammy | ||
return undefined; | ||
} | ||
if (fullWidth < thumbWidth && fullHeight < thumbHeight) { | ||
// no scaling needs to be applied | ||
return fullHeight; | ||
} | ||
var widthMulti = thumbWidth / fullWidth; | ||
var heightMulti = thumbHeight / fullHeight; | ||
if (widthMulti < heightMulti) { | ||
// width is the dominant dimension so scaling will be fixed on that | ||
return Math.floor(widthMulti * fullHeight); | ||
} | ||
else { | ||
// height is the dominant dimension so scaling will be fixed on that | ||
return Math.floor(heightMulti * fullHeight); | ||
} | ||
}, | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,10 +26,6 @@ limitations under the License. | |
|
||
module.exports.components = {}; | ||
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); | ||
module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword'); | ||
module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); | ||
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); | ||
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); | ||
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aside: it would be nice to fix |
||
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); | ||
module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar'); | ||
|
@@ -38,6 +34,10 @@ module.exports.components['structures.ScrollPanel'] = require('./components/stru | |
module.exports.components['structures.TimelinePanel'] = require('./components/structures/TimelinePanel'); | ||
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); | ||
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); | ||
module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword'); | ||
module.exports.components['structures.login.Login'] = require('./components/structures/login/Login'); | ||
module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration'); | ||
module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration'); | ||
module.exports.components['views.avatars.BaseAvatar'] = require('./components/views/avatars/BaseAvatar'); | ||
module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); | ||
module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); | ||
|
@@ -64,10 +64,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie | |
module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin'); | ||
module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm'); | ||
module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig'); | ||
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); | ||
module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody'); | ||
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); | ||
module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody'); | ||
module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent'); | ||
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody'); | ||
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); | ||
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); | ||
|
@@ -77,6 +77,7 @@ module.exports.components['views.rooms.AuxPanel'] = require('./components/views/ | |
module.exports.components['views.rooms.EntityTile'] = require('./components/views/rooms/EntityTile'); | ||
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); | ||
module.exports.components['views.rooms.InviteMemberList'] = require('./components/views/rooms/InviteMemberList'); | ||
module.exports.components['views.rooms.LinkPreviewWidget'] = require('./components/views/rooms/LinkPreviewWidget'); | ||
module.exports.components['views.rooms.MemberInfo'] = require('./components/views/rooms/MemberInfo'); | ||
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); | ||
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); | ||
|
@@ -90,8 +91,8 @@ module.exports.components['views.rooms.RoomPreviewBar'] = require('./components/ | |
module.exports.components['views.rooms.RoomSettings'] = require('./components/views/rooms/RoomSettings'); | ||
module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile'); | ||
module.exports.components['views.rooms.RoomTopicEditor'] = require('./components/views/rooms/RoomTopicEditor'); | ||
module.exports.components['views.rooms.SearchableEntityList'] = require('./components/views/rooms/SearchableEntityList'); | ||
module.exports.components['views.rooms.SearchResultTile'] = require('./components/views/rooms/SearchResultTile'); | ||
module.exports.components['views.rooms.SearchableEntityList'] = require('./components/views/rooms/SearchableEntityList'); | ||
module.exports.components['views.rooms.SimpleRoomHeader'] = require('./components/views/rooms/SimpleRoomHeader'); | ||
module.exports.components['views.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar'); | ||
module.exports.components['views.rooms.TopUnreadMessagesBar'] = require('./components/views/rooms/TopUnreadMessagesBar'); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -302,6 +302,7 @@ module.exports = React.createClass({ | |
ref={this._collectEventNode.bind(this, eventId)} | ||
data-scroll-token={scrollToken}> | ||
<EventTile mxEvent={mxEv} continuation={continuation} | ||
onWidgetLoad={this._onWidgetLoad} | ||
last={last} isSelectedEvent={highlight} /> | ||
</li> | ||
); | ||
|
@@ -368,6 +369,15 @@ module.exports = React.createClass({ | |
this.eventNodes[eventId] = node; | ||
}, | ||
|
||
// once dynamic content in the events load, make the scrollPanel check the | ||
// scroll offsets. | ||
_onWidgetLoad: function() { | ||
var scrollPanel = this.refs.scrollPanel; | ||
if (scrollPanel) { | ||
scrollPanel.checkScroll(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is sufficient - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hm. why doesn't scrollPanel.checkScroll() do this already then? Especially as scrollPanel's gemini isn't exposed to MessagePanel? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. okay, have changed the gemini forceUpdate to the whole scrollpanel forceUpdate. this feels a bit vicious, but i assume you know what you're asking for :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you don't need to do checkScroll as well as forceUpdate. |
||
} | ||
}, | ||
|
||
onResize: function() { | ||
dis.dispatch({ action: 'timeline_resize' }, true); | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,7 +39,7 @@ module.exports = React.createClass({ | |
highlightLink: React.PropTypes.string, | ||
|
||
/* callback called when images in events are loaded */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More than just images now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
onImageLoad: React.PropTypes.func, | ||
onWidgetLoad: React.PropTypes.func, | ||
}, | ||
|
||
|
||
|
@@ -64,6 +64,6 @@ module.exports = React.createClass({ | |
|
||
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} | ||
highlightLink={this.props.highlightLink} | ||
onImageLoad={this.props.onImageLoad} />; | ||
onWidgetLoad={this.props.onWidgetLoad} />; | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ var HtmlUtils = require('../../../HtmlUtils'); | |
var linkify = require('linkifyjs'); | ||
var linkifyElement = require('linkifyjs/element'); | ||
var linkifyMatrix = require('../../../linkify-matrix'); | ||
var sdk = require('../../../index'); | ||
|
||
linkifyMatrix(linkify); | ||
|
||
|
@@ -37,28 +38,76 @@ module.exports = React.createClass({ | |
|
||
/* link URL for the highlights */ | ||
highlightLink: React.PropTypes.string, | ||
|
||
/* callback for when our widget has loaded */ | ||
onWidgetLoad: React.PropTypes.func, | ||
}, | ||
|
||
getInitialState: function() { | ||
return { | ||
link: null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can't be set before the component is mounted, as the links in the message are calculated by linkifyElement() which has to run after the DOM has been created and the component mounted. Therefore we'd still need to re-render() in order to pull in the right LinkPreviewWidget for the links we've discovered - and so might as well still be considered component state. wdyt? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd missed that calculating There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only thing that might be nice would be a comment on |
||
|
||
// track whether the preview widget is hidden | ||
// we can't directly use mxEvent's widgetHidden property | ||
// as shouldComponentUpdate needs to be able to do before & after | ||
// comparisons of the property (and we don't pass it in as a top | ||
// level prop to avoid bloating the number of props flying around) | ||
widgetHidden: false, | ||
}; | ||
}, | ||
|
||
componentDidMount: function() { | ||
linkifyElement(this.refs.content, linkifyMatrix.options); | ||
|
||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") | ||
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); | ||
}, | ||
var link = this.findLink(this.refs.content.children); | ||
if (link) { | ||
this.setState({ link: link.getAttribute("href") }); | ||
|
||
componentDidUpdate: function() { | ||
// XXX: why don't we linkify here? | ||
// XXX: why do we bother doing this on update at all, given events are immutable? | ||
// 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.props.mxEvent.widgetHidden = hidden; | ||
this.setState({ widgetHidden: hidden }); | ||
} | ||
} | ||
|
||
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") | ||
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); | ||
}, | ||
|
||
shouldComponentUpdate: function(nextProps) { | ||
findLink: function(nodes) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. elsewhere we've put helper methods after the lifecycle functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
for (var i = 0; i < nodes.length; i++) { | ||
var node = nodes[i]; | ||
if (node.tagName === "A" && node.getAttribute("href")) { | ||
return node; | ||
} | ||
else if (node.children && node.children.length) { | ||
return this.findLink(node.children) | ||
} | ||
} | ||
}, | ||
|
||
shouldComponentUpdate: function(nextProps, nextState) { | ||
// exploit that events are immutable :) | ||
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || | ||
nextProps.highlights !== this.props.highlights || | ||
nextProps.highlightLink !== this.props.highlightLink); | ||
nextProps.highlightLink !== this.props.highlightLink || | ||
nextState.link !== this.state.link || | ||
nextProps.mxEvent.widgetHidden !== this.state.widgetHidden); | ||
}, | ||
|
||
componentWillUpdate: function(nextProps, nextState) { | ||
this.setState({ widgetHidden: nextProps.mxEvent.widgetHidden }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://facebook.github.io/react/docs/component-specs.html#updating-componentwillupdate says 'You cannot use this.setState() in this method.' Perhaps you could update There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like I've made a spectacular mess of the whole deal of tracking
Which feels like a spectacular violation of there being one source of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes... Is tracking it in mxEvent actually buying us anything, given we track it in TextualBody anyway? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, because MessageContextualMenu can't get at TextualBody, but it can get at mxEvent (when unhiding the wretched things) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm sure there is a better way of communicating this from contextualmenu to textualbody than piping it through mxEvent... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what do you have in mind? dispatcher? or threading properties everywhere?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could expose an interface object from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a really weird mental blank on this - sorry :( Are you suggesting that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, exactly so.
specifically,
|
||
}, | ||
|
||
onCancelClick: function(event) { | ||
this.props.mxEvent.widgetHidden = true; | ||
this.setState({ widgetHidden: true }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Poking this into the js-sdk There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
// FIXME: persist this somewhere smarter than local storage | ||
if (global.localStorage) { | ||
global.localStorage.setItem("hide_preview_" + this.props.mxEvent.getId(), "1"); | ||
} | ||
this.forceUpdate(); | ||
}, | ||
|
||
render: function() { | ||
|
@@ -67,24 +116,38 @@ module.exports = React.createClass({ | |
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, | ||
{highlightLink: this.props.highlightLink}); | ||
|
||
|
||
var widget; | ||
if (this.state.link && !this.state.widgetHidden) { | ||
var LinkPreviewWidget = sdk.getComponent('rooms.LinkPreviewWidget'); | ||
widget = <LinkPreviewWidget | ||
link={ this.state.link } | ||
mxEvent={ this.props.mxEvent } | ||
onCancelClick={ this.onCancelClick } | ||
onWidgetLoad={ this.props.onWidgetLoad }/>; | ||
} | ||
|
||
switch (content.msgtype) { | ||
case "m.emote": | ||
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); | ||
return ( | ||
<span ref="content" className="mx_MEmoteBody mx_EventTile_content"> | ||
* { name } { body } | ||
{ widget } | ||
</span> | ||
); | ||
case "m.notice": | ||
return ( | ||
<span ref="content" className="mx_MNoticeBody mx_EventTile_content"> | ||
{ body } | ||
{ widget } | ||
</span> | ||
); | ||
default: // including "m.text" | ||
return ( | ||
<span ref="content" className="mx_MTextBody mx_EventTile_content"> | ||
{ body } | ||
{ widget } | ||
</span> | ||
); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that this is a public function, it would be nice to give this a comment saying what it does and what the parameters mean. How does its result relate to the
thumbHeight
param? Which of the params are allowed to be undefined? I could figure it out, but I shouldn't have to.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done