diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 95e59a27..fe0cb6fe 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -1,13 +1,42 @@ import SimpleTile from "./SimpleTile"; export default class GapTile extends SimpleTile { - constructor(entry, timeline) { - super(entry); + constructor(options, timeline) { + super(options); this._timeline = timeline; + this._loading = false; + this._error = null; } - // GapTile specific behaviour - fill() { - return this._timeline.fillGap(this._entry, 10); + async fill() { + // prevent doing this twice + if (!this._loading) { + this._loading = true; + this._emitUpdate("isLoading"); + try { + return await this._timeline.fillGap(this._entry, 10); + } catch (err) { + this._loading = false; + this._error = err; + this._emitUpdate("isLoading"); + this._emitUpdate("error"); + } + } + } + + get isLoading() { + return this._loading; + } + + get direction() { + return this._entry.prev_batch ? "backward" : "forward"; + } + + get error() { + if (this._error) { + const dir = this._entry.prev_batch ? "previous" : "next"; + return `Could not load ${dir} messages: ${this._error.message}`; + } + return null; } } diff --git a/src/domain/session/room/timeline/tiles/ImageTile.js b/src/domain/session/room/timeline/tiles/ImageTile.js new file mode 100644 index 00000000..8c18491e --- /dev/null +++ b/src/domain/session/room/timeline/tiles/ImageTile.js @@ -0,0 +1,22 @@ +import MessageTile from "./MessageTile.js"; + +export default class ImageTile extends MessageTile { + constructor(options) { + super(options); + + // we start loading the image here, + // and call this._emitUpdate once it's loaded? + // or maybe we have an becameVisible() callback on tiles where we start loading it? + } + get src() { + return ""; + } + + get width() { + return 200; + } + + get height() { + return 200; + } +} diff --git a/src/domain/session/room/timeline/tiles/LocationTile.js b/src/domain/session/room/timeline/tiles/LocationTile.js new file mode 100644 index 00000000..69dbc629 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/LocationTile.js @@ -0,0 +1,20 @@ +import MessageTile from "./MessageTile.js"; + +/* +map urls: +apple: https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/MapLinks/MapLinks.html +android: https://developers.google.com/maps/documentation/urls/guide +wp: maps:49.275267 -122.988617 +https://www.habaneroconsulting.com/stories/insights/2011/opening-native-map-apps-from-the-mobile-browser +*/ +export default class LocationTile extends MessageTile { + get mapsLink() { + const geoUri = this._getContent().geo_uri; + const [lat, long] = geoUri.split(":")[1].split(","); + return `maps:${lat} ${long}`; + } + + get label() { + return `${this.sender} sent their location, click to see it in maps.`; + } +} diff --git a/src/domain/session/room/timeline/tiles/MessageTile.js b/src/domain/session/room/timeline/tiles/MessageTile.js new file mode 100644 index 00000000..f57f8379 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/MessageTile.js @@ -0,0 +1,26 @@ +import SimpleTile from "./SimpleTile.js"; + +export default class MessageTile extends SimpleTile { + + constructor(options) { + super(options); + this._date = new Date(this._entry.event.origin_server_ts); + } + + get sender() { + return this._entry.event.sender; + } + + get date() { + return this._date.toLocaleDateString(); + } + + get time() { + return this._date.toLocaleTimeString(); + } + + _getContent() { + const event = this._entry.event; + return event && event.content; + } +} diff --git a/src/domain/session/room/timeline/tiles/RoomMemberTile.js b/src/domain/session/room/timeline/tiles/RoomMemberTile.js new file mode 100644 index 00000000..df31945c --- /dev/null +++ b/src/domain/session/room/timeline/tiles/RoomMemberTile.js @@ -0,0 +1,9 @@ +import SimpleTile from "./SimpleTile.js"; + +export default class RoomNameTile extends SimpleTile { + get label() { + const event = this._entry.event; + const content = event.content; + return `${event.sender} changed membership to ${content.membership}`; + } +} diff --git a/src/domain/session/room/timeline/tiles/RoomNameTile.js b/src/domain/session/room/timeline/tiles/RoomNameTile.js new file mode 100644 index 00000000..e352b168 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/RoomNameTile.js @@ -0,0 +1,9 @@ +import SimpleTile from "./SimpleTile.js"; + +export default class RoomNameTile extends SimpleTile { + get label() { + const event = this._entry.event; + const content = event.content; + return `${event.sender} changed the room name to "${content.name}"` + } +} diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 018bacbd..bacb8259 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -1,6 +1,7 @@ export default class SimpleTile { - constructor(entry) { + constructor({entry, emitUpdate}) { this._entry = entry; + this._emitUpdate = emitUpdate; } // view model props for all subclasses // hmmm, could also do instanceof ... ? @@ -33,10 +34,17 @@ export default class SimpleTile { // update received for already included (falls within sort keys) entry updateEntry(entry) { - + // return names of props updated, or true for all, or null for no changes caused + return true; } - // simple entry can only contain 1 entry + // return whether the tile should be removed + // as SimpleTile only has one entry, the tile should be removed + removeEntry(entry) { + return true; + } + + // SimpleTile can only contain 1 entry tryIncludeEntry() { return false; } diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js new file mode 100644 index 00000000..411b1a55 --- /dev/null +++ b/src/domain/session/room/timeline/tiles/TextTile.js @@ -0,0 +1,8 @@ +import MessageTile from "./MessageTile.js"; + +export default class TextTile extends MessageTile { + get text() { + const content = this._getContent(); + return content && content.body; + } +} diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js index 74375f1c..e70088b9 100644 --- a/src/domain/session/room/timeline/tilesCreator.js +++ b/src/domain/session/room/timeline/tilesCreator.js @@ -1,13 +1,15 @@ import GapTile from "./tiles/GapTile.js"; import TextTile from "./tiles/TextTile.js"; import ImageTile from "./tiles/ImageTile.js"; +import LocationTile from "./tiles/LocationTile.js"; import RoomNameTile from "./tiles/RoomNameTile.js"; import RoomMemberTile from "./tiles/RoomMemberTile.js"; -export default function ({timeline}) { +export default function ({timeline, emitUpdate}) { return function tilesCreator(entry) { + const options = {entry, emitUpdate}; if (entry.gap) { - return new GapTile(entry, timeline); + return new GapTile(options, timeline); } else if (entry.event) { const event = entry.event; switch (event.type) { @@ -16,18 +18,23 @@ export default function ({timeline}) { const msgtype = content && content.msgtype; switch (msgtype) { case "m.text": - return new TextTile(entry); + case "m.notice": + return new TextTile(options); case "m.image": - return new ImageTile(entry); + return new ImageTile(options); + case "m.location": + return new LocationTile(options); default: - return null; // unknown tile types are not rendered? + // unknown msgtype not rendered + return null; } } case "m.room.name": - return new RoomNameTile(entry); + return new RoomNameTile(options); case "m.room.member": - return new RoomMemberTile(entry); + return new RoomMemberTile(options); default: + // unknown type not rendered return null; } }