diff --git a/server/index.js b/server/index.js index bc6f5f1f..fd595fde 100644 --- a/server/index.js +++ b/server/index.js @@ -137,13 +137,46 @@ app.use('/api/comments/byepisodeid', async (req, res) => { const cache = new InMemoryCache(); const fetcher = makeRateLimitedFetcher(fetch); + const sentCommenters = {}; + const threadcap = await makeThreadcap(socialInteract[0].uri, { userAgent, cache, fetcher }); - await updateThreadcap(threadcap, { updateTime: new Date().toISOString(), userAgent, cache, fetcher }); + const callbacks = { + onEvent: e => { + if (e.kind === 'node-processed' && e.part === 'replies') { + writeThreadcapChunk(e.nodeId, threadcap, sentCommenters, res); + } + } + } + + await updateThreadcap(threadcap, { updateTime: new Date().toISOString(), userAgent, cache, fetcher, callbacks }); - res.send(threadcap) + res.end() }) +function writeThreadcapChunk(processedNodeId, threadcap, sentCommenters, res) { + const threadcapChunk = {}; + + threadcapChunk.roots = threadcap.roots.filter((root) => root === processedNodeId); + threadcapChunk.nodes = {}; + threadcapChunk.nodes[processedNodeId] = threadcap.nodes[processedNodeId]; + + const comment = threadcapChunk.nodes[processedNodeId].comment; + + // nodes are always new, but commenters can be repeated, we only include + // them in th chunk if they have not been sent before, as there is no purpose + // on sending them again. + // this could be determines by inspecting previous nodes, but this way + // is easier. + if(comment && !sentCommenters[comment.attributedTo]) { + sentCommenters[comment.attributedTo] = true; + threadcapChunk.commenters = {}; + threadcapChunk.commenters[comment.attributedTo] = threadcap.commenters[comment.attributedTo]; + } + + res.write(JSON.stringify(threadcapChunk)) +} + // ------------------------------------------------ // ---------- Static files for API data ----------- // ------------------------------------------------ diff --git a/ui/src/components/Comments/index.tsx b/ui/src/components/Comments/index.tsx index 98115f52..e175d91b 100644 --- a/ui/src/components/Comments/index.tsx +++ b/ui/src/components/Comments/index.tsx @@ -21,7 +21,8 @@ interface StateComment { attributedTo?: Commenter, replies?: StateComment[], commentError?: string, - repliesError?: string + repliesError?: string, + loaded: boolean } interface Commenter { @@ -43,7 +44,7 @@ class Comment extends React.PureComponent { render(): React.ReactNode { return (
- {!this.props.comment.commentError && + {!this.props.comment.commentError && this.props.comment.loaded && @@ -73,6 +74,14 @@ class Comment extends React.PureComponent { !this.props.comment.commentError && this.props.comment.content &&
} + {!this.props.comment.loaded && + + + + Loading... + + + } {this.props.comment.commentError && @@ -103,9 +112,10 @@ export default class Comments extends React.PureComponent { } async onClickShowComments() { - const stateToSet: any = { - showComments: true, - loadingComments: false + const responseBody = { + roots: [], + nodes: {}, + commenters: {} }; if(!this.state.comments.length) { @@ -115,12 +125,47 @@ export default class Comments extends React.PureComponent { const response = await fetch('/api/comments/byepisodeid?' + new URLSearchParams({id: String(this.props.id) })); - const responseBody = await response.json(); + const reader = response.body.getReader(); - stateToSet.comments = responseBody.roots.map((root) => Comments.buildStateComment(root, responseBody)); - } + const thisComponent = this; - this.setState(stateToSet); + await reader.read().then(function processChunk({done, value}) { + if(done) { + thisComponent.setState({ + loadingComments: false + }); + return; + } + const parsedChunk = JSON.parse(new TextDecoder().decode(value)); + + updateResponseBody(responseBody, parsedChunk); + + + const stateToSet: any = { + showComments: true, + }; + + stateToSet.comments = responseBody.roots.map((root) => Comments.buildStateComment(root, responseBody)); + + thisComponent.setState(stateToSet); + return reader.read().then(processChunk); + }); + + function updateResponseBody(responseBody, parsedChunk) { + responseBody.roots = responseBody.roots.concat(parsedChunk.roots); + for(let key in parsedChunk.nodes) { + responseBody.nodes[key] = parsedChunk.nodes[key]; + } + for(let key in parsedChunk.commenters) { + responseBody.commenters[key] = parsedChunk.commenters[key]; + } + } + } + else { + this.setState({ + showComments: true + }); + } } async onClickHideComments() { @@ -128,17 +173,20 @@ export default class Comments extends React.PureComponent { } private static buildStateComment(commentUrl: string, commentsApiResponseBody): StateComment | null { + let stateComment: StateComment = { + url: commentUrl, + loaded: false + } + const node = commentsApiResponseBody.nodes[commentUrl]; if(!node) { - return null; + return stateComment; } - const commenter = node.comment && commentsApiResponseBody.commenters[node.comment.attributedTo]; + stateComment.loaded = true; - let stateComment: StateComment = { - url: commentUrl - } + const commenter = node.comment && commentsApiResponseBody.commenters[node.comment.attributedTo]; if(node.comment) { const summary = node.comment.summary && DOMPurify.sanitize(Comments.resolveLanguageTaggedValues(node.comment.summary));