Merge branch 'experimental' into bwindels/memberqueryfield

pull/21833/head
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'),
'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: [
path.resolve('./test'),
"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',
externals: {
// Don't try to bundle electron: leave it as a commonjs dependency

View File

@ -76,7 +76,6 @@
"lodash": "^4.13.1",
"lolex": "2.3.2",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#develop",
"memfs": "^2.10.1",
"optimist": "^0.6.1",
"pako": "^1.0.5",
"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
*/
.mx_AutoHideScrollbar {
overflow-x: hidden;
overflow-y: auto;
-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
by playing with the paddings. the default below will add a right padding
of the scrollbar width and clear that on hover.
this won't work well on classes that also need to set their padding,
so this needs to be overriden and adjust the padding with calc like so:
```
body.mx_scrollbar_nooverlay .componentClass.mx_AutoHideScrollbar_overflow:hover {
padding-right: calc(15px - var(--scrollbar-width)) !important;
}
```
by having giving the child element (.mx_AutoHideScrollbar_offset) a
negative right margin of the width of the scrollbar when the container
is overflowing. This is what Firefox ends up using. Overflow is detected
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 .mx_AutoHideScrollbar {
box-sizing: border-box;
overflow-y: hidden;
padding-right: var(--scrollbar-width);
}
body.mx_scrollbar_nooverlay .mx_AutoHideScrollbar:hover {
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 {
margin-bottom: 8px;
margin-bottom: 4px;
}
.mx_RoomSubList_labelContainer {
display: flex;
flex-direction: row;
align-items: center;
flex: 0 0 auto;
margin: 8px 19px 0 0;
margin: 0 16px;
height: 36px;
}
.mx_RoomSubList_label {
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;
color: $roomsublist-label-fg-color;
font-weight: 700;
font-size: 12px;
margin-left: 16px;
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; */
margin-left: 8px;
}
.mx_RoomSubList_badge {
height: 18px;
border-radius: 9px;
flex: 0 0 auto;
border-radius: 8px;
color: $accent-fg-color;
font-weight: 600;
font-size: 12px;
vertical-align: middle;
line-height: 18px;
padding: 0 4px;
padding: 0 5px;
background-color: $accent-color;
}
.mx_RoomSubList_label .mx_RoomSubList_badge:hover {
filter: brightness($focus-brightness);
}
.mx_RoomSubList_addRoom, .mx_RoomSubList_badge {
margin: 5px;
margin-left: 7px;
}
.mx_RoomSubList_addRoom {
background-color: $roomheader-addroom-color;
color: $roomsublist-background;
border-radius: 9px;
text-align: center;
vertical-align: middle;
line-height: 18px;
font-weight: bold;
font-size: 18px;
width: 18px;
height: 18px;
background-image: url('../../img/icons-room-add.svg');
background-repeat: no-repeat;
background-position: center;
border-radius: 10px; // 16/2 + 2 padding
height: 16px;
flex: 0 0 16px;
background-clip: content-box;
}
.mx_RoomSubList_badgeHighlight {
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 {
left: 0px;
pointer-events: none;
position: absolute;
top: 11px;
width: 9px;
height: 4px;
background-image: url('../../img/topleft-chevron.svg');
background-size: cover;
// the transition doesn't work as the chevron gets remounted
transition: rotateZ 0.2s ease-in;
background-repeat: no-repeat;
transition: transform 0.2s ease-in;
width: 10px;
height: 10px;
background-position: center;
margin-left: 2px;
}
.mx_RoomSubList_chevronDown {
@ -131,47 +107,26 @@ limitations under the License.
.mx_RoomSubList_scroll {
/* let rooms list grab all available space */
flex: 0 1 auto;
padding: 0 15px !important;
}
/*
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;
padding: 0 8px;
}
.collapsed {
.mx_RoomSubList_label {
height: 17px;
width: 28px; /* collapsed LHS Panel width */
.mx_RoomSubList_scroll {
padding: 0;
}
.mx_RoomSubList_labelContainer {
width: 28px; /* collapsed LHS Panel width */
margin-right: 14px;
margin-left: 2px;
}
/* Hide the bottom of speech bubble */
.mx_RoomSubList_badgeHighlight:after {
display: none;
.mx_RoomSubList_addRoom {
margin-left: 3px;
margin-right: 28px;
}
.mx_RoomSubList_line {
display: none;
}
.mx_RoomSubList_moreBadge {
position: static;
margin-left: 16px;
margin-top: 2px;
}
.mx_RoomSubList_ellipsis {
height: 20px;
}
.mx_RoomSubList_more {
.mx_RoomSubList_label > span {
display: none;
}
}

View File

@ -21,10 +21,30 @@ limitations under the License.
cursor: pointer;
height: 40px;
margin: 0;
padding: 2px 12px;
padding: 0 8px 0 10px;
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 {
display: inline-block;
position: relative;
@ -62,14 +82,12 @@ limitations under the License.
text-overflow: ellipsis;
}
.mx_RoomTile_invite {
/* color: rgba(69, 69, 69, 0.5); */
}
.collapsed {
.mx_RoomTile {
margin: 2px;
padding: 2px 0 2px 12px;
margin: 0 2px;
padding: 0 2px;
position: relative;
justify-content: center;
}
.mx_RoomTile_name {
@ -77,57 +95,26 @@ limitations under the License.
}
.mx_RoomTile_badge {
display: block;
position: absolute;
height: 15px;
right: 8px;
top: 2px;
min-width: 12px;
right: 6px;
top: 0px;
border-radius: 16px;
padding: 0px 4px 0px 4px;
z-index: 3;
border: 0.18em solid $secondary-accent-color;
}
/* Hide the bottom of speech bubble */
.mx_RoomTile_highlight .mx_RoomTile_badge:after {
display: none;
.mx_RoomTile_menuButton {
display: none; //no design for this for now
}
}
/* 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 {
flex: 0 1 content;
min-width: 15px;
border-radius: 8px;
border-radius: 0.8em;
padding: 0 0.4em;
color: $accent-fg-color;
font-weight: 600;
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 {
@ -169,10 +156,6 @@ limitations under the License.
background-color: $roomtile-focused-bg-color;
}
.mx_RoomTile .mx_RoomTile_name.mx_RoomTile_badgeShown {
width: 140px;
}
.mx_RoomTile_arrow {
position: absolute;
right: 0px;

View File

@ -1,23 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="svg4196" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 25 25"
style="enable-background:new 0 0 25 25;" xml:space="preserve">
<style type="text/css">
.st1{opacity:0.7;}
.st2{fill:none;stroke-linecap:round;}
</style>
<g id="icons_people" transform="translate(50.000000, 725.000000)">
<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
S-44.4-700-37.5-700z"/>
<g id="text3879" transform="matrix(1.0243293,0,0,0.97624855,-24.996028,0.15844144)">
<g id="Group-3" transform="translate(14.4375,3.9375)" class="st1">
<path id="Line" class="st2" stroke="#ffffff" d="M-23.2-733.8h4.6"/>
<path id="path3142" class="st2" stroke="#ffffff" d="M-20.9-736.2v4.8"/>
</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>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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="11.521363"
height="11.521363"
viewBox="0 0 11.521363 11.521363"
version="1.1"
id="svg4"
sodipodi:docname="icons-room-add.svg"
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>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,17 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>dropdown</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
<g id="matrix-my-stuff-no-lines-message-context-menu-smaller-icons" transform="translate(-203.000000, -25.000000)" stroke="#212121" stroke-width="1.3">
<g id="Group-3" transform="translate(128.000000, 15.000000)">
<g id="dropdown" transform="translate(76.000000, 11.000000)">
<path d="M0.5,0.5 L4.35868526,3.75422271" id="Line-5"></path>
<path d="M8.13193273,0.560421385 L4.35868526,3.75422271" id="Line-5-Copy"></path>
</g>
</g>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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 -->
<title
id="title2">dropdown</title>
<desc
id="desc4">Created with Sketch.</desc>
<defs
id="defs6" />
<g
id="Page-1"
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>
</svg>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1017 B

After

Width:  |  Height:  |  Size: 2.6 KiB

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
* @type {Object}
@ -201,6 +230,7 @@ export default {
start(matrixClient) {
this._addMatrixClientListener(matrixClient, 'sync', createSyncAction);
this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room.accountData', createRoomAccountDataAction);
this._addMatrixClientListener(matrixClient, 'Room', createRoomAction);
this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction);
this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction);

View File

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

View File

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

View File

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

View File

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

View File

@ -220,7 +220,7 @@ module.exports = React.createClass({
this.setState( { badgeHover: false } );
},
onBadgeClicked: function(e) {
onOpenMenu: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
// Only allow non-guests to access the context menu
@ -276,19 +276,14 @@ module.exports = React.createClass({
if (name == undefined || name == null) name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badgeContent;
if (this.state.badgeHover || this.state.menuDisplayed) {
badgeContent = "\u00B7\u00B7\u00B7";
} else if (badges) {
let badge;
if (badges) {
const limitedCount = FormattingUtils.formatCount(notificationCount);
badgeContent = notificationCount ? limitedCount : '!';
} else {
badgeContent = '\u200B';
const badgeContent = notificationCount ? limitedCount : '!';
badge = <div className={badgeClasses}>{ badgeContent }</div>;
}
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
const EmojiText = sdk.getComponent('elements.EmojiText');
let label;
let tooltip;
@ -317,6 +312,11 @@ module.exports = React.createClass({
// 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');
let dmIndicator;
@ -338,6 +338,7 @@ module.exports = React.createClass({
</div>
</div>
{ label }
{ contextMenuButton }
{ badge }
{ /* { incomingCallBox } */ }
{ 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 by default for participants in this room": "Enable URL previews by default for participants in this room",
"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",
"Show empty room list headings": "Show empty room list headings",
"Collecting app version information": "Collecting app version information",

View File

@ -276,6 +276,16 @@ export const SETTINGS = {
default: true,
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": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable widget screenshots on supported widgets'),

View File

@ -17,6 +17,7 @@ import {Store} from 'flux/utils';
import dis from '../dispatcher';
import DMRoomMap from '../utils/DMRoomMap';
import Unread from '../Unread';
import SettingsStore from "../settings/SettingsStore";
/**
* A class for storing application state for categorising rooms in
@ -53,6 +54,24 @@ class RoomListStore extends Store {
"im.vector.fake.archived": [],
},
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 ||
!this._eventTriggersRecentReorder(payload.event)
) break;
this._clearCachedRoomState(payload.event.getRoomId());
this._generateRoomLists();
}
break;
@ -111,6 +132,8 @@ class RoomListStore extends Store {
if (liveTimeline !== eventTimeline ||
!this._eventTriggersRecentReorder(payload.event)
) break;
this._clearCachedRoomState(payload.event.getRoomId());
this._generateRoomLists();
}
break;
@ -119,6 +142,13 @@ class RoomListStore extends Store {
this._generateRoomLists();
}
break;
case 'MatrixActions.Room.accountData': {
if (payload.event_type === 'm.fully_read') {
this._clearCachedRoomState(payload.room.roomId);
this._generateRoomLists();
}
}
break;
case 'MatrixActions.Room.myMembership': {
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) => {
let comparator;
switch (RoomListStore._listOrders[listKey]) {
case "recent":
comparator = this._recentsComparator;
comparator = (roomA, roomB) => {
return this._recentsComparator(roomA, roomB, pinUnread, pinMentioned);
};
break;
case "manual":
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) {
return ev.getTs() && (
Unread.eventTriggersUnreadCount(ev) ||
@ -261,10 +336,40 @@ class RoomListStore extends Store {
}
}
_recentsComparator(roomA, roomB) {
// XXX: We could use a cache here and update it when we see new
// events that trigger a reorder
return this._tsOfNewestEvent(roomB) - this._tsOfNewestEvent(roomA);
_recentsComparator(roomA, roomB, pinUnread, pinMentioned) {
// We try and set the ordering to be Mentioned > Unread > Recent
// assuming the user has the right settings, of course.
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) {