diff --git a/.env.basspistol b/.env.basspistol new file mode 100644 index 0000000..23ba89d --- /dev/null +++ b/.env.basspistol @@ -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"]}] diff --git a/README.md b/README.md index c23365e..f4d76bd 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,19 @@ Jumble Logo -

logo designed by Daniel David

+ -# 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) +Upstream code: https://github.com/CodyTseng/jumble + ## Forks > Some interesting forks of Jumble. diff --git a/index.html b/index.html index 8677d1e..9e3c8c8 100644 --- a/index.html +++ b/index.html @@ -4,29 +4,29 @@ - Jumble - + Bpistle + - + - + - + - + diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 8d0a6c0..722a661 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/favicon-96x96.png b/public/favicon-96x96.png index d3b391c..1028f7f 100644 Binary files a/public/favicon-96x96.png and b/public/favicon-96x96.png differ diff --git a/public/favicon.ico b/public/favicon.ico index 23976c6..20047ec 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg index 0cb187d..60940dc 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1 +1,7 @@ - \ No newline at end of file + + + + + + + diff --git a/public/pwa-192x192.png b/public/pwa-192x192.png index e6c6e4f..124c881 100644 Binary files a/public/pwa-192x192.png and b/public/pwa-192x192.png differ diff --git a/public/pwa-512x512.png b/public/pwa-512x512.png index 6c5a8d0..1543ecd 100644 Binary files a/public/pwa-512x512.png and b/public/pwa-512x512.png differ diff --git a/public/pwa-monochrome.svg b/public/pwa-monochrome.svg index c4b9a85..60940dc 100644 --- a/public/pwa-monochrome.svg +++ b/public/pwa-monochrome.svg @@ -1,9 +1,7 @@ - - - - - - - - + + + + + + diff --git a/resources/icon-rounded.svg b/resources/icon-rounded.svg index 0cb187d..a4e1e38 100644 --- a/resources/icon-rounded.svg +++ b/resources/icon-rounded.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + + + + diff --git a/resources/icon.svg b/resources/icon.svg index 8f20e6a..4e9c4ff 100644 --- a/resources/icon.svg +++ b/resources/icon.svg @@ -1,10 +1,5 @@ - - - - - - - - - + + + + diff --git a/resources/logo-dark.svg b/resources/logo-dark.svg index 1840617..795a64a 100644 --- a/resources/logo-dark.svg +++ b/resources/logo-dark.svg @@ -1 +1,2 @@ - \ No newline at end of file + + diff --git a/resources/logo-light.svg b/resources/logo-light.svg index f1e3c92..d628dc2 100644 --- a/resources/logo-light.svg +++ b/resources/logo-light.svg @@ -1 +1,2 @@ - \ No newline at end of file + + diff --git a/src/assets/Icon.tsx b/src/assets/Icon.tsx index 0fdb8d6..9439ddb 100644 --- a/src/assets/Icon.tsx +++ b/src/assets/Icon.tsx @@ -17,7 +17,7 @@ export default function Icon({ className }: { className?: string }) { > ) diff --git a/src/assets/Logo.tsx b/src/assets/Logo.tsx index 0887faf..769b736 100644 --- a/src/assets/Logo.tsx +++ b/src/assets/Logo.tsx @@ -17,7 +17,7 @@ export default function Logo({ className }: { className?: string }) { > ) diff --git a/src/assets/favicon.svg b/src/assets/favicon.svg index 0cb187d..5482668 100644 --- a/src/assets/favicon.svg +++ b/src/assets/favicon.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + + + + + diff --git a/src/components/ContentPreview/MusicTrackPreview.tsx b/src/components/ContentPreview/MusicTrackPreview.tsx new file mode 100644 index 0000000..dba954f --- /dev/null +++ b/src/components/ContentPreview/MusicTrackPreview.tsx @@ -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 ( +
+ 🎵 + + {title || t('music.untitled')} + + {artist && ( + + {t('music.by')} {artist} + + )} +
+ ) +} diff --git a/src/components/ContentPreview/index.tsx b/src/components/ContentPreview/index.tsx index bc0f402..fbf1cfa 100644 --- a/src/components/ContentPreview/index.tsx +++ b/src/components/ContentPreview/index.tsx @@ -18,6 +18,7 @@ import PictureNotePreview from './PictureNotePreview' import PollPreview from './PollPreview' import ReactionPreview from './ReactionPreview' import VideoNotePreview from './VideoNotePreview' +import MusicTrackPreview from './MusicTrackPreview' export default function ContentPreview({ event, @@ -120,6 +121,10 @@ export default function ContentPreview({ return } + if (event.kind === ExtendedKind.MUSIC_TRACK) { + return + } + return (
[ diff --git a/src/components/KindFilter/index.tsx b/src/components/KindFilter/index.tsx index a22fa97..f745e64 100644 --- a/src/components/KindFilter/index.tsx +++ b/src/components/KindFilter/index.tsx @@ -20,6 +20,7 @@ const KIND_FILTER_OPTIONS = [ { kindGroup: [ExtendedKind.POLL], label: 'Polls' }, { kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' }, { kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' }, + { kindGroup: [ExtendedKind.MUSIC_TRACK], label: 'Music Posts' }, { kindGroup: [ ExtendedKind.VIDEO, diff --git a/src/components/Note/MusicTrackNote/index.tsx b/src/components/Note/MusicTrackNote/index.tsx new file mode 100644 index 0000000..aabd3ab --- /dev/null +++ b/src/components/Note/MusicTrackNote/index.tsx @@ -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 ( +
+ {t('music.noAudioUrl')} +
+ ) + } + + return ( +
+ {/* Main track container */} +
+ {/* Cover Art */} + {metadata.image && ( +
+ {metadata.alt +
+ )} + + {/* Track Info */} +
+

{metadata.title}

+ + {metadata.artist && ( +

+ {t('music.by')} {metadata.artist} +

+ )} + + {metadata.album && ( +

+ {t('music.album')}: {metadata.album} + {metadata.trackNumber && ` • ${t('music.track')} ${metadata.trackNumber}`} +

+ )} + + {metadata.releaseDate && ( +

+ {t('music.released')}: {dayjs(metadata.releaseDate).format('MMMM D, YYYY')} +

+ )} + + {/* Audio Player */} +
+ +
+ + {/* Show lyrics if they exist */} + {metadata.lyrics && ( +
+

+ {metadata.lyrics} +

+
+ )} + + {/* Show credits if they exist */} + {metadata.credits && ( +
+

+ {t('music.credits')}:
+ {metadata.credits} +

+
+ )} + + + +
+
+ + + {/* License and Metadata Footer */} +
+ {metadata.license && ( +

+ {t('music.license')}: {metadata.license} +

+ )} + + {/* Only show alt if it's different from what we've shown + {metadata.alt && !metadata.credits && ( +

+ {t('music.altText')}: {metadata.alt} +

+ )} + */} + {metadata.genres.length > 0 && ( +
+ {metadata.genres.map((genre, index) => ( + + {genre} + + ))} +
+ )} +
+
+ ) +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 0d53b7a..5ba064f 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -38,6 +38,7 @@ import Reaction from './Reaction' import RelayReview from './RelayReview' import UnknownNote from './UnknownNote' import VideoNote from './VideoNote' +import MusicTrackNote from './MusicTrackNote' export default function Note({ event, @@ -124,6 +125,8 @@ export default function Note({ event.kind === ExtendedKind.ADDRESSABLE_SHORT_VIDEO ) { content = + } else if (event.kind === ExtendedKind.MUSIC_TRACK) { + content = } else if (event.kind === ExtendedKind.RELAY_REVIEW) { content = } else if (event.kind === kinds.Emojisets) { diff --git a/src/components/RelayInfo/index.tsx b/src/components/RelayInfo/index.tsx index 0764117..ca18caf 100644 --- a/src/components/RelayInfo/index.tsx +++ b/src/components/RelayInfo/index.tsx @@ -148,7 +148,7 @@ function RelayControls({ url }: { url: string }) { } const handleCopyShareableUrl = () => { - navigator.clipboard.writeText(`https://jumble.social/?r=${url}`) + navigator.clipboard.writeText(`https://nostr.basspistol.org/?r=${url}`) setCopiedShareableUrl(true) toast.success('Shareable URL copied to clipboard') setTimeout(() => setCopiedShareableUrl(false), 2000) diff --git a/src/components/RelaySetCard/index.tsx b/src/components/RelaySetCard/index.tsx index 7367bc5..531cb04 100644 --- a/src/components/RelaySetCard/index.tsx +++ b/src/components/RelaySetCard/index.tsx @@ -1,6 +1,6 @@ import { cn } from '@/lib/utils' 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 { useTranslation } from 'react-i18next' import RelayIcon from '../RelayIcon' @@ -17,6 +17,21 @@ export default function RelaySetCard({ const { t } = useTranslation() 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 (
onSelectChange(!select)} - > -
-
-
- + onClick={() => onSelectChange(!select)} + > +
+
+
+ {/* Use the dynamic icon component instead of hardcoded FolderClosed */} + +
+
{relaySet.name}
-
{relaySet.name}
-
{t('n relays', { n: relaySet.relayUrls.length })} diff --git a/src/constants.ts b/src/constants.ts index 3434311..53a1b77 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,7 +4,7 @@ import { TRelaySet } from './types' export const JUMBLE_API_BASE_URL = 'https://api.jumble.social' export const RECOMMENDED_BLOSSOM_SERVERS = [ - 'https://blossom.band/', + 'https://basspistol.org', 'https://blossom.primal.net/', 'https://nostr.media/' ] @@ -78,9 +78,9 @@ export const BIG_RELAY_URLS = [ '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 @@ -101,7 +101,8 @@ export const ExtendedKind = { RELAY_REVIEW: 31987, GROUP_METADATA: 39000, ADDRESSABLE_NORMAL_VIDEO: 34235, - ADDRESSABLE_SHORT_VIDEO: 34236 + ADDRESSABLE_SHORT_VIDEO: 34236, + MUSIC_TRACK: 36787 } export const ALLOWED_FILTER_KINDS = [ @@ -118,7 +119,8 @@ export const ALLOWED_FILTER_KINDS = [ kinds.Highlights, kinds.LongFormArticle, ExtendedKind.ADDRESSABLE_NORMAL_VIDEO, - ExtendedKind.ADDRESSABLE_SHORT_VIDEO + ExtendedKind.ADDRESSABLE_SHORT_VIDEO, + ExtendedKind.MUSIC_TRACK ] export const SUPPORTED_KINDS = [ @@ -202,16 +204,16 @@ export const PRIMARY_COLORS = { DEFAULT: { name: 'Default', light: { - primary: '259 43% 56%', - 'primary-hover': '259 43% 65%', + primary: '30 100% 50%', + 'primary-hover': '30 100% 60%', 'primary-foreground': '0 0% 98%', - ring: '259 43% 56%' + ring: '30 100% 50%' }, dark: { - primary: '259 43% 56%', - 'primary-hover': '259 43% 65%', + primary: '30 100% 50%', + 'primary-hover': '30 100% 60%', 'primary-foreground': '240 5.9% 10%', - ring: '259 43% 56%' + ring: '30 100% 50%' } }, RED: { @@ -230,18 +232,18 @@ export const PRIMARY_COLORS = { } }, ORANGE: { - name: 'Orange', + name: 'Lavender', light: { - primary: '30 100% 50%', - 'primary-hover': '30 100% 60%', + primary: '259 43% 56%', + 'primary-hover': '259 43% 65%', 'primary-foreground': '0 0% 98%', - ring: '30 100% 50%' + ring: '259 43% 56%' }, dark: { - primary: '30 100% 50%', - 'primary-hover': '30 100% 60%', + primary: '259 43% 56%', + 'primary-hover': '259 43% 65%', 'primary-foreground': '240 5.9% 10%', - ring: '30 100% 50%' + ring: '259 43% 56%' } }, AMBER: { diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 6e3c18f..8703149 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -686,6 +686,18 @@ export default { 'Allow insecure connections description': 'Allow loading http:// resources and connecting to ws:// relays. May trigger browser mixed content warnings.', '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', + } } } diff --git a/src/index.css b/src/index.css index 7f5901a..07b6650 100644 --- a/src/index.css +++ b/src/index.css @@ -133,12 +133,12 @@ --radius: 0.75rem; } .dark { - --surface-background: 240 10% 3.9%; - --background: 0 0% 9%; + --surface-background: 276, 61.404%, 9%; + --background: 276, 61.404%, 11.176%; --foreground: 0 0% 98%; - --card: 0 0% 12%; + --card: 276, 61.404%, 10%; --card-foreground: 0 0% 98%; - --popover: 0 0% 12%; + --popover: 276, 61.404%, 10%; --popover-foreground: 0 0% 98%; --primary: 259 43% 56%; --primary-hover: 259 43% 65%; diff --git a/src/lib/link.ts b/src/lib/link.ts index c7c868d..c84b2a5 100644 --- a/src/lib/link.ts +++ b/src/lib/link.ts @@ -9,7 +9,7 @@ export const toNote = (eventOrId: Event | string) => { return `/notes/${nevent}` } export const toJumbleNote = (eventOrId: Event | string) => { - return `https://jumble.social${toNote(eventOrId)}` + return `https://nostr.basspistol.org${toNote(eventOrId)}` } export const toNoteList = ({ hashtag, diff --git a/src/pages/primary/NoteListPage/FeedButton.tsx b/src/pages/primary/NoteListPage/FeedButton.tsx index 42dfa76..f5463e5 100644 --- a/src/pages/primary/NoteListPage/FeedButton.tsx +++ b/src/pages/primary/NoteListPage/FeedButton.tsx @@ -8,7 +8,7 @@ import { cn } from '@/lib/utils' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFeed } from '@/providers/FeedProvider' 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 { useTranslation } from 'react-i18next' @@ -90,13 +90,25 @@ const FeedSwitcherTrigger = forwardRef { if (feedInfo?.feedType === 'following') return - if (feedInfo?.feedType === 'pinned') return - if (feedInfo?.feedType === 'relay' && feedInfo.id) { - return - } + if (feedInfo?.feedType === 'pinned') return + if (feedInfo?.feedType === 'relay' && feedInfo.id) { + return + } + if (feedInfo?.feedType === 'relays') { + const relaySetName = feedInfo.name ?? activeRelaySet?.name ?? activeRelaySet?.id ?? '' + const nameLower = relaySetName.toLowerCase() - return - }, [feedInfo]) + // Custom icons for your relay sets + if (nameLower.includes('feed')) return + if (nameLower.includes('music')) return + if (nameLower.includes('backstage')) return + if (nameLower.includes('hood')) return + + // Default relay set icon + return + } + return + }, [feedInfo, activeRelaySet]) const clickable = !IS_COMMUNITY_MODE || COMMUNITY_RELAY_SETS.length + COMMUNITY_RELAYS.length > 1 diff --git a/vite.config.ts b/vite.config.ts index 18130c3..a81cdfa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -59,8 +59,8 @@ export default defineConfig(({ mode }) => { enabled: true }, manifest: { - name: 'Jumble', - short_name: 'Jumble', + name: 'Bpistle', + short_name: 'Bpistle', icons: [ { src: '/pwa-512x512.png',