diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 1ba97d0a..41e66359 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -937,9 +937,7 @@ function MainContentArea({ onClick={goBack} > -
- Back -
+
Back
@@ -2051,7 +2049,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { onClick={goBack} > -
+
{primaryViewType === 'settings' || primaryViewType === 'settings-sub' ? 'Settings' : primaryViewType === 'profile' @@ -2245,7 +2243,11 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } }} > - +
{secondaryStack.map((item, index) => { const isLast = index === secondaryStack.length - 1 diff --git a/src/components/EmojiPicker/index.tsx b/src/components/EmojiPicker/index.tsx index 16a07ef2..7df81692 100644 --- a/src/components/EmojiPicker/index.tsx +++ b/src/components/EmojiPicker/index.tsx @@ -10,10 +10,18 @@ import EmojiPickerReact, { Theme } from 'emoji-picker-react' +export { EMOJI_PICKER_REACTIONS } from '@/lib/like-reaction-emojis' + export default function EmojiPicker({ - onEmojiClick + onEmojiClick, + reactionsDefaultOpen, + reactions }: { onEmojiClick: (emoji: string | TEmoji | undefined, event: MouseEvent) => void + /** When true, show the compact reactions row first (tap + for full picker). */ + reactionsDefaultOpen?: boolean + /** Unified ids for the reactions row; for likes use {@link EMOJI_PICKER_REACTIONS}. */ + reactions?: string[] }) { const { themeSetting } = useTheme() const { isSmallScreen } = useScreenSize() @@ -43,6 +51,8 @@ export default function EmojiPicker({ onEmojiClick(emoji, e) }} customEmojis={customEmojiService.getAllCustomEmojisForPicker()} + {...(reactionsDefaultOpen !== undefined ? { reactionsDefaultOpen } : {})} + {...(reactions !== undefined ? { reactions } : {})} /> ) } diff --git a/src/components/EmojiPickerDialog/index.tsx b/src/components/EmojiPickerDialog/index.tsx index 943668bc..6c8c40bb 100644 --- a/src/components/EmojiPickerDialog/index.tsx +++ b/src/components/EmojiPickerDialog/index.tsx @@ -26,7 +26,14 @@ export default function EmojiPickerDialog({ return ( {children} - + { + const t = e.target as HTMLElement | null + if (t?.closest?.('[data-vaul-overlay]')) return + e.preventDefault() + }} + > Emoji Picker diff --git a/src/components/NoteDrawer/index.tsx b/src/components/NoteDrawer/index.tsx index a70c381b..f633f35f 100644 --- a/src/components/NoteDrawer/index.tsx +++ b/src/components/NoteDrawer/index.tsx @@ -57,7 +57,11 @@ export default function NoteDrawer({ open, onOpenChange, noteId, initialEvent }: }} registerWithModalManager={false} > - +
{ const now = Math.floor(Date.now() / 1000) - return createFakeEvent({ + const base: TDraftEvent = { kind, content, tags: normalizedTags, + created_at: now + } + const withAttribution = applyImwaldAttributionTags(base, { + addClientTag: storage.getAddClientTag() + }) + return createFakeEvent({ + kind, + content, + tags: withAttribution.tags, pubkey: pubkey ?? '', created_at: now }) }, [kind, content, normalizedTags, pubkey]) const buildDraftJson = useCallback(() => { - const draft = { - pubkey: pubkey ?? t('Log in to publish'), - kind, - content, - tags: normalizedTags, - created_at: t('Set when you publish'), - _note: t('id and sig are assigned when you publish') - } + const base: TDraftEvent = { + kind, + content, + tags: normalizedTags, + created_at: dayjs().unix() + } + const withAttribution = applyImwaldAttributionTags(base, { + addClientTag: storage.getAddClientTag() + }) + const draft = { + pubkey: pubkey ?? t('Log in to publish'), + kind: withAttribution.kind, + content: withAttribution.content, + tags: withAttribution.tags, + created_at: t('Set when you publish'), + _note: t('id and sig are assigned when you publish') + } return JSON.stringify(draft, null, 2) }, [pubkey, kind, content, normalizedTags, t]) @@ -189,7 +211,9 @@ export default function EditOrCloneEventDialog({ tags: normalizedTags, created_at: dayjs().unix() } - const newEvent = await publish(draft) + const newEvent = await publish(draft, { + addClientTag: storage.getAddClientTag() + }) if ((newEvent as any)?.relayStatuses) { const rs = (newEvent as any).relayStatuses showPublishingFeedback( @@ -342,7 +366,14 @@ export default function EditOrCloneEventDialog({ - +
+ {storage.getAddClientTag() ? ( +
+ +
+ ) : null} + +
diff --git a/src/components/NoteStats/LikeButton.tsx b/src/components/NoteStats/LikeButton.tsx index ae87d3a8..8c855fb5 100644 --- a/src/components/NoteStats/LikeButton.tsx +++ b/src/components/NoteStats/LikeButton.tsx @@ -1,4 +1,4 @@ -import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer' +import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' import { DropdownMenu, DropdownMenuContent, @@ -25,6 +25,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useUserTrust } from '@/contexts/user-trust-context' import { eventService } from '@/services/client.service' import noteStatsService from '@/services/note-stats.service' +import storage from '@/services/local-storage.service' import { TEmoji } from '@/types' import { SmilePlus } from 'lucide-react' import { Event } from 'nostr-tools' @@ -32,8 +33,7 @@ import { useMemo, useState } from 'react' import logger from '@/lib/logger' import { useTranslation } from 'react-i18next' import Emoji from '../Emoji' -import EmojiPicker from '../EmojiPicker' -import SuggestedEmojis from '../SuggestedEmojis' +import EmojiPicker, { EMOJI_PICKER_REACTIONS } from '../EmojiPicker' import { formatCount } from './utils' import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { WEB_EXTERNAL_REACTION_PUBLISHED_EVENT } from '@/lib/rss-web-feed' @@ -46,7 +46,6 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const [liking, setLiking] = useState(false) const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false) - const [isPickerOpen, setIsPickerOpen] = useState(false) const noteStats = useNoteStatsById(event.id) const isDiscussion = event.kind === ExtendedKind.DISCUSSION const inQuietMode = shouldHideInteractions(event) @@ -124,7 +123,9 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; if (reactionEvent) { // Create and publish a deletion request (kind 5) const deletionRequest = createDeletionRequestDraftEvent(reactionEvent) - const deletedEvent = await publish(deletionRequest) + const deletedEvent = await publish(deletionRequest, { + addClientTag: storage.getAddClientTag() + }) // Show publishing feedback if ((deletedEvent as any)?.relayStatuses) { @@ -151,7 +152,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; } else { // User is adding a new reaction const reaction = createReactionDraftEvent(event, emoji) - const evt = await publish(reaction) + const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() }) // Show publishing feedback if ((evt as any)?.relayStatuses) { @@ -258,24 +259,35 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; ) } + const likeEmojiPicker = ( + { + e.stopPropagation() + setIsEmojiReactionsOpen(false) + if (!emoji) return + like(emoji) + }} + /> + ) + if (isSmallScreen) { return ( <> {trigger} - setIsEmojiReactionsOpen(false)} /> - + { + const t = e.target as HTMLElement | null + if (t?.closest?.('[data-vaul-overlay]')) return + e.preventDefault() + }} + > React - { - setIsEmojiReactionsOpen(false) - if (!emoji) return - - like(emoji) - }} - /> + {likeEmojiPicker} @@ -283,38 +295,10 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; } return ( - { - setIsEmojiReactionsOpen(open) - if (open) { - setIsPickerOpen(false) - } - }} - > + {trigger} - {isPickerOpen ? ( - { - e.stopPropagation() - setIsEmojiReactionsOpen(false) - if (!emoji) return - - like(emoji) - }} - /> - ) : ( - { - setIsEmojiReactionsOpen(false) - like(emoji) - }} - onMoreButtonClick={() => { - setIsPickerOpen(true) - }} - /> - )} + {likeEmojiPicker} ) diff --git a/src/components/NoteStats/Likes.tsx b/src/components/NoteStats/Likes.tsx index ba45170d..9816d8dd 100644 --- a/src/components/NoteStats/Likes.tsx +++ b/src/components/NoteStats/Likes.tsx @@ -8,6 +8,7 @@ import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { useUserTrust } from '@/contexts/user-trust-context' import noteStatsService from '@/services/note-stats.service' +import storage from '@/services/local-storage.service' import { TEmoji } from '@/types' import { Event } from 'nostr-tools' import { useMemo, useRef, useState } from 'react' @@ -66,7 +67,7 @@ export default function Likes({ event }: { event: Event }) { try { const reaction = createReactionDraftEvent(event, emoji) - const evt = await publish(reaction) + const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() }) noteStatsService.updateNoteStatsByEvents([evt], undefined, { interactionTargetNoteId: event.id }) diff --git a/src/components/NoteStats/RepostButton.tsx b/src/components/NoteStats/RepostButton.tsx index 71990e01..2013e8b7 100644 --- a/src/components/NoteStats/RepostButton.tsx +++ b/src/components/NoteStats/RepostButton.tsx @@ -16,6 +16,7 @@ import { useNostr } from '@/providers/NostrProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useUserTrust } from '@/contexts/user-trust-context' import noteStatsService from '@/services/note-stats.service' +import storage from '@/services/local-storage.service' import { PencilLine, Repeat } from 'lucide-react' import { Event } from 'nostr-tools' import { useMemo, useState } from 'react' @@ -62,7 +63,7 @@ export default function RepostButton({ event, hideCount = false }: { event: Even } const repost = createRepostDraftEvent(event) - const evt = await publish(repost) + const evt = await publish(repost, { addClientTag: storage.getAddClientTag() }) // Show publishing feedback if ((evt as any)?.relayStatuses) { diff --git a/src/components/NoteStats/VoteButtons.tsx b/src/components/NoteStats/VoteButtons.tsx index 9680e8d9..f8365202 100644 --- a/src/components/NoteStats/VoteButtons.tsx +++ b/src/components/NoteStats/VoteButtons.tsx @@ -9,6 +9,7 @@ import { createReactionDraftEvent } from '@/lib/draft-event' import { useNoteStatsRelayHints } from '@/hooks/useNoteStatsRelayHints' import { useNostr } from '@/providers/NostrProvider' import noteStatsService from '@/services/note-stats.service' +import storage from '@/services/local-storage.service' import { Event } from 'nostr-tools' import { ChevronDown, ChevronUp } from 'lucide-react' import { useMemo, useState } from 'react' @@ -72,8 +73,8 @@ export default function VoteButtons({ event }: { event: Event }) { if (existingVote) { // Remove vote by creating a reaction with the same emoji (this will toggle it off) const reaction = createReactionDraftEvent(event, emoji) - const evt = await publish(reaction) - + const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() }) + // Show publishing feedback if ((evt as any)?.relayStatuses) { showPublishingFeedback({ @@ -97,12 +98,12 @@ export default function VoteButtons({ event }: { event: Event }) { if (userVote) { const oldEmoji = userVote === 'up' ? DISCUSSION_UPVOTE : DISCUSSION_DOWNVOTE const removeReaction = createReactionDraftEvent(event, oldEmoji) - await publish(removeReaction) + await publish(removeReaction, { addClientTag: storage.getAddClientTag() }) } - + // Then add the new vote const reaction = createReactionDraftEvent(event, emoji) - const evt = await publish(reaction) + const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() }) // Show publishing feedback if ((evt as any)?.relayStatuses) { diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index f77db4b7..a676e542 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -30,7 +30,8 @@ import { createCitationInternalDraftEvent, createCitationExternalDraftEvent, createCitationHardcopyDraftEvent, - createCitationPromptDraftEvent + createCitationPromptDraftEvent, + applyImwaldAttributionTags } from '@/lib/draft-event' import { ExtendedKind } from '@/constants' import { cn, isTouchDevice } from '@/lib/utils' @@ -1012,11 +1013,11 @@ export default function PostContent({ const cleanedText = rewritePlainTextHttpUrls(text) const draftEvent = await createDraftEvent(cleanedText) - return JSON.stringify(draftEvent, null, 2) + return JSON.stringify(applyImwaldAttributionTags(draftEvent, { addClientTag }), null, 2) } catch (error) { return JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) } - }, [text, pubkey, isDiscussionThread, createDraftEvent]) + }, [text, pubkey, isDiscussionThread, createDraftEvent, addClientTag]) const post = async (e?: React.MouseEvent) => { e?.stopPropagation() @@ -2662,6 +2663,7 @@ export default function PostContent({ extraPreviewTags={ isDiscussionThread && !parentEvent ? discussionPreviewExtraTags : rssReplyExtraPreviewTags } + addClientTag={addClientTag} mediaImetaTags={mediaImetaTags} mediaUrl={mediaUrl} headerActions={ diff --git a/src/components/PostEditor/PostTextarea/Preview.tsx b/src/components/PostEditor/PostTextarea/Preview.tsx index b8e08516..2894cd40 100644 --- a/src/components/PostEditor/PostTextarea/Preview.tsx +++ b/src/components/PostEditor/PostTextarea/Preview.tsx @@ -1,6 +1,12 @@ +import ClientTag from '@/components/ClientTag' import { Card } from '@/components/ui/card' import { ExtendedKind, POLL_TYPE } from '@/constants' -import { transformCustomEmojisInContent } from '@/lib/draft-event' +import { + buildAltTag, + buildClientTag, + stripImwaldAttributionTags, + transformCustomEmojisInContent +} from '@/lib/draft-event' import { normalizeTopic } from '@/lib/discussion-topics' import { createFakeEvent } from '@/lib/event' import { randomString } from '@/lib/random' @@ -9,7 +15,7 @@ import { cn } from '@/lib/utils' import { TPollCreateData } from '@/types' import { kinds, nip19 } from 'nostr-tools' import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content' -import { useMemo } from 'react' +import { useMemo, type ReactNode } from 'react' import ContentPreview from '../../ContentPreview' import Content from '../../Content' import Highlight from '../../Note/Highlight' @@ -26,7 +32,8 @@ export default function Preview({ mediaImetaTags, mediaUrl, articleMetadata, - extraPreviewTags + extraPreviewTags, + addClientTag = true }: { content: string className?: string @@ -44,6 +51,8 @@ export default function Preview({ } /** Merged into the fake event (e.g. kind 11 discussion title / topic tags). */ extraPreviewTags?: string[][] + /** When true (default), preview matches publish: Imwald `client` + attribution `alt` tags and badge. */ + addClientTag?: boolean }) { const { content: processedContent, emojiTags, highlightTags, pollTags } = useMemo( () => { @@ -153,8 +162,12 @@ export default function Preview({ if (extraPreviewTags?.length) { tags.push(...extraPreviewTags) } - return tags - }, [emojiTags, highlightTags, pollTags, mediaImetaTags, articleMetadata, kind, extraPreviewTags]) + const stripped = stripImwaldAttributionTags(tags) + if (addClientTag) { + stripped.push(buildClientTag(), buildAltTag()) + } + return stripped + }, [emojiTags, highlightTags, pollTags, mediaImetaTags, articleMetadata, kind, extraPreviewTags, addClientTag]) const fakeEvent = useMemo(() => { // For voice comments, include the media URL in content if not already there @@ -169,11 +182,23 @@ export default function Preview({ kind }) }, [processedContent, allTags, kind, mediaUrl]) - + const selectableClass = 'select-text' + const withClientBadge = (node: ReactNode) => + addClientTag ? ( +
+
+ +
+ {node} +
+ ) : ( + node + ) + // For polls, use ContentPreview to show poll properly if (kind === ExtendedKind.POLL) { - return ( + return withClientBadge( @@ -182,7 +207,7 @@ export default function Preview({ // For highlights, use the Highlight component for proper formatting if (kind === kinds.Highlights) { - return ( + return withClientBadge( @@ -192,7 +217,7 @@ export default function Preview({ // For kind 1 notes, use MarkdownArticle to match actual rendering // This ensures preview matches the final result (no Links section, correct image placement, proper line breaks) if (kind === kinds.ShortTextNote || kind === ExtendedKind.COMMENT || kind === ExtendedKind.VOICE_COMMENT) { - return ( + return withClientBadge( @@ -200,7 +225,7 @@ export default function Preview({ } if (kind === ExtendedKind.DISCUSSION) { - return ( + return withClientBadge( @@ -209,7 +234,7 @@ export default function Preview({ // For LongFormArticle, use MarkdownArticle if (kind === kinds.LongFormArticle) { - return ( + return withClientBadge( @@ -218,7 +243,7 @@ export default function Preview({ // For WikiArticle (AsciiDoc), use AsciidocArticle if (kind === ExtendedKind.WIKI_ARTICLE) { - return ( + return withClientBadge( @@ -227,7 +252,7 @@ export default function Preview({ // For WikiArticleMarkdown, use MarkdownArticle if (kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) { - return ( + return withClientBadge( @@ -236,14 +261,14 @@ export default function Preview({ // For PublicationContent, use AsciidocArticle if (kind === ExtendedKind.PUBLICATION_CONTENT) { - return ( + return withClientBadge( ) } - return ( + return withClientBadge( diff --git a/src/components/PostEditor/PostTextarea/index.tsx b/src/components/PostEditor/PostTextarea/index.tsx index 1002d726..c519be27 100644 --- a/src/components/PostEditor/PostTextarea/index.tsx +++ b/src/components/PostEditor/PostTextarea/index.tsx @@ -59,6 +59,7 @@ const PostTextarea = forwardRef< topics?: string[] } extraPreviewTags?: string[][] + addClientTag?: boolean } >( ( @@ -80,7 +81,8 @@ const PostTextarea = forwardRef< mediaImetaTags, mediaUrl, articleMetadata, - extraPreviewTags + extraPreviewTags, + addClientTag = true }, ref ) => { @@ -271,6 +273,7 @@ const PostTextarea = forwardRef< mediaUrl={mediaUrl} articleMetadata={articleMetadata} extraPreviewTags={extraPreviewTags} + addClientTag={addClientTag} />
diff --git a/src/components/Settings/SettingsMenuBody.tsx b/src/components/Settings/SettingsMenuBody.tsx index d151646e..6a45f7c9 100644 --- a/src/components/Settings/SettingsMenuBody.tsx +++ b/src/components/Settings/SettingsMenuBody.tsx @@ -156,7 +156,7 @@ export default function SettingsMenuBody({ className }: { className?: string })
-
Imwald
+
Imwald
Im Wald
diff --git a/src/components/SuggestedEmojis/index.tsx b/src/components/SuggestedEmojis/index.tsx index 93bfa105..88b4057a 100644 --- a/src/components/SuggestedEmojis/index.tsx +++ b/src/components/SuggestedEmojis/index.tsx @@ -1,4 +1,5 @@ import { Button } from '@/components/ui/button' +import { DEFAULT_SUGGESTED_EMOJIS } from '@/lib/like-reaction-emojis' import { parseEmojiPickerUnified } from '@/lib/utils' import { TEmoji } from '@/types' import { getSuggested } from 'emoji-picker-react/src/dataUtils/suggested' @@ -6,8 +7,6 @@ import { MoreHorizontal } from 'lucide-react' import { useEffect, useState } from 'react' import Emoji from '../Emoji' -const DEFAULT_SUGGESTED_EMOJIS = ['👍', '❤️', '😂', '🥲', '👀', '🫡', '🫂'] - export default function SuggestedEmojis({ onEmojiClick, onMoreButtonClick diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 45be7aa4..2cf31ba6 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -63,7 +63,7 @@ const AlertDialogTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 4ee3973f..d372c7fb 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -119,7 +119,7 @@ const DialogTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/src/components/ui/drawer.tsx b/src/components/ui/drawer.tsx index 793c4cbb..15609a15 100644 --- a/src/components/ui/drawer.tsx +++ b/src/components/ui/drawer.tsx @@ -111,7 +111,7 @@ const DrawerTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index d3e5de25..5ddeedba 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -146,7 +146,7 @@ const SheetTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )) diff --git a/src/index.css b/src/index.css index bffa2b25..42b1a3c2 100644 --- a/src/index.css +++ b/src/index.css @@ -199,6 +199,14 @@ } @layer components { + /** + * Playfair for page/modal chrome titles only (not nav labels or article body). + * Use with optional utilities: truncate, leading-tight, pl-3, etc. + */ + .app-chrome-title { + @apply font-display text-lg font-semibold; + } + .imwald-sidebar { position: relative; isolation: isolate; diff --git a/src/layouts/SecondaryPageLayout/index.tsx b/src/layouts/SecondaryPageLayout/index.tsx index 07977276..f42bac62 100644 --- a/src/layouts/SecondaryPageLayout/index.tsx +++ b/src/layouts/SecondaryPageLayout/index.tsx @@ -177,7 +177,7 @@ export function SecondaryPageTitlebar({
{hideBackButton ? ( -
+
{title}
) : ( @@ -204,7 +204,7 @@ function BackButton({ children }: { children?: React.ReactNode }) { onClick={() => pop()} > -
{children}
+
{children}
) } diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index 305f606b..f2db71dd 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -1405,8 +1405,62 @@ export function buildClientTag(handlerPubkey?: string, handlerIdentifier?: strin return ['client', 'imwald'] } -export function buildAltTag() { - return ['alt', 'This event was published by https://jumble.imwald.eu.'] +/** Canonical `alt` we attach for Imwald / jumble.imwald.eu publishing attribution (NIP-31). */ +export const IMWALD_ATTRIBUTION_ALT_TEXT = 'This event was published by https://jumble.imwald.eu.' + +export function buildAltTag(): string[] { + return ['alt', IMWALD_ATTRIBUTION_ALT_TEXT] +} + +/** + * True for `alt` tags that are *our* app attribution (current or legacy Jumble/Imwald wording). + * Does not match arbitrary user `alt` text unless it clearly points at this app. + */ +export function isImwaldAppAttributionAltTag(tag: string[]): boolean { + if (!Array.isArray(tag) || tag[0] !== 'alt' || tag.length < 2) return false + const raw = tag[1] + if (typeof raw !== 'string') return false + const v = raw.trim() + if (v === IMWALD_ATTRIBUTION_ALT_TEXT) return true + const l = v.toLowerCase() + if (l.includes('jumble.imwald.eu')) return true + if ( + /^this event was published\b/i.test(v) && + (l.includes('imwald') || l.includes('jumble')) + ) { + return true + } + return false +} + +/** Removes every `client` tag and any Jumble/Imwald attribution `alt` (see {@link isImwaldAppAttributionAltTag}). */ +export function stripImwaldAttributionTags(tags: string[][]): string[][] { + return tags.filter( + (tag) => + Array.isArray(tag) && + tag[0] !== 'client' && + !isImwaldAppAttributionAltTag(tag) + ) +} + +/** + * Before sign/publish: strip all `client` tags and Imwald/Jumble attribution `alt` tags, then + * append exactly one {@link buildClientTag} + {@link buildAltTag} when `addClientTag !== false`. + */ +export function applyImwaldAttributionTags( + draftEvent: TDraftEvent, + options?: { addClientTag?: boolean } +): TDraftEvent { + const draft = JSON.parse(JSON.stringify(draftEvent)) as TDraftEvent + const existingTags = Array.isArray(draft.tags) ? draft.tags : [] + const sanitizedTags = stripImwaldAttributionTags(existingTags) + const shouldAdd = options?.addClientTag !== false + if (shouldAdd) { + draft.tags = [...sanitizedTags, buildClientTag(), buildAltTag()] + } else { + draft.tags = [...sanitizedTags] + } + return draft } function buildNsfwTag() { diff --git a/src/lib/event.ts b/src/lib/event.ts index 8deb33b0..7d011908 100644 --- a/src/lib/event.ts +++ b/src/lib/event.ts @@ -529,11 +529,13 @@ export function getNoteBech32Id(event: Event) { export function getUsingClient(event: Event) { const clientTag = event.tags.find(tagNameEquals('client')) if (!clientTag) return undefined - + // NIP-89 client tag format: ["client", "Client Name", "31990:pubkey:identifier", "relay"] // Simple format: ["client", "client_name"] - // For display purposes, we use the client name (second element) - return clientTag[1] + const name = clientTag[1] + if (!name) return undefined + if (name.toLowerCase() === 'imwald') return 'Imwald' + return name } export function getImetaInfosFromEvent(event: Event) { diff --git a/src/lib/like-reaction-emojis.ts b/src/lib/like-reaction-emojis.ts new file mode 100644 index 00000000..fcb396ab --- /dev/null +++ b/src/lib/like-reaction-emojis.ts @@ -0,0 +1,19 @@ +/** + * Single source for the quick-like emoji row (SuggestedEmojis “+” row uses the same glyphs; + * emoji-picker-react needs hex unified ids — see {@link EMOJI_PICKER_REACTIONS}). + */ +export const DEFAULT_SUGGESTED_EMOJIS = ['❤️', '👍', '🔥', '😂', '😢', '🫂', '🚀'] as const + +function emojiToPickerUnified(emoji: string): string { + const parts: string[] = [] + for (const ch of emoji) { + const cp = ch.codePointAt(0) + if (cp != null) parts.push(cp.toString(16)) + } + return parts.join('-') +} + +/** Unified ids for `emoji-picker-react` reactions row — derived from {@link DEFAULT_SUGGESTED_EMOJIS}. */ +export const EMOJI_PICKER_REACTIONS: readonly string[] = DEFAULT_SUGGESTED_EMOJIS.map((e) => + emojiToPickerUnified(e) +) diff --git a/src/pages/primary/ExplorePage/index.tsx b/src/pages/primary/ExplorePage/index.tsx index c7701953..4da1006c 100644 --- a/src/pages/primary/ExplorePage/index.tsx +++ b/src/pages/primary/ExplorePage/index.tsx @@ -168,7 +168,7 @@ function ExplorePageTitlebar({ onRefresh }: { onRefresh: () => void }) {
-
{t('Explore')}
+
{t('Explore')}
diff --git a/src/pages/primary/MePage/index.tsx b/src/pages/primary/MePage/index.tsx index d57ed55d..4d79c46a 100644 --- a/src/pages/primary/MePage/index.tsx +++ b/src/pages/primary/MePage/index.tsx @@ -134,7 +134,7 @@ function MePageTitlebar({ onRefresh }: { onRefresh: () => void }) { const { t } = useTranslation() return (
-
{t('YouTabName')}
+
{t('YouTabName')}
) diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 6a5c8459..b57e1293 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -171,7 +171,7 @@ const NoteListPage = forwardRef((_, ref) => { <> {isSmallScreen ? : null}
-

{feedPageTitle}

+

{feedPageTitle}

{showFavoriteRelaysPicker ? : null} {homeSubHeader} diff --git a/src/pages/primary/ProfilePage/index.tsx b/src/pages/primary/ProfilePage/index.tsx index e487e619..f285a4e7 100644 --- a/src/pages/primary/ProfilePage/index.tsx +++ b/src/pages/primary/ProfilePage/index.tsx @@ -52,7 +52,7 @@ function ProfilePageTitlebar({ onFeedRefresh }: { onFeedRefresh: () => void }) {
-
{t('Profile')}
+
{t('Profile')}
diff --git a/src/pages/primary/RelayPage/index.tsx b/src/pages/primary/RelayPage/index.tsx index f71833df..998ad49e 100644 --- a/src/pages/primary/RelayPage/index.tsx +++ b/src/pages/primary/RelayPage/index.tsx @@ -48,7 +48,7 @@ function RelayPageTitlebar({ url, onRefresh }: { url?: string; onRefresh: () =>
-
{simplifyUrl(url ?? '')}
+
{simplifyUrl(url ?? '')}
diff --git a/src/pages/primary/RssPage/index.tsx b/src/pages/primary/RssPage/index.tsx index db670932..64d9a353 100644 --- a/src/pages/primary/RssPage/index.tsx +++ b/src/pages/primary/RssPage/index.tsx @@ -63,7 +63,7 @@ const RssPage = forwardRef((_, ref) => {
-
{t('RSS + Web')}
+
{t('RSS + Web')}