Merge pull request #49 from bwindels/bwindels/showimages

Show images in timeline
This commit is contained in:
Bruno Windels 2020-05-09 18:04:18 +00:00 committed by GitHub
commit 55c41e56af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 18 deletions

View File

@ -1,26 +1,48 @@
import {MessageTile} from "./MessageTile.js"; import {MessageTile} from "./MessageTile.js";
const MAX_HEIGHT = 300;
const MAX_WIDTH = 400;
export class ImageTile extends MessageTile { export class ImageTile extends MessageTile {
constructor(options) { constructor(options, room) {
super(options); super(options);
this._room = room;
// 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() { get thumbnailUrl() {
return 200; const mxcUrl = this._getContent().url;
return this._room.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeigth, "scale");
} }
get height() { get url() {
return 200; 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() { get label() {
return "this is an image"; return this._getContent().body;
}
get shape() {
return "image";
} }
} }

View File

@ -1,5 +1,6 @@
import {GapTile} from "./tiles/GapTile.js"; import {GapTile} from "./tiles/GapTile.js";
import {TextTile} from "./tiles/TextTile.js"; import {TextTile} from "./tiles/TextTile.js";
import {ImageTile} from "./tiles/ImageTile.js";
import {LocationTile} from "./tiles/LocationTile.js"; import {LocationTile} from "./tiles/LocationTile.js";
import {RoomNameTile} from "./tiles/RoomNameTile.js"; import {RoomNameTile} from "./tiles/RoomNameTile.js";
import {RoomMemberTile} from "./tiles/RoomMemberTile.js"; import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
@ -20,8 +21,7 @@ export function tilesCreator({room, ownUserId}) {
case "m.emote": case "m.emote":
return new TextTile(options); return new TextTile(options);
case "m.image": case "m.image":
return null; // not supported yet return new ImageTile(options, room);
// return new ImageTile(options);
case "m.location": case "m.location":
return new LocationTile(options); return new LocationTile(options);
default: default:

View File

@ -73,8 +73,8 @@ export class HomeServerApi {
); );
} }
_request(method, url, queryParams, body, options) { _encodeQueryParams(queryParams) {
const queryString = Object.entries(queryParams || {}) return Object.entries(queryParams || {})
.filter(([, value]) => value !== undefined) .filter(([, value]) => value !== undefined)
.map(([name, value]) => { .map(([name, value]) => {
if (typeof value === "object") { if (typeof value === "object") {
@ -83,6 +83,10 @@ export class HomeServerApi {
return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
}) })
.join("&"); .join("&");
}
_request(method, url, queryParams, body, options) {
const queryString = this._encodeQueryParams(queryParams);
url = `${url}?${queryString}`; url = `${url}?${queryString}`;
let bodyString; let bodyString;
const headers = new Map(); const headers = new Map();
@ -166,6 +170,35 @@ export class HomeServerApi {
versions(options = null) { versions(options = null) {
return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); 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() { export function tests() {

View File

@ -130,5 +130,13 @@ export class Room extends EventEmitter {
await this._timeline.load(); await this._timeline.load();
return this._timeline; return this._timeline;
} }
mxcUrlThumbnail(url, width, height, method) {
return this._hsApi.mxcUrlThumbnail(url, width, height, method);
}
mxcUrl(url) {
return this._hsApi.mxcUrl(url);
}
} }

View File

@ -1,6 +1,7 @@
import {ListView} from "../../general/ListView.js"; import {ListView} from "../../general/ListView.js";
import {GapView} from "./timeline/GapView.js"; import {GapView} from "./timeline/GapView.js";
import {TextMessageView} from "./timeline/TextMessageView.js"; import {TextMessageView} from "./timeline/TextMessageView.js";
import {ImageView} from "./timeline/ImageView.js";
import {AnnouncementView} from "./timeline/AnnouncementView.js"; import {AnnouncementView} from "./timeline/AnnouncementView.js";
export class TimelineList extends ListView { export class TimelineList extends ListView {
@ -11,6 +12,7 @@ export class TimelineList extends ListView {
case "gap": return new GapView(entry); case "gap": return new GapView(entry);
case "announcement": return new AnnouncementView(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; this._atBottom = false;

View File

@ -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)),
])
);
}
}