diff --git a/.travis.yml b/.travis.yml index e020ba7d..0656064e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,4 +19,4 @@ node_js: install: # clone the deps with depth 1: we know we will only ever need that one # commit. - - scripts/fetch-develop.deps.sh --depth 1 && npm install + - scripts/fetch-develop.deps.sh --depth 1 && npm i phantomjs-prebuilt && npm install diff --git a/package.json b/package.json index f5129006..3d7e6935 100644 --- a/package.json +++ b/package.json @@ -121,6 +121,7 @@ "karma-mocha": "^0.2.2", "karma-phantomjs-launcher": "^1.0.0", "karma-webpack": "^1.7.0", + "matrix-mock-request": "^1.0.0", "minimist": "^1.2.0", "mkdirp": "^0.5.1", "mocha": "^2.4.5", diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index bc82778a..1c287738 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -67,7 +67,7 @@ module.exports = React.createClass({ onResendClick: function() { Resend.resend(this.props.mxEvent); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onViewSourceClick: function() { @@ -75,7 +75,7 @@ module.exports = React.createClass({ Modal.createDialog(ViewSource, { content: this.props.mxEvent.event, }, 'mx_Dialog_viewsource'); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onViewClearSourceClick: function() { @@ -84,7 +84,7 @@ module.exports = React.createClass({ // FIXME: _clearEvent is private content: this.props.mxEvent._clearEvent, }, 'mx_Dialog_viewsource'); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onRedactClick: function() { @@ -106,12 +106,12 @@ module.exports = React.createClass({ }).done(); }, }, 'mx_Dialog_confirmredact'); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onCancelSendClick: function() { Resend.removeFromQueue(this.props.mxEvent); - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onForwardClick: function() { @@ -130,7 +130,7 @@ module.exports = React.createClass({ if (this.props.eventTileOps) { this.props.eventTileOps.unhideWidget(); } - if (this.props.onFinished) this.props.onFinished(); + this.closeMenu(); }, onQuoteClick: function() { @@ -139,6 +139,7 @@ module.exports = React.createClass({ action: 'quote', event: this.props.mxEvent, }); + this.closeMenu(); }, render: function() { diff --git a/src/skins/vector/css/_components.scss b/src/skins/vector/css/_components.scss index 18518bea..59e9425f 100644 --- a/src/skins/vector/css/_components.scss +++ b/src/skins/vector/css/_components.scss @@ -56,6 +56,7 @@ @import "./matrix-react-sdk/views/rooms/_SearchableEntityList.scss"; @import "./matrix-react-sdk/views/rooms/_TabCompleteBar.scss"; @import "./matrix-react-sdk/views/rooms/_TopUnreadMessagesBar.scss"; +@import "./matrix-react-sdk/views/rooms/_AppsDrawer.scss"; @import "./matrix-react-sdk/views/settings/_DevicesPanel.scss"; @import "./matrix-react-sdk/views/settings/_IntegrationsManager.scss"; @import "./matrix-react-sdk/views/voip/_CallView.scss"; diff --git a/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss b/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss index e614aca7..cf722e5a 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/messages/_MEmoteBody.scss @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_MEmoteBody { + white-space: pre-wrap; +} + .mx_MEmoteBody_sender { cursor: pointer; } diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss new file mode 100644 index 00000000..0fcabac1 --- /dev/null +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_AppsDrawer.scss @@ -0,0 +1,159 @@ +/* +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. +*/ + +.mx_AppsDrawer { +} + +.mx_AppsContainer { + display: flex; + flex-direction: row; + align-items: center; +} + +.mx_AddWidget_button { + order: 2; + cursor: pointer; + padding-right: 12px; + padding: 0; + margin: 0 0 5px 0; + color: $accent-color; + font-size: 12px; +} + +.mx_SetAppURLDialog_input { + border-radius: 3px; + border: 1px solid $input-border-color; + padding: 9px; + color: $primary-hairline-color; + background-color: $primary-bg-color; + font-size: 15px; +} + +.mx_AppTile { + width: 50%; + margin: 0 5px 2px 0; + border: 1px solid $primary-hairline-color; + border-radius: 2px; + // height: 350px; + // display: inline-block; +} + +.mx_AppTileFullWidth { + width: 100%; + margin: 0; + padding: 0; + border: 1px solid $primary-hairline-color; + border-radius: 2px; + // height: 350px; + // display: inline-block; +} + +.mx_AppTileMenuBar { + // height: 15px; + margin: 0; + padding: 2px 10px; + // background-color: $e2e-verified-color; + border-bottom: 1px solid $primary-hairline-color; + font-size: 10px; +} + +.mx_AppTileMenuBarWidgets { + float: right; +} +.mx_AppTileMenuBarWidget { + // pointer-events: none; + cursor: pointer; +} + +.mx_AppTileBody iframe { + width: 100%; + height: 350px; + overflow: hidden; + border: none; + padding: 0; + margin: 0; + display: block; +} + +.mx_CloseAppWidget { +} + +.mx_AppTileMenuBarWidgetPadding { + margin-right: 5px; +} + +.mx_AppIconTile { + background-color: $lightbox-bg-color; + border: 1px solid rgba(0, 0, 0, 0); + width: 200px; + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + border-radius: 3px; + margin: 5px; + display: inline-block; +} + +.mx_AppIconTile.mx_AppIconTile_active { + color: $accent-color; + border-color: $accent-color; +} + +.mx_AppIconTile:hover { + border: 1px solid $accent-color; + box-shadow: 0 0 10px 5px rgba(200,200,200,0.5); +} + +.mx_AppIconTile_content { + padding: 2px 16px; + height: 60px; + overflow: hidden; +} + +.mx_AppIconTile_content h4 { + margin-top: 5px; + margin-bottom: 2px; +} + +.mx_AppIconTile_content p { + margin-top: 0; + margin-bottom: 5px; + font-size: smaller; +} + +.mx_AppIconTile_image { + padding: 10px; + width: 75%; + max-width:100px; + max-height:100px; + width: auto; + height: auto; +} + +.mx_AppIconTile_imageContainer { + text-align: center; + width: 100%; + background-color: white; + border-radius: 3px 3px 0 0; + height: 155px; + display: flex; + justify-content: center; + align-items: center; +} + +form.mx_Custom_Widget_Form div { + margin-top: 10px; + margin-bottom: 10px; +} diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss index 062dd0ba..704b3aa9 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_Autocomplete.scss @@ -38,6 +38,7 @@ .mx_Autocomplete_Completion_pill { border-radius: 17px; height: 34px; + padding: 0px 5px; display: flex; user-select: none; cursor: pointer; @@ -45,10 +46,21 @@ color: $primary-fg-color; } -.mx_Autocomplete_Completion_pill * { +.mx_Autocomplete_Completion_pill > * { margin: 0 3px; } +.mx_Autocomplete_provider_name, +.mx_Autocomplete_Completion_title, +.mx_Autocomplete_Completion_subtitle, +.mx_Autocomplete_Completion_description { + /* Ellipsis for long names/subtitles/descriptions*/ + max-width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + /* container for pill-style completions */ .mx_Autocomplete_Completion_container_pill { margin: 12px; diff --git a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss index 85c0e2c7..49111afd 100644 --- a/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss +++ b/src/skins/vector/css/matrix-react-sdk/views/rooms/_MessageComposer.scss @@ -128,7 +128,8 @@ limitations under the License. .mx_MessageComposer_upload, .mx_MessageComposer_hangup, .mx_MessageComposer_voicecall, -.mx_MessageComposer_videocall { +.mx_MessageComposer_videocall, +.mx_MessageComposer_apps { /*display: table-cell;*/ /*vertical-align: middle;*/ /*padding-left: 10px;*/ @@ -140,7 +141,8 @@ limitations under the License. .mx_MessageComposer_upload object, .mx_MessageComposer_hangup object, .mx_MessageComposer_voicecall object, -.mx_MessageComposer_videocall object { +.mx_MessageComposer_videocall object, +.mx_MessageComposer_apps object { pointer-events: none; } diff --git a/src/skins/vector/img/edit.svg b/src/skins/vector/img/edit.svg new file mode 100644 index 00000000..a0be3454 --- /dev/null +++ b/src/skins/vector/img/edit.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/skins/vector/img/icons-apps-active.svg b/src/skins/vector/img/icons-apps-active.svg new file mode 100644 index 00000000..ea222d05 --- /dev/null +++ b/src/skins/vector/img/icons-apps-active.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/skins/vector/img/icons-apps.svg b/src/skins/vector/img/icons-apps.svg new file mode 100644 index 00000000..affd8e64 --- /dev/null +++ b/src/skins/vector/img/icons-apps.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/test/app-tests/joining.js b/test/app-tests/joining.js index d9670125..11fd3d48 100644 --- a/test/app-tests/joining.js +++ b/test/app-tests/joining.js @@ -36,7 +36,7 @@ var expect = require('expect'); var q = require('q'); var test_utils = require('../test-utils'); -var MockHttpBackend = require('../mock-request'); +var MockHttpBackend = require('matrix-mock-request'); var HS_URL='http://localhost'; var IS_URL='http://localhost'; diff --git a/test/app-tests/loading.js b/test/app-tests/loading.js index 54e98760..d01836a3 100644 --- a/test/app-tests/loading.js +++ b/test/app-tests/loading.js @@ -33,7 +33,7 @@ import {VIEWS} from 'matrix-react-sdk/lib/components/structures/MatrixChat'; import dis from 'matrix-react-sdk/lib/dispatcher'; import * as test_utils from '../test-utils'; -import MockHttpBackend from '../mock-request'; +import MockHttpBackend from 'matrix-mock-request'; import {parseQs, parseQsFromFragment} from '../../src/vector/url_utils'; var DEFAULT_HS_URL='http://my_server'; diff --git a/test/mock-request.js b/test/mock-request.js deleted file mode 100644 index 64ac6c06..00000000 --- a/test/mock-request.js +++ /dev/null @@ -1,336 +0,0 @@ -"use strict"; -const q = require("q"); -import expect from 'expect'; - -/** - * Construct a mock HTTP backend, heavily inspired by Angular.js. - * @constructor - */ -function HttpBackend() { - this.requests = []; - this.expectedRequests = []; - const self = this; - // the request function dependency that the SDK needs. - this.requestFn = function(opts, callback) { - const req = new Request(opts, callback); - console.log(`${Date.now()} HTTP backend received request: ${req}`); - self.requests.push(req); - - const abort = function() { - const idx = self.requests.indexOf(req); - if (idx >= 0) { - console.log("Aborting HTTP request: %s %s", opts.method, - opts.uri); - self.requests.splice(idx, 1); - req.callback("aborted"); - } - }; - - return { - abort: abort, - }; - }; - - // very simplistic mapping from the whatwg fetch interface onto the request - // interface, so we can use the same mock backend for both. - this.fetchFn = function(input, init) { - init = init || {}; - const requestOpts = { - uri: input, - method: init.method || 'GET', - body: init.body, - }; - - return new Promise((resolve, reject) => { - function callback(err, response, body) { - if (err) { - reject(err); - } - resolve({ - ok: response.statusCode >= 200 && response.statusCode < 300, - json: () => body, - }); - }; - - const req = new Request(requestOpts, callback); - console.log(`HTTP backend received request: ${req}`); - self.requests.push(req); - }); - }; -} -HttpBackend.prototype = { - /** - * Respond to all of the requests (flush the queue). - * @param {string} path The path to flush (optional) default: all. - * @param {integer} numToFlush The number of things to flush (optional), default: all. - * @param {integer=} waitTime The time (in ms) to wait for a request to happen. - * default: 100 - * - * @return {Promise} resolves when there is nothing left to flush, with the - * number of requests flushed - */ - flush: function(path, numToFlush, waitTime) { - const defer = q.defer(); - const self = this; - let flushed = 0; - if (waitTime === undefined) { - waitTime = 100; - } - - function log(msg) { - console.log(`${Date.now()} flush[${path || ''}]: ${msg}`); - } - - log("HTTP backend flushing... (path=" + path - + " numToFlush=" + numToFlush - + " waitTime=" + waitTime - + ")", - ); - const endTime = waitTime + Date.now(); - - const tryFlush = function() { - // if there's more real requests and more expected requests, flush 'em. - log(` trying to flush => reqs=[${self.requests}] ` + - `expected=[${self.expectedRequests}]`, - ); - if (self._takeFromQueue(path)) { - // try again on the next tick. - flushed += 1; - if (numToFlush && flushed === numToFlush) { - log(`Flushed assigned amount: ${numToFlush}`); - defer.resolve(flushed); - } else { - log(` flushed. Trying for more.`); - setTimeout(tryFlush, 0); - } - } else if (flushed === 0 && Date.now() < endTime) { - // we may not have made the request yet, wait a generous amount of - // time before giving up. - log(` nothing to flush yet; waiting for requests.`); - setTimeout(tryFlush, 5); - } else { - if (flushed === 0) { - log("nothing to flush; giving up"); - } else { - log(`no more flushes after flushing ${flushed} requests`); - } - defer.resolve(flushed); - } - }; - - setTimeout(tryFlush, 0); - - return defer.promise; - }, - - /** - * Attempts to resolve requests/expected requests. - * @param {string} path The path to flush (optional) default: all. - * @return {boolean} true if something was resolved. - */ - _takeFromQueue: function(path) { - let req = null; - let i; - let j; - let matchingReq = null; - let expectedReq = null; - let testResponse = null; - for (i = 0; i < this.requests.length; i++) { - req = this.requests[i]; - for (j = 0; j < this.expectedRequests.length; j++) { - expectedReq = this.expectedRequests[j]; - if (path && path !== expectedReq.path) { - continue; - } - if (expectedReq.method === req.method && - req.path.indexOf(expectedReq.path) !== -1) { - if (!expectedReq.data || (JSON.stringify(expectedReq.data) === - JSON.stringify(req.data))) { - matchingReq = expectedReq; - this.expectedRequests.splice(j, 1); - break; - } - } - } - - if (matchingReq) { - // remove from request queue - this.requests.splice(i, 1); - i--; - - for (j = 0; j < matchingReq.checks.length; j++) { - matchingReq.checks[j](req); - } - testResponse = matchingReq.response; - console.log(`${Date.now()} responding to ${matchingReq.path}`); - let body = testResponse.body; - if (Object.prototype.toString.call(body) == "[object Function]") { - body = body(req.path, req.data); - } - req.callback( - testResponse.err, testResponse.response, body, - ); - matchingReq = null; - } - } - if (testResponse) { // flushed something - return true; - } - return false; - }, - - /** - * Makes sure that the SDK hasn't sent any more requests to the backend. - */ - verifyNoOutstandingRequests: function() { - const firstOutstandingReq = this.requests[0] || {}; - expect(this.requests.length).toEqual(0, - "Expected no more HTTP requests but received request to " + - firstOutstandingReq.path, - ); - }, - - /** - * Makes sure that the test doesn't have any unresolved requests. - */ - verifyNoOutstandingExpectation: function() { - const firstOutstandingExpectation = this.expectedRequests[0] || {}; - expect(this.expectedRequests.length).toEqual(0, - "Expected to see HTTP request for " + firstOutstandingExpectation.path, - ); - }, - - /** - * Create an expected request. - * @param {string} method The HTTP method - * @param {string} path The path (which can be partial) - * @param {Object} data The expected data. - * @return {Request} An expected request. - */ - when: function(method, path, data) { - const pendingReq = new ExpectedRequest(method, path, data); - this.expectedRequests.push(pendingReq); - return pendingReq; - }, -}; - -/** - * Represents the expectation of a request. - * - *

Includes the conditions to be matched against, the checks to be made, - * and the response to be returned. - * - * @constructor - * @param {string} method - * @param {string} path - * @param {object?} data - */ -function ExpectedRequest(method, path, data) { - this.method = method; - this.path = path; - this.data = data; - this.response = null; - this.checks = []; -} - -ExpectedRequest.prototype = { - toString: function() { - return this.method + " " + this.path - }, - - /** - * Execute a check when this request has been satisfied. - * @param {Function} fn The function to execute. - * @return {Request} for chaining calls. - */ - check: function(fn) { - this.checks.push(fn); - return this; - }, - - /** - * Respond with the given data when this request is satisfied. - * @param {Number} code The HTTP status code. - * @param {Object|Function} data The HTTP JSON body. If this is a function, - * it will be invoked when the JSON body is required (which should be returned). - */ - respond: function(code, data) { - this.response = { - response: { - statusCode: code, - headers: {}, - }, - body: data, - err: null, - }; - }, - - /** - * Fail with an Error when this request is satisfied. - * @param {Number} code The HTTP status code. - * @param {Error} err The error to throw (e.g. Network Error) - */ - fail: function(code, err) { - this.response = { - response: { - statusCode: code, - headers: {}, - }, - body: null, - err: err, - }; - }, -}; - -/** - * Represents a request made by the app. - * - * @constructor - * @param {object} opts opts passed to request() - * @param {function} callback - */ -function Request(opts, callback) { - this.opts = opts; - this.callback = callback; - - Object.defineProperty(this, 'method', { - get: function() { - return opts.method; - }, - }); - - Object.defineProperty(this, 'path', { - get: function() { - return opts.uri; - }, - }); - - Object.defineProperty(this, 'data', { - get: function() { - return opts.body; - }, - }); - - Object.defineProperty(this, 'queryParams', { - get: function() { - return opts.qs; - }, - }); - - Object.defineProperty(this, 'headers', { - get: function() { - return opts.headers || {}; - }, - }); -} - -Request.prototype = { - toString: function() { - return this.method + " " + this.path; - }, -}; - -/** - * The HttpBackend class. - */ -module.exports = HttpBackend;