-
- {renderedCompletions}
-
+
this.container = e}>
+ {renderedCompletions}
);
}
diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js
index bc0317addd..b1df3f3267 100644
--- a/src/components/views/rooms/EventTile.js
+++ b/src/components/views/rooms/EventTile.js
@@ -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({
- 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 = (
-
+
- );
- }
- if (EventTileType.needsSenderProfile()) {
- sender =
;
- }
+ );
+ }
+
+ 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 =
;
}
var editButton = (
diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js
index 59e186da06..c02b009c39 100644
--- a/src/components/views/rooms/MemberInfo.js
+++ b/src/components/views/rooms/MemberInfo.js
@@ -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);
diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js
index 1896207c09..145b8559b7 100644
--- a/src/components/views/rooms/RoomSettings.js
+++ b/src/components/views/rooms/RoomSettings.js
@@ -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 = (
+
+ Leave room
+
+ );
+ }
+ else if (myMember.membership === "leave") {
+ leaveButton = (
+
+ Forget room
+
+ );
+ }
+ }
+
// TODO: support editing custom events_levels
// TODO: support editing custom user_levels
@@ -627,6 +671,8 @@ module.exports = React.createClass({
return (
+ { leaveButton }
+
{ tagsSection }
diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js
index cc416ace2f..8e8351de43 100644
--- a/src/components/views/settings/DevicesPanelEntry.js
+++ b/src/components/views/settings/DevicesPanelEntry.js
@@ -100,7 +100,7 @@ export default class DevicesPanelEntry extends React.Component {
deleteButton =
{this.state.deleteError}
} else {
deleteButton = (
-
Delete
diff --git a/src/linkify-matrix.js b/src/linkify-matrix.js
index 99b7ee5c33..99e4898182 100644
--- a/src/linkify-matrix.js
+++ b/src/linkify-matrix.js
@@ -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 {
diff --git a/test/components/structures/TimelinePanel-test.js b/test/components/structures/TimelinePanel-test.js
index 7a603d138f..027d888d2d 100644
--- a/test/components/structures/TimelinePanel-test.js
+++ b/test/components/structures/TimelinePanel-test.js
@@ -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(
-
{scrollDefer.resolve()}} />,
+ {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);
});