Merge branch 'develop' into kegan/reject-invites

pull/298/head
Kegan Dougal 2015-11-02 09:47:51 +00:00
commit 38780ad492
92 changed files with 1466 additions and 421 deletions

4
.gitignore vendored
View File

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

14
.modernizr.json Normal file
View File

@ -0,0 +1,14 @@
{
"minify": true,
"classPrefix": "modernizr_",
"options": [
"setClasses"
],
"feature-detects": [
"test/css/displaytable",
"test/css/flexbox",
"test/es5/specification",
"test/css/objectfit",
"test/storage/localstorage"
]
}

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 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` 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 the `develop` branches of these libraries and then use `npm link` to tell Vector
about them: about them:

View File

@ -11,6 +11,7 @@
"style": "bundle.css", "style": "bundle.css",
"scripts": { "scripts": {
"reskindex": "reskindex vector -h src/skins/vector/header", "reskindex": "reskindex vector -h src/skins/vector/header",
"build:modernizr": "modernizr -c .modernizr.json -d src/vector/modernizr.js",
"build:css": "catw \"src/skins/vector/css/**/*.css\" -o vector/bundle.css -c uglifycss --no-watch", "build:css": "catw \"src/skins/vector/css/**/*.css\" -o vector/bundle.css -c uglifycss --no-watch",
"build:compile": "babel --source-maps -d lib src", "build:compile": "babel --source-maps -d lib src",
"build:bundle": "NODE_ENV=production webpack -p lib/vector/index.js vector/bundle.js", "build:bundle": "NODE_ENV=production webpack -p lib/vector/index.js vector/bundle.js",
@ -27,11 +28,13 @@
"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",
"modernizr": "^3.1.0",
"matrix-js-sdk": "^0.3.0", "matrix-js-sdk": "^0.3.0",
"matrix-react-sdk": "^0.0.2", "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",
"sanitize-html": "^1.11.1"
}, },
"devDependencies": { "devDependencies": {
"babel": "^5.8.23", "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("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName); cli.on("RoomMember.name", this.onRoomMemberName);
var rooms = this.getRoomList(); var s = this.getRoomLists();
this.setState({ s.activityMap = {};
roomList: rooms, this.setState(s);
activityMap: {}
});
}, },
componentDidMount: function() { componentDidMount: function() {
@ -87,9 +85,7 @@ module.exports = {
onRoomTimeline: function(ev, room, toStartOfTimeline) { onRoomTimeline: function(ev, room, toStartOfTimeline) {
if (toStartOfTimeline) return; if (toStartOfTimeline) return;
var newState = { var newState = this.getRoomLists();
roomList: this.getRoomList()
};
if ( if (
room.roomId != this.props.selectedRoom && room.roomId != this.props.selectedRoom &&
ev.getSender() != MatrixClientPeg.get().credentials.userId) ev.getSender() != MatrixClientPeg.get().credentials.userId)
@ -123,18 +119,23 @@ module.exports = {
refreshRoomList: function() { refreshRoomList: function() {
var rooms = this.getRoomList(); this.setState(this.getRoomLists());
this.setState({
roomList: rooms
});
}, },
getRoomList: function() { getRoomLists: function() {
return RoomListSorter.mostRecentActivityFirst( var s = {};
var inviteList = [];
s.roomList = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms().filter(function(room) { MatrixClientPeg.get().getRooms().filter(function(room) {
var me = room.getMember(MatrixClientPeg.get().credentials.userId); var me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me && me.membership == "invite") {
inviteList.push(room);
return false;
}
var shouldShowRoom = ( var shouldShowRoom = (
me && (me.membership == "join" || me.membership == "invite") me && (me.membership == "join")
); );
// hiding conf rooms only ever toggles shouldShowRoom to false // hiding conf rooms only ever toggles shouldShowRoom to false
if (shouldShowRoom && HIDE_CONFERENCE_CHANS) { if (shouldShowRoom && HIDE_CONFERENCE_CHANS) {
@ -153,6 +154,8 @@ module.exports = {
return shouldShowRoom; return shouldShowRoom;
}) })
); );
s.inviteList = RoomListSorter.mostRecentActivityFirst(inviteList);
return s;
}, },
_recheckCallElement: function(selectedRoomId) { _recheckCallElement: function(selectedRoomId) {
@ -174,10 +177,10 @@ module.exports = {
} }
}, },
makeRoomTiles: function() { makeRoomTiles: function(list, isInvite) {
var self = this; var self = this;
var RoomTile = sdk.getComponent("molecules.RoomTile"); 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; var selected = room.roomId == self.props.selectedRoom;
return ( return (
<RoomTile <RoomTile
@ -187,6 +190,7 @@ module.exports = {
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}
isInvite={isInvite}
/> />
); );
}); });

View File

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
var Matrix = require("matrix-js-sdk");
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg"); var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
var React = require("react"); var React = require("react");
var q = require("q"); var q = require("q");
@ -38,6 +39,8 @@ module.exports = {
uploadingRoomSettings: false, uploadingRoomSettings: false,
numUnreadMessages: 0, numUnreadMessages: 0,
draggingFile: false, draggingFile: false,
searching: false,
searchResults: null,
} }
}, },
@ -356,6 +359,41 @@ module.exports = {
return WhoIsTyping.whoIsTypingString(this.state.room); 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() { getEventTiles: function() {
var DateSeparator = sdk.getComponent('molecules.DateSeparator'); var DateSeparator = sdk.getComponent('molecules.DateSeparator');
@ -364,6 +402,36 @@ module.exports = {
var EventTile = sdk.getComponent('molecules.EventTile'); 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) { for (var i = this.state.room.timeline.length-1; i >= 0 && count < this.state.messageCap; --i) {
var mxEv = this.state.room.timeline[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 { .mx_MemberAvatar {
z-index: 20; z-index: 20;
border-radius: 20px; border-radius: 20px;
background-color: #dbdbdb;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,3 +14,49 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { .mx_MemberTile {
display: table-row; display: table-row;
height: 49px;
position: relative; position: relative;
color: #454545;
cursor: pointer;
} }
.mx_MemberTile_avatar { .mx_MemberTile_avatar {
display: table-cell; display: table-cell;
padding-left: 14px; padding-left: 3px;
padding-right: 12px; padding-right: 12px;
padding-top: 3px; padding-top: 2px;
padding-bottom: 3px; padding-bottom: 0px;
vertical-align: middle; vertical-align: middle;
width: 40px; width: 36px;
height: 40px; height: 36px;
position: relative; 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 { .mx_MemberTile_power {
position: absolute; position: absolute;
width: 48px; width: 44px;
height: 48px; height: 44px;
left: 10px; left: 10px;
top: -1px; top: -1px;
} }
@ -79,20 +54,18 @@ limitations under the License.
vertical-align: middle; vertical-align: middle;
} }
.mx_MemberTile_hover {
background-color: #f0f0f0;
font-size: 12px;
color: #747474;
}
.mx_MemberTile_userId { .mx_MemberTile_userId {
font-weight: bold; font-size: 14px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.mx_MemberTile_leave { .mx_MemberTile_presence {
cursor: pointer; font-size: 12px;
opacity: 0.5;
}
.mx_MemberTile_chevron {
margin-top: 8px; margin-top: 8px;
margin-right: -4px; margin-right: -4px;
margin-left: 6px; margin-left: 6px;
@ -113,14 +86,14 @@ limitations under the License.
.mx_MemberTile_unavailable .mx_MemberTile_avatar, .mx_MemberTile_unavailable .mx_MemberTile_avatar,
.mx_MemberTile_unavailable .mx_MemberTile_name, .mx_MemberTile_unavailable .mx_MemberTile_name,
.mx_MemberTile_unavailable .mx_MemberTile_nameSpan .mx_MemberTile_unavailable .mx_MemberTile_userId
{ {
opacity: 0.66; opacity: 0.66;
} }
.mx_MemberTile_offline .mx_MemberTile_avatar, .mx_MemberTile_offline .mx_MemberTile_avatar,
.mx_MemberTile_offline .mx_MemberTile_name, .mx_MemberTile_offline .mx_MemberTile_name,
.mx_MemberTile_offline .mx_MemberTile_nameSpan .mx_MemberTile_offline .mx_MemberTile_userId
{ {
opacity: 0.25; opacity: 0.25;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ limitations under the License.
.mx_RoomTooltip { .mx_RoomTooltip {
display: none; display: none;
position: fixed; position: fixed;
border: 1px solid #a9dbf4; border: 1px solid #a4a4a4;
border-radius: 8px; border-radius: 8px;
background-color: #fff; background-color: #fff;
z-index: 1000; 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 { .mx_IncomingCallBox {
text-align: center; text-align: center;
border: 1px solid #a9dbf4; border: 1px solid #a4a4a4;
border-radius: 8px; border-radius: 8px;
background-color: #fff; background-color: #fff;
position: absolute; position: absolute;

View File

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

View File

@ -53,17 +53,20 @@ limitations under the License.
-webkit-order: 3; -webkit-order: 3;
order: 3; order: 3;
-webkit-flex: 0 0 170px; -webkit-flex: 0 0 126px;
flex: 0 0 170px; flex: 0 0 126px;
border-top: 1px solid #f3f8fa;
} }
.mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile { .mx_LeftPanel .mx_BottomLeftMenu .mx_RoomTile {
color: #378bb4; color: #454545;
} }
.mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options { .mx_LeftPanel .mx_BottomLeftMenu .mx_BottomLeftMenu_options {
margin-top: 12px; margin-top: 12px;
width: 100%; 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 { .mx_MemberList {
height: 100%; height: 100%;
margin-bottom: 100px;
padding: 8px;
-webkit-flex: 1; -webkit-flex: 1;
flex: 1; flex: 1;
@ -39,22 +37,47 @@ limitations under the License.
} }
.mx_MemberList_border { .mx_MemberList_border {
border: 1px solid #a9dbf4;
overflow-y: auto; overflow-y: auto;
border-radius: 8px;
background-color: #fff;
order: 1; order: 1;
-webkit-flex: 1 1 0; -webkit-flex: 1 1 0;
flex: 1 1 0px; 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 { .mx_MemberList_wrapper {
display: table; display: table;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
} }
.mx_MemberList h2 {
margin: 14px;
}

View File

@ -33,32 +33,53 @@ limitations under the License.
-webkit-order: 1; -webkit-order: 1;
order: 1; order: 1;
-webkit-flex: 0 0 66px; -webkit-flex: 0 0 83px;
flex: 0 0 66px; flex: 0 0 83px;
} }
/** Fixme - factor this out with the main header **/ /** Fixme - factor this out with the main header **/
.mx_RightPanel_headerButtonGroup { .mx_RightPanel_headerButtonGroup {
margin-top: 18px; margin-top: 32px;
height: 48px; float: left;
float: right;
background-color: #fff; background-color: #fff;
border-radius: 48px; margin-left: -4px;
border: 1px solid #a9dbf4;
margin-right: 22px;
} }
.mx_RightPanel_headerButton { .mx_RightPanel_headerButton {
cursor: pointer; cursor: pointer;
height: 48px;
display: table-cell; display: table-cell;
vertical-align: middle; vertical-align: middle;
padding-left: 8px; padding-left: 15px;
padding-right: 8px; 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; -webkit-box-ordinal-group: 2;
-moz-box-ordinal-group: 2; -moz-box-ordinal-group: 2;
-ms-flex-order: 2; -ms-flex-order: 2;

View File

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

View File

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

View File

@ -15,7 +15,7 @@ limitations under the License.
*/ */
.mx_UserSettings { .mx_UserSettings {
width: 720px; width: 960px;
margin-left: auto; margin-left: auto;
margin-right: 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 { .mx_ViewSource pre {
text-align: left; text-align: left;
font-size: 12px;
padding: 0.5em 1em 0.5em 1em;
word-wrap: break-word;
} }

View File

@ -0,0 +1,19 @@
.mx_CompatibilityPage {
width: 100%;
height: 100%;
background-color: #e55;
}
.mx_CompatibilityPage_box {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 500px;
height: 300px;
border: 1px solid;
padding: 10px;
background-color: #fcc;
}

View File

@ -69,6 +69,8 @@ limitations under the License.
-webkit-order: 1; -webkit-order: 1;
order: 1; order: 1;
background-color: #eaf5f0;
-webkit-flex: 0 0 230px; -webkit-flex: 0 0 230px;
flex: 0 0 230px; flex: 0 0 230px;
} }
@ -87,7 +89,7 @@ limitations under the License.
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
background-color: #f3f8fa; background-color: #fff;
-webkit-flex: 1; -webkit-flex: 1;
flex: 1; flex: 1;
@ -114,7 +116,6 @@ limitations under the License.
-webkit-order: 3; -webkit-order: 3;
order: 3; order: 3;
background-color: #f3f8fa;
-webkit-flex: 0 0 230px; -webkit-flex: 0 0 230px;
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 = {}; 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');
@ -33,6 +30,9 @@ 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.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.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.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.MessageContextMenu'] = require('./views/molecules/MessageContextMenu');
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');
@ -61,6 +61,7 @@ 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.RoomTooltip'] = require('./views/molecules/RoomTooltip');
skin['molecules.SearchBar'] = require('./views/molecules/SearchBar');
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');

View File

@ -18,10 +18,19 @@ limitations under the License.
var React = require('react'); var React = require('react');
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg');
var DateUtils = require('../../../../DateUtils');
var filesize = require('filesize');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ImageView', 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() { componentDidMount: function() {
document.addEventListener("keydown", this.onKeyDown); document.addEventListener("keydown", this.onKeyDown);
}, },
@ -38,9 +47,28 @@ 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() { 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 width = this.props.width || 500;
var height = this.props.height || 500; var height = this.props.height || 500;
@ -65,9 +93,55 @@ module.exports = React.createClass({
width: displayWidth, width: displayWidth,
height: displayHeight 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 ( 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'; 'use strict';
var React = require('react'); var React = require('react');
var DateUtils = require('../../../../DateUtils');
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',
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">
{ this.formatDate(date) } { DateUtils.formatDate(date) }
</span> </span>
); );
}, },

View File

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

View File

@ -48,7 +48,7 @@ module.exports = React.createClass({
return ( return (
<div className="mx_RoomTile" onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick}> <div className="mx_RoomTile" onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onClick={this.props.onClick}>
<div className="mx_RoomTile_avatar"> <div className="mx_RoomTile_avatar">
<img src={ this.props.img } width="36" height="36"/> <img src={ this.props.img } width="24" height="24"/>
</div> </div>
{ label } { label }
</div> </div>

View File

@ -30,6 +30,7 @@ var eventTileTypes = {
'm.call.invite' : 'molecules.EventAsTextTile', 'm.call.invite' : 'molecules.EventAsTextTile',
'm.call.answer' : 'molecules.EventAsTextTile', 'm.call.answer' : 'molecules.EventAsTextTile',
'm.call.hangup' : 'molecules.EventAsTextTile', 'm.call.hangup' : 'molecules.EventAsTextTile',
'm.room.name' : 'molecules.EventAsTextTile',
'm.room.topic' : 'molecules.EventAsTextTile', 'm.room.topic' : 'molecules.EventAsTextTile',
}; };
@ -76,7 +77,7 @@ module.exports = React.createClass({
// This shouldn't happen: the caller should check we support this type // This shouldn't happen: the caller should check we support this type
// before trying to instantiate us // before trying to instantiate us
if (!EventTileType) { if (!EventTileType) {
return null; throw new Error("Event type not supported");
} }
var classes = classNames({ var classes = classNames({
@ -88,12 +89,13 @@ module.exports = React.createClass({
mx_EventTile_highlight: this.shouldHighlight(), mx_EventTile_highlight: this.shouldHighlight(),
mx_EventTile_continuation: this.props.continuation, mx_EventTile_continuation: this.props.continuation,
mx_EventTile_last: this.props.last, 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 timestamp = <MessageTimestamp ts={this.props.mxEvent.getTs()} />
var editButton = ( var editButton = (
<input <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} className="mx_EventTile_editButton" onClick={this.onEditClicked}
/> />
); );
@ -108,7 +110,7 @@ module.exports = React.createClass({
if (this.props.mxEvent.sender) { if (this.props.mxEvent.sender) {
avatar = ( avatar = (
<div className="mx_EventTile_avatar"> <div className="mx_EventTile_avatar">
<MemberAvatar member={this.props.mxEvent.sender} /> <MemberAvatar member={this.props.mxEvent.sender} width={24} height={24} />
</div> </div>
); );
} }
@ -120,10 +122,10 @@ module.exports = React.createClass({
<div className={classes}> <div className={classes}>
{ avatar } { avatar }
{ sender } { sender }
<div> <div className="mx_EventTile_line">
{ timestamp } { timestamp }
{ editButton } { editButton }
<EventTileType mxEvent={this.props.mxEvent} /> <EventTileType mxEvent={this.props.mxEvent} searchTerm={this.props.searchTerm} />
</div> </div>
</div> </div>
); );

View File

@ -57,8 +57,9 @@ module.exports = React.createClass({
Modal.createDialog(ImageView, { Modal.createDialog(ImageView, {
src: httpUrl, src: httpUrl,
width: content.info.w, 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 cli = MatrixClientPeg.get();
var thumbHeight = null; 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 = {}; var imgStyle = {};
if (thumbHeight) imgStyle['height'] = thumbHeight; if (thumbHeight) imgStyle['height'] = thumbHeight;
@ -75,7 +76,7 @@ module.exports = React.createClass({
return ( return (
<span className="mx_MImageTile"> <span className="mx_MImageTile">
<a href={cli.mxcUrlToHttp(content.url)} onClick={ this.onClick }> <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> </a>
<div className="mx_MImageTile_download"> <div className="mx_MImageTile_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank"> <a href={cli.mxcUrlToHttp(content.url)} target="_blank">

View File

@ -17,18 +17,71 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var sanitizeHtml = require('sanitize-html');
var MNoticeTileController = require('matrix-react-sdk/lib/controllers/molecules/MNoticeTile') 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({ module.exports = React.createClass({
displayName: 'MNoticeTile', displayName: 'MNoticeTile',
mixins: [MNoticeTileController], mixins: [MNoticeTileController],
// FIXME: this entire class is copy-pasted from MTextTile :(
render: function() { render: function() {
var content = this.props.mxEvent.getContent(); 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 ( return (
<span ref="content" className="mx_MNoticeTile mx_MessageTile_content"> <span ref="content" className="mx_MNoticeTile mx_MessageTile_content">
{content.body} { body }
</span> </span>
); );
}, },

View File

@ -17,18 +17,71 @@ limitations under the License.
'use strict'; 'use strict';
var React = require('react'); var React = require('react');
var sanitizeHtml = require('sanitize-html');
var MTextTileController = require('matrix-react-sdk/lib/controllers/molecules/MTextTile') 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({ module.exports = React.createClass({
displayName: 'MTextTile', displayName: 'MTextTile',
mixins: [MTextTileController], mixins: [MTextTileController],
// FIXME: this entire class is copy-pasted from MTextTile :(
render: function() { render: function() {
var content = this.props.mxEvent.getContent(); 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 ( return (
<span ref="content" className="mx_MTextTile mx_MessageTile_content"> <span ref="content" className="mx_MTextTile mx_MessageTile_content">
{content.body} { body }
</span> </span>
); );
}, },

View File

@ -20,19 +20,30 @@ var React = require('react');
var Loader = require("../atoms/Spinner"); var Loader = require("../atoms/Spinner");
var MatrixClientPeg = require('matrix-react-sdk/lib/MatrixClientPeg'); 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') 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({ module.exports = React.createClass({
displayName: 'MemberInfo', displayName: 'MemberInfo',
mixins: [MemberInfoController], mixins: [MemberInfoController],
onCancel: function(e) {
dis.dispatch({
action: "view_user",
member: null
});
},
render: function() { render: function() {
var interactButton, kickButton, banButton, muteButton, giveModButton, spinner; 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_MemberInfo_field" onClick={this.onLeaveClick}>Leave room</div>;
} }
else { 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) { if (this.state.creatingRoom) {
@ -40,30 +51,43 @@ module.exports = React.createClass({
} }
if (this.state.can.kick) { if (this.state.can.kick) {
kickButton = <div className="mx_ContextualMenu_field" onClick={this.onKick}> kickButton = <div className="mx_MemberInfo_field" onClick={this.onKick}>
Kick Kick
</div>; </div>;
} }
if (this.state.can.ban) { if (this.state.can.ban) {
banButton = <div className="mx_ContextualMenu_field" onClick={this.onBan}> banButton = <div className="mx_MemberInfo_field" onClick={this.onBan}>
Ban Ban
</div>; </div>;
} }
if (this.state.can.mute) { if (this.state.can.mute) {
var muteLabel = this.state.muted ? "Unmute" : "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} {muteLabel}
</div>; </div>;
} }
if (this.state.can.modifyLevel) { if (this.state.can.modifyLevel) {
var giveOpLabel = this.state.isTargetMod ? "Revoke Mod" : "Make Mod"; 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} {giveOpLabel}
</div> </div>
} }
var MemberAvatar = sdk.getComponent('atoms.MemberAvatar');
return ( return (
<div> <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} {interactButton}
{muteButton} {muteButton}
{kickButton} {kickButton}
@ -71,6 +95,7 @@ module.exports = React.createClass({
{giveModButton} {giveModButton}
{spinner} {spinner}
</div> </div>
</div>
); );
} }
}); });

View File

@ -20,7 +20,7 @@ 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 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') 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. // 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) { onClick: function(e) {
var self = this; dis.dispatch({
self.setState({ 'menu': true }); action: 'view_user',
var MemberInfo = sdk.getComponent('molecules.MemberInfo'); member: this.props.member,
ContextualMenu.createMenu(MemberInfo, {
member: self.props.member,
right: window.innerWidth - e.pageX,
top: e.pageY,
onFinished: function() {
self.setState({ 'menu': false });
}
}); });
}, },
@ -119,10 +112,10 @@ module.exports = React.createClass({
var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId; var isMyUser = MatrixClientPeg.get().credentials.userId == this.props.member.userId;
var power; var power;
if (this.props.member && this.props.member.powerLevelNorm > 0) { // if (this.props.member && this.props.member.powerLevelNorm > 0) {
var img = "img/p/p" + Math.floor(20 * this.props.member.powerLevelNorm / 100) + ".png"; // 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=""/>; // power = <img src={ img } className="mx_MemberTile_power" width="44" height="44" alt=""/>;
} // }
var presenceClass = "mx_MemberTile_offline"; var presenceClass = "mx_MemberTile_offline";
var mainClassName = "mx_MemberTile "; var mainClassName = "mx_MemberTile ";
if (this.props.member.user) { if (this.props.member.user) {
@ -134,13 +127,13 @@ module.exports = React.createClass({
} }
} }
mainClassName += presenceClass; mainClassName += presenceClass;
if (this.state.hover || this.state.menu) { if (this.state.hover) {
mainClassName += " mx_MemberTile_hover"; mainClassName += " mx_MemberTile_hover";
} }
var name = this.props.member.name; var name = this.props.member.name;
// if (isMyUser) name += " (me)"; // this does nothing other than introduce line wrapping and pain // 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"; var nameClass = "mx_MemberTile_name";
if (zalgo.test(name)) { if (zalgo.test(name)) {
@ -148,7 +141,7 @@ module.exports = React.createClass({
} }
var nameEl; var nameEl;
if (this.state.hover || this.state.menu) { if (this.state.hover) {
var presence; var presence;
// FIXME: make presence data update whenever User.presence changes... // 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; 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 = nameEl =
<div className="mx_MemberTile_details"> <div className="mx_MemberTile_details">
{ leave } <img className="mx_MemberTile_chevron" src="img/member_chevron.png" width="8" height="12"/>
<div className="mx_MemberTile_userId">{ this.props.member.userId }</div> <div className="mx_MemberTile_userId">{ name }</div>
{ presence } { presence }
</div> </div>
} }
@ -177,7 +170,7 @@ module.exports = React.createClass({
return ( return (
<div className={mainClassName} title={ this.getPowerLabel() } onClick={ this.onClick } onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }> <div className={mainClassName} title={ this.getPowerLabel() } onClick={ this.onClick } onMouseEnter={ this.mouseEnter } onMouseLeave={ this.mouseLeave }>
<div className="mx_MemberTile_avatar"> <div className="mx_MemberTile_avatar">
<MemberAvatar member={this.props.member} /> <MemberAvatar member={this.props.member} width={36} height={36} />
{ power } { power }
</div> </div>
{ nameEl } { 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 MessageComposerController = require('matrix-react-sdk/lib/controllers/molecules/MessageComposer')
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: 'MessageComposer', displayName: 'MessageComposer',
@ -40,6 +41,14 @@ module.exports = React.createClass({
this.refs.uploadInput.getDOMNode().value = null; 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() { render: function() {
var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
var uploadInputStyle = {display: 'none'}; var uploadInputStyle = {display: 'none'};
@ -49,15 +58,18 @@ module.exports = React.createClass({
<div className="mx_MessageComposer_wrapper"> <div className="mx_MessageComposer_wrapper">
<div className="mx_MessageComposer_row"> <div className="mx_MessageComposer_row">
<div className="mx_MessageComposer_avatar"> <div className="mx_MessageComposer_avatar">
<MemberAvatar member={me} /> <MemberAvatar member={me} width={24} height={24} />
</div> </div>
<div className="mx_MessageComposer_input"> <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>
<div className="mx_MessageComposer_upload" onClick={this.onUploadClick}> <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} /> <input type="file" style={uploadInputStyle} ref="uploadInput" onChange={this.onUploadFileSelected} />
</div> </div>
<div className="mx_MessageComposer_call" onClick={this.onCallClick}>
<img src="img/call.png" width="28" height="20"/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@ -50,7 +50,16 @@ module.exports = React.createClass({
'mx_RoomTile_highlight': this.props.highlight, 'mx_RoomTile_highlight': this.props.highlight,
'mx_RoomTile_invited': this.props.room.currentState.members[myUserId].membership == 'invite' '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; var badge;
if (this.props.highlight) { if (this.props.highlight) {
badge = <div className="mx_RoomTile_badge"/>; badge = <div className="mx_RoomTile_badge"/>;
@ -73,7 +82,8 @@ module.exports = React.createClass({
var label; var label;
if (!this.props.collapsed) { 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) { else if (this.state.hover) {
var RoomTooltip = sdk.getComponent("molecules.RoomTooltip"); var RoomTooltip = sdk.getComponent("molecules.RoomTooltip");
@ -84,7 +94,7 @@ module.exports = React.createClass({
return ( return (
<div className={classes} onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <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} width="24" height="24" />
{ badge } { badge }
</div> </div>
{ label } { 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"; classes += " collapsed";
} }
else { 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 ( return (

View File

@ -30,7 +30,6 @@ module.exports = React.createClass({
mixins: [MemberListController], mixins: [MemberListController],
getInitialState: function() { getInitialState: function() {
return { editing: false };
}, },
memberSort: function(userIdA, userIdB) { memberSort: function(userIdA, userIdB) {
@ -71,43 +70,21 @@ module.exports = React.createClass({
}); });
}, },
onPopulateInvite: function(inputText, shouldSubmit) { onPopulateInvite: function(e) {
// reset back to placeholder this.onInvite(this.refs.invite.getDOMNode().value);
this.refs.invite.setValue("Invite", false, true); e.preventDefault();
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();
}, },
inviteTile: function() { 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) { if (this.state.inviting) {
return ( return (
<Loader /> <Loader />
); );
} else { } else {
return ( return (
<div className={ classes } onClick={ this.onClickInvite } > <form onSubmit={this.onPopulateInvite}>
<div className="mx_MemberTile_avatar"><img src="img/create-big.png" width="40" height="40" alt=""/></div> <input className="mx_MemberList_invite" ref="invite" placeholder="Invite another user"/>
<div className="mx_MemberTile_name"> </form>
<EditableText ref="invite" label="Invite" placeHolder="@user:domain.com" initialValue="" onValueChanged={this.onPopulateInvite}/>
</div>
</div>
); );
} }
}, },
@ -117,7 +94,7 @@ module.exports = React.createClass({
var invitedMemberTiles = this.makeMemberTiles('invite'); var invitedMemberTiles = this.makeMemberTiles('invite');
if (invitedMemberTiles.length > 0) { if (invitedMemberTiles.length > 0) {
invitedSection = ( invitedSection = (
<div> <div className="mx_MemberList_invited">
<h2>Invited</h2> <h2>Invited</h2>
<div className="mx_MemberList_wrapper"> <div className="mx_MemberList_wrapper">
{invitedMemberTiles} {invitedMemberTiles}
@ -127,18 +104,14 @@ module.exports = React.createClass({
} }
return ( return (
<div className="mx_MemberList"> <div className="mx_MemberList">
<div className="mx_MemberList_chevron">
<img src="img/chevron.png" width="24" height="13"/>
</div>
<div className="mx_MemberList_border"> <div className="mx_MemberList_border">
{this.inviteTile()}
<div> <div>
<h2>Members</h2>
<div className="mx_MemberList_wrapper"> <div className="mx_MemberList_wrapper">
{this.makeMemberTiles('join')} {this.makeMemberTiles('join')}
</div> </div>
</div> </div>
{invitedSection} {invitedSection}
{this.inviteTile()}
</div> </div>
</div> </div>
); );

View File

@ -19,6 +19,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 dis = require('matrix-react-sdk/lib/dispatcher');
var MatrixClientPeg = require("matrix-react-sdk/lib/MatrixClientPeg");
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'RightPanel', displayName: 'RightPanel',
@ -26,6 +27,20 @@ module.exports = React.createClass({
Phase : { Phase : {
MemberList: 'MemberList', MemberList: 'MemberList',
FileList: 'FileList', 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() { 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() { render: function() {
var MemberList = sdk.getComponent('organisms.MemberList'); var MemberList = sdk.getComponent('organisms.MemberList');
var buttonGroup; var buttonGroup;
var panel; 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) { if (this.props.roomId) {
buttonGroup = buttonGroup =
<div className="mx_RightPanel_headerButtonGroup"> <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 }> <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>
</div>; </div>;
if (!this.props.collapsed && this.state.phase == this.Phase.MemberList) { if (!this.props.collapsed) {
if(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} />
} }
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"; var classes = "mx_RightPanel";

View File

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

View File

@ -41,22 +41,38 @@ module.exports = React.createClass({
callElement = <CallView className="mx_MatrixChat_callView"/> callElement = <CallView className="mx_MatrixChat_callView"/>
} }
var recentsLabel = this.props.collapsed ? var expandButton = this.props.collapsed ?
<img style={{cursor: 'pointer'}} onClick={ this.onShowClick } src="img/menu.png" width="27" height="20" alt=">"/> : <img className="mx_RoomList_expandButton" onClick={ this.onShowClick } src="img/menu.png" width="20" alt=">"/> :
"Recents"; 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 ( return (
<div className="mx_RoomList" onScroll={this._repositionTooltip}> <div className="mx_RoomList" onScroll={this._repositionTooltip}>
{callElement} { expandButton }
<h2 className="mx_RoomList_favourites_label">Favourites</h2> { callElement }
<h2 className="mx_RoomList_favouritesLabel">Favourites</h2>
<RoomDropTarget text="Drop here to favourite"/> <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"> <div className="mx_RoomList_recents">
{this.makeRoomTiles()} {this.makeRoomTiles(this.state.roomList, false)}
</div> </div>
<h2 className="mx_RoomList_archive_label">Archive</h2> <h2 className="mx_RoomList_archiveLabel">Archive</h2>
<RoomDropTarget text="Drop here to archive"/> <RoomDropTarget text="Drop here to archive"/>
</div> </div>
); );

View File

@ -82,6 +82,10 @@ module.exports = React.createClass({
}); });
}, },
onSearchClick: function() {
this.setState({ searching: true });
},
onConferenceNotificationClick: function() { onConferenceNotificationClick: function() {
dis.dispatch({ dis.dispatch({
action: 'place_call', action: 'place_call',
@ -108,6 +112,7 @@ module.exports = React.createClass({
var MessageComposer = sdk.getComponent('molecules.MessageComposer'); var MessageComposer = sdk.getComponent('molecules.MessageComposer');
var CallView = sdk.getComponent("molecules.voip.CallView"); var CallView = sdk.getComponent("molecules.voip.CallView");
var RoomSettings = sdk.getComponent("molecules.RoomSettings"); var RoomSettings = sdk.getComponent("molecules.RoomSettings");
var SearchBar = sdk.getComponent("molecules.SearchBar");
if (!this.state.room) { if (!this.state.room) {
if (this.props.roomId) { if (this.props.roomId) {
@ -181,8 +186,8 @@ module.exports = React.createClass({
<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_uploadIcon" src="img/fileicon.png" width="17" height="22"/>
<img className="mx_RoomView_uploadCancel" src="img/cancel.png" width="40" height="40"/> <img className="mx_RoomView_uploadCancel" src="img/cancel.png" width="18" height="18"/>
<div className="mx_RoomView_uploadBytes"> <div className="mx_RoomView_uploadBytes">
{ uploadedSize } / { totalSize } { uploadedSize } / { totalSize }
</div> </div>
@ -197,7 +202,7 @@ module.exports = React.createClass({
if (unreadMsgs) { if (unreadMsgs) {
statusBar = ( statusBar = (
<div className="mx_RoomView_unreadMessagesBar" onClick={ this.scrollToBottom }> <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} {unreadMsgs}
</div> </div>
); );
@ -205,19 +210,22 @@ module.exports = React.createClass({
else if (typingString) { else if (typingString) {
statusBar = ( statusBar = (
<div className="mx_RoomView_typingBar"> <div className="mx_RoomView_typingBar">
<img src="img/typing.png" width="40" height="40" alt=""/> <div className="mx_RoomView_typingImage">...</div>
{typingString} {typingString}
</div> </div>
); );
} }
} }
var roomEdit = null; var aux = null;
if (this.state.editingRoomSettings) { 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) { else if (this.state.uploadingRoomSettings) {
roomEdit = <Loader/>; aux = <Loader/>;
}
else if (this.state.searching) {
aux = <SearchBar ref="search_bar" onCancelClick={this.onCancelClick} onSearch={this.onSearch}/>;
} }
var conferenceCallNotification = null; var conferenceCallNotification = null;
@ -233,7 +241,7 @@ module.exports = React.createClass({
if (this.state.draggingFile) { if (this.state.draggingFile) {
fileDropTarget = <div className="mx_RoomView_fileDropTarget"> fileDropTarget = <div className="mx_RoomView_fileDropTarget">
<div className="mx_RoomView_fileDropTargetLabel"> <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 Drop File Here
</div> </div>
</div>; </div>;
@ -241,12 +249,12 @@ module.exports = React.createClass({
return ( return (
<div className="mx_RoomView"> <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} /> onSettingsClick={this.onSettingsClick} onSaveClick={this.onSaveClick} onCancelClick={this.onCancelClick} />
<div className="mx_RoomView_auxPanel"> <div className="mx_RoomView_auxPanel">
<CallView room={this.state.room}/> <CallView room={this.state.room}/>
{ conferenceCallNotification } { conferenceCallNotification }
{ roomEdit } { aux }
</div> </div>
<div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }> <div ref="messageWrapper" className="mx_RoomView_messagePanel" onScroll={ this.onMessageListScroll }>
<div className="mx_RoomView_messageListWrapper"> <div className="mx_RoomView_messageListWrapper">
@ -260,6 +268,7 @@ module.exports = React.createClass({
</div> </div>
<div className="mx_RoomView_statusArea"> <div className="mx_RoomView_statusArea">
<div className="mx_RoomView_statusAreaBox"> <div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line"></div>
{statusBar} {statusBar}
</div> </div>
</div> </div>

View File

@ -21,6 +21,26 @@ var React = require('react');
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ViewSource', 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() { render: function() {
return ( return (
<div className="mx_ViewSource"> <div className="mx_ViewSource">

View File

@ -0,0 +1,62 @@
/*
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: 'CompatibilityPage',
propTypes: {
onAccept: React.PropTypes.func
},
getDefaultProps: function() {
return {
onAccept: function() {} // NOP
};
},
onAccept: function() {
this.props.onAccept();
},
render: function() {
return (
<div className="mx_CompatibilityPage">
<div className="mx_CompatibilityPage_box">
<p>Sorry, your browser is <b>not</b> able to run Vector.</p>
<p>
Buttons and images may appear out of place, communication may
not be possible and all manner of chaos may be unleashed.
</p>
<p>
Please install <a href={"https://www.google.com/chrome"}>Chrome</a> for
the best experience.
</p>
<p>
Though if you like taking risks with your life, you can still try it
out by clicking that you understand the risks involved.
</p>
<button onClick={this.onAccept}>
I understand the risks and wish to continue
</button>
</div>
</div>
);
}
});

View File

@ -16,6 +16,7 @@ limitations under the License.
'use strict'; 'use strict';
var RunModernizrTests = require("./modernizr"); // this side-effects a global
var React = require("react"); var React = require("react");
var sdk = require("matrix-react-sdk"); var sdk = require("matrix-react-sdk");
sdk.loadSkin(require('../skins/vector/skindex')); sdk.loadSkin(require('../skins/vector/skindex'));
@ -25,6 +26,34 @@ var qs = require("querystring");
var lastLocationHashSet = null; var lastLocationHashSet = null;
function checkBrowserFeatures(featureList) {
if (!window.Modernizr) {
console.error("Cannot check features - Modernizr global is missing.");
return false;
}
var featureComplete = true;
for (var i = 0; i < featureList.length; i++) {
if (window.Modernizr[featureList[i]] === undefined) {
console.error(
"Looked for feature '%s' but Modernizr has no results for this. " +
"Has it been configured correctly?", featureList[i]
);
return false;
}
if (window.Modernizr[featureList[i]] === false) {
console.error("Browser missing feature: '%s'", featureList[i]);
// toggle flag rather than return early so we log all missing features
// rather than just the first.
featureComplete = false;
}
}
return featureComplete;
}
var validBrowser = checkBrowserFeatures([
"displaytable", "flexbox", "es5object", "es5function", "localstorage",
"objectfit"
]);
// We want to support some name / value pairs in the fragment // We want to support some name / value pairs in the fragment
// so we're re-using query string like format // so we're re-using query string like format
@ -84,14 +113,11 @@ var makeRegistrationUrl = function() {
'#/register'; '#/register';
} }
var MatrixChat = sdk.getComponent('pages.MatrixChat');
window.matrixChat = React.render(
<MatrixChat onNewScreen={onNewScreen} registrationUrl={makeRegistrationUrl()} />,
document.getElementById('matrixchat')
);
window.addEventListener('hashchange', onHashChange); window.addEventListener('hashchange', onHashChange);
window.onload = function() { window.onload = function() {
if (!validBrowser) {
return;
}
routeUrl(window.location); routeUrl(window.location);
loaded = true; loaded = true;
if (lastLoadedScreen) { if (lastLoadedScreen) {
@ -100,3 +126,28 @@ window.onload = function() {
} }
} }
function loadApp() {
if (validBrowser) {
var MatrixChat = sdk.getComponent('pages.MatrixChat');
window.matrixChat = React.render(
<MatrixChat onNewScreen={onNewScreen} registrationUrl={makeRegistrationUrl()} />,
document.getElementById('matrixchat')
);
}
else {
console.error("Browser is missing required features.");
// take to a different landing page to AWOOOOOGA at the user
var CompatibilityPage = require("../skins/vector/views/pages/CompatibilityPage");
window.matrixChat = React.render(
<CompatibilityPage onAccept={function() {
validBrowser = true;
console.log("User accepts the compatibility risks.");
loadApp();
window.onload(); // still do the same code paths for compatible clients
}} />,
document.getElementById('matrixchat')
);
}
}
loadApp();

3
src/vector/modernizr.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Vector</title> <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="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="60x60" href="/icons/apple-touch-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/icons/apple-touch-icon-72x72.png"> <link rel="apple-touch-icon" sizes="72x72" href="/icons/apple-touch-icon-72x72.png">