Browse Source

fix font headings

add client tags
update opengraph
imwald
Silberengel 4 weeks ago
parent
commit
cbcd888dcf
  1. 12
      src/PageManager.tsx
  2. 12
      src/components/EmojiPicker/index.tsx
  3. 9
      src/components/EmojiPickerDialog/index.tsx
  4. 6
      src/components/NoteDrawer/index.tsx
  5. 53
      src/components/NoteOptions/EditOrCloneEventDialog.tsx
  6. 76
      src/components/NoteStats/LikeButton.tsx
  7. 3
      src/components/NoteStats/Likes.tsx
  8. 3
      src/components/NoteStats/RepostButton.tsx
  9. 11
      src/components/NoteStats/VoteButtons.tsx
  10. 8
      src/components/PostEditor/PostContent.tsx
  11. 55
      src/components/PostEditor/PostTextarea/Preview.tsx
  12. 5
      src/components/PostEditor/PostTextarea/index.tsx
  13. 2
      src/components/Settings/SettingsMenuBody.tsx
  14. 3
      src/components/SuggestedEmojis/index.tsx
  15. 2
      src/components/ui/alert-dialog.tsx
  16. 2
      src/components/ui/dialog.tsx
  17. 2
      src/components/ui/drawer.tsx
  18. 2
      src/components/ui/sheet.tsx
  19. 8
      src/index.css
  20. 4
      src/layouts/SecondaryPageLayout/index.tsx
  21. 58
      src/lib/draft-event.ts
  22. 8
      src/lib/event.ts
  23. 19
      src/lib/like-reaction-emojis.ts
  24. 2
      src/pages/primary/ExplorePage/index.tsx
  25. 2
      src/pages/primary/MePage/index.tsx
  26. 2
      src/pages/primary/NoteListPage/index.tsx
  27. 2
      src/pages/primary/ProfilePage/index.tsx
  28. 2
      src/pages/primary/RelayPage/index.tsx
  29. 2
      src/pages/primary/RssPage/index.tsx
  30. 2
      src/pages/primary/SettingsPrimaryPage/index.tsx
  31. 2
      src/pages/primary/SpellsPage/index.tsx
  32. 2
      src/pages/secondary/NoteListPage/index.tsx
  33. 34
      src/providers/NostrProvider/index.tsx

12
src/PageManager.tsx

@ -937,9 +937,7 @@ function MainContentArea({
onClick={goBack} onClick={goBack}
> >
<ChevronLeft /> <ChevronLeft />
<div className="truncate text-lg font-semibold"> <div className="app-chrome-title truncate">Back</div>
Back
</div>
</Button> </Button>
</div> </div>
<div className="flex-1 flex justify-center"> <div className="flex-1 flex justify-center">
@ -2051,7 +2049,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
onClick={goBack} onClick={goBack}
> >
<ChevronLeft /> <ChevronLeft />
<div className="truncate font-display text-lg font-semibold"> <div className="app-chrome-title truncate">
{primaryViewType === 'settings' || primaryViewType === 'settings-sub' {primaryViewType === 'settings' || primaryViewType === 'settings-sub'
? 'Settings' ? 'Settings'
: primaryViewType === 'profile' : primaryViewType === 'profile'
@ -2245,7 +2243,11 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
} }
}} }}
> >
<SheetContent side="right" className="w-full sm:max-w-[1042px] overflow-y-auto p-0"> <SheetContent
side="right"
className="w-full sm:max-w-[1042px] overflow-y-auto p-0"
hideClose
>
<div className="h-full"> <div className="h-full">
{secondaryStack.map((item, index) => { {secondaryStack.map((item, index) => {
const isLast = index === secondaryStack.length - 1 const isLast = index === secondaryStack.length - 1

12
src/components/EmojiPicker/index.tsx

@ -10,10 +10,18 @@ import EmojiPickerReact, {
Theme Theme
} from 'emoji-picker-react' } from 'emoji-picker-react'
export { EMOJI_PICKER_REACTIONS } from '@/lib/like-reaction-emojis'
export default function EmojiPicker({ export default function EmojiPicker({
onEmojiClick onEmojiClick,
reactionsDefaultOpen,
reactions
}: { }: {
onEmojiClick: (emoji: string | TEmoji | undefined, event: MouseEvent) => void 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 { themeSetting } = useTheme()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
@ -43,6 +51,8 @@ export default function EmojiPicker({
onEmojiClick(emoji, e) onEmojiClick(emoji, e)
}} }}
customEmojis={customEmojiService.getAllCustomEmojisForPicker()} customEmojis={customEmojiService.getAllCustomEmojisForPicker()}
{...(reactionsDefaultOpen !== undefined ? { reactionsDefaultOpen } : {})}
{...(reactions !== undefined ? { reactions } : {})}
/> />
) )
} }

9
src/components/EmojiPickerDialog/index.tsx

@ -26,7 +26,14 @@ export default function EmojiPickerDialog({
return ( return (
<Drawer open={open} onOpenChange={setOpen}> <Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>{children}</DrawerTrigger> <DrawerTrigger asChild>{children}</DrawerTrigger>
<DrawerContent portalContainer={portalContainer}> <DrawerContent
portalContainer={portalContainer}
onPointerDownOutside={(e) => {
const t = e.target as HTMLElement | null
if (t?.closest?.('[data-vaul-overlay]')) return
e.preventDefault()
}}
>
<DrawerHeader className="sr-only"> <DrawerHeader className="sr-only">
<DrawerTitle>Emoji Picker</DrawerTitle> <DrawerTitle>Emoji Picker</DrawerTitle>
</DrawerHeader> </DrawerHeader>

6
src/components/NoteDrawer/index.tsx

@ -57,7 +57,11 @@ export default function NoteDrawer({ open, onOpenChange, noteId, initialEvent }:
}} }}
registerWithModalManager={false} registerWithModalManager={false}
> >
<SheetContent side="right" className="w-full sm:max-w-[1042px] overflow-y-auto p-0"> <SheetContent
side="right"
className="w-full sm:max-w-[1042px] overflow-y-auto p-0"
hideClose
>
<div className="min-h-full"> <div className="min-h-full">
<NotePage <NotePage
id={displayNoteId} id={displayNoteId}

53
src/components/NoteOptions/EditOrCloneEventDialog.tsx

@ -17,7 +17,9 @@ import ContentPreview from '@/components/ContentPreview'
import Highlight from '@/components/Note/Highlight' import Highlight from '@/components/Note/Highlight'
import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle' import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle' import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle'
import ClientTag from '@/components/ClientTag'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { applyImwaldAttributionTags, stripImwaldAttributionTags } from '@/lib/draft-event'
import { createFakeEvent } from '@/lib/event' import { createFakeEvent } from '@/lib/event'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { import {
@ -27,6 +29,8 @@ import {
} from '@/lib/publishing-feedback' } from '@/lib/publishing-feedback'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import type { TDraftEvent } from '@/types'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Plus, Trash2 } from 'lucide-react' import { Plus, Trash2 } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
@ -123,24 +127,42 @@ export default function EditOrCloneEventDialog({
const previewEvent = useMemo(() => { const previewEvent = useMemo(() => {
const now = Math.floor(Date.now() / 1000) const now = Math.floor(Date.now() / 1000)
return createFakeEvent({ const base: TDraftEvent = {
kind, kind,
content, content,
tags: normalizedTags, tags: normalizedTags,
created_at: now
}
const withAttribution = applyImwaldAttributionTags(base, {
addClientTag: storage.getAddClientTag()
})
return createFakeEvent({
kind,
content,
tags: withAttribution.tags,
pubkey: pubkey ?? '', pubkey: pubkey ?? '',
created_at: now created_at: now
}) })
}, [kind, content, normalizedTags, pubkey]) }, [kind, content, normalizedTags, pubkey])
const buildDraftJson = useCallback(() => { const buildDraftJson = useCallback(() => {
const draft = { const base: TDraftEvent = {
pubkey: pubkey ?? t('Log in to publish'), kind,
kind, content,
content, tags: normalizedTags,
tags: normalizedTags, created_at: dayjs().unix()
created_at: t('Set when you publish'), }
_note: t('id and sig are assigned when you publish') 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) return JSON.stringify(draft, null, 2)
}, [pubkey, kind, content, normalizedTags, t]) }, [pubkey, kind, content, normalizedTags, t])
@ -189,7 +211,9 @@ export default function EditOrCloneEventDialog({
tags: normalizedTags, tags: normalizedTags,
created_at: dayjs().unix() created_at: dayjs().unix()
} }
const newEvent = await publish(draft) const newEvent = await publish(draft, {
addClientTag: storage.getAddClientTag()
})
if ((newEvent as any)?.relayStatuses) { if ((newEvent as any)?.relayStatuses) {
const rs = (newEvent as any).relayStatuses const rs = (newEvent as any).relayStatuses
showPublishingFeedback( showPublishingFeedback(
@ -342,7 +366,14 @@ export default function EditOrCloneEventDialog({
<TabsContent value="preview" className="flex-1 min-h-0 mt-0 data-[state=inactive]:hidden"> <TabsContent value="preview" className="flex-1 min-h-0 mt-0 data-[state=inactive]:hidden">
<ScrollArea className="h-[min(50vh,420px)] pr-3"> <ScrollArea className="h-[min(50vh,420px)] pr-3">
<StaticEventPreview event={previewEvent} /> <div className="space-y-1.5">
{storage.getAddClientTag() ? (
<div className="flex min-h-[1.125rem] items-center px-0.5">
<ClientTag event={previewEvent} />
</div>
) : null}
<StaticEventPreview event={previewEvent} />
</div>
</ScrollArea> </ScrollArea>
</TabsContent> </TabsContent>

76
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 { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -25,6 +25,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/contexts/user-trust-context' import { useUserTrust } from '@/contexts/user-trust-context'
import { eventService } from '@/services/client.service' import { eventService } from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
import { SmilePlus } from 'lucide-react' import { SmilePlus } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
@ -32,8 +33,7 @@ import { useMemo, useState } from 'react'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Emoji from '../Emoji' import Emoji from '../Emoji'
import EmojiPicker from '../EmojiPicker' import EmojiPicker, { EMOJI_PICKER_REACTIONS } from '../EmojiPicker'
import SuggestedEmojis from '../SuggestedEmojis'
import { formatCount } from './utils' import { formatCount } from './utils'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import { WEB_EXTERNAL_REACTION_PUBLISHED_EVENT } from '@/lib/rss-web-feed' 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 { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const [liking, setLiking] = useState(false) const [liking, setLiking] = useState(false)
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false) const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
const [isPickerOpen, setIsPickerOpen] = useState(false)
const noteStats = useNoteStatsById(event.id) const noteStats = useNoteStatsById(event.id)
const isDiscussion = event.kind === ExtendedKind.DISCUSSION const isDiscussion = event.kind === ExtendedKind.DISCUSSION
const inQuietMode = shouldHideInteractions(event) const inQuietMode = shouldHideInteractions(event)
@ -124,7 +123,9 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
if (reactionEvent) { if (reactionEvent) {
// Create and publish a deletion request (kind 5) // Create and publish a deletion request (kind 5)
const deletionRequest = createDeletionRequestDraftEvent(reactionEvent) const deletionRequest = createDeletionRequestDraftEvent(reactionEvent)
const deletedEvent = await publish(deletionRequest) const deletedEvent = await publish(deletionRequest, {
addClientTag: storage.getAddClientTag()
})
// Show publishing feedback // Show publishing feedback
if ((deletedEvent as any)?.relayStatuses) { if ((deletedEvent as any)?.relayStatuses) {
@ -151,7 +152,7 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
} else { } else {
// User is adding a new reaction // User is adding a new reaction
const reaction = createReactionDraftEvent(event, emoji) const reaction = createReactionDraftEvent(event, emoji)
const evt = await publish(reaction) const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() })
// Show publishing feedback // Show publishing feedback
if ((evt as any)?.relayStatuses) { if ((evt as any)?.relayStatuses) {
@ -258,24 +259,35 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
) )
} }
const likeEmojiPicker = (
<EmojiPicker
reactionsDefaultOpen
reactions={[...EMOJI_PICKER_REACTIONS]}
onEmojiClick={(emoji, e) => {
e.stopPropagation()
setIsEmojiReactionsOpen(false)
if (!emoji) return
like(emoji)
}}
/>
)
if (isSmallScreen) { if (isSmallScreen) {
return ( return (
<> <>
{trigger} {trigger}
<Drawer open={isEmojiReactionsOpen} onOpenChange={setIsEmojiReactionsOpen}> <Drawer open={isEmojiReactionsOpen} onOpenChange={setIsEmojiReactionsOpen}>
<DrawerOverlay onClick={() => setIsEmojiReactionsOpen(false)} /> <DrawerContent
<DrawerContent hideOverlay> onPointerDownOutside={(e) => {
const t = e.target as HTMLElement | null
if (t?.closest?.('[data-vaul-overlay]')) return
e.preventDefault()
}}
>
<DrawerHeader className="sr-only"> <DrawerHeader className="sr-only">
<DrawerTitle>React</DrawerTitle> <DrawerTitle>React</DrawerTitle>
</DrawerHeader> </DrawerHeader>
<EmojiPicker {likeEmojiPicker}
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
if (!emoji) return
like(emoji)
}}
/>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
</> </>
@ -283,38 +295,10 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
} }
return ( return (
<DropdownMenu <DropdownMenu open={isEmojiReactionsOpen} onOpenChange={setIsEmojiReactionsOpen}>
open={isEmojiReactionsOpen}
onOpenChange={(open) => {
setIsEmojiReactionsOpen(open)
if (open) {
setIsPickerOpen(false)
}
}}
>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger> <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="p-0 w-fit"> <DropdownMenuContent side="top" className="p-0 w-fit">
{isPickerOpen ? ( {likeEmojiPicker}
<EmojiPicker
onEmojiClick={(emoji, e) => {
e.stopPropagation()
setIsEmojiReactionsOpen(false)
if (!emoji) return
like(emoji)
}}
/>
) : (
<SuggestedEmojis
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
like(emoji)
}}
onMoreButtonClick={() => {
setIsPickerOpen(true)
}}
/>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) )

3
src/components/NoteStats/Likes.tsx

@ -8,6 +8,7 @@ import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/contexts/user-trust-context' import { useUserTrust } from '@/contexts/user-trust-context'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useRef, useState } from 'react' import { useMemo, useRef, useState } from 'react'
@ -66,7 +67,7 @@ export default function Likes({ event }: { event: Event }) {
try { try {
const reaction = createReactionDraftEvent(event, emoji) const reaction = createReactionDraftEvent(event, emoji)
const evt = await publish(reaction) const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() })
noteStatsService.updateNoteStatsByEvents([evt], undefined, { noteStatsService.updateNoteStatsByEvents([evt], undefined, {
interactionTargetNoteId: event.id interactionTargetNoteId: event.id
}) })

3
src/components/NoteStats/RepostButton.tsx

@ -16,6 +16,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/contexts/user-trust-context' import { useUserTrust } from '@/contexts/user-trust-context'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { PencilLine, Repeat } from 'lucide-react' import { PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
@ -62,7 +63,7 @@ export default function RepostButton({ event, hideCount = false }: { event: Even
} }
const repost = createRepostDraftEvent(event) const repost = createRepostDraftEvent(event)
const evt = await publish(repost) const evt = await publish(repost, { addClientTag: storage.getAddClientTag() })
// Show publishing feedback // Show publishing feedback
if ((evt as any)?.relayStatuses) { if ((evt as any)?.relayStatuses) {

11
src/components/NoteStats/VoteButtons.tsx

@ -9,6 +9,7 @@ import { createReactionDraftEvent } from '@/lib/draft-event'
import { useNoteStatsRelayHints } from '@/hooks/useNoteStatsRelayHints' import { useNoteStatsRelayHints } from '@/hooks/useNoteStatsRelayHints'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { ChevronDown, ChevronUp } from 'lucide-react' import { ChevronDown, ChevronUp } from 'lucide-react'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
@ -72,8 +73,8 @@ export default function VoteButtons({ event }: { event: Event }) {
if (existingVote) { if (existingVote) {
// Remove vote by creating a reaction with the same emoji (this will toggle it off) // Remove vote by creating a reaction with the same emoji (this will toggle it off)
const reaction = createReactionDraftEvent(event, emoji) const reaction = createReactionDraftEvent(event, emoji)
const evt = await publish(reaction) const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() })
// Show publishing feedback // Show publishing feedback
if ((evt as any)?.relayStatuses) { if ((evt as any)?.relayStatuses) {
showPublishingFeedback({ showPublishingFeedback({
@ -97,12 +98,12 @@ export default function VoteButtons({ event }: { event: Event }) {
if (userVote) { if (userVote) {
const oldEmoji = userVote === 'up' ? DISCUSSION_UPVOTE : DISCUSSION_DOWNVOTE const oldEmoji = userVote === 'up' ? DISCUSSION_UPVOTE : DISCUSSION_DOWNVOTE
const removeReaction = createReactionDraftEvent(event, oldEmoji) const removeReaction = createReactionDraftEvent(event, oldEmoji)
await publish(removeReaction) await publish(removeReaction, { addClientTag: storage.getAddClientTag() })
} }
// Then add the new vote // Then add the new vote
const reaction = createReactionDraftEvent(event, emoji) const reaction = createReactionDraftEvent(event, emoji)
const evt = await publish(reaction) const evt = await publish(reaction, { addClientTag: storage.getAddClientTag() })
// Show publishing feedback // Show publishing feedback
if ((evt as any)?.relayStatuses) { if ((evt as any)?.relayStatuses) {

8
src/components/PostEditor/PostContent.tsx

@ -30,7 +30,8 @@ import {
createCitationInternalDraftEvent, createCitationInternalDraftEvent,
createCitationExternalDraftEvent, createCitationExternalDraftEvent,
createCitationHardcopyDraftEvent, createCitationHardcopyDraftEvent,
createCitationPromptDraftEvent createCitationPromptDraftEvent,
applyImwaldAttributionTags
} from '@/lib/draft-event' } from '@/lib/draft-event'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { cn, isTouchDevice } from '@/lib/utils' import { cn, isTouchDevice } from '@/lib/utils'
@ -1012,11 +1013,11 @@ export default function PostContent({
const cleanedText = rewritePlainTextHttpUrls(text) const cleanedText = rewritePlainTextHttpUrls(text)
const draftEvent = await createDraftEvent(cleanedText) const draftEvent = await createDraftEvent(cleanedText)
return JSON.stringify(draftEvent, null, 2) return JSON.stringify(applyImwaldAttributionTags(draftEvent, { addClientTag }), null, 2)
} catch (error) { } catch (error) {
return JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2) 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) => { const post = async (e?: React.MouseEvent) => {
e?.stopPropagation() e?.stopPropagation()
@ -2662,6 +2663,7 @@ export default function PostContent({
extraPreviewTags={ extraPreviewTags={
isDiscussionThread && !parentEvent ? discussionPreviewExtraTags : rssReplyExtraPreviewTags isDiscussionThread && !parentEvent ? discussionPreviewExtraTags : rssReplyExtraPreviewTags
} }
addClientTag={addClientTag}
mediaImetaTags={mediaImetaTags} mediaImetaTags={mediaImetaTags}
mediaUrl={mediaUrl} mediaUrl={mediaUrl}
headerActions={ headerActions={

55
src/components/PostEditor/PostTextarea/Preview.tsx

@ -1,6 +1,12 @@
import ClientTag from '@/components/ClientTag'
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { ExtendedKind, POLL_TYPE } from '@/constants' 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 { normalizeTopic } from '@/lib/discussion-topics'
import { createFakeEvent } from '@/lib/event' import { createFakeEvent } from '@/lib/event'
import { randomString } from '@/lib/random' import { randomString } from '@/lib/random'
@ -9,7 +15,7 @@ import { cn } from '@/lib/utils'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { kinds, nip19 } from 'nostr-tools' import { kinds, nip19 } from 'nostr-tools'
import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content' import { replaceStandardEmojiShortcodesInContent } from '@/lib/emoji-content'
import { useMemo } from 'react' import { useMemo, type ReactNode } from 'react'
import ContentPreview from '../../ContentPreview' import ContentPreview from '../../ContentPreview'
import Content from '../../Content' import Content from '../../Content'
import Highlight from '../../Note/Highlight' import Highlight from '../../Note/Highlight'
@ -26,7 +32,8 @@ export default function Preview({
mediaImetaTags, mediaImetaTags,
mediaUrl, mediaUrl,
articleMetadata, articleMetadata,
extraPreviewTags extraPreviewTags,
addClientTag = true
}: { }: {
content: string content: string
className?: string className?: string
@ -44,6 +51,8 @@ export default function Preview({
} }
/** Merged into the fake event (e.g. kind 11 discussion title / topic tags). */ /** Merged into the fake event (e.g. kind 11 discussion title / topic tags). */
extraPreviewTags?: string[][] extraPreviewTags?: string[][]
/** When true (default), preview matches publish: Imwald `client` + attribution `alt` tags and badge. */
addClientTag?: boolean
}) { }) {
const { content: processedContent, emojiTags, highlightTags, pollTags } = useMemo( const { content: processedContent, emojiTags, highlightTags, pollTags } = useMemo(
() => { () => {
@ -153,8 +162,12 @@ export default function Preview({
if (extraPreviewTags?.length) { if (extraPreviewTags?.length) {
tags.push(...extraPreviewTags) tags.push(...extraPreviewTags)
} }
return tags const stripped = stripImwaldAttributionTags(tags)
}, [emojiTags, highlightTags, pollTags, mediaImetaTags, articleMetadata, kind, extraPreviewTags]) if (addClientTag) {
stripped.push(buildClientTag(), buildAltTag())
}
return stripped
}, [emojiTags, highlightTags, pollTags, mediaImetaTags, articleMetadata, kind, extraPreviewTags, addClientTag])
const fakeEvent = useMemo(() => { const fakeEvent = useMemo(() => {
// For voice comments, include the media URL in content if not already there // For voice comments, include the media URL in content if not already there
@ -169,11 +182,23 @@ export default function Preview({
kind kind
}) })
}, [processedContent, allTags, kind, mediaUrl]) }, [processedContent, allTags, kind, mediaUrl])
const selectableClass = 'select-text' const selectableClass = 'select-text'
const withClientBadge = (node: ReactNode) =>
addClientTag ? (
<div className="space-y-1.5">
<div className="flex min-h-[1.125rem] items-center px-0.5">
<ClientTag event={fakeEvent} />
</div>
{node}
</div>
) : (
node
)
// For polls, use ContentPreview to show poll properly // For polls, use ContentPreview to show poll properly
if (kind === ExtendedKind.POLL) { if (kind === ExtendedKind.POLL) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<ContentPreview event={fakeEvent} /> <ContentPreview event={fakeEvent} />
</Card> </Card>
@ -182,7 +207,7 @@ export default function Preview({
// For highlights, use the Highlight component for proper formatting // For highlights, use the Highlight component for proper formatting
if (kind === kinds.Highlights) { if (kind === kinds.Highlights) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<Highlight event={fakeEvent} /> <Highlight event={fakeEvent} />
</Card> </Card>
@ -192,7 +217,7 @@ export default function Preview({
// For kind 1 notes, use MarkdownArticle to match actual rendering // 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) // 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) { if (kind === kinds.ShortTextNote || kind === ExtendedKind.COMMENT || kind === ExtendedKind.VOICE_COMMENT) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} /> <MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card> </Card>
@ -200,7 +225,7 @@ export default function Preview({
} }
if (kind === ExtendedKind.DISCUSSION) { if (kind === ExtendedKind.DISCUSSION) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} /> <MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card> </Card>
@ -209,7 +234,7 @@ export default function Preview({
// For LongFormArticle, use MarkdownArticle // For LongFormArticle, use MarkdownArticle
if (kind === kinds.LongFormArticle) { if (kind === kinds.LongFormArticle) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} /> <MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card> </Card>
@ -218,7 +243,7 @@ export default function Preview({
// For WikiArticle (AsciiDoc), use AsciidocArticle // For WikiArticle (AsciiDoc), use AsciidocArticle
if (kind === ExtendedKind.WIKI_ARTICLE) { if (kind === ExtendedKind.WIKI_ARTICLE) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} /> <AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} />
</Card> </Card>
@ -227,7 +252,7 @@ export default function Preview({
// For WikiArticleMarkdown, use MarkdownArticle // For WikiArticleMarkdown, use MarkdownArticle
if (kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) { if (kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} /> <MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card> </Card>
@ -236,14 +261,14 @@ export default function Preview({
// For PublicationContent, use AsciidocArticle // For PublicationContent, use AsciidocArticle
if (kind === ExtendedKind.PUBLICATION_CONTENT) { if (kind === ExtendedKind.PUBLICATION_CONTENT) {
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} /> <AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} />
</Card> </Card>
) )
} }
return ( return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}> <Card className={cn('p-3', className, selectableClass)}>
<Content event={fakeEvent} className="h-full" mustLoadMedia /> <Content event={fakeEvent} className="h-full" mustLoadMedia />
</Card> </Card>

5
src/components/PostEditor/PostTextarea/index.tsx

@ -59,6 +59,7 @@ const PostTextarea = forwardRef<
topics?: string[] topics?: string[]
} }
extraPreviewTags?: string[][] extraPreviewTags?: string[][]
addClientTag?: boolean
} }
>( >(
( (
@ -80,7 +81,8 @@ const PostTextarea = forwardRef<
mediaImetaTags, mediaImetaTags,
mediaUrl, mediaUrl,
articleMetadata, articleMetadata,
extraPreviewTags extraPreviewTags,
addClientTag = true
}, },
ref ref
) => { ) => {
@ -271,6 +273,7 @@ const PostTextarea = forwardRef<
mediaUrl={mediaUrl} mediaUrl={mediaUrl}
articleMetadata={articleMetadata} articleMetadata={articleMetadata}
extraPreviewTags={extraPreviewTags} extraPreviewTags={extraPreviewTags}
addClientTag={addClientTag}
/> />
</div> </div>
</TabsContent> </TabsContent>

2
src/components/Settings/SettingsMenuBody.tsx

@ -156,7 +156,7 @@ export default function SettingsMenuBody({ className }: { className?: string })
</SettingItem> </SettingItem>
</AboutInfoDialog> </AboutInfoDialog>
<div className="py-6 text-center text-muted-foreground"> <div className="py-6 text-center text-muted-foreground">
<div className="text-lg font-semibold">Imwald</div> <div className="app-chrome-title">Imwald</div>
<div className="font-semibold text-green-600 dark:text-green-500">Im Wald</div> <div className="font-semibold text-green-600 dark:text-green-500">Im Wald</div>
</div> </div>
</div> </div>

3
src/components/SuggestedEmojis/index.tsx

@ -1,4 +1,5 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { DEFAULT_SUGGESTED_EMOJIS } from '@/lib/like-reaction-emojis'
import { parseEmojiPickerUnified } from '@/lib/utils' import { parseEmojiPickerUnified } from '@/lib/utils'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
import { getSuggested } from 'emoji-picker-react/src/dataUtils/suggested' import { getSuggested } from 'emoji-picker-react/src/dataUtils/suggested'
@ -6,8 +7,6 @@ import { MoreHorizontal } from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import Emoji from '../Emoji' import Emoji from '../Emoji'
const DEFAULT_SUGGESTED_EMOJIS = ['👍', '❤', '😂', '🥲', '👀', '🫡', '🫂']
export default function SuggestedEmojis({ export default function SuggestedEmojis({
onEmojiClick, onEmojiClick,
onMoreButtonClick onMoreButtonClick

2
src/components/ui/alert-dialog.tsx

@ -63,7 +63,7 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
ref={ref} ref={ref}
className={cn('text-lg font-semibold', className)} className={cn('app-chrome-title', className)}
{...props} {...props}
/> />
)) ))

2
src/components/ui/dialog.tsx

@ -119,7 +119,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Title <DialogPrimitive.Title
ref={ref} ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)} className={cn('app-chrome-title leading-none tracking-tight', className)}
{...props} {...props}
/> />
)) ))

2
src/components/ui/drawer.tsx

@ -111,7 +111,7 @@ const DrawerTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DrawerPrimitive.Title <DrawerPrimitive.Title
ref={ref} ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)} className={cn('app-chrome-title leading-none tracking-tight', className)}
{...props} {...props}
/> />
)) ))

2
src/components/ui/sheet.tsx

@ -146,7 +146,7 @@ const SheetTitle = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Title <SheetPrimitive.Title
ref={ref} ref={ref}
className={cn('text-lg font-semibold text-foreground', className)} className={cn('app-chrome-title text-foreground', className)}
{...props} {...props}
/> />
)) ))

8
src/index.css

@ -199,6 +199,14 @@
} }
@layer components { @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 { .imwald-sidebar {
position: relative; position: relative;
isolation: isolate; isolation: isolate;

4
src/layouts/SecondaryPageLayout/index.tsx

@ -177,7 +177,7 @@ export function SecondaryPageTitlebar({
<ReadOnlySessionIndicator variant="titlebar" /> <ReadOnlySessionIndicator variant="titlebar" />
<div className="flex min-w-0 flex-1 items-center justify-between gap-1"> <div className="flex min-w-0 flex-1 items-center justify-between gap-1">
{hideBackButton ? ( {hideBackButton ? (
<div className="flex gap-2 items-center pl-2 w-fit truncate font-display text-lg font-semibold"> <div className="app-chrome-title flex w-fit items-center gap-2 truncate pl-2">
{title} {title}
</div> </div>
) : ( ) : (
@ -204,7 +204,7 @@ function BackButton({ children }: { children?: React.ReactNode }) {
onClick={() => pop()} onClick={() => pop()}
> >
<ChevronLeft /> <ChevronLeft />
<div className="truncate font-display text-lg font-semibold">{children}</div> <div className="app-chrome-title truncate">{children}</div>
</Button> </Button>
) )
} }

58
src/lib/draft-event.ts

@ -1405,8 +1405,62 @@ export function buildClientTag(handlerPubkey?: string, handlerIdentifier?: strin
return ['client', 'imwald'] return ['client', 'imwald']
} }
export function buildAltTag() { /** Canonical `alt` we attach for Imwald / jumble.imwald.eu publishing attribution (NIP-31). */
return ['alt', 'This event was published by https://jumble.imwald.eu.'] 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() { function buildNsfwTag() {

8
src/lib/event.ts

@ -529,11 +529,13 @@ export function getNoteBech32Id(event: Event) {
export function getUsingClient(event: Event) { export function getUsingClient(event: Event) {
const clientTag = event.tags.find(tagNameEquals('client')) const clientTag = event.tags.find(tagNameEquals('client'))
if (!clientTag) return undefined if (!clientTag) return undefined
// NIP-89 client tag format: ["client", "Client Name", "31990:pubkey:identifier", "relay"] // NIP-89 client tag format: ["client", "Client Name", "31990:pubkey:identifier", "relay"]
// Simple format: ["client", "client_name"] // Simple format: ["client", "client_name"]
// For display purposes, we use the client name (second element) const name = clientTag[1]
return clientTag[1] if (!name) return undefined
if (name.toLowerCase() === 'imwald') return 'Imwald'
return name
} }
export function getImetaInfosFromEvent(event: Event) { export function getImetaInfosFromEvent(event: Event) {

19
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)
)

2
src/pages/primary/ExplorePage/index.tsx

@ -168,7 +168,7 @@ function ExplorePageTitlebar({ onRefresh }: { onRefresh: () => void }) {
<div className="flex h-full min-w-0 w-full items-center justify-between gap-2 px-2 py-1 sm:pl-3 sm:pr-2"> <div className="flex h-full min-w-0 w-full items-center justify-between gap-2 px-2 py-1 sm:pl-3 sm:pr-2">
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-2">
<Compass className="size-5 shrink-0" /> <Compass className="size-5 shrink-0" />
<div className="text-lg font-semibold">{t('Explore')}</div> <div className="app-chrome-title">{t('Explore')}</div>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
<RefreshButton onClick={onRefresh} /> <RefreshButton onClick={onRefresh} />

2
src/pages/primary/MePage/index.tsx

@ -134,7 +134,7 @@ function MePageTitlebar({ onRefresh }: { onRefresh: () => void }) {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex h-full w-full items-center justify-between gap-2 pl-3 pr-1"> <div className="flex h-full w-full items-center justify-between gap-2 pl-3 pr-1">
<div className="text-lg font-semibold">{t('YouTabName')}</div> <div className="app-chrome-title">{t('YouTabName')}</div>
<RefreshButton onClick={onRefresh} /> <RefreshButton onClick={onRefresh} />
</div> </div>
) )

2
src/pages/primary/NoteListPage/index.tsx

@ -171,7 +171,7 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
<> <>
{isSmallScreen ? <FavoriteRelaysActiveStripMobileBar /> : null} {isSmallScreen ? <FavoriteRelaysActiveStripMobileBar /> : null}
<div className="w-full min-w-0 border-b border-border/80 bg-background px-3 py-2 sm:px-4"> <div className="w-full min-w-0 border-b border-border/80 bg-background px-3 py-2 sm:px-4">
<h1 className="text-lg font-semibold leading-tight tracking-tight">{feedPageTitle}</h1> <h1 className="app-chrome-title leading-tight tracking-tight">{feedPageTitle}</h1>
</div> </div>
{showFavoriteRelaysPicker ? <FavoriteRelaysFeedPicker /> : null} {showFavoriteRelaysPicker ? <FavoriteRelaysFeedPicker /> : null}
{homeSubHeader} {homeSubHeader}

2
src/pages/primary/ProfilePage/index.tsx

@ -52,7 +52,7 @@ function ProfilePageTitlebar({ onFeedRefresh }: { onFeedRefresh: () => void }) {
<div className="flex h-full w-full items-center justify-between gap-2 pl-3 pr-1"> <div className="flex h-full w-full items-center justify-between gap-2 pl-3 pr-1">
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<UserRound className="size-5 shrink-0" /> <UserRound className="size-5 shrink-0" />
<div className="truncate text-lg font-semibold">{t('Profile')}</div> <div className="app-chrome-title truncate">{t('Profile')}</div>
</div> </div>
<div className="flex shrink-0 items-center gap-1"> <div className="flex shrink-0 items-center gap-1">
<RefreshButton onClick={onFeedRefresh} /> <RefreshButton onClick={onFeedRefresh} />

2
src/pages/primary/RelayPage/index.tsx

@ -48,7 +48,7 @@ function RelayPageTitlebar({ url, onRefresh }: { url?: string; onRefresh: () =>
<div className="flex w-full items-center justify-between gap-2 px-1 h-full"> <div className="flex w-full items-center justify-between gap-2 px-1 h-full">
<div className="flex min-w-0 flex-1 items-center gap-2 px-2"> <div className="flex min-w-0 flex-1 items-center gap-2 px-2">
<Server /> <Server />
<div className="text-lg font-semibold truncate">{simplifyUrl(url ?? '')}</div> <div className="app-chrome-title truncate">{simplifyUrl(url ?? '')}</div>
</div> </div>
<RefreshButton onClick={onRefresh} /> <RefreshButton onClick={onRefresh} />
</div> </div>

2
src/pages/primary/RssPage/index.tsx

@ -63,7 +63,7 @@ const RssPage = forwardRef<TPageRef>((_, ref) => {
<div className="flex h-full w-full items-center justify-between gap-2 pr-1"> <div className="flex h-full w-full items-center justify-between gap-2 pr-1">
<div className="flex items-center gap-2 pl-3"> <div className="flex items-center gap-2 pl-3">
<Rss className="size-5" /> <Rss className="size-5" />
<div className="text-lg font-semibold">{t('RSS + Web')}</div> <div className="app-chrome-title">{t('RSS + Web')}</div>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button

2
src/pages/primary/SettingsPrimaryPage/index.tsx

@ -30,7 +30,7 @@ const SettingsPrimaryPage = forwardRef<TPageRef>((_, ref) => {
<div className="flex h-full w-full items-center justify-between gap-2 pl-3 pr-1"> <div className="flex h-full w-full items-center justify-between gap-2 pl-3 pr-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Settings className="size-5 shrink-0" /> <Settings className="size-5 shrink-0" />
<div className="text-lg font-semibold">{t('Settings')}</div> <div className="app-chrome-title">{t('Settings')}</div>
</div> </div>
<RefreshButton onClick={bumpMenu} /> <RefreshButton onClick={bumpMenu} />
</div> </div>

2
src/pages/primary/SpellsPage/index.tsx

@ -1624,7 +1624,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
titlebar={ titlebar={
<div className="flex h-full w-full items-center justify-between gap-2 pr-1"> <div className="flex h-full w-full items-center justify-between gap-2 pr-1">
<div <div
className="min-w-0 flex-1 truncate pl-3 text-lg font-semibold" className="app-chrome-title min-w-0 flex-1 truncate pl-3"
title={spellsTitlebarTitle} title={spellsTitlebarTitle}
> >
{spellsTitlebarTitle} {spellsTitlebarTitle}

2
src/pages/secondary/NoteListPage/index.tsx

@ -320,7 +320,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
<> <>
<div className="px-4 py-2 border-b"> <div className="px-4 py-2 border-b">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="text-lg font-semibold">{title}</div> <div className="app-chrome-title">{title}</div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<RefreshButton onClick={bumpFeed} /> <RefreshButton onClick={bumpFeed} />
{titlebarExtras} {titlebarExtras}

34
src/providers/NostrProvider/index.tsx

@ -11,8 +11,7 @@ import {
SEARCHABLE_RELAY_URLS SEARCHABLE_RELAY_URLS
} from '@/constants' } from '@/constants'
import { import {
buildAltTag, applyImwaldAttributionTags,
buildClientTag,
createDeletionRequestDraftEvent, createDeletionRequestDraftEvent,
createFollowListDraftEvent, createFollowListDraftEvent,
createMuteListDraftEvent, createMuteListDraftEvent,
@ -1169,19 +1168,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
return null return null
} }
const normalizeDraftEventTags = (draftEvent: TDraftEvent): TDraftEvent => { const normalizeDraftEventTags = (
const draft = JSON.parse(JSON.stringify(draftEvent)) as TDraftEvent draftEvent: TDraftEvent,
const imwaldAttributionAlt = buildAltTag()[1] options?: { addClientTag?: boolean }
const existingTags = Array.isArray(draft.tags) ? draft.tags : [] ): TDraftEvent => applyImwaldAttributionTags(draftEvent, options)
const sanitizedTags = existingTags.filter(
(tag) =>
Array.isArray(tag) &&
tag[0] !== 'client' &&
!(tag[0] === 'alt' && tag[1] === imwaldAttributionAlt)
)
draft.tags = [...sanitizedTags, buildClientTag(), buildAltTag()]
return draft
}
const setupNewUser = async (signer: ISigner) => { const setupNewUser = async (signer: ISigner) => {
await Promise.allSettled([ await Promise.allSettled([
@ -1204,8 +1194,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
]) ])
} }
const signEvent = async (draftEvent: TDraftEvent) => { const signEvent = async (
const normalizedDraft = normalizeDraftEventTags(draftEvent) draftEvent: TDraftEvent,
normalizeOpts?: { addClientTag?: boolean }
) => {
const normalizedDraft = normalizeDraftEventTags(draftEvent, normalizeOpts)
// Add timeout to prevent hanging // Add timeout to prevent hanging
const signEventWithTimeout = new Promise((resolve, reject) => { const signEventWithTimeout = new Promise((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
@ -1262,13 +1255,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
throw new Error('Invalid account state - pubkey is missing or invalid') throw new Error('Invalid account state - pubkey is missing or invalid')
} }
const draft = normalizeDraftEventTags(draftEvent) const normalizeOpts = { addClientTag: options.addClientTag }
const draft = normalizeDraftEventTags(draftEvent, normalizeOpts)
let event: VerifiedEvent let event: VerifiedEvent
if (minPow > 0) { if (minPow > 0) {
const unsignedEvent = await minePow({ ...draft, pubkey: account.pubkey }, minPow) const unsignedEvent = await minePow({ ...draft, pubkey: account.pubkey }, minPow)
event = await signEvent(unsignedEvent) event = await signEvent(unsignedEvent, normalizeOpts)
} else { } else {
event = await signEvent(draft) event = await signEvent(draft, normalizeOpts)
} }
if (event.kind !== kinds.Application && event.pubkey !== account.pubkey) { if (event.kind !== kinds.Application && event.pubkey !== account.pubkey) {

Loading…
Cancel
Save