Merge pull request #296 from vector-im/matthew/redesign

Matthew/redesign
pull/314/head
Matthew Hodgson 2015-10-30 18:30:13 +00:00
commit e25d31a9fe
87 changed files with 1308 additions and 414 deletions

4
.gitignore vendored
View File

@ -1,3 +1,7 @@
node_modules
vector/bundle.*
lib
.DS_Store
key.pem
cert.pem
build

View File

@ -28,7 +28,7 @@ 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
too, which isn't handled by Vector's `package.json`. To get the right dependencies, check out
the `develop` branches of these libraries and then use `npm link` to tell Vector
about them:

View File

@ -33,7 +33,8 @@
"matrix-react-sdk": "^0.0.2",
"q": "^1.4.1",
"react": "^0.13.3",
"react-loader": "^1.4.0"
"react-loader": "^1.4.0",
"sanitize-html": "^1.11.1"
},
"devDependencies": {
"babel": "^5.8.23",

45
src/DateUtils.js Normal file
View File

@ -0,0 +1,45 @@
/*
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 days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
module.exports = {
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());
}
}
}

View File

@ -36,11 +36,9 @@ module.exports = {
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
var rooms = this.getRoomList();
this.setState({
roomList: rooms,
activityMap: {}
});
var s = this.getRoomLists();
s.activityMap = {};
this.setState(s);
},
componentDidMount: function() {
@ -87,9 +85,7 @@ module.exports = {
onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (toStartOfTimeline) return;
var newState = {
roomList: this.getRoomList()
};
var newState = this.getRoomLists();
if (
room.roomId != this.props.selectedRoom &&
ev.getSender() != MatrixClientPeg.get().credentials.userId)
@ -123,18 +119,23 @@ module.exports = {
refreshRoomList: function() {
var rooms = this.getRoomList();
this.setState({
roomList: rooms
});
this.setState(this.getRoomLists());
},
getRoomList: function() {
return RoomListSorter.mostRecentActivityFirst(
getRoomLists: function() {
var s = {};
var inviteList = [];
s.roomList = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms().filter(function(room) {
var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && me.membership == "invite") {
inviteList.push(room);
return false;
}
var shouldShowRoom = (
me && (me.membership == "join" || me.membership == "invite")
me && (me.membership == "join")
);
// hiding conf rooms only ever toggles shouldShowRoom to false
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
@ -153,6 +154,8 @@ module.exports = {
return shouldShowRoom;
})
);
s.inviteList = RoomListSorter.mostRecentActivityFirst(inviteList);
return s;
},
_recheckCallElement: function(selectedRoomId) {
@ -174,10 +177,10 @@ module.exports = {
}
},
makeRoomTiles: function() {
makeRoomTiles: function(list, isInvite) {
var self = this;
var RoomTile = sdk.getComponent("molecules.RoomTile");
return this.state.roomList.map(function(room) {
return list.map(function(room) {
var selected = room.roomId == self.props.selectedRoom;
return (
<RoomTile
@ -187,6 +190,7 @@ module.exports = {
selected={selected}
unread={self.state.activityMap[room.roomId] === 1}
highlight={self.state.activityMap[room.roomId] === 2}
isInvite={isInvite}
/>
);
});

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var Matrix = require("matrix-js-sdk");
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var React = require("react");
var q = require("q");
@ -38,6 +39,8 @@ module.exports = {
uploadingRoomSettings: false,
numUnreadMessages: 0,
draggingFile: false,
searching: false,
searchResults: null,
}
},
@ -356,6 +359,41 @@ module.exports = {
return WhoIsTyping.whoIsTypingString(this.state.room);
},
onSearch: function(term, scope) {
var filter;
if (scope === "Room") { // FIXME: should be enum
filter = {
// XXX: it's unintuitive that the filter for searching doesn't have the same shape as the v2 filter API :(
rooms: [
this.props.roomId
]
};
}
var self = this;
MatrixClientPeg.get().search({
body: {
search_categories: {
room_events: {
search_term: term,
filter: filter,
event_context: {
before_limit: 1,
after_limit: 1,
}
}
}
}
}).then(function(data) {
self.setState({
searchTerm: term,
searchResults: data,
});
}, function(error) {
// TODO: show dialog or something
});
},
getEventTiles: function() {
var DateSeparator = sdk.getComponent('molecules.DateSeparator');
@ -364,6 +402,36 @@ module.exports = {
var EventTile = sdk.getComponent('molecules.EventTile');
if (this.state.searchResults) {
// XXX: this dance is foul, due to the results API not returning sorted results
var results = this.state.searchResults.search_categories.room_events.results;
var eventIds = Object.keys(results);
// XXX: todo: merge overlapping results somehow?
// XXX: why doesn't searching on name work?
var resultList = eventIds.map(function(key) { return results[key]; }).sort(function(a, b) { b.rank - a.rank });
for (var i = 0; i < resultList.length; i++) {
var ts1 = resultList[i].result.origin_server_ts;
ret.push(<li key={ts1 + "-search"}><DateSeparator ts={ts1}/></li>); // Rank: {resultList[i].rank}
var mxEv = new Matrix.MatrixEvent(resultList[i].result);
if (resultList[i].context.events_before[0]) {
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_before[0]);
if (EventTile.supportsEventType(mxEv2.getType())) {
ret.push(<li key={mxEv.getId() + "-1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
}
}
if (EventTile.supportsEventType(mxEv.getType())) {
ret.push(<li key={mxEv.getId() + "+0"}><EventTile mxEvent={mxEv} searchTerm={this.state.searchTerm}/></li>);
}
if (resultList[i].context.events_after[0]) {
var mxEv2 = new Matrix.MatrixEvent(resultList[i].context.events_after[0]);
if (EventTile.supportsEventType(mxEv2.getType())) {
ret.push(<li key={mxEv.getId() + "+1"}><EventTile mxEvent={mxEv2} contextual={true} /></li>);
}
}
}
return ret;
}
for (var i = this.state.room.timeline.length-1; i >= 0 && count < this.state.messageCap; --i) {
var mxEv = this.state.room.timeline[i];

View File

@ -0,0 +1,144 @@
/*
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.
*/
/* This has got to be the most fragile piece of CSS ever written.
But empirically it works on Chrome/FF/Safari
*/
.mx_ImageView {
display: -webkit-flex;
display: flex;
width: 100%;
height: 100%;
-webkit-align-items: center;
align-items: center;
}
.mx_ImageView_lhs {
-webkit-box-ordinal-group: 1;
order: 1;
-webkit-flex: 1;
flex: 1 1 10%;
min-width: 60px;
/*
background-color: #080;
height: 20px;
*/
}
.mx_ImageView_content {
-webkit-box-ordinal-group: 2;
order: 2;
/* min-width hack needed for FF */
min-width: 0px;
height: 90%;
-webkit-flex: 15;
flex: 15 15 0;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
-webkit-justify-content: center;
align-items: center;
justify-content: center;
}
.mx_ImageView_content img {
max-width: 100%;
/* XXX: max-height interacts badly with flex on Chrome and doesn't relayout properly until you refresh */
max-height: 100%;
/* object-fit hack needed for Chrome due to Chrome not relaying out until you refresh */
object-fit: contain;
/* background-image: url('img/trans.png'); */
}
.mx_ImageView_labelWrapper {
position: absolute;
top: 0px;
height: 100%;
overflow: auto;
}
.mx_ImageView_label {
text-align: left;
display: flex;
display: -webkit-flex;
justify-content: center;
-webkit-justify-content: center;
flex-direction: column;
-webkit-flex-direction: column;
padding-left: 60px;
padding-right: 60px;
min-height: 100%;
color: #fff;
}
.mx_ImageView_name {
font-size: 20px;
margin-bottom: 6px;
pointer-events: all;
}
.mx_ImageView_metadata {
font-size: 16px;
opacity: 0.5;
pointer-events: all;
}
.mx_ImageView_download {
pointer-events: all;
display: table;
margin-top: 24px;
margin-bottom: 6px;
border-radius: 5px;
background-color: #454545;
font-size: 16px;
padding: 9px;
border: 1px solid #fff;
}
.mx_ImageView_size {
font-size: 12px;
}
.mx_ImageView_link {
color: #fff ! important;
text-decoration: none ! important;
}
.mx_ImageView_button {
pointer-events: all;
font-size: 16px;
opacity: 0.5;
margin-top: 18px;
cursor: pointer;
}
.mx_ImageView_shim {
height: 30px;
}
.mx_ImageView_rhs {
-webkit-box-ordinal-group: 3;
order: 3;
-webkit-flex: 1;
flex: 1 1 10%;
min-width: 300px;
/*
background-color: #800;
height: 20px;
*/
}

View File

@ -17,6 +17,5 @@ limitations under the License.
.mx_MemberAvatar {
z-index: 20;
border-radius: 20px;
background-color: #dbdbdb;
}

View File

@ -22,7 +22,7 @@ html {
}
body {
font-family: 'Lato', Helvetica, Arial, Sans-Serif;
font-family: 'Myriad Pro', Helvetica, Arial, Sans-Serif;
font-size: 16px;
color: #454545;
border: 0px;
@ -34,7 +34,7 @@ div.error {
}
h2 {
color: #80cef4;
color: #454545;
font-weight: 400;
font-size: 20px;
margin-top: 16px;
@ -44,7 +44,7 @@ h2 {
a:hover,
a:link,
a:visited {
color: #80CEF4;
color: #76cfa6;
}
.mx_ContextualMenu_background {
@ -58,7 +58,7 @@ a:visited {
}
.mx_ContextualMenu {
border: 1px solid #a9dbf4;
border: 1px solid #a4a4a4;
border-radius: 8px;
background-color: #fff;
color: #747474;
@ -129,13 +129,21 @@ a:visited {
font-size: 16px;
position: relative;
border-radius: 8px;
max-width: 75%;
max-width: 80%;
}
.mx_ImageView {
margin: 6px;
/* hack: flexbox bug? */
margin-bottom: 4px;
.mx_Dialog_lightbox .mx_Dialog_background {
opacity: 0.85;
}
.mx_Dialog_lightbox .mx_Dialog {
border-radius: 0px;
background-color: transparent;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
pointer-events: none;
}
.mx_Dialog_content {
@ -153,7 +161,7 @@ a:visited {
font-weight: 400;
font-size: 16px;
color: #fff;
background-color: #80cef4;
background-color: #76cfa6;
margin-left: 8px;
margin-right: 8px;
padding-left: 1em;
@ -164,7 +172,7 @@ a:visited {
.mx_QuestionDialogTitle {
min-height: 16px;
padding: 12px;
border-bottom: 1px solid #a9dbf4;
border-bottom: 1px solid #a4a4a4;
font-weight: bold;
font-size: 20px;
line-height: 1.4;

View File

@ -1,7 +1,4 @@
.mx_RoomDropTarget,
.mx_RoomList_favourites_label,
.mx_RoomList_archive_label,
.mx_RoomHeader_search,
.mx_RoomSettings_encrypt,
.mx_CreateRoom_encrypt,
.mx_RightPanel_filebutton

View File

@ -0,0 +1,19 @@
/*
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.
*/
.mx_EventAsTextTile {
opacity: 0.5;
}

View File

@ -17,20 +17,19 @@ limitations under the License.
.mx_EventTile {
max-width: 100%;
clear: both;
margin-top: 32px;
margin-top: 24px;
margin-left: 56px;
}
.mx_EventTile_avatar {
padding-left: 12px;
padding-left: 18px;
padding-right: 12px;
margin-left: -64px;
margin-top: -7px;
margin-top: -4px;
float: left;
}
.mx_EventTile_avatar img {
background-color: #dbdbdb;
border-radius: 20px;
border: 0px;
}
@ -48,19 +47,30 @@ limitations under the License.
}
.mx_EventTile .mx_MessageTimestamp {
color: #454545;
opacity: 0.5;
font-size: 14px;
color: #acacac;
font-size: 12px;
float: right;
}
.mx_EventTile_line {
position: relative;
}
.mx_EventTile_content {
padding-right: 100px;
display: block;
}
.mx_EventTile_notice .mx_MessageTile_content {
opacity: 0.5;
.mx_MessageTile_content {
display: block;
margin-right: 100px;
}
.mx_MessageTile_searchHighlight {
background-color: #76cfa6;
color: #fff;
border-radius: 5px;
padding: 4px;
}
.mx_EventTile_sending {
@ -75,38 +85,41 @@ limitations under the License.
color: #FF0064;
}
.mx_EventTile_contextual {
opacity: 0.4;
}
.mx_EventTile_msgOption {
float: right;
}
.mx_MessageTimestamp {
display: none;
visibility: hidden;
}
.mx_EventTile_last .mx_MessageTimestamp {
display: block;
visibility: visible;
}
.mx_EventTile:hover .mx_MessageTimestamp {
display: block;
visibility: visible;
}
.mx_EventTile_editButton {
float: right;
display: none;
border: 0px;
outline: none;
margin-right: 3px;
position: absolute;
right: 1px;
top: 15px;
visibility: hidden;
}
.mx_EventTile:hover .mx_EventTile_editButton {
display: inline-block;
visibility: visible;
}
.mx_EventTile.menu .mx_EventTile_editButton {
display: inline-block;
visibility: visible;
}
.mx_EventTile.menu .mx_MessageTimestamp {
display: inline-block;
visibility: visible;
}

View File

@ -23,12 +23,12 @@ limitations under the License.
}
.mx_MImageTile_download {
color: #80cef4;
color: #76cfa6;
cursor: pointer;
}
.mx_MImageTile_download a {
color: #80cef4;
color: #76cfa6;
text-decoration: none;
}

View File

@ -15,5 +15,5 @@ limitations under the License.
*/
.mx_MNoticeTile {
opacity: 0.5;
opacity: 0.6;
}

View File

@ -17,4 +17,3 @@ limitations under the License.
.mx_MTextTile {
white-space: pre-wrap;
}

View File

@ -14,3 +14,49 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_MemberInfo {
height: 100%;
}
.mx_MemberInfo h2 {
margin-top: 6px;
}
.mx_MemberInfo_cancel {
float: right;
margin-right: 18px;
cursor: pointer;
}
.mx_MemberInfo_avatar {
clear: both;
}
.mx_MemberInfo_avatar img {
border-radius: 48px;
}
.mx_MemberInfo_profileField {
opacity: 0.6;
font-size: 14px;
}
.mx_MemberInfo_buttons {
margin-top: 18px;
}
.mx_MemberInfo_field {
cursor: pointer;
width: 100px;
text-align: center;
font-size: 12px;
background-color: #888;
color: #fff;
font-weight: bold;
border-radius: 20px;
padding-left: 6px;
padding-right: 6px;
padding-top: 4px;
padding-bottom: 2px;
margin-bottom: 4px;
}

View File

@ -16,52 +16,27 @@ limitations under the License.
.mx_MemberTile {
display: table-row;
height: 49px;
position: relative;
color: #454545;
cursor: pointer;
}
.mx_MemberTile_avatar {
display: table-cell;
padding-left: 14px;
padding-left: 3px;
padding-right: 12px;
padding-top: 3px;
padding-bottom: 3px;
padding-top: 2px;
padding-bottom: 0px;
vertical-align: middle;
width: 40px;
height: 40px;
width: 36px;
height: 36px;
position: relative;
}
.mx_MemberTile_inviteTile {
cursor: pointer;
}
.mx_MemberTile_inviteEditing {
display: initial ! important;
}
.mx_MemberTile_inviteEditing .mx_MemberTile_avatar {
display: none;
}
.mx_MemberTile_inviteEditing .mx_MemberTile_name {
width: 200px;
}
.mx_MemberTile_inviteEditing .mx_MemberTile_name input {
border-radius: 3px;
border: 1px solid #c7c7c7;
font-weight: 300;
font-size: 14px;
padding: 9px;
margin-top: 6px;
margin-left: 14px;
}
.mx_MemberTile_power {
position: absolute;
width: 48px;
height: 48px;
width: 44px;
height: 44px;
left: 10px;
top: -1px;
}
@ -79,20 +54,18 @@ limitations under the License.
vertical-align: middle;
}
.mx_MemberTile_hover {
background-color: #f0f0f0;
font-size: 12px;
color: #747474;
}
.mx_MemberTile_userId {
font-weight: bold;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_MemberTile_leave {
cursor: pointer;
.mx_MemberTile_presence {
font-size: 12px;
opacity: 0.5;
}
.mx_MemberTile_chevron {
margin-top: 8px;
margin-right: -4px;
margin-left: 6px;
@ -113,14 +86,14 @@ limitations under the License.
.mx_MemberTile_unavailable .mx_MemberTile_avatar,
.mx_MemberTile_unavailable .mx_MemberTile_name,
.mx_MemberTile_unavailable .mx_MemberTile_nameSpan
.mx_MemberTile_unavailable .mx_MemberTile_userId
{
opacity: 0.66;
}
.mx_MemberTile_offline .mx_MemberTile_avatar,
.mx_MemberTile_offline .mx_MemberTile_name,
.mx_MemberTile_offline .mx_MemberTile_nameSpan
.mx_MemberTile_offline .mx_MemberTile_userId
{
opacity: 0.25;
}

View File

@ -15,39 +15,37 @@ limitations under the License.
*/
.mx_MessageComposer_wrapper {
max-width: 720px;
height: 50px;
max-width: 960px;
height: 70px;
vertical-align: middle;
margin: auto;
background-color: #fff;
border-radius: 25px;
border: 1px solid #a9dbf4;
border-top: 2px solid #e1dddd;
}
.mx_MessageComposer_row {
display: table-row;
width: 100%;
height: 50px;
height: 70px;
}
.mx_MessageComposer .mx_MessageComposer_avatar {
display: table-cell;
padding-left: 5px;
padding-right: 10px;
height: 50px;
padding-left: 10px;
padding-right: 20px;
height: 70px;
}
.mx_MessageComposer .mx_MessageComposer_avatar img {
margin-top: 5px;
margin-top: 18px;
border-radius: 20px;
background-color: #dbdbdb;
}
.mx_MessageComposer_input {
display: table-cell;
width: 100%;
vertical-align: middle;
height: 50px;
height: 70px;
}
.mx_MessageComposer_input textarea {
@ -64,21 +62,32 @@ limitations under the License.
box-shadow: none;
/* needed for FF */
font-family: 'Lato', Helvetica, Arial, Sans-Serif;
font-family: 'Myriad Pro', Helvetica, Arial, Sans-Serif;
}
/* hack for FF as vertical alignment of custom placeholder text is broken */
.mx_MessageComposer_input textarea::-moz-placeholder {
line-height: 100%;
color: #76cfa6;
}
.mx_MessageComposer_input textarea::-webkit-input-placeholder {
color: #76cfa6;
}
.mx_MessageComposer_upload {
.mx_MessageComposer_upload,
.mx_MessageComposer_call {
display: table-cell;
vertical-align: middle;
padding-right: 15px;
padding-left: 10px;
padding-right: 10px;
cursor: pointer;
}
.mx_MessageComposer_call {
padding-right: 10px;
padding-top: 4px;
}
.mx_MessageComposer_upload img {
margin-top: 5px;
}

View File

@ -18,10 +18,10 @@ limitations under the License.
}
.mx_RoomHeader_wrapper {
max-width: 720px;
max-width: 960px;
margin: auto;
height: 88px;
border-bottom: 1px solid #a8dbf3;
height: 83px;
border-bottom: 1px solid #eeeeee;
display: -webkit-box;
display: -moz-box;
@ -47,7 +47,7 @@ limitations under the License.
.mx_RoomHeader_textButton {
height: 48px;
margin-top: 18px;
background-color: #80cef4;
background-color: #76cfa6;
border-radius: 48px;
margin-right: 8px;
color: #fff;
@ -71,11 +71,8 @@ limitations under the License.
}
.mx_RoomHeader_rightRow {
height: 48px;
margin-top: 18px;
margin-top: 32px;
background-color: #fff;
border-radius: 48px;
border: 1px solid #a9dbf4;
-webkit-box-ordinal-group: 3;
-moz-box-ordinal-group: 3;
@ -91,8 +88,8 @@ limitations under the License.
}
.mx_RoomHeader_simpleHeader {
line-height: 88px;
color: #80cef4;
line-height: 83px;
color: #76cfa6;
font-weight: 400;
font-size: 20px;
overflow: hidden;
@ -100,18 +97,39 @@ limitations under the License.
}
.mx_RoomHeader_name {
cursor: pointer;
vertical-align: middle;
height: 28px;
color: #80cef4;
font-weight: 400;
font-size: 20px;
padding-left: 16px;
color: #454545;
font-weight: 800;
font-size: 24px;
padding-left: 8px;
padding-right: 16px;
text-overflow: ellipsis;
}
.mx_RoomHeader_nametext {
display: inline-block;
}
.mx_RoomHeader_settingsButton {
display: inline-block;
visibility: hidden;
position: relative;
bottom: 10px;
left: 4px;
}
.mx_RoomHeader_name:hover {
color: #76cfa6;
}
.mx_RoomHeader_name:hover .mx_RoomHeader_settingsButton {
visibility: visible;
}
.mx_RoomHeader_nameEditing {
padding-left: 16px;
padding-left: 8px;
padding-right: 16px;
margin-top: -5px;
}
@ -133,9 +151,9 @@ limitations under the License.
vertical-align: bottom;
float: left;
max-height: 38px;
color: #70b5d7;
color: #454545;
font-weight: 300;
padding-left: 16px;
padding-left: 8px;
padding-right: 16px;
overflow: hidden;
text-overflow: ellipsis;
@ -153,9 +171,8 @@ limitations under the License.
}
.mx_RoomHeader_button {
height: 48px;
display: table-cell;
vertical-align: middle;
vertical-align: top;
padding-left: 8px;
padding-right: 8px;
}
@ -170,4 +187,4 @@ limitations under the License.
.mx_RoomHeader_voipButtons {
margin-top: 18px;
}
}

View File

@ -61,7 +61,7 @@ limitations under the License.
font-weight: 400;
font-size: 16px;
color: #fff;
background-color: #80cef4;
background-color: #76cfa6;
width: auto;
margin: auto;
padding: 6px;

View File

@ -17,24 +17,24 @@ limitations under the License.
.mx_RoomTile {
cursor: pointer;
display: table-row;
color: #818794;
font-size: 14px;
}
.mx_RoomTile_avatar {
display: table-cell;
padding-right: 10px;
padding-top: 3px;
padding-bottom: 3px;
padding-left: 10px;
background: #eaf5f0;
padding-right: 8px;
padding-top: 4px;
padding-bottom: 2px;
padding-left: 18px;
vertical-align: middle;
width: 36px;
height: 36px;
width: 24px;
height: 24px;
position: relative;
}
.mx_RoomTile_avatar img {
border-radius: 20px;
background-color: #dbdbdb;
}
.mx_RoomTile_name {
@ -43,6 +43,13 @@ limitations under the License.
overflow: hidden;
text-overflow: ellipsis;
padding-right: 16px;
color: #454545;
opacity: 0.8;
}
.mx_RoomTile_invite {
opacity: 0.5;
font-weight: normal;
}
.collapsed .mx_RoomTile_name {
@ -63,7 +70,7 @@ limitations under the License.
}
.mx_RoomTile_badge {
background-color: #80cef4;
background-color: #76cfa6;
color: #fff;
border-radius: 26px;
font-weight: 400;
@ -75,6 +82,7 @@ limitations under the License.
}
*/
/*
.mx_RoomTile_badge {
background-color: #ff0064;
border: 3px solid #fff;
@ -85,19 +93,36 @@ limitations under the License.
right: 9px;
bottom: 3px;
}
*/
.mx_RoomTile_badge {
background-color: #76cfa6;
width: 4px;
position: absolute;
left: 0px;
top: 5px;
height: 24px;
}
.mx_RoomTile_unread,
.mx_RoomTile_highlight,
.mx_RoomTile_invited
{
font-weight: bold;
color: #000;
}
.mx_RoomTile_selected {
background-color: #f3f8fa;
color: #80cef4;
font-weight: bold;
}
.mx_RoomTile.mx_RoomTile_selected {
background: url('img/selected.png');
background-repeat: no-repeat;
background-position: right center;
}
.mx_RoomTile_arrow {
position: absolute;
right: 0px;
}
.mx_RoomTile:hover {

View File

@ -17,7 +17,7 @@ limitations under the License.
.mx_RoomTooltip {
display: none;
position: fixed;
border: 1px solid #a9dbf4;
border: 1px solid #a4a4a4;
border-radius: 8px;
background-color: #fff;
z-index: 1000;

View File

@ -0,0 +1,64 @@
/*
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.
*/
.mx_SearchBar {
padding-top: 5px;
padding-bottom: 5px;
display: flex;
align-items: center;
}
.mx_SearchBar input {
display: inline block;
border-radius: 3px;
border: 1px solid #f0f0f0;
font-size: 16px;
padding: 9px;
padding-left: 11px;
margin-right: 17px;
width: auto;
flex: 1 1 0;
}
.mx_SearchBar_button {
display: inline;
border: 0px;
border-radius: 36px;
font-weight: 400;
font-size: 16px;
color: #fff;
background-color: #76cfa6;
width: auto;
margin: auto;
margin-left: 7px;
padding-top: 6px;
padding-bottom: 4px;
padding-left: 24px;
padding-right: 24px;
cursor: pointer;
}
.mx_SearchBar_unselected {
background-color: #fff;
color: #9fddc1;
border: #9fddc1 1px solid;
}
.mx_SearchBar_cancel {
padding-left: 14px;
padding-right: 14px;
cursor: pointer;
}

View File

@ -16,7 +16,7 @@ limitations under the License.
.mx_IncomingCallBox {
text-align: center;
border: 1px solid #a9dbf4;
border: 1px solid #a4a4a4;
border-radius: 8px;
background-color: #fff;
position: absolute;

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
.mx_CreateRoom {
width: 720px;
width: 960px;
margin-left: auto;
margin-right: auto;
color: #4a4a4a;

View File

@ -53,17 +53,20 @@ limitations under the License.
-webkit-order: 3;
order: 3;
-webkit-flex: 0 0 170px;
flex: 0 0 170px;
border-top: 1px solid #f3f8fa;
-webkit-flex: 0 0 126px;
flex: 0 0 126px;
}
.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile {
color: #378bb4;
color: #454545;
}
.mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options {
margin-top: 12px;
width: 100%;
}
.mx_LeftPanel .mx_BottomLeftMenu img {
border-radius: 0px;
background-color: transparent;
}

View File

@ -16,8 +16,6 @@ limitations under the License.
.mx_MemberList {
height: 100%;
margin-bottom: 100px;
padding: 8px;
-webkit-flex: 1;
flex: 1;
@ -39,22 +37,47 @@ limitations under the License.
}
.mx_MemberList_border {
border: 1px solid #a9dbf4;
overflow-y: auto;
border-radius: 8px;
background-color: #fff;
order: 1;
-webkit-flex: 1 1 0;
flex: 1 1 0px;
}
.mx_MemberList_invite {
font-family: 'Myriad Pro', Helvetica, Arial, Sans-Serif;
border-radius: 3px;
border: 1px solid #f0f0f0;
padding: 9px;
color: #454545;
margin-left: 3px;
font-size: 16px;
margin-bottom: 8px;
width: 180px;
}
.mx_MemberList_invite::-moz-placeholder {
color: #454545;
opacity: 0.5;
}
.mx_MessageList_invite::-webkit-input-placeholder {
color: #454545;
opacity: 0.5;
}
.mx_MemberList_invited h2 {
text-transform: uppercase;
color: #3d3b39;
font-weight: 600;
font-size: 14px;
padding-left: 3px;
padding-right: 12px;
margin-top: 8px;
margin-bottom: 4px;
}
.mx_MemberList_wrapper {
display: table;
table-layout: fixed;
width: 100%;
}
.mx_MemberList h2 {
margin: 14px;
}

View File

@ -33,32 +33,53 @@ limitations under the License.
-webkit-order: 1;
order: 1;
-webkit-flex: 0 0 66px;
flex: 0 0 66px;
-webkit-flex: 0 0 83px;
flex: 0 0 83px;
}
/** Fixme - factor this out with the main header **/
.mx_RightPanel_headerButtonGroup {
margin-top: 18px;
height: 48px;
float: right;
margin-top: 32px;
float: left;
background-color: #fff;
border-radius: 48px;
border: 1px solid #a9dbf4;
margin-right: 22px;
margin-left: -4px;
}
.mx_RightPanel_headerButton {
cursor: pointer;
height: 48px;
display: table-cell;
vertical-align: middle;
padding-left: 8px;
padding-right: 8px;
padding-left: 15px;
padding-right: 15px;
position: relative;
}
.mx_RightPanel .mx_MemberList {
.mx_RightPanel_headerButton_highlight {
position: absolute;
bottom: -2px;
left: 10px;
width: 25px;
height: 4px;
background-color: #76cfa6;
}
.mx_RightPanel_headerButton_badge {
position: absolute;
top: 5px;
left: 28px;
font-size: 12px;
background-color: #76cfa6;
color: #fff;
font-weight: bold;
border-radius: 20px;
padding-left: 4px;
padding-right: 4px;
padding-top: 2px;
}
.mx_RightPanel .mx_MemberList,
.mx_RightPanel .mx_MemberInfo {
-webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2;
-ms-flex-order: 2;

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
.mx_RoomDirectory {
width: 720px;
width: 960px;
margin-left: auto;
margin-right: auto;
margin-bottom: 12px;

View File

@ -15,17 +15,30 @@ limitations under the License.
*/
.mx_RoomList {
padding-top: 24px;
}
.mx_RoomList_invites,
.mx_RoomList_recents {
margin-top: -12px;
display: table;
table-layout: fixed;
width: 100%;
}
.mx_RoomList_expandButton {
margin-left: 8px;
cursor: pointer;
padding-left: 12px;
padding-right: 12px;
}
.mx_RoomList h2 {
padding-left: 16px;
padding-right: 16px;
padding-bottom: 10px;
}
text-transform: uppercase;
color: #3d3b39;
font-weight: 600;
font-size: 14px;
padding-left: 12px;
padding-right: 12px;
margin-top: 8px;
margin-bottom: 4px;
}

View File

@ -36,13 +36,13 @@ limitations under the License.
-webkit-order: 1;
order: 1;
-webkit-flex: 0 0 88px;
flex: 0 0 88px;
-webkit-flex: 0 0 83px;
flex: 0 0 83px;
}
.mx_RoomView_fileDropTarget {
min-width: 0px;
max-width: 720px;
max-width: 960px;
width: 100%;
font-size: 20px;
text-align: center;
@ -61,10 +61,10 @@ limitations under the License.
border-top-right-radius: 10px;
background-color: rgba(255, 255, 255, 0.9);
border: 2px dashed #80cef4;
border: 2px #e1dddd solid;
border-bottom: none;
position: absolute;
top: 88px;
top: 83px;
bottom: 0px;
z-index: 3000;
}
@ -84,12 +84,12 @@ limitations under the License.
order: 2;
min-width: 0px;
max-width: 720px;
max-width: 960px;
width: 100%;
margin: auto;
overflow: auto;
border-bottom: 1px solid #a8dbf3;
border-bottom: 1px solid #eee;
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
@ -111,7 +111,7 @@ limitations under the License.
}
.mx_RoomView_messageListWrapper {
max-width: 720px;
max-width: 960px;
margin: auto;
}
@ -129,8 +129,9 @@ limitations under the License.
clear: both;
margin-top: 32px;
margin-bottom: 8px;
margin-left: 54px;
padding-bottom: 6px;
border-bottom: 1px solid #a8dbf3;
border-bottom: 1px solid #eee;
}
.mx_RoomView_invitePrompt {
@ -141,7 +142,7 @@ limitations under the License.
order: 2;
min-width: 0px;
max-width: 720px;
max-width: 960px;
width: 100%;
margin: auto;
@ -157,44 +158,45 @@ limitations under the License.
order: 4;
width: 100%;
-webkit-flex: 0 0 58px;
flex: 0 0 58px;
-webkit-flex: 0 0 36px;
flex: 0 0 36px;
}
.mx_RoomView_statusAreaBox {
max-width: 720px;
max-width: 960px;
margin: auto;
border-top: 1px solid #a8dbf3;
}
.mx_RoomView_statusAreaBox_line {
border-top: 1px solid #eee;
margin-left: 54px;
height: 1px;
}
.mx_RoomView_unreadMessagesBar {
margin-top: 13px;
color: #fff;
font-weight: bold;
background-color: #ff0064;
border-radius: 30px;
height: 30px;
line-height: 30px;
color: #ff0064;
cursor: pointer;
margin-top: 5px;
}
.mx_RoomView_unreadMessagesBar img {
padding-left: 22px;
padding-left: 10px;
padding-right: 22px;
vertical-align: middle;
}
.mx_RoomView_typingBar {
margin-top: 17px;
margin-left: 56px;
color: #818794;
margin-top: 10px;
margin-left: 54px;
color: #4a4a4a;
opacity: 0.5;
}
.mx_RoomView_typingBar img {
padding-left: 12px;
padding-right: 12px;
margin-left: -64px;
margin-top: -7px;
float: left;
.mx_RoomView_typingImage {
display: inline;
margin-left: -38px;
margin-top: -4px;
float: left;
}
.mx_RoomView .mx_MessageComposer {
@ -205,44 +207,46 @@ limitations under the License.
order: 5;
width: 100%;
-webkit-flex: 0 0 63px;
flex: 0 0 63px;
-webkit-flex: 0 0 70px;
flex: 0 0 70px;
margin-right: 2px;
}
.mx_RoomView_uploadProgressOuter {
width: 100%;
background-color: rgba(169, 219, 244, 0.5);
height: 4px;
margin-left: 54px;
margin-top: -1px;
}
.mx_RoomView_uploadProgressInner {
background-color: #80cef4;
background-color: #76cfa6;
height: 4px;
}
.mx_RoomView_uploadFilename {
margin-top: 15px;
margin-top: 5px;
margin-left: 56px;
opacity: 0.5;
color: #4a4a4a;
}
.mx_RoomView_uploadIcon {
float: left;
margin-top: 6px;
margin-left: 5px;
margin-top: 1px;
margin-left: 14px;
}
.mx_RoomView_uploadCancel {
float: right;
margin-top: 6px;
margin-top: 5px;
margin-right: 10px;
}
.mx_RoomView_uploadBytes {
float: right;
opacity: 0.5;
margin-top: 15px;
margin-right: 10px;
margin-top: 5px;
margin-right: 30px;
color: #76cfa6;
}
.mx_RoomView_ongoingConfCallNotification {
@ -251,5 +255,5 @@ limitations under the License.
background-color: #ff0064;
color: #fff;
font-weight: bold;
padding: 6px;
}
padding: 6px 0;
}

View File

@ -15,7 +15,7 @@ limitations under the License.
*/
.mx_UserSettings {
width: 720px;
width: 960px;
margin-left: auto;
margin-right: auto;
}

View File

@ -1,3 +1,22 @@
/*
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.
*/
.mx_ViewSource pre {
text-align: left;
font-size: 12px;
padding: 0.5em 1em 0.5em 1em;
word-wrap: break-word;
}

View File

@ -69,6 +69,8 @@ limitations under the License.
-webkit-order: 1;
order: 1;
background-color: #eaf5f0;
-webkit-flex: 0 0 230px;
flex: 0 0 230px;
}
@ -87,7 +89,7 @@ limitations under the License.
padding-left: 12px;
padding-right: 12px;
background-color: #f3f8fa;
background-color: #fff;
-webkit-flex: 1;
flex: 1;
@ -114,7 +116,6 @@ limitations under the License.
-webkit-order: 3;
order: 3;
background-color: #f3f8fa;
-webkit-flex: 0 0 230px;
flex: 0 0 230px;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
@font-face {
font-family: 'Myriad Pro';
font-style: normal;
font-weight: normal;
src: local('Myriad Pro'), local('MyriadPro'), url(MyriadPro-Regular.woff) format('woff');
}
@font-face {
font-family: 'Myriad Pro';
font-style: normal;
font-weight: 600;
src: local('Myriad Pro SemiBold'), local('MyriadPro-SemiBold'), url(MyriadPro-SemiBold.woff) format('woff');
}
@font-face {
font-family: 'Myriad Pro';
font-style: normal;
font-weight: bold;
src: local('Myriad Pro Bold'), local('MyriadPro-Bold'), url(MyriadPro-Bold.woff) format('woff');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 977 B

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 B

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 713 B

View File

@ -23,9 +23,6 @@ limitations under the License.
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.EnableNotificationsButton'] = require('./views/atoms/EnableNotificationsButton');
skin['atoms.ImageView'] = require('./views/atoms/ImageView');
@ -33,6 +30,9 @@ skin['atoms.LogoutButton'] = require('./views/atoms/LogoutButton');
skin['atoms.MemberAvatar'] = require('./views/atoms/MemberAvatar');
skin['atoms.MessageTimestamp'] = require('./views/atoms/MessageTimestamp');
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['molecules.BottomLeftMenu'] = require('./views/molecules/BottomLeftMenu');
skin['molecules.BottomLeftMenuTile'] = require('./views/molecules/BottomLeftMenuTile');
@ -42,18 +42,18 @@ skin['molecules.ChangePassword'] = require('./views/molecules/ChangePassword');
skin['molecules.DateSeparator'] = require('./views/molecules/DateSeparator');
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.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.MImageTile'] = require('./views/molecules/MImageTile');
skin['molecules.MNoticeTile'] = require('./views/molecules/MNoticeTile');
skin['molecules.MRoomMemberTile'] = require('./views/molecules/MRoomMemberTile');
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.MessageContextMenu'] = require('./views/molecules/MessageContextMenu');
skin['molecules.MessageTile'] = require('./views/molecules/MessageTile');
skin['molecules.ProgressBar'] = require('./views/molecules/ProgressBar');
skin['molecules.RoomCreate'] = require('./views/molecules/RoomCreate');
skin['molecules.RoomDropTarget'] = require('./views/molecules/RoomDropTarget');
@ -61,6 +61,7 @@ skin['molecules.RoomHeader'] = require('./views/molecules/RoomHeader');
skin['molecules.RoomSettings'] = require('./views/molecules/RoomSettings');
skin['molecules.RoomTile'] = require('./views/molecules/RoomTile');
skin['molecules.RoomTooltip'] = require('./views/molecules/RoomTooltip');
skin['molecules.SearchBar'] = require('./views/molecules/SearchBar');
skin['molecules.SenderProfile'] = require('./views/molecules/SenderProfile');
skin['molecules.ServerConfig'] = require('./views/molecules/ServerConfig');
skin['molecules.UnknownMessageTile'] = require('./views/molecules/UnknownMessageTile');

View File

@ -18,10 +18,19 @@ limitations under the License.
var React = require('react');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var DateUtils = require('../../../../DateUtils');
var filesize = require('filesize');
module.exports = React.createClass({
displayName: 'ImageView',
// XXX: keyboard shortcuts for managing dialogs should be done by the modal dialog base class omehow, surely...
propTypes: {
onFinished: React.PropTypes.func.isRequired
},
// XXX: keyboard shortcuts for managing dialogs should be done by the modal dialog base class somehow, surely...
componentDidMount: function() {
document.addEventListener("keydown", this.onKeyDown);
},
@ -38,10 +47,29 @@ module.exports = React.createClass({
}
},
onRedactClick: function() {
var self = this;
MatrixClientPeg.get().redactEvent(
this.props.mxEvent.getRoomId(), this.props.mxEvent.getId()
).done(function() {
if (self.props.onFinished) self.props.onFinished();
}, 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 image. (" + code + ")"
});
});
},
render: function() {
// XXX: can't we just do max-width: 80%, max-height: 80% on the CSS?
/*
// In theory max-width: 80%, max-height: 80% on the CSS should work
// but in practice, it doesn't, so do it manually:
var width = this.props.width || 500;
var height = this.props.height || 500;
@ -65,9 +93,55 @@ module.exports = React.createClass({
width: displayWidth,
height: displayHeight
};
*/
var style, res;
if (this.props.width && this.props.height) {
style = {
width: this.props.width,
height: this.props.height,
};
res = ", " + style.width + "x" + style.height + "px";
}
return (
<img className="mx_ImageView" src={this.props.src} style={style} />
<div className="mx_ImageView">
<div className="mx_ImageView_lhs">
</div>
<div className="mx_ImageView_content">
<img src={this.props.src} style={style}/>
<div className="mx_ImageView_labelWrapper">
<div className="mx_ImageView_label">
<div className="mx_ImageView_shim">
</div>
<div className="mx_ImageView_name">
{ this.props.mxEvent.getContent().body }
</div>
<div className="mx_ImageView_metadata">
Uploaded on { DateUtils.formatDate(new Date(this.props.mxEvent.getTs())) } by { this.props.mxEvent.getSender() }
</div>
<a className="mx_ImageView_link" href={ this.props.src } target="_blank">
<div className="mx_ImageView_download">
Download this file<br/>
<span className="mx_ImageView_size">({ filesize(this.props.mxEvent.getContent().info.size) }{ res })</span>
</div>
</a>
<div className="mx_ImageView_button">
<a className="mx_ImageView_link" href={ this.props.src } target="_blank">
View full screen
</a>
</div>
<div className="mx_ImageView_button" onClick={this.onRedactClick}>
Redact
</div>
<div className="mx_ImageView_shim">
</div>
</div>
</div>
</div>
<div className="mx_ImageView_rhs">
</div>
</div>
);
}
});

View File

@ -17,40 +17,16 @@ limitations under the License.
'use strict';
var React = require('react');
var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
var DateUtils = require('../../../../DateUtils');
module.exports = React.createClass({
displayName: 'MessageTimestamp',
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() {
var date = new Date(this.props.ts);
return (
<span className="mx_MessageTimestamp">
{ this.formatDate(date) }
{ DateUtils.formatDate(date) }
</span>
);
},

View File

@ -33,7 +33,7 @@ module.exports = React.createClass({
},
getFallbackAvatar: function() {
var images = [ '80cef4', '50e2c2', 'f4c371' ];
var images = [ '76cfa6', '50e2c2', 'f4c371' ];
var total = 0;
for (var i = 0; i < this.props.room.roomId.length; ++i) {
total += this.props.room.roomId.charCodeAt(i);

View File

@ -48,7 +48,7 @@ module.exports = React.createClass({
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"/>
<img src={ this.props.img } width="24" height="24"/>
</div>
{ label }
</div>

View File

@ -30,6 +30,7 @@ var eventTileTypes = {
'm.call.invite' : 'molecules.EventAsTextTile',
'm.call.answer' : 'molecules.EventAsTextTile',
'm.call.hangup' : 'molecules.EventAsTextTile',
'm.room.name' : 'molecules.EventAsTextTile',
'm.room.topic' : 'molecules.EventAsTextTile',
};
@ -88,12 +89,13 @@ module.exports = React.createClass({
mx_EventTile_highlight: this.shouldHighlight(),
mx_EventTile_continuation: this.props.continuation,
mx_EventTile_last: this.props.last,
menu: this.state.menu
mx_EventTile_contextual: this.props.contextual,
menu: this.state.menu,
});
var timestamp = <MessageTimestamp ts={this.props.mxEvent.getTs()} />
var editButton = (
<input
type="image" src="img/edit.png" alt="Edit"
type="image" src="img/edit.png" alt="Edit" width="14" height="14"
className="mx_EventTile_editButton" onClick={this.onEditClicked}
/>
);
@ -108,7 +110,7 @@ module.exports = React.createClass({
if (this.props.mxEvent.sender) {
avatar = (
<div className="mx_EventTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender} />
<MemberAvatar member={this.props.mxEvent.sender} width={24} height={24} />
</div>
);
}
@ -120,10 +122,10 @@ module.exports = React.createClass({
<div className={classes}>
{ avatar }
{ sender }
<div>
<div className="mx_EventTile_line">
{ timestamp }
{ editButton }
<EventTileType mxEvent={this.props.mxEvent} />
<EventTileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />
</div>
</div>
);

View File

@ -57,8 +57,9 @@ module.exports = React.createClass({
Modal.createDialog(ImageView, {
src: httpUrl,
width: content.info.w,
height: content.info.h
});
height: content.info.h,
mxEvent: this.props.mxEvent,
}, "mx_Dialog_lightbox");
}
},
@ -67,7 +68,7 @@ module.exports = React.createClass({
var cli = MatrixClientPeg.get();
var thumbHeight = null;
if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 320, 240);
if (content.info) thumbHeight = this.thumbHeight(content.info.w, content.info.h, 480, 360);
var imgStyle = {};
if (thumbHeight) imgStyle['height'] = thumbHeight;
@ -75,7 +76,7 @@ module.exports = React.createClass({
return (
<span className="mx_MImageTile">
<a href={cli.mxcUrlToHttp(content.url)} onClick={ this.onClick }>
<img className="mx_MImageTile_thumbnail" src={cli.mxcUrlToHttp(content.url, 320, 240)} alt={content.body} style={imgStyle} />
<img className="mx_MImageTile_thumbnail" src={cli.mxcUrlToHttp(content.url, 480, 360)} alt={content.body} style={imgStyle} />
</a>
<div className="mx_MImageTile_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank">

View File

@ -17,18 +17,71 @@ limitations under the License.
'use strict';
var React = require('react');
var sanitizeHtml = require('sanitize-html');
var MNoticeTileController = require('matrix-react-sdk/lib/controllers/molecules/MNoticeTile')
var allowedAttributes = sanitizeHtml.defaults.allowedAttributes;
allowedAttributes['font'] = ['color'];
var sanitizeHtmlParams = {
allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'font' ]),
allowedAttributes: allowedAttributes,
};
module.exports = React.createClass({
displayName: 'MNoticeTile',
mixins: [MNoticeTileController],
// FIXME: this entire class is copy-pasted from MTextTile :(
render: function() {
var content = this.props.mxEvent.getContent();
var originalBody = content.body;
var body;
if (this.props.searchTerm) {
var lastOffset = 0;
var bodyList = [];
var k = 0;
var offset;
// XXX: rather than searching for the search term in the body,
// we should be looking at the match delimiters returned by the FTS engine
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
var safeSearchTerm = sanitizeHtml(this.props.searchTerm, sanitizeHtmlParams);
while ((offset = safeBody.indexOf(safeSearchTerm, lastOffset)) >= 0) {
// FIXME: we need to apply the search highlighting to only the text elements of HTML, which means
// hooking into the sanitizer parser rather than treating it as a string. Otherwise
// the act of highlighting a <b/> or whatever will break the HTML badly.
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset, offset) }} />);
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSearchTerm }} className="mx_MessageTile_searchHighlight" />);
lastOffset = offset + safeSearchTerm.length;
}
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset) }} />);
}
else {
while ((offset = originalBody.indexOf(this.props.searchTerm, lastOffset)) >= 0) {
bodyList.push(<span key={ k++ } >{ originalBody.substring(lastOffset, offset) }</span>);
bodyList.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ this.props.searchTerm }</span>);
lastOffset = offset + this.props.searchTerm.length;
}
bodyList.push(<span key={ k++ }>{ originalBody.substring(lastOffset) }</span>);
}
body = bodyList;
}
else {
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
body = <span dangerouslySetInnerHTML={{ __html: safeBody }} />;
}
else {
body = originalBody;
}
}
return (
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
{content.body}
{ body }
</span>
);
},

View File

@ -17,18 +17,71 @@ limitations under the License.
'use strict';
var React = require('react');
var sanitizeHtml = require('sanitize-html');
var MTextTileController = require('matrix-react-sdk/lib/controllers/molecules/MTextTile')
var allowedAttributes = sanitizeHtml.defaults.allowedAttributes;
allowedAttributes['font'] = ['color'];
var sanitizeHtmlParams = {
allowedTags: sanitizeHtml.defaults.allowedTags.concat([ 'font' ]),
allowedAttributes: allowedAttributes,
};
module.exports = React.createClass({
displayName: 'MTextTile',
mixins: [MTextTileController],
// FIXME: this entire class is copy-pasted from MTextTile :(
render: function() {
var content = this.props.mxEvent.getContent();
var originalBody = content.body;
var body;
if (this.props.searchTerm) {
var lastOffset = 0;
var bodyList = [];
var k = 0;
var offset;
// XXX: rather than searching for the search term in the body,
// we should be looking at the match delimiters returned by the FTS engine
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
var safeSearchTerm = sanitizeHtml(this.props.searchTerm, sanitizeHtmlParams);
while ((offset = safeBody.indexOf(safeSearchTerm, lastOffset)) >= 0) {
// FIXME: we need to apply the search highlighting to only the text elements of HTML, which means
// hooking into the sanitizer parser rather than treating it as a string. Otherwise
// the act of highlighting a <b/> or whatever will break the HTML badly.
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset, offset) }} />);
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeSearchTerm }} className="mx_MessageTile_searchHighlight" />);
lastOffset = offset + safeSearchTerm.length;
}
bodyList.push(<span key={ k++ } dangerouslySetInnerHTML={{ __html: safeBody.substring(lastOffset) }} />);
}
else {
while ((offset = originalBody.indexOf(this.props.searchTerm, lastOffset)) >= 0) {
bodyList.push(<span key={ k++ } >{ originalBody.substring(lastOffset, offset) }</span>);
bodyList.push(<span key={ k++ } className="mx_MessageTile_searchHighlight">{ this.props.searchTerm }</span>);
lastOffset = offset + this.props.searchTerm.length;
}
bodyList.push(<span key={ k++ }>{ originalBody.substring(lastOffset) }</span>);
}
body = bodyList;
}
else {
if (content.format === "org.matrix.custom.html") {
var safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
body = <span dangerouslySetInnerHTML={{ __html: safeBody }} />;
}
else {
body = originalBody;
}
}
return (
<span ref="content" className="mx_MTextTile mx_MessageTile_content">
{content.body}
{ body }
</span>
);
},

View File

@ -20,19 +20,30 @@ var React = require('react');
var Loader = require("../atoms/Spinner");
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var MemberInfoController = require('matrix-react-sdk/lib/controllers/molecules/MemberInfo')
// FIXME: this should probably be an organism, to match with MemberList, not a molecule
module.exports = React.createClass({
displayName: 'MemberInfo',
mixins: [MemberInfoController],
onCancel: function(e) {
dis.dispatch({
action: "view_user",
member: null
});
},
render: function() {
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner;
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_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>;
}
else {
interactButton = <div className="mx_ContextualMenu_field" onClick={this.onChatClick}>Start chat</div>;
interactButton = <div className="mx_MemberInfo_field" onClick={this.onChatClick}>Start chat</div>;
}
if (this.state.creatingRoom) {
@ -40,36 +51,50 @@ module.exports = React.createClass({
}
if (this.state.can.kick) {
kickButton = <div className="mx_ContextualMenu_field" onClick={this.onKick}>
kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
Kick
</div>;
}
if (this.state.can.ban) {
banButton = <div className="mx_ContextualMenu_field" onClick={this.onBan}>
banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
Ban
</div>;
}
if (this.state.can.mute) {
var muteLabel = this.state.muted ? "Unmute" : "Mute";
muteButton = <div className="mx_ContextualMenu_field" onClick={this.onMuteToggle}>
muteButton = <div className="mx_MemberInfo_field" onClick={this.onMuteToggle}>
{muteLabel}
</div>;
}
if (this.state.can.modifyLevel) {
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod";
giveModButton = <div className="mx_ContextualMenu_field" onClick={this.onModToggle}>
giveModButton = <div className="mx_MemberInfo_field" onClick={this.onModToggle}>
{giveOpLabel}
</div>
}
var MemberAvatar = sdk.getComponent('atoms.MemberAvatar');
return (
<div>
{interactButton}
{muteButton}
{kickButton}
{banButton}
{giveModButton}
{spinner}
<div className="mx_MemberInfo">
<img className="mx_MemberInfo_cancel" src="img/cancel-black.png" width="18" height="18" onClick={this.onCancel}/>
<div className="mx_MemberInfo_avatar">
<MemberAvatar member={this.props.member} width={48} height={48} />
</div>
<h2>{ this.props.member.name }</h2>
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
<div className="mx_MemberInfo_profileField">
power: { this.props.member.powerLevelNorm }%
</div>
<div className="mx_MemberInfo_buttons">
{interactButton}
{muteButton}
{kickButton}
{banButton}
{giveModButton}
{spinner}
</div>
</div>
);
}

View File

@ -20,7 +20,7 @@ var React = require('react');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var sdk = require('matrix-react-sdk')
var ContextualMenu = require('../../../../ContextualMenu');
var dis = require('matrix-react-sdk/lib/dispatcher');
var MemberTileController = require('matrix-react-sdk/lib/controllers/molecules/MemberTile')
// The Lato WOFF doesn't include sensible combining diacritics, so Chrome chokes on rendering them.
@ -58,16 +58,9 @@ module.exports = React.createClass({
},
onClick: function(e) {
var self = this;
self.setState({ 'menu': true });
var MemberInfo = sdk.getComponent('molecules.MemberInfo');
ContextualMenu.createMenu(MemberInfo, {
member: self.props.member,
right: window.innerWidth - e.pageX,
top: e.pageY,
onFinished: function() {
self.setState({ 'menu': false });
}
dis.dispatch({
action: 'view_user',
member: this.props.member,
});
},
@ -119,10 +112,10 @@ module.exports = React.createClass({
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
var power;
if (this.props.member && this.props.member.powerLevelNorm > 0) {
var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
power = <img src={ img } className="mx_MemberTile_power" width="48" height="48" alt=""/>;
}
// if (this.props.member && this.props.member.powerLevelNorm > 0) {
// var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png";
// power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
// }
var presenceClass = "mx_MemberTile_offline";
var mainClassName = "mx_MemberTile ";
if (this.props.member.user) {
@ -134,13 +127,13 @@ module.exports = React.createClass({
}
}
mainClassName += presenceClass;
if (this.state.hover || this.state.menu) {
if (this.state.hover) {
mainClassName += " mx_MemberTile_hover";
}
var name = this.props.member.name;
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain
var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
//var leave = isMyUser ? <img className="mx_MemberTile_leave" src="img/delete.png" width="10" height="10" onClick={this.onLeaveClick}/> : null;
var nameClass = "mx_MemberTile_name";
if (zalgo.test(name)) {
@ -148,7 +141,7 @@ module.exports = React.createClass({
}
var nameEl;
if (this.state.hover || this.state.menu) {
if (this.state.hover) {
var presence;
// FIXME: make presence data update whenever User.presence changes...
var active = this.props.member.user ? ((Date.now() - (this.props.member.user.lastPresenceTs - this.props.member.user.lastActiveAgo)) || -1) : -1;
@ -161,8 +154,8 @@ module.exports = React.createClass({
nameEl =
<div className="mx_MemberTile_details">
{ leave }
<div className="mx_MemberTile_userId">{ this.props.member.userId }</div>
<img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_MemberTile_userId">{ name }</div>
{ presence }
</div>
}
@ -177,7 +170,7 @@ module.exports = React.createClass({
return (
<div className={mainClassName} title={ this.getPowerLabel() } onClick={ this.onClick } onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }>
<div className="mx_MemberTile_avatar">
<MemberAvatar member={this.props.member} />
<MemberAvatar member={this.props.member} width={36} height={36} />
{ power }
</div>
{ nameEl }

View File

@ -22,6 +22,7 @@ var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var MessageComposerController = require('matrix-react-sdk/lib/controllers/molecules/MessageComposer')
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher')
module.exports = React.createClass({
displayName: 'MessageComposer',
@ -40,6 +41,14 @@ module.exports = React.createClass({
this.refs.uploadInput.getDOMNode().value = null;
},
onCallClick: function(ev) {
dis.dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId
});
},
render: function() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'};
@ -49,15 +58,18 @@ module.exports = React.createClass({
<div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row">
<div className="mx_MessageComposer_avatar">
<MemberAvatar member={me} />
<MemberAvatar member={me} width={24} height={24} />
</div>
<div className="mx_MessageComposer_input">
<textarea ref="textarea" onKeyDown={this.onKeyDown} placeholder="Type a message" />
<textarea ref="textarea" onKeyDown={this.onKeyDown} placeholder="Type a message..." />
</div>
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick}>
<img src="img/upload.png" width="32" height="32"/>
<img src="img/upload.png" width="17" height="22"/>
<input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
</div>
<div className="mx_MessageComposer_call" onClick={this.onCallClick}>
<img src="img/call.png" width="28" height="20"/>
</div>
</div>
</div>
</div>

View File

@ -84,7 +84,7 @@ module.exports = React.createClass({
else {
redactButton = (
<div className="mx_ContextualMenu_field" onClick={this.onRedactClick}>
Delete
Redact
</div>
);
}

View File

@ -50,6 +50,6 @@ module.exports = React.createClass({
TileType = tileTypes[msgtype];
}
return <TileType mxEvent={this.props.mxEvent} />;
return <TileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />;
},
});

View File

@ -41,7 +41,7 @@ module.exports = React.createClass({
onFullscreenClick: function() {
dis.dispatch({action: 'video_fullscreen', fullscreen: true}, true);
},
render: function() {
var EditableText = sdk.getComponent("atoms.EditableText");
var RoomAvatar = sdk.getComponent('atoms.RoomAvatar');
@ -59,7 +59,6 @@ module.exports = React.createClass({
var topic = this.props.room.currentState.getStateEvents('m.room.topic', '');
var call_buttons;
var zoom_button;
if (this.state && this.state.call_state != 'ended') {
//var muteVideoButton;
var activeCall = (
@ -111,16 +110,15 @@ module.exports = React.createClass({
cancel_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onCancelClick}>Cancel</div>
save_button = <div className="mx_RoomHeader_textButton" onClick={this.props.onSaveClick}>Save Changes</div>
} else {
// <EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
name =
<div className="mx_RoomHeader_name">
<EditableText label={this.props.room.name} initialValue={actual_name} placeHolder="Name" onValueChanged={this.onNameChange} />
<div className="mx_RoomHeader_name" onClick={this.props.onSettingsClick}>
<div className="mx_RoomHeader_nametext">{ this.props.room.name }</div>
<div className="mx_RoomHeader_settingsButton">
<img src="img/settings.png" width="12" height="12"/>
</div>
</div>
if (topic) topic_el = <div className="mx_RoomHeader_topic" title={topic.getContent().topic}>{ topic.getContent().topic }</div>;
settings_button = (
<div className="mx_RoomHeader_button" onClick={this.props.onSettingsClick}>
<img src="img/settings.png" width="32" height="32"/>
</div>
);
}
var roomAvatar = null;
@ -130,12 +128,23 @@ module.exports = React.createClass({
);
}
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>
);
var zoom_button, video_button, voice_button;
if (activeCall) {
if (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': '-5px' }}/>
</div>
);
}
video_button =
<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" style={{ 'marginTop': '-8px' }}/>
</div>;
voice_button =
<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" style={{ 'marginTop': '-8px' }}/>
</div>;
}
header =
@ -153,16 +162,11 @@ module.exports = React.createClass({
{cancel_button}
{save_button}
<div className="mx_RoomHeader_rightRow">
{ settings_button }
{ video_button }
{ voice_button }
{ zoom_button }
<div className="mx_RoomHeader_button mx_RoomHeader_search">
<img src="img/search.png" title="Search" alt="Search" width="32" height="32"/>
</div>
<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"/>
</div>
<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"/>
<div className="mx_RoomHeader_button">
<img src="img/search.png" title="Search" alt="Search" width="21" height="19" onClick={this.props.onSearchClick}/>
</div>
</div>
</div>

View File

@ -50,7 +50,16 @@ module.exports = React.createClass({
'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': this.props.room.currentState.members[myUserId].membership == 'invite'
});
var name = this.props.room.name.replace(":", ":\u200b");
var name;
if (this.props.isInvite) {
name = this.props.room.getMember(MatrixClientPeg.get().credentials.userId).events.member.getSender();
}
else {
name = this.props.room.name;
}
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
var badge;
if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge"/>;
@ -73,7 +82,8 @@ module.exports = React.createClass({
var label;
if (!this.props.collapsed) {
label = <div className="mx_RoomTile_name">{name}</div>;
var className = 'mx_RoomTile_name' + (this.props.isInvite ? ' mx_RoomTile_invite' : '');
label = <div className={ className }>{name}</div>;
}
else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("molecules.RoomTooltip");
@ -84,7 +94,7 @@ module.exports = React.createClass({
return (
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<div className="mx_RoomTile_avatar">
<RoomAvatar room={this.props.room} />
<RoomAvatar room={this.props.room} width="24" height="24" />
{ badge }
</div>
{ label }

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 MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var sdk = require('matrix-react-sdk');
module.exports = React.createClass({
displayName: 'SearchBar',
getInitialState: function() {
return ({
scope: 'Room'
});
},
onThisRoomClick: function() {
this.setState({ scope: 'Room' });
},
onAllRoomsClick: function() {
this.setState({ scope: 'All' });
},
onSearchChange: function(e) {
if (e.keyCode === 13) { // on enter...
this.props.onSearch(this.refs.search_term.getDOMNode().value, this.state.scope);
}
},
render: function() {
return (
<div className="mx_SearchBar">
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder="Search..." onKeyDown={this.onSearchChange}/>
<div className={"mx_SearchBar_button" + (this.state.scope !== 'Room' ? " mx_SearchBar_unselected" : "")} onClick={this.onThisRoomClick}>This Room</div>
<div className={"mx_SearchBar_button" + (this.state.scope !== 'All' ? " mx_SearchBar_unselected" : "")} onClick={this.onAllRoomsClick}>All Rooms</div>
<img className="mx_SearchBar_cancel" src="img/cancel-black.png" width="18" height="18" onClick={this.props.onCancelClick} />
</div>
);
}
});

View File

@ -40,7 +40,8 @@ module.exports = React.createClass({
classes += " collapsed";
}
else {
collapseButton = <img className="mx_LeftPanel_hideButton" onClick={ this.onHideClick } src="img/hide.png" width="12" height="20" alt="<"/>
// Hide the collapse button until we work out how to display it in the new skin
// collapseButton = <img className="mx_LeftPanel_hideButton" onClick={ this.onHideClick } src="img/hide.png" width="12" height="20" alt="<"/>
}
return (

View File

@ -30,7 +30,6 @@ module.exports = React.createClass({
mixins: [MemberListController],
getInitialState: function() {
return { editing: false };
},
memberSort: function(userIdA, userIdB) {
@ -71,43 +70,21 @@ module.exports = React.createClass({
});
},
onPopulateInvite: function(inputText, shouldSubmit) {
// reset back to placeholder
this.refs.invite.setValue("Invite", false, true);
this.setState({ editing: false });
if (!shouldSubmit) {
return; // enter key wasn't pressed
}
this.onInvite(inputText);
},
onClickInvite: function(ev) {
this.setState({ editing: true });
this.refs.invite.onClickDiv();
ev.stopPropagation();
ev.preventDefault();
onPopulateInvite: function(e) {
this.onInvite(this.refs.invite.getDOMNode().value);
e.preventDefault();
},
inviteTile: function() {
var classes = classNames({
mx_MemberTile: true,
mx_MemberTile_inviteTile: true,
mx_MemberTile_inviteEditing: this.state.editing,
});
var EditableText = sdk.getComponent("atoms.EditableText");
if (this.state.inviting) {
return (
<Loader />
);
} 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>
<form onSubmit={this.onPopulateInvite}>
<input className="mx_MemberList_invite" ref="invite" placeholder="Invite another user"/>
</form>
);
}
},
@ -117,7 +94,7 @@ module.exports = React.createClass({
var invitedMemberTiles = this.makeMemberTiles('invite');
if (invitedMemberTiles.length > 0) {
invitedSection = (
<div>
<div className="mx_MemberList_invited">
<h2>Invited</h2>
<div className="mx_MemberList_wrapper">
{invitedMemberTiles}
@ -127,18 +104,14 @@ module.exports = React.createClass({
}
return (
<div className="mx_MemberList">
<div className="mx_MemberList_chevron">
<img src="img/chevron.png" width="24" height="13"/>
</div>
<div className="mx_MemberList_border">
{this.inviteTile()}
<div>
<h2>Members</h2>
<div className="mx_MemberList_wrapper">
{this.makeMemberTiles('join')}
</div>
</div>
{invitedSection}
{this.inviteTile()}
</div>
</div>
);

View File

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react');
var sdk = require('matrix-react-sdk')
var dis = require('matrix-react-sdk/lib/dispatcher');
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
module.exports = React.createClass({
displayName: 'RightPanel',
@ -26,6 +27,20 @@ module.exports = React.createClass({
Phase : {
MemberList: 'MemberList',
FileList: 'FileList',
MemberInfo: 'MemberInfo',
},
componentWillMount: function() {
this.dispatcherRef = dis.register(this.onAction);
var cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember);
}
},
getInitialState: function() {
@ -48,25 +63,85 @@ module.exports = React.createClass({
}
},
onRoomStateMember: function(ev, state, member) {
// redraw the badge on the membership list
if (this.state.phase == this.Phase.MemberList && member.roomId === this.props.roomId) {
this.forceUpdate();
}
},
onAction: function(payload) {
if (payload.action === "view_user") {
if (payload.member) {
this.setState({
phase: this.Phase.MemberInfo,
member: payload.member,
});
}
else {
this.setState({
phase: this.Phase.MemberList
});
}
}
if (payload.action === "view_room") {
if (this.state.phase === this.Phase.MemberInfo) {
this.setState({
phase: this.Phase.MemberList
});
}
}
},
render: function() {
var MemberList = sdk.getComponent('organisms.MemberList');
var buttonGroup;
var panel;
var filesHighlight;
var membersHighlight;
if (!this.props.collapsed) {
if (this.state.phase == this.Phase.MemberList || this.state.phase === this.Phase.MemberInfo) {
membersHighlight = <div className="mx_RightPanel_headerButton_highlight"></div>;
}
else if (this.state.phase == this.Phase.FileList) {
filesHighlight = <div className="mx_RightPanel_headerButton_highlight"></div>;
}
}
var membersBadge;
if ((this.state.phase == this.Phase.MemberList || this.state.phase === this.Phase.MemberInfo) && this.props.roomId) {
var cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId);
if (room) {
membersBadge = <div className="mx_RightPanel_headerButton_badge">{ room.getJoinedMembers().length }</div>;
}
}
if (this.props.roomId) {
buttonGroup =
<div className="mx_RightPanel_headerButtonGroup">
<div className="mx_RightPanel_headerButton mx_RightPanel_filebutton">
<img src="img/file.png" width="32" height="32" title="Files" alt="Files"/>
</div>
<div className="mx_RightPanel_headerButton" onClick={ this.onMemberListButtonClick }>
<img src="img/members.png" width="32" height="32" title="Members" alt="Members"/>
<img src="img/members.png" width="17" height="22" title="Members" alt="Members"/>
{ membersBadge }
{ membersHighlight }
</div>
<div className="mx_RightPanel_headerButton mx_RightPanel_filebutton">
<img src="img/files.png" width="17" height="22" title="Files" alt="Files"/>
{ filesHighlight }
</div>
</div>;
if (!this.props.collapsed && this.state.phase == this.Phase.MemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />
if (!this.props.collapsed) {
if(this.state.phase == this.Phase.MemberList) {
panel = <MemberList roomId={this.props.roomId} key={this.props.roomId} />
}
else if(this.state.phase == this.Phase.MemberInfo) {
var MemberInfo = sdk.getComponent('molecules.MemberInfo');
panel = <MemberInfo roomId={this.props.roomId} member={this.state.member} key={this.props.roomId} />
}
}
}
var classes = "mx_RightPanel";

View File

@ -69,6 +69,7 @@ module.exports = React.createClass({
});
}, function(err) {
console.error("Failed to join room: %s", JSON.stringify(err));
var ErrorDialog = sdk.getComponent("organisms.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to join room",
description: err.message

View File

@ -41,22 +41,38 @@ module.exports = React.createClass({
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";
var expandButton = this.props.collapsed ?
<img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> :
null;
var invitesLabel = this.props.collapsed ? null : "Invites";
var recentsLabel = this.props.collapsed ? null : "Recent";
var invites;
if (this.state.inviteList.length) {
invites = <div>
<h2 className="mx_RoomList_invitesLabel">{ invitesLabel }</h2>
<div className="mx_RoomList_invites">
{this.makeRoomTiles(this.state.inviteList, true)}
</div>
</div>
}
return (
<div className="mx_RoomList" onScroll={this._repositionTooltip}>
{callElement}
<h2 className="mx_RoomList_favourites_label">Favourites</h2>
{ expandButton }
{ callElement }
<h2 className="mx_RoomList_favouritesLabel">Favourites</h2>
<RoomDropTarget text="Drop here to favourite"/>
<h2 className="mx_RoomList_recents_label">{ recentsLabel }</h2>
{ invites }
<h2 className="mx_RoomList_recentsLabel">{ recentsLabel }</h2>
<div className="mx_RoomList_recents">
{this.makeRoomTiles()}
{this.makeRoomTiles(this.state.roomList, false)}
</div>
<h2 className="mx_RoomList_archive_label">Archive</h2>
<h2 className="mx_RoomList_archiveLabel">Archive</h2>
<RoomDropTarget text="Drop here to archive"/>
</div>
);

View File

@ -63,6 +63,10 @@ module.exports = React.createClass({
this.setState(this.getInitialState());
},
onSearchClick: function() {
this.setState({ searching: true });
},
onConferenceNotificationClick: function() {
dis.dispatch({
action: 'place_call',
@ -89,6 +93,7 @@ module.exports = React.createClass({
var MessageComposer = sdk.getComponent('molecules.MessageComposer');
var CallView = sdk.getComponent("molecules.voip.CallView");
var RoomSettings = sdk.getComponent("molecules.RoomSettings");
var SearchBar = sdk.getComponent("molecules.SearchBar");
if (!this.state.room) {
if (this.props.roomId) {
@ -159,8 +164,8 @@ module.exports = React.createClass({
<div className="mx_RoomView_uploadProgressOuter">
<div className="mx_RoomView_uploadProgressInner" style={innerProgressStyle}></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"/>
<img className="mx_RoomView_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
<img className="mx_RoomView_uploadCancel" src="img/cancel.png" width="18" height="18"/>
<div className="mx_RoomView_uploadBytes">
{ uploadedSize } / { totalSize }
</div>
@ -175,7 +180,7 @@ module.exports = React.createClass({
if (unreadMsgs) {
statusBar = (
<div className="mx_RoomView_unreadMessagesBar" onClick={ this.scrollToBottom }>
<img src="img/newmessages.png" width="10" height="12" alt=""/>
<img src="img/newmessages.png" width="24" height="24" alt=""/>
{unreadMsgs}
</div>
);
@ -183,19 +188,22 @@ module.exports = React.createClass({
else if (typingString) {
statusBar = (
<div className="mx_RoomView_typingBar">
<img src="img/typing.png" width="40" height="40" alt=""/>
<div className="mx_RoomView_typingImage">...</div>
{typingString}
</div>
);
}
}
var roomEdit = null;
var aux = null;
if (this.state.editingRoomSettings) {
roomEdit = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} room={this.state.room} />;
aux = <RoomSettings ref="room_settings" onSaveClick={this.onSaveClick} room={this.state.room} />;
}
if (this.state.uploadingRoomSettings) {
roomEdit = <Loader/>;
else if (this.state.uploadingRoomSettings) {
aux = <Loader/>;
}
else if (this.state.searching) {
aux = <SearchBar ref="search_bar" onCancelClick={this.onCancelClick} onSearch={this.onSearch}/>;
}
var conferenceCallNotification = null;
@ -211,7 +219,7 @@ module.exports = React.createClass({
if (this.state.draggingFile) {
fileDropTarget = <div className="mx_RoomView_fileDropTarget">
<div className="mx_RoomView_fileDropTargetLabel">
<img src="img/upload-big.png" width="46" height="61" alt="Drop File Here"/><br/>
<img src="img/upload-big.png" width="43" height="57" alt="Drop File Here"/><br/>
Drop File Here
</div>
</div>;
@ -219,12 +227,12 @@ module.exports = React.createClass({
return (
<div className="mx_RoomView">
<RoomHeader ref="header" room={this.state.room} editing={this.state.editingRoomSettings}
<RoomHeader ref="header" room={this.state.room} editing={this.state.editingRoomSettings} onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} />
<div className="mx_RoomView_auxPanel">
<CallView room={this.state.room}/>
{ conferenceCallNotification }
{ roomEdit }
{ aux }
</div>
<div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
<div className="mx_RoomView_messageListWrapper">
@ -238,6 +246,7 @@ module.exports = React.createClass({
</div>
<div className="mx_RoomView_statusArea">
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{statusBar}
</div>
</div>

View File

@ -21,6 +21,26 @@ var React = require('react');
module.exports = React.createClass({
displayName: 'ViewSource',
propTypes: {
onFinished: React.PropTypes.func.isRequired
},
componentDidMount: function() {
document.addEventListener("keydown", this.onKeyDown);
},
componentWillUnmount: function() {
document.removeEventListener("keydown", this.onKeyDown);
},
onKeyDown: function(ev) {
if (ev.keyCode == 27) { // escape
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
},
render: function() {
return (
<div className="mx_ViewSource">

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>Vector</title>
<link href='fonts/Lato.css' rel='stylesheet' type='text/css'>
<link href='fonts/MyriadPro.css' rel='stylesheet' type='text/css'>
<link rel="apple-touch-icon" sizes="57x57" href="/icons/apple-touch-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/icons/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/icons/apple-touch-icon-72x72.png">