forked from mystiq/hydrogen-web
Merge pull request #61 from vector-im/bwindels/roomlistsorting
Add unread state, badges, highlight state and sorting to room list
This commit is contained in:
commit
5930097f84
10 changed files with 242 additions and 43 deletions
|
@ -32,6 +32,7 @@ export class RoomViewModel extends ViewModel {
|
||||||
this._sendError = null;
|
this._sendError = null;
|
||||||
this._closeCallback = closeCallback;
|
this._closeCallback = closeCallback;
|
||||||
this._composerVM = new ComposerViewModel(this);
|
this._composerVM = new ComposerViewModel(this);
|
||||||
|
this._clearUnreadTimout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
|
@ -49,6 +50,15 @@ export class RoomViewModel extends ViewModel {
|
||||||
this._timelineError = err;
|
this._timelineError = err;
|
||||||
this.emitChange("error");
|
this.emitChange("error");
|
||||||
}
|
}
|
||||||
|
this._clearUnreadTimout = this.clock.createTimeout(2000);
|
||||||
|
try {
|
||||||
|
await this._clearUnreadTimout.elapsed();
|
||||||
|
await this._room.clearUnread();
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name !== "AbortError") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
|
@ -57,6 +67,10 @@ export class RoomViewModel extends ViewModel {
|
||||||
// will stop the timeline from delivering updates on entries
|
// will stop the timeline from delivering updates on entries
|
||||||
this._timeline.close();
|
this._timeline.close();
|
||||||
}
|
}
|
||||||
|
if (this._clearUnreadTimout) {
|
||||||
|
this._clearUnreadTimout.abort();
|
||||||
|
this._clearUnreadTimout = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
|
|
@ -17,17 +17,18 @@ limitations under the License.
|
||||||
import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
|
import {avatarInitials, getIdentifierColorNumber} from "../../avatar.js";
|
||||||
import {ViewModel} from "../../ViewModel.js";
|
import {ViewModel} from "../../ViewModel.js";
|
||||||
|
|
||||||
|
function isSortedAsUnread(vm) {
|
||||||
|
return vm.isUnread || (vm.isOpen && vm._wasUnreadWhenOpening);
|
||||||
|
}
|
||||||
|
|
||||||
export class RoomTileViewModel extends ViewModel {
|
export class RoomTileViewModel extends ViewModel {
|
||||||
// we use callbacks to parent VM instead of emit because
|
|
||||||
// it would be annoying to keep track of subscriptions in
|
|
||||||
// parent for all RoomTileViewModels
|
|
||||||
// emitUpdate is ObservableMap/ObservableList update mechanism
|
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {room, emitOpen} = options;
|
const {room, emitOpen} = options;
|
||||||
this._room = room;
|
this._room = room;
|
||||||
this._emitOpen = emitOpen;
|
this._emitOpen = emitOpen;
|
||||||
this._isOpen = false;
|
this._isOpen = false;
|
||||||
|
this._wasUnreadWhenOpening = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// called by parent for now (later should integrate with router)
|
// called by parent for now (later should integrate with router)
|
||||||
|
@ -39,24 +40,53 @@ export class RoomTileViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
this._isOpen = true;
|
if (!this._isOpen) {
|
||||||
this.emitChange("isOpen");
|
this._isOpen = true;
|
||||||
this._emitOpen(this._room, this);
|
this._wasUnreadWhenOpening = this._room.isUnread;
|
||||||
|
this.emitChange("isOpen");
|
||||||
|
this._emitOpen(this._room, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compare(other) {
|
compare(other) {
|
||||||
// sort alphabetically
|
const myRoom = this._room;
|
||||||
const nameCmp = this._room.name.localeCompare(other._room.name);
|
const theirRoom = other._room;
|
||||||
if (nameCmp === 0) {
|
|
||||||
return this._room.id.localeCompare(other._room.id);
|
if (isSortedAsUnread(this) !== isSortedAsUnread(other)) {
|
||||||
|
if (isSortedAsUnread(this)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
return nameCmp;
|
const myTimestamp = myRoom.lastMessageTimestamp;
|
||||||
|
const theirTimestamp = theirRoom.lastMessageTimestamp;
|
||||||
|
// rooms with a timestamp come before rooms without one
|
||||||
|
if ((myTimestamp === null) !== (theirTimestamp === null)) {
|
||||||
|
if (theirTimestamp === null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const timeDiff = theirTimestamp - myTimestamp;
|
||||||
|
if (timeDiff === 0) {
|
||||||
|
// sort alphabetically
|
||||||
|
const nameCmp = this._room.name.localeCompare(other._room.name);
|
||||||
|
if (nameCmp === 0) {
|
||||||
|
return this._room.id.localeCompare(other._room.id);
|
||||||
|
}
|
||||||
|
return nameCmp;
|
||||||
|
}
|
||||||
|
return timeDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isOpen() {
|
get isOpen() {
|
||||||
return this._isOpen;
|
return this._isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isUnread() {
|
||||||
|
return this._room.isUnread;
|
||||||
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return this._room.name;
|
return this._room.name;
|
||||||
}
|
}
|
||||||
|
@ -80,4 +110,12 @@ export class RoomTileViewModel extends ViewModel {
|
||||||
get avatarTitle() {
|
get avatarTitle() {
|
||||||
return this.name;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get badgeCount() {
|
||||||
|
return this._room.notificationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isHighlighted() {
|
||||||
|
return this._room.highlightCount !== 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,7 +142,7 @@ export class Sync {
|
||||||
room = this._session.createRoom(roomId);
|
room = this._session.createRoom(roomId);
|
||||||
}
|
}
|
||||||
console.log(` * applying sync response to room ${roomId} ...`);
|
console.log(` * applying sync response to room ${roomId} ...`);
|
||||||
const changes = await room.writeSync(roomResponse, membership, syncTxn);
|
const changes = await room.writeSync(roomResponse, membership, isInitialSync, syncTxn);
|
||||||
roomChanges.push({room, changes});
|
roomChanges.push({room, changes});
|
||||||
});
|
});
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
|
@ -136,6 +136,11 @@ export class HomeServerApi {
|
||||||
return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options);
|
return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
receipt(roomId, receiptType, eventId, options = null) {
|
||||||
|
return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`,
|
||||||
|
{}, {}, options);
|
||||||
|
}
|
||||||
|
|
||||||
passwordLogin(username, password, options = null) {
|
passwordLogin(username, password, options = null) {
|
||||||
return this._post("/login", null, {
|
return this._post("/login", null, {
|
||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
|
|
|
@ -31,7 +31,7 @@ export class Room extends EventEmitter {
|
||||||
this._roomId = roomId;
|
this._roomId = roomId;
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._hsApi = hsApi;
|
this._hsApi = hsApi;
|
||||||
this._summary = new RoomSummary(roomId);
|
this._summary = new RoomSummary(roomId, user.id);
|
||||||
this._fragmentIdComparer = new FragmentIdComparer([]);
|
this._fragmentIdComparer = new FragmentIdComparer([]);
|
||||||
this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer});
|
this._syncWriter = new SyncWriter({roomId, fragmentIdComparer: this._fragmentIdComparer});
|
||||||
this._emitCollectionChange = emitCollectionChange;
|
this._emitCollectionChange = emitCollectionChange;
|
||||||
|
@ -42,8 +42,13 @@ export class Room extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @package */
|
/** @package */
|
||||||
async writeSync(roomResponse, membership, txn) {
|
async writeSync(roomResponse, membership, isInitialSync, txn) {
|
||||||
const summaryChanges = this._summary.writeSync(roomResponse, membership, txn);
|
const isTimelineOpen = !!this._timeline;
|
||||||
|
const summaryChanges = this._summary.writeSync(
|
||||||
|
roomResponse,
|
||||||
|
membership,
|
||||||
|
isInitialSync, isTimelineOpen,
|
||||||
|
txn);
|
||||||
const {entries, newLiveKey, changedMembers} = await this._syncWriter.writeSync(roomResponse, txn);
|
const {entries, newLiveKey, changedMembers} = await this._syncWriter.writeSync(roomResponse, txn);
|
||||||
let removedPendingEvents;
|
let removedPendingEvents;
|
||||||
if (roomResponse.timeline && roomResponse.timeline.events) {
|
if (roomResponse.timeline && roomResponse.timeline.events) {
|
||||||
|
@ -184,6 +189,64 @@ export class Room extends EventEmitter {
|
||||||
return this._summary.avatarUrl;
|
return this._summary.avatarUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get lastMessageTimestamp() {
|
||||||
|
return this._summary.lastMessageTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUnread() {
|
||||||
|
return this._summary.isUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
get notificationCount() {
|
||||||
|
return this._summary.notificationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get highlightCount() {
|
||||||
|
return this._summary.highlightCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _getLastEventId() {
|
||||||
|
const lastKey = this._syncWriter.lastMessageKey;
|
||||||
|
if (lastKey) {
|
||||||
|
const txn = await this._storage.readTxn([
|
||||||
|
this._storage.storeNames.timelineEvents,
|
||||||
|
]);
|
||||||
|
const eventEntry = await txn.timelineEvents.get(this._roomId, lastKey);
|
||||||
|
return eventEntry?.event?.event_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearUnread() {
|
||||||
|
if (this.isUnread || this.notificationCount) {
|
||||||
|
const txn = await this._storage.readWriteTxn([
|
||||||
|
this._storage.storeNames.roomSummary,
|
||||||
|
]);
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = this._summary.writeClearUnread(txn);
|
||||||
|
} catch (err) {
|
||||||
|
txn.abort();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
await txn.complete();
|
||||||
|
this._summary.applyChanges(data);
|
||||||
|
this.emit("change");
|
||||||
|
this._emitCollectionChange(this);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lastEventId = await this._getLastEventId();
|
||||||
|
if (lastEventId) {
|
||||||
|
await this._hsApi.receipt(this._roomId, "m.read", lastEventId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// ignore ConnectionError
|
||||||
|
if (err.name !== "ConnectionError") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
async openTimeline() {
|
async openTimeline() {
|
||||||
if (this._timeline) {
|
if (this._timeline) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function applySyncResponse(data, roomResponse, membership) {
|
function applySyncResponse(data, roomResponse, membership, isInitialSync, isTimelineOpen, ownUserId) {
|
||||||
if (roomResponse.summary) {
|
if (roomResponse.summary) {
|
||||||
data = updateSummary(data, roomResponse.summary);
|
data = updateSummary(data, roomResponse.summary);
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ function applySyncResponse(data, roomResponse, membership) {
|
||||||
}
|
}
|
||||||
// state comes before timeline
|
// state comes before timeline
|
||||||
if (roomResponse.state) {
|
if (roomResponse.state) {
|
||||||
data = roomResponse.state.events.reduce(processEvent, data);
|
data = roomResponse.state.events.reduce(processStateEvent, data);
|
||||||
}
|
}
|
||||||
if (roomResponse.timeline) {
|
if (roomResponse.timeline) {
|
||||||
const {timeline} = roomResponse;
|
const {timeline} = roomResponse;
|
||||||
|
@ -32,45 +32,43 @@ function applySyncResponse(data, roomResponse, membership) {
|
||||||
data = data.cloneIfNeeded();
|
data = data.cloneIfNeeded();
|
||||||
data.lastPaginationToken = timeline.prev_batch;
|
data.lastPaginationToken = timeline.prev_batch;
|
||||||
}
|
}
|
||||||
data = timeline.events.reduce(processEvent, data);
|
data = timeline.events.reduce((data, event) => {
|
||||||
|
if (typeof event.state_key === "string") {
|
||||||
|
return processStateEvent(data, event);
|
||||||
|
} else {
|
||||||
|
return processTimelineEvent(data, event,
|
||||||
|
isInitialSync, isTimelineOpen, ownUserId);
|
||||||
|
}
|
||||||
|
}, data);
|
||||||
}
|
}
|
||||||
const unreadNotifications = roomResponse.unread_notifications;
|
const unreadNotifications = roomResponse.unread_notifications;
|
||||||
if (unreadNotifications) {
|
if (unreadNotifications) {
|
||||||
data = data.cloneIfNeeded();
|
data = data.cloneIfNeeded();
|
||||||
data.highlightCount = unreadNotifications.highlight_count;
|
data.highlightCount = unreadNotifications.highlight_count || 0;
|
||||||
data.notificationCount = unreadNotifications.notification_count;
|
data.notificationCount = unreadNotifications.notification_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function processEvent(data, event) {
|
function processStateEvent(data, event) {
|
||||||
if (event.type === "m.room.encryption") {
|
if (event.type === "m.room.encryption") {
|
||||||
if (!data.isEncrypted) {
|
if (!data.isEncrypted) {
|
||||||
data = data.cloneIfNeeded();
|
data = data.cloneIfNeeded();
|
||||||
data.isEncrypted = true;
|
data.isEncrypted = true;
|
||||||
}
|
}
|
||||||
}
|
} else if (event.type === "m.room.name") {
|
||||||
if (event.type === "m.room.name") {
|
|
||||||
const newName = event.content?.name;
|
const newName = event.content?.name;
|
||||||
if (newName !== data.name) {
|
if (newName !== data.name) {
|
||||||
data = data.cloneIfNeeded();
|
data = data.cloneIfNeeded();
|
||||||
data.name = newName;
|
data.name = newName;
|
||||||
}
|
}
|
||||||
} if (event.type === "m.room.avatar") {
|
} else if (event.type === "m.room.avatar") {
|
||||||
const newUrl = event.content?.url;
|
const newUrl = event.content?.url;
|
||||||
if (newUrl !== data.avatarUrl) {
|
if (newUrl !== data.avatarUrl) {
|
||||||
data = data.cloneIfNeeded();
|
data = data.cloneIfNeeded();
|
||||||
data.avatarUrl = newUrl;
|
data.avatarUrl = newUrl;
|
||||||
}
|
}
|
||||||
} else if (event.type === "m.room.message") {
|
|
||||||
const {content} = event;
|
|
||||||
const body = content?.body;
|
|
||||||
const msgtype = content?.msgtype;
|
|
||||||
if (msgtype === "m.text") {
|
|
||||||
data = data.cloneIfNeeded();
|
|
||||||
data.lastMessageBody = body;
|
|
||||||
}
|
|
||||||
} else if (event.type === "m.room.canonical_alias") {
|
} else if (event.type === "m.room.canonical_alias") {
|
||||||
const content = event.content;
|
const content = event.content;
|
||||||
data = data.cloneIfNeeded();
|
data = data.cloneIfNeeded();
|
||||||
|
@ -80,6 +78,23 @@ function processEvent(data, event) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function processTimelineEvent(data, event, isInitialSync, isTimelineOpen, ownUserId) {
|
||||||
|
if (event.type === "m.room.message") {
|
||||||
|
data = data.cloneIfNeeded();
|
||||||
|
data.lastMessageTimestamp = event.origin_server_ts;
|
||||||
|
if (!isInitialSync && event.sender !== ownUserId && !isTimelineOpen) {
|
||||||
|
data.isUnread = true;
|
||||||
|
}
|
||||||
|
const {content} = event;
|
||||||
|
const body = content?.body;
|
||||||
|
const msgtype = content?.msgtype;
|
||||||
|
if (msgtype === "m.text") {
|
||||||
|
data.lastMessageBody = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
function updateSummary(data, summary) {
|
function updateSummary(data, summary) {
|
||||||
const heroes = summary["m.heroes"];
|
const heroes = summary["m.heroes"];
|
||||||
const inviteCount = summary["m.joined_member_count"];
|
const inviteCount = summary["m.joined_member_count"];
|
||||||
|
@ -105,10 +120,10 @@ class SummaryData {
|
||||||
this.roomId = copy ? copy.roomId : roomId;
|
this.roomId = copy ? copy.roomId : roomId;
|
||||||
this.name = copy ? copy.name : null;
|
this.name = copy ? copy.name : null;
|
||||||
this.lastMessageBody = copy ? copy.lastMessageBody : null;
|
this.lastMessageBody = copy ? copy.lastMessageBody : null;
|
||||||
this.unreadCount = copy ? copy.unreadCount : null;
|
this.lastMessageTimestamp = copy ? copy.lastMessageTimestamp : null;
|
||||||
this.mentionCount = copy ? copy.mentionCount : null;
|
this.isUnread = copy ? copy.isUnread : false;
|
||||||
this.isEncrypted = copy ? copy.isEncrypted : null;
|
this.isEncrypted = copy ? copy.isEncrypted : false;
|
||||||
this.isDirectMessage = copy ? copy.isDirectMessage : null;
|
this.isDirectMessage = copy ? copy.isDirectMessage : false;
|
||||||
this.membership = copy ? copy.membership : null;
|
this.membership = copy ? copy.membership : null;
|
||||||
this.inviteCount = copy ? copy.inviteCount : 0;
|
this.inviteCount = copy ? copy.inviteCount : 0;
|
||||||
this.joinCount = copy ? copy.joinCount : 0;
|
this.joinCount = copy ? copy.joinCount : 0;
|
||||||
|
@ -138,7 +153,8 @@ class SummaryData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RoomSummary {
|
export class RoomSummary {
|
||||||
constructor(roomId) {
|
constructor(roomId, ownUserId) {
|
||||||
|
this._ownUserId = ownUserId;
|
||||||
this._data = new SummaryData(null, roomId);
|
this._data = new SummaryData(null, roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,10 +174,26 @@ export class RoomSummary {
|
||||||
return this._data.roomId;
|
return this._data.roomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isUnread() {
|
||||||
|
return this._data.isUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
get notificationCount() {
|
||||||
|
return this._data.notificationCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
get highlightCount() {
|
||||||
|
return this._data.highlightCount;
|
||||||
|
}
|
||||||
|
|
||||||
get lastMessage() {
|
get lastMessage() {
|
||||||
return this._data.lastMessageBody;
|
return this._data.lastMessageBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get lastMessageTimestamp() {
|
||||||
|
return this._data.lastMessageTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
get inviteCount() {
|
get inviteCount() {
|
||||||
return this._data.inviteCount;
|
return this._data.inviteCount;
|
||||||
}
|
}
|
||||||
|
@ -182,6 +214,15 @@ export class RoomSummary {
|
||||||
return this._data.lastPaginationToken;
|
return this._data.lastPaginationToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeClearUnread(txn) {
|
||||||
|
const data = new SummaryData(this._data);
|
||||||
|
data.isUnread = false;
|
||||||
|
data.notificationCount = 0;
|
||||||
|
data.highlightCount = 0;
|
||||||
|
txn.roomSummary.set(data.serialize());
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
writeHasFetchedMembers(value, txn) {
|
writeHasFetchedMembers(value, txn) {
|
||||||
const data = new SummaryData(this._data);
|
const data = new SummaryData(this._data);
|
||||||
data.hasFetchedMembers = value;
|
data.hasFetchedMembers = value;
|
||||||
|
@ -189,11 +230,15 @@ export class RoomSummary {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSync(roomResponse, membership, txn) {
|
writeSync(roomResponse, membership, isInitialSync, isTimelineOpen, txn) {
|
||||||
// clear cloned flag, so cloneIfNeeded makes a copy and
|
// clear cloned flag, so cloneIfNeeded makes a copy and
|
||||||
// this._data is not modified if any field is changed.
|
// this._data is not modified if any field is changed.
|
||||||
this._data.cloned = false;
|
this._data.cloned = false;
|
||||||
const data = applySyncResponse(this._data, roomResponse, membership);
|
const data = applySyncResponse(
|
||||||
|
this._data, roomResponse,
|
||||||
|
membership,
|
||||||
|
isInitialSync, isTimelineOpen,
|
||||||
|
this._ownUserId);
|
||||||
if (data !== this._data) {
|
if (data !== this._data) {
|
||||||
// need to think here how we want to persist
|
// need to think here how we want to persist
|
||||||
// things like unread status (as read marker, or unread count)?
|
// things like unread status (as read marker, or unread count)?
|
||||||
|
|
|
@ -209,6 +209,10 @@ export class SyncWriter {
|
||||||
afterSync(newLiveKey) {
|
afterSync(newLiveKey) {
|
||||||
this._lastLiveKey = newLiveKey;
|
this._lastLiveKey = newLiveKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get lastMessageKey() {
|
||||||
|
return this._lastLiveKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//import MemoryStorage from "../storage/memory/MemoryStorage.js";
|
//import MemoryStorage from "../storage/memory/MemoryStorage.js";
|
||||||
|
|
|
@ -35,10 +35,12 @@ limitations under the License.
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LeftPanel .description > * {
|
.LeftPanel .description > .name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,8 +164,30 @@ button.styled {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LeftPanel .description .last-message {
|
.LeftPanel .description {
|
||||||
font-size: 0.8em;
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LeftPanel .name.unread {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LeftPanel .badge {
|
||||||
|
min-width: 1.6rem;
|
||||||
|
height: 1.6rem;
|
||||||
|
border-radius: 1.6rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
background-color: #61708b;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LeftPanel .badge.highlighted {
|
||||||
|
background-color: #ff4b55;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -301,6 +323,9 @@ ul.Timeline > li.continuation .profile {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.Timeline > li.continuation time {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.message-container {
|
.message-container {
|
||||||
padding: 1px 10px 0px 10px;
|
padding: 1px 10px 0px 10px;
|
||||||
|
|
|
@ -21,7 +21,10 @@ export class RoomTile extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
return t.li({"className": {"active": vm => vm.isOpen}}, [
|
return t.li({"className": {"active": vm => vm.isOpen}}, [
|
||||||
renderAvatar(t, vm, 32),
|
renderAvatar(t, vm, 32),
|
||||||
t.div({className: "description"}, t.div({className: "name"}, vm => vm.name))
|
t.div({className: "description"}, [
|
||||||
|
t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name),
|
||||||
|
t.div({className: {"badge": true, highlighted: vm => vm.isHighlighted, hidden: vm => !vm.badgeCount}}, vm => vm.badgeCount),
|
||||||
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue