From 7f221cda65ea8aa1cd986cb3ffaef4747104a4bb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sat, 9 May 2020 20:02:08 +0200 Subject: [PATCH] show images intimeline --- .../session/room/timeline/tiles/ImageTile.js | 48 ++++++++++++++----- .../session/room/timeline/tilesCreator.js | 4 +- src/matrix/net/HomeServerApi.js | 37 +++++++++++++- src/matrix/room/Room.js | 8 ++++ src/ui/web/session/room/TimelineList.js | 4 +- src/ui/web/session/room/timeline/ImageView.js | 15 ++++++ 6 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 src/ui/web/session/room/timeline/ImageView.js diff --git a/src/domain/session/room/timeline/tiles/ImageTile.js b/src/domain/session/room/timeline/tiles/ImageTile.js index 8aee454b..5a6f7376 100644 --- a/src/domain/session/room/timeline/tiles/ImageTile.js +++ b/src/domain/session/room/timeline/tiles/ImageTile.js @@ -1,26 +1,48 @@ import {MessageTile} from "./MessageTile.js"; +const MAX_HEIGHT = 300; +const MAX_WIDTH = 400; + export class ImageTile extends MessageTile { - constructor(options) { + constructor(options, room) { 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 ""; + this._room = room; } - get width() { - return 200; + get thumbnailUrl() { + const mxcUrl = this._getContent().url; + return this._room.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeigth, "scale"); } - get height() { - return 200; + get url() { + const mxcUrl = this._getContent().url; + return this._room.mxcUrl(mxcUrl); + } + + _scaleFactor() { + const {info} = this._getContent(); + const scaleHeightFactor = MAX_HEIGHT / info.h; + const scaleWidthFactor = MAX_WIDTH / info.w; + // take the smallest scale factor, to respect all constraints + // we should not upscale images, so limit scale factor to 1 upwards + return Math.min(scaleWidthFactor, scaleHeightFactor, 1); + } + + get thumbnailWidth() { + const {info} = this._getContent(); + return Math.round(info.w * this._scaleFactor()); + } + + get thumbnailHeigth() { + const {info} = this._getContent(); + return Math.round(info.h * this._scaleFactor()); } get label() { - return "this is an image"; + return this._getContent().body; + } + + get shape() { + return "image"; } } diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js index 9f53a378..7f4d57e3 100644 --- a/src/domain/session/room/timeline/tilesCreator.js +++ b/src/domain/session/room/timeline/tilesCreator.js @@ -1,5 +1,6 @@ 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"; @@ -20,8 +21,7 @@ export function tilesCreator({room, ownUserId}) { case "m.emote": return new TextTile(options); case "m.image": - return null; // not supported yet - // return new ImageTile(options); + return new ImageTile(options, room); case "m.location": return new LocationTile(options); default: diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 20a1dd92..c65c4dfa 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -73,8 +73,8 @@ export class HomeServerApi { ); } - _request(method, url, queryParams, body, options) { - const queryString = Object.entries(queryParams || {}) + _encodeQueryParams(queryParams) { + return Object.entries(queryParams || {}) .filter(([, value]) => value !== undefined) .map(([name, value]) => { if (typeof value === "object") { @@ -83,6 +83,10 @@ export class HomeServerApi { return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; }) .join("&"); + } + + _request(method, url, queryParams, body, options) { + const queryString = this._encodeQueryParams(queryParams); url = `${url}?${queryString}`; let bodyString; const headers = new Map(); @@ -166,6 +170,35 @@ export class HomeServerApi { versions(options = null) { return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); } + + _parseMxcUrl(url) { + const prefix = "mxc://"; + if (url.startsWith(prefix)) { + return url.substr(prefix.length).split("/", 2); + } else { + return null; + } + } + + mxcUrlThumbnail(url, width, height, method) { + const parts = this._parseMxcUrl(url); + if (parts) { + const [serverName, mediaId] = parts; + const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; + return httpUrl + "?" + this._encodeQueryParams({width, height, method}); + } + return null; + } + + mxcUrl(url) { + const parts = this._parseMxcUrl(url); + if (parts) { + const [serverName, mediaId] = parts; + return `${this._homeserver}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; + } else { + return null; + } + } } export function tests() { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index 60983df2..cc8e60d5 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -130,5 +130,13 @@ export class Room extends EventEmitter { await this._timeline.load(); return this._timeline; } + + mxcUrlThumbnail(url, width, height, method) { + return this._hsApi.mxcUrlThumbnail(url, width, height, method); + } + + mxcUrl(url) { + return this._hsApi.mxcUrl(url); + } } diff --git a/src/ui/web/session/room/TimelineList.js b/src/ui/web/session/room/TimelineList.js index 8d042212..1d8bfd47 100644 --- a/src/ui/web/session/room/TimelineList.js +++ b/src/ui/web/session/room/TimelineList.js @@ -1,6 +1,7 @@ import {ListView} from "../../general/ListView.js"; import {GapView} from "./timeline/GapView.js"; import {TextMessageView} from "./timeline/TextMessageView.js"; +import {ImageView} from "./timeline/ImageView.js"; import {AnnouncementView} from "./timeline/AnnouncementView.js"; export class TimelineList extends ListView { @@ -10,7 +11,8 @@ export class TimelineList extends ListView { switch (entry.shape) { case "gap": return new GapView(entry); case "announcement": return new AnnouncementView(entry); - case "message":return new TextMessageView(entry); + case "message": return new TextMessageView(entry); + case "image": return new ImageView(entry); } }); this._atBottom = false; diff --git a/src/ui/web/session/room/timeline/ImageView.js b/src/ui/web/session/room/timeline/ImageView.js new file mode 100644 index 00000000..725fbf4e --- /dev/null +++ b/src/ui/web/session/room/timeline/ImageView.js @@ -0,0 +1,15 @@ +import {TemplateView} from "../../../general/TemplateView.js"; + +export class ImageView extends TemplateView { + render(t, vm) { + return t.li( + {className: {"TextMessageView": true, own: vm.isOwn, pending: vm.isPending}}, + t.div({className: "message-container"}, [ + t.div({className: "sender"}, vm => vm.isContinuation ? "" : vm.sender), + t.div(t.a({href: vm.url, target: "_blank"}, + t.img({src: vm.thumbnailUrl, width: vm.thumbnailWidth, heigth: vm.thumbnailHeigth, loading: "lazy", alt: vm.label}))), + t.p(t.time(vm.date + " " + vm.time)), + ]) + ); + } +}