Skip to content

Commit

Permalink
feat: Enhance comment reactions with profile resolution and rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
TimDaub committed Feb 18, 2025
1 parent 5861ab9 commit be08057
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 49 deletions.
170 changes: 129 additions & 41 deletions src/views/story.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,44 @@ export default async function (trie, theme, index, value, referral) {

story.comments = moderation.flag(story.comments, policy);

for await (let comment of story.comments) {
const profile = await ens.resolve(comment.identity);
if (profile && profile.displayName) {
comment.displayName = profile.displayName;
} else {
comment.displayName = comment.identity;
}
if (profile && profile.safeAvatar) {
comment.avatar = profile.safeAvatar;
}
// Collect all identities that need resolving
const identities = new Set();
story.comments.forEach((comment) => {
identities.add(comment.identity);
comment.reactions.forEach((reaction) => {
reaction.reactors.forEach((reactor) => identities.add(reactor));
});
});

// Resolve all profiles at once
const profileResults = await Promise.allSettled(
Array.from(identities).map((id) => ens.resolve(id)),
);

const resolvedProfiles = Object.fromEntries(
Array.from(identities).map((id, i) => [
id,
profileResults[i].status === "fulfilled" ? profileResults[i].value : null,
]),
);

// Enrich comments with resolved profiles
for (let comment of story.comments) {
const profile = resolvedProfiles[comment.identity];
comment.displayName = profile?.displayName || comment.identity;
comment.avatar = profile?.safeAvatar;
comment.identity = {
address: comment.identity,
...profile,
};

// Enrich reactions with resolved profiles
comment.reactions = comment.reactions.map((reaction) => ({
...reaction,
reactorProfiles: reaction.reactors
.map((reactor) => resolvedProfiles[reactor])
.filter(Boolean),
}));
}
const actions = profiles.sort((a, b) => a.timestamp - b.timestamp);
story.avatars = avatars;
Expand Down Expand Up @@ -345,7 +373,8 @@ export default async function (trie, theme, index, value, referral) {
>${!comment.flagged
? html`<a
style="color: black;"
href="/upvotes?address=${comment.identity}"
href="/upvotes?address=${comment
.identity.address}"
>${truncateName(comment.displayName)}</a
>`
: truncateName(comment.displayName)}</b
Expand Down Expand Up @@ -391,36 +420,95 @@ export default async function (trie, theme, index, value, referral) {
>Moderated because: "${comment.reason}"</i
>`
: html`<span
class="comment-text"
dangerouslySetInnerHTML=${{
__html: comment.title
.split("\n")
.map((line) => {
if (line.startsWith(">")) {
return `<div style="border-left: 3px solid #ccc; padding-left: 10px; margin: 8px 0 0 0; color: #666;">${DOMPurify.sanitize(
line.substring(2),
)}</div>`;
}
return line.trim()
? `<div>${DOMPurify.sanitize(
line,
)}</div>`
: "<br/>";
})
.join("")
.replace(
/(https?:\/\/[^\s<]+)/g,
(url) =>
`<a class="meta-link selectable-link" href="${url}" target="${
url.startsWith(
"https://news.kiwistand.com",
)
? "_self"
: "_blank"
}">${url}</a>`,
),
}}
></span>`}
class="comment-text"
dangerouslySetInnerHTML=${{
__html: comment.title
.split("\n")
.map((line) => {
if (line.startsWith(">")) {
return `<div style="border-left: 3px solid #ccc; padding-left: 10px; margin: 8px 0 0 0; color: #666;">${DOMPurify.sanitize(
line.substring(2),
)}</div>`;
}
return line.trim()
? `<div>${DOMPurify.sanitize(
line,
)}</div>`
: "<br/>";
})
.join("")
.replace(
/(https?:\/\/[^\s<]+)/g,
(url) =>
`<a class="meta-link selectable-link" href="${url}" target="${
url.startsWith(
"https://news.kiwistand.com",
)
? "_self"
: "_blank"
}">${url}</a>`,
),
}}
></span>
<div
class="reactions-container"
data-comment-index="${comment.index}"
data-comment="${JSON.stringify({
...comment,
reactions: (
comment.reactions || []
).map((reaction) => ({
...reaction,
reactors: reaction.reactors,
reactorProfiles:
reaction.reactorProfiles,
})),
})}"
style="display: flex; flex-wrap: wrap; gap: 16px; min-height: 59px;"
>
${["🥝", "🔥", "👀", "💯", "🤭"].map(
(emoji) => {
const reaction =
comment.reactions.find(
(r) => r.emoji === emoji,
);
return html`
<div
style="margin-top: 32px; display: inline-flex; align-items: center; padding: 4px 12px; background-color: var(--bg-off-white); border: var(--border-thin); border-radius: 2px; font-size: 10pt;"
>
<span
style="margin-right: ${reaction
?.reactors?.length
? "4px"
: "0"}"
>${emoji}</span
>
${reaction?.reactorProfiles?.map(
(profile, i) => html`
<img
loading="lazy"
src="${profile.safeAvatar}"
alt="reactor"
style="z-index: ${i}; width: ${i >
0
? "13px"
: "12px"}; height: ${i > 0
? "13px"
: "12px"}; border-radius: 2px; border: ${i >
0
? "1px solid #f3f3f3"
: "1px solid #828282"}; margin-left: ${i >
0
? "-4px"
: "0"};"
/>
`,
)}
</div>
`;
},
)}
</div>`}
</span>`,
)}
</div>
Expand Down
21 changes: 14 additions & 7 deletions src/web/src/CommentSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function truncateName(name) {
return name.slice(0, maxLength) + "...";
}

const EmojiReaction = ({ comment, allowlist, delegations, toast }) => {
export const EmojiReaction = ({ comment, allowlist, delegations, toast }) => {
const [isReacting, setIsReacting] = useState(false);
const [kiwis, setKiwis] = useState(
comment.reactions?.find((r) => r.emoji === "🥝")?.reactors || [],
Expand Down Expand Up @@ -137,12 +137,19 @@ const EmojiReaction = ({ comment, allowlist, delegations, toast }) => {
);

// Optimistically update UI with reaction immediately using pre-resolved avatar if available
const resolvedAvatar = preResolvedAvatar || await resolveAvatar(identity);
const newReaction = {
emoji,
reactorProfiles: [{ address: identity, safeAvatar: resolvedAvatar }],
};
comment.reactions.push(newReaction);
const resolvedAvatar =
preResolvedAvatar || (await resolveAvatar(identity));
const existingReaction = comment.reactions.find(
(r) => r.emoji === emoji && Array.isArray(r.reactorProfiles)
);
if (existingReaction) {
existingReaction.reactorProfiles.push({ address: identity, safeAvatar: resolvedAvatar });
} else {
comment.reactions.push({
emoji,
reactorProfiles: [{ address: identity, safeAvatar: resolvedAvatar }],
});
}

switch (emoji) {
case "🥝":
Expand Down
52 changes: 52 additions & 0 deletions src/web/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,53 @@ async function addAvatar(allowlist) {
}
}

async function addStoryEmojiReactions(allowlist, delegations, toast) {
const reactionContainers = document.querySelectorAll(".reactions-container");
if (reactionContainers && reactionContainers.length > 0) {
const [commentSection, wagmi, rainbowKit, clientConfig] = await Promise.all([
import("./CommentSection.jsx"),
import("wagmi"),
import("@rainbow-me/rainbowkit"),
import("./client.mjs"),
]);

const { EmojiReaction } = commentSection;
const { WagmiConfig } = wagmi;
const { RainbowKitProvider } = rainbowKit;
const { client, chains } = clientConfig;

reactionContainers.forEach((container) => {
const commentData = container.getAttribute("data-comment");
if (commentData) {
const comment = JSON.parse(commentData);
const root = createRoot(container);

// Keep existing content as fallback while React loads
const existingContent = container.innerHTML;

// Prepare the React component
const reactComponent = (
<StrictMode>
<WagmiConfig config={client}>
<RainbowKitProvider chains={chains}>
<EmojiReaction
comment={comment}
allowlist={allowlist}
delegations={delegations}
toast={toast}
/>
</RainbowKitProvider>
</WagmiConfig>
</StrictMode>
);

// Render React component while preserving existing content
root.render(reactComponent);
}
});
}
}

async function addNFTPrice() {
const nftPriceElements = document.querySelectorAll("nft-price");
if (nftPriceElements && nftPriceElements.length > 0) {
Expand Down Expand Up @@ -906,6 +953,11 @@ async function start() {
const results1 = await Promise.allSettled([
addDynamicNavElements(),
addInviteLink(toast),
addStoryEmojiReactions(
await allowlistPromise,
await delegationsPromise,
toast,
),
addDecayingPriceLink(),
addCommentInput(toast, await allowlistPromise, await delegationsPromise),
addSubscriptionButton(await allowlistPromise, toast),
Expand Down
2 changes: 1 addition & 1 deletion src/web/src/request_monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// NOTE: This script monitors <img> elements for extremely delayed loading.
// If an image takes longer than DEFAULT_IMAGE_TIMEOUT to load,
// its src attribute will be cleared to stop the hanging request.
const DEFAULT_IMAGE_TIMEOUT = 5000;
const DEFAULT_IMAGE_TIMEOUT = 10000;

function handleImage(img) {
if (!img.dataset.timeoutSet) {
Expand Down

0 comments on commit be08057

Please sign in to comment.