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({ @@ -937,9 +937,7 @@ function MainContentArea({
onClick={goBack}
>
<ChevronLeft />
<div className="truncate text-lg font-semibold">
Back
</div>
<div className="app-chrome-title truncate">Back</div>
</Button>
</div>
<div className="flex-1 flex justify-center">
@ -2051,7 +2049,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2051,7 +2049,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
onClick={goBack}
>
<ChevronLeft />
<div className="truncate font-display text-lg font-semibold">
<div className="app-chrome-title truncate">
{primaryViewType === 'settings' || primaryViewType === 'settings-sub'
? 'Settings'
: primaryViewType === 'profile'
@ -2245,7 +2243,11 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -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">
{secondaryStack.map((item, index) => {
const isLast = index === secondaryStack.length - 1

12
src/components/EmojiPicker/index.tsx

@ -10,10 +10,18 @@ import EmojiPickerReact, { @@ -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({ @@ -43,6 +51,8 @@ export default function EmojiPicker({
onEmojiClick(emoji, e)
}}
customEmojis={customEmojiService.getAllCustomEmojisForPicker()}
{...(reactionsDefaultOpen !== undefined ? { reactionsDefaultOpen } : {})}
{...(reactions !== undefined ? { reactions } : {})}
/>
)
}

9
src/components/EmojiPickerDialog/index.tsx

@ -26,7 +26,14 @@ export default function EmojiPickerDialog({ @@ -26,7 +26,14 @@ export default function EmojiPickerDialog({
return (
<Drawer open={open} onOpenChange={setOpen}>
<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">
<DrawerTitle>Emoji Picker</DrawerTitle>
</DrawerHeader>

6
src/components/NoteDrawer/index.tsx

@ -57,7 +57,11 @@ export default function NoteDrawer({ open, onOpenChange, noteId, initialEvent }: @@ -57,7 +57,11 @@ export default function NoteDrawer({ open, onOpenChange, noteId, initialEvent }:
}}
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">
<NotePage
id={displayNoteId}

53
src/components/NoteOptions/EditOrCloneEventDialog.tsx

@ -17,7 +17,9 @@ import ContentPreview from '@/components/ContentPreview' @@ -17,7 +17,9 @@ import ContentPreview from '@/components/ContentPreview'
import Highlight from '@/components/Note/Highlight'
import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle'
import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle'
import ClientTag from '@/components/ClientTag'
import { ExtendedKind } from '@/constants'
import { applyImwaldAttributionTags, stripImwaldAttributionTags } from '@/lib/draft-event'
import { createFakeEvent } from '@/lib/event'
import logger from '@/lib/logger'
import {
@ -27,6 +29,8 @@ import { @@ -27,6 +29,8 @@ import {
} from '@/lib/publishing-feedback'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import type { TDraftEvent } from '@/types'
import dayjs from 'dayjs'
import { Plus, Trash2 } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
@ -123,24 +127,42 @@ export default function EditOrCloneEventDialog({ @@ -123,24 +127,42 @@ export default function EditOrCloneEventDialog({
const previewEvent = useMemo(() => {
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({ @@ -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({ @@ -342,7 +366,14 @@ export default function EditOrCloneEventDialog({
<TabsContent value="preview" className="flex-1 min-h-0 mt-0 data-[state=inactive]:hidden">
<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>
</TabsContent>

76
src/components/NoteStats/LikeButton.tsx

@ -1,4 +1,4 @@ @@ -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' @@ -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' @@ -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; @@ -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; @@ -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; @@ -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; @@ -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) {
return (
<>
{trigger}
<Drawer open={isEmojiReactionsOpen} onOpenChange={setIsEmojiReactionsOpen}>
<DrawerOverlay onClick={() => setIsEmojiReactionsOpen(false)} />
<DrawerContent hideOverlay>
<DrawerContent
onPointerDownOutside={(e) => {
const t = e.target as HTMLElement | null
if (t?.closest?.('[data-vaul-overlay]')) return
e.preventDefault()
}}
>
<DrawerHeader className="sr-only">
<DrawerTitle>React</DrawerTitle>
</DrawerHeader>
<EmojiPicker
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
if (!emoji) return
like(emoji)
}}
/>
{likeEmojiPicker}
</DrawerContent>
</Drawer>
</>
@ -283,38 +295,10 @@ export default function LikeButton({ event, hideCount = false }: { event: Event; @@ -283,38 +295,10 @@ export default function LikeButton({ event, hideCount = false }: { event: Event;
}
return (
<DropdownMenu
open={isEmojiReactionsOpen}
onOpenChange={(open) => {
setIsEmojiReactionsOpen(open)
if (open) {
setIsPickerOpen(false)
}
}}
>
<DropdownMenu open={isEmojiReactionsOpen} onOpenChange={setIsEmojiReactionsOpen}>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="p-0 w-fit">
{isPickerOpen ? (
<EmojiPicker
onEmojiClick={(emoji, e) => {
e.stopPropagation()
setIsEmojiReactionsOpen(false)
if (!emoji) return
like(emoji)
}}
/>
) : (
<SuggestedEmojis
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
like(emoji)
}}
onMoreButtonClick={() => {
setIsPickerOpen(true)
}}
/>
)}
{likeEmojiPicker}
</DropdownMenuContent>
</DropdownMenu>
)

3
src/components/NoteStats/Likes.tsx

@ -8,6 +8,7 @@ import { cn } from '@/lib/utils' @@ -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 }) { @@ -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
})

3
src/components/NoteStats/RepostButton.tsx

@ -16,6 +16,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -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 @@ -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) {

11
src/components/NoteStats/VoteButtons.tsx

@ -9,6 +9,7 @@ import { createReactionDraftEvent } from '@/lib/draft-event' @@ -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 }) { @@ -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 }) { @@ -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) {

8
src/components/PostEditor/PostContent.tsx

@ -30,7 +30,8 @@ import { @@ -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({ @@ -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({ @@ -2662,6 +2663,7 @@ export default function PostContent({
extraPreviewTags={
isDiscussionThread && !parentEvent ? discussionPreviewExtraTags : rssReplyExtraPreviewTags
}
addClientTag={addClientTag}
mediaImetaTags={mediaImetaTags}
mediaUrl={mediaUrl}
headerActions={

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

@ -1,6 +1,12 @@ @@ -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' @@ -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({ @@ -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({ @@ -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({ @@ -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({ @@ -169,11 +182,23 @@ export default function Preview({
kind
})
}, [processedContent, allTags, kind, mediaUrl])
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
if (kind === ExtendedKind.POLL) {
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<ContentPreview event={fakeEvent} />
</Card>
@ -182,7 +207,7 @@ export default function Preview({ @@ -182,7 +207,7 @@ export default function Preview({
// For highlights, use the Highlight component for proper formatting
if (kind === kinds.Highlights) {
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<Highlight event={fakeEvent} />
</Card>
@ -192,7 +217,7 @@ export default function Preview({ @@ -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(
<Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card>
@ -200,7 +225,7 @@ export default function Preview({ @@ -200,7 +225,7 @@ export default function Preview({
}
if (kind === ExtendedKind.DISCUSSION) {
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card>
@ -209,7 +234,7 @@ export default function Preview({ @@ -209,7 +234,7 @@ export default function Preview({
// For LongFormArticle, use MarkdownArticle
if (kind === kinds.LongFormArticle) {
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card>
@ -218,7 +243,7 @@ export default function Preview({ @@ -218,7 +243,7 @@ export default function Preview({
// For WikiArticle (AsciiDoc), use AsciidocArticle
if (kind === ExtendedKind.WIKI_ARTICLE) {
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} />
</Card>
@ -227,7 +252,7 @@ export default function Preview({ @@ -227,7 +252,7 @@ export default function Preview({
// For WikiArticleMarkdown, use MarkdownArticle
if (kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) {
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<MarkdownArticle event={fakeEvent} hideMetadata={true} />
</Card>
@ -236,14 +261,14 @@ export default function Preview({ @@ -236,14 +261,14 @@ export default function Preview({
// For PublicationContent, use AsciidocArticle
if (kind === ExtendedKind.PUBLICATION_CONTENT) {
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} />
</Card>
)
}
return (
return withClientBadge(
<Card className={cn('p-3', className, selectableClass)}>
<Content event={fakeEvent} className="h-full" mustLoadMedia />
</Card>

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

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

2
src/components/Settings/SettingsMenuBody.tsx

@ -156,7 +156,7 @@ export default function SettingsMenuBody({ className }: { className?: string }) @@ -156,7 +156,7 @@ export default function SettingsMenuBody({ className }: { className?: string })
</SettingItem>
</AboutInfoDialog>
<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>
</div>

3
src/components/SuggestedEmojis/index.tsx

@ -1,4 +1,5 @@ @@ -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' @@ -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

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

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

2
src/components/ui/dialog.tsx

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

2
src/components/ui/drawer.tsx

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

2
src/components/ui/sheet.tsx

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

8
src/index.css

@ -199,6 +199,14 @@ @@ -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;

4
src/layouts/SecondaryPageLayout/index.tsx

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

58
src/lib/draft-event.ts

@ -1405,8 +1405,62 @@ export function buildClientTag(handlerPubkey?: string, handlerIdentifier?: strin @@ -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() {

8
src/lib/event.ts

@ -529,11 +529,13 @@ export function getNoteBech32Id(event: Event) { @@ -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) {

19
src/lib/like-reaction-emojis.ts

@ -0,0 +1,19 @@ @@ -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 }) { @@ -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 shrink-0 items-center gap-2">
<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 className="flex shrink-0 items-center gap-1">
<RefreshButton onClick={onRefresh} />

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

@ -134,7 +134,7 @@ function MePageTitlebar({ onRefresh }: { onRefresh: () => void }) { @@ -134,7 +134,7 @@ function MePageTitlebar({ onRefresh }: { onRefresh: () => void }) {
const { t } = useTranslation()
return (
<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} />
</div>
)

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

@ -171,7 +171,7 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => { @@ -171,7 +171,7 @@ const NoteListPage = forwardRef<TPageRef>((_, ref) => {
<>
{isSmallScreen ? <FavoriteRelaysActiveStripMobileBar /> : null}
<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>
{showFavoriteRelaysPicker ? <FavoriteRelaysFeedPicker /> : null}
{homeSubHeader}

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

@ -52,7 +52,7 @@ function ProfilePageTitlebar({ onFeedRefresh }: { onFeedRefresh: () => void }) { @@ -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 min-w-0 items-center gap-2">
<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 className="flex shrink-0 items-center gap-1">
<RefreshButton onClick={onFeedRefresh} />

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

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

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

@ -63,7 +63,7 @@ const RssPage = forwardRef<TPageRef>((_, ref) => { @@ -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 items-center gap-2 pl-3">
<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 className="flex items-center gap-1">
<Button

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

@ -30,7 +30,7 @@ const SettingsPrimaryPage = forwardRef<TPageRef>((_, ref) => { @@ -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 items-center gap-2">
<Settings className="size-5 shrink-0" />
<div className="text-lg font-semibold">{t('Settings')}</div>
<div className="app-chrome-title">{t('Settings')}</div>
</div>
<RefreshButton onClick={bumpMenu} />
</div>

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

@ -1624,7 +1624,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage( @@ -1624,7 +1624,7 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
titlebar={
<div className="flex h-full w-full items-center justify-between gap-2 pr-1">
<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}
>
{spellsTitlebarTitle}

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

@ -320,7 +320,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -320,7 +320,7 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
<>
<div className="px-4 py-2 border-b">
<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">
<RefreshButton onClick={bumpFeed} />
{titlebarExtras}

34
src/providers/NostrProvider/index.tsx

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

Loading…
Cancel
Save