Files
qotnews/webclient/src/Comments.js
2025-12-03 00:50:58 +00:00

141 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import { HashLink } from 'react-router-hash-link';
import { Helmet } from 'react-helmet';
import moment from 'moment';
import localForage from 'localforage';
import { infoLine, ToggleDot } from './utils.js';
function countComments(c) {
return c.comments.reduce((sum, x) => sum + countComments(x), 1);
}
function Comments({ cache }) {
const { id } = useParams();
if (id in cache) console.log('cache hit');
const [story, setStory] = useState(cache[id] || false);
const [error, setError] = useState('');
const [collapsed, setCollapsed] = useState([]);
const [expanded, setExpanded] = useState([]);
useEffect(() => {
localForage.getItem(id)
.then(
(value) => {
if (value) {
setStory(value);
}
}
);
fetch('/api/' + id)
.then(res => {
if (!res.ok) {
throw new Error(`Server responded with ${res.status} ${res.statusText}`);
}
return res.json();
})
.then(
(result) => {
setStory(result.story);
localForage.setItem(id, result.story);
const hash = window.location.hash.substring(1);
if (hash) {
setTimeout(() => {
const element = document.getElementById(hash);
if (element) {
element.scrollIntoView();
}
}, 0);
}
},
(error) => {
const errorMessage = `Failed to fetch comments (ID: ${id}). Your connection may be down or the server might be experiencing issues. ${error.toString()}.`;
setError(errorMessage);
}
);
}, [id]);
const collapseComment = (cid) => {
setCollapsed(prev => [...prev, cid]);
setExpanded(prev => prev.filter(x => x !== cid));
};
const expandComment = (cid) => {
setCollapsed(prev => prev.filter(x => x !== cid));
setExpanded(prev => [...prev, cid]);
};
const displayComment = (story, c, level) => {
const cid = c.author+c.date;
const isCollapsed = collapsed.includes(cid);
const isExpanded = expanded.includes(cid);
const hidden = isCollapsed || (level == 4 && !isExpanded);
const hasChildren = c.comments.length !== 0;
return (
<div className={level ? 'comment lined' : 'comment'} key={cid}>
<div className='info'>
<p>
{c.author === story.author ? '[OP]' : ''} {c.author || '[Deleted]'}
{' '} | <HashLink to={'#'+cid} id={cid}>{moment.unix(c.date).fromNow()}</HashLink>
{hidden || hasChildren &&
<button className='collapser pointer' onClick={() => collapseComment(cid)}></button>
}
</p>
</div>
<div className={isCollapsed ? 'text hidden' : 'text'} dangerouslySetInnerHTML={{ __html: c.text || '<p>[Empty / deleted comment]</p>'}} />
{hidden && hasChildren ?
<button className='comment lined info pointer' onClick={() => expandComment(cid)}>[show {countComments(c)-1} more]</button>
:
c.comments.map(i => displayComment(story, i, level + 1))
}
</div>
);
};
return (
<div className='container'>
{error &&
<details style={{marginBottom: '1rem'}}>
<summary>Connection error? Click to expand.</summary>
<p>{error}</p>
{story && <p>Loaded comments from cache.</p>}
</details>
}
{story ?
<div className='article'>
<Helmet>
<title>{story.title} | QotNews</title>
<meta name="robots" content="noindex" />
</Helmet>
<h1>{story.title}</h1>
<div className='info'>
<Link to={'/' + story.id}>View article</Link>
</div>
{infoLine(story)}
<div className='comments'>
{story.comments.map(c => displayComment(story, c, 0))}
</div>
</div>
:
<p>loading...</p>
}
<ToggleDot id={id} article={true} />
</div>
);
}
export default Comments;