Merge pull request #260 from matrix-org/matthew/preview_urls

URL previewing support
This commit is contained in:
Matthew Hodgson 2016-04-11 23:55:15 +01:00
commit f9c7ae1ab9
11 changed files with 312 additions and 62 deletions

57
src/ImageUtils.js Normal file
View file

@ -0,0 +1,57 @@
/*
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 = {
/**
* Returns the actual height that an image of dimensions (fullWidth, fullHeight)
* will occupy if resized to fit inside a thumbnail bounding box of size
* (thumbWidth, thumbHeight).
*
* If the aspect ratio of the source image is taller than the aspect ratio of
* the thumbnail bounding box, then we return the thumbHeight parameter unchanged.
* Otherwise we return the thumbHeight parameter scaled down appropriately to
* reflect the actual height the scaled thumbnail occupies.
*
* This is very useful for calculating how much height a thumbnail will actually
* consume in the timeline, when performing scroll offset calcuations
* (e.g. scroll locking)
*/
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);
}
},
}

View file

@ -26,10 +26,6 @@ limitations under the License.
module.exports.components = {}; module.exports.components = {};
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom'); 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'); module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel'); module.exports.components['structures.MessagePanel'] = require('./components/structures/MessagePanel');
module.exports.components['structures.RoomStatusBar'] = require('./components/structures/RoomStatusBar'); 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.TimelinePanel'] = require('./components/structures/TimelinePanel');
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar'); module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings'); 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.BaseAvatar'] = require('./components/views/avatars/BaseAvatar');
module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar'); module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar'); 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.PasswordLogin'] = require('./components/views/login/PasswordLogin');
module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm'); 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.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.MFileBody'] = require('./components/views/messages/MFileBody');
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody'); 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.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.TextualBody'] = require('./components/views/messages/TextualBody');
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent'); module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody'); 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.EntityTile'] = require('./components/views/rooms/EntityTile');
module.exports.components['views.rooms.EventTile'] = require('./components/views/rooms/EventTile'); 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.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.MemberInfo'] = require('./components/views/rooms/MemberInfo');
module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList'); module.exports.components['views.rooms.MemberList'] = require('./components/views/rooms/MemberList');
module.exports.components['views.rooms.MemberTile'] = require('./components/views/rooms/MemberTile'); 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.RoomSettings'] = require('./components/views/rooms/RoomSettings');
module.exports.components['views.rooms.RoomTile'] = require('./components/views/rooms/RoomTile'); 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.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.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.SimpleRoomHeader'] = require('./components/views/rooms/SimpleRoomHeader');
module.exports.components['views.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar'); module.exports.components['views.rooms.TabCompleteBar'] = require('./components/views/rooms/TabCompleteBar');
module.exports.components['views.rooms.TopUnreadMessagesBar'] = require('./components/views/rooms/TopUnreadMessagesBar'); module.exports.components['views.rooms.TopUnreadMessagesBar'] = require('./components/views/rooms/TopUnreadMessagesBar');

View file

@ -337,6 +337,7 @@ module.exports = React.createClass({
ref={this._collectEventNode.bind(this, eventId)} ref={this._collectEventNode.bind(this, eventId)}
data-scroll-token={scrollToken}> data-scroll-token={scrollToken}>
<EventTile mxEvent={mxEv} continuation={continuation} <EventTile mxEvent={mxEv} continuation={continuation}
onWidgetLoad={this._onWidgetLoad}
last={last} isSelectedEvent={highlight} /> last={last} isSelectedEvent={highlight} />
</li> </li>
); );
@ -398,6 +399,15 @@ module.exports = React.createClass({
this.eventNodes[eventId] = node; 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.forceUpdate();
}
},
onResize: function() { onResize: function() {
dis.dispatch({ action: 'timeline_resize' }, true); dis.dispatch({ action: 'timeline_resize' }, true);
}, },

View file

@ -798,9 +798,9 @@ module.exports = React.createClass({
} }
} }
// once images in the search results load, make the scrollPanel check // once dynamic content in the search results load, make the scrollPanel check
// the scroll offsets. // the scroll offsets.
var onImageLoad = () => { var onWidgetLoad = () => {
var scrollPanel = this.refs.searchResultsPanel; var scrollPanel = this.refs.searchResultsPanel;
if (scrollPanel) { if (scrollPanel) {
scrollPanel.checkScroll(); scrollPanel.checkScroll();
@ -844,7 +844,7 @@ module.exports = React.createClass({
searchResult={result} searchResult={result}
searchHighlights={this.state.searchHighlights} searchHighlights={this.state.searchHighlights}
resultLink={resultLink} resultLink={resultLink}
onImageLoad={onImageLoad}/>); onWidgetLoad={onWidgetLoad}/>);
} }
return ret; return ret;
}, },

View file

@ -20,6 +20,7 @@ var React = require('react');
var filesize = require('filesize'); var filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg'); var MatrixClientPeg = require('../../../MatrixClientPeg');
var ImageUtils = require('../../../ImageUtils');
var Modal = require('../../../Modal'); var Modal = require('../../../Modal');
var sdk = require('../../../index'); var sdk = require('../../../index');
var dis = require("../../../dispatcher"); var dis = require("../../../dispatcher");
@ -30,31 +31,6 @@ module.exports = React.createClass({
propTypes: { propTypes: {
/* the MatrixEvent to show */ /* the MatrixEvent to show */
mxEvent: React.PropTypes.object.isRequired, mxEvent: React.PropTypes.object.isRequired,
/* callback called when images in events are loaded */
onImageLoad: React.PropTypes.func,
},
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);
}
}, },
onClick: function onClick(ev) { onClick: function onClick(ev) {
@ -71,6 +47,7 @@ module.exports = React.createClass({
if (content.info) { if (content.info) {
params.width = content.info.w; params.width = content.info.w;
params.height = content.info.h; params.height = content.info.h;
params.fileSize = content.info.size;
} }
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox"); Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
@ -134,7 +111,9 @@ module.exports = React.createClass({
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box // the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
//console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth); //console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight); if (content.info) {
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
}
this.refs.image.style.height = thumbHeight + "px"; this.refs.image.style.height = thumbHeight + "px";
// console.log("Image height now", thumbHeight); // console.log("Image height now", thumbHeight);
}, },
@ -152,8 +131,7 @@ module.exports = React.createClass({
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image" <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body} alt={content.body}
onMouseEnter={this.onImageEnter} onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} onMouseLeave={this.onImageLeave} />
onLoad={this.props.onImageLoad} />
</a> </a>
<div className="mx_MImageBody_download"> <div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank"> <a href={cli.mxcUrlToHttp(content.url)} target="_blank">

View file

@ -38,15 +38,18 @@ module.exports = React.createClass({
/* link URL for the highlights */ /* link URL for the highlights */
highlightLink: React.PropTypes.string, highlightLink: React.PropTypes.string,
/* callback called when images in events are loaded */ /* callback called when dynamic content in events are loaded */
onImageLoad: React.PropTypes.func, onWidgetLoad: React.PropTypes.func,
}, },
getEventTileOps: function() {
return this.refs.body ? this.refs.body.getEventTileOps() : null;
},
render: function() { render: function() {
var UnknownMessageTile = sdk.getComponent('messages.UnknownBody'); var UnknownBody = sdk.getComponent('messages.UnknownBody');
var tileTypes = { var bodyTypes = {
'm.text': sdk.getComponent('messages.TextualBody'), 'm.text': sdk.getComponent('messages.TextualBody'),
'm.notice': sdk.getComponent('messages.TextualBody'), 'm.notice': sdk.getComponent('messages.TextualBody'),
'm.emote': sdk.getComponent('messages.TextualBody'), 'm.emote': sdk.getComponent('messages.TextualBody'),
@ -57,13 +60,13 @@ module.exports = React.createClass({
var content = this.props.mxEvent.getContent(); var content = this.props.mxEvent.getContent();
var msgtype = content.msgtype; var msgtype = content.msgtype;
var TileType = UnknownMessageTile; var BodyType = UnknownBody;
if (msgtype && tileTypes[msgtype]) { if (msgtype && bodyTypes[msgtype]) {
TileType = tileTypes[msgtype]; BodyType = bodyTypes[msgtype];
} }
return <TileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} return <BodyType ref="body" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
onImageLoad={this.props.onImageLoad} />; onWidgetLoad={this.props.onWidgetLoad} />;
}, },
}); });

View file

@ -22,6 +22,7 @@ var HtmlUtils = require('../../../HtmlUtils');
var linkify = require('linkifyjs'); var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element'); var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix'); var linkifyMatrix = require('../../../linkify-matrix');
var sdk = require('../../../index');
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -37,28 +38,84 @@ module.exports = React.createClass({
/* link URL for the highlights */ /* link URL for the highlights */
highlightLink: React.PropTypes.string, highlightLink: React.PropTypes.string,
/* callback for when our widget has loaded */
onWidgetLoad: React.PropTypes.func,
},
getInitialState: function() {
return {
// the URL (if any) to be previewed with a LinkPreviewWidget
// inside this TextualBody.
link: null,
// track whether the preview widget is hidden
widgetHidden: false,
};
}, },
componentDidMount: function() { componentDidMount: function() {
linkifyElement(this.refs.content, linkifyMatrix.options); linkifyElement(this.refs.content, linkifyMatrix.options);
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") var link = this.findLink(this.refs.content.children);
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); if (link) {
}, this.setState({ link: link.getAttribute("href") });
componentDidUpdate: function() { // lazy-load the hidden state of the preview widget from localstorage
// XXX: why don't we linkify here? if (global.localStorage) {
// XXX: why do we bother doing this on update at all, given events are immutable? var hidden = global.localStorage.getItem("hide_preview_" + this.props.mxEvent.getId());
this.setState({ widgetHidden: hidden });
}
}
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") if (this.props.mxEvent.getContent().format === "org.matrix.custom.html")
HtmlUtils.highlightDom(ReactDOM.findDOMNode(this)); HtmlUtils.highlightDom(ReactDOM.findDOMNode(this));
}, },
shouldComponentUpdate: function(nextProps) { shouldComponentUpdate: function(nextProps, nextState) {
// exploit that events are immutable :) // exploit that events are immutable :)
return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() ||
nextProps.highlights !== this.props.highlights || nextProps.highlights !== this.props.highlights ||
nextProps.highlightLink !== this.props.highlightLink); nextProps.highlightLink !== this.props.highlightLink ||
nextState.link !== this.state.link ||
nextState.widgetHidden !== this.state.widgetHidden);
},
findLink: function(nodes) {
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)
}
}
},
onCancelClick: function(event) {
this.setState({ widgetHidden: true });
// FIXME: persist this somewhere smarter than local storage
if (global.localStorage) {
global.localStorage.setItem("hide_preview_" + this.props.mxEvent.getId(), "1");
}
this.forceUpdate();
},
getEventTileOps: function() {
var self = this;
return {
isWidgetHidden: function() {
return self.state.widgetHidden;
},
unhideWidget: function() {
self.setState({ widgetHidden: false });
if (global.localStorage) {
global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId());
}
},
}
}, },
render: function() { render: function() {
@ -67,24 +124,38 @@ module.exports = React.createClass({
var body = HtmlUtils.bodyToHtml(content, this.props.highlights, var body = HtmlUtils.bodyToHtml(content, this.props.highlights,
{highlightLink: this.props.highlightLink}); {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) { switch (content.msgtype) {
case "m.emote": case "m.emote":
var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender();
return ( return (
<span ref="content" className="mx_MEmoteBody mx_EventTile_content"> <span ref="content" className="mx_MEmoteBody mx_EventTile_content">
* { name } { body } * { name } { body }
{ widget }
</span> </span>
); );
case "m.notice": case "m.notice":
return ( return (
<span ref="content" className="mx_MNoticeBody mx_EventTile_content"> <span ref="content" className="mx_MNoticeBody mx_EventTile_content">
{ body } { body }
{ widget }
</span> </span>
); );
default: // including "m.text" default: // including "m.text"
return ( return (
<span ref="content" className="mx_MTextBody mx_EventTile_content"> <span ref="content" className="mx_MTextBody mx_EventTile_content">
{ body } { body }
{ widget }
</span> </span>
); );
} }

View file

@ -105,8 +105,8 @@ module.exports = React.createClass({
/* is this the focused event */ /* is this the focused event */
isSelectedEvent: React.PropTypes.bool, isSelectedEvent: React.PropTypes.bool,
/* callback called when images in events are loaded */ /* callback called when dynamic content in events are loaded */
onImageLoad: React.PropTypes.func, onWidgetLoad: React.PropTypes.func,
}, },
getInitialState: function() { getInitialState: function() {
@ -137,6 +137,7 @@ module.exports = React.createClass({
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
left: x, left: x,
top: y, top: y,
eventTileOps: this.refs.tile ? this.refs.tile.getEventTileOps() : undefined,
onFinished: function() { onFinished: function() {
self.setState({menu: false}); self.setState({menu: false});
} }
@ -343,9 +344,9 @@ module.exports = React.createClass({
{ avatar } { avatar }
{ sender } { sender }
<div className="mx_EventTile_line"> <div className="mx_EventTile_line">
<EventTileType mxEvent={this.props.mxEvent} highlights={this.props.highlights} <EventTileType ref="tile" mxEvent={this.props.mxEvent} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
onImageLoad={this.props.onImageLoad} /> onWidgetLoad={this.props.onWidgetLoad} />
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,129 @@
/*
Copyright 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';
var React = require('react');
var sdk = require('../../../index');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var ImageUtils = require('../../../ImageUtils');
var Modal = require('../../../Modal');
var linkify = require('linkifyjs');
var linkifyElement = require('linkifyjs/element');
var linkifyMatrix = require('../../../linkify-matrix');
linkifyMatrix(linkify);
module.exports = React.createClass({
displayName: 'LinkPreviewWidget',
propTypes: {
link: React.PropTypes.string.isRequired, // the URL being previewed
mxEvent: React.PropTypes.object.isRequired, // the Event associated with the preview
onCancelClick: React.PropTypes.func, // called when the preview's cancel ('hide') button is clicked
onWidgetLoad: React.PropTypes.func, // called when the preview's contents has loaded
},
getInitialState: function() {
return {
preview: null
};
},
componentWillMount: function() {
MatrixClientPeg.get().getUrlPreview(this.props.link, this.props.mxEvent.getTs()).then((res)=>{
this.setState(
{ preview: res },
this.props.onWidgetLoad
);
}, (error)=>{
console.error("Failed to get preview for " + this.props.link + " " + error);
});
},
componentDidMount: function() {
if (this.refs.description)
linkifyElement(this.refs.description, linkifyMatrix.options);
},
componentDidUpdate: function() {
if (this.refs.description)
linkifyElement(this.refs.description, linkifyMatrix.options);
},
onImageClick: function(ev) {
var p = this.state.preview;
if (ev.button != 0 || ev.metaKey) return;
ev.preventDefault();
var ImageView = sdk.getComponent("elements.ImageView");
var src = p["og:image"];
if (src && src.startsWith("mxc://")) {
src = MatrixClientPeg.get().mxcUrlToHttp(src);
}
var params = {
src: src,
width: p["og:image:width"],
height: p["og:image:height"],
name: p["og:title"] || p["og:description"] || this.props.link,
fileSize: p["matrix:image:size"],
link: this.props.link,
};
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox");
},
render: function() {
var p = this.state.preview;
if (!p) return <div/>;
// FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing?
var image = p["og:image"];
var imageMaxWidth = 100, imageMaxHeight = 100;
if (image && image.startsWith("mxc://")) {
image = MatrixClientPeg.get().mxcUrlToHttp(image, imageMaxWidth, imageMaxHeight);
}
var thumbHeight = imageMaxHeight;
if (p["og:image:width"] && p["og:image:height"]) {
thumbHeight = ImageUtils.thumbHeight(p["og:image:width"], p["og:image:height"], imageMaxWidth, imageMaxHeight);
}
var img;
if (image) {
img = <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}>
<img style={{ maxWidth: imageMaxWidth, maxHeight: imageMaxHeight }} src={ image } onClick={ this.onImageClick }/>
</div>
}
return (
<div className="mx_LinkPreviewWidget" >
{ img }
<div className="mx_LinkPreviewWidget_caption">
<div className="mx_LinkPreviewWidget_title"><a href={ this.props.link } target="_blank">{ p["og:title"] }</a></div>
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
<div className="mx_LinkPreviewWidget_description" ref="description">
{ p["og:description"] }
</div>
</div>
<img className="mx_LinkPreviewWidget_cancel" src="img/cancel.svg" width="18" height="18"
onClick={ this.props.onCancelClick }/>
</div>
);
}
});

View file

@ -32,7 +32,7 @@ module.exports = React.createClass({
// href for the highlights in this result // href for the highlights in this result
resultLink: React.PropTypes.string, resultLink: React.PropTypes.string,
onImageLoad: React.PropTypes.func, onWidgetLoad: React.PropTypes.func,
}, },
render: function() { render: function() {
@ -56,7 +56,7 @@ module.exports = React.createClass({
if (EventTile.haveTileForEvent(ev)) { if (EventTile.haveTileForEvent(ev)) {
ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights} ret.push(<EventTile key={eventId+"+"+j} mxEvent={ev} contextual={contextual} highlights={highlights}
highlightLink={this.props.resultLink} highlightLink={this.props.resultLink}
onImageLoad={this.props.onImageLoad} />); onWidgetLoad={this.props.onWidgetLoad} />);
} }
} }
return ( return (