matrix-public-archive/shared/views/ArchiveRoomView.js

218 lines
8.1 KiB
JavaScript

'use strict';
const {
TemplateView,
AvatarView,
TimelineView,
RightPanelView,
LightboxView,
} = require('hydrogen-view-sdk');
const {
customViewClassForTile,
} = require('matrix-public-archive-shared/lib/custom-tile-utilities');
const DeveloperOptionsContentView = require('matrix-public-archive-shared/views/DeveloperOptionsContentView');
const ModalView = require('matrix-public-archive-shared/views/ModalView');
class RoomHeaderView extends TemplateView {
render(t, vm) {
return t.div({ className: 'RoomHeader middle-header' }, [
t.a(
{
className: 'button-utility RoomHeader_actionButton',
href: vm.roomDirectoryUrl,
title: vm.i18n`Go back to the room directory`,
},
[
// Home icon from Element
t.svg(
{
xmlns: 'http://www.w3.org/2000/svg',
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'currentColor',
'aria-hidden': 'true',
},
[
t.path({
d: 'M20.2804 7.90031L13.2804 2.06697C12.5387 1.4489 11.4613 1.4489 10.7196 2.06698L3.71963 7.90031C3.26365 8.28029 3 8.84319 3 9.43675V20.5C3 21.6046 3.89543 22.5 5 22.5H7C8.10457 22.5 9 21.6046 9 20.5V16C9 14.8954 9.89543 14 11 14H13C14.1046 14 15 14.8954 15 16V20.5C15 21.6046 15.8954 22.5 17 22.5H19C20.1046 22.5 21 21.6046 21 20.5V9.43675C21 8.84319 20.7364 8.28029 20.2804 7.90031Z',
}),
]
),
]
),
t.view(new AvatarView(vm.roomAvatarViewModel, 32)),
t.div({ className: 'room-description' }, [t.h2((vm) => vm.roomName)]),
t.button(
{
className: 'button-utility RoomHeader_actionButton RoomHeader_changeDatesButton',
title: vm.i18n`Change dates`,
onClick: (/*event*/) => {
vm.openRightPanel();
},
},
[
// Calendar icon (via `calendar2-date` from Bootstrap)
t.svg(
{
xmlns: 'http://www.w3.org/2000/svg',
width: '16',
height: '16',
viewBox: '0 0 16 16',
fill: 'currentColor',
'aria-hidden': 'true',
},
[
t.path({
d: 'M6.445 12.688V7.354h-.633A12.6 12.6 0 0 0 4.5 8.16v.695c.375-.257.969-.62 1.258-.777h.012v4.61h.675zm1.188-1.305c.047.64.594 1.406 1.703 1.406 1.258 0 2-1.066 2-2.871 0-1.934-.781-2.668-1.953-2.668-.926 0-1.797.672-1.797 1.809 0 1.16.824 1.77 1.676 1.77.746 0 1.23-.376 1.383-.79h.027c-.004 1.316-.461 2.164-1.305 2.164-.664 0-1.008-.45-1.05-.82h-.684zm2.953-2.317c0 .696-.559 1.18-1.184 1.18-.601 0-1.144-.383-1.144-1.2 0-.823.582-1.21 1.168-1.21.633 0 1.16.398 1.16 1.23z',
}),
t.path({
d: 'M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM2 2a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H2z',
}),
t.path({
d: 'M2.5 4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5H3a.5.5 0 0 1-.5-.5V4z',
}),
]
),
]
),
]);
}
}
class DisabledComposerView extends TemplateView {
render(t /*, vm*/) {
return t.div({ className: 'DisabledComposerView' }, [
t.h3([
t.map(
(vm) => vm.currentTopPositionEventEntry,
(_currentTopPositionEventEntry, t, vm) => {
const activeDate = new Date(
// If the date from our `archiveRoomViewModel` is available, use that
vm?.currentTopPositionEventEntry?.timestamp ||
// Otherwise, use our initial `dayTimestampTo`
vm.dayTimestampTo
);
const dateString = activeDate.toISOString().split('T')[0];
return t.span(`You're viewing an archive of events from ${dateString}. Use a `);
}
),
t.a(
{
href: (vm) => vm.roomPermalink,
rel: 'noopener',
target: '_blank',
},
['Matrix client']
),
` to start chatting in this room.`,
]),
]);
}
}
class ArchiveRoomView extends TemplateView {
constructor(vm) {
super(vm);
// Keep track of the `IntersectionObserver` so we can disconnect it when necessary
this._interSectionObserverForUpdatedTopPositionEventEntry = null;
}
unmount() {
if (this._interSectionObserverForUpdatedTopPositionEventEntry) {
this._interSectionObserverForUpdatedTopPositionEventEntry.disconnect();
}
}
render(t, vm) {
const rootElement = t.div(
{
className: {
ArchiveRoomView: true,
'right-shown': (vm) => vm.shouldShowRightPanel,
},
},
[
// The red border and yellow background trail around the event that is
// driving the active date as you scroll around.
t.if(
(vm) => vm._developerOptionsContentViewModel?.debugActiveDateIntersectionObserver,
(t /*, vm*/) => {
return t.style({}, (vm) => {
return `
[data-event-id] {
transition: background-color 800ms;
}
[data-event-id="${vm.currentTopPositionEventEntry?.id}"] {
background-color: #ffff8a;
outline: 1px solid #f00;
outline-offset: -1px;
transition: background-color 0ms;
}
`;
});
}
),
t.main({ className: 'ArchiveRoomView_mainArea' }, [
t.view(new RoomHeaderView(vm)),
t.main({ className: 'ArchiveRoomView_mainBody' }, [
t.view(new TimelineView(vm.timelineViewModel, customViewClassForTile)),
t.view(new DisabledComposerView(vm)),
]),
]),
// We can't just conditionally render the right-panel with `t.ifView(...)` based
// on `shouldShowRightPanel` because the right-panel being "hidden" only applies
// to the mobile break points and is always shown on desktop.
t.view(new RightPanelView(vm.rightPanelModel)),
t.mapView(
(vm) => vm.lightboxViewModel,
(lightboxViewModel) => (lightboxViewModel ? new LightboxView(lightboxViewModel) : null)
),
t.view(new ModalView(DeveloperOptionsContentView, vm.developerOptionsModalViewModel)),
]
);
// Avoid an error when server-side rendering (SSR) because it doesn't have all the
// DOM API's available (and doesn't need it for this case)
if (typeof IntersectionObserver === 'function') {
const scrollRoot = rootElement.querySelector('.Timeline_scroller');
this._interSectionObserverForUpdatedTopPositionEventEntry = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const eventId = entry.target.getAttribute('data-event-id');
const eventEntry = vm.eventEntriesByEventId[eventId];
vm.setCurrentTopPositionEventEntry(eventEntry);
}
});
},
{
root: scrollRoot,
// Select the current active day from the top-edge of the scroll viewport.
//
// This is a trick that pushes the bottom margin up to the top of the
// root so there is just a 0px region at the top to detect
// intersections. This way we always recognize the element at the top.
// As mentioned in:
// - https://stackoverflow.com/a/54874286/796832
// - https://css-tricks.com/an-explanation-of-how-the-intersection-observer-watches/#aa-creating-a-position-sticky-event
//
// The format is the same as margin: top, left, bottom, right.
rootMargin: '0px 0px -100% 0px',
threshold: 0,
}
);
[...scrollRoot.querySelectorAll(`:scope > ul > [data-event-id]`)].forEach((el) => {
this._interSectionObserverForUpdatedTopPositionEventEntry.observe(el);
});
}
return rootElement;
}
}
module.exports = ArchiveRoomView;