render video messages

This commit is contained in:
Bruno Windels 2021-03-09 19:35:10 +01:00
parent b955cac7ce
commit ee6f3e5457
7 changed files with 103 additions and 15 deletions

View file

@ -0,0 +1,43 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 The Matrix.org Foundation C.I.C.
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 {BaseMediaTile} from "./BaseMediaTile.js";
export class VideoTile extends BaseMediaTile {
async loadVideo() {
const file = this._getContent().file;
if (file && !this._decryptedFile) {
this._decryptedFile = await this._loadEncryptedFile(file);
this.emitChange("videoUrl");
}
}
get videoUrl() {
if (this._decryptedFile) {
return this._decryptedFile.url;
}
const mxcUrl = this._getContent()?.url;
if (typeof mxcUrl === "string") {
return this._mediaRepository.mxcUrl(mxcUrl);
}
return "";
}
get shape() {
return "video";
}
}

View file

@ -17,6 +17,7 @@ limitations under the License.
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 {ImageTile} from "./tiles/ImageTile.js";
import {VideoTile} from "./tiles/VideoTile.js";
import {FileTile} from "./tiles/FileTile.js"; import {FileTile} from "./tiles/FileTile.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";
@ -44,6 +45,8 @@ export function tilesCreator(baseOptions) {
return new TextTile(options); return new TextTile(options);
case "m.image": case "m.image":
return new ImageTile(options); return new ImageTile(options);
case "m.video":
return new VideoTile(options);
case "m.file": case "m.file":
return new FileTile(options); return new FileTile(options);
case "m.location": case "m.location":

View file

@ -509,7 +509,7 @@ ul.Timeline > li.messageStatus .message-container > p {
.message-container { .message-container {
padding: 1px 10px 0px 10px; padding: 1px 10px 0px 10px;
margin: 5px 10px 0 10px; margin: 5px 10px 0 10px;
/* so the .picture can grow horizontally and its spacer can grow vertically */ /* so the .media can grow horizontally and its spacer can grow vertically */
width: 100%; width: 100%;
} }
@ -555,14 +555,14 @@ ul.Timeline > li.messageStatus .message-container > p {
} }
.message-container .picture { .message-container .media {
display: grid; display: grid;
margin-top: 4px; margin-top: 4px;
width: 100%; width: 100%;
} }
.message-container .picture > a { .message-container .media > a {
text-decoration: none; text-decoration: none;
width: 100%; width: 100%;
display: block; display: block;
@ -570,12 +570,12 @@ ul.Timeline > li.messageStatus .message-container > p {
/* .spacer grows with an inline padding-top to the size of the image, /* .spacer grows with an inline padding-top to the size of the image,
so the timeline doesn't jump when the image loads */ so the timeline doesn't jump when the image loads */
.message-container .picture > * { .message-container .media > * {
grid-row: 1; grid-row: 1;
grid-column: 1; grid-column: 1;
} }
.message-container .picture img { .message-container .media img, .message-container .media video {
width: 100%; width: 100%;
height: auto; height: auto;
/* for IE11 to still scale even though the spacer is too tall */ /* for IE11 to still scale even though the spacer is too tall */
@ -587,29 +587,30 @@ so the timeline doesn't jump when the image loads */
where we can trust the spacer to always have the correct height, where we can trust the spacer to always have the correct height,
otherwise the image starts with height 0 and with loading=lazy otherwise the image starts with height 0 and with loading=lazy
only loads when the top comes into view*/ only loads when the top comes into view*/
.hydrogen:not(.legacy) .message-container .picture img { .hydrogen:not(.legacy) .message-container .media img,
.hydrogen:not(.legacy) .message-container .media video {
align-self: stretch; align-self: stretch;
} }
.message-container .picture > .sendStatus { .message-container .media > .sendStatus {
align-self: end; align-self: end;
justify-self: start; justify-self: start;
font-size: 0.8em; font-size: 0.8em;
} }
.message-container .picture > progress { .message-container .media > progress {
align-self: center; align-self: center;
justify-self: center; justify-self: center;
width: 75%; width: 75%;
} }
.message-container .picture > time { .message-container .media > time {
align-self: end; align-self: end;
justify-self: end; justify-self: end;
} }
.message-container .picture > time, .message-container .media > time,
.message-container .picture > .sendStatus { .message-container .media > .sendStatus {
color: #2e2f32; color: #2e2f32;
display: block; display: block;
padding: 2px; padding: 2px;
@ -617,7 +618,7 @@ only loads when the top comes into view*/
background-color: rgba(255, 255, 255, 0.75); background-color: rgba(255, 255, 255, 0.75);
border-radius: 4px; border-radius: 4px;
} }
.message-container .picture > .spacer { .message-container .media > .spacer {
/* TODO: can we implement this with a pseudo element? or perhaps they are not grid items? */ /* TODO: can we implement this with a pseudo element? or perhaps they are not grid items? */
width: 100%; width: 100%;
/* don't stretch height as it is a spacer, just in case it doesn't match with image height */ /* don't stretch height as it is a spacer, just in case it doesn't match with image height */

View file

@ -37,11 +37,12 @@ limitations under the License.
margin: 5px 0; margin: 5px 0;
} }
.message-container .picture { .message-container .media {
display: block; display: block;
} }
.message-container .picture > img { .message-container .media > img,
.message-container .media > video {
display: block; display: block;
} }

View file

@ -94,7 +94,7 @@ export const TAG_NAMES = {
[HTML_NS]: [ [HTML_NS]: [
"br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6",
"p", "strong", "em", "span", "img", "section", "main", "article", "aside", "p", "strong", "em", "span", "img", "section", "main", "article", "aside",
"pre", "button", "time", "input", "textarea", "label", "form", "progress", "output"], "pre", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"],
[SVG_NS]: ["svg", "circle"] [SVG_NS]: ["svg", "circle"]
}; };

View file

@ -18,6 +18,7 @@ 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 {ImageView} from "./timeline/ImageView.js";
import {VideoView} from "./timeline/VideoView.js";
import {FileView} from "./timeline/FileView.js"; import {FileView} from "./timeline/FileView.js";
import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js"; import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
import {AnnouncementView} from "./timeline/AnnouncementView.js"; import {AnnouncementView} from "./timeline/AnnouncementView.js";
@ -30,6 +31,7 @@ function viewClassForEntry(entry) {
case "message-status": case "message-status":
return TextMessageView; return TextMessageView;
case "image": return ImageView; case "image": return ImageView;
case "video": return VideoView;
case "file": return FileView; case "file": return FileView;
case "missing-attachment": return MissingAttachmentView; case "missing-attachment": return MissingAttachmentView;
} }

View file

@ -0,0 +1,38 @@
/*
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 {BaseMediaView} from "./BaseMediaView.js";
export class VideoView extends BaseMediaView {
renderMedia(t, vm) {
return t.video({
// provide empty data url if video is not decrypted yet.
// Chrome/Electron need this to enable the play button.
src: vm => vm.videoUrl || `data:${vm.mimeType},`,
title: vm => vm.label,
controls: true,
preload: "none",
poster: vm => vm.thumbnailUrl,
onPlay: async evt => {
if (!vm.videoUrl) {
await vm.loadVideo();
evt.target.play();
}
},
style: `max-width: ${vm.width}px; max-height: ${vm.height}px;`
});
}
}