Merge branch 'develop' into experimental
commit
c19b593f5c
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue