diff --git a/public/styles/styles.css b/public/styles/styles.css new file mode 100644 index 0000000..57e3ff0 --- /dev/null +++ b/public/styles/styles.css @@ -0,0 +1,8 @@ +.room-layout { + display: flex; +} + +.right-panel { + width: 20%; + background-color: #f2f5f8; +} diff --git a/server/fetch-events-for-timestamp.js b/server/fetch-events-for-timestamp.js index 7bf502e..0978396 100644 --- a/server/fetch-events-for-timestamp.js +++ b/server/fetch-events-for-timestamp.js @@ -1,45 +1,10 @@ const assert = require('assert'); -const fetch = require('node-fetch'); const urlJoin = require('./lib/url-join'); +const fetchEndpoint = require('./lib/fetch-endpoint'); const { matrixServerUrl } = require('../config.json'); -const secrets = require('../secrets.json'); - -const matrixAccessToken = secrets.matrixAccessToken; -assert(matrixAccessToken); - -class HTTPResponseError extends Error { - constructor(response, responseText, ...args) { - super( - `HTTP Error Response: ${response.status} ${response.statusText}: ${responseText}\n URL=${response.url}`, - ...args - ); - this.response = response; - } -} - -const checkStatus = async (response) => { - if (response.ok) { - // response.status >= 200 && response.status < 300 - return response; - } else { - const responseText = await response.text(); - throw new HTTPResponseError(response, responseText); - } -}; - -async function fetchEndpoint(endpoint) { - const res = await fetch(endpoint, { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${matrixAccessToken}`, - }, - }); - await checkStatus(res); - const data = await res.json(); - return data; -} +assert(matrixServerUrl); async function fetchEventsForTimestamp(roomId, ts) { assert(roomId); @@ -50,8 +15,6 @@ async function fetchEventsForTimestamp(roomId, ts) { `_matrix/client/unstable/org.matrix.msc3030/rooms/${roomId}/timestamp_to_event?ts=${ts}&dir=f` ); const timestampToEventResData = await fetchEndpoint(timestampToEventEndpoint); - //console.log('timestampToEventResData', timestampToEventResData); - const eventIdForTimestamp = timestampToEventResData.event_id; assert(eventIdForTimestamp); @@ -67,11 +30,10 @@ async function fetchEventsForTimestamp(roomId, ts) { `_matrix/client/r0/rooms/${roomId}/messages?from=${contextResData.start}&limit=50&filter={"lazy_load_members":true,"include_redundant_members":true}` ); const messageResData = await fetchEndpoint(messagesEndpoint); - //console.log('messageResData', messageResData); //console.log('messageResData.state', messageResData.state); const stateEventMap = {}; - for (const stateEvent of messageResData.state) { + for (const stateEvent of messageResData.state || []) { if (stateEvent.type === 'm.room.member') { stateEventMap[stateEvent.state_key] = stateEvent; } diff --git a/server/fetch-room-data.js b/server/fetch-room-data.js new file mode 100644 index 0000000..ecef4fb --- /dev/null +++ b/server/fetch-room-data.js @@ -0,0 +1,43 @@ +const assert = require('assert'); + +const urlJoin = require('./lib/url-join'); +const fetchEndpoint = require('./lib/fetch-endpoint'); + +const { matrixServerUrl } = require('../config.json'); +assert(matrixServerUrl); + +async function fetchRoomData(roomId) { + const stateNameEndpoint = urlJoin( + matrixServerUrl, + `_matrix/client/r0/rooms/${roomId}/state/m.room.name` + ); + const stateAvatarEndpoint = urlJoin( + matrixServerUrl, + `_matrix/client/r0/rooms/${roomId}/state/m.room.avatar` + ); + + const [stateNameResDataOutcome, stateAvatarResDataOutcome] = await Promise.allSettled([ + fetchEndpoint(stateNameEndpoint), + fetchEndpoint(stateAvatarEndpoint), + ]); + + console.log('stateAvatarResDataOutcome', stateAvatarResDataOutcome); + + let name; + if (stateNameResDataOutcome.reason === undefined) { + name = stateNameResDataOutcome.value.name; + } + + let avatarUrl; + if (stateAvatarResDataOutcome.reason === undefined) { + avatarUrl = stateAvatarResDataOutcome.value.url; + } + + return { + id: roomId, + name, + avatarUrl, + }; +} + +module.exports = fetchRoomData; diff --git a/server/lib/fetch-endpoint.js b/server/lib/fetch-endpoint.js new file mode 100644 index 0000000..c73a78e --- /dev/null +++ b/server/lib/fetch-endpoint.js @@ -0,0 +1,39 @@ +const assert = require('assert'); +const fetch = require('node-fetch'); + +const { matrixAccessToken } = require('../../secrets.json'); +assert(matrixAccessToken); + +class HTTPResponseError extends Error { + constructor(response, responseText, ...args) { + super( + `HTTP Error Response: ${response.status} ${response.statusText}: ${responseText}\n URL=${response.url}`, + ...args + ); + this.response = response; + } +} + +const checkResponseStatus = async (response) => { + if (response.ok) { + // response.status >= 200 && response.status < 300 + return response; + } else { + const responseText = await response.text(); + throw new HTTPResponseError(response, responseText); + } +}; + +async function fetchEndpoint(endpoint) { + const res = await fetch(endpoint, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${matrixAccessToken}`, + }, + }); + await checkResponseStatus(res); + const data = await res.json(); + return data; +} + +module.exports = fetchEndpoint; diff --git a/server/render-hydrogen-to-string.js b/server/render-hydrogen-to-string.js index 7d1000e..7537d1d 100644 --- a/server/render-hydrogen-to-string.js +++ b/server/render-hydrogen-to-string.js @@ -7,7 +7,8 @@ const { parseHTML } = require('linkedom'); const config = require('../config.json'); -async function renderToString(events, stateEventMap) { +async function renderToString(roomData, events, stateEventMap) { + assert(roomData); assert(events); assert(stateEventMap); @@ -40,12 +41,13 @@ async function renderToString(events, stateEventMap) { // So we can see logs from the underlying vm vmContext.global.console = console; + vmContext.global.INPUT_ROOM_DATA = roomData; vmContext.global.INPUT_EVENTS = events; vmContext.global.INPUT_STATE_EVENT_MAP = stateEventMap; vmContext.global.INPUT_CONFIG = config; const hydrogenRenderScriptCode = await readFile( - path.resolve(__dirname, './hydrogen-vm-render-script.js'), + path.resolve(__dirname, '../shared/hydrogen-vm-render-script.js'), 'utf8' ); const hydrogenRenderScript = new vm.Script(hydrogenRenderScriptCode, { diff --git a/server/server1.js b/server/server1.js index c8ad934..c0a2fa2 100644 --- a/server/server1.js +++ b/server/server1.js @@ -3,6 +3,7 @@ const express = require('express'); const asyncHandler = require('./lib/express-async-handler'); const urlJoin = require('./lib/url-join'); +const fetchRoomData = require('./fetch-room-data'); const fetchEventsForTimestamp = require('./fetch-events-for-timestamp'); const renderHydrogenToString = require('./render-hydrogen-to-string'); @@ -12,7 +13,7 @@ assert(basePath); const app = express(); -app.get('/style.css', async function (req, res) { +app.get('/hydrogen-styles.css', async function (req, res) { res.set('Content-Type', 'text/css'); res.sendFile(require.resolve('hydrogen-view-sdk/style.css')); }); @@ -24,6 +25,8 @@ app.get( }) ); +// Based off of the Gitter archive routes, +// https://gitlab.com/gitterHQ/webapp/-/blob/14954e05c905e8c7cb675efebb89116c07cfaab5/server/handlers/app/archive.js#L190-297 app.get( '/:roomIdOrAlias/date/:yyyy(\\d{4})/:mm(\\d{2})/:dd(\\d{2})/:hourRange(\\d\\d?-\\d\\d?)?', asyncHandler(async function (req, res) { @@ -77,19 +80,30 @@ app.get( toTimestamp = Date.UTC(yyyy, mm, dd, toHour); } - const { events, stateEventMap } = await fetchEventsForTimestamp(roomIdOrAlias, fromTimestamp); + const [roomData, { events, stateEventMap }] = await Promise.all([ + fetchRoomData(roomIdOrAlias), + fetchEventsForTimestamp(roomIdOrAlias, fromTimestamp), + ]); - const hydrogenHtmlOutput = await renderHydrogenToString(events, stateEventMap); + const hydrogenHtmlOutput = await renderHydrogenToString(roomData, events, stateEventMap); - const styleUrl = urlJoin(basePath, 'style.css'); + const hydrogenStylesUrl = urlJoin(basePath, 'hydrogen-styles.css'); + const stylesUrl = urlJoin(basePath, 'styles.css'); const pageHtml = ` - + + - ${hydrogenHtmlOutput} +
+ ${hydrogenHtmlOutput} + + +
`; diff --git a/shared/ArchiveView.js b/shared/ArchiveView.js new file mode 100644 index 0000000..412dc03 --- /dev/null +++ b/shared/ArchiveView.js @@ -0,0 +1,17 @@ +const { + TemplateView, + RoomView, + RightPanelView +} = require('hydrogen-view-sdk'); + +export class ArchiveView extends TemplateView { + render(t, vm) { + return t.div({ + className: { + "ArchiveView": true, + }, + }, [ + + ] + } +} diff --git a/server/hydrogen-vm-render-script.js b/shared/hydrogen-vm-render-script.js similarity index 67% rename from server/hydrogen-vm-render-script.js rename to shared/hydrogen-vm-render-script.js index da651b9..e5e31f3 100644 --- a/server/hydrogen-vm-render-script.js +++ b/shared/hydrogen-vm-render-script.js @@ -11,8 +11,12 @@ const { encodeEventIdKey, Timeline, TimelineView, + RoomView, + RoomViewModel, } = require('hydrogen-view-sdk'); +const roomData = global.INPUT_ROOM_DATA; +assert(roomData); const events = global.INPUT_EVENTS; assert(events); const stateEventMap = global.INPUT_STATE_EVENT_MAP; @@ -25,7 +29,6 @@ let eventIndexCounter = 0; const fragmentIdComparer = new FragmentIdComparer([]); function makeEventEntryFromEventJson(eventJson, memberEvent) { assert(eventJson); - assert(memberEvent); const eventIndex = eventIndexCounter; const eventEntry = new EventEntry( @@ -34,8 +37,8 @@ function makeEventEntryFromEventJson(eventJson, memberEvent) { eventIndex: eventIndex, // TODO: What should this be? roomId: roomId, event: eventJson, - displayName: memberEvent.content && memberEvent.content.displayname, - avatarUrl: memberEvent.content && memberEvent.content.avatar_url, + displayName: memberEvent && memberEvent.content && memberEvent.content.displayname, + avatarUrl: memberEvent && memberEvent.content && memberEvent.content.avatar_url, key: encodeKey(roomId, 0, eventIndex), eventIdKey: encodeEventIdKey(roomId, eventJson.event_id), }, @@ -64,29 +67,35 @@ async function mountHydrogen() { //hsApi: this._hsApi }); + const mediaRepository = new MediaRepository({ + homeserver: config.matrixServerUrl, + }); + + const urlCreator = { + urlUntilSegment: () => { + return 'todo'; + }, + urlForSegments: () => { + return 'todo'; + }, + }; + + const navigation = { + segment: () => { + return 'todo'; + }, + }; + const tilesCreator = makeTilesCreator({ platform, roomVM: { room: { - mediaRepository: new MediaRepository({ - homeserver: config.matrixServerUrl, - }), + mediaRepository, }, }, timeline, - urlCreator: { - urlUntilSegment: () => { - return 'todo'; - }, - urlForSegments: () => { - return 'todo'; - }, - }, - navigation: { - segment: () => { - return 'todo'; - }, - }, + urlCreator, + navigation, }); // Something we can modify with new state updates as we see them @@ -129,7 +138,45 @@ async function mountHydrogen() { tiles: tiles, }; - const view = new TimelineView(timelineViewModel); + // const view = new TimelineView(timelineViewModel); + + // const roomViewModel = { + // kind: 'room', + // timelineViewModel, + // composerViewModel: { + // kind: 'none', + // }, + // i18n: RoomViewModel.prototype.i18n, + + // id: roomData.id, + // name: roomData.name, + // avatarUrl(size) { + // return getAvatarHttpUrl(roomData.avatarUrl, size, platform, mediaRepository); + // }, + // }; + + const room = { + name: roomData.name, + id: roomData.id, + avatarUrl: roomData.avatarUrl, + avatarColorId: roomData.id, + mediaRepository: mediaRepository, + }; + + const roomViewModel = new RoomViewModel({ + room, + ownUserId: 'xxx', + platform, + urlCreator, + navigation, + }); + + roomViewModel._timelineVM = timelineViewModel; + roomViewModel._composerVM = { + kind: 'none', + }; + + const view = new RoomView(roomViewModel); //console.log('view.mount()', view.mount()); app.appendChild(view.mount());