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 (m) {
|
||||
delete attribs.target;
|
||||
}
|
||||
else {
|
||||
attribs.target = '_blank';
|
||||
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;
|
||||
}
|
||||
}
|
||||
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({
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
}: {
|
||||
title: ?string,
|
||||
subtitle: ?string,
|
||||
description: ?string
|
||||
}) {
|
||||
return (
|
||||
<div style={{width: '100%'}}>
|
||||
<span>{title}</span>
|
||||
<em>{subtitle}</em>
|
||||
<span style={{color: 'gray', float: 'right'}}>{description}</span>
|
||||
</div>
|
||||
);
|
||||
/* 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,
|
||||
className,
|
||||
...restProps,
|
||||
} = 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() {
|
||||
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}>
|
||||
{renderedCompletions}
|
||||
</ReactCSSTransitionGroup>
|
||||
<div className="mx_Autocomplete" ref={(e) => this.container = e}>
|
||||
{renderedCompletions}
|
||||
</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,25 +395,44 @@ module.exports = React.createClass({
|
|||
<MessageTimestamp ts={this.props.mxEvent.getTs()} />
|
||||
</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 avatar, sender;
|
||||
if (!this.props.continuation && !isInfoMessage) {
|
||||
if (this.props.mxEvent.sender) {
|
||||
avatar = (
|
||||
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={30} height={30} onClick={ this.onMemberAvatarClick } />
|
||||
<MemberAvatar member={this.props.mxEvent.sender}
|
||||
width={avatarSize} height={avatarSize}
|
||||
onClick={ this.onMemberAvatarClick }
|
||||
/>
|
||||
</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 = (
|
||||
|
|
|
@ -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