Compare commits

..

8 commits

Author SHA1 Message Date
sakrecoer
536c18adca better naming 2026-04-09 15:55:45 +02:00
sakrecoer
d616691395 customize icons 2026-04-09 15:44:23 +02:00
sakrecoer
c8476bf519 our environement file 2026-04-08 21:15:40 +02:00
ddcf7d52a8 Update README.md 2026-04-08 21:12:28 +02:00
sakrecoer
ad77866a50 branding and music implementation 2026-04-08 21:03:43 +02:00
sakrecoer
1ec3d95c35 nostr music: tracks 2026-04-08 21:03:09 +02:00
sakrecoer
6d00aa8e0a theme and branding 2026-04-08 21:01:08 +02:00
sakrecoer
7bfc3ed900 custom assets 2026-04-08 20:54:37 +02:00
30 changed files with 350 additions and 78 deletions

2
.env.basspistol Normal file
View file

@ -0,0 +1,2 @@
VITE_COMMUNITY_RELAYS="wss://basspistol.org/favorites,wss://basspistol.org/popular,wss://basspistol.org/uppermost,wss://basspistol.org/personal"
VITE_COMMUNITY_RELAY_SETS=[{"id": "basspistol", "name": "Basspistol", "relayUrls": ["wss://basspistol.org","wss://drops.basspistol.org"]},{"id": "member", "name": "Backstage", "relayUrls": ["wss://basspistol.org/internal"]},{"id": "hood", "name": "Hood", "relayUrls": ["wss://nestr.nedao.ch","wss://pyramid.fiatjaf.com","wss://spatia-arcana.com","wss://lightning.red","wss://inner.sebastix.social"]}]

View file

@ -2,15 +2,19 @@
<picture> <picture>
<img src="./resources/logo-light.svg" alt="Jumble Logo" width="400" /> <img src="./resources/logo-light.svg" alt="Jumble Logo" width="400" />
</picture> </picture>
<p>logo designed by <a href="http://wolfertdan.com/">Daniel David</a></p>
</div> </div>
# Jumble # Bpistle
A user-friendly Nostr client for exploring relay feeds A community fork of [Jumble](https://github.com/CodyTseng/jumble), the user-friendly Nostr client for exploring relay feeds, made by and for music lovers.
Experience Bpistle at [https://nostr.basspistol.org](https://nostr.basspistol.org)
Experience Jumble at [https://jumble.social](https://jumble.social) Experience Jumble at [https://jumble.social](https://jumble.social)
Upstream code: https://github.com/CodyTseng/jumble
## Forks ## Forks
> Some interesting forks of Jumble. > Some interesting forks of Jumble.

View file

@ -4,29 +4,29 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Jumble</title> <title>Bpistle</title>
<meta name="description" content="A user-friendly Nostr client for exploring relay feeds" /> <meta name="description" content="A user-friendly Nostr client for exploring the Basspistol Network" />
<meta <meta
name="keywords" name="keywords"
content="jumble, nostr, web, client, relay, feed, social, pwa, simple, clean" content="basspistol, music, syndicate, creative commons, sovereign, jumble, nostr, web, client, relay, feed, social, pwa, simple, clean"
/> />
<meta name="apple-mobile-web-app-title" content="Jumble" /> <meta name="apple-mobile-web-app-title" content="Bpistle" />
<link rel="icon" href="/favicon.ico" sizes="48x48" /> <link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />
<meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#200B2E" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)" />
<meta property="og:url" content="https://jumble.social" /> <meta property="og:url" content="https://nostr.basspistol.org" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Jumble" /> <meta property="og:title" content="Bpistle" />
<meta <meta
property="og:description" property="og:description"
content="A user-friendly Nostr client for exploring relay feeds" content="A user-friendly Nostr client for exploring relay feeds"
/> />
<meta <meta
property="og:image" property="og:image"
content="https://github.com/CodyTseng/jumble/blob/master/resources/og-image.png?raw=true" content="https://tortellino.basspistol.org/06052f39027abe9e7e5bc755211ce0ecfb838dcc0c39c7bd767db0354ed5a293.png"
/> />
</head> </head>
<body> <body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Before After
Before After

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

@ -17,7 +17,7 @@ export default function Icon({ className }: { className?: string }) {
> >
<path <path
id="Icon-Curve-Cut" id="Icon-Curve-Cut"
d="M360.047,1225.75c-31.046,-3.901 -75.11,-14.46 -106.756,-25.58c-101.676,-35.727 -175.164,-93.066 -215.387,-168.055c-12.079,-22.521 -30.071,-71.422 -27.297,-74.195c0.736,-0.736 11.648,5.578 24.249,14.031c135.436,90.86 301.047,169.043 465.056,219.547l32.77,10.091l-20.27,7.416c-43.455,15.896 -105.159,22.678 -152.365,16.745Zm166.293,-59.234c-168.523,-50.004 -331.475,-126.514 -481.755,-226.196c-37.737,-25.031 -41.489,-28.372 -43.419,-38.663c-3.585,-19.109 1.498,-83.894 9.798,-124.886c7.343,-36.266 27.664,-106.034 32.278,-110.818c2.023,-2.099 217.924,48.207 221.274,51.557c0.975,0.975 -1.132,11.339 -4.682,23.032c-24.542,80.842 -27.217,127.586 -9.935,173.593c22.507,59.917 114.521,99.888 177.281,77.012c29.23,-10.654 56.593,-41.085 82.629,-91.894c29.288,-57.155 32.348,-64.988 196.483,-503.076c81.138,-216.562 148.499,-394.821 149.692,-396.131c2.1,-2.304 217.949,76.926 223.076,81.884c2.056,1.988 -262.476,712.505 -307.806,826.747c-18.422,46.426 -56.939,123.045 -77.918,154.993c-10.157,15.469 -30.753,40.901 -45.769,56.515c-27.821,28.93 -66.46,58.952 -75.447,58.621c-2.738,-0.106 -23.339,-5.631 -45.78,-12.29Z" d="m989.52 727.65q0 110.66-102.99 199.41-102.99 88.749-215.85 88.749-49.305 0-95.323-18.626-44.922-18.626-73.409-40.54-28.487-21.913-60.262-40.54-30.679-18.626-53.688-18.626-21.913 0-40.54 13.148-17.531 13.148-18.626 35.061h-18.626q1.0953-62.453 56.975-107.38 56.975-44.922 121.62-46.018 43.827-47.114 43.827-175.31v-168.73q0-27.392-2.1917-46.018-1.0953-19.722-7.6693-47.114-5.4787-27.392-21.913-41.635-16.435-15.339-41.635-15.339-26.296 0-41.635 21.913-14.244 21.913-14.244 49.305 0 30.679 35.061 93.131 35.061 61.357 35.061 102.99 0 97.514-157.78 146.82l-7.6693-12.052q53.688-24.105 53.688-63.549 0-25.2-36.157-86.558-35.061-61.357-35.061-112.85 0-75.601 70.122-123.81 71.218-49.305 151.2-49.305 112.85 0 157.78 89.844l132.58-89.844q60.262 3.287 106.28 35.061 46.018 30.679 46.018 87.653 0 53.688-38.348 89.845-37.252 36.157-95.323 54.783 78.888 4.3822 123.81 60.262 44.922 54.783 44.922 135.86zm-167.64-273.92q0-39.444-21.913-69.027-21.913-30.679-59.166-30.679-30.679 0-64.644 31.774 7.6693 64.644 7.6693 153.39 0 23.009 0 54.783 56.974-17.531 97.514-52.592 40.54-36.157 40.54-87.653zm46.018 374.72q0-62.453-52.592-133.67-51.496-71.218-110.66-71.218-14.244 0-23.009 4.3823-4.3823 37.253-21.913 65.74-16.435 27.392-32.87 39.444-15.339 12.052-51.496 33.966-35.061 21.913-52.592 33.966 38.348 10.957 129.29 71.218 92.036 59.166 127.1 59.166 40.54 0 64.644-30.679 24.105-30.679 24.105-72.314z"
/> />
</svg> </svg>
) )

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

@ -0,0 +1,24 @@
import { useTranslation } from 'react-i18next'
import { Event } from 'nostr-tools'
export default function MusicTrackPreview({ event }: { event: Event }) {
const { t } = useTranslation()
const title = event.tags.find(tag => tag[0] === 'title')?.[1]
const artist = event.tags.find(tag => tag[0] === 'artist')?.[1]
return (
<div className="flex items-center gap-2 truncate">
<span>🎵</span>
<span className="font-medium truncate">
{title || t('music.untitled')}
</span>
{artist && (
<span className="text-muted-foreground truncate">
{t('music.by')} {artist}
</span>
)}
</div>
)
}

View file

@ -18,6 +18,7 @@ import PictureNotePreview from './PictureNotePreview'
import PollPreview from './PollPreview' import PollPreview from './PollPreview'
import ReactionPreview from './ReactionPreview' import ReactionPreview from './ReactionPreview'
import VideoNotePreview from './VideoNotePreview' import VideoNotePreview from './VideoNotePreview'
import MusicTrackPreview from './MusicTrackPreview'
export default function ContentPreview({ export default function ContentPreview({
event, event,
@ -120,6 +121,10 @@ export default function ContentPreview({
return <ReactionPreview event={event} className={className} /> return <ReactionPreview event={event} className={className} />
} }
if (event.kind === ExtendedKind.MUSIC_TRACK) {
return <MusicTrackPreview event={event} />
}
return ( return (
<div className={className}> <div className={className}>
[ [

View file

@ -20,6 +20,7 @@ const KIND_FILTER_OPTIONS = [
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' }, { kindGroup: [ExtendedKind.POLL], label: 'Polls' },
{ kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' }, { kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' },
{ kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' }, { kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' },
{ kindGroup: [ExtendedKind.MUSIC_TRACK], label: 'Music Posts' },
{ {
kindGroup: [ kindGroup: [
ExtendedKind.VIDEO, ExtendedKind.VIDEO,

View file

@ -0,0 +1,173 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import { Event } from 'nostr-tools'
import AudioPlayer from '@/components/AudioPlayer'
interface MusicTrackNoteProps {
event: Event
className?: string
}
export default function MusicTrackNote({ event, className }: MusicTrackNoteProps) {
const { t } = useTranslation()
const metadata = useMemo(() => {
const getTagValue = (tagName: string) => {
const tag = event.tags.find(tag => tag[0] === tagName)
return tag?.[1] || null
}
const getTagValues = (tagName: string) => {
return event.tags
.filter(tag => tag[0] === tagName)
.map(tag => tag[1])
.filter(Boolean)
}
let lyrics = null
let credits = null
if (event.content) {
const creditsMatch = event.content.match(/Credits:\s*\n([\s\S]*)/i)
if (creditsMatch) {
credits = creditsMatch[1].trim()
const lyricsMatch = event.content.match(/^([\s\S]*?)Credits:/i)
lyrics = lyricsMatch ? lyricsMatch[1].trim() : null
} else {
lyrics = event.content
}
}
return {
title: getTagValue('title') || t('music.untitled'),
url: getTagValue('url'),
image: getTagValue('image'),
license: getTagValue('license'),
alt: getTagValue('alt'),
releaseDate: getTagValue('released'),
artist: getTagValue('artist'),
album: getTagValue('album'),
trackNumber: getTagValue('track_number'),
duration: getTagValue('duration'),
genres: getTagValues('t').filter(tag =>
!['music', 'electronic', 'lofi pop'].includes(tag)
),
lyrics,
credits
}
}, [event, t])
if (!metadata.url) {
return (
<div className={`p-4 text-red-500 ${className || ''}`}>
{t('music.noAudioUrl')}
</div>
)
}
return (
<div className={`space-y-4 p-4 ${className || ''}`}>
{/* Main track container */}
<div className="flex gap-4 flex-col sm:flex-row">
{/* Cover Art */}
{metadata.image && (
<div className="flex-shrink-0">
<img
src={metadata.image}
alt={metadata.alt || metadata.title}
className="w-48 h-48 object-cover rounded-lg shadow-md"
loading="lazy"
/>
</div>
)}
{/* Track Info */}
<div className="flex-1 space-y-2">
<h3 className="text-xl font-bold">{metadata.title}</h3>
{metadata.artist && (
<p className="text-muted-foreground">
{t('music.by')} {metadata.artist}
</p>
)}
{metadata.album && (
<p className="text-sm">
{t('music.album')}: {metadata.album}
{metadata.trackNumber && `${t('music.track')} ${metadata.trackNumber}`}
</p>
)}
{metadata.releaseDate && (
<p className="text-sm text-muted-foreground">
{t('music.released')}: {dayjs(metadata.releaseDate).format('MMMM D, YYYY')}
</p>
)}
{/* Audio Player */}
<div className="mt-4">
<AudioPlayer
src={metadata.url}
/>
</div>
{/* Show lyrics if they exist */}
{metadata.lyrics && (
<div className="pt-2">
<p className="text-sm whitespace-pre-wrap text-muted-foreground">
{metadata.lyrics}
</p>
</div>
)}
{/* Show credits if they exist */}
{metadata.credits && (
<div className="pt-2">
<p className="text-sm italic text-muted-foreground whitespace-pre-wrap">
<span className="font-semibold not-italic">{t('music.credits')}:</span><br />
{metadata.credits}
</p>
</div>
)}
</div>
</div>
{/* License and Metadata Footer */}
<div className="pt-4 border-t space-y-1">
{metadata.license && (
<p className="text-xs text-muted-foreground">
{t('music.license')}: {metadata.license}
</p>
)}
{/* Only show alt if it's different from what we've shown
{metadata.alt && !metadata.credits && (
<p className="text-xs text-muted-foreground">
{t('music.altText')}: {metadata.alt}
</p>
)}
*/}
{metadata.genres.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{metadata.genres.map((genre, index) => (
<span
key={index}
className="px-2 py-1 text-xs rounded-full bg-secondary"
>
{genre}
</span>
))}
</div>
)}
</div>
</div>
)
}

View file

@ -38,6 +38,7 @@ import Reaction from './Reaction'
import RelayReview from './RelayReview' import RelayReview from './RelayReview'
import UnknownNote from './UnknownNote' import UnknownNote from './UnknownNote'
import VideoNote from './VideoNote' import VideoNote from './VideoNote'
import MusicTrackNote from './MusicTrackNote'
export default function Note({ export default function Note({
event, event,
@ -124,6 +125,8 @@ export default function Note({
event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO
) { ) {
content = <VideoNote className="mt-2" event={event} /> content = <VideoNote className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.MUSIC_TRACK) {
content = <MusicTrackNote className="mt-2" event={event} />
} else if (event.kind === ExtendedKind.RELAY_REVIEW) { } else if (event.kind === ExtendedKind.RELAY_REVIEW) {
content = <RelayReview className="mt-2" event={event} /> content = <RelayReview className="mt-2" event={event} />
} else if (event.kind === kinds.Emojisets) { } else if (event.kind === kinds.Emojisets) {

View file

@ -148,7 +148,7 @@ function RelayControls({ url }: { url: string }) {
} }
const handleCopyShareableUrl = () => { const handleCopyShareableUrl = () => {
navigator.clipboard.writeText(`https://jumble.social/?r=${url}`) navigator.clipboard.writeText(`https://nostr.basspistol.org/?r=${url}`)
setCopiedShareableUrl(true) setCopiedShareableUrl(true)
toast.success('Shareable URL copied to clipboard') toast.success('Shareable URL copied to clipboard')
setTimeout(() => setCopiedShareableUrl(false), 2000) setTimeout(() => setCopiedShareableUrl(false), 2000)

View file

@ -1,6 +1,6 @@
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { TRelaySet } from '@/types' import { TRelaySet } from '@/types'
import { ChevronDown, FolderClosed } from 'lucide-react' import { ChevronDown, FolderClosed, Music, Radio, Trees, DoorOpen } from 'lucide-react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon' import RelayIcon from '../RelayIcon'
@ -17,6 +17,21 @@ export default function RelaySetCard({
const { t } = useTranslation() const { t } = useTranslation()
const [expand, setExpand] = useState(false) const [expand, setExpand] = useState(false)
const getRelaySetIcon = (name: string) => {
const nameLower = name.toLowerCase()
if (nameLower.includes('feed')) return Radio
if (nameLower.includes('music')) return Music
if (nameLower.includes('backstage')) return DoorOpen
if (nameLower.includes('hood')) return Trees
return FolderClosed
}
const IconComponent = getRelaySetIcon(relaySet.name)
return ( return (
<div <div
className={cn( className={cn(
@ -30,7 +45,8 @@ export default function RelaySetCard({
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-3"> <div className="flex min-w-0 flex-1 items-center gap-3">
<div className="flex size-6 shrink-0 items-center justify-center"> <div className="flex size-6 shrink-0 items-center justify-center">
<FolderClosed className="size-5" /> {/* Use the dynamic icon component instead of hardcoded FolderClosed */}
<IconComponent className="size-5" />
</div> </div>
<div className="select-none truncate font-medium">{relaySet.name}</div> <div className="select-none truncate font-medium">{relaySet.name}</div>
</div> </div>

View file

@ -4,7 +4,7 @@ import { TRelaySet } from './types'
export const JUMBLE_API_BASE_URL = 'https://api.jumble.social' export const JUMBLE_API_BASE_URL = 'https://api.jumble.social'
export const RECOMMENDED_BLOSSOM_SERVERS = [ export const RECOMMENDED_BLOSSOM_SERVERS = [
'https://blossom.band/', 'https://basspistol.org',
'https://blossom.primal.net/', 'https://blossom.primal.net/',
'https://nostr.media/' 'https://nostr.media/'
] ]
@ -78,9 +78,9 @@ export const BIG_RELAY_URLS = [
'wss://offchain.pub/' 'wss://offchain.pub/'
] ]
export const SEARCHABLE_RELAY_URLS = ['wss://search.nos.today/', 'wss://relay.nostr.band/'] export const SEARCHABLE_RELAY_URLS = ['wss://basspistol.org/','wss://pyramid.fiatjaf.com/','wss://spatia-arcana.com/']
export const TRENDING_NOTES_RELAY_URLS = ['wss://trending.relays.land/'] export const TRENDING_NOTES_RELAY_URLS = ['wss://basspistol.org/uppermost']
export const GROUP_METADATA_EVENT_KIND = 39000 export const GROUP_METADATA_EVENT_KIND = 39000
@ -101,7 +101,8 @@ export const ExtendedKind = {
RELAY_REVIEW: 31987, RELAY_REVIEW: 31987,
GROUP_METADATA: 39000, GROUP_METADATA: 39000,
ADDRESSABLE_NORMAL_VIDEO: 34235, ADDRESSABLE_NORMAL_VIDEO: 34235,
ADDRESSABLE_SHORT_VIDEO: 34236 ADDRESSABLE_SHORT_VIDEO: 34236,
MUSIC_TRACK: 36787
} }
export const ALLOWED_FILTER_KINDS = [ export const ALLOWED_FILTER_KINDS = [
@ -118,7 +119,8 @@ export const ALLOWED_FILTER_KINDS = [
kinds.Highlights, kinds.Highlights,
kinds.LongFormArticle, kinds.LongFormArticle,
ExtendedKind.ADDRESSABLE_NORMAL_VIDEO, ExtendedKind.ADDRESSABLE_NORMAL_VIDEO,
ExtendedKind.ADDRESSABLE_SHORT_VIDEO ExtendedKind.ADDRESSABLE_SHORT_VIDEO,
ExtendedKind.MUSIC_TRACK
] ]
export const SUPPORTED_KINDS = [ export const SUPPORTED_KINDS = [
@ -202,16 +204,16 @@ export const PRIMARY_COLORS = {
DEFAULT: { DEFAULT: {
name: 'Default', name: 'Default',
light: { light: {
primary: '259 43% 56%', primary: '30 100% 50%',
'primary-hover': '259 43% 65%', 'primary-hover': '30 100% 60%',
'primary-foreground': '0 0% 98%', 'primary-foreground': '0 0% 98%',
ring: '259 43% 56%' ring: '30 100% 50%'
}, },
dark: { dark: {
primary: '259 43% 56%', primary: '30 100% 50%',
'primary-hover': '259 43% 65%', 'primary-hover': '30 100% 60%',
'primary-foreground': '240 5.9% 10%', 'primary-foreground': '240 5.9% 10%',
ring: '259 43% 56%' ring: '30 100% 50%'
} }
}, },
RED: { RED: {
@ -230,18 +232,18 @@ export const PRIMARY_COLORS = {
} }
}, },
ORANGE: { ORANGE: {
name: 'Orange', name: 'Lavender',
light: { light: {
primary: '30 100% 50%', primary: '259 43% 56%',
'primary-hover': '30 100% 60%', 'primary-hover': '259 43% 65%',
'primary-foreground': '0 0% 98%', 'primary-foreground': '0 0% 98%',
ring: '30 100% 50%' ring: '259 43% 56%'
}, },
dark: { dark: {
primary: '30 100% 50%', primary: '259 43% 56%',
'primary-hover': '30 100% 60%', 'primary-hover': '259 43% 65%',
'primary-foreground': '240 5.9% 10%', 'primary-foreground': '240 5.9% 10%',
ring: '30 100% 50%' ring: '259 43% 56%'
} }
}, },
AMBER: { AMBER: {

View file

@ -686,6 +686,18 @@ export default {
'Allow insecure connections description': 'Allow insecure connections description':
'Allow loading http:// resources and connecting to ws:// relays. May trigger browser mixed content warnings.', 'Allow loading http:// resources and connecting to ws:// relays. May trigger browser mixed content warnings.',
'reacted to': 'reacted to', 'reacted to': 'reacted to',
Reaction: 'Reaction' Reaction: 'Reaction',
music: {
untitled: 'Untitled Track',
by: 'by',
album: 'Album',
track: 'Track',
released: 'Released',
license: 'License',
altText: 'Credit',
credits: 'Credits', // Add this
showLyrics: 'Show Lyrics', // Add this (optional)
noAudioUrl: 'No audio URL provided for this track',
}
} }
} }

View file

@ -133,12 +133,12 @@
--radius: 0.75rem; --radius: 0.75rem;
} }
.dark { .dark {
--surface-background: 240 10% 3.9%; --surface-background: 276, 61.404%, 9%;
--background: 0 0% 9%; --background: 276, 61.404%, 11.176%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
--card: 0 0% 12%; --card: 276, 61.404%, 10%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 98%;
--popover: 0 0% 12%; --popover: 276, 61.404%, 10%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 98%;
--primary: 259 43% 56%; --primary: 259 43% 56%;
--primary-hover: 259 43% 65%; --primary-hover: 259 43% 65%;

View file

@ -9,7 +9,7 @@ export const toNote = (eventOrId: Event | string) => {
return `/notes/${nevent}` return `/notes/${nevent}`
} }
export const toJumbleNote = (eventOrId: Event | string) => { export const toJumbleNote = (eventOrId: Event | string) => {
return `https://jumble.social${toNote(eventOrId)}` return `https://nostr.basspistol.org${toNote(eventOrId)}`
} }
export const toNoteList = ({ export const toNoteList = ({
hashtag, hashtag,

View file

@ -8,7 +8,7 @@ import { cn } from '@/lib/utils'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useFeed } from '@/providers/FeedProvider' import { useFeed } from '@/providers/FeedProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { ChevronDown, Server, Star, UsersRound } from 'lucide-react' import { ChevronDown, Server, Star, UsersRound, Music, Radio, Trees, DoorOpen } from 'lucide-react'
import { forwardRef, HTMLAttributes, useMemo, useState } from 'react' import { forwardRef, HTMLAttributes, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -94,9 +94,21 @@ const FeedSwitcherTrigger = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
if (feedInfo?.feedType === 'relay' && feedInfo.id) { if (feedInfo?.feedType === 'relay' && feedInfo.id) {
return <RelayIcon url={feedInfo.id} /> return <RelayIcon url={feedInfo.id} />
} }
if (feedInfo?.feedType === 'relays') {
const relaySetName = feedInfo.name ?? activeRelaySet?.name ?? activeRelaySet?.id ?? ''
const nameLower = relaySetName.toLowerCase()
// Custom icons for your relay sets
if (nameLower.includes('feed')) return <Radio />
if (nameLower.includes('music')) return <Music />
if (nameLower.includes('backstage')) return <DoorOpen />
if (nameLower.includes('hood')) return <Trees />
// Default relay set icon
return <Server /> return <Server />
}, [feedInfo]) }
return <Server />
}, [feedInfo, activeRelaySet])
const clickable = const clickable =
!IS_COMMUNITY_MODE || COMMUNITY_RELAY_SETS.length + COMMUNITY_RELAYS.length > 1 !IS_COMMUNITY_MODE || COMMUNITY_RELAY_SETS.length + COMMUNITY_RELAYS.length > 1

View file

@ -59,8 +59,8 @@ export default defineConfig(({ mode }) => {
enabled: true enabled: true
}, },
manifest: { manifest: {
name: 'Jumble', name: 'Bpistle',
short_name: 'Jumble', short_name: 'Bpistle',
icons: [ icons: [
{ {
src: '/pwa-512x512.png', src: '/pwa-512x512.png',