import { proxyUrlFor } from '../media/registry.ts';
import { anchorOf, type Comment, type Media, type Post, type Quoted, type Segment } from '../types.ts';
import { html, raw, type Raw } from './html.ts';
import { badge, layout, originalUrlBlock } from './layout.ts';
import { renderMarkdown } from './markdown.ts';
import { linkify } from './text.ts';
/**
* Put the ratio on the media itself, never on the figure around it. On the
* figure, at the full width of the column, a portrait video forces a box
* taller than the window and the rest of the post gets pushed off screen.
*/
function aspect(item: Media): string {
return item.width && item.height ? `aspect-ratio: ${item.width} / ${item.height};` : '';
}
function renderItem(item: Media, index: number): Raw {
const src = proxyUrlFor(item);
if (item.kind === 'video') {
const poster = item.poster ? proxyUrlFor(item.poster) : undefined;
return html`
${item.hls
? html`Streaming format — plays natively in Safari.`
: ''}
`;
}
return html``;
}
function renderMedia(media: Media[]): Raw {
if (media.length === 0) return html``;
const many = media.length > 1;
return html`
${many
? html`
`;
}
function renderWhen(postedAt: string | undefined): Raw {
if (!postedAt) return html``;
const date = new Date(postedAt);
if (Number.isNaN(date.getTime())) return html``;
return html``;
}
/**
* The post being quoted, drawn as a post of its own inside the one quoting
* it: its author, its words, its pictures, and a way to open it.
*
* The author line is the point. Lifting only the media out — which is what
* this used to do — put someone else's picture under the quoter's name with
* nothing to say so.
*/
function renderQuoted(quoted: Quoted, post: Post): Raw {
const avatar = quoted.author.avatar ? proxyUrlFor(quoted.author.avatar) : undefined;
return html`
`;
}
/**
* One post in the chain. When there is only one, the wrapper is invisible;
* when there are several, each is separated and the one that was actually
* linked is marked, since it may not be the first.
*/
function renderSegment(segment: Segment, post: Post, threaded: boolean): Raw {
const above = post.textPosition === 'above';
return html`
${threaded && segment.isAnchor
? html`
the post you followed
`
: ''}
${above ? renderText(segment, post) : ''}
${renderMedia(segment.media)}
${above ? '' : renderText(segment, post)}
${segment.quoted ? renderQuoted(segment.quoted, post) : ''}
`;
}
/** Short enough to sit on a comment's byline without wrapping. Absolute
* rather than "3 hours ago", because the page is cached for an hour and a
* relative time would quietly become wrong while it sat there. */
function shortWhen(postedAt: string | undefined): Raw {
if (!postedAt) return html``;
const date = new Date(postedAt);
if (Number.isNaN(date.getTime())) return html``;
return html``;
}
/**
* A picture inside a comment.
*
* Through the proxy like everything else — a comment full of `preview.redd.it`
* addresses would otherwise have the viewer's browser fetch every one of them
* straight from Reddit, which is the thing this whole app exists to avoid.
*
* No dimensions to reserve space with: the size is in the payload but not in
* the Markdown, so these are capped by the stylesheet and load at whatever
* shape they are.
*/
function renderCommentImage(url: string, alt: string): string {
return html``
.value;
}
/** Everything hanging off a comment, however deep. Shown only while it is
* collapsed, so what a fold is hiding is never a mystery. */
function descendantsOf(comment: Comment): number {
return comment.replies.reduce((total, reply) => total + 1 + descendantsOf(reply), 0);
}
/**
* One comment and everything under it.
*
* A `` per comment, so collapsing works with the stylesheet turned
* off and the keyboard alone, and takes the whole subtree with it because
* the replies are nested inside rather than listed alongside.
*/
function renderComment(comment: Comment, depth: number): Raw {
const marks = [
comment.isAuthor ? 'OP' : undefined,
comment.distinguished === 'moderator' ? 'MOD' : undefined,
comment.distinguished === 'admin' ? 'ADMIN' : undefined,
].filter((mark) => mark !== undefined);
return html`${comment.author}
${marks.map((mark) => html`${mark}`)}
${typeof comment.score === 'number'
? html`${comment.score} ${Math.abs(comment.score) === 1 ? 'point' : 'points'}`
: ''}
${shortWhen(comment.postedAt)}
${descendantsOf(comment) > 0
? html`+${descendantsOf(comment)} ${
descendantsOf(comment) === 1 ? 'reply' : 'replies'
}`
: ''}
${comment.text
? html`
${comment.moreReplies} more ${
comment.moreReplies === 1 ? 'reply' : 'replies'
}, on Reddit
`
: ''}
`
: ''}
`;
}
/**
* The conversation under the post, where the platform has one.
*
* Everything the first page carried, in the order it was ranked. What is
* missing is what was behind a "load more" — following those means going to
* the platform, so they are counted rather than pretended away.
*/
function renderComments(post: Post): Raw {
const comments = post.comments;
if (!comments?.length) return html``;
const threads = comments.length;
const shown = comments.reduce((total, comment) => total + 1 + descendantsOf(comment), 0);
const total = post.commentCount;
return html`
Comments
${
total !== undefined && total > shown ? html`${shown} of ${total}` : html`${shown}`
}, in ${threads} ${threads === 1 ? 'thread' : 'threads'}