forked from mystiq/hydrogen-web
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);
|
||||
if (newTile) {
|
||||
if (prevTile) {
|
||||
prevTile.updateNextSibling(newTile);
|
||||
// this emits an update while the add hasn't been emitted yet
|
||||
newTile.updatePreviousSibling(prevTile);
|
||||
}
|
||||
if (nextTile) {
|
||||
newTile.updateNextSibling(nextTile);
|
||||
nextTile.updatePreviousSibling(newTile);
|
||||
}
|
||||
prevTile?.updateNextSibling(newTile);
|
||||
// this emits an update while the add hasn't been emitted yet
|
||||
newTile.updatePreviousSibling(prevTile);
|
||||
newTile.updateNextSibling(nextTile);
|
||||
nextTile?.updatePreviousSibling(newTile);
|
||||
this._tiles.splice(tileIdx, 0, newTile);
|
||||
this.emitAdd(tileIdx, newTile);
|
||||
// add event is emitted, now the tile
|
||||
|
|
|
@ -21,7 +21,6 @@ import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../
|
|||
export class BaseMessageTile extends SimpleTile {
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||
this._isContinuation = false;
|
||||
this._reactions = null;
|
||||
this._replyTile = null;
|
||||
|
@ -78,10 +77,6 @@ export class BaseMessageTile extends SimpleTile {
|
|||
return this.displayName;
|
||||
}
|
||||
|
||||
get date() {
|
||||
return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
|
||||
}
|
||||
|
||||
get time() {
|
||||
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);
|
||||
if (action?.shouldReplace || !this._replyTile) {
|
||||
this.disposeTracked(this._replyTile);
|
||||
const tileClassForEntry = this._options.tileClassForEntry;
|
||||
const ReplyTile = tileClassForEntry(replyEntry);
|
||||
const ReplyTile = this._options.tileClassForEntry(replyEntry);
|
||||
if (ReplyTile) {
|
||||
this._replyTile = new ReplyTile(replyEntry, this._options);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ export class SimpleTile extends ViewModel {
|
|||
constructor(entry, options) {
|
||||
super(options);
|
||||
this._entry = entry;
|
||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||
this._hasDateSeparator = false;
|
||||
this._emitUpdate = undefined;
|
||||
}
|
||||
// view model props for all subclasses
|
||||
|
@ -38,13 +40,35 @@ export class SimpleTile extends ViewModel {
|
|||
}
|
||||
|
||||
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() {
|
||||
return this._entry.asEventKey();
|
||||
}
|
||||
|
||||
get date() {
|
||||
return this._date && this._date.toLocaleDateString({}, {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'});
|
||||
}
|
||||
|
||||
get eventId() {
|
||||
return this._entry.id;
|
||||
}
|
||||
|
@ -122,9 +146,12 @@ export class SimpleTile extends ViewModel {
|
|||
tryIncludeEntry() {
|
||||
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
|
||||
|
|
|
@ -103,7 +103,7 @@ limitations under the License.
|
|||
/* reset body margin */
|
||||
margin: 0;
|
||||
/* first try break-all, then break-word, which isn't supported everywhere */
|
||||
word-break: break-all;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
@ -422,3 +422,17 @@ only loads when the top comes into view*/
|
|||
.GapView.isAtTop {
|
||||
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",
|
||||
onClick: () => this._clearReplyingTo()
|
||||
}, "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"}, [
|
||||
|
|
|
@ -192,6 +192,7 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
|||
super({
|
||||
list: tiles,
|
||||
onItemClick: (tileView, evt) => tileView.onClick(evt),
|
||||
tagName: "div"
|
||||
}, tile => {
|
||||
const TileView = viewClassForTile(tile);
|
||||
return new TileView(tile, viewClassForTile);
|
||||
|
|
|
@ -14,18 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TemplateView} from "../../../general/TemplateView";
|
||||
import {BaseTileView} from "./BaseTileView";
|
||||
|
||||
export class AnnouncementView extends TemplateView {
|
||||
// ignore other arguments
|
||||
constructor(vm) {
|
||||
super(vm);
|
||||
export class AnnouncementView extends BaseTileView {
|
||||
renderTile(t, vm) {
|
||||
return t.div({className: "AnnouncementView"}, t.div(vm => vm.announcement));
|
||||
}
|
||||
|
||||
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 {tag} from "../../../general/html";
|
||||
import {mountView} from "../../../general/utils";
|
||||
import {TemplateView} from "../../../general/TemplateView";
|
||||
import {BaseTileView} from "./BaseTileView";
|
||||
import {Popup} from "../../../general/Popup.js";
|
||||
import {Menu} from "../../../general/Menu.js";
|
||||
import {ReactionsView} from "./ReactionsView.js";
|
||||
|
||||
export class BaseMessageView extends TemplateView {
|
||||
constructor(value, viewClassForTile, renderFlags, tagName = "li") {
|
||||
super(value);
|
||||
export class BaseMessageView extends BaseTileView {
|
||||
constructor(value, viewClassForTile, renderFlags) {
|
||||
super(value, viewClassForTile);
|
||||
this._menuPopup = null;
|
||||
this._tagName = tagName;
|
||||
this._viewClassForTile = viewClassForTile;
|
||||
// TODO An enum could be nice to make code easier to read at call sites.
|
||||
this._renderFlags = renderFlags;
|
||||
}
|
||||
|
@ -36,12 +34,12 @@ export class BaseMessageView extends TemplateView {
|
|||
get _interactive() { return this._renderFlags?.interactive ?? true; }
|
||||
get _isReplyPreview() { return this._renderFlags?.reply; }
|
||||
|
||||
render(t, vm) {
|
||||
renderTile(t, vm) {
|
||||
const children = [this.renderMessageBody(t, vm)];
|
||||
if (this._interactive) {
|
||||
children.push(t.button({className: "Timeline_messageOptions"}, "⋯"));
|
||||
}
|
||||
const li = t.el(this._tagName, {
|
||||
const tile = t.div({
|
||||
className: {
|
||||
"Timeline_message": true,
|
||||
own: vm.isOwn,
|
||||
|
@ -59,13 +57,13 @@ export class BaseMessageView extends TemplateView {
|
|||
// don't use `t` from within the side-effect callback
|
||||
t.mapSideEffect(vm => vm.isContinuation, (isContinuation, wasContinuation) => {
|
||||
if (isContinuation && wasContinuation === false) {
|
||||
li.removeChild(li.querySelector(".Timeline_messageAvatar"));
|
||||
li.removeChild(li.querySelector(".Timeline_messageSender"));
|
||||
tile.removeChild(tile.querySelector(".Timeline_messageAvatar"));
|
||||
tile.removeChild(tile.querySelector(".Timeline_messageSender"));
|
||||
} else if (!isContinuation && !this._isReplyPreview) {
|
||||
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);
|
||||
li.insertBefore(avatar, li.firstChild);
|
||||
li.insertBefore(sender, li.firstChild);
|
||||
tile.insertBefore(avatar, tile.firstChild);
|
||||
tile.insertBefore(sender, tile.firstChild);
|
||||
}
|
||||
});
|
||||
// similarly, we could do this with a simple ifView,
|
||||
|
@ -75,15 +73,15 @@ export class BaseMessageView extends TemplateView {
|
|||
if (reactions && this._interactive && !reactionsView) {
|
||||
reactionsView = new ReactionsView(reactions);
|
||||
this.addSubView(reactionsView);
|
||||
li.appendChild(mountView(reactionsView));
|
||||
tile.appendChild(mountView(reactionsView));
|
||||
} else if (!reactions && reactionsView) {
|
||||
li.removeChild(reactionsView.root());
|
||||
tile.removeChild(reactionsView.root());
|
||||
reactionsView.unmount();
|
||||
this.removeSubView(reactionsView);
|
||||
reactionsView = null;
|
||||
}
|
||||
});
|
||||
return li;
|
||||
return tile;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
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({
|
||||
className: {
|
||||
"Timeline_messageBody": true,
|
||||
|
|
Loading…
Reference in a new issue