Merge pull request #260 from matrix-org/matthew/preview_urls
URL previewing support
This commit is contained in:
commit
f9c7ae1ab9
11 changed files with 312 additions and 62 deletions
57
src/ImageUtils.js
Normal file
57
src/ImageUtils.js
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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} />;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
@ -123,7 +123,7 @@ module.exports = React.createClass({
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions.tweaks.highlight;
|
return actions.tweaks.highlight;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
129
src/components/views/rooms/LinkPreviewWidget.js
Normal file
129
src/components/views/rooms/LinkPreviewWidget.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -451,7 +451,7 @@ module.exports = React.createClass({
|
||||||
onMemberAvatarClick: function () {
|
onMemberAvatarClick: function () {
|
||||||
var avatarUrl = this.props.member.user.avatarUrl;
|
var avatarUrl = this.props.member.user.avatarUrl;
|
||||||
if(!avatarUrl) return;
|
if(!avatarUrl) return;
|
||||||
|
|
||||||
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl);
|
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl);
|
||||||
var ImageView = sdk.getComponent("elements.ImageView");
|
var ImageView = sdk.getComponent("elements.ImageView");
|
||||||
var params = {
|
var params = {
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue