Merge branch 'develop' into wmwragg/direct-chat-sublist
This commit is contained in:
commit
769e7d3b2e
24 changed files with 707 additions and 130 deletions
3
.travis.yml
Normal file
3
.travis.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- node # Latest stable version of nodejs.
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -1,3 +1,57 @@
|
||||||
|
Changes in [0.6.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.5) (2016-08-28)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4-r1...v0.6.5)
|
||||||
|
|
||||||
|
* re-add leave button in RoomSettings
|
||||||
|
* add /user URLs
|
||||||
|
* recognise matrix.to links and other vector links
|
||||||
|
* fix linkify dependency
|
||||||
|
* fix avatar clicking in MemberInfo
|
||||||
|
* fix emojione sizing
|
||||||
|
[\#431](https://github.com/matrix-org/matrix-react-sdk/pull/431)
|
||||||
|
* Fix NPE when we don't know the sender of an event
|
||||||
|
[\#430](https://github.com/matrix-org/matrix-react-sdk/pull/430)
|
||||||
|
* Update annoying TimelinePanel test
|
||||||
|
[\#429](https://github.com/matrix-org/matrix-react-sdk/pull/429)
|
||||||
|
* add fancy changelog dialog
|
||||||
|
[\#416](https://github.com/matrix-org/matrix-react-sdk/pull/416)
|
||||||
|
* Send bot options with leading underscore on the state key
|
||||||
|
[\#428](https://github.com/matrix-org/matrix-react-sdk/pull/428)
|
||||||
|
* Update autocomplete design and scroll it correctly
|
||||||
|
[\#419](https://github.com/matrix-org/matrix-react-sdk/pull/419)
|
||||||
|
* Add ability to query and set bot options
|
||||||
|
[\#427](https://github.com/matrix-org/matrix-react-sdk/pull/427)
|
||||||
|
* Add .travis.yml
|
||||||
|
[\#425](https://github.com/matrix-org/matrix-react-sdk/pull/425)
|
||||||
|
* Added event/info message avatars back in
|
||||||
|
[\#426](https://github.com/matrix-org/matrix-react-sdk/pull/426)
|
||||||
|
* Add postMessage API required for integration provisioning
|
||||||
|
[\#423](https://github.com/matrix-org/matrix-react-sdk/pull/423)
|
||||||
|
* Fix TimelinePanel test
|
||||||
|
[\#424](https://github.com/matrix-org/matrix-react-sdk/pull/424)
|
||||||
|
* Wmwragg/chat message presentation
|
||||||
|
[\#422](https://github.com/matrix-org/matrix-react-sdk/pull/422)
|
||||||
|
* Only try to delete room rule if it exists
|
||||||
|
[\#421](https://github.com/matrix-org/matrix-react-sdk/pull/421)
|
||||||
|
* Make the notification slider work
|
||||||
|
[\#420](https://github.com/matrix-org/matrix-react-sdk/pull/420)
|
||||||
|
* Don't download E2E devices if feature disabled
|
||||||
|
[\#418](https://github.com/matrix-org/matrix-react-sdk/pull/418)
|
||||||
|
* strip (IRC) suffix from tabcomplete entries
|
||||||
|
[\#417](https://github.com/matrix-org/matrix-react-sdk/pull/417)
|
||||||
|
* ignore local busy
|
||||||
|
[\#415](https://github.com/matrix-org/matrix-react-sdk/pull/415)
|
||||||
|
* defaultDeviceDisplayName should be a prop
|
||||||
|
[\#414](https://github.com/matrix-org/matrix-react-sdk/pull/414)
|
||||||
|
* Use server-generated deviceId
|
||||||
|
[\#410](https://github.com/matrix-org/matrix-react-sdk/pull/410)
|
||||||
|
* Set initial_device_display_name on login and register
|
||||||
|
[\#413](https://github.com/matrix-org/matrix-react-sdk/pull/413)
|
||||||
|
* Add device_id to devices display
|
||||||
|
[\#409](https://github.com/matrix-org/matrix-react-sdk/pull/409)
|
||||||
|
* Don't use MatrixClientPeg for temporary clients
|
||||||
|
[\#408](https://github.com/matrix-org/matrix-react-sdk/pull/408)
|
||||||
|
|
||||||
Changes in [0.6.4-r1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.4-r1) (2016-08-12)
|
Changes in [0.6.4-r1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.6.4-r1) (2016-08-12)
|
||||||
=========================================================================================================
|
=========================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4...v0.6.4-r1)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.6.4...v0.6.4-r1)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "0.6.4-r1",
|
"version": "0.6.5",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -37,10 +37,10 @@
|
||||||
"fuse.js": "^2.2.0",
|
"fuse.js": "^2.2.0",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.14",
|
||||||
"highlight.js": "^8.9.1",
|
"highlight.js": "^8.9.1",
|
||||||
"linkifyjs": "^2.0.0-beta.4",
|
"linkifyjs": "2.0.0-beta.4",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
"marked": "^0.3.5",
|
"marked": "^0.3.5",
|
||||||
"matrix-js-sdk": "0.5.5",
|
"matrix-js-sdk": "0.5.6",
|
||||||
"optimist": "^0.6.1",
|
"optimist": "^0.6.1",
|
||||||
"q": "^1.4.1",
|
"q": "^1.4.1",
|
||||||
"react": "^15.2.1",
|
"react": "^15.2.1",
|
||||||
|
|
|
@ -49,7 +49,7 @@ export function unicodeToImage(str) {
|
||||||
alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode];
|
||||||
const title = mappedUnicode[unicode];
|
const title = mappedUnicode[unicode];
|
||||||
|
|
||||||
replaceWith = `<img class="emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${unicode}.svg${emojione.cacheBustParam}"/>`;
|
replaceWith = `<img class="mx_emojione" title="${title}" alt="${alt}" src="${emojione.imagePathSVG}${unicode}.svg${emojione.cacheBustParam}"/>`;
|
||||||
return replaceWith;
|
return replaceWith;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -85,12 +85,28 @@ var sanitizeHtmlParams = {
|
||||||
transformTags: { // custom to matrix
|
transformTags: { // custom to matrix
|
||||||
// add blank targets to all hyperlinks except vector URLs
|
// add blank targets to all hyperlinks except vector URLs
|
||||||
'a': function(tagName, attribs) {
|
'a': function(tagName, attribs) {
|
||||||
var m = attribs.href ? attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN) : null;
|
if (attribs.href) {
|
||||||
if (m) {
|
attribs.target = '_blank'; // by default
|
||||||
delete attribs.target;
|
|
||||||
}
|
var m;
|
||||||
else {
|
// FIXME: horrible duplication with linkify-matrix
|
||||||
attribs.target = '_blank';
|
m = attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN);
|
||||||
|
if (m) {
|
||||||
|
attribs.href = m[1];
|
||||||
|
delete attribs.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
||||||
|
if (m) {
|
||||||
|
var entity = m[1];
|
||||||
|
if (entity[0] === '@') {
|
||||||
|
attribs.href = '#/user/' + entity;
|
||||||
|
}
|
||||||
|
else if (entity[0] === '#' || entity[0] === '!') {
|
||||||
|
attribs.href = '#/room/' + entity;
|
||||||
|
}
|
||||||
|
delete attribs.target;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||||
return { tagName: tagName, attribs : attribs };
|
return { tagName: tagName, attribs : attribs };
|
||||||
|
@ -271,7 +287,7 @@ module.exports = {
|
||||||
|
|
||||||
emojifyText: function(text) {
|
emojifyText: function(text) {
|
||||||
return {
|
return {
|
||||||
__html: emojione.unicodeToImage(escape(text)),
|
__html: unicodeToImage(escape(text)),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
273
src/ScalarMessaging.js
Normal file
273
src/ScalarMessaging.js
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
|
||||||
|
{
|
||||||
|
action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
|
||||||
|
room_id: $ROOM_ID,
|
||||||
|
user_id: $USER_ID
|
||||||
|
// additional request fields
|
||||||
|
}
|
||||||
|
|
||||||
|
The complete request object is returned to the caller with an additional "response" key like so:
|
||||||
|
{
|
||||||
|
action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
|
||||||
|
room_id: $ROOM_ID,
|
||||||
|
user_id: $USER_ID,
|
||||||
|
// additional request fields
|
||||||
|
response: { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
The "action" determines the format of the request and response. All actions can return an error response.
|
||||||
|
An error response is a "response" object which consists of a sole "error" key to indicate an error.
|
||||||
|
They look like:
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
message: "Unable to invite user into room.",
|
||||||
|
_error: <Original Error Object>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
The "message" key should be a human-friendly string.
|
||||||
|
|
||||||
|
ACTIONS
|
||||||
|
=======
|
||||||
|
All actions can return an error response instead of the response outlined below.
|
||||||
|
|
||||||
|
invite
|
||||||
|
------
|
||||||
|
Invites a user into a room.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- room_id is the room to invite the user into.
|
||||||
|
- user_id is the user ID to invite.
|
||||||
|
- No additional fields.
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
action: "invite",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
user_id: "@invitee:bar",
|
||||||
|
response: {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_bot_options
|
||||||
|
---------------
|
||||||
|
Set the m.room.bot.options state event for a bot user.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- room_id is the room to send the state event into.
|
||||||
|
- user_id is the user ID of the bot who you're setting options for.
|
||||||
|
- "content" is an object consisting of the content you wish to set.
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
action: "set_bot_options",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
user_id: "@bot:bar",
|
||||||
|
content: {
|
||||||
|
default_option: "alpha"
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
success: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
membership_state AND bot_options
|
||||||
|
--------------------------------
|
||||||
|
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
|
||||||
|
|
||||||
|
NB: Whilst this API is basically equivalent to getStateEvent, we specifically do not
|
||||||
|
want external entities to be able to query any state event for any room, hence the
|
||||||
|
restrictive API outlined here.
|
||||||
|
|
||||||
|
Request:
|
||||||
|
- room_id is the room which has the state event.
|
||||||
|
- user_id is the state_key parameter which in both cases is a user ID (the member or the bot).
|
||||||
|
- No additional fields.
|
||||||
|
Response:
|
||||||
|
- The event content. If there is no state event, the "response" key should be null.
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
action: "membership_state",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
user_id: "@somemember:bar",
|
||||||
|
response: {
|
||||||
|
membership: "join",
|
||||||
|
displayname: "Bob",
|
||||||
|
avatar_url: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SdkConfig = require('./SdkConfig');
|
||||||
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||||
|
|
||||||
|
function sendResponse(event, res) {
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = res;
|
||||||
|
event.source.postMessage(data, event.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendError(event, msg, nestedError) {
|
||||||
|
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
||||||
|
const data = JSON.parse(JSON.stringify(event.data));
|
||||||
|
data.response = {
|
||||||
|
error: {
|
||||||
|
message: msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (nestedError) {
|
||||||
|
data.response.error._error = nestedError;
|
||||||
|
}
|
||||||
|
event.source.postMessage(data, event.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inviteUser(event, roomId, userId) {
|
||||||
|
console.log(`Received request to invite ${userId} into room ${roomId}`);
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
sendError(event, "You need to be logged in.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (room) {
|
||||||
|
// if they are already invited we can resolve immediately.
|
||||||
|
const member = room.getMember(userId);
|
||||||
|
if (member && member.membership === "invite") {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.invite(roomId, userId).done(function() {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}, function(err) {
|
||||||
|
sendError(event, "You need to be able to invite users to do that.", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBotOptions(event, roomId, userId) {
|
||||||
|
console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
sendError(event, "You need to be logged in.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
|
||||||
|
sendResponse(event, {
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}, (err) => {
|
||||||
|
sendError(event, err.message ? err.message : "Failed to send request.", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMembershipState(event, roomId, userId) {
|
||||||
|
console.log(`membership_state of ${userId} in room ${roomId} requested.`);
|
||||||
|
returnStateEvent(event, roomId, "m.room.member", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function botOptions(event, roomId, userId) {
|
||||||
|
console.log(`bot_options of ${userId} in room ${roomId} requested.`);
|
||||||
|
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function returnStateEvent(event, roomId, eventType, stateKey) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
if (!client) {
|
||||||
|
sendError(event, "You need to be logged in.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const room = client.getRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
sendError(event, "This room is not recognised.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
|
||||||
|
if (!stateEvent) {
|
||||||
|
sendResponse(event, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendResponse(event, stateEvent.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMessage = function(event) {
|
||||||
|
if (!event.origin) { // stupid chrome
|
||||||
|
event.origin = event.originalEvent.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check it is from the integrations UI URL (remove trailing spaces)
|
||||||
|
let url = SdkConfig.get().integrations_ui_url;
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
url = url.substr(0, url.length - 1);
|
||||||
|
}
|
||||||
|
if (url !== event.origin) {
|
||||||
|
console.warn("Unauthorised postMessage received. Source URL: " + event.origin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = event.data.room_id;
|
||||||
|
const userId = event.data.user_id;
|
||||||
|
if (!userId) {
|
||||||
|
sendError(event, "Missing user_id in request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!roomId) {
|
||||||
|
sendError(event, "Missing room_id in request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (event.data.action) {
|
||||||
|
case "membership_state":
|
||||||
|
getMembershipState(event, roomId, userId);
|
||||||
|
break;
|
||||||
|
case "invite":
|
||||||
|
inviteUser(event, roomId, userId);
|
||||||
|
break;
|
||||||
|
case "bot_options":
|
||||||
|
botOptions(event, roomId, userId);
|
||||||
|
break;
|
||||||
|
case "set_bot_options":
|
||||||
|
setBotOptions(event, roomId, userId);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
startListening: function() {
|
||||||
|
window.addEventListener("message", onMessage, false);
|
||||||
|
},
|
||||||
|
|
||||||
|
stopListening: function() {
|
||||||
|
window.removeEventListener("message", onMessage);
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
export default class AutocompleteProvider {
|
export default class AutocompleteProvider {
|
||||||
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
constructor(commandRegex?: RegExp, fuseOpts?: any) {
|
||||||
|
@ -51,4 +52,9 @@ export default class AutocompleteProvider {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return 'Default Provider';
|
return 'Default Provider';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
console.error('stub; should be implemented in subclasses');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Commands';
|
return '*️⃣ Commands';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): CommandProvider {
|
static getInstance(): CommandProvider {
|
||||||
|
@ -83,4 +83,10 @@ export default class CommandProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,62 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export function TextualCompletion({
|
/* These were earlier stateless functional components but had to be converted
|
||||||
title,
|
since we need to use refs/findDOMNode to access the underlying DOM node to focus the correct completion,
|
||||||
subtitle,
|
something that is not entirely possible with stateless functional components. One could
|
||||||
description,
|
presumably wrap them in a <div> before rendering but I think this is the better way to do it.
|
||||||
}: {
|
*/
|
||||||
title: ?string,
|
|
||||||
subtitle: ?string,
|
export class TextualCompletion extends React.Component {
|
||||||
description: ?string
|
render() {
|
||||||
}) {
|
const {
|
||||||
return (
|
title,
|
||||||
<div style={{width: '100%'}}>
|
subtitle,
|
||||||
<span>{title}</span>
|
description,
|
||||||
<em>{subtitle}</em>
|
className,
|
||||||
<span style={{color: 'gray', float: 'right'}}>{description}</span>
|
...restProps,
|
||||||
</div>
|
} = this.props;
|
||||||
);
|
return (
|
||||||
|
<div className={classNames('mx_Autocomplete_Completion_block', className)} {...restProps}>
|
||||||
|
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
||||||
|
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
||||||
|
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
TextualCompletion.propTypes = {
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
subtitle: React.PropTypes.string,
|
||||||
|
description: React.PropTypes.string,
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PillCompletion extends React.Component {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
description,
|
||||||
|
initialComponent,
|
||||||
|
className,
|
||||||
|
...restProps,
|
||||||
|
} = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classNames('mx_Autocomplete_Completion_pill', className)} {...restProps}>
|
||||||
|
{initialComponent}
|
||||||
|
<span className="mx_Autocomplete_Completion_title">{title}</span>
|
||||||
|
<span className="mx_Autocomplete_Completion_subtitle">{subtitle}</span>
|
||||||
|
<span className="mx_Autocomplete_Completion_description">{description}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PillCompletion.propTypes = {
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
subtitle: React.PropTypes.string,
|
||||||
|
description: React.PropTypes.string,
|
||||||
|
initialComponent: React.PropTypes.element,
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
};
|
||||||
|
|
|
@ -78,7 +78,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Results from DuckDuckGo';
|
return '🔍 Results from DuckDuckGo';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): DuckDuckGoProvider {
|
static getInstance(): DuckDuckGoProvider {
|
||||||
|
@ -87,4 +87,10 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_block">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
|
import sdk from '../index';
|
||||||
|
import {PillCompletion} from './Components';
|
||||||
|
|
||||||
const EMOJI_REGEX = /:\w*:?/g;
|
const EMOJI_REGEX = /:\w*:?/g;
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
||||||
|
@ -16,28 +18,28 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
completions = this.fuse.search(command[0]).map(result => {
|
completions = this.fuse.search(command[0]).map(result => {
|
||||||
let shortname = EMOJI_SHORTNAMES[result];
|
const shortname = EMOJI_SHORTNAMES[result];
|
||||||
let imageHTML = shortnameToImage(shortname);
|
const unicode = shortnameToUnicode(shortname);
|
||||||
return {
|
return {
|
||||||
completion: shortnameToUnicode(shortname),
|
completion: unicode,
|
||||||
component: (
|
component: (
|
||||||
<div className="mx_Autocomplete_Completion">
|
<PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} />
|
||||||
<span style={{maxWidth: '1em'}} dangerouslySetInnerHTML={{__html: imageHTML}}></span> {shortname}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
}).slice(0, 4);
|
}).slice(0, 8);
|
||||||
}
|
}
|
||||||
return Q.when(completions);
|
return Q.when(completions);
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Emoji';
|
return '😃 Emoji';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
|
@ -45,4 +47,10 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
instance = new EmojiProvider();
|
instance = new EmojiProvider();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import {TextualCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../MatrixTools';
|
import {getDisplayAliasForRoom} from '../MatrixTools';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
const ROOM_REGEX = /(?=#)([^\s]*)/g;
|
const ROOM_REGEX = /(?=#)([^\s]*)/g;
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
|
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||||
|
|
||||||
let client = MatrixClientPeg.get();
|
let client = MatrixClientPeg.get();
|
||||||
let completions = [];
|
let completions = [];
|
||||||
const {command, range} = this.getCurrentCommand(query, selection);
|
const {command, range} = this.getCurrentCommand(query, selection);
|
||||||
|
@ -39,7 +42,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: displayAlias,
|
||||||
component: (
|
component: (
|
||||||
<TextualCompletion title={room.name} description={displayAlias} />
|
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
};
|
};
|
||||||
|
@ -49,7 +52,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Rooms';
|
return '💬 Rooms';
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance() {
|
static getInstance() {
|
||||||
|
@ -59,4 +62,10 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ import React from 'react';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import {TextualCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
|
import sdk from '../index';
|
||||||
|
|
||||||
const USER_REGEX = /@[^\s]*/g;
|
const USER_REGEX = /@[^\s]*/g;
|
||||||
|
|
||||||
|
@ -20,6 +21,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCompletions(query: string, selection: {start: number, end: number}) {
|
getCompletions(query: string, selection: {start: number, end: number}) {
|
||||||
|
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||||
|
|
||||||
let completions = [];
|
let completions = [];
|
||||||
let {command, range} = this.getCurrentCommand(query, selection);
|
let {command, range} = this.getCurrentCommand(query, selection);
|
||||||
if (command) {
|
if (command) {
|
||||||
|
@ -29,7 +32,8 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
return {
|
return {
|
||||||
completion: user.userId,
|
completion: user.userId,
|
||||||
component: (
|
component: (
|
||||||
<TextualCompletion
|
<PillCompletion
|
||||||
|
initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
|
||||||
title={displayName}
|
title={displayName}
|
||||||
description={user.userId} />
|
description={user.userId} />
|
||||||
),
|
),
|
||||||
|
@ -41,7 +45,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
return 'Users';
|
return '👥 Users';
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserList(users) {
|
setUserList(users) {
|
||||||
|
@ -54,4 +58,10 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
}
|
}
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||||
|
return <div className="mx_Autocomplete_Completion_container_pill">
|
||||||
|
{completions}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ module.exports = React.createClass({
|
||||||
UserSettings: "user_settings",
|
UserSettings: "user_settings",
|
||||||
CreateRoom: "create_room",
|
CreateRoom: "create_room",
|
||||||
RoomDirectory: "room_directory",
|
RoomDirectory: "room_directory",
|
||||||
|
UserView: "user_view",
|
||||||
},
|
},
|
||||||
|
|
||||||
AuxPanel: {
|
AuxPanel: {
|
||||||
|
@ -87,6 +88,10 @@ module.exports = React.createClass({
|
||||||
// in the case where we view a room by ID or by RoomView when it resolves
|
// in the case where we view a room by ID or by RoomView when it resolves
|
||||||
// what ID an alias points at.
|
// what ID an alias points at.
|
||||||
currentRoomId: null,
|
currentRoomId: null,
|
||||||
|
|
||||||
|
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||||
|
viewUserId: null,
|
||||||
|
|
||||||
logged_in: false,
|
logged_in: false,
|
||||||
collapse_lhs: false,
|
collapse_lhs: false,
|
||||||
collapse_rhs: false,
|
collapse_rhs: false,
|
||||||
|
@ -94,6 +99,9 @@ module.exports = React.createClass({
|
||||||
width: 10000,
|
width: 10000,
|
||||||
sideOpacity: 1.0,
|
sideOpacity: 1.0,
|
||||||
middleOpacity: 1.0,
|
middleOpacity: 1.0,
|
||||||
|
|
||||||
|
version: null,
|
||||||
|
newVersion: null,
|
||||||
};
|
};
|
||||||
return s;
|
return s;
|
||||||
},
|
},
|
||||||
|
@ -736,6 +744,18 @@ module.exports = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch(payload);
|
dis.dispatch(payload);
|
||||||
}
|
}
|
||||||
|
} else if (screen.indexOf('user/') == 0) {
|
||||||
|
var userId = screen.substring(5);
|
||||||
|
this.setState({ viewUserId: userId });
|
||||||
|
this._setPage(this.PageTypes.UserView);
|
||||||
|
this.notifyNewScreen('user/' + userId);
|
||||||
|
var member = new Matrix.RoomMember(null, userId);
|
||||||
|
if (member) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_user',
|
||||||
|
member: member,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.info("Ignoring showScreen for '%s'", screen);
|
console.info("Ignoring showScreen for '%s'", screen);
|
||||||
|
@ -756,15 +776,13 @@ module.exports = React.createClass({
|
||||||
onUserClick: function(event, userId) {
|
onUserClick: function(event, userId) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
/*
|
// var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
||||||
var MemberInfo = sdk.getComponent('rooms.MemberInfo');
|
// var member = new Matrix.RoomMember(null, userId);
|
||||||
var member = new Matrix.RoomMember(null, userId);
|
// ContextualMenu.createMenu(MemberInfo, {
|
||||||
ContextualMenu.createMenu(MemberInfo, {
|
// member: member,
|
||||||
member: member,
|
// right: window.innerWidth - event.pageX,
|
||||||
right: window.innerWidth - event.pageX,
|
// top: event.pageY
|
||||||
top: event.pageY
|
// });
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
var member = new Matrix.RoomMember(null, userId);
|
var member = new Matrix.RoomMember(null, userId);
|
||||||
if (!member) { return; }
|
if (!member) { return; }
|
||||||
|
@ -856,6 +874,7 @@ module.exports = React.createClass({
|
||||||
onVersion: function(current, latest) {
|
onVersion: function(current, latest) {
|
||||||
this.setState({
|
this.setState({
|
||||||
version: current,
|
version: current,
|
||||||
|
newVersion: latest,
|
||||||
hasNewVersion: current !== latest
|
hasNewVersion: current !== latest
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -988,11 +1007,15 @@ module.exports = React.createClass({
|
||||||
page_element = <RoomDirectory />
|
page_element = <RoomDirectory />
|
||||||
right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
|
right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
|
||||||
break;
|
break;
|
||||||
|
case this.PageTypes.UserView:
|
||||||
|
page_element = null; // deliberately null for now
|
||||||
|
right_panel = <RightPanel userId={this.state.viewUserId} collapsed={false} opacity={this.state.sideOpacity} />
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var topBar;
|
var topBar;
|
||||||
if (this.state.hasNewVersion) {
|
if (this.state.hasNewVersion) {
|
||||||
topBar = <NewVersionBar />;
|
topBar = <NewVersionBar version={this.state.version} newVersion={this.state.newVersion} />;
|
||||||
}
|
}
|
||||||
else if (MatrixClientPeg.get().isGuest()) {
|
else if (MatrixClientPeg.get().isGuest()) {
|
||||||
topBar = <GuestWarningBar />;
|
topBar = <GuestWarningBar />;
|
||||||
|
|
|
@ -31,7 +31,6 @@ var KeyCode = require('../../KeyCode');
|
||||||
|
|
||||||
var PAGINATE_SIZE = 20;
|
var PAGINATE_SIZE = 20;
|
||||||
var INITIAL_SIZE = 20;
|
var INITIAL_SIZE = 20;
|
||||||
var TIMELINE_CAP = 250; // the most events to show in a timeline
|
|
||||||
|
|
||||||
var DEBUG = false;
|
var DEBUG = false;
|
||||||
|
|
||||||
|
@ -82,6 +81,9 @@ var TimelinePanel = React.createClass({
|
||||||
|
|
||||||
// opacity for dynamic UI fading effects
|
// opacity for dynamic UI fading effects
|
||||||
opacity: React.PropTypes.number,
|
opacity: React.PropTypes.number,
|
||||||
|
|
||||||
|
// maximum number of events to show in a timeline
|
||||||
|
timelineCap: React.PropTypes.number,
|
||||||
},
|
},
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
|
@ -92,6 +94,12 @@ var TimelinePanel = React.createClass({
|
||||||
roomReadMarkerTsMap: {},
|
roomReadMarkerTsMap: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
timelineCap: 250,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
var initialReadMarker =
|
var initialReadMarker =
|
||||||
TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
|
TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
|
||||||
|
@ -684,7 +692,7 @@ var TimelinePanel = React.createClass({
|
||||||
_loadTimeline: function(eventId, pixelOffset, offsetBase) {
|
_loadTimeline: function(eventId, pixelOffset, offsetBase) {
|
||||||
this._timelineWindow = new Matrix.TimelineWindow(
|
this._timelineWindow = new Matrix.TimelineWindow(
|
||||||
MatrixClientPeg.get(), this.props.room,
|
MatrixClientPeg.get(), this.props.room,
|
||||||
{windowLimit: TIMELINE_CAP});
|
{windowLimit: this.props.timelineCap});
|
||||||
|
|
||||||
var onLoaded = () => {
|
var onLoaded = () => {
|
||||||
this._reloadEvents();
|
this._reloadEvents();
|
||||||
|
|
|
@ -47,6 +47,9 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getState: function(props) {
|
_getState: function(props) {
|
||||||
|
if (!props.member) {
|
||||||
|
console.error("MemberAvatar called somehow with null member");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
name: props.member.name,
|
name: props.member.name,
|
||||||
title: props.member.userId,
|
title: props.member.userId,
|
||||||
|
|
|
@ -22,12 +22,6 @@ var sdk = require('../../../index');
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'MessageEvent',
|
displayName: 'MessageEvent',
|
||||||
|
|
||||||
statics: {
|
|
||||||
needsSenderProfile: function() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
/* the MatrixEvent to show */
|
/* the MatrixEvent to show */
|
||||||
mxEvent: React.PropTypes.object.isRequired,
|
mxEvent: React.PropTypes.object.isRequired,
|
||||||
|
|
|
@ -24,12 +24,6 @@ import sdk from '../../../index';
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'TextualEvent',
|
displayName: 'TextualEvent',
|
||||||
|
|
||||||
statics: {
|
|
||||||
needsSenderProfile: function() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||||
var text = TextForEvent.textForEvent(this.props.mxEvent);
|
var text = TextForEvent.textForEvent(this.props.mxEvent);
|
||||||
|
@ -39,4 +33,3 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import flatMap from 'lodash/flatMap';
|
import flatMap from 'lodash/flatMap';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
|
||||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
import {getCompletions} from '../../../autocomplete/Autocompleter';
|
||||||
|
|
||||||
|
@ -100,11 +101,27 @@ export default class Autocomplete extends React.Component {
|
||||||
this.setState({selectionOffset});
|
this.setState({selectionOffset});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
// this is the selected completion, so scroll it into view if needed
|
||||||
|
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
|
||||||
|
if (selectedCompletion && this.container) {
|
||||||
|
const domNode = ReactDOM.findDOMNode(selectedCompletion);
|
||||||
|
const offsetTop = domNode && domNode.offsetTop;
|
||||||
|
if (offsetTop > this.container.scrollTop + this.container.offsetHeight ||
|
||||||
|
offsetTop < this.container.scrollTop) {
|
||||||
|
this.container.scrollTop = offsetTop - this.container.offsetTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const EmojiText = sdk.getComponent('views.elements.EmojiText');
|
||||||
|
|
||||||
let position = 0;
|
let position = 0;
|
||||||
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
let renderedCompletions = this.state.completions.map((completionResult, i) => {
|
||||||
let completions = completionResult.completions.map((completion, i) => {
|
let completions = completionResult.completions.map((completion, i) => {
|
||||||
let className = classNames('mx_Autocomplete_Completion', {
|
|
||||||
|
const className = classNames('mx_Autocomplete_Completion', {
|
||||||
'selected': position === this.state.selectionOffset,
|
'selected': position === this.state.selectionOffset,
|
||||||
});
|
});
|
||||||
let componentPosition = position;
|
let componentPosition = position;
|
||||||
|
@ -116,40 +133,27 @@ export default class Autocomplete extends React.Component {
|
||||||
this.onConfirm();
|
this.onConfirm();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return React.cloneElement(completion.component, {
|
||||||
<div key={i}
|
key: i,
|
||||||
className={className}
|
ref: `completion${i}`,
|
||||||
onMouseOver={onMouseOver}
|
className,
|
||||||
onClick={onClick}>
|
onMouseOver,
|
||||||
{completion.component}
|
onClick,
|
||||||
</div>
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return completions.length > 0 ? (
|
return completions.length > 0 ? (
|
||||||
<div key={i} className="mx_Autocomplete_ProviderSection">
|
<div key={i} className="mx_Autocomplete_ProviderSection">
|
||||||
<span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
|
<EmojiText element="div" className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</EmojiText>
|
||||||
<ReactCSSTransitionGroup
|
{completionResult.provider.renderCompletions(completions)}
|
||||||
component="div"
|
|
||||||
transitionName="autocomplete"
|
|
||||||
transitionEnterTimeout={300}
|
|
||||||
transitionLeaveTimeout={300}>
|
|
||||||
{completions}
|
|
||||||
</ReactCSSTransitionGroup>
|
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_Autocomplete">
|
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||||
<ReactCSSTransitionGroup
|
{renderedCompletions}
|
||||||
component="div"
|
|
||||||
transitionName="autocomplete"
|
|
||||||
transitionEnterTimeout={300}
|
|
||||||
transitionLeaveTimeout={300}>
|
|
||||||
{renderedCompletions}
|
|
||||||
</ReactCSSTransitionGroup>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ var MAX_READ_AVATARS = 5;
|
||||||
// '----------------------------------------------------------'
|
// '----------------------------------------------------------'
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
displayName: 'Event',
|
displayName: 'EventTile',
|
||||||
|
|
||||||
statics: {
|
statics: {
|
||||||
haveTileForEvent: function(e) {
|
haveTileForEvent: function(e) {
|
||||||
|
@ -368,7 +368,7 @@ module.exports = React.createClass({
|
||||||
// room, or emote messages
|
// room, or emote messages
|
||||||
var isInfoMessage = (msgtype === 'm.emote' || eventType !== 'm.room.message');
|
var isInfoMessage = (msgtype === 'm.emote' || eventType !== 'm.room.message');
|
||||||
|
|
||||||
var EventTileType = sdk.getComponent(eventTileTypes[this.props.mxEvent.getType()]);
|
var EventTileType = sdk.getComponent(eventTileTypes[eventType]);
|
||||||
// This shouldn't happen: the caller should check we support this type
|
// This shouldn't happen: the caller should check we support this type
|
||||||
// before trying to instantiate us
|
// before trying to instantiate us
|
||||||
if (!EventTileType) {
|
if (!EventTileType) {
|
||||||
|
@ -395,25 +395,44 @@ module.exports = React.createClass({
|
||||||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
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 readAvatars = this.getReadAvatars();
|
||||||
|
|
||||||
var avatar, sender;
|
var avatar, sender;
|
||||||
if (!this.props.continuation && !isInfoMessage) {
|
let avatarSize;
|
||||||
if (this.props.mxEvent.sender) {
|
let needsSenderProfile;
|
||||||
avatar = (
|
|
||||||
|
if (isInfoMessage) {
|
||||||
|
// a small avatar, with no sender profile, for emotes and
|
||||||
|
// joins/parts/etc
|
||||||
|
avatarSize = 14;
|
||||||
|
needsSenderProfile = false;
|
||||||
|
} else if (this.props.continuation) {
|
||||||
|
// no avatar or sender profile for continuation messages
|
||||||
|
avatarSize = 0;
|
||||||
|
needsSenderProfile = false;
|
||||||
|
} else {
|
||||||
|
avatarSize = 30;
|
||||||
|
needsSenderProfile = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.mxEvent.sender && avatarSize) {
|
||||||
|
avatar = (
|
||||||
<div className="mx_EventTile_avatar">
|
<div className="mx_EventTile_avatar">
|
||||||
<MemberAvatar member={this.props.mxEvent.sender} width={30} height={30} onClick={ this.onMemberAvatarClick } />
|
<MemberAvatar member={this.props.mxEvent.sender}
|
||||||
|
width={avatarSize} height={avatarSize}
|
||||||
|
onClick={ this.onMemberAvatarClick }
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (EventTileType.needsSenderProfile()) {
|
|
||||||
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
|
if (needsSenderProfile) {
|
||||||
}
|
let 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";
|
||||||
|
|
||||||
|
sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
var editButton = (
|
var editButton = (
|
||||||
|
|
|
@ -531,7 +531,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMemberAvatarClick: function () {
|
onMemberAvatarClick: function () {
|
||||||
var avatarUrl = this.props.member.user.avatarUrl;
|
var avatarUrl = this.props.member.user ? this.props.member.user.avatarUrl : this.props.member.events.member.getContent().avatar_url;
|
||||||
if(!avatarUrl) return;
|
if(!avatarUrl) return;
|
||||||
|
|
||||||
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl);
|
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl);
|
||||||
|
|
|
@ -23,6 +23,7 @@ var Modal = require('../../../Modal');
|
||||||
var ObjectUtils = require("../../../ObjectUtils");
|
var ObjectUtils = require("../../../ObjectUtils");
|
||||||
var dis = require("../../../dispatcher");
|
var dis = require("../../../dispatcher");
|
||||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
var ScalarAuthClient = require("../../../ScalarAuthClient");
|
||||||
|
var ScalarMessaging = require('../../../ScalarMessaging');
|
||||||
var UserSettingsStore = require('../../../UserSettingsStore');
|
var UserSettingsStore = require('../../../UserSettingsStore');
|
||||||
|
|
||||||
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
// parse a string as an integer; if the input is undefined, or cannot be parsed
|
||||||
|
@ -70,6 +71,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
|
ScalarMessaging.startListening();
|
||||||
MatrixClientPeg.get().getRoomDirectoryVisibility(
|
MatrixClientPeg.get().getRoomDirectoryVisibility(
|
||||||
this.props.room.roomId
|
this.props.room.roomId
|
||||||
).done((result) => {
|
).done((result) => {
|
||||||
|
@ -93,6 +95,8 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
|
ScalarMessaging.stopListening();
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'ui_opacity',
|
action: 'ui_opacity',
|
||||||
sideOpacity: 1.0,
|
sideOpacity: 1.0,
|
||||||
|
@ -422,6 +426,27 @@ module.exports = React.createClass({
|
||||||
}, "");
|
}, "");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onLeaveClick() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'leave_room',
|
||||||
|
room_id: this.props.room.roomId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onForgetClick() {
|
||||||
|
// FIXME: duplicated with RoomTagContextualMenu (and dead code in RoomView)
|
||||||
|
MatrixClientPeg.get().forget(this.props.room.roomId).done(function() {
|
||||||
|
dis.dispatch({ action: 'view_next_room' });
|
||||||
|
}, function(err) {
|
||||||
|
var errCode = err.errcode || "unknown error code";
|
||||||
|
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: "Error",
|
||||||
|
description: `Failed to forget room (${errCode})`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_renderEncryptionSection: function() {
|
_renderEncryptionSection: function() {
|
||||||
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -540,6 +565,25 @@ module.exports = React.createClass({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var leaveButton = null;
|
||||||
|
var myMember = this.props.room.getMember(user_id);
|
||||||
|
if (myMember) {
|
||||||
|
if (myMember.membership === "join") {
|
||||||
|
leaveButton = (
|
||||||
|
<div className="mx_RoomSettings_leaveButton" onClick={ this.onLeaveClick }>
|
||||||
|
Leave room
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (myMember.membership === "leave") {
|
||||||
|
leaveButton = (
|
||||||
|
<div className="mx_RoomSettings_leaveButton" onClick={ this.onForgetClick }>
|
||||||
|
Forget room
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: support editing custom events_levels
|
// TODO: support editing custom events_levels
|
||||||
// TODO: support editing custom user_levels
|
// TODO: support editing custom user_levels
|
||||||
|
|
||||||
|
@ -627,6 +671,8 @@ module.exports = React.createClass({
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomSettings">
|
<div className="mx_RoomSettings">
|
||||||
|
|
||||||
|
{ leaveButton }
|
||||||
|
|
||||||
{ tagsSection }
|
{ tagsSection }
|
||||||
|
|
||||||
<div className="mx_RoomSettings_toggles">
|
<div className="mx_RoomSettings_toggles">
|
||||||
|
|
|
@ -100,7 +100,7 @@ export default class DevicesPanelEntry extends React.Component {
|
||||||
deleteButton = <div className="error">{this.state.deleteError}</div>
|
deleteButton = <div className="error">{this.state.deleteError}</div>
|
||||||
} else {
|
} else {
|
||||||
deleteButton = (
|
deleteButton = (
|
||||||
<div className="textButton"
|
<div className="mx_textButton"
|
||||||
onClick={this._onDeleteClick}>
|
onClick={this._onDeleteClick}>
|
||||||
Delete
|
Delete
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -95,6 +95,7 @@ function matrixLinkify(linkify) {
|
||||||
S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID);
|
S_AT_NAME_COLON_DOMAIN_DOT.on(TT.TLD, S_USERID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stubs, overwritten in MatrixChat's componentDidMount
|
||||||
matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); };
|
matrixLinkify.onUserClick = function(e, userId) { e.preventDefault(); };
|
||||||
matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); };
|
matrixLinkify.onAliasClick = function(e, roomAlias) { e.preventDefault(); };
|
||||||
|
|
||||||
|
@ -102,11 +103,14 @@ var escapeRegExp = function(string) {
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
};
|
};
|
||||||
|
|
||||||
// we only recognise URLs which match our current URL as being the same app
|
// Recognise URLs from both our local vector and official vector as vector.
|
||||||
// as if someone explicitly links to vector.im/develop and we're on vector.im/beta
|
// anyone else really should be using matrix.to.
|
||||||
// they may well be trying to get us to explicitly go to develop.
|
matrixLinkify.VECTOR_URL_PATTERN = "^(?:https?:\/\/)?(?:"
|
||||||
// FIXME: intercept matrix.to URLs as well.
|
+ escapeRegExp(window.location.host + window.location.pathname) + "|"
|
||||||
matrixLinkify.VECTOR_URL_PATTERN = "^(https?:\/\/)?" + escapeRegExp(window.location.host + window.location.pathname);
|
+ "(?:www\\.)?vector\\.im/(?:beta|staging|develop)/"
|
||||||
|
+ ")(#.*)";
|
||||||
|
|
||||||
|
matrixLinkify.MATRIXTO_URL_PATTERN = "^(?:https?:\/\/)?(?:www\\.)?matrix\\.to/#/((#|@|!).*)";
|
||||||
|
|
||||||
matrixLinkify.options = {
|
matrixLinkify.options = {
|
||||||
events: function (href, type) {
|
events: function (href, type) {
|
||||||
|
@ -131,8 +135,25 @@ matrixLinkify.options = {
|
||||||
case 'roomalias':
|
case 'roomalias':
|
||||||
return '#/room/' + href;
|
return '#/room/' + href;
|
||||||
case 'userid':
|
case 'userid':
|
||||||
return '#';
|
return '#/user/' + href;
|
||||||
default:
|
default:
|
||||||
|
var m;
|
||||||
|
// FIXME: horrible duplication with HtmlUtils' transform tags
|
||||||
|
m = href.match(matrixLinkify.VECTOR_URL_PATTERN);
|
||||||
|
if (m) {
|
||||||
|
return m[1];
|
||||||
|
}
|
||||||
|
m = href.match(matrixLinkify.MATRIXTO_URL_PATTERN);
|
||||||
|
if (m) {
|
||||||
|
var entity = m[1];
|
||||||
|
if (entity[0] === '@') {
|
||||||
|
return '#/user/' + entity;
|
||||||
|
}
|
||||||
|
else if (entity[0] === '#' || entity[0] === '!') {
|
||||||
|
return '#/room/' + entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return href;
|
return href;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -143,7 +164,9 @@ matrixLinkify.options = {
|
||||||
|
|
||||||
target: function(href, type) {
|
target: function(href, type) {
|
||||||
if (type === 'url') {
|
if (type === 'url') {
|
||||||
if (href.match(matrixLinkify.VECTOR_URL_PATTERN)) {
|
if (href.match(matrixLinkify.VECTOR_URL_PATTERN) ||
|
||||||
|
href.match(matrixLinkify.MATRIXTO_URL_PATTERN))
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -40,11 +40,12 @@ describe('TimelinePanel', function() {
|
||||||
var timeline;
|
var timeline;
|
||||||
var parentDiv;
|
var parentDiv;
|
||||||
|
|
||||||
function mkMessage() {
|
function mkMessage(opts) {
|
||||||
return test_utils.mkMessage(
|
return test_utils.mkMessage(
|
||||||
{
|
{
|
||||||
event: true, room: ROOM_ID, user: USER_ID,
|
event: true, room: ROOM_ID, user: USER_ID,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
|
... opts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ describe('TimelinePanel', function() {
|
||||||
// this is https://github.com/vector-im/vector-web/issues/1367
|
// this is https://github.com/vector-im/vector-web/issues/1367
|
||||||
|
|
||||||
// enough events to allow us to scroll back
|
// enough events to allow us to scroll back
|
||||||
var N_EVENTS = 20;
|
var N_EVENTS = 30;
|
||||||
for (var i = 0; i < N_EVENTS; i++) {
|
for (var i = 0; i < N_EVENTS; i++) {
|
||||||
timeline.addEvent(mkMessage());
|
timeline.addEvent(mkMessage());
|
||||||
}
|
}
|
||||||
|
@ -207,10 +208,11 @@ describe('TimelinePanel', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should let you scroll down again after you've scrolled up", function(done) {
|
it("should let you scroll down again after you've scrolled up", function(done) {
|
||||||
var N_EVENTS = 600;
|
var TIMELINE_CAP = 100; // needs to be more than we can fit in the div
|
||||||
|
var N_EVENTS = 120; // needs to be more than TIMELINE_CAP
|
||||||
|
|
||||||
// sadly, loading all those events takes a while
|
// sadly, loading all those events takes a while
|
||||||
this.timeout(N_EVENTS * 30);
|
this.timeout(N_EVENTS * 50);
|
||||||
|
|
||||||
// client.getRoom is called a /lot/ in this test, so replace
|
// client.getRoom is called a /lot/ in this test, so replace
|
||||||
// sinon's spy with a fast noop.
|
// sinon's spy with a fast noop.
|
||||||
|
@ -218,13 +220,15 @@ describe('TimelinePanel', function() {
|
||||||
|
|
||||||
// fill the timeline with lots of events
|
// fill the timeline with lots of events
|
||||||
for (var i = 0; i < N_EVENTS; i++) {
|
for (var i = 0; i < N_EVENTS; i++) {
|
||||||
timeline.addEvent(mkMessage());
|
timeline.addEvent(mkMessage({msg: "Event "+i}));
|
||||||
}
|
}
|
||||||
console.log("added events to timeline");
|
console.log("added events to timeline");
|
||||||
|
|
||||||
var scrollDefer;
|
var scrollDefer;
|
||||||
var panel = ReactDOM.render(
|
var panel = ReactDOM.render(
|
||||||
<TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}} />,
|
<TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
|
||||||
|
timelineCap={TIMELINE_CAP}
|
||||||
|
/>,
|
||||||
parentDiv
|
parentDiv
|
||||||
);
|
);
|
||||||
console.log("TimelinePanel rendered");
|
console.log("TimelinePanel rendered");
|
||||||
|
@ -256,14 +260,18 @@ describe('TimelinePanel', function() {
|
||||||
console.log("back paginating...");
|
console.log("back paginating...");
|
||||||
setScrollTop(0);
|
setScrollTop(0);
|
||||||
return awaitScroll().then(() => {
|
return awaitScroll().then(() => {
|
||||||
|
let eventTiles = scryEventTiles(panel);
|
||||||
|
let firstEvent = eventTiles[0].props.mxEvent;
|
||||||
|
|
||||||
|
console.log("TimelinePanel contains " + eventTiles.length +
|
||||||
|
" events; first is " +
|
||||||
|
firstEvent.getContent().body);
|
||||||
|
|
||||||
if(scrollingDiv.scrollTop > 0) {
|
if(scrollingDiv.scrollTop > 0) {
|
||||||
// need to go further
|
// need to go further
|
||||||
return backPaginate();
|
return backPaginate();
|
||||||
}
|
}
|
||||||
console.log("paginated to start.");
|
console.log("paginated to start.");
|
||||||
|
|
||||||
// hopefully, we got to the start of the timeline
|
|
||||||
expect(messagePanel.props.backPaginating).toBe(false);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,16 +284,38 @@ describe('TimelinePanel', function() {
|
||||||
// back-paginate until we hit the start
|
// back-paginate until we hit the start
|
||||||
return backPaginate();
|
return backPaginate();
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
// hopefully, we got to the start of the timeline
|
||||||
|
expect(messagePanel.props.backPaginating).toBe(false);
|
||||||
|
|
||||||
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
|
expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
|
||||||
var events = scryEventTiles(panel);
|
var events = scryEventTiles(panel);
|
||||||
expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0])
|
expect(events[0].props.mxEvent).toBe(timeline.getEvents()[0]);
|
||||||
|
expect(events.length).toEqual(TIMELINE_CAP);
|
||||||
|
|
||||||
// we should now be able to scroll down, and paginate in the other
|
// we should now be able to scroll down, and paginate in the other
|
||||||
// direction.
|
// direction.
|
||||||
setScrollTop(scrollingDiv.scrollHeight);
|
setScrollTop(scrollingDiv.scrollHeight);
|
||||||
scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
|
scrollingDiv.scrollTop = scrollingDiv.scrollHeight;
|
||||||
return awaitScroll();
|
|
||||||
|
// the delay() below is a heinous hack to deal with the fact that,
|
||||||
|
// without it, we may or may not get control back before the
|
||||||
|
// forward pagination completes. The delay means that it should
|
||||||
|
// have completed.
|
||||||
|
return awaitScroll().delay(0);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
expect(messagePanel.props.backPaginating).toBe(false);
|
||||||
|
expect(messagePanel.props.forwardPaginating).toBe(false);
|
||||||
|
expect(messagePanel.props.suppressFirstDateSeparator).toBe(true);
|
||||||
|
|
||||||
|
var events = scryEventTiles(panel);
|
||||||
|
expect(events.length).toEqual(TIMELINE_CAP);
|
||||||
|
|
||||||
|
// we don't really know what the first event tile will be, since that
|
||||||
|
// depends on how much the timelinepanel decides to paginate.
|
||||||
|
//
|
||||||
|
// just check that the first tile isn't event 0.
|
||||||
|
expect(events[0].props.mxEvent).toNotBe(timeline.getEvents()[0]);
|
||||||
|
|
||||||
console.log("done");
|
console.log("done");
|
||||||
}).done(done, done);
|
}).done(done, done);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue