diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 5f327ef9..2f7e341e 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -207,7 +207,9 @@ export class SessionViewModel extends ViewModel { this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); } if (eventId) { - this._lightboxViewModel = this.track(new LightboxViewModel(this.childOptions({eventId}))); + const roomId = this.navigation.path.get("room").value; + const room = this._sessionContainer.session.rooms.get(roomId); + this._lightboxViewModel = this.track(new LightboxViewModel(this.childOptions({eventId, room}))); } this.emitChange("lightboxViewModel"); } diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index 00599eca..f6da39b0 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -20,14 +20,78 @@ export class LightboxViewModel extends ViewModel { constructor(options) { super(options); this._eventId = options.eventId; + this._unencryptedImageUrl = null; + this._decryptedImage = null; this._closeUrl = this.urlCreator.urlUntilSegment("room"); + this._eventEntry = null; + this._date = null; + this._subscribeToEvent(options.room, options.eventId); } - get eventId() { - return this._eventId; + _subscribeToEvent(room, eventId) { + const eventObservable = room.observeEvent(eventId); + this.track(eventObservable.subscribe(eventEntry => { + this._loadEvent(room, eventEntry); + })); + this._loadEvent(room, eventObservable.get()); + } + + async _loadEvent(room, eventEntry) { + if (!eventEntry) { + return; + } + const {mediaRepository} = room; + this._eventEntry = eventEntry; + const {content} = this._eventEntry; + this._date = this._eventEntry.timestamp ? new Date(this._eventEntry.timestamp) : null; + if (content.url) { + this._unencryptedImageUrl = mediaRepository.mxcUrl(content.url); + this.emitChange("imageUrl"); + } else if (content.file) { + this._decryptedImage = this.track(await mediaRepository.downloadEncryptedFile(content.file)); + this.emitChange("imageUrl"); + } + } + + get imageWidth() { + return this._eventEntry?.content?.info?.w; + } + + get imageHeight() { + return this._eventEntry?.content?.info?.h; + } + + get name() { + return this._eventEntry?.content?.body; + } + + get sender() { + return this._eventEntry?.displayName; + } + + get imageUrl() { + if (this._decryptedImage) { + return this._decryptedImage.url; + } else if (this._unencryptedImageUrl) { + return this._unencryptedImageUrl; + } else { + return ""; + } + } + + get date() { + return this._date && this._date.toLocaleDateString({}, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + } + + get time() { + return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"}); } get closeUrl() { return this._closeUrl; } + + close() { + this.platform.history.pushUrl(this.closeUrl); + } } diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index 1a32167b..eb3f8355 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -109,7 +109,6 @@ main { use numeric positions because named grid areas are not present in mobile layout */ grid-area: 2 / 1 / 3 / 3; - background-color: rgba(0,0,0,0.5); /* this should not be necessary, but chrome seems to have a bug when there are scrollbars in other grid items, it seems to put the scroll areas on top of the other grid items unless they have a z-index */ z-index: 1; diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 9473b307..33d5c6f0 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -632,3 +632,63 @@ button.link { color: #03B381; font-weight: 600; } + +.lightbox { + background-color: rgba(0,0,0,0.75); + display: grid; + grid-template: + "content close" auto + "content details" 1fr / + 1fr auto; + color: white; +} + +@media (max-aspect-ratio: 1/1) { + .lightbox { + grid-template: + "close" auto + "content" 1fr + "details" auto / + 1fr; + } + + .lightbox .details { + width: 100% !important; + } +} + +.lightbox .picture { + grid-area: content; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + width: 100%; + height: 100%; + margin: auto; +} + +.lightbox .loading { + grid-area: content; + margin: auto; +} + +.lightbox .close { + grid-area: close; + margin-left: auto; + background-image: url('icons/dismiss.svg'); + background-position: center; + background-size: 16px; + background-repeat: no-repeat; + width: 16px; + height: 16px; + padding: 12px; +} + +.lightbox .details { + grid-area: details; + padding: 12px; + font-size: 1.5rem; + width: 200px; +} + + diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index e03eab6b..1196295e 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -35,7 +35,7 @@ export class LoginView extends TemplateView { }); const homeserver = t.input({ id: "homeserver", - type: "text", + type: "url", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, disabled diff --git a/src/platform/web/ui/session/room/LightboxView.js b/src/platform/web/ui/session/room/LightboxView.js index 4c4ef316..4553c3bb 100644 --- a/src/platform/web/ui/session/room/LightboxView.js +++ b/src/platform/web/ui/session/room/LightboxView.js @@ -15,9 +15,39 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView.js"; +import {spinner} from "../../common.js"; export class LightboxView extends TemplateView { render(t, vm) { - return t.div({className: "lightbox"}, [vm.eventId, t.br(), t.a({href: vm.closeUrl}, "close")]); + const close = t.a({href: vm.closeUrl, title: vm.i18n`Close`, className: "close"}); + const image = t.div({ + role: "img", + "aria-label": vm => vm.name, + title: vm => vm.name, + className: { + picture: true, + hidden: vm => !vm.imageUrl, + }, + style: vm => `background-image: url('${vm.imageUrl}'); max-width: ${vm.imageWidth}px; max-height: ${vm.imageHeight}px;` + }); + const loading = t.div({ + className: { + loading: true, + hidden: vm => !!vm.imageUrl + } + }, [ + spinner(t), + t.div(vm.i18n`Loading image…`) + ]); + const details = t.div({ + className: "details" + }, [t.strong(vm => vm.name), t.br(), "uploaded by ", t.strong(vm => vm.sender), vm => ` at ${vm.time} on ${vm.date}.`]); + return t.div({className: "lightbox", onClick: evt => this.close(evt)}, [image, loading, details, close]); + } + + close(evt) { + if (evt.target === this.root()) { + this.value.close(); + } } }