Merge branch 'develop'

pull/446/merge
David Baker 2015-10-28 18:27:33 +00:00
commit 74afa710d1
75 changed files with 1899 additions and 278 deletions

View File

@ -22,18 +22,34 @@ into the `vector` directory and run your own server.
Development Development
=========== ===========
You can work on any of the source files within Vector with the setup above,
and your changes will cause an instant rebuild. If you also need to make
changes to the react sdk, you can:
1. Link the react sdk package into the example: For simple tweaks, you can work on any of the source files within Vector with the
setup above, and your changes will cause an instant rebuild.
However, all serious development on Vector happens on the `develop` branch. This typically
depends on the `develop` snapshot versions of `matrix-react-sdk` and `matrix-js-sdk`
too, which isn't expressed in Vector's `package.json`. To do this, check out
the `develop` branches of these libraries and then use `npm link` to tell Vector
about them:
1. `git clone git@github.com:matrix-org/matrix-react-sdk.git`
2. `cd matrix-react-sdk`
3. `git checkout develop`
4. `npm install`
5. `npm start` (to start the dev rebuilder)
6. `cd ../vector-web`
7. Link the react sdk package into the example:
`npm link path/to/your/react/sdk` `npm link path/to/your/react/sdk`
2. Start the development rebuilder in your react SDK directory:
`npm start` Similarly, you may need to `npm link path/to/your/js/sdk` in your `matrix-react-sdk`
directory.
If you add or remove any components from the Vector skin, you will need to rebuild If you add or remove any components from the Vector skin, you will need to rebuild
the skin's index by running, `npm run reskindex`. the skin's index by running, `npm run reskindex`.
You may need to run `npm i source-map-loader` in matrix-js-sdk if you get errors
about "Cannot resolve module 'source-map-loader'" due to shortcomings in webpack.
Deployment Deployment
========== ==========

View File

@ -27,8 +27,8 @@
"filesize": "^3.1.2", "filesize": "^3.1.2",
"flux": "~2.0.3", "flux": "~2.0.3",
"linkifyjs": "^2.0.0-beta.4", "linkifyjs": "^2.0.0-beta.4",
"matrix-js-sdk": "^0.2.1", "matrix-js-sdk": "^0.3.0",
"matrix-react-sdk": "^0.0.1", "matrix-react-sdk": "^0.0.2",
"q": "^1.4.1", "q": "^1.4.1",
"react": "^0.13.3", "react": "^0.13.3",
"react-loader": "^1.4.0" "react-loader": "^1.4.0"

View File

@ -20,14 +20,17 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
module.exports = { module.exports = {
avatarUrlForMember: function(member, width, height, resizeMethod) { avatarUrlForMember: function(member, width, height, resizeMethod) {
var url = MatrixClientPeg.get().getAvatarUrlForMember( var url = member.getAvatarUrl(
member, MatrixClientPeg.get().getHomeserverUrl(),
width, width,
height, height,
resizeMethod resizeMethod
); );
if (!url) { if (!url) {
url = this.defaultAvatarUrlForString(member.userId); // member can be null here currently since on invites, the JS SDK
// does not have enough info to build a RoomMember object for
// the inviter.
url = this.defaultAvatarUrlForString(member ? member.userId : '');
} }
return url; return url;
}, },

View File

@ -49,15 +49,25 @@ module.exports = {
var position = { var position = {
top: props.top - 20, top: props.top - 20,
right: props.right + 8,
}; };
var chevron = null;
if (props.left) {
chevron = <img className="mx_ContextualMenu_chevron_left" src="img/chevron-left.png" width="9" height="16" />
position.left = props.left + 8;
} else {
chevron = <img className="mx_ContextualMenu_chevron_right" src="img/chevron-right.png" width="9" height="16" />
position.right = props.right + 8;
}
var className = 'mx_ContextualMenu_wrapper';
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished // FIXME: If a menu uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the menu from a button click! // property set here so you can't close the menu from a button click!
var menu = ( var menu = (
<div className="mx_ContextualMenu_wrapper"> <div className={className}>
<div className="mx_ContextualMenu" style={position}> <div className="mx_ContextualMenu" style={position}>
<img className="mx_ContextualMenu_chevron" src="img/chevron-right.png" width="9" height="16" /> {chevron}
<Element {...props} onFinished={closeMenu}/> <Element {...props} onFinished={closeMenu}/>
</div> </div>
<div className="mx_ContextualMenu_background" onClick={closeMenu}></div> <div className="mx_ContextualMenu_background" onClick={closeMenu}></div>

View File

@ -74,8 +74,10 @@ module.exports = {
); );
if (call) { if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement()); call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
// N.B. the remote video element is used for playback for audio for voice calls
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement()); call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
// give a separate element for audio stream playback - both for voice calls
// and for the voice stream of screen captures
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
} }
if (call && call.type === "video" && call.state !== 'ended') { if (call && call.type === "video" && call.state !== 'ended') {
// if this call is a conf call, don't display local video as the // if this call is a conf call, don't display local video as the
@ -88,6 +90,7 @@ module.exports = {
else { else {
this.getVideoView().getLocalVideoElement().style.display = "none"; this.getVideoView().getLocalVideoElement().style.display = "none";
this.getVideoView().getRemoteVideoElement().style.display = "none"; this.getVideoView().getRemoteVideoElement().style.display = "none";
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
} }
} }
}; };

View File

@ -33,6 +33,8 @@ module.exports = {
cli.on("Room", this.onRoom); cli.on("Room", this.onRoom);
cli.on("Room.timeline", this.onRoomTimeline); cli.on("Room.timeline", this.onRoomTimeline);
cli.on("Room.name", this.onRoomName); cli.on("Room.name", this.onRoomName);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
var rooms = this.getRoomList(); var rooms = this.getRoomList();
this.setState({ this.setState({
@ -52,6 +54,11 @@ module.exports = {
case 'call_state': case 'call_state':
this._recheckCallElement(this.props.selectedRoom); this._recheckCallElement(this.props.selectedRoom);
break; break;
case 'view_tooltip':
this.tooltip = payload.tooltip;
this._repositionTooltip();
if (this.tooltip) this.tooltip.style.display = 'block';
break
} }
}, },
@ -61,6 +68,7 @@ module.exports = {
MatrixClientPeg.get().removeListener("Room", this.onRoom); MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
} }
}, },
@ -105,6 +113,15 @@ module.exports = {
this.refreshRoomList(); this.refreshRoomList();
}, },
onRoomStateEvents: function(ev, state) {
setTimeout(this.refreshRoomList, 0);
},
onRoomMemberName: function(ev, member) {
setTimeout(this.refreshRoomList, 0);
},
refreshRoomList: function() { refreshRoomList: function() {
var rooms = this.getRoomList(); var rooms = this.getRoomList();
this.setState({ this.setState({
@ -150,6 +167,13 @@ module.exports = {
}); });
}, },
_repositionTooltip: function(e) {
if (this.tooltip && this.tooltip.parentElement) {
var scroll = this.getDOMNode();
this.tooltip.style.top = (scroll.parentElement.offsetTop + this.tooltip.parentElement.offsetTop - scroll.scrollTop) + "px";
}
},
makeRoomTiles: function() { makeRoomTiles: function() {
var self = this; var self = this;
var RoomTile = sdk.getComponent("molecules.RoomTile"); var RoomTile = sdk.getComponent("molecules.RoomTile");
@ -159,6 +183,7 @@ module.exports = {
<RoomTile <RoomTile
room={room} room={room}
key={room.roomId} key={room.roomId}
collapsed={self.props.collapsed}
selected={selected} selected={selected}
unread={self.state.activityMap[room.roomId] === 1} unread={self.state.activityMap[room.roomId] === 1}
highlight={self.state.activityMap[room.roomId] === 2} highlight={self.state.activityMap[room.roomId] === 2}

View File

@ -0,0 +1,503 @@
/*
Copyright 2015 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.
*/
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var React = require("react");
var q = require("q");
var ContentMessages = require("matrix-react-sdk/lib//ContentMessages");
var WhoIsTyping = require("matrix-react-sdk/lib/WhoIsTyping");
var Modal = require("matrix-react-sdk/lib/Modal");
var sdk = require('matrix-react-sdk/lib/index');
var CallHandler = require('matrix-react-sdk/lib/CallHandler');
var VectorConferenceHandler = require('../../modules/VectorConferenceHandler');
var dis = require("matrix-react-sdk/lib/dispatcher");
var PAGINATE_SIZE = 20;
var INITIAL_SIZE = 20;
module.exports = {
getInitialState: function() {
return {
room: this.props.roomId ? MatrixClientPeg.get().getRoom(this.props.roomId) : null,
messageCap: INITIAL_SIZE,
editingRoomSettings: false,
uploadingRoomSettings: false,
numUnreadMessages: 0,
draggingFile: false,
}
},
componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
MatrixClientPeg.get().on("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember);
this.atBottom = true;
},
componentWillUnmount: function() {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
messageWrapper.removeEventListener('drop', this.onDrop);
messageWrapper.removeEventListener('dragover', this.onDragOver);
messageWrapper.removeEventListener('dragleave', this.onDragLeaveOrEnd);
messageWrapper.removeEventListener('dragend', this.onDragLeaveOrEnd);
}
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
MatrixClientPeg.get().removeListener("RoomMember.typing", this.onRoomMemberTyping);
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
}
},
onAction: function(payload) {
switch (payload.action) {
case 'message_send_failed':
case 'message_sent':
case 'message_resend_started':
this.setState({
room: MatrixClientPeg.get().getRoom(this.props.roomId)
});
this.forceUpdate();
break;
case 'notifier_enabled':
this.forceUpdate();
break;
case 'call_state':
if (CallHandler.getCallForRoom(this.props.roomId)) {
// Call state has changed so we may be loading video elements
// which will obscure the message log.
// scroll to bottom
var messageWrapper = this.refs.messageWrapper;
if (messageWrapper) {
messageWrapper = messageWrapper.getDOMNode();
messageWrapper.scrollTop = messageWrapper.scrollHeight;
}
}
// possibly remove the conf call notification if we're now in
// the conf
this._updateConfCallNotification();
break;
}
},
// MatrixRoom still showing the messages from the old room?
// Set the key to the room_id. Sadly you can no longer get at
// the key from inside the component, or we'd check this in code.
/*componentWillReceiveProps: function(props) {
},*/
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (!this.isMounted()) return;
// ignore anything that comes in whilst pagingating: we get one
// event for each new matrix event so this would cause a huge
// number of UI updates. Just update the UI when the paginate
// call returns.
if (this.state.paginating) return;
// no point handling anything while we're waiting for the join to finish:
// we'll only be showing a spinner.
if (this.state.joining) return;
if (room.roomId != this.props.roomId) return;
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
this.atBottom = (
messageWrapper.scrollHeight - messageWrapper.scrollTop <=
(messageWrapper.clientHeight + 150)
);
}
var currentUnread = this.state.numUnreadMessages;
if (!toStartOfTimeline &&
(ev.getSender() !== MatrixClientPeg.get().credentials.userId)) {
// update unread count when scrolled up
if (this.atBottom) {
currentUnread = 0;
}
else {
currentUnread += 1;
}
}
this.setState({
room: MatrixClientPeg.get().getRoom(this.props.roomId),
numUnreadMessages: currentUnread
});
if (toStartOfTimeline && !this.state.paginating) {
this.fillSpace();
}
},
onRoomName: function(room) {
if (room.roomId == this.props.roomId) {
this.setState({
room: room
});
}
},
onRoomMemberTyping: function(ev, member) {
this.forceUpdate();
},
onRoomStateMember: function(ev, state, member) {
if (member.roomId !== this.props.roomId ||
member.userId !== VectorConferenceHandler.getConferenceUserIdForRoom(member.roomId)) {
return;
}
this._updateConfCallNotification();
},
_updateConfCallNotification: function() {
var room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) return;
var confMember = room.getMember(
VectorConferenceHandler.getConferenceUserIdForRoom(this.props.roomId)
);
if (!confMember) {
return;
}
var confCall = VectorConferenceHandler.getConferenceCallForRoom(confMember.roomId);
// A conf call notification should be displayed if there is an ongoing
// conf call but this cilent isn't a part of it.
this.setState({
displayConfCallNotification: (
(!confCall || confCall.call_state === "ended") &&
confMember.membership === "join"
)
});
},
componentDidMount: function() {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
messageWrapper.addEventListener('drop', this.onDrop);
messageWrapper.addEventListener('dragover', this.onDragOver);
messageWrapper.addEventListener('dragleave', this.onDragLeaveOrEnd);
messageWrapper.addEventListener('dragend', this.onDragLeaveOrEnd);
messageWrapper.scrollTop = messageWrapper.scrollHeight;
this.fillSpace();
}
this._updateConfCallNotification();
},
componentDidUpdate: function() {
if (!this.refs.messageWrapper) return;
var messageWrapper = this.refs.messageWrapper.getDOMNode();
if (this.state.paginating && !this.waiting_for_paginate) {
var heightGained = messageWrapper.scrollHeight - this.oldScrollHeight;
messageWrapper.scrollTop += heightGained;
this.oldScrollHeight = undefined;
if (!this.fillSpace()) {
this.setState({paginating: false});
}
} else if (this.atBottom) {
messageWrapper.scrollTop = messageWrapper.scrollHeight;
if (this.state.numUnreadMessages !== 0) {
this.setState({numUnreadMessages: 0});
}
}
},
fillSpace: function() {
if (!this.refs.messageWrapper) return;
var messageWrapper = this.refs.messageWrapper.getDOMNode();
if (messageWrapper.scrollTop < messageWrapper.clientHeight && this.state.room.oldState.paginationToken) {
this.setState({paginating: true});
this.oldScrollHeight = messageWrapper.scrollHeight;
if (this.state.messageCap < this.state.room.timeline.length) {
this.waiting_for_paginate = false;
var cap = Math.min(this.state.messageCap + PAGINATE_SIZE, this.state.room.timeline.length);
this.setState({messageCap: cap, paginating: true});
} else {
this.waiting_for_paginate = true;
var cap = this.state.messageCap + PAGINATE_SIZE;
this.setState({messageCap: cap, paginating: true});
var self = this;
MatrixClientPeg.get().scrollback(this.state.room, PAGINATE_SIZE).finally(function() {
self.waiting_for_paginate = false;
if (self.isMounted()) {
self.setState({
room: MatrixClientPeg.get().getRoom(self.props.roomId)
});
}
// wait and set paginating to false when the component updates
});
}
return true;
}
return false;
},
onJoinButtonClicked: function(ev) {
var self = this;
MatrixClientPeg.get().joinRoom(this.props.roomId).then(function() {
self.setState({
joining: false,
room: MatrixClientPeg.get().getRoom(self.props.roomId)
});
}, function(error) {
self.setState({
joining: false,
joinError: error
});
});
this.setState({
joining: true
});
},
onMessageListScroll: function(ev) {
if (this.refs.messageWrapper) {
var messageWrapper = this.refs.messageWrapper.getDOMNode();
var wasAtBottom = this.atBottom;
this.atBottom = messageWrapper.scrollHeight - messageWrapper.scrollTop <= messageWrapper.clientHeight;
if (this.atBottom && !wasAtBottom) {
this.forceUpdate(); // remove unread msg count
}
}
if (!this.state.paginating) this.fillSpace();
},
onDragOver: function(ev) {
ev.stopPropagation();
ev.preventDefault();
ev.dataTransfer.dropEffect = 'none';
var items = ev.dataTransfer.items;
if (items.length == 1) {
if (items[0].kind == 'file') {
this.setState({ draggingFile : true });
ev.dataTransfer.dropEffect = 'copy';
}
}
},
onDrop: function(ev) {
ev.stopPropagation();
ev.preventDefault();
this.setState({ draggingFile : false });
var files = ev.dataTransfer.files;
if (files.length == 1) {
this.uploadFile(files[0]);
}
},
onDragLeaveOrEnd: function(ev) {
ev.stopPropagation();
ev.preventDefault();
this.setState({ draggingFile : false });
},
uploadFile: function(file) {
this.setState({
upload: {
fileName: file.name,
uploadedBytes: 0,
totalBytes: file.size
}
});
var self = this;
ContentMessages.sendContentToRoom(
file, this.props.roomId, MatrixClientPeg.get()
).progress(function(ev) {
//console.log("Upload: "+ev.loaded+" / "+ev.total);
self.setState({
upload: {
fileName: file.name,
uploadedBytes: ev.loaded,
totalBytes: ev.total
}
});
}).finally(function() {
self.setState({
upload: undefined
});
}).done(undefined, function() {
// display error message
});
},
getWhoIsTypingString: function() {
return WhoIsTyping.whoIsTypingString(this.state.room);
},
getEventTiles: function() {
var DateSeparator = sdk.getComponent('molecules.DateSeparator');
var ret = [];
var count = 0;
var EventTile = sdk.getComponent('molecules.EventTile');
for (var i = this.state.room.timeline.length-1; i >= 0 && count < this.state.messageCap; --i) {
var mxEv = this.state.room.timeline[i];
if (!EventTile.supportsEventType(mxEv.getType())) {
continue;
}
var continuation = false;
var last = false;
var dateSeparator = null;
if (i == this.state.room.timeline.length - 1) {
last = true;
}
if (i > 0 && count < this.state.messageCap - 1) {
if (this.state.room.timeline[i].sender &&
this.state.room.timeline[i - 1].sender &&
(this.state.room.timeline[i].sender.userId ===
this.state.room.timeline[i - 1].sender.userId) &&
(this.state.room.timeline[i].getType() ==
this.state.room.timeline[i - 1].getType())
)
{
continuation = true;
}
var ts0 = this.state.room.timeline[i - 1].getTs();
var ts1 = this.state.room.timeline[i].getTs();
if (new Date(ts0).toDateString() !== new Date(ts1).toDateString()) {
dateSeparator = <DateSeparator key={ts1} ts={ts1}/>;
continuation = false;
}
}
if (i === 1) { // n.b. 1, not 0, as the 0th event is an m.room.create and so doesn't show on the timeline
var ts1 = this.state.room.timeline[i].getTs();
dateSeparator = <li key={ts1}><DateSeparator ts={ts1}/></li>;
continuation = false;
}
ret.unshift(
<li key={mxEv.getId()}><EventTile mxEvent={mxEv} continuation={continuation} last={last}/></li>
);
if (dateSeparator) {
ret.unshift(dateSeparator);
}
++count;
}
return ret;
},
uploadNewState: function(new_name, new_topic, new_join_rule, new_history_visibility, new_power_levels) {
var old_name = this.state.room.name;
var old_topic = this.state.room.currentState.getStateEvents('m.room.topic', '');
if (old_topic) {
old_topic = old_topic.getContent().topic;
} else {
old_topic = "";
}
var old_join_rule = this.state.room.currentState.getStateEvents('m.room.join_rules', '');
if (old_join_rule) {
old_join_rule = old_join_rule.getContent().join_rule;
} else {
old_join_rule = "invite";
}
var old_history_visibility = this.state.room.currentState.getStateEvents('m.room.history_visibility', '');
if (old_history_visibility) {
old_history_visibility = old_history_visibility.getContent().history_visibility;
} else {
old_history_visibility = "shared";
}
var deferreds = [];
if (old_name != new_name && new_name != undefined && new_name) {
deferreds.push(
MatrixClientPeg.get().setRoomName(this.state.room.roomId, new_name)
);
}
if (old_topic != new_topic && new_topic != undefined) {
deferreds.push(
MatrixClientPeg.get().setRoomTopic(this.state.room.roomId, new_topic)
);
}
if (old_join_rule != new_join_rule && new_join_rule != undefined) {
deferreds.push(
MatrixClientPeg.get().sendStateEvent(
this.state.room.roomId, "m.room.join_rules", {
join_rule: new_join_rule,
}, ""
)
);
}
if (old_history_visibility != new_history_visibility && new_history_visibility != undefined) {
deferreds.push(
MatrixClientPeg.get().sendStateEvent(
this.state.room.roomId, "m.room.history_visibility", {
history_visibility: new_history_visibility,
}, ""
)
);
}
if (new_power_levels) {
deferreds.push(
MatrixClientPeg.get().sendStateEvent(
this.state.room.roomId, "m.room.power_levels", new_power_levels, ""
)
);
}
if (deferreds.length) {
var self = this;
q.all(deferreds).fail(function(err) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to set state",
description: err.toString()
});
}).finally(function() {
self.setState({
uploadingRoomSettings: false,
});
});
} else {
this.setState({
editingRoomSettings: false,
uploadingRoomSettings: false,
});
}
}
};

View File

@ -0,0 +1,58 @@
/*
Copyright 2015 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.
*/
'use strict';
var extend = require('matrix-react-sdk/lib/extend');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var BaseRegisterController = require('matrix-react-sdk/lib/controllers/templates/Register.js');
var RegisterController = {};
extend(RegisterController, BaseRegisterController);
RegisterController.onRegistered = function(user_id, access_token) {
MatrixClientPeg.replaceUsingAccessToken(
this.state.hs_url, this.state.is_url, user_id, access_token
);
this.setState({
step: 'profile',
busy: true
});
var self = this;
var cli = MatrixClientPeg.get();
cli.getProfileInfo(cli.credentials.userId).done(function(result) {
self.setState({
avatarUrl: result.avatar_url,
busy: false
});
},
function(err) {
console.err(err);
self.setState({
busy: false
});
});
};
RegisterController.onAccountReady = function() {
if (this.props.onLoggedIn) {
this.props.onLoggedIn();
}
};
module.exports = RegisterController;

View File

@ -14,6 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
html {
/* hack to stop overscroll bounce on OSX and iOS.
N.B. Breaks things when we have legitimate horizontal overscroll */
height: 100%;
overflow: hidden;
}
body { body {
font-family: 'Lato', Helvetica, Arial, Sans-Serif; font-family: 'Lato', Helvetica, Arial, Sans-Serif;
font-size: 16px; font-size: 16px;
@ -34,6 +41,12 @@ h2 {
margin-bottom: 16px; margin-bottom: 16px;
} }
a:hover,
a:link,
a:visited {
color: #80CEF4;
}
.mx_ContextualMenu_background { .mx_ContextualMenu_background {
position: fixed; position: fixed;
top: 0; top: 0;
@ -54,18 +67,29 @@ h2 {
padding: 6px; padding: 6px;
} }
.mx_ContextualMenu_chevron { .mx_ContextualMenu_chevron_right {
padding: 12px; padding: 12px;
position: absolute; position: absolute;
right: -21px; right: -21px;
top: 0px; top: 0px;
} }
.mx_ContextualMenu_chevron_left {
padding: 12px;
position: absolute;
left: -21px;
top: 0px;
}
.mx_ContextualMenu_field { .mx_ContextualMenu_field {
padding: 3px 6px 3px 6px; padding: 3px 6px 3px 6px;
cursor: pointer; cursor: pointer;
} }
.mx_ContextualMenu_spinner {
display: block;
margin: 0 auto;
}
.mx_Dialog_background { .mx_Dialog_background {
position: fixed; position: fixed;

View File

@ -1,7 +1,6 @@
.mx_RoomDropTarget, .mx_RoomDropTarget,
.mx_RoomList_favourites_label, .mx_RoomList_favourites_label,
.mx_RoomList_archive_label, .mx_RoomList_archive_label,
.mx_LeftPanel_hideButton,
.mx_RoomHeader_search, .mx_RoomHeader_search,
.mx_RoomSettings_encrypt, .mx_RoomSettings_encrypt,
.mx_CreateRoom_encrypt, .mx_CreateRoom_encrypt,

View File

@ -14,14 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_MessageTile { .mx_EventTile {
max-width: 100%; max-width: 100%;
clear: both; clear: both;
margin-top: 32px; margin-top: 32px;
margin-left: 56px; margin-left: 56px;
} }
.mx_MessageTile_avatar { .mx_EventTile_avatar {
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
margin-left: -64px; margin-left: -64px;
@ -29,17 +29,17 @@ limitations under the License.
float: left; float: left;
} }
.mx_MessageTile_avatar img { .mx_EventTile_avatar img {
background-color: #dbdbdb; background-color: #dbdbdb;
border-radius: 20px; border-radius: 20px;
border: 0px; border: 0px;
} }
.mx_MessageTile_continuation { .mx_EventTile_continuation {
margin-top: 8px ! important; margin-top: 8px ! important;
} }
.mx_MessageTile .mx_SenderProfile { .mx_EventTile .mx_SenderProfile {
color: #454545; color: #454545;
opacity: 0.5; opacity: 0.5;
font-size: 14px; font-size: 14px;
@ -47,35 +47,35 @@ limitations under the License.
display: block; display: block;
} }
.mx_MessageTile .mx_MessageTimestamp { .mx_EventTile .mx_MessageTimestamp {
color: #454545; color: #454545;
opacity: 0.5; opacity: 0.5;
font-size: 14px; font-size: 14px;
float: right; float: right;
} }
.mx_MessageTile_content { .mx_EventTile_content {
padding-right: 100px; padding-right: 100px;
display: block; display: block;
} }
.mx_MessageTile_notice .mx_MessageTile_content { .mx_EventTile_notice .mx_MessageTile_content {
opacity: 0.5; opacity: 0.5;
} }
.mx_MessageTile_sending { .mx_EventTile_sending {
color: #ddd; color: #ddd;
} }
.mx_MessageTile_notSent { .mx_EventTile_notSent {
color: #f11; color: #f11;
} }
.mx_MessageTile_highlight { .mx_EventTile_highlight {
color: #00f; color: #FF0064;
} }
.mx_MessageTile_msgOption { .mx_EventTile_msgOption {
float: right; float: right;
} }
@ -83,10 +83,30 @@ limitations under the License.
display: none; display: none;
} }
.mx_MessageTile_last .mx_MessageTimestamp { .mx_EventTile_last .mx_MessageTimestamp {
display: block; display: block;
} }
.mx_MessageTile:hover .mx_MessageTimestamp { .mx_EventTile:hover .mx_MessageTimestamp {
display: block; display: block;
} }
.mx_EventTile_editButton {
float: right;
display: none;
border: 0px;
outline: none;
margin-right: 3px;
}
.mx_EventTile:hover .mx_EventTile_editButton {
display: inline-block;
}
.mx_EventTile.menu .mx_EventTile_editButton {
display: inline-block;
}
.mx_EventTile.menu .mx_MessageTimestamp {
display: inline-block;
}

View File

@ -15,7 +15,6 @@ limitations under the License.
*/ */
.mx_MatrixToolbar { .mx_MatrixToolbar {
width: 100%;
text-align: center; text-align: center;
background-color: #ff0064; background-color: #ff0064;
color: #fff; color: #fff;

View File

@ -128,3 +128,7 @@ limitations under the License.
.mx_MemberTile_zalgo { .mx_MemberTile_zalgo {
font-family: Helvetica, Arial, Sans-Serif; font-family: Helvetica, Arial, Sans-Serif;
} }
.mx_MemberTile:hover .mx_MessageTimestamp {
display: block;
}

View File

@ -116,13 +116,16 @@ limitations under the License.
margin-top: -5px; margin-top: -5px;
} }
.mx_RoomHeader_nameInput { .mx_RoomHeader_name input, .mx_RoomHeader_nameInput {
border-radius: 3px; border-radius: 3px;
width: 260px; width: 260px;
border: 1px solid #c7c7c7; border: 1px solid #c7c7c7;
font-weight: 300; font-weight: 300;
font-size: 14px; font-size: 14px;
padding: 9px; padding: 9px;
}
.mx_RoomHeader_nameInput {
margin-top: 6px; margin-top: 6px;
} }
@ -160,3 +163,11 @@ limitations under the License.
.mx_RoomHeader_button img { .mx_RoomHeader_button img {
cursor: pointer; cursor: pointer;
} }
.mx_RoomHeader_voipButton {
display: table-cell;
}
.mx_RoomHeader_voipButtons {
margin-top: 18px;
}

View File

@ -22,13 +22,13 @@ limitations under the License.
.mx_RoomTile_avatar { .mx_RoomTile_avatar {
display: table-cell; display: table-cell;
padding-right: 12px; padding-right: 10px;
padding-top: 3px; padding-top: 3px;
padding-bottom: 3px; padding-bottom: 3px;
padding-left: 16px; padding-left: 10px;
vertical-align: middle; vertical-align: middle;
width: 40px; width: 36px;
height: 40px; height: 36px;
position: relative; position: relative;
} }
@ -45,6 +45,10 @@ limitations under the License.
padding-right: 16px; padding-right: 16px;
} }
.collapsed .mx_RoomTile_name {
display: none;
}
/* /*
.mx_RoomTile_nameBadge { .mx_RoomTile_nameBadge {
display: table; display: table;

View File

@ -14,7 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict'; .mx_RoomTooltip {
display: none;
position: fixed;
border: 1px solid #a9dbf4;
border-radius: 8px;
background-color: #fff;
z-index: 1000;
margin-top: 6px;
left: 64px;
padding: 6px;
}
module.exports = { .mx_RoomTooltip_chevron {
}; position: absolute;
left: -9px;
top: 8px;
}

View File

@ -29,7 +29,9 @@ limitations under the License.
.mx_LeftPanel_hideButton { .mx_LeftPanel_hideButton {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 0px;
padding: 8px;
cursor: pointer;
} }
.mx_LeftPanel .mx_RoomList { .mx_LeftPanel .mx_RoomList {
@ -39,7 +41,7 @@ limitations under the License.
-webkit-order: 1; -webkit-order: 1;
order: 1; order: 1;
overflow-y: scroll; overflow-y: auto;
-webkit-flex: 1 1 0; -webkit-flex: 1 1 0;
flex: 1 1 0; flex: 1 1 0;
} }
@ -61,10 +63,6 @@ limitations under the License.
color: #378bb4; color: #378bb4;
} }
.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile_avatar {
padding-left: 14px;
}
.mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options { .mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options {
margin-top: 12px; margin-top: 12px;
width: 100%; width: 100%;

View File

@ -57,7 +57,7 @@ limitations under the License.
} }
.mx_RoomDirectory_tableWrapper { .mx_RoomDirectory_tableWrapper {
overflow-y: scroll; overflow-y: auto;
-webkit-flex: 1 1 0; -webkit-flex: 1 1 0;
flex: 1 1 0; flex: 1 1 0;
} }

View File

@ -27,4 +27,5 @@ limitations under the License.
.mx_RoomList h2 { .mx_RoomList h2 {
padding-left: 16px; padding-left: 16px;
padding-right: 16px; padding-right: 16px;
padding-bottom: 10px;
} }

View File

@ -106,10 +106,8 @@ limitations under the License.
flex: 1 1 0; flex: 1 1 0;
width: 100%; width: 100%;
margin-top: 18px;
margin-bottom: 18px;
overflow-y: scroll; overflow-y: auto;
} }
.mx_RoomView_messageListWrapper { .mx_RoomView_messageListWrapper {
@ -123,6 +121,10 @@ limitations under the License.
padding: 0px; padding: 0px;
} }
.mx_RoomView_MessageList li {
clear: both;
}
.mx_RoomView_MessageList h2 { .mx_RoomView_MessageList h2 {
clear: both; clear: both;
margin-top: 32px; margin-top: 32px;
@ -210,13 +212,37 @@ limitations under the License.
.mx_RoomView_uploadProgressOuter { .mx_RoomView_uploadProgressOuter {
width: 100%; width: 100%;
background-color: black; background-color: rgba(169, 219, 244, 0.5);
height: 5px; height: 4px;
} }
.mx_RoomView_uploadProgressInner { .mx_RoomView_uploadProgressInner {
background-color: blue; background-color: #80cef4;
height: 5px; height: 4px;
}
.mx_RoomView_uploadFilename {
margin-top: 15px;
margin-left: 56px;
}
.mx_RoomView_uploadIcon {
float: left;
margin-top: 6px;
margin-left: 5px;
}
.mx_RoomView_uploadCancel {
float: right;
margin-top: 6px;
margin-right: 10px;
}
.mx_RoomView_uploadBytes {
float: right;
opacity: 0.5;
margin-top: 15px;
margin-right: 10px;
} }
.mx_RoomView_ongoingConfCallNotification { .mx_RoomView_ongoingConfCallNotification {

View File

@ -0,0 +1,3 @@
.mx_ViewSource pre {
text-align: left;
}

View File

@ -73,6 +73,11 @@ limitations under the License.
flex: 0 0 230px; flex: 0 0 230px;
} }
.mx_MatrixChat .mx_LeftPanel.collapsed {
-webkit-flex: 0 0 60px;
flex: 0 0 60px;
}
.mx_MatrixChat .mx_MatrixChat_middlePanel { .mx_MatrixChat .mx_MatrixChat_middlePanel {
-webkit-box-ordinal-group: 2; -webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2; -moz-box-ordinal-group: 2;
@ -83,7 +88,9 @@ limitations under the License.
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
background-color: #f3f8fa; background-color: #f3f8fa;
width: 100%;
-webkit-flex: 1;
flex: 1;
/* XXX: Hack: apparently if you try to nest a flex-box /* XXX: Hack: apparently if you try to nest a flex-box
* within a non-flex-box within a flex-box, the height * within a non-flex-box within a flex-box, the height
@ -111,3 +118,8 @@ limitations under the License.
-webkit-flex: 0 0 230px; -webkit-flex: 0 0 230px;
flex: 0 0 230px; flex: 0 0 230px;
} }
.mx_MatrixChat .mx_RightPanel.collapsed {
-webkit-flex: 0 0 72px;
flex: 0 0 72px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 999 B

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/skins/vector/img/menu.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 B

After

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -23,6 +23,9 @@ limitations under the License.
var skin = {}; var skin = {};
skin['atoms.create_room.CreateRoomButton'] = require('./views/atoms/create_room/CreateRoomButton');
skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets');
skin['atoms.create_room.RoomAlias'] = require('./views/atoms/create_room/RoomAlias');
skin['atoms.EditableText'] = require('./views/atoms/EditableText'); skin['atoms.EditableText'] = require('./views/atoms/EditableText');
skin['atoms.EnableNotificationsButton'] = require('./views/atoms/EnableNotificationsButton'); skin['atoms.EnableNotificationsButton'] = require('./views/atoms/EnableNotificationsButton');
skin['atoms.ImageView'] = require('./views/atoms/ImageView'); skin['atoms.ImageView'] = require('./views/atoms/ImageView');
@ -30,32 +33,34 @@ skin['atoms.LogoutButton'] = require('./views/atoms/LogoutButton');
skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar'); skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar');
skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp'); skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp');
skin['atoms.RoomAvatar'] = require('./views/atoms/RoomAvatar'); skin['atoms.RoomAvatar'] = require('./views/atoms/RoomAvatar');
skin['atoms.create_room.CreateRoomButton'] = require('./views/atoms/create_room/CreateRoomButton');
skin['atoms.create_room.Presets'] = require('./views/atoms/create_room/Presets');
skin['atoms.create_room.RoomAlias'] = require('./views/atoms/create_room/RoomAlias');
skin['atoms.voip.VideoFeed'] = require('./views/atoms/voip/VideoFeed'); skin['atoms.voip.VideoFeed'] = require('./views/atoms/voip/VideoFeed');
skin['molecules.BottomLeftMenu'] = require('./views/molecules/BottomLeftMenu'); skin['molecules.BottomLeftMenu'] = require('./views/molecules/BottomLeftMenu');
skin['molecules.BottomLeftMenuTile'] = require('./views/molecules/BottomLeftMenuTile');
skin['molecules.ChangeAvatar'] = require('./views/molecules/ChangeAvatar'); skin['molecules.ChangeAvatar'] = require('./views/molecules/ChangeAvatar');
skin['molecules.ChangeDisplayName'] = require('./views/molecules/ChangeDisplayName');
skin['molecules.ChangePassword'] = require('./views/molecules/ChangePassword'); skin['molecules.ChangePassword'] = require('./views/molecules/ChangePassword');
skin['molecules.DateSeparator'] = require('./views/molecules/DateSeparator'); skin['molecules.DateSeparator'] = require('./views/molecules/DateSeparator');
skin['molecules.EventAsTextTile'] = require('./views/molecules/EventAsTextTile'); skin['molecules.EventAsTextTile'] = require('./views/molecules/EventAsTextTile');
skin['molecules.EventTile'] = require('./views/molecules/EventTile');
skin['molecules.MatrixToolbar'] = require('./views/molecules/MatrixToolbar');
skin['molecules.MemberInfo'] = require('./views/molecules/MemberInfo');
skin['molecules.MemberTile'] = require('./views/molecules/MemberTile');
skin['molecules.MEmoteTile'] = require('./views/molecules/MEmoteTile'); skin['molecules.MEmoteTile'] = require('./views/molecules/MEmoteTile');
skin['molecules.MessageComposer'] = require('./views/molecules/MessageComposer');
skin['molecules.MessageContextMenu'] = require('./views/molecules/MessageContextMenu');
skin['molecules.MessageTile'] = require('./views/molecules/MessageTile');
skin['molecules.MFileTile'] = require('./views/molecules/MFileTile'); skin['molecules.MFileTile'] = require('./views/molecules/MFileTile');
skin['molecules.MImageTile'] = require('./views/molecules/MImageTile'); skin['molecules.MImageTile'] = require('./views/molecules/MImageTile');
skin['molecules.MNoticeTile'] = require('./views/molecules/MNoticeTile'); skin['molecules.MNoticeTile'] = require('./views/molecules/MNoticeTile');
skin['molecules.MRoomMemberTile'] = require('./views/molecules/MRoomMemberTile'); skin['molecules.MRoomMemberTile'] = require('./views/molecules/MRoomMemberTile');
skin['molecules.MTextTile'] = require('./views/molecules/MTextTile'); skin['molecules.MTextTile'] = require('./views/molecules/MTextTile');
skin['molecules.MatrixToolbar'] = require('./views/molecules/MatrixToolbar');
skin['molecules.MemberInfo'] = require('./views/molecules/MemberInfo');
skin['molecules.MemberTile'] = require('./views/molecules/MemberTile');
skin['molecules.MessageComposer'] = require('./views/molecules/MessageComposer');
skin['molecules.MessageTile'] = require('./views/molecules/MessageTile');
skin['molecules.ProgressBar'] = require('./views/molecules/ProgressBar'); skin['molecules.ProgressBar'] = require('./views/molecules/ProgressBar');
skin['molecules.RoomCreate'] = require('./views/molecules/RoomCreate'); skin['molecules.RoomCreate'] = require('./views/molecules/RoomCreate');
skin['molecules.RoomDropTarget'] = require('./views/molecules/RoomDropTarget'); skin['molecules.RoomDropTarget'] = require('./views/molecules/RoomDropTarget');
skin['molecules.RoomHeader'] = require('./views/molecules/RoomHeader'); skin['molecules.RoomHeader'] = require('./views/molecules/RoomHeader');
skin['molecules.RoomSettings'] = require('./views/molecules/RoomSettings'); skin['molecules.RoomSettings'] = require('./views/molecules/RoomSettings');
skin['molecules.RoomTile'] = require('./views/molecules/RoomTile'); skin['molecules.RoomTile'] = require('./views/molecules/RoomTile');
skin['molecules.RoomTooltip'] = require('./views/molecules/RoomTooltip');
skin['molecules.SenderProfile'] = require('./views/molecules/SenderProfile'); skin['molecules.SenderProfile'] = require('./views/molecules/SenderProfile');
skin['molecules.ServerConfig'] = require('./views/molecules/ServerConfig'); skin['molecules.ServerConfig'] = require('./views/molecules/ServerConfig');
skin['molecules.UnknownMessageTile'] = require('./views/molecules/UnknownMessageTile'); skin['molecules.UnknownMessageTile'] = require('./views/molecules/UnknownMessageTile');
@ -63,6 +68,7 @@ skin['molecules.UserSelector'] = require('./views/molecules/UserSelector');
skin['molecules.voip.CallView'] = require('./views/molecules/voip/CallView'); skin['molecules.voip.CallView'] = require('./views/molecules/voip/CallView');
skin['molecules.voip.IncomingCallBox'] = require('./views/molecules/voip/IncomingCallBox'); skin['molecules.voip.IncomingCallBox'] = require('./views/molecules/voip/IncomingCallBox');
skin['molecules.voip.VideoView'] = require('./views/molecules/voip/VideoView'); skin['molecules.voip.VideoView'] = require('./views/molecules/voip/VideoView');
skin['organisms.CasLogin'] = require('./views/organisms/CasLogin');
skin['organisms.CreateRoom'] = require('./views/organisms/CreateRoom'); skin['organisms.CreateRoom'] = require('./views/organisms/CreateRoom');
skin['organisms.ErrorDialog'] = require('./views/organisms/ErrorDialog'); skin['organisms.ErrorDialog'] = require('./views/organisms/ErrorDialog');
skin['organisms.LeftPanel'] = require('./views/organisms/LeftPanel'); skin['organisms.LeftPanel'] = require('./views/organisms/LeftPanel');
@ -75,6 +81,7 @@ skin['organisms.RoomDirectory'] = require('./views/organisms/RoomDirectory');
skin['organisms.RoomList'] = require('./views/organisms/RoomList'); skin['organisms.RoomList'] = require('./views/organisms/RoomList');
skin['organisms.RoomView'] = require('./views/organisms/RoomView'); skin['organisms.RoomView'] = require('./views/organisms/RoomView');
skin['organisms.UserSettings'] = require('./views/organisms/UserSettings'); skin['organisms.UserSettings'] = require('./views/organisms/UserSettings');
skin['organisms.ViewSource'] = require('./views/organisms/ViewSource');
skin['pages.MatrixChat'] = require('./views/pages/MatrixChat'); skin['pages.MatrixChat'] = require('./views/pages/MatrixChat');
skin['templates.Login'] = require('./views/templates/Login'); skin['templates.Login'] = require('./views/templates/Login');
skin['templates.Register'] = require('./views/templates/Register'); skin['templates.Register'] = require('./views/templates/Register');

View File

@ -18,11 +18,8 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ImageViewController = require('../../../../controllers/atoms/ImageView')
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ImageView', displayName: 'ImageView',
mixins: [ImageViewController],
// XXX: keyboard shortcuts for managing dialogs should be done by the modal dialog base class omehow, surely... // XXX: keyboard shortcuts for managing dialogs should be done by the modal dialog base class omehow, surely...
componentDidMount: function() { componentDidMount: function() {

View File

@ -18,17 +18,39 @@ limitations under the License.
var React = require('react'); var React = require('react');
var MessageTimestampController = require('matrix-react-sdk/lib/controllers/atoms/MessageTimestamp') var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MessageTimestamp', displayName: 'MessageTimestamp',
mixins: [MessageTimestampController],
formatDate: function(date) {
// date.toLocaleTimeString is completely system dependent.
// just go 24h for now
function pad(n) {
return (n < 10 ? '0' : '') + n;
}
var now = new Date();
if (date.toDateString() === now.toDateString()) {
return pad(date.getHours()) + ':' + pad(date.getMinutes());
}
else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()] + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
else if (now.getFullYear() === date.getFullYear()) {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
else {
return days[date.getDay()] + ", " + months[date.getMonth()] + " " + (date.getDay()+1) + " " + date.getFullYear() + " " + pad(date.getHours()) + ':' + pad(date.getMinutes());
}
},
render: function() { render: function() {
var date = new Date(this.props.ts); var date = new Date(this.props.ts);
return ( return (
<span className="mx_MessageTimestamp"> <span className="mx_MessageTimestamp">
{date.toLocaleTimeString()} { this.formatDate(date) }
</span> </span>
); );
}, },

View File

@ -24,10 +24,31 @@ module.exports = React.createClass({
displayName: 'RoomAvatar', displayName: 'RoomAvatar',
mixins: [RoomAvatarController], mixins: [RoomAvatarController],
getUrlList: function() {
return [
this.roomAvatarUrl(),
this.getOneToOneAvatar(),
this.getFallbackAvatar()
];
},
getFallbackAvatar: function() {
var images = [ '80cef4', '50e2c2', 'f4c371' ];
var total = 0;
for (var i = 0; i < this.props.room.roomId.length; ++i) {
total += this.props.room.roomId.charCodeAt(i);
}
return 'img/' + images[total % images.length] + '.png';
},
render: function() { render: function() {
var style = {
maxWidth: this.props.width,
maxHeight: this.props.height,
};
return ( return (
<img className="mx_RoomAvatar" src={this.state.imageUrl} onError={this.onError} <img className="mx_RoomAvatar" src={this.state.imageUrl} onError={this.onError}
width={this.props.width} height={this.props.height} style={style}
/> />
); );
} }

View File

@ -0,0 +1,34 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
module.exports = React.createClass({
displayName: 'Spinner',
render: function() {
var w = this.props.w || 32;
var h = this.props.h || 32;
var imgClass = this.props.imgClassName || "";
return (
<div>
<img src="img/spinner.gif" width={w} height={h} className={imgClass}/>
</div>
);
}
});

View File

@ -18,11 +18,8 @@ limitations under the License.
var React = require('react'); var React = require('react');
var VideoFeedController = require('matrix-react-sdk/lib/controllers/atoms/voip/VideoFeed')
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'VideoFeed', displayName: 'VideoFeed',
mixins: [VideoFeedController],
render: function() { render: function() {
return ( return (

View File

@ -17,7 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher'); var dis = require('matrix-react-sdk/lib/dispatcher');
module.exports = React.createClass({ module.exports = React.createClass({
@ -35,28 +35,24 @@ module.exports = React.createClass({
dis.dispatch({action: 'view_create_room'}); dis.dispatch({action: 'view_create_room'});
}, },
getLabel: function(name) {
if (!this.props.collapsed) {
return <div className="mx_RoomTile_name">{name}</div>
}
else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("molecules.RoomTooltip");
return <RoomTooltip name={name}/>;
}
},
render: function() { render: function() {
var BottomLeftMenuTile = sdk.getComponent('molecules.BottomLeftMenuTile');
return ( return (
<div className="mx_BottomLeftMenu"> <div className="mx_BottomLeftMenu">
<div className="mx_BottomLeftMenu_options"> <div className="mx_BottomLeftMenu_options">
<div className="mx_RoomTile" onClick={this.onCreateRoomClick}> <BottomLeftMenuTile collapsed={ this.props.collapsed } img="img/create-big.png" label="Create new room" onClick={ this.onCreateRoomClick }/>
<div className="mx_RoomTile_avatar"> <BottomLeftMenuTile collapsed={ this.props.collapsed } img="img/directory-big.png" label="Directory" onClick={ this.onRoomDirectoryClick }/>
<img src="img/create-big.png" alt="Create new room" title="Create new room" width="42" height="42"/> <BottomLeftMenuTile collapsed={ this.props.collapsed } img="img/settings-big.png" label="Settings" onClick={ this.onSettingsClick }/>
</div>
<div className="mx_RoomTile_name">Create new room</div>
</div>
<div className="mx_RoomTile" onClick={this.onRoomDirectoryClick}>
<div className="mx_RoomTile_avatar">
<img src="img/directory-big.png" alt="Directory" title="Directory" width="42" height="42"/>
</div>
<div className="mx_RoomTile_name">Directory</div>
</div>
<div className="mx_RoomTile" onClick={this.onSettingsClick}>
<div className="mx_RoomTile_avatar">
<img src="img/settings-big.png" alt="Settings" title="Settings" width="42" height="42"/>
</div>
<div className="mx_RoomTile_name">Settings</div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,57 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
var sdk = require('matrix-react-sdk')
module.exports = React.createClass({
displayName: 'BottomLeftMenuTile',
getInitialState: function() {
return( { hover : false });
},
onMouseEnter: function() {
this.setState( { hover : true });
},
onMouseLeave: function() {
this.setState( { hover : false });
},
render: function() {
var label;
if (!this.props.collapsed) {
label = <div className="mx_RoomTile_name">{ this.props.label }</div>;
}
else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("molecules.RoomTooltip");
label = <RoomTooltip bottom={ true } label={ this.props.label }/>;
}
return (
<div className="mx_RoomTile" onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick}>
<div className="mx_RoomTile_avatar">
<img src={ this.props.img } width="36" height="36"/>
</div>
{ label }
</div>
);
}
});

View File

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk')
var ChangeAvatarController = require('matrix-react-sdk/lib/controllers/molecules/ChangeAvatar') var ChangeAvatarController = require('matrix-react-sdk/lib/controllers/molecules/ChangeAvatar')
var Loader = require("react-loader"); var Loader = require("react-loader");
@ -28,6 +29,7 @@ module.exports = React.createClass({
mixins: [ChangeAvatarController], mixins: [ChangeAvatarController],
onFileSelected: function(ev) { onFileSelected: function(ev) {
this.avatarSet = true;
this.setAvatarFromFile(ev.target.files[0]); this.setAvatarFromFile(ev.target.files[0]);
}, },
@ -38,22 +40,33 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var RoomAvatar = sdk.getComponent('atoms.RoomAvatar');
var avatarImg;
// Having just set an avatar we just display that since it will take a little
// time to propagate through to the RoomAvatar.
if (this.props.room && !this.avatarSet) {
avatarImg = <RoomAvatar room={this.props.room} width='320' height='240' resizeMethod='scale' />;
} else {
var style = {
maxWidth: 320,
maxHeight: 240,
};
avatarImg = <img src={this.state.avatarUrl} style={style} />;
}
switch (this.state.phase) { switch (this.state.phase) {
case this.Phases.Display: case this.Phases.Display:
case this.Phases.Error: case this.Phases.Error:
return ( return (
<div> <div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<img src={this.state.avatarUrl}/> {avatarImg}
</div> </div>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
Upload new: Upload new:
<input type="file" onChange={this.onFileSelected}/> <input type="file" onChange={this.onFileSelected}/>
{this.state.errorText} {this.state.errorText}
</div> </div>
<div className="mx_Dialog_buttons">
<button onClick={this.props.onFinished}>Cancel</button>
</div>
</div> </div>
); );
case this.Phases.Uploading: case this.Phases.Uploading:

View File

@ -0,0 +1,56 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
var sdk = require('matrix-react-sdk');
var ChangeDisplayNameController = require("matrix-react-sdk/lib/controllers/molecules/ChangeDisplayName");
var Loader = require("react-loader");
module.exports = React.createClass({
displayName: 'ChangeDisplayName',
mixins: [ChangeDisplayNameController],
edit: function() {
this.refs.displayname_edit.edit()
},
onValueChanged: function(new_value, shouldSubmit) {
if (shouldSubmit) {
this.changeDisplayname(new_value);
}
},
render: function() {
if (this.state.busy) {
return (
<Loader />
);
} else if (this.state.errorString) {
return (
<div className="error">{this.state.errorString}</div>
);
} else {
var EditableText = sdk.getComponent('atoms.EditableText');
return (
<EditableText ref="displayname_edit" initialValue={this.state.displayName} label="Click to set display name." onValueChanged={this.onValueChanged}/>
);
}
}
});

View File

@ -18,33 +18,24 @@ limitations under the License.
var React = require('react'); var React = require('react');
var EventAsTextTileController = require('matrix-react-sdk/lib/controllers/molecules/EventAsTextTile')
var sdk = require('matrix-react-sdk')
var TextForEvent = require('matrix-react-sdk/lib/TextForEvent'); var TextForEvent = require('matrix-react-sdk/lib/TextForEvent');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'EventAsTextTile', displayName: 'EventAsTextTile',
mixins: [EventAsTextTileController],
statics: {
needsSenderProfile: function() {
return false;
}
},
render: function() { render: function() {
var MessageTimestamp = sdk.getComponent('atoms.MessageTimestamp');
var MemberAvatar = sdk.getComponent('atoms.MemberAvatar');
var text = TextForEvent.textForEvent(this.props.mxEvent); var text = TextForEvent.textForEvent(this.props.mxEvent);
if (text == null || text.length == 0) return null; if (text == null || text.length == 0) return null;
var timestamp = this.props.last ? <MessageTimestamp ts={this.props.mxEvent.getTs()} /> : null;
var avatar = this.props.mxEvent.sender ? <MemberAvatar member={this.props.mxEvent.sender} /> : null;
return ( return (
<div className="mx_MessageTile mx_MessageTile_notice"> <div className="mx_EventAsTextTile">
<div className="mx_MessageTile_avatar"> {TextForEvent.textForEvent(this.props.mxEvent)}
{ avatar }
</div>
{ timestamp }
<span className="mx_SenderProfile"></span>
<span className="mx_MessageTile_content">
{TextForEvent.textForEvent(this.props.mxEvent)}
</span>
</div> </div>
); );
}, },

View File

@ -0,0 +1,131 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
var classNames = require("classnames");
var sdk = require('matrix-react-sdk')
var EventTileController = require('matrix-react-sdk/lib/controllers/molecules/EventTile')
var ContextualMenu = require('../../../../ContextualMenu');
var eventTileTypes = {
'm.room.message': 'molecules.MessageTile',
'm.room.member' : 'molecules.EventAsTextTile',
'm.call.invite' : 'molecules.EventAsTextTile',
'm.call.answer' : 'molecules.EventAsTextTile',
'm.call.hangup' : 'molecules.EventAsTextTile',
'm.room.topic' : 'molecules.EventAsTextTile',
};
module.exports = React.createClass({
displayName: 'EventTile',
mixins: [EventTileController],
statics: {
supportsEventType: function(et) {
return eventTileTypes[et] !== undefined;
}
},
getInitialState: function() {
return {menu: false};
},
onEditClicked: function(e) {
var MessageContextMenu = sdk.getComponent('molecules.MessageContextMenu');
var buttonRect = e.target.getBoundingClientRect()
var x = buttonRect.right;
var y = buttonRect.top + (e.target.height / 2);
var self = this;
ContextualMenu.createMenu(MessageContextMenu, {
mxEvent: this.props.mxEvent,
left: x,
top: y,
onFinished: function() {
self.setState({menu: false});
}
});
this.setState({menu: true});
},
render: function() {
var MessageTimestamp = sdk.getComponent('atoms.MessageTimestamp');
var SenderProfile = sdk.getComponent('molecules.SenderProfile');
var MemberAvatar = sdk.getComponent('atoms.MemberAvatar');
var content = this.props.mxEvent.getContent();
var msgtype = content.msgtype;
var EventTileType = sdk.getComponent(eventTileTypes[this.props.mxEvent.getType()]);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!EventTileType) {
return null;
}
var classes = classNames({
mx_EventTile: true,
mx_EventTile_sending: ['sending', 'queued'].indexOf(
this.props.mxEvent.status
) !== -1,
mx_EventTile_notSent: this.props.mxEvent.status == 'not_sent',
mx_EventTile_highlight: this.shouldHighlight(),
mx_EventTile_continuation: this.props.continuation,
mx_EventTile_last: this.props.last,
menu: this.state.menu
});
var timestamp = <MessageTimestamp ts={this.props.mxEvent.getTs()} />
var editButton = (
<input
type="image" src="img/edit.png" alt="Edit"
className="mx_EventTile_editButton" onClick={this.onEditClicked}
/>
);
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 avatar, sender;
if (!this.props.continuation) {
if (this.props.mxEvent.sender) {
avatar = (
<div className="mx_EventTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
);
}
if (EventTileType.needsSenderProfile()) {
sender = <SenderProfile mxEvent={this.props.mxEvent} aux={aux} />;
}
}
return (
<div className={classes}>
{ avatar }
{ sender }
<div>
{ timestamp }
{ editButton }
<EventTileType mxEvent={this.props.mxEvent} />
</div>
</div>
);
},
});

View File

@ -19,15 +19,12 @@ limitations under the License.
var React = require('react'); var React = require('react');
var filesize = require('filesize'); var filesize = require('filesize');
var MImageTileController = require('matrix-react-sdk/lib/controllers/molecules/MImageTile')
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var Modal = require('matrix-react-sdk/lib/Modal'); var Modal = require('matrix-react-sdk/lib/Modal');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MImageTile', displayName: 'MImageTile',
mixins: [MImageTileController],
thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) { thumbHeight: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
if (!fullWidth || !fullHeight) { if (!fullWidth || !fullHeight) {

View File

@ -18,14 +18,11 @@ limitations under the License.
var React = require('react'); var React = require('react');
var MRoomMemberTileController = require('matrix-react-sdk/lib/controllers/molecules/MRoomMemberTile')
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var TextForEvent = require('matrix-react-sdk/lib/TextForEvent'); var TextForEvent = require('matrix-react-sdk/lib/TextForEvent');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MRoomMemberTile', displayName: 'MRoomMemberTile',
mixins: [MRoomMemberTileController],
getMemberEventText: function() { getMemberEventText: function() {
return TextForEvent.textForEvent(this.props.mxEvent); return TextForEvent.textForEvent(this.props.mxEvent);

View File

@ -20,11 +20,8 @@ var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var MatrixToolbarController = require('matrix-react-sdk/lib/controllers/molecules/MatrixToolbar')
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MatrixToolbar', displayName: 'MatrixToolbar',
mixins: [MatrixToolbarController],
hideToolbar: function() { hideToolbar: function() {
var Notifier = sdk.getComponent('organisms.Notifier'); var Notifier = sdk.getComponent('organisms.Notifier');

View File

@ -17,6 +17,7 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var Loader = require("../atoms/Spinner");
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var MemberInfoController = require('matrix-react-sdk/lib/controllers/molecules/MemberInfo') var MemberInfoController = require('matrix-react-sdk/lib/controllers/molecules/MemberInfo')
@ -26,7 +27,7 @@ module.exports = React.createClass({
mixins: [MemberInfoController], mixins: [MemberInfoController],
render: function() { render: function() {
var interactButton, kickButton, banButton, muteButton, giveModButton; var interactButton, kickButton, banButton, muteButton, giveModButton, spinner;
if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) { if (this.props.member.userId === MatrixClientPeg.get().credentials.userId) {
interactButton = <div className="mx_ContextualMenu_field" onClick={this.onLeaveClick}>Leave room</div>; interactButton = <div className="mx_ContextualMenu_field" onClick={this.onLeaveClick}>Leave room</div>;
} }
@ -34,6 +35,10 @@ module.exports = React.createClass({
interactButton = <div className="mx_ContextualMenu_field" onClick={this.onChatClick}>Start chat</div>; interactButton = <div className="mx_ContextualMenu_field" onClick={this.onChatClick}>Start chat</div>;
} }
if (this.state.creatingRoom) {
spinner = <Loader imgClassName="mx_ContextualMenu_spinner"/>;
}
if (this.state.can.kick) { if (this.state.can.kick) {
kickButton = <div className="mx_ContextualMenu_field" onClick={this.onKick}> kickButton = <div className="mx_ContextualMenu_field" onClick={this.onKick}>
Kick Kick
@ -64,6 +69,7 @@ module.exports = React.createClass({
{kickButton} {kickButton}
{banButton} {banButton}
{giveModButton} {giveModButton}
{spinner}
</div> </div>
); );
} }

View File

@ -31,6 +31,24 @@ module.exports = React.createClass({
displayName: 'MemberTile', displayName: 'MemberTile',
mixins: [MemberTileController], mixins: [MemberTileController],
shouldComponentUpdate: function(nextProps, nextState) {
if (this.state.hover !== nextState.hover) return true;
if (
this.member_last_modified_time === undefined ||
this.member_last_modified_time < nextProps.member.getLastModifiedTime()
) {
return true
}
if (
nextProps.member.user &&
(this.user_last_modified_time === undefined ||
this.user_last_modified_time < nextProps.member.user.getLastModifiedTime())
) {
return true
}
return false;
},
mouseEnter: function(e) { mouseEnter: function(e) {
this.setState({ 'hover': true }); this.setState({ 'hover': true });
}, },
@ -93,6 +111,11 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
this.member_last_modified_time = this.props.member.getLastModifiedTime();
if (this.props.member.user) {
this.user_last_modified_time = this.props.member.user.getLastModifiedTime();
}
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId; var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
var power; var power;

View File

@ -0,0 +1,105 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var dis = require('matrix-react-sdk/lib/dispatcher');
var sdk = require('matrix-react-sdk')
var Modal = require('matrix-react-sdk/lib/Modal');
module.exports = React.createClass({
displayName: 'MessageContextMenu',
onResendClick: function() {
MatrixClientPeg.get().resendEvent(
this.props.mxEvent, MatrixClientPeg.get().getRoom(
this.props.mxEvent.getRoomId()
)
).done(function() {
dis.dispatch({
action: 'message_sent'
});
}, function() {
dis.dispatch({
action: 'message_send_failed'
});
});
dis.dispatch({action: 'message_resend_started'});
if (this.props.onFinished) this.props.onFinished();
},
onViewSourceClick: function() {
var ViewSource = sdk.getComponent('organisms.ViewSource');
Modal.createDialog(ViewSource, {
mxEvent: this.props.mxEvent
});
if (this.props.onFinished) this.props.onFinished();
},
onRedactClick: function() {
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
).done(function() {
// message should disappear by itself
}, function(e) {
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
// display error message stating you couldn't delete this.
var code = e.errcode || e.statusCode;
Modal.createDialog(ErrorDialog, {
title: "Error",
description: "You cannot delete this message. (" + code + ")"
});
});
if (this.props.onFinished) this.props.onFinished();
},
render: function() {
var resendButton;
var viewSourceButton;
var redactButton;
if (this.props.mxEvent.status == 'not_sent') {
resendButton = (
<div className="mx_ContextualMenu_field" onClick={this.onResendClick}>
Resend
</div>
);
}
else {
redactButton = (
<div className="mx_ContextualMenu_field" onClick={this.onRedactClick}>
Delete
</div>
);
}
viewSourceButton = (
<div className="mx_ContextualMenu_field" onClick={this.onViewSourceClick}>
View Source
</div>
);
return (
<div>
{resendButton}
{redactButton}
{viewSourceButton}
</div>
);
}
});

View File

@ -18,8 +18,6 @@ limitations under the License.
var React = require('react'); var React = require('react');
var classNames = require("classnames");
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var MessageTileController = require('matrix-react-sdk/lib/controllers/molecules/MessageTile') var MessageTileController = require('matrix-react-sdk/lib/controllers/molecules/MessageTile')
@ -28,11 +26,13 @@ module.exports = React.createClass({
displayName: 'MessageTile', displayName: 'MessageTile',
mixins: [MessageTileController], mixins: [MessageTileController],
render: function() { statics: {
var MessageTimestamp = sdk.getComponent('atoms.MessageTimestamp'); needsSenderProfile: function() {
var SenderProfile = sdk.getComponent('molecules.SenderProfile'); return true;
var MemberAvatar = sdk.getComponent('atoms.MemberAvatar'); }
},
render: function() {
var UnknownMessageTile = sdk.getComponent('molecules.UnknownMessageTile'); var UnknownMessageTile = sdk.getComponent('molecules.UnknownMessageTile');
var tileTypes = { var tileTypes = {
@ -49,47 +49,7 @@ module.exports = React.createClass({
if (msgtype && tileTypes[msgtype]) { if (msgtype && tileTypes[msgtype]) {
TileType = tileTypes[msgtype]; TileType = tileTypes[msgtype];
} }
var classes = classNames({
mx_MessageTile: true,
mx_MessageTile_sending: ['sending', 'queued'].indexOf(
this.props.mxEvent.status
) !== -1,
mx_MessageTile_notSent: this.props.mxEvent.status == 'not_sent',
mx_MessageTile_highlight: this.shouldHighlight(),
mx_MessageTile_continuation: this.props.continuation,
mx_MessageTile_last: this.props.last,
});
var timestamp = <MessageTimestamp ts={this.props.mxEvent.getTs()} />
var aux = null; return <TileType mxEvent={this.props.mxEvent} />;
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 avatar, sender, resend;
if (!this.props.continuation) {
if (this.props.mxEvent.sender) {
avatar = (
<div className="mx_MessageTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender} />
</div>
);
}
sender = <SenderProfile mxEvent={this.props.mxEvent} aux={aux} />;
}
if (this.props.mxEvent.status === "not_sent" && !this.state.resending) {
resend = <button className="mx_MessageTile_msgOption" onClick={this.onResend}>
Resend
</button>;
}
return (
<div className={classes}>
{ avatar }
{ timestamp }
{ resend }
{ sender }
<TileType mxEvent={this.props.mxEvent} />
</div>
);
}, },
}); });

View File

@ -18,7 +18,9 @@ limitations under the License.
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher')
var CallHandler = require('matrix-react-sdk/lib/CallHandler');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var RoomHeaderController = require('matrix-react-sdk/lib/controllers/molecules/RoomHeader') var RoomHeaderController = require('matrix-react-sdk/lib/controllers/molecules/RoomHeader')
@ -36,6 +38,10 @@ module.exports = React.createClass({
return this.refs.name_edit.getDOMNode().value; return this.refs.name_edit.getDOMNode().value;
}, },
onFullscreenClick: function() {
dis.dispatch({action: 'video_fullscreen', fullscreen: true}, true);
},
render: function() { render: function() {
var EditableText = sdk.getComponent("atoms.EditableText"); var EditableText = sdk.getComponent("atoms.EditableText");
var RoomAvatar = sdk.getComponent('atoms.RoomAvatar'); var RoomAvatar = sdk.getComponent('atoms.RoomAvatar');
@ -52,18 +58,41 @@ module.exports = React.createClass({
else { else {
var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
var callButtons; var call_buttons;
if (this.state) { var zoom_button;
switch (this.state.call_state) { if (this.state && this.state.call_state != 'ended') {
case "ringback": //var muteVideoButton;
case "connected": var activeCall = (
callButtons = ( CallHandler.getCallForRoom(this.props.room.roomId)
<div className="mx_RoomHeader_textButton" onClick={this.onHangupClick}> );
End call /*
</div> if (activeCall && activeCall.type === "video") {
); muteVideoButton = (
break; <div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
onClick={this.onMuteVideoClick}>
{
(activeCall.isLocalVideoMuted() ?
"Unmute" : "Mute") + " video"
}
</div>
);
} }
{muteVideoButton}
<div className="mx_RoomHeader_textButton mx_RoomHeader_voipButton"
onClick={this.onMuteAudioClick}>
{
(activeCall && activeCall.isMicrophoneMuted() ?
"Unmute" : "Mute") + " audio"
}
</div>
*/
call_buttons = (
<div className="mx_RoomHeader_textButton"
onClick={this.onHangupClick}>
End call
</div>
);
} }
var name = null; var name = null;
@ -97,7 +126,15 @@ module.exports = React.createClass({
var roomAvatar = null; var roomAvatar = null;
if (this.props.room) { if (this.props.room) {
roomAvatar = ( roomAvatar = (
<RoomAvatar room={this.props.room} /> <RoomAvatar room={this.props.room} width="48" height="48" />
);
}
if (activeCall && activeCall.type == "video") {
zoom_button = (
<div className="mx_RoomHeader_button" onClick={this.onFullscreenClick}>
<img src="img/zoom.png" title="Fullscreen" alt="Fullscreen" width="32" height="32" style={{ 'marginTop': '3px' }}/>
</div>
); );
} }
@ -112,18 +149,19 @@ module.exports = React.createClass({
{ topic_el } { topic_el }
</div> </div>
</div> </div>
{callButtons} {call_buttons}
{cancel_button} {cancel_button}
{save_button} {save_button}
<div className="mx_RoomHeader_rightRow"> <div className="mx_RoomHeader_rightRow">
{ settings_button } { settings_button }
{ zoom_button }
<div className="mx_RoomHeader_button mx_RoomHeader_search"> <div className="mx_RoomHeader_button mx_RoomHeader_search">
<img src="img/search.png" title="Search" alt="Search" width="32" height="32"/> <img src="img/search.png" title="Search" alt="Search" width="32" height="32"/>
</div> </div>
<div className="mx_RoomHeader_button mx_RoomHeader_video" onClick={this.onVideoClick}> <div className="mx_RoomHeader_button mx_RoomHeader_video" onClick={activeCall && activeCall.type === "video" ? this.onMuteVideoClick : this.onVideoClick}>
<img src="img/video.png" title="Video call" alt="Video call" width="32" height="32"/> <img src="img/video.png" title="Video call" alt="Video call" width="32" height="32"/>
</div> </div>
<div className="mx_RoomHeader_button mx_RoomHeader_voice" onClick={this.onVoiceClick}> <div className="mx_RoomHeader_button mx_RoomHeader_voice" onClick={activeCall ? this.onMuteAudioClick : this.onVoiceClick}>
<img src="img/voip.png" title="VoIP call" alt="VoIP call" width="32" height="32"/> <img src="img/voip.png" title="VoIP call" alt="VoIP call" width="32" height="32"/>
</div> </div>
</div> </div>

View File

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var sdk = require('matrix-react-sdk');
var RoomSettingsController = require('matrix-react-sdk/lib/controllers/molecules/RoomSettings') var RoomSettingsController = require('matrix-react-sdk/lib/controllers/molecules/RoomSettings')
@ -65,6 +66,8 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar');
var topic = this.props.room.currentState.getStateEvents('m.room.topic', ''); var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
if (topic) topic = topic.getContent().topic; if (topic) topic = topic.getContent().topic;
@ -76,6 +79,8 @@ module.exports = React.createClass({
var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', ''); var power_levels = this.props.room.currentState.getStateEvents('m.room.power_levels', '');
var events_levels = power_levels.events || {};
if (power_levels) { if (power_levels) {
power_levels = power_levels.getContent(); power_levels = power_levels.getContent();
@ -91,8 +96,7 @@ module.exports = React.createClass({
if (power_levels.kick == undefined) kick_level = 50; if (power_levels.kick == undefined) kick_level = 50;
if (power_levels.redact == undefined) redact_level = 50; if (power_levels.redact == undefined) redact_level = 50;
var user_levels = power_levels.users || []; var user_levels = power_levels.users || {};
var events_levels = power_levels.events || [];
var user_id = MatrixClientPeg.get().credentials.userId; var user_id = MatrixClientPeg.get().credentials.userId;
@ -124,7 +128,21 @@ module.exports = React.createClass({
var can_change_levels = false; var can_change_levels = false;
} }
var banned = this.props.room.getMembersWithMemership("ban"); var room_avatar_level = parseInt(power_levels.state_default || 0);
if (events_levels['m.room.avatar'] !== undefined) {
room_avatar_level = events_levels['m.room.avatar'];
}
var can_set_room_avatar = current_user_level >= room_avatar_level;
var change_avatar;
if (can_set_room_avatar) {
change_avatar = <div>
<h3>Room Icon</h3>
<ChangeAvatar room={this.props.room} />
</div>;
}
var banned = this.props.room.getMembersWithMembership("ban");
return ( return (
<div className="mx_RoomSettings"> <div className="mx_RoomSettings">
@ -207,6 +225,7 @@ module.exports = React.createClass({
); );
})} })}
</div> </div>
{change_avatar}
</div> </div>
); );
} }

View File

@ -28,6 +28,19 @@ var sdk = require('matrix-react-sdk')
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RoomTile', displayName: 'RoomTile',
mixins: [RoomTileController], mixins: [RoomTileController],
getInitialState: function() {
return( { hover : false });
},
onMouseEnter: function() {
this.setState( { hover : true });
},
onMouseLeave: function() {
this.setState( { hover : false });
},
render: function() { render: function() {
var myUserId = MatrixClientPeg.get().credentials.userId; var myUserId = MatrixClientPeg.get().credentials.userId;
var classes = classNames({ var classes = classNames({
@ -57,14 +70,24 @@ module.exports = React.createClass({
nameCell = <div className="mx_RoomTile_name">{name}</div>; nameCell = <div className="mx_RoomTile_name">{name}</div>;
} }
*/ */
var label;
if (!this.props.collapsed) {
label = <div className="mx_RoomTile_name">{name}</div>;
}
else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("molecules.RoomTooltip");
label = <RoomTooltip room={this.props.room}/>;
}
var RoomAvatar = sdk.getComponent('atoms.RoomAvatar'); var RoomAvatar = sdk.getComponent('atoms.RoomAvatar');
return ( return (
<div className={classes} onClick={this.onClick}> <div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar"> <div className="mx_RoomTile_avatar">
<RoomAvatar room={this.props.room} /> <RoomAvatar room={this.props.room} />
{ badge } { badge }
</div> </div>
<div className="mx_RoomTile_name">{name}</div> { label }
</div> </div>
); );
} }

View File

@ -0,0 +1,59 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
var dis = require('matrix-react-sdk/lib/dispatcher');
module.exports = React.createClass({
displayName: 'RoomTooltip',
componentDidMount: function() {
if (!this.props.bottom) {
// tell the roomlist about us so it can position us
dis.dispatch({
action: 'view_tooltip',
tooltip: this.getDOMNode(),
});
}
else {
var tooltip = this.getDOMNode();
tooltip.style.top = tooltip.parentElement.getBoundingClientRect().top + "px";
tooltip.style.display = "block";
}
},
componentDidUnmount: function() {
if (!this.props.bottom) {
dis.dispatch({
action: 'view_tooltip',
tooltip: null,
});
}
},
render: function() {
var label = this.props.room ? this.props.room.name : this.props.label;
return (
<div className="mx_RoomTooltip">
<img className="mx_RoomTooltip_chevron" src="img/chevron-left.png" width="9" height="16"/>
{ label }
</div>
);
}
});

View File

@ -19,15 +19,12 @@ limitations under the License.
var React = require('react'); var React = require('react');
var classNames = require("classnames"); var classNames = require("classnames");
var SenderProfileController = require('matrix-react-sdk/lib/controllers/molecules/SenderProfile')
// The Lato WOFF doesn't include sensible combining diacritics, so Chrome chokes on rendering them. // The Lato WOFF doesn't include sensible combining diacritics, so Chrome chokes on rendering them.
// Revert to Arial when this happens, which on OSX works at least. // Revert to Arial when this happens, which on OSX works at least.
var zalgo = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/; var zalgo = /[\u0300-\u036f\u1ab0-\u1aff\u1dc0-\u1dff\u20d0-\u20ff\ufe20-\ufe2f]/;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'SenderProfile', displayName: 'SenderProfile',
mixins: [SenderProfileController],
render: function() { render: function() {
var mxEvent = this.props.mxEvent; var mxEvent = this.props.mxEvent;

View File

@ -18,11 +18,8 @@ limitations under the License.
var React = require('react'); var React = require('react');
var UnknownMessageTileController = require('matrix-react-sdk/lib/controllers/molecules/UnknownMessageTile')
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'UnknownMessageTile', displayName: 'UnknownMessageTile',
mixins: [UnknownMessageTileController],
render: function() { render: function() {
var content = this.props.mxEvent.getContent(); var content = this.props.mxEvent.getContent();

View File

@ -31,24 +31,28 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
// NB: This block MUST have a "key" so React doesn't clobber the elements
// between in-call / not-in-call.
var audioBlock = (
<audio ref="ringAudio" key="voip_ring_audio" loop>
<source src="media/ring.ogg" type="audio/ogg" />
<source src="media/ring.mp3" type="audio/mpeg" />
</audio>
);
if (!this.state.incomingCall || !this.state.incomingCall.roomId) { if (!this.state.incomingCall || !this.state.incomingCall.roomId) {
return ( return (
<div> <div>
<audio ref="ringAudio" loop> {audioBlock}
<source src="media/ring.ogg" type="audio/ogg" />
<source src="media/ring.mp3" type="audio/mpeg" />
</audio>
</div> </div>
); );
} }
var caller = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId).name; var caller = MatrixClientPeg.get().getRoom(this.state.incomingCall.roomId).name;
return ( return (
<div className="mx_IncomingCallBox"> <div className="mx_IncomingCallBox">
{audioBlock}
<img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" /> <img className="mx_IncomingCallBox_chevron" src="img/chevron-left.png" width="9" height="16" />
<audio ref="ringAudio" loop>
<source src="media/ring.ogg" type="audio/ogg" />
<source src="media/ring.mp3" type="audio/mpeg" />
</audio>
<div className="mx_IncomingCallBox_title"> <div className="mx_IncomingCallBox_title">
Incoming { this.state.incomingCall ? this.state.incomingCall.type : '' } call from { caller } Incoming { this.state.incomingCall ? this.state.incomingCall.type : '' } call from { caller }
</div> </div>

View File

@ -19,26 +19,69 @@ limitations under the License.
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var VideoViewController = require('matrix-react-sdk/lib/controllers/molecules/voip/VideoView') var dis = require('matrix-react-sdk/lib/dispatcher')
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'VideoView', displayName: 'VideoView',
mixins: [VideoViewController],
componentWillMount: function() {
dis.register(this.onAction);
},
getRemoteVideoElement: function() { getRemoteVideoElement: function() {
return this.refs.remote.getDOMNode(); return this.refs.remote.getDOMNode();
}, },
getRemoteAudioElement: function() {
return this.refs.remoteAudio.getDOMNode();
},
getLocalVideoElement: function() { getLocalVideoElement: function() {
return this.refs.local.getDOMNode(); return this.refs.local.getDOMNode();
}, },
setContainer: function(c) {
this.container = c;
},
onAction: function(payload) {
switch (payload.action) {
case 'video_fullscreen':
if (!this.container) {
return;
}
var element = this.container.getDOMNode();
if (payload.fullscreen) {
var requestMethod = (
element.requestFullScreen ||
element.webkitRequestFullScreen ||
element.mozRequestFullScreen ||
element.msRequestFullscreen
);
requestMethod.call(element);
}
else {
var exitMethod = (
document.exitFullscreen ||
document.mozCancelFullScreen ||
document.webkitExitFullscreen ||
document.msExitFullscreen
);
if (exitMethod) {
exitMethod.call(document);
}
}
break;
}
},
render: function() { render: function() {
var VideoFeed = sdk.getComponent('atoms.voip.VideoFeed'); var VideoFeed = sdk.getComponent('atoms.voip.VideoFeed');
return ( return (
<div className="mx_VideoView"> <div className="mx_VideoView" ref={this.setContainer}>
<div className="mx_VideoView_remoteVideoFeed"> <div className="mx_VideoView_remoteVideoFeed">
<VideoFeed ref="remote"/> <VideoFeed ref="remote"/>
<audio ref="remoteAudio"/>
</div> </div>
<div className="mx_VideoView_localVideoFeed"> <div className="mx_VideoView_localVideoFeed">
<VideoFeed ref="local"/> <VideoFeed ref="local"/>

View File

@ -0,0 +1,35 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
var CasLoginController = require('matrix-react-sdk/lib/controllers/organisms/CasLogin');
module.exports = React.createClass({
displayName: 'CasLogin',
mixins: [CasLoginController],
render: function() {
return (
<div>
<button onClick={this.onCasClicked}>Sign in with CAS</button>
</div>
);
},
});

View File

@ -18,21 +18,37 @@ limitations under the License.
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'LeftPanel', displayName: 'LeftPanel',
onHideClick: function() {
dis.dispatch({
action: 'hide_left_panel',
});
},
render: function() { render: function() {
var RoomList = sdk.getComponent('organisms.RoomList'); var RoomList = sdk.getComponent('organisms.RoomList');
var BottomLeftMenu = sdk.getComponent('molecules.BottomLeftMenu'); var BottomLeftMenu = sdk.getComponent('molecules.BottomLeftMenu');
var IncomingCallBox = sdk.getComponent('molecules.voip.IncomingCallBox'); var IncomingCallBox = sdk.getComponent('molecules.voip.IncomingCallBox');
var collapseButton;
var classes = "mx_LeftPanel";
if (this.props.collapsed) {
classes += " collapsed";
}
else {
collapseButton = <img className="mx_LeftPanel_hideButton" onClick={ this.onHideClick } src="img/hide.png" width="12" height="20" alt="<"/>
}
return ( return (
<aside className="mx_LeftPanel"> <aside className={classes}>
<img className="mx_LeftPanel_hideButton" src="img/hide.png" width="32" height="32" alt="<"/> { collapseButton }
<IncomingCallBox /> <IncomingCallBox />
<RoomList selectedRoom={this.props.selectedRoom} /> <RoomList selectedRoom={this.props.selectedRoom} collapsed={this.props.collapsed}/>
<BottomLeftMenu /> <BottomLeftMenu collapsed={this.props.collapsed}/>
</aside> </aside>
); );
} }

View File

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var classNames = require('classnames'); var classNames = require('classnames');
var Loader = require('react-loader');
var MemberListController = require('matrix-react-sdk/lib/controllers/organisms/MemberList') var MemberListController = require('matrix-react-sdk/lib/controllers/organisms/MemberList')
@ -32,12 +33,38 @@ module.exports = React.createClass({
return { editing: false }; return { editing: false };
}, },
makeMemberTiles: function() { memberSort: function(userIdA, userIdB) {
var userA = this.memberDict[userIdA].user;
var userB = this.memberDict[userIdB].user;
var presenceMap = {
online: 3,
unavailable: 2,
offline: 1
};
var presenceOrdA = userA ? presenceMap[userA.presence] : 0;
var presenceOrdB = userB ? presenceMap[userB.presence] : 0;
if (presenceOrdA != presenceOrdB) {
return presenceOrdB - presenceOrdA;
}
var latA = userA ? (userA.lastPresenceTs - (userA.lastActiveAgo || userA.lastPresenceTs)) : 0;
var latB = userB ? (userB.lastPresenceTs - (userB.lastActiveAgo || userB.lastPresenceTs)) : 0;
return latB - latA;
},
makeMemberTiles: function(membership) {
var MemberTile = sdk.getComponent("molecules.MemberTile"); var MemberTile = sdk.getComponent("molecules.MemberTile");
var self = this; var self = this;
return Object.keys(self.state.memberDict).map(function(userId) { return self.state.members.filter(function(userId) {
var m = self.state.memberDict[userId]; var m = self.memberDict[userId];
return m.membership == membership;
}).map(function(userId) {
var m = self.memberDict[userId];
return ( return (
<MemberTile key={userId} member={m} ref={userId} /> <MemberTile key={userId} member={m} ref={userId} />
); );
@ -69,28 +96,49 @@ module.exports = React.createClass({
}); });
var EditableText = sdk.getComponent("atoms.EditableText"); var EditableText = sdk.getComponent("atoms.EditableText");
return ( if (this.state.inviting) {
<div className={ classes } onClick={ this.onClickInvite } > return (
<div className="mx_MemberTile_avatar"><img src="img/create-big.png" width="40" height="40" alt=""/></div> <Loader />
<div className="mx_MemberTile_name"> );
<EditableText ref="invite" label="Invite" placeHolder="@user:domain.com" initialValue="" onValueChanged={this.onPopulateInvite}/> } else {
return (
<div className={ classes } onClick={ this.onClickInvite } >
<div className="mx_MemberTile_avatar"><img src="img/create-big.png" width="40" height="40" alt=""/></div>
<div className="mx_MemberTile_name">
<EditableText ref="invite" label="Invite" placeHolder="@user:domain.com" initialValue="" onValueChanged={this.onPopulateInvite}/>
</div>
</div> </div>
</div> );
); }
}, },
render: function() { render: function() {
var invitedSection = null;
var invitedMemberTiles = this.makeMemberTiles('invite');
if (invitedMemberTiles.length > 0) {
invitedSection = (
<div>
<h2>Invited</h2>
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
</div>
</div>
);
}
return ( return (
<div className="mx_MemberList"> <div className="mx_MemberList">
<div className="mx_MemberList_chevron"> <div className="mx_MemberList_chevron">
<img src="img/chevron.png" width="24" height="13"/> <img src="img/chevron.png" width="24" height="13"/>
</div> </div>
<div className="mx_MemberList_border"> <div className="mx_MemberList_border">
<h2>Members</h2> <div>
<div className="mx_MemberList_wrapper"> <h2>Members</h2>
{this.makeMemberTiles()} <div className="mx_MemberList_wrapper">
{this.inviteTile()} {this.makeMemberTiles('join')}
</div>
</div> </div>
{invitedSection}
{this.inviteTile()}
</div> </div>
</div> </div>
); );

View File

@ -58,15 +58,16 @@ var NotifierView = {
if (ev.getContent().body) msg = ev.getContent().body; if (ev.getContent().body) msg = ev.getContent().body;
} }
var avatarUrl = Avatar.avatarUrlForMember( var avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop' ev.sender, 40, 40, 'crop'
); ) : null;
var notification = new global.Notification( var notification = new global.Notification(
title, title,
{ {
"body": msg, "body": msg,
"icon": avatarUrl "icon": avatarUrl,
"tag": "vector"
} }
); );

View File

@ -18,13 +18,12 @@ limitations under the License.
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RightPanel', displayName: 'RightPanel',
Phase : { Phase : {
Blank: 'Blank',
None: 'None',
MemberList: 'MemberList', MemberList: 'MemberList',
FileList: 'FileList', FileList: 'FileList',
}, },
@ -36,11 +35,16 @@ module.exports = React.createClass({
}, },
onMemberListButtonClick: function() { onMemberListButtonClick: function() {
if (this.state.phase == this.Phase.None) { if (this.props.collapsed) {
this.setState({ phase: this.Phase.MemberList }); this.setState({ phase: this.Phase.MemberList });
dis.dispatch({
action: 'show_right_panel',
});
} }
else { else {
this.setState({ phase: this.Phase.None }); dis.dispatch({
action: 'hide_right_panel',
});
} }
}, },
@ -48,6 +52,7 @@ module.exports = React.createClass({
var MemberList = sdk.getComponent('organisms.MemberList'); var MemberList = sdk.getComponent('organisms.MemberList');
var buttonGroup; var buttonGroup;
var panel; var panel;
if (this.props.roomId) { if (this.props.roomId) {
buttonGroup = buttonGroup =
<div className="mx_RightPanel_headerButtonGroup"> <div className="mx_RightPanel_headerButtonGroup">
@ -59,13 +64,18 @@ module.exports = React.createClass({
</div> </div>
</div>; </div>;
if (this.state.phase == this.Phase.MemberList) { if (!this.props.collapsed && this.state.phase == this.Phase.MemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} /> panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />
} }
} }
var classes = "mx_RightPanel";
if (this.props.collapsed) {
classes += " collapsed";
}
return ( return (
<aside className="mx_RightPanel"> <aside className={classes}>
<div className="mx_RightPanel_header"> <div className="mx_RightPanel_header">
{ buttonGroup } { buttonGroup }
</div> </div>

View File

@ -18,6 +18,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var RoomListController = require('../../../../controllers/organisms/RoomList') var RoomListController = require('../../../../controllers/organisms/RoomList')
@ -25,6 +26,12 @@ module.exports = React.createClass({
displayName: 'RoomList', displayName: 'RoomList',
mixins: [RoomListController], mixins: [RoomListController],
onShowClick: function() {
dis.dispatch({
action: 'show_left_panel',
});
},
render: function() { render: function() {
var CallView = sdk.getComponent('molecules.voip.CallView'); var CallView = sdk.getComponent('molecules.voip.CallView');
var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget'); var RoomDropTarget = sdk.getComponent('molecules.RoomDropTarget');
@ -34,13 +41,17 @@ module.exports = React.createClass({
callElement = <CallView className="mx_MatrixChat_callView"/> callElement = <CallView className="mx_MatrixChat_callView"/>
} }
var recentsLabel = this.props.collapsed ?
<img style={{cursor: 'pointer'}} onClick={ this.onShowClick } src="img/menu.png" width="27" height="20" alt=">"/> :
"Recents";
return ( return (
<div className="mx_RoomList"> <div className="mx_RoomList" onScroll={this._repositionTooltip}>
{callElement} {callElement}
<h2 className="mx_RoomList_favourites_label">Favourites</h2> <h2 className="mx_RoomList_favourites_label">Favourites</h2>
<RoomDropTarget text="Drop here to favourite"/> <RoomDropTarget text="Drop here to favourite"/>
<h2 className="mx_RoomList_recents_label">Recents</h2> <h2 className="mx_RoomList_recents_label">{ recentsLabel }</h2>
<div className="mx_RoomList_recents"> <div className="mx_RoomList_recents">
{this.makeRoomTiles()} {this.makeRoomTiles()}
</div> </div>

View File

@ -19,12 +19,13 @@ limitations under the License.
var React = require('react'); var React = require('react');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var dis = require('matrix-react-sdk/lib/dispatcher');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var classNames = require("classnames"); var classNames = require("classnames");
var filesize = require('filesize'); var filesize = require('filesize');
var RoomViewController = require('matrix-react-sdk/lib/controllers/organisms/RoomView') var RoomViewController = require('../../../../controllers/organisms/RoomView')
var Loader = require("react-loader"); var Loader = require("react-loader");
@ -62,6 +63,14 @@ module.exports = React.createClass({
this.setState(this.getInitialState()); this.setState(this.getInitialState());
}, },
onConferenceNotificationClick: function() {
dis.dispatch({
action: 'place_call',
type: "video",
room_id: this.props.roomId
});
},
getUnreadMessagesString: function() { getUnreadMessagesString: function() {
if (!this.state.numUnreadMessages) { if (!this.state.numUnreadMessages) {
return ""; return "";
@ -129,19 +138,33 @@ module.exports = React.createClass({
<div /> <div />
); );
// for testing UI...
// this.state.upload = {
// uploadedBytes: 123493,
// totalBytes: 347534,
// fileName: "testing_fooble.jpg",
// }
if (this.state.upload) { if (this.state.upload) {
var innerProgressStyle = { var innerProgressStyle = {
width: ((this.state.upload.uploadedBytes / this.state.upload.totalBytes) * 100) + '%' width: ((this.state.upload.uploadedBytes / this.state.upload.totalBytes) * 100) + '%'
}; };
var uploadedSize = filesize(this.state.upload.uploadedBytes);
var totalSize = filesize(this.state.upload.totalBytes);
if (uploadedSize.replace(/^.* /,'') === totalSize.replace(/^.* /,'')) {
uploadedSize = uploadedSize.replace(/ .*/, '');
}
statusBar = ( statusBar = (
<div className="mx_RoomView_uploadBar"> <div className="mx_RoomView_uploadBar">
<span className="mx_RoomView_uploadFilename">Uploading {this.state.upload.fileName}</span>
<span className="mx_RoomView_uploadBytes">
{filesize(this.state.upload.uploadedBytes)} / {filesize(this.state.upload.totalBytes)}
</span>
<div className="mx_RoomView_uploadProgressOuter"> <div className="mx_RoomView_uploadProgressOuter">
<div className="mx_RoomView_uploadProgressInner" style={innerProgressStyle}></div> <div className="mx_RoomView_uploadProgressInner" style={innerProgressStyle}></div>
</div> </div>
<img className="mx_RoomView_uploadIcon" src="img/fileicon.png" width="40" height="40"/>
<img className="mx_RoomView_uploadCancel" src="img/cancel.png" width="40" height="40"/>
<div className="mx_RoomView_uploadBytes">
{ uploadedSize } / { totalSize }
</div>
<div className="mx_RoomView_uploadFilename">Uploading {this.state.upload.fileName}</div>
</div> </div>
); );
} else { } else {

View File

@ -30,7 +30,15 @@ module.exports = React.createClass({
editAvatar: function() { editAvatar: function() {
var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl); var url = MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl);
var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar'); var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar');
Modal.createDialog(ChangeAvatar, {initialAvatarUrl: url}); var avatarDialog = (
<div>
<ChangeAvatar initialAvatarUrl={url} />
<div className="mx_Dialog_buttons">
<button onClick={this.onAvatarDialogCancel}>Cancel</button>
</div>
</div>
);
this.avatarDialog = Modal.createDialogWithElement(avatarDialog);
}, },
addEmail: function() { addEmail: function() {
@ -55,12 +63,16 @@ module.exports = React.createClass({
this.logoutModal.closeDialog(); this.logoutModal.closeDialog();
}, },
onAvatarDialogCancel: function() {
this.avatarDialog.close();
},
render: function() { render: function() {
switch (this.state.phase) { switch (this.state.phase) {
case this.Phases.Loading: case this.Phases.Loading:
return <Loader /> return <Loader />
case this.Phases.Display: case this.Phases.Display:
var EditableText = sdk.getComponent('atoms.EditableText'); var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName');
var EnableNotificationsButton = sdk.getComponent('atoms.EnableNotificationsButton'); var EnableNotificationsButton = sdk.getComponent('atoms.EnableNotificationsButton');
return ( return (
<div className="mx_UserSettings"> <div className="mx_UserSettings">
@ -74,13 +86,13 @@ module.exports = React.createClass({
</div> </div>
<div className="mx_UserSettings_DisplayName"> <div className="mx_UserSettings_DisplayName">
<EditableText ref="displayname" initialValue={this.state.displayName} label="Click to set display name." onValueChanged={this.changeDisplayname}/> <ChangeDisplayName ref="displayname" />
<div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>Edit</div> <div className="mx_UserSettings_DisplayName_Edit" onClick={this.editDisplayName}>Edit</div>
</div> </div>
<div className="mx_UserSettings_3pids"> <div className="mx_UserSettings_3pids">
{this.state.threepids.map(function(val) { {this.state.threepids.map(function(val) {
return <div>{val.address}</div>; return <div key={val.address}>{val.address}</div>;
})} })}
</div> </div>

View File

@ -0,0 +1,34 @@
/*
Copyright 2015 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.
*/
'use strict';
var React = require('react');
module.exports = React.createClass({
displayName: 'ViewSource',
render: function() {
return (
<div className="mx_ViewSource">
<pre>
{JSON.stringify(this.props.mxEvent.event, null, 2)}
</pre>
</div>
);
}
});

View File

@ -25,12 +25,66 @@ var MatrixChatController = require('matrix-react-sdk/lib/controllers/pages/Matri
var Loader = require("react-loader"); var Loader = require("react-loader");
var dis = require('matrix-react-sdk/lib/dispatcher'); var dis = require('matrix-react-sdk/lib/dispatcher');
var Matrix = require("matrix-js-sdk");
var ContextualMenu = require("../../../../ContextualMenu");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MatrixChat', displayName: 'MatrixChat',
mixins: [MatrixChatController], mixins: [MatrixChatController],
getInitialState: function() {
return {
width: 10000,
}
},
componentDidMount: function() {
window.addEventListener('resize', this.handleResize);
this.handleResize();
},
componentWillUnmount: function() {
window.removeEventListener('resize', this.handleResize);
},
onAliasClick: function(event, alias) {
event.preventDefault();
dis.dispatch({action: 'view_room_alias', room_alias: alias});
},
onUserClick: function(event, userId) {
event.preventDefault();
var MemberInfo = sdk.getComponent('molecules.MemberInfo');
var member = new Matrix.RoomMember(null, userId);
ContextualMenu.createMenu(MemberInfo, {
member: member,
right: window.innerWidth - event.pageX,
top: event.pageY
});
},
handleResize: function(e) {
var hideLhsThreshold = 1000;
var showLhsThreshold = 1000;
var hideRhsThreshold = 820;
var showRhsThreshold = 820;
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
dis.dispatch({ action: 'hide_left_panel' });
}
if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
dis.dispatch({ action: 'show_left_panel' });
}
if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
dis.dispatch({ action: 'hide_right_panel' });
}
if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
dis.dispatch({ action: 'show_right_panel' });
}
this.setState({width: window.innerWidth});
},
onRoomCreated: function(room_id) { onRoomCreated: function(room_id) {
dis.dispatch({ dis.dispatch({
action: "view_room", action: "view_room",
@ -57,19 +111,19 @@ module.exports = React.createClass({
switch (this.state.page_type) { switch (this.state.page_type) {
case this.PageTypes.RoomView: case this.PageTypes.RoomView:
page_element = <RoomView roomId={this.state.currentRoom} key={this.state.currentRoom} /> page_element = <RoomView roomId={this.state.currentRoom} key={this.state.currentRoom} />
right_panel = <RightPanel roomId={this.state.currentRoom} /> right_panel = <RightPanel roomId={this.state.currentRoom} collapsed={this.state.collapse_rhs} />
break; break;
case this.PageTypes.UserSettings: case this.PageTypes.UserSettings:
page_element = <UserSettings /> page_element = <UserSettings />
right_panel = <RightPanel/> right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
break; break;
case this.PageTypes.CreateRoom: case this.PageTypes.CreateRoom:
page_element = <CreateRoom onRoomCreated={this.onRoomCreated}/> page_element = <CreateRoom onRoomCreated={this.onRoomCreated}/>
right_panel = <RightPanel/> right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
break; break;
case this.PageTypes.RoomDirectory: case this.PageTypes.RoomDirectory:
page_element = <RoomDirectory /> page_element = <RoomDirectory />
right_panel = <RightPanel/> right_panel = <RightPanel collapsed={this.state.collapse_rhs}/>
break; break;
} }
@ -79,7 +133,7 @@ module.exports = React.createClass({
<div className="mx_MatrixChat_wrapper"> <div className="mx_MatrixChat_wrapper">
<MatrixToolbar /> <MatrixToolbar />
<div className="mx_MatrixChat mx_MatrixChat_toolbarShowing"> <div className="mx_MatrixChat mx_MatrixChat_toolbarShowing">
<LeftPanel selectedRoom={this.state.currentRoom} /> <LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} />
<main className="mx_MatrixChat_middlePanel"> <main className="mx_MatrixChat_middlePanel">
{page_element} {page_element}
</main> </main>
@ -91,7 +145,7 @@ module.exports = React.createClass({
else { else {
return ( return (
<div className="mx_MatrixChat"> <div className="mx_MatrixChat">
<LeftPanel selectedRoom={this.state.currentRoom} /> <LeftPanel selectedRoom={this.state.currentRoom} collapsed={this.state.collapse_lhs} />
<main className="mx_MatrixChat_middlePanel"> <main className="mx_MatrixChat_middlePanel">
{page_element} {page_element}
</main> </main>

View File

@ -141,6 +141,11 @@ module.exports = React.createClass({
</form> </form>
</div> </div>
); );
case 'stage_m.login.cas':
var CasLogin = sdk.getComponent('organisms.CasLogin');
return (
<CasLogin />
);
} }
}, },

View File

@ -19,15 +19,15 @@ limitations under the License.
var React = require('react'); var React = require('react');
var sdk = require('matrix-react-sdk') var sdk = require('matrix-react-sdk')
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg')
var Loader = require("react-loader"); var Loader = require("react-loader");
var RegisterController = require('matrix-react-sdk/lib/controllers/templates/Register') var RegisterController = require('../../../../controllers/templates/Register')
var config = require('../../../../../config.json');
module.exports = React.createClass({ module.exports = React.createClass({
DEFAULT_HS_URL: 'https://matrix.org',
DEFAULT_IS_URL: 'https://vector.im',
displayName: 'Register', displayName: 'Register',
mixins: [RegisterController], mixins: [RegisterController],
@ -38,8 +38,8 @@ module.exports = React.createClass({
}, },
componentWillMount: function() { componentWillMount: function() {
this.customHsUrl = this.DEFAULT_HS_URL; this.customHsUrl = config.default_hs_url;
this.customIsUrl = this.DEFAULT_IS_URL; this.customIsUrl = config.default_is_url;
}, },
getRegFormVals: function() { getRegFormVals: function() {
@ -55,7 +55,7 @@ module.exports = React.createClass({
if (this.state.serverConfigVisible) { if (this.state.serverConfigVisible) {
return this.customHsUrl; return this.customHsUrl;
} else { } else {
return this.DEFAULT_HS_URL; return config.default_hs_url;
} }
}, },
@ -63,7 +63,7 @@ module.exports = React.createClass({
if (this.state.serverConfigVisible) { if (this.state.serverConfigVisible) {
return this.customIsUrl; return this.customIsUrl;
} else { } else {
return this.DEFAULT_IS_URL; return config.default_is_url;
} }
}, },
@ -79,6 +79,10 @@ module.exports = React.createClass({
this.forceUpdate(); this.forceUpdate();
}, },
onProfileContinueClicked: function() {
this.onAccountReady();
},
componentForStep: function(step) { componentForStep: function(step) {
switch (step) { switch (step) {
case 'initial': case 'initial':
@ -127,6 +131,18 @@ module.exports = React.createClass({
return ( return (
<Loader /> <Loader />
); );
} else if (this.state.step == 'profile') {
var ChangeDisplayName = sdk.getComponent('molecules.ChangeDisplayName');
var ChangeAvatar = sdk.getComponent('molecules.ChangeAvatar');
return (
<div className="mx_Login_profile">
Set a display name:
<ChangeDisplayName />
Upload an avatar:
<ChangeAvatar initialAvatarUrl={MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl)} />
<button onClick={this.onProfileContinueClicked}>Continue</button>
</div>
);
} else { } else {
return ( return (
<div> <div>
@ -156,6 +172,12 @@ module.exports = React.createClass({
case this.FieldErrors.InUse: case this.FieldErrors.InUse:
strings.push(keys[i]+" is already taken"); strings.push(keys[i]+" is already taken");
break; break;
case this.FieldErrors.Length:
strings.push(keys[i] + " is not long enough.");
break;
default:
console.error("Unhandled FieldError: %s", bad[keys[i]]);
break;
} }
} }
var errtxt = strings.join(', '); var errtxt = strings.join(', ');

View File

@ -21,24 +21,29 @@ var sdk = require("matrix-react-sdk");
sdk.loadSkin(require('../skins/vector/skindex')); sdk.loadSkin(require('../skins/vector/skindex'));
sdk.loadModule(require('../modules/VectorConferenceHandler')); sdk.loadModule(require('../modules/VectorConferenceHandler'));
var qs = require("querystring");
var lastLocationHashSet = null; var lastLocationHashSet = null;
// We want to support some name / value pairs in the fragment
// so we're re-using query string like format
function parseQsFromFragment(location) {
var hashparts = location.hash.split('?');
if (hashparts.length > 1) {
return qs.parse(hashparts[1]);
}
return {};
}
// Here, we do some crude URL analysis to allow // Here, we do some crude URL analysis to allow
// deep-linking. We only support registration // deep-linking. We only support registration
// deep-links in this example. // deep-links in this example.
function routeUrl(location) { function routeUrl(location) {
if (location.hash.indexOf('#/register') == 0) { if (location.hash.indexOf('#/register') == 0) {
var hashparts = location.hash.split('?'); window.matrixChat.showScreen('register', parseQsFromFragment(location));
var params = {}; } else if (location.hash.indexOf('#/login/cas') == 0) {
if (hashparts.length == 2) { window.matrixChat.showScreen('cas_login', parseQsFromFragment(location));
var pairs = hashparts[1].split('&');
for (var i = 0; i < pairs.length; ++i) {
var parts = pairs[i].split('=');
if (parts.length != 2) continue;
params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
}
}
window.matrixChat.showScreen('register', params);
} else { } else {
window.matrixChat.showScreen(location.hash.substring(2)); window.matrixChat.showScreen(location.hash.substring(2));
} }
@ -53,14 +58,18 @@ function onHashChange(ev) {
} }
var loaded = false; var loaded = false;
var lastLoadedScreen = null;
// This will be called whenever the SDK changes screens, // This will be called whenever the SDK changes screens,
// so a web page can update the URL bar appropriately. // so a web page can update the URL bar appropriately.
var onNewScreen = function(screen) { var onNewScreen = function(screen) {
if (!loaded) return; if (!loaded) {
var hash = '#/' + screen; lastLoadedScreen = screen;
lastLocationHashSet = hash; } else {
window.location.hash = hash; var hash = '#/' + screen;
lastLocationHashSet = hash;
window.location.hash = hash;
}
} }
// We use this to work out what URL the SDK should // We use this to work out what URL the SDK should
@ -85,5 +94,9 @@ window.addEventListener('hashchange', onHashChange);
window.onload = function() { window.onload = function() {
routeUrl(window.location); routeUrl(window.location);
loaded = true; loaded = true;
if (lastLoadedScreen) {
onNewScreen(lastLoadedScreen);
lastLoadedScreen = null;
}
} }

View File

@ -11,6 +11,19 @@ module.exports = {
{ test: /\.js$/, loader: "babel", include: path.resolve('./src') }, { test: /\.js$/, loader: "babel", include: path.resolve('./src') },
] ]
}, },
output: {
devtoolModuleFilenameTemplate: function(info) {
// Reading input source maps gives only relative paths here for
// everything. Until I figure out how to fix this, this is a
// workaround.
// We use the relative resource path with any '../'s on the front
// removed which gives a tree with matrix-react-sdk and vector
// trees smashed together, but this fixes everything being under
// various levels of '.' and '..'
// Also, sometimes the resource path is absolute.
return path.relative(process.cwd(), info.resourcePath).replace(/^[\/\.]*/, '');
}
},
resolve: { resolve: {
alias: { alias: {
// alias any requires to the react module to the one in our path, otherwise // alias any requires to the react module to the one in our path, otherwise
@ -19,7 +32,12 @@ module.exports = {
}, },
}, },
plugins: [ plugins: [
new webpack.IgnorePlugin(/^olm/) new webpack.IgnorePlugin(/^olm/),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV)
}
})
], ],
devtool: 'source-map' devtool: 'source-map'
}; };