([ExtendedKind.EXTERNAL_REACTION])
-
function isWebThreadTailKind(kind: number): boolean {
- return EA_THREAD_TAIL_REFERENCE_KINDS.has(kind) || WEB_THREAD_EXTRA_TAIL_KINDS.has(kind)
+ return EA_THREAD_TAIL_REFERENCE_KINDS.has(kind)
}
/** Kind 1111 / 1244 that includes the thread root id on an e/E tag (common on relays; stricter root-tag walks may miss these). */
@@ -277,7 +262,7 @@ function noteReactionEtagEqualsHex(ev: NEvent, hexLower: string): boolean {
/**
* Thread REQ may still omit some kind-7 rows; merge reactions that tag the root hex so OP stats stay warm.
- * Listed reactions under βAntwortenβ come from {@link ReplyNoteList} BFS + {@link replyMatchesThreadForList}.
+ * Reactions are not listed under βAntwortenβ; this merge keeps OP stats warm when the thread REQ omits kind 7.
*/
function mergeFetchedKind7ReactionsIntoRootNoteStats(all: NEvent[], rootInfo: TRootInfo) {
if (rootInfo.type === 'E') {
@@ -1637,8 +1622,6 @@ function ReplyNoteList({
(item: NEvent) => {
if (isPollVoteKind(item)) return false
if (isZapPollThreadZapReceipt(item, event)) return false
- if (isLowEffortCollapsedReactionEvent(item, isDiscussionRoot)) return false
- if (shouldHideOwnReactionThreadRow(item, userPubkey)) return false
if (shouldHideThreadResponseEvent(item, mutePubkeySet, hideContentMentioningMutedUsers)) {
return false
}
@@ -1667,8 +1650,7 @@ function ReplyNoteList({
rootInfo?.type,
repliesMap,
event,
- isDiscussionRoot,
- userPubkey
+ isDiscussionRoot
]
)
@@ -1845,7 +1827,6 @@ function ReplyNoteList({
)}
-
{!loading && !quoteLoading && (
{mergedFeed.length > 0 ? t('no more replies') : t('no replies')}
diff --git a/src/lib/like-reaction-emojis.ts b/src/lib/like-reaction-emojis.ts
index c3966d6c..c3fb9ade 100644
--- a/src/lib/like-reaction-emojis.ts
+++ b/src/lib/like-reaction-emojis.ts
@@ -14,11 +14,6 @@ export const DEFAULT_LIKE_REACTION_CONTENT = '+' as const
*/
export const DEFAULT_LIKE_REACTION_DISPLAY_EMOJI = '\u2665\uFE0F'
-export const THUMBS_UP_DISPLAY_EMOJI = '\u{1F44D}' as const
-export const THUMBS_DOWN_DISPLAY_EMOJI = '\u{1F44E}' as const
-export const ARROW_UP_DISPLAY_EMOJI = '\u2B06\uFE0F' as const
-export const ARROW_DOWN_DISPLAY_EMOJI = '\u2B07\uFE0F' as const
-
export const DEFAULT_SUGGESTED_EMOJIS = [
DEFAULT_LIKE_REACTION_CONTENT,
'π',
@@ -50,7 +45,6 @@ const COMMON_HEART_LIKE_GLYPHS = new Set([
const THUMBS_UP_GLYPHS = new Set(['π', '+1'])
const THUMBS_DOWN_GLYPHS = new Set(['π', '-1'])
-
const ARROW_UP_GLYPHS = new Set(['β¬', 'β¬οΈ', 'β', 'πΌ'])
const ARROW_DOWN_GLYPHS = new Set(['β¬', 'β¬οΈ', 'β', 'π½'])
@@ -59,7 +53,6 @@ const THUMBS_UP_SHORTCODES = new Set(['thumbsup', 'thumbs_up', '+1', 'like', 'th
const THUMBS_DOWN_SHORTCODES = new Set(['thumbsdown', 'thumbs_down', '-1', 'thumbdown'])
const ARROW_UP_SHORTCODES = new Set(['arrow_up', 'arrowup', 'up', 'upvote', 'up_arrow'])
const ARROW_DOWN_SHORTCODES = new Set(['arrow_down', 'arrowdown', 'down', 'downvote', 'down_arrow'])
-/** NIP-30 shortcode only β not π (sympathy/sadness, not a downvote). */
const DISLIKE_SHORTCODES = new Set(['dislike'])
function normalizedReactionString(emoji: TEmoji | string): string | undefined {
@@ -70,7 +63,6 @@ function normalizedReactionString(emoji: TEmoji | string): string | undefined {
return undefined
}
-/** Strip emoji presentation selectors so β¬οΈ matches β¬. */
function normalizedGlyph(s: string): string {
return s.normalize('NFC').replace(/\ufe0f/gi, '').trim()
}
@@ -96,7 +88,7 @@ function matchesShortcodeSet(
return false
}
-export function isHeartOrPlusLikeReactionEmoji(emoji: TEmoji | string): boolean {
+function isHeartOrPlusLike(emoji: TEmoji | string): boolean {
if (typeof emoji === 'object' && emoji !== null && 'shortcode' in emoji) {
return HEART_LIKE_SHORTCODES.has(emoji.shortcode.trim().toLowerCase())
}
@@ -105,7 +97,7 @@ export function isHeartOrPlusLikeReactionEmoji(emoji: TEmoji | string): boolean
return c === '' || c === DEFAULT_LIKE_REACTION_CONTENT || COMMON_HEART_LIKE_GLYPHS.has(c)
}
-export function isThumbsUpReactionEmoji(emoji: TEmoji | string): boolean {
+function isThumbsUp(emoji: TEmoji | string): boolean {
const normalized = normalizedReactionString(emoji)
if (normalized === undefined) return false
if (matchesShortcodeSet(emoji, normalized, THUMBS_UP_SHORTCODES)) return true
@@ -113,7 +105,7 @@ export function isThumbsUpReactionEmoji(emoji: TEmoji | string): boolean {
return false
}
-export function isThumbsDownReactionEmoji(emoji: TEmoji | string): boolean {
+function isThumbsDown(emoji: TEmoji | string): boolean {
const normalized = normalizedReactionString(emoji)
if (normalized === undefined) return false
if (matchesShortcodeSet(emoji, normalized, THUMBS_DOWN_SHORTCODES)) return true
@@ -121,7 +113,7 @@ export function isThumbsDownReactionEmoji(emoji: TEmoji | string): boolean {
return false
}
-export function isArrowUpReactionEmoji(emoji: TEmoji | string): boolean {
+function isArrowUp(emoji: TEmoji | string): boolean {
const normalized = normalizedReactionString(emoji)
if (normalized === undefined) return false
if (matchesShortcodeSet(emoji, normalized, ARROW_UP_SHORTCODES)) return true
@@ -129,7 +121,7 @@ export function isArrowUpReactionEmoji(emoji: TEmoji | string): boolean {
return false
}
-export function isArrowDownReactionEmoji(emoji: TEmoji | string): boolean {
+function isArrowDown(emoji: TEmoji | string): boolean {
const normalized = normalizedReactionString(emoji)
if (normalized === undefined) return false
if (matchesShortcodeSet(emoji, normalized, ARROW_DOWN_SHORTCODES)) return true
@@ -137,8 +129,7 @@ export function isArrowDownReactionEmoji(emoji: TEmoji | string): boolean {
return false
}
-/** Explicit `dislike` shortcode / content only (not π or π). */
-export function isDislikeReactionEmoji(emoji: TEmoji | string): boolean {
+function isDislike(emoji: TEmoji | string): boolean {
const normalized = normalizedReactionString(emoji)
if (normalized === undefined) return false
if (matchesShortcodeSet(emoji, normalized, DISLIKE_SHORTCODES)) return true
@@ -146,39 +137,24 @@ export function isDislikeReactionEmoji(emoji: TEmoji | string): boolean {
return false
}
-export function isPositiveLowEffortReactionEmoji(emoji: TEmoji | string): boolean {
- return (
- isHeartOrPlusLikeReactionEmoji(emoji) ||
- isThumbsUpReactionEmoji(emoji) ||
- isArrowUpReactionEmoji(emoji)
- )
-}
-
-export function isNegativeLowEffortReactionEmoji(emoji: TEmoji | string): boolean {
- return (
- isThumbsDownReactionEmoji(emoji) ||
- isArrowDownReactionEmoji(emoji) ||
- isDislikeReactionEmoji(emoji)
- )
-}
-
/**
- * Reactions collapsed into {@link ThreadLowEffortStrip} and hidden as thread rows.
+ * Generic positive/negative reactions (hearts, +, thumbs, arrows, explicit dislike) β counted in
+ * note stats only; not rendered as separate thread rows.
*/
-export function isLowEffortCollapsedReactionEmoji(emoji: TEmoji | string): boolean {
- return isPositiveLowEffortReactionEmoji(emoji) || isNegativeLowEffortReactionEmoji(emoji)
-}
-
-/** @deprecated Prefer {@link isLowEffortCollapsedReactionEmoji}. */
-export function isDefaultPlusLikeReactionEmoji(emoji: TEmoji | string): boolean {
- return isLowEffortCollapsedReactionEmoji(emoji)
+export function isGenericStatsReactionEmoji(emoji: TEmoji | string): boolean {
+ return (
+ isHeartOrPlusLike(emoji) ||
+ isThumbsUp(emoji) ||
+ isThumbsDown(emoji) ||
+ isArrowUp(emoji) ||
+ isArrowDown(emoji) ||
+ isDislike(emoji)
+ )
}
-export function isLowEffortCollapsedReactionContent(content: string): boolean {
- return isLowEffortCollapsedReactionEmoji(content)
+export function isGenericStatsReactionContent(content: string): boolean {
+ return isGenericStatsReactionEmoji(content)
}
-/** @deprecated Prefer {@link isLowEffortCollapsedReactionContent}. */
-export function isDefaultPlusLikeReactionContent(content: string): boolean {
- return isLowEffortCollapsedReactionContent(content)
-}
+/** @deprecated Use {@link isGenericStatsReactionContent}. */
+export const isLowEffortCollapsedReactionContent = isGenericStatsReactionContent
diff --git a/src/lib/thread-response-filter.test.ts b/src/lib/thread-response-filter.test.ts
index 3b1dcb74..c56dabcd 100644
--- a/src/lib/thread-response-filter.test.ts
+++ b/src/lib/thread-response-filter.test.ts
@@ -4,7 +4,7 @@ import type { Event } from 'nostr-tools'
import { ExtendedKind } from '@/constants'
import {
isThreadBoosterOnlyRow,
- shouldHideOwnReactionThreadRow,
+ isThreadReactionOnlyRow,
shouldHideThreadResponseEvent
} from './thread-response-filter'
@@ -50,21 +50,23 @@ describe('thread response filter', () => {
expect(isThreadBoosterOnlyRow(repost)).toBe(true)
})
- it('hides viewer NIP-25 reactions in thread rows (own or othersβ notes)', () => {
- const viewer = 'b'.repeat(64)
- const myReaction = baseEvent({
- pubkey: viewer,
+ it('hides all NIP-25 reactions from thread rows (stats only)', () => {
+ const reaction = baseEvent({
kind: kinds.Reaction,
- content: '+',
+ content: 'π₯',
tags: [['e', 'c'.repeat(64), '', 'root']]
})
- expect(shouldHideOwnReactionThreadRow(myReaction, viewer)).toBe(true)
- expect(shouldHideOwnReactionThreadRow(myReaction, 'a'.repeat(64))).toBe(false)
- expect(
- shouldHideOwnReactionThreadRow(
- baseEvent({ pubkey: 'c'.repeat(64), kind: kinds.Reaction, content: '+' }),
- viewer
- )
- ).toBe(false)
+ expect(isThreadReactionOnlyRow(reaction)).toBe(true)
+ expect(shouldHideThreadResponseEvent(reaction, new Set(), false)).toBe(true)
+
+ const external = baseEvent({ kind: ExtendedKind.EXTERNAL_REACTION, content: 'π' })
+ expect(isThreadReactionOnlyRow(external)).toBe(true)
+ expect(shouldHideThreadResponseEvent(external, new Set(), false)).toBe(true)
+ })
+
+ it('does not hide kind-1 replies as reactions', () => {
+ const reply = baseEvent({ kind: kinds.ShortTextNote, content: 'thanks' })
+ expect(isThreadReactionOnlyRow(reply)).toBe(false)
+ expect(shouldHideThreadResponseEvent(reply, new Set(), false)).toBe(false)
})
})
diff --git a/src/lib/thread-response-filter.ts b/src/lib/thread-response-filter.ts
index 5419f72b..13d52cae 100644
--- a/src/lib/thread-response-filter.ts
+++ b/src/lib/thread-response-filter.ts
@@ -14,37 +14,39 @@ export function buildNormalizedBlockedRelaySet(blockedRelays: readonly string[]
}
/**
- * NIP-18 boosts: kind **6** (repost kind-1) and kind **16** (generic repost). Shown on the OP
- * booster strip only β never as discussion thread rows.
+ * NIP-18 boosts: kind **6** (repost kind-1) and kind **16** (generic repost). Stats on OP/replies
+ * only β never as thread rows (see notifications for full boost events).
*/
export function isThreadBoosterOnlyRow(evt: Event): boolean {
return isNip18RepostKind(evt.kind)
}
/**
- * The signed-in user's NIP-25 reactions are already on the note stats bar β omit duplicate thread rows.
- * Counts still use {@link noteStatsService} / merged stats; this only affects thread list rendering.
+ * NIP-25 reactions: kind **7** and **17** (external). Stats on OP/replies only β never thread rows.
*/
+export function isThreadReactionOnlyRow(evt: Event): boolean {
+ return isNip25ReactionKind(evt.kind)
+}
+
+/** @deprecated Use {@link isThreadReactionOnlyRow} / {@link shouldHideThreadResponseEvent}. */
export function shouldHideOwnReactionThreadRow(
item: Event,
- viewerPubkey: string | null | undefined
+ _viewerPubkey?: string | null
): boolean {
- const viewer = viewerPubkey?.trim().toLowerCase()
- if (!viewer || !/^[0-9a-f]{64}$/i.test(viewer)) return false
- if (item.pubkey.toLowerCase() !== viewer) return false
- return isNip25ReactionKind(item.kind)
+ return isThreadReactionOnlyRow(item)
}
/** @deprecated Use {@link shouldHideOwnReactionThreadRow}. */
export const shouldHideOwnReactionInOthersThread = shouldHideOwnReactionThreadRow
-/** Hide thread replies / backlinks: boosts, wire-format JSON blobs, muted author, or mute mentions. */
+/** Hide thread replies / backlinks: boosts, reactions, muted author, or mute mentions. */
export function shouldHideThreadResponseEvent(
evt: Event,
mutePubkeySet: Set,
hideContentMentioningMutedUsers: boolean | undefined
): boolean {
if (isThreadBoosterOnlyRow(evt)) return true
+ if (isThreadReactionOnlyRow(evt)) return true
if (muteSetHas(mutePubkeySet, evt.pubkey)) return true
if (hideContentMentioningMutedUsers === true && isMentioningMutedUsers(evt, mutePubkeySet)) return true
return false