Compare commits
7 commits
master
...
bwindels/d
Author | SHA1 | Date | |
---|---|---|---|
|
2bba8f09ed | ||
|
fa534b0ca9 | ||
|
457c096c9b | ||
|
a488ff143e | ||
|
49752a0e7c | ||
|
6697bb8ddf | ||
|
e69a39d6a7 |
10 changed files with 127 additions and 49 deletions
|
@ -130,15 +130,11 @@ export class TilesCollection extends BaseObservableList {
|
||||||
|
|
||||||
const newTile = this._createTile(entry);
|
const newTile = this._createTile(entry);
|
||||||
if (newTile) {
|
if (newTile) {
|
||||||
if (prevTile) {
|
prevTile?.updateNextSibling(newTile);
|
||||||
prevTile.updateNextSibling(newTile);
|
|
||||||
// this emits an update while the add hasn't been emitted yet
|
// this emits an update while the add hasn't been emitted yet
|
||||||
newTile.updatePreviousSibling(prevTile);
|
newTile.updatePreviousSibling(prevTile);
|
||||||
}
|
|
||||||
if (nextTile) {
|
|
||||||
newTile.updateNextSibling(nextTile);
|
newTile.updateNextSibling(nextTile);
|
||||||
nextTile.updatePreviousSibling(newTile);
|
nextTile?.updatePreviousSibling(newTile);
|
||||||
}
|
|
||||||
this._tiles.splice(tileIdx, 0, newTile);
|
this._tiles.splice(tileIdx, 0, newTile);
|
||||||
this.emitAdd(tileIdx, newTile);
|
this.emitAdd(tileIdx, newTile);
|
||||||
// add event is emitted, now the tile
|
// add event is emitted, now the tile
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../
|
||||||
export class BaseMessageTile extends SimpleTile {
|
export class BaseMessageTile extends SimpleTile {
|
||||||
constructor(entry, options) {
|
constructor(entry, options) {
|
||||||
super(entry, options);
|
super(entry, options);
|
||||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
|
||||||
this._isContinuation = false;
|
this._isContinuation = false;
|
||||||
this._reactions = null;
|
this._reactions = null;
|
||||||
this._replyTile = null;
|
this._replyTile = null;
|
||||||
|
@ -78,10 +77,6 @@ export class BaseMessageTile extends SimpleTile {
|
||||||
return this.displayName;
|
return this.displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
get date() {
|
|
||||||
return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
|
|
||||||
}
|
|
||||||
|
|
||||||
get time() {
|
get time() {
|
||||||
return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"});
|
return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"});
|
||||||
}
|
}
|
||||||
|
@ -138,8 +133,7 @@ export class BaseMessageTile extends SimpleTile {
|
||||||
const action = this._replyTile?.updateEntry(replyEntry, param);
|
const action = this._replyTile?.updateEntry(replyEntry, param);
|
||||||
if (action?.shouldReplace || !this._replyTile) {
|
if (action?.shouldReplace || !this._replyTile) {
|
||||||
this.disposeTracked(this._replyTile);
|
this.disposeTracked(this._replyTile);
|
||||||
const tileClassForEntry = this._options.tileClassForEntry;
|
const ReplyTile = this._options.tileClassForEntry(replyEntry);
|
||||||
const ReplyTile = tileClassForEntry(replyEntry);
|
|
||||||
if (ReplyTile) {
|
if (ReplyTile) {
|
||||||
this._replyTile = new ReplyTile(replyEntry, this._options);
|
this._replyTile = new ReplyTile(replyEntry, this._options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ export class SimpleTile extends ViewModel {
|
||||||
constructor(entry, options) {
|
constructor(entry, options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._entry = entry;
|
this._entry = entry;
|
||||||
|
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||||
|
this._hasDateSeparator = false;
|
||||||
this._emitUpdate = undefined;
|
this._emitUpdate = undefined;
|
||||||
}
|
}
|
||||||
// view model props for all subclasses
|
// view model props for all subclasses
|
||||||
|
@ -38,13 +40,35 @@ export class SimpleTile extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasDateSeparator() {
|
get hasDateSeparator() {
|
||||||
return false;
|
return this._hasDateSeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateDateSeparator(prev) {
|
||||||
|
let hasDateSeparator;
|
||||||
|
if (prev instanceof SimpleTile) {
|
||||||
|
if (prev && prev._date) {
|
||||||
|
hasDateSeparator = prev._date.getFullYear() !== this._date.getFullYear() ||
|
||||||
|
prev._date.getMonth() !== this._date.getMonth() ||
|
||||||
|
prev._date.getDate() !== this._date.getDate();
|
||||||
|
} else {
|
||||||
|
hasDateSeparator = !!this._date;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasDateSeparator = true;
|
||||||
|
}
|
||||||
|
const changed = hasDateSeparator !== this._hasDateSeparator;
|
||||||
|
this._hasDateSeparator = hasDateSeparator;
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
get id() {
|
get id() {
|
||||||
return this._entry.asEventKey();
|
return this._entry.asEventKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get date() {
|
||||||
|
return this._date && this._date.toLocaleDateString({}, {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'});
|
||||||
|
}
|
||||||
|
|
||||||
get eventId() {
|
get eventId() {
|
||||||
return this._entry.id;
|
return this._entry.id;
|
||||||
}
|
}
|
||||||
|
@ -122,9 +146,12 @@ export class SimpleTile extends ViewModel {
|
||||||
tryIncludeEntry() {
|
tryIncludeEntry() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// let item know it has a new sibling
|
|
||||||
updatePreviousSibling(/*prev*/) {
|
|
||||||
|
|
||||||
|
// let item know it has a new sibling
|
||||||
|
updatePreviousSibling(prev) {
|
||||||
|
if (this._updateDateSeparator(prev)) {
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// let item know it has a new sibling
|
// let item know it has a new sibling
|
||||||
|
|
|
@ -422,3 +422,17 @@ only loads when the top comes into view*/
|
||||||
.GapView.isAtTop {
|
.GapView.isAtTop {
|
||||||
padding: 52px 20px 12px 20px;
|
padding: 52px 20px 12px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.DateSeparator {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DateSeparator > time {
|
||||||
|
background-color: var(--background-color-primary--darker-10);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ export class MessageComposer extends TemplateView {
|
||||||
className: "cancel",
|
className: "cancel",
|
||||||
onClick: () => this._clearReplyingTo()
|
onClick: () => this._clearReplyingTo()
|
||||||
}, "Close"),
|
}, "Close"),
|
||||||
t.view(new TileView(rvm, this._viewClassForTile, { interactive: false }, "div"))
|
t.view(new TileView(rvm, this._viewClassForTile, { interactive: false }))
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
const input = t.div({className: "MessageComposer_input"}, [
|
const input = t.div({className: "MessageComposer_input"}, [
|
||||||
|
|
|
@ -192,6 +192,7 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
||||||
super({
|
super({
|
||||||
list: tiles,
|
list: tiles,
|
||||||
onItemClick: (tileView, evt) => tileView.onClick(evt),
|
onItemClick: (tileView, evt) => tileView.onClick(evt),
|
||||||
|
tagName: "div"
|
||||||
}, tile => {
|
}, tile => {
|
||||||
const TileView = viewClassForTile(tile);
|
const TileView = viewClassForTile(tile);
|
||||||
return new TileView(tile, viewClassForTile);
|
return new TileView(tile, viewClassForTile);
|
||||||
|
|
|
@ -14,18 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TemplateView} from "../../../general/TemplateView";
|
import {BaseTileView} from "./BaseTileView";
|
||||||
|
|
||||||
export class AnnouncementView extends TemplateView {
|
export class AnnouncementView extends BaseTileView {
|
||||||
// ignore other arguments
|
renderTile(t, vm) {
|
||||||
constructor(vm) {
|
return t.div({className: "AnnouncementView"}, t.div(vm => vm.announcement));
|
||||||
super(vm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(t) {
|
|
||||||
return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* This is called by the parent ListView, which just has 1 listener for the whole list */
|
|
||||||
onClick() {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,17 +18,15 @@ limitations under the License.
|
||||||
import {renderStaticAvatar} from "../../../avatar";
|
import {renderStaticAvatar} from "../../../avatar";
|
||||||
import {tag} from "../../../general/html";
|
import {tag} from "../../../general/html";
|
||||||
import {mountView} from "../../../general/utils";
|
import {mountView} from "../../../general/utils";
|
||||||
import {TemplateView} from "../../../general/TemplateView";
|
import {BaseTileView} from "./BaseTileView";
|
||||||
import {Popup} from "../../../general/Popup.js";
|
import {Popup} from "../../../general/Popup.js";
|
||||||
import {Menu} from "../../../general/Menu.js";
|
import {Menu} from "../../../general/Menu.js";
|
||||||
import {ReactionsView} from "./ReactionsView.js";
|
import {ReactionsView} from "./ReactionsView.js";
|
||||||
|
|
||||||
export class BaseMessageView extends TemplateView {
|
export class BaseMessageView extends BaseTileView {
|
||||||
constructor(value, viewClassForTile, renderFlags, tagName = "li") {
|
constructor(value, viewClassForTile, renderFlags) {
|
||||||
super(value);
|
super(value, viewClassForTile);
|
||||||
this._menuPopup = null;
|
this._menuPopup = null;
|
||||||
this._tagName = tagName;
|
|
||||||
this._viewClassForTile = viewClassForTile;
|
|
||||||
// TODO An enum could be nice to make code easier to read at call sites.
|
// TODO An enum could be nice to make code easier to read at call sites.
|
||||||
this._renderFlags = renderFlags;
|
this._renderFlags = renderFlags;
|
||||||
}
|
}
|
||||||
|
@ -36,12 +34,12 @@ export class BaseMessageView extends TemplateView {
|
||||||
get _interactive() { return this._renderFlags?.interactive ?? true; }
|
get _interactive() { return this._renderFlags?.interactive ?? true; }
|
||||||
get _isReplyPreview() { return this._renderFlags?.reply; }
|
get _isReplyPreview() { return this._renderFlags?.reply; }
|
||||||
|
|
||||||
render(t, vm) {
|
renderTile(t, vm) {
|
||||||
const children = [this.renderMessageBody(t, vm)];
|
const children = [this.renderMessageBody(t, vm)];
|
||||||
if (this._interactive) {
|
if (this._interactive) {
|
||||||
children.push(t.button({className: "Timeline_messageOptions"}, "⋯"));
|
children.push(t.button({className: "Timeline_messageOptions"}, "⋯"));
|
||||||
}
|
}
|
||||||
const li = t.el(this._tagName, {
|
const tile = t.div({
|
||||||
className: {
|
className: {
|
||||||
"Timeline_message": true,
|
"Timeline_message": true,
|
||||||
own: vm.isOwn,
|
own: vm.isOwn,
|
||||||
|
@ -59,13 +57,13 @@ export class BaseMessageView extends TemplateView {
|
||||||
// don't use `t` from within the side-effect callback
|
// don't use `t` from within the side-effect callback
|
||||||
t.mapSideEffect(vm => vm.isContinuation, (isContinuation, wasContinuation) => {
|
t.mapSideEffect(vm => vm.isContinuation, (isContinuation, wasContinuation) => {
|
||||||
if (isContinuation && wasContinuation === false) {
|
if (isContinuation && wasContinuation === false) {
|
||||||
li.removeChild(li.querySelector(".Timeline_messageAvatar"));
|
tile.removeChild(tile.querySelector(".Timeline_messageAvatar"));
|
||||||
li.removeChild(li.querySelector(".Timeline_messageSender"));
|
tile.removeChild(tile.querySelector(".Timeline_messageSender"));
|
||||||
} else if (!isContinuation && !this._isReplyPreview) {
|
} else if (!isContinuation && !this._isReplyPreview) {
|
||||||
const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]);
|
const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]);
|
||||||
const sender = tag.div({className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`}, vm.displayName);
|
const sender = tag.div({className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`}, vm.displayName);
|
||||||
li.insertBefore(avatar, li.firstChild);
|
tile.insertBefore(avatar, tile.firstChild);
|
||||||
li.insertBefore(sender, li.firstChild);
|
tile.insertBefore(sender, tile.firstChild);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// similarly, we could do this with a simple ifView,
|
// similarly, we could do this with a simple ifView,
|
||||||
|
@ -75,15 +73,15 @@ export class BaseMessageView extends TemplateView {
|
||||||
if (reactions && this._interactive && !reactionsView) {
|
if (reactions && this._interactive && !reactionsView) {
|
||||||
reactionsView = new ReactionsView(reactions);
|
reactionsView = new ReactionsView(reactions);
|
||||||
this.addSubView(reactionsView);
|
this.addSubView(reactionsView);
|
||||||
li.appendChild(mountView(reactionsView));
|
tile.appendChild(mountView(reactionsView));
|
||||||
} else if (!reactions && reactionsView) {
|
} else if (!reactions && reactionsView) {
|
||||||
li.removeChild(reactionsView.root());
|
tile.removeChild(reactionsView.root());
|
||||||
reactionsView.unmount();
|
reactionsView.unmount();
|
||||||
this.removeSubView(reactionsView);
|
this.removeSubView(reactionsView);
|
||||||
reactionsView = null;
|
reactionsView = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return li;
|
return tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This is called by the parent ListView, which just has 1 listener for the whole list */
|
/* This is called by the parent ListView, which just has 1 listener for the whole list */
|
||||||
|
|
56
src/platform/web/ui/session/room/timeline/BaseTileView.js
Normal file
56
src/platform/web/ui/session/room/timeline/BaseTileView.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {TemplateView} from "../../../general/TemplateView";
|
||||||
|
|
||||||
|
export class BaseTileView extends TemplateView {
|
||||||
|
// ignore other arguments
|
||||||
|
constructor(vm, viewClassForTile) {
|
||||||
|
super(vm);
|
||||||
|
this._viewClassForTile = viewClassForTile;
|
||||||
|
this._root = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
root() {
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(t, vm) {
|
||||||
|
const tile = this.renderTile(t, vm);
|
||||||
|
const swapRoot = newRoot => {
|
||||||
|
this._root?.replaceWith(newRoot);
|
||||||
|
this._root = newRoot;
|
||||||
|
}
|
||||||
|
t.mapSideEffect(vm => vm.hasDateSeparator, hasDateSeparator => {
|
||||||
|
if (hasDateSeparator) {
|
||||||
|
const container = t.div([this._renderDateSeparator(t, vm)]);
|
||||||
|
swapRoot(container);
|
||||||
|
container.appendChild(tile);
|
||||||
|
} else {
|
||||||
|
swapRoot(tile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderDateSeparator(t, vm) {
|
||||||
|
// if this needs any bindings, we need to use a subview
|
||||||
|
return t.div({className: "DateSeparator"}, t.time(vm.date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This is called by the parent ListView, which just has 1 listener for the whole list */
|
||||||
|
onClick() {}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@ import {ReplyPreviewError, ReplyPreviewView} from "./ReplyPreviewView.js";
|
||||||
|
|
||||||
export class TextMessageView extends BaseMessageView {
|
export class TextMessageView extends BaseMessageView {
|
||||||
renderMessageBody(t, vm) {
|
renderMessageBody(t, vm) {
|
||||||
const time = t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time);
|
const time = t.time({className: {hidden: vm => !vm.time}}, vm => vm.time);
|
||||||
const container = t.div({
|
const container = t.div({
|
||||||
className: {
|
className: {
|
||||||
"Timeline_messageBody": true,
|
"Timeline_messageBody": true,
|
||||||
|
|
Reference in a new issue