Skip to content

Commit 373a1a1

Browse files
lcompleteclaude
andcommitted
feat(client): render tweet detail with TweetRoot and smart title generation
- Add getPageDisplayTitle to derive display titles from tweet text/author - Render tweet pages using TweetRoot with engagement stats (reply/retweet/like/view) - Skip markdown-body styling and TOC for tweet content types - Update setDocTitle to handle null/undefined titles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 95410eb commit 373a1a1

3 files changed

Lines changed: 315 additions & 164 deletions

File tree

app/client/src/common/docUtils.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,85 @@
1-
export function setDocTitle(title:string){
2-
document.title = title + " / Huntly";
1+
import {TweetProperties} from "../interfaces/tweetProperties";
2+
3+
type PageTitleSource = {
4+
title?: string | null;
5+
description?: string | null;
6+
content?: string | null;
7+
contentType?: number | null;
8+
pageJsonProperties?: string | null;
9+
};
10+
11+
const TWEET_CONTENT_TYPES = new Set([1, 3]);
12+
const TWEET_TITLE_MAX_LENGTH = 80;
13+
14+
function normalizeTitlePart(value?: string | null) {
15+
return value?.replace(/\s+/g, " ").trim() || "";
16+
}
17+
18+
function truncateTitle(value: string, maxLength = TWEET_TITLE_MAX_LENGTH) {
19+
if (value.length <= maxLength) {
20+
return value;
21+
}
22+
23+
return value.slice(0, maxLength - 3).trimEnd() + "...";
24+
}
25+
26+
function extractTweetText(tweetProps?: TweetProperties | null) {
27+
if (!tweetProps?.fullText) {
28+
return "";
29+
}
30+
31+
if (tweetProps.noteTweet) {
32+
return normalizeTitlePart(tweetProps.fullText);
33+
}
34+
35+
const fullTextChars = Array.from(tweetProps.fullText);
36+
const start = tweetProps.displayTextRange?.[0] ?? 0;
37+
const end = tweetProps.displayTextRange?.[1] ?? fullTextChars.length;
38+
39+
return normalizeTitlePart(fullTextChars.slice(start, end).join(""));
40+
}
41+
42+
function resolveTweetTitle(pageJsonProperties?: string | null) {
43+
if (!pageJsonProperties) {
44+
return "";
45+
}
46+
47+
try {
48+
const tweetProps = JSON.parse(pageJsonProperties) as TweetProperties;
49+
const mainTweet = tweetProps?.retweetedTweet || tweetProps;
50+
const tweetText = extractTweetText(mainTweet);
51+
const author = normalizeTitlePart(mainTweet?.userName)
52+
|| (normalizeTitlePart(mainTweet?.userScreeName) ? `@${normalizeTitlePart(mainTweet?.userScreeName)}` : "");
53+
54+
if (author && tweetText) {
55+
return truncateTitle(`${author}: ${tweetText}`);
56+
}
57+
58+
return truncateTitle(tweetText || author);
59+
} catch (error) {
60+
return "";
61+
}
62+
}
63+
64+
export function getPageDisplayTitle(page?: PageTitleSource | null) {
65+
const explicitTitle = normalizeTitlePart(page?.title);
66+
if (explicitTitle) {
67+
return explicitTitle;
68+
}
69+
70+
if (TWEET_CONTENT_TYPES.has(page?.contentType ?? -1)) {
71+
const tweetTitle = resolveTweetTitle(page?.pageJsonProperties);
72+
if (tweetTitle) {
73+
return tweetTitle;
74+
}
75+
}
76+
77+
return truncateTitle(
78+
normalizeTitlePart(page?.description) || normalizeTitlePart(page?.content)
79+
);
80+
}
81+
82+
export function setDocTitle(title?: string | null){
83+
const normalizedTitle = normalizeTitlePart(title);
84+
document.title = normalizedTitle ? `${normalizedTitle} / Huntly` : "Huntly";
385
}

0 commit comments

Comments
 (0)