Merge branch 'develop' into wmwragg/direct-chat-sublist
						commit
						769e7d3b2e
					
				| 
						 | 
				
			
			@ -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)
 | 
			
		||||
=========================================================================================================
 | 
			
		||||
[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",
 | 
			
		||||
  "version": "0.6.4-r1",
 | 
			
		||||
  "version": "0.6.5",
 | 
			
		||||
  "description": "SDK for matrix.org using React",
 | 
			
		||||
  "author": "matrix.org",
 | 
			
		||||
  "repository": {
 | 
			
		||||
| 
						 | 
				
			
			@ -37,10 +37,10 @@
 | 
			
		|||
    "fuse.js": "^2.2.0",
 | 
			
		||||
    "glob": "^5.0.14",
 | 
			
		||||
    "highlight.js": "^8.9.1",
 | 
			
		||||
    "linkifyjs": "^2.0.0-beta.4",
 | 
			
		||||
    "linkifyjs": "2.0.0-beta.4",
 | 
			
		||||
    "lodash": "^4.13.1",
 | 
			
		||||
    "marked": "^0.3.5",
 | 
			
		||||
    "matrix-js-sdk": "0.5.5",
 | 
			
		||||
    "matrix-js-sdk": "0.5.6",
 | 
			
		||||
    "optimist": "^0.6.1",
 | 
			
		||||
    "q": "^1.4.1",
 | 
			
		||||
    "react": "^15.2.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,7 +49,7 @@ export function unicodeToImage(str) {
 | 
			
		|||
            alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : 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;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -85,12 +85,28 @@ var sanitizeHtmlParams = {
 | 
			
		|||
    transformTags: { // custom to matrix
 | 
			
		||||
        // add blank targets to all hyperlinks except vector URLs
 | 
			
		||||
        'a': function(tagName, attribs) {
 | 
			
		||||
            var m = attribs.href ? attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN) : null;
 | 
			
		||||
            if (attribs.href) {
 | 
			
		||||
                attribs.target = '_blank'; // by default
 | 
			
		||||
 | 
			
		||||
                var m;
 | 
			
		||||
                // FIXME: horrible duplication with linkify-matrix
 | 
			
		||||
                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;
 | 
			
		||||
                }
 | 
			
		||||
            else {
 | 
			
		||||
                attribs.target = '_blank';
 | 
			
		||||
            }
 | 
			
		||||
            attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
 | 
			
		||||
            return { tagName: tagName, attribs : attribs };
 | 
			
		||||
| 
						 | 
				
			
			@ -271,7 +287,7 @@ module.exports = {
 | 
			
		|||
 | 
			
		||||
    emojifyText: function(text) {
 | 
			
		||||
        return {
 | 
			
		||||
            __html: emojione.unicodeToImage(escape(text)),
 | 
			
		||||
            __html: unicodeToImage(escape(text)),
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 React from 'react';
 | 
			
		||||
 | 
			
		||||
export default class AutocompleteProvider {
 | 
			
		||||
    constructor(commandRegex?: RegExp, fuseOpts?: any) {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,4 +52,9 @@ export default class AutocompleteProvider {
 | 
			
		|||
    getName(): string {
 | 
			
		||||
        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() {
 | 
			
		||||
        return 'Commands';
 | 
			
		||||
        return '*️⃣ Commands';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static getInstance(): CommandProvider {
 | 
			
		||||
| 
						 | 
				
			
			@ -83,4 +83,10 @@ export default class CommandProvider extends AutocompleteProvider {
 | 
			
		|||
 | 
			
		||||
        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 ReactDOM from 'react-dom';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
 | 
			
		||||
export function TextualCompletion({
 | 
			
		||||
/* These were earlier stateless functional components but had to be converted
 | 
			
		||||
since we need to use refs/findDOMNode to access the underlying DOM node to focus the correct completion,
 | 
			
		||||
something that is not entirely possible with stateless functional components. One could
 | 
			
		||||
presumably wrap them in a <div> before rendering but I think this is the better way to do it.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class TextualCompletion extends React.Component {
 | 
			
		||||
    render() {
 | 
			
		||||
        const {
 | 
			
		||||
            title,
 | 
			
		||||
            subtitle,
 | 
			
		||||
            description,
 | 
			
		||||
}: {
 | 
			
		||||
    title: ?string,
 | 
			
		||||
    subtitle: ?string,
 | 
			
		||||
    description: ?string
 | 
			
		||||
}) {
 | 
			
		||||
            className,
 | 
			
		||||
            ...restProps,
 | 
			
		||||
        } = this.props;
 | 
			
		||||
        return (
 | 
			
		||||
        <div style={{width: '100%'}}>
 | 
			
		||||
            <span>{title}</span>
 | 
			
		||||
            <em>{subtitle}</em>
 | 
			
		||||
            <span style={{color: 'gray', float: 'right'}}>{description}</span>
 | 
			
		||||
            <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() {
 | 
			
		||||
        return 'Results from DuckDuckGo';
 | 
			
		||||
        return '🔍 Results from DuckDuckGo';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static getInstance(): DuckDuckGoProvider {
 | 
			
		||||
| 
						 | 
				
			
			@ -87,4 +87,10 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
 | 
			
		|||
        }
 | 
			
		||||
        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 {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione';
 | 
			
		||||
import Fuse from 'fuse.js';
 | 
			
		||||
import sdk from '../index';
 | 
			
		||||
import {PillCompletion} from './Components';
 | 
			
		||||
 | 
			
		||||
const EMOJI_REGEX = /:\w*:?/g;
 | 
			
		||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
 | 
			
		||||
| 
						 | 
				
			
			@ -16,28 +18,28 @@ export default class EmojiProvider extends AutocompleteProvider {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    getCompletions(query: string, selection: {start: number, end: number}) {
 | 
			
		||||
        const EmojiText = sdk.getComponent('views.elements.EmojiText');
 | 
			
		||||
 | 
			
		||||
        let completions = [];
 | 
			
		||||
        let {command, range} = this.getCurrentCommand(query, selection);
 | 
			
		||||
        if (command) {
 | 
			
		||||
            completions = this.fuse.search(command[0]).map(result => {
 | 
			
		||||
                let shortname = EMOJI_SHORTNAMES[result];
 | 
			
		||||
                let imageHTML = shortnameToImage(shortname);
 | 
			
		||||
                const shortname = EMOJI_SHORTNAMES[result];
 | 
			
		||||
                const unicode = shortnameToUnicode(shortname);
 | 
			
		||||
                return {
 | 
			
		||||
                    completion: shortnameToUnicode(shortname),
 | 
			
		||||
                    completion: unicode,
 | 
			
		||||
                    component: (
 | 
			
		||||
                        <div className="mx_Autocomplete_Completion">
 | 
			
		||||
                            <span style={{maxWidth: '1em'}} dangerouslySetInnerHTML={{__html: imageHTML}}></span>  {shortname}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <PillCompletion title={shortname} initialComponent={<EmojiText style={{maxWidth: '1em'}}>{unicode}</EmojiText>} />
 | 
			
		||||
                    ),
 | 
			
		||||
                    range,
 | 
			
		||||
                };
 | 
			
		||||
            }).slice(0, 4);
 | 
			
		||||
            }).slice(0, 8);
 | 
			
		||||
        }
 | 
			
		||||
        return Q.when(completions);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getName() {
 | 
			
		||||
        return 'Emoji';
 | 
			
		||||
        return '😃 Emoji';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static getInstance() {
 | 
			
		||||
| 
						 | 
				
			
			@ -45,4 +47,10 @@ export default class EmojiProvider extends AutocompleteProvider {
 | 
			
		|||
            instance = new EmojiProvider();
 | 
			
		||||
        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 MatrixClientPeg from '../MatrixClientPeg';
 | 
			
		||||
import Fuse from 'fuse.js';
 | 
			
		||||
import {TextualCompletion} from './Components';
 | 
			
		||||
import {PillCompletion} from './Components';
 | 
			
		||||
import {getDisplayAliasForRoom} from '../MatrixTools';
 | 
			
		||||
import sdk from '../index';
 | 
			
		||||
 | 
			
		||||
const ROOM_REGEX = /(?=#)([^\s]*)/g;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,8 @@ export default class RoomProvider extends AutocompleteProvider {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    getCompletions(query: string, selection: {start: number, end: number}) {
 | 
			
		||||
        const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
 | 
			
		||||
 | 
			
		||||
        let client = MatrixClientPeg.get();
 | 
			
		||||
        let completions = [];
 | 
			
		||||
        const {command, range} = this.getCurrentCommand(query, selection);
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +42,7 @@ export default class RoomProvider extends AutocompleteProvider {
 | 
			
		|||
                return {
 | 
			
		||||
                    completion: displayAlias,
 | 
			
		||||
                    component: (
 | 
			
		||||
                        <TextualCompletion title={room.name} description={displayAlias} />
 | 
			
		||||
                        <PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
 | 
			
		||||
                    ),
 | 
			
		||||
                    range,
 | 
			
		||||
                };
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +52,7 @@ export default class RoomProvider extends AutocompleteProvider {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    getName() {
 | 
			
		||||
        return 'Rooms';
 | 
			
		||||
        return '💬 Rooms';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    static getInstance() {
 | 
			
		||||
| 
						 | 
				
			
			@ -59,4 +62,10 @@ export default class RoomProvider extends AutocompleteProvider {
 | 
			
		|||
        
 | 
			
		||||
        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 Q from 'q';
 | 
			
		||||
import Fuse from 'fuse.js';
 | 
			
		||||
import {TextualCompletion} from './Components';
 | 
			
		||||
import {PillCompletion} from './Components';
 | 
			
		||||
import sdk from '../index';
 | 
			
		||||
 | 
			
		||||
const USER_REGEX = /@[^\s]*/g;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +21,8 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    getCompletions(query: string, selection: {start: number, end: number}) {
 | 
			
		||||
        const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
 | 
			
		||||
 | 
			
		||||
        let completions = [];
 | 
			
		||||
        let {command, range} = this.getCurrentCommand(query, selection);
 | 
			
		||||
        if (command) {
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +32,8 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
                return {
 | 
			
		||||
                    completion: user.userId,
 | 
			
		||||
                    component: (
 | 
			
		||||
                        <TextualCompletion
 | 
			
		||||
                        <PillCompletion
 | 
			
		||||
                            initialComponent={<MemberAvatar member={user} width={24} height={24}/>}
 | 
			
		||||
                            title={displayName}
 | 
			
		||||
                            description={user.userId} />
 | 
			
		||||
                    ),
 | 
			
		||||
| 
						 | 
				
			
			@ -41,7 +45,7 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    getName() {
 | 
			
		||||
        return 'Users';
 | 
			
		||||
        return '👥 Users';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setUserList(users) {
 | 
			
		||||
| 
						 | 
				
			
			@ -54,4 +58,10 @@ export default class UserProvider extends AutocompleteProvider {
 | 
			
		|||
        }
 | 
			
		||||
        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",
 | 
			
		||||
        CreateRoom: "create_room",
 | 
			
		||||
        RoomDirectory: "room_directory",
 | 
			
		||||
        UserView: "user_view",
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
            // what ID an alias points at.
 | 
			
		||||
            currentRoomId: null,
 | 
			
		||||
 | 
			
		||||
            // If we're trying to just view a user ID (i.e. /user URL), this is it
 | 
			
		||||
            viewUserId: null,
 | 
			
		||||
 | 
			
		||||
            logged_in: false,
 | 
			
		||||
            collapse_lhs: false,
 | 
			
		||||
            collapse_rhs: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +99,9 @@ module.exports = React.createClass({
 | 
			
		|||
            width: 10000,
 | 
			
		||||
            sideOpacity: 1.0,
 | 
			
		||||
            middleOpacity: 1.0,
 | 
			
		||||
 | 
			
		||||
            version: null,
 | 
			
		||||
            newVersion: null,
 | 
			
		||||
        };
 | 
			
		||||
        return s;
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -736,6 +744,18 @@ module.exports = React.createClass({
 | 
			
		|||
            } else {
 | 
			
		||||
                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 {
 | 
			
		||||
            console.info("Ignoring showScreen for '%s'", screen);
 | 
			
		||||
| 
						 | 
				
			
			@ -756,15 +776,13 @@ module.exports = React.createClass({
 | 
			
		|||
    onUserClick: function(event, userId) {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
        var MemberInfo = sdk.getComponent('rooms.MemberInfo');
 | 
			
		||||
        var member = new Matrix.RoomMember(null, userId);
 | 
			
		||||
        ContextualMenu.createMenu(MemberInfo, {
 | 
			
		||||
            member: member,
 | 
			
		||||
            right: window.innerWidth - event.pageX,
 | 
			
		||||
            top: event.pageY
 | 
			
		||||
        });
 | 
			
		||||
        */
 | 
			
		||||
        // var MemberInfo = sdk.getComponent('rooms.MemberInfo');
 | 
			
		||||
        // var member = new Matrix.RoomMember(null, userId);
 | 
			
		||||
        // ContextualMenu.createMenu(MemberInfo, {
 | 
			
		||||
        //     member: member,
 | 
			
		||||
        //     right: window.innerWidth - event.pageX,
 | 
			
		||||
        //     top: event.pageY
 | 
			
		||||
        // });
 | 
			
		||||
 | 
			
		||||
        var member = new Matrix.RoomMember(null, userId);
 | 
			
		||||
        if (!member) { return; }
 | 
			
		||||
| 
						 | 
				
			
			@ -856,6 +874,7 @@ module.exports = React.createClass({
 | 
			
		|||
    onVersion: function(current, latest) {
 | 
			
		||||
        this.setState({
 | 
			
		||||
            version: current,
 | 
			
		||||
            newVersion: latest,
 | 
			
		||||
            hasNewVersion: current !== latest
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -988,11 +1007,15 @@ module.exports = React.createClass({
 | 
			
		|||
                    page_element = <RoomDirectory />
 | 
			
		||||
                    right_panel = <RightPanel collapsed={this.state.collapse_rhs} opacity={this.state.sideOpacity}/>
 | 
			
		||||
                    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;
 | 
			
		||||
            if (this.state.hasNewVersion) {
 | 
			
		||||
                topBar = <NewVersionBar />;
 | 
			
		||||
                topBar = <NewVersionBar version={this.state.version} newVersion={this.state.newVersion} />;
 | 
			
		||||
            }
 | 
			
		||||
            else if (MatrixClientPeg.get().isGuest()) {
 | 
			
		||||
                topBar = <GuestWarningBar />;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,6 @@ var KeyCode = require('../../KeyCode');
 | 
			
		|||
 | 
			
		||||
var PAGINATE_SIZE = 20;
 | 
			
		||||
var INITIAL_SIZE = 20;
 | 
			
		||||
var TIMELINE_CAP = 250; // the most events to show in a timeline
 | 
			
		||||
 | 
			
		||||
var DEBUG = false;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +81,9 @@ var TimelinePanel = React.createClass({
 | 
			
		|||
 | 
			
		||||
        // opacity for dynamic UI fading effects
 | 
			
		||||
        opacity: React.PropTypes.number,
 | 
			
		||||
 | 
			
		||||
        // maximum number of events to show in a timeline
 | 
			
		||||
        timelineCap: React.PropTypes.number,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    statics: {
 | 
			
		||||
| 
						 | 
				
			
			@ -92,6 +94,12 @@ var TimelinePanel = React.createClass({
 | 
			
		|||
        roomReadMarkerTsMap: {},
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getDefaultProps: function() {
 | 
			
		||||
        return {
 | 
			
		||||
            timelineCap: 250,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    getInitialState: function() {
 | 
			
		||||
        var initialReadMarker =
 | 
			
		||||
            TimelinePanel.roomReadMarkerMap[this.props.room.roomId]
 | 
			
		||||
| 
						 | 
				
			
			@ -684,7 +692,7 @@ var TimelinePanel = React.createClass({
 | 
			
		|||
    _loadTimeline: function(eventId, pixelOffset, offsetBase) {
 | 
			
		||||
        this._timelineWindow = new Matrix.TimelineWindow(
 | 
			
		||||
            MatrixClientPeg.get(), this.props.room,
 | 
			
		||||
            {windowLimit: TIMELINE_CAP});
 | 
			
		||||
            {windowLimit: this.props.timelineCap});
 | 
			
		||||
 | 
			
		||||
        var onLoaded = () => {
 | 
			
		||||
            this._reloadEvents();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,9 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    _getState: function(props) {
 | 
			
		||||
        if (!props.member) {
 | 
			
		||||
            console.error("MemberAvatar called somehow with null member");
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
            name: props.member.name,
 | 
			
		||||
            title: props.member.userId,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,12 +22,6 @@ var sdk = require('../../../index');
 | 
			
		|||
module.exports = React.createClass({
 | 
			
		||||
    displayName: 'MessageEvent',
 | 
			
		||||
 | 
			
		||||
    statics: {
 | 
			
		||||
        needsSenderProfile: function() {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    propTypes: {
 | 
			
		||||
        /* the MatrixEvent to show */
 | 
			
		||||
        mxEvent: React.PropTypes.object.isRequired,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,12 +24,6 @@ import sdk from '../../../index';
 | 
			
		|||
module.exports = React.createClass({
 | 
			
		||||
    displayName: 'TextualEvent',
 | 
			
		||||
 | 
			
		||||
    statics: {
 | 
			
		||||
        needsSenderProfile: function() {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    render: function() {
 | 
			
		||||
        const EmojiText = sdk.getComponent('elements.EmojiText');
 | 
			
		||||
        var text = TextForEvent.textForEvent(this.props.mxEvent);
 | 
			
		||||
| 
						 | 
				
			
			@ -39,4 +33,3 @@ module.exports = React.createClass({
 | 
			
		|||
        );
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,8 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
 | 
			
		||||
import ReactDOM from 'react-dom';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import flatMap from 'lodash/flatMap';
 | 
			
		||||
import sdk from '../../../index';
 | 
			
		||||
 | 
			
		||||
import {getCompletions} from '../../../autocomplete/Autocompleter';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -100,11 +101,27 @@ export default class Autocomplete extends React.Component {
 | 
			
		|||
        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() {
 | 
			
		||||
        const EmojiText = sdk.getComponent('views.elements.EmojiText');
 | 
			
		||||
 | 
			
		||||
        let position = 0;
 | 
			
		||||
        let renderedCompletions = this.state.completions.map((completionResult, 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,
 | 
			
		||||
                });
 | 
			
		||||
                let componentPosition = position;
 | 
			
		||||
| 
						 | 
				
			
			@ -116,40 +133,27 @@ export default class Autocomplete extends React.Component {
 | 
			
		|||
                    this.onConfirm();
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                return (
 | 
			
		||||
                    <div key={i}
 | 
			
		||||
                         className={className}
 | 
			
		||||
                         onMouseOver={onMouseOver}
 | 
			
		||||
                         onClick={onClick}>
 | 
			
		||||
                        {completion.component}
 | 
			
		||||
                    </div>
 | 
			
		||||
                );
 | 
			
		||||
                return React.cloneElement(completion.component, {
 | 
			
		||||
                    key: i,
 | 
			
		||||
                    ref: `completion${i}`,
 | 
			
		||||
                    className,
 | 
			
		||||
                    onMouseOver,
 | 
			
		||||
                    onClick,
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            return completions.length > 0 ? (
 | 
			
		||||
                <div key={i} className="mx_Autocomplete_ProviderSection">
 | 
			
		||||
                    <span className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</span>
 | 
			
		||||
                    <ReactCSSTransitionGroup
 | 
			
		||||
                        component="div"
 | 
			
		||||
                        transitionName="autocomplete"
 | 
			
		||||
                        transitionEnterTimeout={300}
 | 
			
		||||
                        transitionLeaveTimeout={300}>
 | 
			
		||||
                        {completions}
 | 
			
		||||
                    </ReactCSSTransitionGroup>
 | 
			
		||||
                    <EmojiText element="div" className="mx_Autocomplete_provider_name">{completionResult.provider.getName()}</EmojiText>
 | 
			
		||||
                    {completionResult.provider.renderCompletions(completions)}
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="mx_Autocomplete">
 | 
			
		||||
                <ReactCSSTransitionGroup
 | 
			
		||||
                    component="div"
 | 
			
		||||
                    transitionName="autocomplete"
 | 
			
		||||
                    transitionEnterTimeout={300}
 | 
			
		||||
                    transitionLeaveTimeout={300}>
 | 
			
		||||
            <div className="mx_Autocomplete" ref={(e) => this.container = e}>
 | 
			
		||||
                {renderedCompletions}
 | 
			
		||||
                </ReactCSSTransitionGroup>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,7 +62,7 @@ var MAX_READ_AVATARS = 5;
 | 
			
		|||
// '----------------------------------------------------------'
 | 
			
		||||
 | 
			
		||||
module.exports = React.createClass({
 | 
			
		||||
    displayName: 'Event',
 | 
			
		||||
    displayName: 'EventTile',
 | 
			
		||||
 | 
			
		||||
    statics: {
 | 
			
		||||
        haveTileForEvent: function(e) {
 | 
			
		||||
| 
						 | 
				
			
			@ -368,7 +368,7 @@ module.exports = React.createClass({
 | 
			
		|||
        // room, or emote messages
 | 
			
		||||
        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
 | 
			
		||||
        // before trying to instantiate us
 | 
			
		||||
        if (!EventTileType) {
 | 
			
		||||
| 
						 | 
				
			
			@ -395,26 +395,45 @@ module.exports = React.createClass({
 | 
			
		|||
                            <MessageTimestamp ts={this.props.mxEvent.getTs()} />
 | 
			
		||||
                        </a>
 | 
			
		||||
 | 
			
		||||
        var aux = null;
 | 
			
		||||
        var readAvatars = this.getReadAvatars();
 | 
			
		||||
 | 
			
		||||
        var avatar, sender;
 | 
			
		||||
        let avatarSize;
 | 
			
		||||
        let needsSenderProfile;
 | 
			
		||||
 | 
			
		||||
        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">
 | 
			
		||||
                        <MemberAvatar member={this.props.mxEvent.sender}
 | 
			
		||||
                            width={avatarSize} height={avatarSize}
 | 
			
		||||
                            onClick={ this.onMemberAvatarClick }
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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";
 | 
			
		||||
 | 
			
		||||
        var readAvatars = this.getReadAvatars();
 | 
			
		||||
 | 
			
		||||
        var avatar, sender;
 | 
			
		||||
        if (!this.props.continuation && !isInfoMessage) {
 | 
			
		||||
            if (this.props.mxEvent.sender) {
 | 
			
		||||
                avatar = (
 | 
			
		||||
                    <div className="mx_EventTile_avatar">
 | 
			
		||||
                        <MemberAvatar member={this.props.mxEvent.sender} width={30} height={30} onClick={ this.onMemberAvatarClick } />
 | 
			
		||||
                    </div>
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
            if (EventTileType.needsSenderProfile()) {
 | 
			
		||||
            sender = <SenderProfile onClick={ this.onSenderProfileClick } mxEvent={this.props.mxEvent} aux={aux} />;
 | 
			
		||||
        }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var editButton = (
 | 
			
		||||
            <img className="mx_EventTile_editButton" src="img/icon_context_message.svg" width="19" height="19" alt="Options" title="Options" onClick={this.onEditClicked} />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -531,7 +531,7 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
 | 
			
		||||
        var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(avatarUrl);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,6 +23,7 @@ var Modal = require('../../../Modal');
 | 
			
		|||
var ObjectUtils = require("../../../ObjectUtils");
 | 
			
		||||
var dis = require("../../../dispatcher");
 | 
			
		||||
var ScalarAuthClient = require("../../../ScalarAuthClient");
 | 
			
		||||
var ScalarMessaging = require('../../../ScalarMessaging');
 | 
			
		||||
var UserSettingsStore = require('../../../UserSettingsStore');
 | 
			
		||||
 | 
			
		||||
// 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() {
 | 
			
		||||
        ScalarMessaging.startListening();
 | 
			
		||||
        MatrixClientPeg.get().getRoomDirectoryVisibility(
 | 
			
		||||
            this.props.room.roomId
 | 
			
		||||
        ).done((result) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +95,8 @@ module.exports = React.createClass({
 | 
			
		|||
    },
 | 
			
		||||
 | 
			
		||||
    componentWillUnmount: function() {
 | 
			
		||||
        ScalarMessaging.stopListening();
 | 
			
		||||
 | 
			
		||||
        dis.dispatch({
 | 
			
		||||
            action: 'ui_opacity',
 | 
			
		||||
            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() {
 | 
			
		||||
        if (!UserSettingsStore.isFeatureEnabled("e2e_encryption")) {
 | 
			
		||||
            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 user_levels
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -627,6 +671,8 @@ module.exports = React.createClass({
 | 
			
		|||
        return (
 | 
			
		||||
            <div className="mx_RoomSettings">
 | 
			
		||||
 | 
			
		||||
                { leaveButton }
 | 
			
		||||
 | 
			
		||||
                { tagsSection }
 | 
			
		||||
 | 
			
		||||
                <div className="mx_RoomSettings_toggles">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -100,7 +100,7 @@ export default class DevicesPanelEntry extends React.Component {
 | 
			
		|||
            deleteButton = <div className="error">{this.state.deleteError}</div>
 | 
			
		||||
        } else {
 | 
			
		||||
            deleteButton = (
 | 
			
		||||
                <div className="textButton"
 | 
			
		||||
                <div className="mx_textButton"
 | 
			
		||||
                  onClick={this._onDeleteClick}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,7 @@ function matrixLinkify(linkify) {
 | 
			
		|||
    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.onAliasClick = function(e, roomAlias) { e.preventDefault(); };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -102,11 +103,14 @@ var escapeRegExp = function(string) {
 | 
			
		|||
    return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// we only recognise URLs which match our current URL as being the same app
 | 
			
		||||
// as if someone explicitly links to vector.im/develop and we're on vector.im/beta
 | 
			
		||||
// they may well be trying to get us to explicitly go to develop.
 | 
			
		||||
// FIXME: intercept matrix.to URLs as well.
 | 
			
		||||
matrixLinkify.VECTOR_URL_PATTERN = "^(https?:\/\/)?" + escapeRegExp(window.location.host + window.location.pathname);
 | 
			
		||||
// Recognise URLs from both our local vector and official vector as vector.
 | 
			
		||||
// anyone else really should be using matrix.to.
 | 
			
		||||
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 = {
 | 
			
		||||
    events: function (href, type) {
 | 
			
		||||
| 
						 | 
				
			
			@ -131,8 +135,25 @@ matrixLinkify.options = {
 | 
			
		|||
            case 'roomalias':
 | 
			
		||||
                return '#/room/' + href;
 | 
			
		||||
            case 'userid':
 | 
			
		||||
                return '#';
 | 
			
		||||
                return '#/user/' + href;
 | 
			
		||||
            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;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +164,9 @@ matrixLinkify.options = {
 | 
			
		|||
 | 
			
		||||
    target: function(href, type) {
 | 
			
		||||
        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;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,11 +40,12 @@ describe('TimelinePanel', function() {
 | 
			
		|||
    var timeline;
 | 
			
		||||
    var parentDiv;
 | 
			
		||||
 | 
			
		||||
    function mkMessage() {
 | 
			
		||||
    function mkMessage(opts) {
 | 
			
		||||
        return test_utils.mkMessage(
 | 
			
		||||
            {
 | 
			
		||||
                event: true, room: ROOM_ID, user: USER_ID,
 | 
			
		||||
                ts: Date.now(),
 | 
			
		||||
                ... opts,
 | 
			
		||||
            });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -87,7 +88,7 @@ describe('TimelinePanel', function() {
 | 
			
		|||
        // this is https://github.com/vector-im/vector-web/issues/1367
 | 
			
		||||
 | 
			
		||||
        // enough events to allow us to scroll back
 | 
			
		||||
        var N_EVENTS = 20;
 | 
			
		||||
        var N_EVENTS = 30;
 | 
			
		||||
        for (var i = 0; i < N_EVENTS; i++) {
 | 
			
		||||
            timeline.addEvent(mkMessage());
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -207,10 +208,11 @@ describe('TimelinePanel', function() {
 | 
			
		|||
    });
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        this.timeout(N_EVENTS * 30);
 | 
			
		||||
        this.timeout(N_EVENTS * 50);
 | 
			
		||||
 | 
			
		||||
        // client.getRoom is called a /lot/ in this test, so replace
 | 
			
		||||
        // sinon's spy with a fast noop.
 | 
			
		||||
| 
						 | 
				
			
			@ -218,13 +220,15 @@ describe('TimelinePanel', function() {
 | 
			
		|||
 | 
			
		||||
        // fill the timeline with lots of events
 | 
			
		||||
        for (var i = 0; i < N_EVENTS; i++) {
 | 
			
		||||
            timeline.addEvent(mkMessage());
 | 
			
		||||
            timeline.addEvent(mkMessage({msg: "Event "+i}));
 | 
			
		||||
        }
 | 
			
		||||
        console.log("added events to timeline");
 | 
			
		||||
 | 
			
		||||
        var scrollDefer;
 | 
			
		||||
        var panel = ReactDOM.render(
 | 
			
		||||
            <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}} />,
 | 
			
		||||
            <TimelinePanel room={room} onScroll={() => {scrollDefer.resolve()}}
 | 
			
		||||
                timelineCap={TIMELINE_CAP}
 | 
			
		||||
            />,
 | 
			
		||||
            parentDiv
 | 
			
		||||
        );
 | 
			
		||||
        console.log("TimelinePanel rendered");
 | 
			
		||||
| 
						 | 
				
			
			@ -256,14 +260,18 @@ describe('TimelinePanel', function() {
 | 
			
		|||
            console.log("back paginating...");
 | 
			
		||||
            setScrollTop(0);
 | 
			
		||||
            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) {
 | 
			
		||||
                    // need to go further
 | 
			
		||||
                    return backPaginate();
 | 
			
		||||
                }
 | 
			
		||||
                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
 | 
			
		||||
            return backPaginate();
 | 
			
		||||
        }).then(() => {
 | 
			
		||||
            // hopefully, we got to the start of the timeline
 | 
			
		||||
            expect(messagePanel.props.backPaginating).toBe(false);
 | 
			
		||||
 | 
			
		||||
            expect(messagePanel.props.suppressFirstDateSeparator).toBe(false);
 | 
			
		||||
            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
 | 
			
		||||
            // direction.
 | 
			
		||||
            setScrollTop(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(() => {
 | 
			
		||||
            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");
 | 
			
		||||
        }).done(done, done);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue