Merge branch 'experimental' into bwindels/memberqueryfield

This commit is contained in:
Bruno Windels 2018-11-07 13:29:28 +01:00
commit 883126a528
16 changed files with 408 additions and 221 deletions

View file

@ -199,25 +199,12 @@ module.exports = function (config) {
'matrix-react-sdk': path.resolve('test/skinned-sdk.js'), 'matrix-react-sdk': path.resolve('test/skinned-sdk.js'),
'sinon': 'sinon/pkg/sinon.js', 'sinon': 'sinon/pkg/sinon.js',
// To make webpack happy
// Related: https://github.com/request/request/issues/1529
// (there's no mock available for fs, so we fake a mock by using
// an in-memory version of fs)
"fs": "memfs",
}, },
modules: [ modules: [
path.resolve('./test'), path.resolve('./test'),
"node_modules" "node_modules"
], ],
}, },
node: {
// Because webpack is made of fail
// https://github.com/request/request/issues/1529
// Note: 'mock' is the new 'empty'
net: 'mock',
tls: 'mock'
},
devtool: 'inline-source-map', devtool: 'inline-source-map',
externals: { externals: {
// Don't try to bundle electron: leave it as a commonjs dependency // Don't try to bundle electron: leave it as a commonjs dependency

View file

@ -76,7 +76,6 @@
"lodash": "^4.13.1", "lodash": "^4.13.1",
"lolex": "2.3.2", "lolex": "2.3.2",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"memfs": "^2.10.1",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"pako": "^1.0.5", "pako": "^1.0.5",
"prop-types": "^15.5.8", "prop-types": "^15.5.8",

View file

@ -18,6 +18,7 @@ limitations under the License.
1. for browsers that support native overlay auto-hiding scrollbars 1. for browsers that support native overlay auto-hiding scrollbars
*/ */
.mx_AutoHideScrollbar { .mx_AutoHideScrollbar {
overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-ms-overflow-style: -ms-autohiding-scrollbar; -ms-overflow-style: -ms-autohiding-scrollbar;
} }
@ -34,23 +35,20 @@ body.mx_scrollbar_overlay_noautohide .mx_AutoHideScrollbar:hover {
} }
/* /*
3. as a last fallback, compensate for the scrollbar taking up space in the layout 3. as a last fallback, compensate for the scrollbar taking up space in the layout
by playing with the paddings. the default below will add a right padding by having giving the child element (.mx_AutoHideScrollbar_offset) a
of the scrollbar width and clear that on hover. negative right margin of the width of the scrollbar when the container
this won't work well on classes that also need to set their padding, is overflowing. This is what Firefox ends up using. Overflow is detected
so this needs to be overriden and adjust the padding with calc like so: in javascript, and adds the mx_AutoHideScrollbar_overflow class to the container.
``` This only works in Firefox, which should be fine as this fallback is only needed there.
body.mx_scrollbar_nooverlay .componentClass.mx_AutoHideScrollbar_overflow:hover {
padding-right: calc(15px - var(--scrollbar-width)) !important;
}
```
*/ */
body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar { body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar {
box-sizing: border-box;
overflow-y: hidden; overflow-y: hidden;
padding-right: var(--scrollbar-width);
} }
body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover { body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover {
overflow-y: auto; overflow-y: auto;
padding-right: 0; }
body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover.mx_AutoHideScrollbar_overflow > .mx_AutoHideScrollbar_offset {
margin-right: calc(-1 * var(--scrollbar-width));
} }

View file

@ -22,98 +22,74 @@ limitations under the License.
} }
.mx_RoomSubList_nonEmpty { .mx_RoomSubList_nonEmpty {
margin-bottom: 8px; margin-bottom: 4px;
} }
.mx_RoomSubList_labelContainer { .mx_RoomSubList_labelContainer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center;
flex: 0 0 auto; flex: 0 0 auto;
margin: 8px 19px 0 0; margin: 0 16px;
height: 36px;
} }
.mx_RoomSubList_label { .mx_RoomSubList_label {
flex: 1; flex: 1;
position: relative; cursor: pointer;
display: flex;
align-items: center;
padding: 0 6px;
}
.mx_RoomSubList_label > span {
flex: 1 1 auto;
text-transform: uppercase; text-transform: uppercase;
color: $roomsublist-label-fg-color; color: $roomsublist-label-fg-color;
font-weight: 700; font-weight: 700;
font-size: 12px; font-size: 12px;
margin-left: 16px; margin-left: 8px;
padding-left: 16px; /* gutter */
padding-right: 16px; /* gutter */
padding-top: 6px;
padding-bottom: 6px;
cursor: pointer;
}
.mx_RoomSubList_label.mx_RoomSubList_fixed {
position: fixed;
top: 0;
z-index: 5;
/* pointer-events: none; */
} }
.mx_RoomSubList_badge { .mx_RoomSubList_badge {
height: 18px; flex: 0 0 auto;
border-radius: 9px; border-radius: 8px;
color: $accent-fg-color; color: $accent-fg-color;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
vertical-align: middle; padding: 0 5px;
line-height: 18px;
padding: 0 4px;
background-color: $accent-color; background-color: $accent-color;
} }
.mx_RoomSubList_label .mx_RoomSubList_badge:hover {
filter: brightness($focus-brightness);
}
.mx_RoomSubList_addRoom, .mx_RoomSubList_badge { .mx_RoomSubList_addRoom, .mx_RoomSubList_badge {
margin: 5px; margin-left: 7px;
} }
.mx_RoomSubList_addRoom { .mx_RoomSubList_addRoom {
background-color: $roomheader-addroom-color; background-color: $roomheader-addroom-color;
color: $roomsublist-background; color: $roomsublist-background;
border-radius: 9px; background-image: url('../../img/icons-room-add.svg');
text-align: center; background-repeat: no-repeat;
vertical-align: middle; background-position: center;
line-height: 18px; border-radius: 10px; // 16/2 + 2 padding
font-weight: bold; height: 16px;
font-size: 18px; flex: 0 0 16px;
width: 18px; background-clip: content-box;
height: 18px;
} }
.mx_RoomSubList_badgeHighlight { .mx_RoomSubList_badgeHighlight {
background-color: $warning-color; background-color: $warning-color;
} }
/* This is the bottom of the speech bubble */
.mx_RoomSubList_badgeHighlight:after {
content: "";
position: absolute;
display: block;
width: 0;
height: 0;
margin-left: 5px;
border-top: 5px solid $warning-color;
border-right: 7px solid transparent;
}
.mx_RoomSubList_chevron { .mx_RoomSubList_chevron {
left: 0px;
pointer-events: none; pointer-events: none;
position: absolute;
top: 11px;
width: 9px;
height: 4px;
background-image: url('../../img/topleft-chevron.svg'); background-image: url('../../img/topleft-chevron.svg');
background-size: cover; background-repeat: no-repeat;
// the transition doesn't work as the chevron gets remounted transition: transform 0.2s ease-in;
transition: rotateZ 0.2s ease-in; width: 10px;
height: 10px;
background-position: center;
margin-left: 2px;
} }
.mx_RoomSubList_chevronDown { .mx_RoomSubList_chevronDown {
@ -131,47 +107,26 @@ limitations under the License.
.mx_RoomSubList_scroll { .mx_RoomSubList_scroll {
/* let rooms list grab all available space */ /* let rooms list grab all available space */
flex: 0 1 auto; flex: 0 1 auto;
padding: 0 15px !important; padding: 0 8px;
}
/*
for browsers that don't support overlay scrollbars,
subtract scrollbar width from right padding on hover when overflowing
so the content doesn't jump when showing the scrollbars
*/
body.mx_scrollbar_nooverlay .mx_RoomSubList_scroll.mx_AutoHideScrollbar_overflow:hover {
padding-right: calc(15px - var(--scrollbar-width)) !important;
} }
.collapsed { .collapsed {
.mx_RoomSubList_label {
height: 17px; .mx_RoomSubList_scroll {
width: 28px; /* collapsed LHS Panel width */ padding: 0;
} }
.mx_RoomSubList_labelContainer { .mx_RoomSubList_labelContainer {
width: 28px; /* collapsed LHS Panel width */ margin-right: 14px;
margin-left: 2px;
} }
/* Hide the bottom of speech bubble */ .mx_RoomSubList_addRoom {
.mx_RoomSubList_badgeHighlight:after { margin-left: 3px;
display: none; margin-right: 28px;
} }
.mx_RoomSubList_line { .mx_RoomSubList_label > span {
display: none;
}
.mx_RoomSubList_moreBadge {
position: static;
margin-left: 16px;
margin-top: 2px;
}
.mx_RoomSubList_ellipsis {
height: 20px;
}
.mx_RoomSubList_more {
display: none; display: none;
} }
} }

View file

@ -21,10 +21,30 @@ limitations under the License.
cursor: pointer; cursor: pointer;
height: 40px; height: 40px;
margin: 0; margin: 0;
padding: 2px 12px; padding: 0 8px 0 10px;
position: relative; position: relative;
} }
.mx_RoomTile_menuButton {
display: none;
flex: 0 0 16px;
height: 16px;
background-image: url('../../img/icon_context.svg');
background-repeat: no-repeat;
background-position: center;
}
// toggle menuButton and badge on hover/menu displayed
.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover, .mx_RoomTile_menuDisplayed {
.mx_RoomTile_menuButton {
display: block;
}
.mx_RoomTile_badge {
display: none;
}
}
.mx_RoomTile_tooltip { .mx_RoomTile_tooltip {
display: inline-block; display: inline-block;
position: relative; position: relative;
@ -62,14 +82,12 @@ limitations under the License.
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.mx_RoomTile_invite {
/* color: rgba(69, 69, 69, 0.5); */
}
.collapsed { .collapsed {
.mx_RoomTile { .mx_RoomTile {
margin: 2px; margin: 0 2px;
padding: 2px 0 2px 12px; padding: 0 2px;
position: relative;
justify-content: center;
} }
.mx_RoomTile_name { .mx_RoomTile_name {
@ -77,57 +95,26 @@ limitations under the License.
} }
.mx_RoomTile_badge { .mx_RoomTile_badge {
display: block;
position: absolute; position: absolute;
height: 15px; right: 6px;
right: 8px; top: 0px;
top: 2px;
min-width: 12px;
border-radius: 16px; border-radius: 16px;
padding: 0px 4px 0px 4px;
z-index: 3; z-index: 3;
border: 0.18em solid $secondary-accent-color;
} }
/* Hide the bottom of speech bubble */ .mx_RoomTile_menuButton {
.mx_RoomTile_highlight .mx_RoomTile_badge:after { display: none; //no design for this for now
display: none;
} }
} }
/* This is the bottom of the speech bubble */
.mx_RoomTile_highlight .mx_RoomTile_badge:after {
content: "";
position: absolute;
display: block;
width: 0;
height: 0;
margin-left: 5px;
border-top: 5px solid $warning-color;
border-right: 7px solid transparent;
}
.mx_RoomTile_badge { .mx_RoomTile_badge {
flex: 0 1 content; flex: 0 1 content;
min-width: 15px; border-radius: 0.8em;
border-radius: 8px; padding: 0 0.4em;
color: $accent-fg-color; color: $accent-fg-color;
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
text-align: center;
padding-top: 1px;
padding-left: 4px;
padding-right: 4px;
}
.mx_RoomTile .mx_RoomTile_badge.mx_RoomTile_badgeButton,
.mx_RoomTile.mx_RoomTile_menuDisplayed .mx_RoomTile_badge {
letter-spacing: 0.1em;
opacity: 1;
}
.mx_RoomTile.mx_RoomTile_noBadges .mx_RoomTile_badge.mx_RoomTile_badgeButton,
.mx_RoomTile.mx_RoomTile_menuDisplayed.mx_RoomTile_noBadges .mx_RoomTile_badge {
background-color: $neutral-badge-color;
} }
.mx_RoomTile_unreadNotify .mx_RoomTile_badge { .mx_RoomTile_unreadNotify .mx_RoomTile_badge {
@ -169,10 +156,6 @@ limitations under the License.
background-color: $roomtile-focused-bg-color; background-color: $roomtile-focused-bg-color;
} }
.mx_RoomTile .mx_RoomTile_name.mx_RoomTile_badgeShown {
width: 140px;
}
.mx_RoomTile_arrow { .mx_RoomTile_arrow {
position: absolute; position: absolute;
right: 0px; right: 0px;

View file

@ -1,23 +1,71 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg
<svg version="1.1" xmlns:dc="http://purl.org/dc/elements/1.1/"
id="svg4196" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 25 25" xmlns:cc="http://creativecommons.org/ns#"
style="enable-background:new 0 0 25 25;" xml:space="preserve"> xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
<style type="text/css"> xmlns:svg="http://www.w3.org/2000/svg"
.st1{opacity:0.7;} xmlns="http://www.w3.org/2000/svg"
.st2{fill:none;stroke-linecap:round;} xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
</style> xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
<g id="icons_people" transform="translate(50.000000, 725.000000)"> width="11.521363"
<path id="Oval-1-Copy-7" fill="#76CFA6" d="M-37.5-700c6.9,0,12.5-5.6,12.5-12.5S-30.6-725-37.5-725S-50-719.4-50-712.5 height="11.521363"
S-44.4-700-37.5-700z"/> viewBox="0 0 11.521363 11.521363"
<g id="text3879" transform="matrix(1.0243293,0,0,0.97624855,-24.996028,0.15844144)"> version="1.1"
<g id="Group-3" transform="translate(14.4375,3.9375)" class="st1"> id="svg4"
<path id="Line" class="st2" stroke="#ffffff" d="M-23.2-733.8h4.6"/> sodipodi:docname="icons-room-add.svg"
<path id="path3142" class="st2" stroke="#ffffff" d="M-20.9-736.2v4.8"/> style="fill:none"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1586"
inkscape:window-height="988"
id="namedview6"
showgrid="false"
fit-margin-top="2"
fit-margin-left="2"
fit-margin-right="2"
fit-margin-bottom="2"
inkscape:zoom="29.5"
inkscape:cx="5.8284785"
inkscape:cy="5.7606831"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<path
d="m 5.7606819,2.7606819 v 6"
id="path2"
inkscape:connector-curvature="0"
style="stroke:#ffffff;stroke-width:1.5;stroke-linecap:round" />
<g
style="fill:none"
id="g876"
transform="translate(1.7606819,4.7606819)">
<path
id="path865"
d="M 7,1 H 1"
inkscape:connector-curvature="0"
style="stroke:#ffffff;stroke-width:1.5;stroke-linecap:round" />
</g> </g>
<path id="path3002" fill="#ffffff" d="M-11.4-731.3l-0.5,2.6h2.2v1h-2.4l-0.7,3.3h-1.1l0.7-3.3
h-2.3l-0.6,3.3h-1.1l0.6-3.3h-2v-1h2.2l0.5-2.6H-18v-1h2.3l0.6-3.4h1.1l-0.6,3.4h2.4l0.7-3.4h1l-0.7,3.4h2v1H-11.4 M-15.3-728.7
h2.3l0.5-2.6h-2.3L-15.3-728.7"/>
</g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

View file

@ -1,15 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="10px" height="6px" viewBox="0 0 10 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="9.946106"
height="5.5662179"
viewBox="0 0 9.946106 5.5662179"
version="1.1"
id="svg14"
sodipodi:docname="topleft-chevron.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata18">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>dropdown</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1277"
inkscape:window-height="653"
id="namedview16"
showgrid="false"
fit-margin-top="0.5"
fit-margin-left="0.5"
fit-margin-right="0.5"
fit-margin-bottom="0.5"
inkscape:zoom="35.2"
inkscape:cx="4.6570922"
inkscape:cy="2.9102278"
inkscape:window-x="459"
inkscape:window-y="90"
inkscape:window-maximized="0"
inkscape:current-layer="svg14" />
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch --> <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>dropdown</title> <title
<desc>Created with Sketch.</desc> id="title2">dropdown</title>
<defs></defs> <desc
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> id="desc4">Created with Sketch.</desc>
<g id="matrix-my-stuff-no-lines-message-context-menu-smaller-icons" transform="translate(-203.000000, -25.000000)" stroke="#212121" stroke-width="1.3"> <defs
<g id="Group-3" transform="translate(128.000000, 15.000000)"> id="defs6" />
<g id="dropdown" transform="translate(76.000000, 11.000000)"> <g
<path d="M0.5,0.5 L4.35868526,3.75422271" id="Line-5"></path> id="Page-1"
<path d="M8.13193273,0.560421385 L4.35868526,3.75422271" id="Line-5-Copy"></path> style="fill:none;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round"
transform="translate(-0.3429078,-0.34400861)">
<g
id="matrix-my-stuff-no-lines-message-context-menu-smaller-icons"
transform="translate(-203,-25)"
style="stroke:#212121;stroke-width:1.29999995">
<g
id="Group-3"
transform="translate(128,15)">
<g
id="dropdown"
transform="translate(76,11)">
<path
d="M 0.5,0.5 4.3586853,3.7542227"
id="Line-5"
inkscape:connector-curvature="0" />
<path
d="M 8.1319327,0.56042139 4.3586853,3.7542227"
id="Line-5-Copy"
inkscape:connector-curvature="0" />
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 1,017 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

@ -62,6 +62,35 @@ function createAccountDataAction(matrixClient, accountDataEvent) {
}; };
} }
/**
* @typedef RoomAccountDataAction
* @type {Object}
* @property {string} action 'MatrixActions.Room.accountData'.
* @property {MatrixEvent} event the MatrixEvent that triggered the dispatch.
* @property {string} event_type the type of the MatrixEvent, e.g. "m.direct".
* @property {Object} event_content the content of the MatrixEvent.
* @property {Room} room the room where the account data was changed.
*/
/**
* Create a MatrixActions.Room.accountData action that represents a MatrixClient `Room.accountData`
* matrix event.
*
* @param {MatrixClient} matrixClient the matrix client.
* @param {MatrixEvent} accountDataEvent the account data event.
* @param {Room} room the room where account data was changed
* @returns {RoomAccountDataAction} an action of type MatrixActions.Room.accountData.
*/
function createRoomAccountDataAction(matrixClient, accountDataEvent, room) {
return {
action: 'MatrixActions.Room.accountData',
event: accountDataEvent,
event_type: accountDataEvent.getType(),
event_content: accountDataEvent.getContent(),
room: room,
};
}
/** /**
* @typedef RoomAction * @typedef RoomAction
* @type {Object} * @type {Object}
@ -201,6 +230,7 @@ export default {
start(matrixClient) { start(matrixClient) {
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction); this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);

View file

@ -112,7 +112,9 @@ export default class AutoHideScrollbar extends React.Component {
ref={this._collectContainerRef} ref={this._collectContainerRef}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")} className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
> >
<div className="mx_AutoHideScrollbar_offset">
{ this.props.children } { this.props.children }
</div>
</div>); </div>);
} }
} }

View file

@ -151,8 +151,7 @@ const LeftPanel = React.createClass({
} }
} while (element && !( } while (element && !(
classes.contains("mx_RoomTile") || classes.contains("mx_RoomTile") ||
classes.contains("mx_SearchBox_search") || classes.contains("mx_SearchBox_search")));
classes.contains("mx_RoomSubList_ellipsis")));
if (element) { if (element) {
element.focus(); element.focus();

View file

@ -243,9 +243,8 @@ const RoomSubList = React.createClass({
const subListNotifCount = subListNotifications[0]; const subListNotifCount = subListNotifications[0];
const subListNotifHighlight = subListNotifications[1]; const subListNotifHighlight = subListNotifications[1];
let badge; let badge;
if (this.state.hidden) { if (!this.props.collapsed) {
const badgeClasses = classNames({ const badgeClasses = classNames({
'mx_RoomSubList_badge': true, 'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight, 'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
@ -285,9 +284,7 @@ const RoomSubList = React.createClass({
let addRoomButton; let addRoomButton;
if (this.props.onAddRoom) { if (this.props.onAddRoom) {
addRoomButton = ( addRoomButton = (
<AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom"> <AccessibleButton onClick={ this.props.onAddRoom } className="mx_RoomSubList_addRoom" />
+
</AccessibleButton>
); );
} }
@ -307,7 +304,7 @@ const RoomSubList = React.createClass({
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header"> <div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}> <AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
{ chevron } { chevron }
{ this.props.collapsed ? '' : this.props.label } <span>{this.props.label}</span>
{ incomingCall } { incomingCall }
</AccessibleButton> </AccessibleButton>
{ badge } { badge }

View file

@ -82,6 +82,8 @@ const SIMPLE_SETTINGS = [
{ id: "TagPanel.disableTagPanel" }, { id: "TagPanel.disableTagPanel" },
{ id: "enableWidgetScreenshots" }, { id: "enableWidgetScreenshots" },
{ id: "RoomSubList.showEmpty" }, { id: "RoomSubList.showEmpty" },
{ id: "pinMentionedRooms" },
{ id: "pinUnreadRooms" },
{ id: "showDeveloperTools" }, { id: "showDeveloperTools" },
]; ];

View file

@ -220,7 +220,7 @@ module.exports = React.createClass({
this.setState( { badgeHover: false } ); this.setState( { badgeHover: false } );
}, },
onBadgeClicked: function(e) { onOpenMenu: function(e) {
// Prevent the RoomTile onClick event firing as well // Prevent the RoomTile onClick event firing as well
e.stopPropagation(); e.stopPropagation();
// Only allow non-guests to access the context menu // Only allow non-guests to access the context menu
@ -276,19 +276,14 @@ module.exports = React.createClass({
if (name == undefined || name == null) name = ''; if (name == undefined || name == null) name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badgeContent;
if (this.state.badgeHover || this.state.menuDisplayed) { let badge;
badgeContent = "\u00B7\u00B7\u00B7"; if (badges) {
} else if (badges) {
const limitedCount = FormattingUtils.formatCount(notificationCount); const limitedCount = FormattingUtils.formatCount(notificationCount);
badgeContent = notificationCount ? limitedCount : '!'; const badgeContent = notificationCount ? limitedCount : '!';
} else { badge = <div className={badgeClasses}>{ badgeContent }</div>;
badgeContent = '\u200B';
} }
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
const EmojiText = sdk.getComponent('elements.EmojiText'); const EmojiText = sdk.getComponent('elements.EmojiText');
let label; let label;
let tooltip; let tooltip;
@ -317,6 +312,11 @@ module.exports = React.createClass({
// incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>; // incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
//} //}
let contextMenuButton;
if (!MatrixClientPeg.get().isGuest()) {
contextMenuButton = <AccessibleButton className="mx_RoomTile_menuButton" onClick={this.onOpenMenu} />;
}
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
let dmIndicator; let dmIndicator;
@ -338,6 +338,7 @@ module.exports = React.createClass({
</div> </div>
</div> </div>
{ label } { label }
{ contextMenuButton }
{ badge } { badge }
{ /* { incomingCallBox } */ } { /* { incomingCallBox } */ }
{ tooltip } { tooltip }

View file

@ -249,6 +249,8 @@
"Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)", "Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)",
"Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room", "Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room",
"Room Colour": "Room Colour", "Room Colour": "Room Colour",
"Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list",
"Pin rooms I'm mentioned in to the top of the room list": "Pin rooms I'm mentioned in to the top of the room list",
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
"Show empty room list headings": "Show empty room list headings", "Show empty room list headings": "Show empty room list headings",
"Collecting app version information": "Collecting app version information", "Collecting app version information": "Collecting app version information",

View file

@ -276,6 +276,16 @@ export const SETTINGS = {
default: true, default: true,
controller: new AudioNotificationsEnabledController(), controller: new AudioNotificationsEnabledController(),
}, },
"pinMentionedRooms": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Pin rooms I'm mentioned in to the top of the room list"),
default: false,
},
"pinUnreadRooms": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Pin unread rooms to the top of the room list"),
default: false,
},
"enableWidgetScreenshots": { "enableWidgetScreenshots": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable widget screenshots on supported widgets'), displayName: _td('Enable widget screenshots on supported widgets'),

View file

@ -17,6 +17,7 @@ import {Store} from 'flux/utils';
import dis from '../dispatcher'; import dis from '../dispatcher';
import DMRoomMap from '../utils/DMRoomMap'; import DMRoomMap from '../utils/DMRoomMap';
import Unread from '../Unread'; import Unread from '../Unread';
import SettingsStore from "../settings/SettingsStore";
/** /**
* A class for storing application state for categorising rooms in * A class for storing application state for categorising rooms in
@ -53,6 +54,24 @@ class RoomListStore extends Store {
"im.vector.fake.archived": [], "im.vector.fake.archived": [],
}, },
ready: false, ready: false,
// The room cache stores a mapping of roomId to cache record.
// Each cache record is a key/value pair for various bits of
// data used to sort the room list. Currently this stores the
// following bits of informations:
// "timestamp": number, The timestamp of the last relevant
// event in the room.
// "notifications": boolean, Whether or not the user has been
// highlighted on any unread events.
// "unread": boolean, Whether or not the user has any
// unread events.
//
// All of the cached values are lazily loaded on read in the
// recents comparator. When an event is received for a particular
// room, all the cached values are invalidated - forcing the
// next read to set new values. The entries do not expire on
// their own.
roomCache: {},
}; };
} }
@ -84,6 +103,8 @@ class RoomListStore extends Store {
!payload.isLiveUnfilteredRoomTimelineEvent || !payload.isLiveUnfilteredRoomTimelineEvent ||
!this._eventTriggersRecentReorder(payload.event) !this._eventTriggersRecentReorder(payload.event)
) break; ) break;
this._clearCachedRoomState(payload.event.getRoomId());
this._generateRoomLists(); this._generateRoomLists();
} }
break; break;
@ -111,6 +132,8 @@ class RoomListStore extends Store {
if (liveTimeline !== eventTimeline || if (liveTimeline !== eventTimeline ||
!this._eventTriggersRecentReorder(payload.event) !this._eventTriggersRecentReorder(payload.event)
) break; ) break;
this._clearCachedRoomState(payload.event.getRoomId());
this._generateRoomLists(); this._generateRoomLists();
} }
break; break;
@ -119,6 +142,13 @@ class RoomListStore extends Store {
this._generateRoomLists(); this._generateRoomLists();
} }
break; break;
case 'MatrixActions.Room.accountData': {
if (payload.event_type === 'm.fully_read') {
this._clearCachedRoomState(payload.room.roomId);
this._generateRoomLists();
}
}
break;
case 'MatrixActions.Room.myMembership': { case 'MatrixActions.Room.myMembership': {
this._generateRoomLists(); this._generateRoomLists();
} }
@ -216,11 +246,18 @@ class RoomListStore extends Store {
} }
}); });
// Note: we check the settings up here instead of in the forEach or
// in the _recentsComparator to avoid hitting the SettingsStore a few
// thousand times.
const pinUnread = SettingsStore.getValue("pinUnreadRooms");
const pinMentioned = SettingsStore.getValue("pinMentionedRooms");
Object.keys(lists).forEach((listKey) => { Object.keys(lists).forEach((listKey) => {
let comparator; let comparator;
switch (RoomListStore._listOrders[listKey]) { switch (RoomListStore._listOrders[listKey]) {
case "recent": case "recent":
comparator = this._recentsComparator; comparator = (roomA, roomB) => {
return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned);
};
break; break;
case "manual": case "manual":
default: default:
@ -236,6 +273,44 @@ class RoomListStore extends Store {
}); });
} }
_updateCachedRoomState(roomId, type, value) {
const roomCache = this._state.roomCache;
if (!roomCache[roomId]) roomCache[roomId] = {};
if (value) roomCache[roomId][type] = value;
else delete roomCache[roomId][type];
this._setState({roomCache});
}
_clearCachedRoomState(roomId) {
const roomCache = this._state.roomCache;
delete roomCache[roomId];
this._setState({roomCache});
}
_getRoomState(room, type) {
const roomId = room.roomId;
const roomCache = this._state.roomCache;
if (roomCache[roomId] && typeof roomCache[roomId][type] !== 'undefined') {
return roomCache[roomId][type];
}
if (type === "timestamp") {
const ts = this._tsOfNewestEvent(room);
this._updateCachedRoomState(roomId, "timestamp", ts);
return ts;
} else if (type === "unread") {
const unread = room.getUnreadNotificationCount() > 0;
this._updateCachedRoomState(roomId, "unread", unread);
return unread;
} else if (type === "notifications") {
const notifs = room.getUnreadNotificationCount("highlight") > 0;
this._updateCachedRoomState(roomId, "notifications", notifs);
return notifs;
} else throw new Error("Unrecognized room cache type: " + type);
}
_eventTriggersRecentReorder(ev) { _eventTriggersRecentReorder(ev) {
return ev.getTs() && ( return ev.getTs() && (
Unread.eventTriggersUnreadCount(ev) || Unread.eventTriggersUnreadCount(ev) ||
@ -261,10 +336,40 @@ class RoomListStore extends Store {
} }
} }
_recentsComparator(roomA, roomB) { _recentsComparator(roomA, roomB, pinUnread, pinMentioned) {
// XXX: We could use a cache here and update it when we see new // We try and set the ordering to be Mentioned > Unread > Recent
// events that trigger a reorder // assuming the user has the right settings, of course.
return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA);
const timestampA = this._getRoomState(roomA, "timestamp");
const timestampB = this._getRoomState(roomB, "timestamp");
const timestampDiff = timestampB - timestampA;
if (pinMentioned) {
const mentionsA = this._getRoomState(roomA, "notifications");
const mentionsB = this._getRoomState(roomB, "notifications");
if (mentionsA && !mentionsB) return -1;
if (!mentionsA && mentionsB) return 1;
// If they both have notifications, sort by timestamp.
// If neither have notifications (the fourth check not shown
// here), then try and sort by unread messages and finally by
// timestamp.
if (mentionsA && mentionsB) return timestampDiff;
}
if (pinUnread) {
const unreadA = this._getRoomState(roomA, "unread");
const unreadB = this._getRoomState(roomB, "unread");
if (unreadA && !unreadB) return -1;
if (!unreadA && unreadB) return 1;
// If they both have unread messages, sort by timestamp
// If nether have unread message (the fourth check not shown
// here), then just sort by timestamp anyways.
if (unreadA && unreadB) return timestampDiff;
}
return timestampDiff;
} }
_lexicographicalComparator(roomA, roomB) { _lexicographicalComparator(roomA, roomB) {