diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js new file mode 100644 index 0000000000..824f59ab20 --- /dev/null +++ b/src/HtmlUtils.js @@ -0,0 +1,108 @@ +/* +Copyright 2015 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 sanitizeHtml = require('sanitize-html'); +var highlight = require('highlight.js'); + +var sanitizeHtmlParams = { + allowedTags: [ + 'font', // custom to matrix. deliberately no h1/h2 to stop people shouting. + 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', + 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', + 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre' + ], + allowedAttributes: { + // custom ones first: + font: [ 'color' ], // custom to matrix + a: [ 'href', 'name', 'target' ], // remote target: custom to matrix + // We don't currently allow img itself by default, but this + // would make sense if we did + img: [ 'src' ], + }, + // Lots of these won't come up by default because we don't allow them + selfClosing: [ 'img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta' ], + // URL schemes we permit + allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], + allowedSchemesByTag: {}, + + transformTags: { // custom to matrix + // add blank targets to all hyperlinks + 'a': sanitizeHtml.simpleTransform('a', { target: '_blank'} ) + }, +}; + +module.exports = { + bodyToHtml: function(content, searchTerm) { + var originalBody = content.body; + var body; + + if (searchTerm) { + var lastOffset = 0; + var bodyList = []; + var k = 0; + var offset; + + // XXX: rather than searching for the search term in the body, + // we should be looking at the match delimiters returned by the FTS engine + if (content.format === "org.matrix.custom.html") { + + var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); + var safeSearchTerm = sanitizeHtml(searchTerm, sanitizeHtmlParams); + while ((offset = safeBody.indexOf(safeSearchTerm, lastOffset)) >= 0) { + // FIXME: we need to apply the search highlighting to only the text elements of HTML, which means + // hooking into the sanitizer parser rather than treating it as a string. Otherwise + // the act of highlighting a or whatever will break the HTML badly. + bodyList.push(); + bodyList.push(); + lastOffset = offset + safeSearchTerm.length; + } + bodyList.push(); + } + else { + while ((offset = originalBody.indexOf(searchTerm, lastOffset)) >= 0) { + bodyList.push({ originalBody.substring(lastOffset, offset) }); + bodyList.push({ searchTerm }); + lastOffset = offset + searchTerm.length; + } + bodyList.push({ originalBody.substring(lastOffset) }); + } + body = bodyList; + } + else { + if (content.format === "org.matrix.custom.html") { + var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); + body = ; + } + else { + body = originalBody; + } + } + + return body; + }, + + highlightDom: function(element) { + var blocks = element.getElementsByTagName("code"); + for (var i = 0; i < blocks.length; i++) { + highlight.highlightBlock(blocks[i]); + } + }, + +} + diff --git a/src/components/views/messages/Event.js b/src/components/views/messages/Event.js new file mode 100644 index 0000000000..d2862d304a --- /dev/null +++ b/src/components/views/messages/Event.js @@ -0,0 +1,277 @@ +/* +Copyright 2015 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 ReactDom = require('react-dom'); +var classNames = require("classnames"); + +var sdk = require('../../../index'); +var MatrixClientPeg = require('../../../MatrixClientPeg') +var TextForEvent = require('../../../TextForEvent'); + +// FIXME BROKEN IMPORTS +var ContextualMenu = require('../../../../ContextualMenu'); +var Velociraptor = require('../../../../Velociraptor'); +require('../../../../VelocityBounce'); + +var bounce = false; +try { + if (global.localStorage) { + bounce = global.localStorage.getItem('avatar_bounce') == 'true'; + } +} catch (e) { +} + +var eventTileTypes = { + 'm.room.message': 'messages.Message', + 'm.room.member' : 'messages.TextualEvent', + 'm.call.invite' : 'messages.TextualEvent', + 'm.call.answer' : 'messages.TextualEvent', + 'm.call.hangup' : 'messages.TextualEvent', + 'm.room.name' : 'messages.TextualEvent', + 'm.room.topic' : 'messages.TextualEvent', +}; + +var MAX_READ_AVATARS = 5; + +module.exports = React.createClass({ + displayName: 'Event', + + statics: { + haveTileForEvent: function(e) { + if (eventTileTypes[e.getType()] == undefined) return false; + if (eventTileTypes[e.getType()] == 'messages.TextualEvent') { + return TextForEvent.textForEvent(e) !== ''; + } else { + return true; + } + } + }, + + getInitialState: function() { + return {menu: false, allReadAvatars: false}; + }, + + shouldHighlight: function() { + var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent); + if (!actions || !actions.tweaks) { return false; } + return actions.tweaks.highlight; + }, + + onEditClicked: function(e) { + var MessageContextMenu = sdk.getComponent('molecules.MessageContextMenu'); + var buttonRect = e.target.getBoundingClientRect() + var x = buttonRect.right; + var y = buttonRect.top + (e.target.height / 2); + var self = this; + ContextualMenu.createMenu(MessageContextMenu, { + mxEvent: this.props.mxEvent, + left: x, + top: y, + onFinished: function() { + self.setState({menu: false}); + } + }); + this.setState({menu: true}); + }, + + toggleAllReadAvatars: function() { + this.setState({ + allReadAvatars: !this.state.allReadAvatars + }); + }, + + getReadAvatars: function() { + var avatars = []; + + var room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + + if (!room) return []; + + var myUserId = MatrixClientPeg.get().credentials.userId; + + // get list of read receipts, sorted most recent first + var receipts = room.getReceiptsForEvent(this.props.mxEvent).filter(function(r) { + return r.type === "m.read" && r.userId != myUserId; + }).sort(function(r1, r2) { + return r2.data.ts - r1.data.ts; + }); + + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + + var left = 0; + + var reorderTransitionOpts = { + duration: 100, + easing: 'easeOut' + }; + + for (var i = 0; i < receipts.length; ++i) { + var member = room.getMember(receipts[i].userId); + + // Using react refs here would mean both getting Velociraptor to expose + // them and making them scoped to the whole RoomView. Not impossible, but + // getElementById seems simpler at least for a first cut. + var oldAvatarDomNode = document.getElementById('mx_readAvatar'+member.userId); + var startStyles = []; + var enterTransitionOpts = []; + var oldNodeTop = -15; // For avatars that weren't on screen, act as if they were just off the top + if (oldAvatarDomNode) { + oldNodeTop = oldAvatarDomNode.getBoundingClientRect().top; + } + + if (this.readAvatarNode) { + var topOffset = oldNodeTop - this.readAvatarNode.getBoundingClientRect().top; + + if (oldAvatarDomNode && oldAvatarDomNode.style.left !== '0px') { + var leftOffset = oldAvatarDomNode.style.left; + // start at the old height and in the old h pos + startStyles.push({ top: topOffset, left: leftOffset }); + enterTransitionOpts.push(reorderTransitionOpts); + } + + // then shift to the rightmost column, + // and then it will drop down to its resting position + startStyles.push({ top: topOffset, left: '0px' }); + enterTransitionOpts.push({ + duration: bounce ? Math.min(Math.log(Math.abs(topOffset)) * 200, 3000) : 300, + easing: bounce ? 'easeOutBounce' : 'easeOutCubic', + }); + } + + var style = { + left: left+'px', + top: '0px', + visibility: ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) ? 'visible' : 'hidden' + }; + + //console.log("i = " + i + ", MAX_READ_AVATARS = " + MAX_READ_AVATARS + ", allReadAvatars = " + this.state.allReadAvatars + " visibility = " + style.visibility); + + // add to the start so the most recent is on the end (ie. ends up rightmost) + avatars.unshift( + + ); + // TODO: we keep the extra read avatars in the dom to make animation simpler + // we could optimise this to reduce the dom size. + if (i < MAX_READ_AVATARS - 1 || this.state.allReadAvatars) { // XXX: where does this -1 come from? is it to make the max'th avatar animate properly? + left -= 15; + } + } + var editButton; + if (!this.state.allReadAvatars) { + var remainder = receipts.length - MAX_READ_AVATARS; + var remText; + if (i >= MAX_READ_AVATARS - 1) left -= 15; + if (remainder > 0) { + remText = { remainder }+ + ; + left -= 15; + } + editButton = ( + + ); + } + + return + { editButton } + { remText } + + { avatars } + + ; + }, + + collectReadAvatarNode: function(node) { + this.readAvatarNode = ReactDom.findDOMNode(node); + }, + + render: function() { + var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); + var SenderProfile = sdk.getComponent('molecules.SenderProfile'); + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + + var content = this.props.mxEvent.getContent(); + var msgtype = content.msgtype; + + var EventTileType = sdk.getComponent(eventTileTypes[this.props.mxEvent.getType()]); + // This shouldn't happen: the caller should check we support this type + // before trying to instantiate us + if (!EventTileType) { + throw new Error("Event type not supported"); + } + + var classes = classNames({ + mx_EventTile: true, + mx_EventTile_sending: ['sending', 'queued'].indexOf( + this.props.mxEvent.status + ) !== -1, + mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent', + mx_EventTile_highlight: this.shouldHighlight(), + mx_EventTile_continuation: this.props.continuation, + mx_EventTile_last: this.props.last, + mx_EventTile_contextual: this.props.contextual, + menu: this.state.menu, + }); + var timestamp = + + var aux = null; + if (msgtype === 'm.image') aux = "sent an image"; + else if (msgtype === 'm.video') aux = "sent a video"; + else if (msgtype === 'm.file') aux = "uploaded a file"; + + var readAvatars = this.getReadAvatars(); + + var avatar, sender; + if (!this.props.continuation) { + if (this.props.mxEvent.sender) { + avatar = ( +
+ +
+ ); + } + if (EventTileType.needsSenderProfile()) { + sender = ; + } + } + return ( +
+
+ { timestamp } + { readAvatars } +
+ { avatar } + { sender } +
+ +
+
+ ); + }, +}); diff --git a/src/controllers/molecules/MTextTile.js b/src/components/views/messages/MEmoteMessage.js similarity index 59% rename from src/controllers/molecules/MTextTile.js rename to src/components/views/messages/MEmoteMessage.js index d32d8ae911..26e29363cd 100644 --- a/src/controllers/molecules/MTextTile.js +++ b/src/components/views/messages/MEmoteMessage.js @@ -16,15 +16,29 @@ limitations under the License. 'use strict'; +var React = require('react'); var linkify = require('linkifyjs'); var linkifyElement = require('linkifyjs/element'); -var linkifyMatrix = require('../../linkify-matrix'); +var linkifyMatrix = require('../../../linkify-matrix'); linkifyMatrix(linkify); -module.exports = { +module.exports = React.createClass({ + displayName: 'MEmoteMessage', + componentDidMount: function() { linkifyElement(this.refs.content, linkifyMatrix.options); - } -}; + }, + + render: function() { + var mxEvent = this.props.mxEvent; + var content = mxEvent.getContent(); + var name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + return ( + + * {name} {content.body} + + ); + }, +}); diff --git a/src/controllers/molecules/MFileTile.js b/src/components/views/messages/MFileMessage.js similarity index 54% rename from src/controllers/molecules/MFileTile.js rename to src/components/views/messages/MFileMessage.js index 13b2e41ca5..93e366a2cd 100644 --- a/src/controllers/molecules/MFileTile.js +++ b/src/components/views/messages/MFileMessage.js @@ -16,9 +16,13 @@ limitations under the License. 'use strict'; +var React = require('react'); var filesize = require('filesize'); +var MatrixClientPeg = require('../../../MatrixClientPeg'); + +module.exports = React.createClass({ + displayName: 'MFileMessage', -module.exports = { presentableTextForFile: function(content) { var linkText = 'Attachment'; if (content.body && content.body.length > 0) { @@ -39,6 +43,31 @@ module.exports = { linkText += ' (' + additionals.join(', ') + ')'; } return linkText; - } -}; + }, + render: function() { + var content = this.props.mxEvent.getContent(); + var cli = MatrixClientPeg.get(); + + var httpUrl = cli.mxcUrlToHttp(content.url); + var text = this.presentableTextForFile(content); + + if (httpUrl) { + return ( + + + + ); + } else { + var extra = text ? ': '+text : ''; + return + Invalid file{extra} + + } + }, +}); diff --git a/src/components/views/messages/MImageMessage.js b/src/components/views/messages/MImageMessage.js new file mode 100644 index 0000000000..dee5c37c1e --- /dev/null +++ b/src/components/views/messages/MImageMessage.js @@ -0,0 +1,136 @@ +/* +Copyright 2015 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 filesize = require('filesize'); + +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var Modal = require('../../../Modal'); +var sdk = require('../../../index'); + +module.exports = React.createClass({ + displayName: 'MImageMessage', + + 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(ev) { + if (ev.button == 0 && !ev.metaKey) { + ev.preventDefault(); + var content = this.props.mxEvent.getContent(); + var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(content.url); + var ImageView = sdk.getComponent("elements.ImageView"); + Modal.createDialog(ImageView, { + src: httpUrl, + width: content.info.w, + height: content.info.h, + mxEvent: this.props.mxEvent, + }, "mx_Dialog_lightbox"); + } + }, + + _isGif: function() { + var content = this.props.mxEvent.getContent(); + return (content && content.info && content.info.mimetype === "image/gif"); + }, + + onImageEnter: function(e) { + if (!this._isGif()) { + return; + } + var imgElement = e.target; + imgElement.src = MatrixClientPeg.get().mxcUrlToHttp( + this.props.mxEvent.getContent().url + ); + }, + + onImageLeave: function(e) { + if (!this._isGif()) { + return; + } + var imgElement = e.target; + imgElement.src = this._getThumbUrl(); + }, + + _getThumbUrl: function() { + var content = this.props.mxEvent.getContent(); + return MatrixClientPeg.get().mxcUrlToHttp(content.url, 480, 360); + }, + + render: function() { + var content = this.props.mxEvent.getContent(); + var cli = MatrixClientPeg.get(); + + var thumbHeight = null; + if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 480, 360); + + var imgStyle = {}; + if (thumbHeight) imgStyle['height'] = thumbHeight; + + var thumbUrl = this._getThumbUrl(); + if (thumbUrl) { + return ( + + + {content.body} + + + + ); + } else if (content.body) { + return ( + + Image '{content.body}' cannot be displayed. + + ); + } else { + return ( + + This image cannot be displayed. + + ); + } + }, +}); diff --git a/src/components/views/messages/MNoticeMessage.js b/src/components/views/messages/MNoticeMessage.js new file mode 100644 index 0000000000..3a89d1ff6a --- /dev/null +++ b/src/components/views/messages/MNoticeMessage.js @@ -0,0 +1,59 @@ +/* +Copyright 2015 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 linkify = require('linkifyjs'); +var linkifyElement = require('linkifyjs/element'); +var linkifyMatrix = require('../../../linkify-matrix.js'); +linkifyMatrix(linkify); +var HtmlUtils = require('../../../HtmlUtils'); + +module.exports = React.createClass({ + displayName: 'MNoticeMessage', + + componentDidMount: function() { + linkifyElement(this.refs.content, linkifyMatrix.options); + + if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") + HtmlUtils.highlightDom(this.getDOMNode()); + }, + + componentDidUpdate: function() { + if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") + HtmlUtils.highlightDom(this.getDOMNode()); + }, + + shouldComponentUpdate: function(nextProps) { + // exploit that events are immutable :) + return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || + nextProps.searchTerm !== this.props.searchTerm); + }, + + // XXX: fix horrible duplication with MTextTile + render: function() { + var content = this.props.mxEvent.getContent(); + var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm); + + return ( + + { body } + + ); + }, +}); + diff --git a/src/components/views/messages/MRoomMemberEvent.js b/src/components/views/messages/MRoomMemberEvent.js new file mode 100644 index 0000000000..6e73519f2e --- /dev/null +++ b/src/components/views/messages/MRoomMemberEvent.js @@ -0,0 +1,52 @@ +/* +Copyright 2015 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 TextForEvent = require('../../../TextForEvent'); + +module.exports = React.createClass({ + displayName: 'MRoomMemberEvent', + + getMemberEventText: function() { + return TextForEvent.textForEvent(this.props.mxEvent); + }, + + render: function() { + // XXX: for now, just cheekily borrow the css from message tile... + var timestamp = this.props.last ? : null; + var text = this.getMemberEventText(); + if (!text) return
; + var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); + var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + return ( +
+
+ +
+ { timestamp } + + + { text } + +
+ ); + }, +}); + diff --git a/src/components/views/messages/MTextMessage.js b/src/components/views/messages/MTextMessage.js new file mode 100644 index 0000000000..d3b337cbc1 --- /dev/null +++ b/src/components/views/messages/MTextMessage.js @@ -0,0 +1,59 @@ +/* +Copyright 2015 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 HtmlUtils = require('../../../HtmlUtils'); +var linkify = require('linkifyjs'); +var linkifyElement = require('linkifyjs/element'); +var linkifyMatrix = require('../../../linkify-matrix'); + +linkifyMatrix(linkify); + +module.exports = React.createClass({ + displayName: 'MTextMessage', + + componentDidMount: function() { + linkifyElement(this.refs.content, linkifyMatrix.options); + + if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") + HtmlUtils.highlightDom(this.getDOMNode()); + }, + + componentDidUpdate: function() { + if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") + HtmlUtils.highlightDom(this.getDOMNode()); + }, + + shouldComponentUpdate: function(nextProps) { + // exploit that events are immutable :) + return (nextProps.mxEvent.getId() !== this.props.mxEvent.getId() || + nextProps.searchTerm !== this.props.searchTerm); + }, + + render: function() { + var content = this.props.mxEvent.getContent(); + var body = HtmlUtils.bodyToHtml(content, this.props.searchTerm); + + return ( + + { body } + + ); + }, +}); + diff --git a/src/components/views/messages/MVideoMessage.js b/src/components/views/messages/MVideoMessage.js new file mode 100644 index 0000000000..5771ed2172 --- /dev/null +++ b/src/components/views/messages/MVideoMessage.js @@ -0,0 +1,83 @@ +/* +Copyright 2015 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 filesize = require('filesize'); + +var MatrixClientPeg = require('../../../MatrixClientPeg'); +var Modal = require('../../../Modal'); +var sdk = require('../../../index'); + +module.exports = React.createClass({ + displayName: 'MVideoMessage', + + thumbScale: 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 widthMulti; + } + else { + // height is the dominant dimension so scaling will be fixed on that + return heightMulti; + } + }, + + render: function() { + var content = this.props.mxEvent.getContent(); + var cli = MatrixClientPeg.get(); + + var height = null; + var width = null; + var poster = null; + var preload = "metadata"; + if (content.info) { + var scale = this.thumbScale(content.info.w, content.info.h, 480, 360); + if (scale) { + width = Math.floor(content.info.w * scale); + height = Math.floor(content.info.h * scale); + } + + if (content.info.thumbnail_url) { + poster = cli.mxcUrlToHttp(content.info.thumbnail_url); + preload = "none"; + } + } + + + + return ( + + + + ); + }, +}); diff --git a/src/components/views/messages/Message.js b/src/components/views/messages/Message.js new file mode 100644 index 0000000000..fa74a8e137 --- /dev/null +++ b/src/components/views/messages/Message.js @@ -0,0 +1,52 @@ +/* +Copyright 2015 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'); + +module.exports = React.createClass({ + displayName: 'Message', + + statics: { + needsSenderProfile: function() { + return true; + } + }, + + render: function() { + var UnknownMessageTile = sdk.getComponent('messages.UnknownMessage'); + + var tileTypes = { + 'm.text': sdk.getComponent('messages.MTextMessage'), + 'm.notice': sdk.getComponent('messages.MNoticeMessage'), + 'm.emote': sdk.getComponent('messages.MEmoteMessage'), + 'm.image': sdk.getComponent('messages.MImageMessage'), + 'm.file': sdk.getComponent('messages.MFileMessage'), + 'm.video': sdk.getComponent('messages.MVideoMessage') + }; + + var content = this.props.mxEvent.getContent(); + var msgtype = content.msgtype; + var TileType = UnknownMessageTile; + if (msgtype && tileTypes[msgtype]) { + TileType = tileTypes[msgtype]; + } + + return ; + }, +}); diff --git a/src/controllers/molecules/MEmoteTile.js b/src/components/views/messages/TextualEvent.js similarity index 50% rename from src/controllers/molecules/MEmoteTile.js rename to src/components/views/messages/TextualEvent.js index d32d8ae911..5a9a1408c5 100644 --- a/src/controllers/molecules/MEmoteTile.js +++ b/src/components/views/messages/TextualEvent.js @@ -16,15 +16,28 @@ limitations under the License. 'use strict'; -var linkify = require('linkifyjs'); -var linkifyElement = require('linkifyjs/element'); -var linkifyMatrix = require('../../linkify-matrix'); +var React = require('react'); -linkifyMatrix(linkify); +var TextForEvent = require('../../../TextForEvent'); -module.exports = { - componentDidMount: function() { - linkifyElement(this.refs.content, linkifyMatrix.options); - } -}; +module.exports = React.createClass({ + displayName: 'TextualEvent', + + statics: { + needsSenderProfile: function() { + return false; + } + }, + + render: function() { + var text = TextForEvent.textForEvent(this.props.mxEvent); + if (text == null || text.length == 0) return null; + + return ( +
+ {TextForEvent.textForEvent(this.props.mxEvent)} +
+ ); + }, +}); diff --git a/src/controllers/molecules/EventTile.js b/src/components/views/messages/UnknownMessage.js similarity index 64% rename from src/controllers/molecules/EventTile.js rename to src/components/views/messages/UnknownMessage.js index 953e33b516..c0392cbaf5 100644 --- a/src/controllers/molecules/EventTile.js +++ b/src/components/views/messages/UnknownMessage.js @@ -16,13 +16,17 @@ limitations under the License. 'use strict'; -var MatrixClientPeg = require("../../MatrixClientPeg"); +var React = require('react'); -module.exports = { - shouldHighlight: function() { - var actions = MatrixClientPeg.get().getPushActionsForEvent(this.props.mxEvent); - if (!actions || !actions.tweaks) { return false; } - return actions.tweaks.highlight; - } -}; +module.exports = React.createClass({ + displayName: 'UnknownMessage', + render: function() { + var content = this.props.mxEvent.getContent(); + return ( + + {content.body} + + ); + }, +}); diff --git a/src/controllers/molecules/MNoticeTile.js b/src/controllers/molecules/MNoticeTile.js deleted file mode 100644 index 597ce3cd10..0000000000 --- a/src/controllers/molecules/MNoticeTile.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2015 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 linkify = require('linkifyjs'); -var linkifyElement = require('linkifyjs/element'); -var linkifyMatrix = require('../../linkify-matrix.js'); -linkifyMatrix(linkify); - -module.exports = { - componentDidMount: function() { - linkifyElement(this.refs.content, linkifyMatrix.options); - } -}; diff --git a/src/controllers/molecules/MessageTile.js b/src/controllers/molecules/MessageTile.js deleted file mode 100644 index 7f3416d6db..0000000000 --- a/src/controllers/molecules/MessageTile.js +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright 2015 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 MatrixClientPeg = require("../../MatrixClientPeg"); - -module.exports = { -}; -