feat: audio
This commit is contained in:
243
package-lock.json
generated
243
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-scroll-area": "1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
@@ -3219,6 +3220,217 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.5.tgz",
|
||||
"integrity": "sha512-rkfe2pU2NBAYfGaxa3Mqosi7VZEWX5CxKaanRv0vZd4Zhl9fvQrg0VM93dv3xGLGfrHuoTRF3JXH8nb9g+B3fw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.1",
|
||||
"@radix-ui/primitive": "1.1.2",
|
||||
"@radix-ui/react-collection": "1.1.7",
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-direction": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||
"@radix-ui/react-use-previous": "1.1.1",
|
||||
"@radix-ui/react-use-size": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
|
||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-previous": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
|
||||
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
@@ -3357,6 +3569,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@radix-ui/react-scroll-area": "1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slider": "^1.3.5",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
|
||||
113
src/components/AudioPlayer/index.tsx
Normal file
113
src/components/AudioPlayer/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { cn } from '@/lib/utils'
|
||||
import mediaManager from '@/services/media-manager.service'
|
||||
import { Pause, Play } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface AudioPlayerProps {
|
||||
src: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function AudioPlayer({ src, className }: AudioPlayerProps) {
|
||||
const audioRef = useRef<HTMLAudioElement>(null)
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
const seekTimeoutRef = useRef<NodeJS.Timeout>()
|
||||
const isSeeking = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
|
||||
const updateTime = () => {
|
||||
if (!isSeeking.current) {
|
||||
setCurrentTime(audio.currentTime)
|
||||
}
|
||||
}
|
||||
const updateDuration = () => setDuration(audio.duration)
|
||||
const handleEnded = () => setIsPlaying(false)
|
||||
const handlePause = () => setIsPlaying(false)
|
||||
const handlePlay = () => setIsPlaying(true)
|
||||
|
||||
audio.addEventListener('timeupdate', updateTime)
|
||||
audio.addEventListener('loadedmetadata', updateDuration)
|
||||
audio.addEventListener('ended', handleEnded)
|
||||
audio.addEventListener('pause', handlePause)
|
||||
audio.addEventListener('play', handlePlay)
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('timeupdate', updateTime)
|
||||
audio.removeEventListener('loadedmetadata', updateDuration)
|
||||
audio.removeEventListener('ended', handleEnded)
|
||||
audio.removeEventListener('pause', handlePause)
|
||||
audio.removeEventListener('play', handlePlay)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const togglePlay = () => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
|
||||
if (isPlaying) {
|
||||
audio.pause()
|
||||
setIsPlaying(false)
|
||||
} else {
|
||||
audio.play()
|
||||
setIsPlaying(true)
|
||||
mediaManager.play(audio)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeek = (value: number[]) => {
|
||||
const audio = audioRef.current
|
||||
if (!audio) return
|
||||
|
||||
isSeeking.current = true
|
||||
setCurrentTime(value[0])
|
||||
|
||||
if (seekTimeoutRef.current) {
|
||||
clearTimeout(seekTimeoutRef.current)
|
||||
}
|
||||
|
||||
seekTimeoutRef.current = setTimeout(() => {
|
||||
audio.currentTime = value[0]
|
||||
isSeeking.current = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 py-2 pl-2 pr-4 border rounded-full max-w-md',
|
||||
className
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<audio ref={audioRef} src={src} preload="metadata" />
|
||||
|
||||
{/* Play/Pause Button */}
|
||||
<Button size="icon" className="rounded-full shrink-0" onClick={togglePlay}>
|
||||
{isPlaying ? <Pause fill="currentColor" /> : <Play fill="currentColor" />}
|
||||
</Button>
|
||||
|
||||
{/* Progress Section */}
|
||||
<div className="flex-1 relative">
|
||||
<Slider value={[currentTime]} max={duration || 100} step={1} onValueChange={handleSeek} />
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-mono text-muted-foreground">{formatTime(duration)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const formatTime = (time: number) => {
|
||||
if (time === Infinity || isNaN(time)) {
|
||||
return '-:--'
|
||||
}
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
EmbeddedHashtagParser,
|
||||
EmbeddedImageParser,
|
||||
EmbeddedLNInvoiceParser,
|
||||
EmbeddedMediaParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedVideoParser,
|
||||
EmbeddedWebsocketUrlParser,
|
||||
parseContent
|
||||
} from '@/lib/content-parser'
|
||||
@@ -28,14 +28,14 @@ import {
|
||||
} from '../Embedded'
|
||||
import Emoji from '../Emoji'
|
||||
import ImageGallery from '../ImageGallery'
|
||||
import VideoPlayer from '../VideoPlayer'
|
||||
import MediaPlayer from '../MediaPlayer'
|
||||
import WebPreview from '../WebPreview'
|
||||
|
||||
const Content = memo(({ event, className }: { event: Event; className?: string }) => {
|
||||
const translatedEvent = useTranslatedEvent(event.id)
|
||||
const nodes = parseContent(translatedEvent?.content ?? event.content, [
|
||||
EmbeddedImageParser,
|
||||
EmbeddedVideoParser,
|
||||
EmbeddedMediaParser,
|
||||
EmbeddedNormalUrlParser,
|
||||
EmbeddedLNInvoiceParser,
|
||||
EmbeddedWebsocketUrlParser,
|
||||
@@ -91,8 +91,8 @@ const Content = memo(({ event, className }: { event: Event; className?: string }
|
||||
<ImageGallery className="mt-2" key={index} images={allImages} start={start} end={end} />
|
||||
)
|
||||
}
|
||||
if (node.type === 'video') {
|
||||
return <VideoPlayer className="mt-2" key={index} src={node.data} />
|
||||
if (node.type === 'media') {
|
||||
return <MediaPlayer className="mt-2" key={index} src={node.data} />
|
||||
}
|
||||
if (node.type === 'url') {
|
||||
return <EmbeddedNormalUrl url={node.data} key={index} />
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
EmbeddedEventParser,
|
||||
EmbeddedImageParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedVideoParser,
|
||||
EmbeddedMediaParser,
|
||||
parseContent
|
||||
} from '@/lib/content-parser'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -26,7 +26,7 @@ export default function Content({
|
||||
const nodes = useMemo(() => {
|
||||
return parseContent(content, [
|
||||
EmbeddedImageParser,
|
||||
EmbeddedVideoParser,
|
||||
EmbeddedMediaParser,
|
||||
EmbeddedEventParser,
|
||||
EmbeddedMentionParser,
|
||||
EmbeddedEmojiParser
|
||||
@@ -42,8 +42,8 @@ export default function Content({
|
||||
if (node.type === 'image' || node.type === 'images') {
|
||||
return index > 0 ? ` [${t('image')}]` : `[${t('image')}]`
|
||||
}
|
||||
if (node.type === 'video') {
|
||||
return index > 0 ? ` [${t('video')}]` : `[${t('video')}]`
|
||||
if (node.type === 'media') {
|
||||
return index > 0 ? ` [${t('media')}]` : `[${t('media')}]`
|
||||
}
|
||||
if (node.type === 'event') {
|
||||
return index > 0 ? ` [${t('note')}]` : `[${t('note')}]`
|
||||
|
||||
@@ -36,7 +36,15 @@ export default function ContentPreview({
|
||||
)
|
||||
}
|
||||
|
||||
if ([kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.PICTURE].includes(event.kind)) {
|
||||
if (
|
||||
[
|
||||
kinds.ShortTextNote,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.PICTURE,
|
||||
ExtendedKind.VOICE,
|
||||
ExtendedKind.VOICE_COMMENT
|
||||
].includes(event.kind)
|
||||
) {
|
||||
return <NormalContentPreview event={event} className={className} />
|
||||
}
|
||||
|
||||
|
||||
49
src/components/MediaPlayer/index.tsx
Normal file
49
src/components/MediaPlayer/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import AudioPlayer from '../AudioPlayer'
|
||||
import VideoPlayer from '../VideoPlayer'
|
||||
|
||||
export default function MediaPlayer({ src, className }: { src: string; className?: string }) {
|
||||
const [mediaType, setMediaType] = useState<'video' | 'audio' | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!src) {
|
||||
setMediaType(null)
|
||||
return
|
||||
}
|
||||
|
||||
const url = new URL(src)
|
||||
const extension = url.pathname.split('.').pop()?.toLowerCase()
|
||||
|
||||
if (extension && ['mp3', 'wav', 'flac', 'aac', 'm4a', 'opus', 'wma'].includes(extension)) {
|
||||
setMediaType('audio')
|
||||
return
|
||||
}
|
||||
|
||||
const video = document.createElement('video')
|
||||
video.src = src
|
||||
video.preload = 'metadata'
|
||||
video.crossOrigin = 'anonymous'
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
setMediaType(video.videoWidth > 0 || video.videoHeight > 0 ? 'video' : 'audio')
|
||||
}
|
||||
|
||||
video.onerror = () => {
|
||||
setMediaType(null)
|
||||
}
|
||||
|
||||
return () => {
|
||||
video.src = ''
|
||||
}
|
||||
}, [src])
|
||||
|
||||
if (!mediaType) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (mediaType === 'video') {
|
||||
return <VideoPlayer src={src} className={className} />
|
||||
}
|
||||
|
||||
return <AudioPlayer src={src} className={className} />
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import AudioPlayer from '../AudioPlayer'
|
||||
import Content from '../Content'
|
||||
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||
import ImageGallery from '../ImageGallery'
|
||||
@@ -71,7 +72,9 @@ export default function Note({
|
||||
ExtendedKind.GROUP_METADATA,
|
||||
ExtendedKind.PICTURE,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.POLL
|
||||
ExtendedKind.POLL,
|
||||
ExtendedKind.VOICE,
|
||||
ExtendedKind.VOICE_COMMENT
|
||||
].includes(event.kind)
|
||||
) {
|
||||
content = <UnknownNote className="mt-2" event={event} />
|
||||
@@ -96,6 +99,8 @@ export default function Note({
|
||||
<Poll className="mt-2" event={event} />
|
||||
</>
|
||||
)
|
||||
} else if (event.kind === ExtendedKind.VOICE || event.kind === ExtendedKind.VOICE_COMMENT) {
|
||||
content = <AudioPlayer className="mt-2" src={event.content} />
|
||||
} else {
|
||||
content = <Content className="mt-2" event={event} />
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ const KINDS = [
|
||||
kinds.Highlights,
|
||||
kinds.LongFormArticle,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.POLL
|
||||
ExtendedKind.POLL,
|
||||
ExtendedKind.VOICE,
|
||||
ExtendedKind.VOICE_COMMENT
|
||||
]
|
||||
|
||||
export default function NoteList({
|
||||
|
||||
@@ -31,7 +31,10 @@ export function NotificationItem({
|
||||
if (notification.kind === kinds.Zap) {
|
||||
return <ZapNotification notification={notification} isNew={isNew} />
|
||||
}
|
||||
if (notification.kind === ExtendedKind.COMMENT) {
|
||||
if (
|
||||
notification.kind === ExtendedKind.COMMENT ||
|
||||
notification.kind === ExtendedKind.VOICE_COMMENT
|
||||
) {
|
||||
return <CommentNotification notification={notification} isNew={isNew} />
|
||||
}
|
||||
if (notification.kind === ExtendedKind.POLL_RESPONSE) {
|
||||
|
||||
@@ -39,7 +39,7 @@ const NotificationList = forwardRef((_, ref) => {
|
||||
const filterKinds = useMemo(() => {
|
||||
switch (notificationType) {
|
||||
case 'mentions':
|
||||
return [kinds.ShortTextNote, ExtendedKind.COMMENT]
|
||||
return [kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT]
|
||||
case 'reactions':
|
||||
return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE]
|
||||
case 'zaps':
|
||||
@@ -51,7 +51,8 @@ const NotificationList = forwardRef((_, ref) => {
|
||||
kinds.Reaction,
|
||||
kinds.Zap,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.POLL_RESPONSE
|
||||
ExtendedKind.POLL_RESPONSE,
|
||||
ExtendedKind.VOICE_COMMENT
|
||||
]
|
||||
}
|
||||
}, [notificationType])
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
||||
if (event.kind !== kinds.ShortTextNote) {
|
||||
filters.push({
|
||||
'#E': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit: LIMIT
|
||||
})
|
||||
}
|
||||
@@ -166,7 +166,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
||||
},
|
||||
{
|
||||
'#A': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit: LIMIT
|
||||
}
|
||||
)
|
||||
@@ -176,7 +176,7 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
|
||||
} else {
|
||||
filters.push({
|
||||
'#I': [rootInfo.id],
|
||||
kinds: [ExtendedKind.COMMENT],
|
||||
kinds: [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT],
|
||||
limit: LIMIT
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn, isInViewport } from '@/lib/utils'
|
||||
import { useAutoplay } from '@/providers/AutoplayProvider'
|
||||
import videoManager from '@/services/video-manager.service'
|
||||
import mediaManager from '@/services/media-manager.service'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
|
||||
@@ -21,11 +21,11 @@ export default function VideoPlayer({ src, className }: { src: string; className
|
||||
if (entry.isIntersecting) {
|
||||
setTimeout(() => {
|
||||
if (isInViewport(container)) {
|
||||
videoManager.autoPlay(video)
|
||||
mediaManager.autoPlay(video)
|
||||
}
|
||||
}, 200)
|
||||
} else {
|
||||
videoManager.pause(video)
|
||||
mediaManager.pause(video)
|
||||
}
|
||||
},
|
||||
{ threshold: 1 }
|
||||
@@ -48,7 +48,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
|
||||
src={src}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onPlay={(event) => {
|
||||
videoManager.play(event.currentTarget)
|
||||
mediaManager.play(event.currentTarget)
|
||||
}}
|
||||
muted
|
||||
/>
|
||||
|
||||
41
src/components/ui/slider.tsx
Normal file
41
src/components/ui/slider.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & { hideThumb?: boolean }
|
||||
>(({ className, ...props }, ref) => {
|
||||
const [isHovered, setIsHovered] = React.useState(false)
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn('relative flex w-full touch-none select-none items-center', className)}
|
||||
{...props}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onTouchStart={() => setIsHovered(true)}
|
||||
onTouchEnd={() => setIsHovered(false)}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
className={cn(
|
||||
'relative w-full grow overflow-hidden rounded-full bg-primary/20 cursor-pointer transition-all',
|
||||
isHovered ? 'h-3' : 'h-1.5'
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary rounded-full" />
|
||||
</SliderPrimitive.Track>
|
||||
{/* <SliderPrimitive.Thumb
|
||||
className={cn(
|
||||
'block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-all duration-300 cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
isHovered ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/> */}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
})
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -65,6 +65,8 @@ export const ExtendedKind = {
|
||||
POLL: 1068,
|
||||
POLL_RESPONSE: 1018,
|
||||
COMMENT: 1111,
|
||||
VOICE: 1222,
|
||||
VOICE_COMMENT: 1244,
|
||||
FAVORITE_RELAYS: 10012,
|
||||
BLOSSOM_SERVER_LIST: 10063,
|
||||
GROUP_METADATA: 39000
|
||||
@@ -76,8 +78,8 @@ export const WS_URL_REGEX =
|
||||
/wss?:\/\/[\w\p{L}\p{N}\p{M}&.-/?=#\-@%+_:!~*]+(?<![.,;:'")\]}!?,。;:""''!?】)])/gu
|
||||
export const IMAGE_REGEX =
|
||||
/https?:\/\/[\w\p{L}\p{N}\p{M}&.-]+\/(?:[^/\s?]*\/)*([^/\s?]*\.(jpg|jpeg|png|gif|webp|bmp|tiff|heic|svg|avif))(?!\w)(?:\?[\w\p{L}\p{N}\p{M}&=.-]*)?(?<![.,;:'")\]}!?,。;:""''!?】)])/giu
|
||||
export const VIDEO_REGEX =
|
||||
/https?:\/\/[\w\p{L}\p{N}\p{M}&.-]+\/(?:[^/\s?]*\/)*([^/\s?]*\.(mp4|webm|ogg|mov))(?!\w)(?:\?[\w\p{L}\p{N}\p{M}&=.-]*)?(?<![.,;:'")\]}!?,。;:""''!?】)])/giu
|
||||
export const MEDIA_REGEX =
|
||||
/https?:\/\/[\w\p{L}\p{N}\p{M}&.-]+\/(?:[^/\s?]*\/)*([^/\s?]*\.(mp4|webm|ogg|mov|mp3|wav|flac|aac|m4a|opus|wma))(?!\w)(?:\?[\w\p{L}\p{N}\p{M}&=.-]*)?(?<![.,;:'")\]}!?,。;:""''!?】)])/giu
|
||||
export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
export const EMOJI_SHORT_CODE_REGEX = /:[a-zA-Z0-9_-]+:/g
|
||||
export const EMBEDDED_EVENT_REGEX = /nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g
|
||||
|
||||
@@ -313,6 +313,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'عناوين المرحلات (اختياري، مفصولة بفواصل)',
|
||||
'Remove poll': 'إزالة الاستطلاع',
|
||||
'Refresh results': 'تحديث النتائج',
|
||||
Poll: 'استطلاع'
|
||||
Poll: 'استطلاع',
|
||||
media: 'الوسائط'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +320,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'Relay-URLs (optional, durch Kommas getrennt)',
|
||||
'Remove poll': 'Umfrage entfernen',
|
||||
'Refresh results': 'Ergebnisse aktualisieren',
|
||||
Poll: 'Umfrage'
|
||||
Poll: 'Umfrage',
|
||||
media: 'Medien'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'Relay URLs (optional, comma-separated)',
|
||||
'Remove poll': 'Remove poll',
|
||||
'Refresh results': 'Refresh results',
|
||||
Poll: 'Poll'
|
||||
Poll: 'Poll',
|
||||
media: 'media'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'URLs de relé (opcional, separadas por comas)',
|
||||
'Remove poll': 'Eliminar encuesta',
|
||||
'Refresh results': 'Actualizar resultados',
|
||||
Poll: 'Encuesta'
|
||||
Poll: 'Encuesta',
|
||||
media: 'medios'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'آدرسهای رله (اختیاری، جدا شده با کاما)',
|
||||
'Remove poll': 'حذف نظرسنجی',
|
||||
'Refresh results': 'بارگیری مجدد نتایج',
|
||||
Poll: 'نظرسنجی'
|
||||
Poll: 'نظرسنجی',
|
||||
media: 'رسانه'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +319,7 @@ export default {
|
||||
'URLs de relais (optionnel, séparées par des virgules)',
|
||||
'Remove poll': 'Supprimer le sondage',
|
||||
'Refresh results': 'Rafraîchir les résultats',
|
||||
Poll: 'Sondage'
|
||||
Poll: 'Sondage',
|
||||
media: 'média'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'URL relay (opzionale, separati da virgole)',
|
||||
'Remove poll': 'Rimuovi sondaggio',
|
||||
'Refresh results': 'Aggiorna risultati',
|
||||
Poll: 'Sondaggio'
|
||||
Poll: 'Sondaggio',
|
||||
media: 'media'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'リレーURL(任意、カンマ区切り)',
|
||||
'Remove poll': '投票を削除',
|
||||
'Refresh results': '結果を更新',
|
||||
Poll: '投票'
|
||||
Poll: '投票',
|
||||
media: 'メディア'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': '릴레이 URL (선택사항, 쉼표로 구분)',
|
||||
'Remove poll': '투표 제거',
|
||||
'Refresh results': '결과 새로 고침',
|
||||
Poll: '투표'
|
||||
Poll: '투표',
|
||||
media: '미디어'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ export default {
|
||||
'Adresy URL przekaźników (opcjonalne, oddzielone przecinkami)',
|
||||
'Remove poll': 'Usuń ankietę',
|
||||
'Refresh results': 'Odśwież wyniki',
|
||||
Poll: 'Ankieta'
|
||||
Poll: 'Ankieta',
|
||||
media: 'media'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'URLs de relay (opcional, separadas por vírgulas)',
|
||||
'Remove poll': 'Remover enquete',
|
||||
'Refresh results': 'Atualizar resultados',
|
||||
Poll: 'Enquete'
|
||||
Poll: 'Enquete',
|
||||
media: 'Mídia'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'URLs de relay (opcional, separadas por vírgulas)',
|
||||
'Remove poll': 'Remover sondagem',
|
||||
'Refresh results': 'Atualizar resultados',
|
||||
Poll: 'Sondagem'
|
||||
Poll: 'Sondagem',
|
||||
media: 'mídia'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'URL релеев (необязательно, через запятую)',
|
||||
'Remove poll': 'Удалить опрос',
|
||||
'Refresh results': 'Обновить результаты',
|
||||
Poll: 'Опрос'
|
||||
Poll: 'Опрос',
|
||||
media: 'медиа'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': 'URL รีเลย์ (ไม่บังคับ, คั่นด้วยจุลภาค)',
|
||||
'Remove poll': 'ลบโพลล์',
|
||||
'Refresh results': 'รีเฟรชผลลัพธ์',
|
||||
Poll: 'โพลล์'
|
||||
Poll: 'โพลล์',
|
||||
media: 'สื่อ'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@ export default {
|
||||
'Relay URLs (optional, comma-separated)': '中继服务器 URL(可选,逗号分隔)',
|
||||
'Remove poll': '移除投票',
|
||||
'Refresh results': '刷新结果',
|
||||
Poll: '投票'
|
||||
Poll: '投票',
|
||||
media: '媒体'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
IMAGE_REGEX,
|
||||
LN_INVOICE_REGEX,
|
||||
URL_REGEX,
|
||||
VIDEO_REGEX,
|
||||
MEDIA_REGEX,
|
||||
WS_URL_REGEX
|
||||
} from '@/constants'
|
||||
|
||||
@@ -14,7 +14,7 @@ export type TEmbeddedNodeType =
|
||||
| 'text'
|
||||
| 'image'
|
||||
| 'images'
|
||||
| 'video'
|
||||
| 'media'
|
||||
| 'event'
|
||||
| 'mention'
|
||||
| 'legacy-mention'
|
||||
@@ -61,9 +61,9 @@ export const EmbeddedImageParser: TContentParser = {
|
||||
regex: IMAGE_REGEX
|
||||
}
|
||||
|
||||
export const EmbeddedVideoParser: TContentParser = {
|
||||
type: 'video',
|
||||
regex: VIDEO_REGEX
|
||||
export const EmbeddedMediaParser: TContentParser = {
|
||||
type: 'media',
|
||||
regex: MEDIA_REGEX
|
||||
}
|
||||
|
||||
export const EmbeddedWebsocketUrlParser: TContentParser = {
|
||||
|
||||
@@ -494,28 +494,16 @@ async function extractRelatedEventIds(content: string, parentEvent?: Event) {
|
||||
|
||||
async function extractCommentMentions(content: string, parentEvent: Event) {
|
||||
const quoteEventIds: string[] = []
|
||||
const rootCoordinateTag =
|
||||
parentEvent.kind === ExtendedKind.COMMENT
|
||||
? parentEvent.tags.find(tagNameEquals('A'))
|
||||
: isReplaceableEvent(parentEvent.kind)
|
||||
? buildATag(parentEvent, true)
|
||||
: undefined
|
||||
const rootEventId =
|
||||
parentEvent.kind === ExtendedKind.COMMENT
|
||||
? parentEvent.tags.find(tagNameEquals('E'))?.[1]
|
||||
: parentEvent.id
|
||||
const rootKind =
|
||||
parentEvent.kind === ExtendedKind.COMMENT
|
||||
? parentEvent.tags.find(tagNameEquals('K'))?.[1]
|
||||
: parentEvent.kind
|
||||
const rootPubkey =
|
||||
parentEvent.kind === ExtendedKind.COMMENT
|
||||
? parentEvent.tags.find(tagNameEquals('P'))?.[1]
|
||||
: parentEvent.pubkey
|
||||
const rootUrl =
|
||||
parentEvent.kind === ExtendedKind.COMMENT
|
||||
? parentEvent.tags.find(tagNameEquals('I'))?.[1]
|
||||
const isComment = [ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT].includes(parentEvent.kind)
|
||||
const rootCoordinateTag = isComment
|
||||
? parentEvent.tags.find(tagNameEquals('A'))
|
||||
: isReplaceableEvent(parentEvent.kind)
|
||||
? buildATag(parentEvent, true)
|
||||
: undefined
|
||||
const rootEventId = isComment ? parentEvent.tags.find(tagNameEquals('E'))?.[1] : parentEvent.id
|
||||
const rootKind = isComment ? parentEvent.tags.find(tagNameEquals('K'))?.[1] : parentEvent.kind
|
||||
const rootPubkey = isComment ? parentEvent.tags.find(tagNameEquals('P'))?.[1] : parentEvent.pubkey
|
||||
const rootUrl = isComment ? parentEvent.tags.find(tagNameEquals('I'))?.[1] : undefined
|
||||
|
||||
const addToSet = (arr: string[], item: string) => {
|
||||
if (!arr.includes(item)) arr.push(item)
|
||||
|
||||
@@ -21,7 +21,10 @@ export function isNsfwEvent(event: Event) {
|
||||
}
|
||||
|
||||
export function isReplyNoteEvent(event: Event) {
|
||||
if (![kinds.ShortTextNote, ExtendedKind.COMMENT].includes(event.kind)) return false
|
||||
if ([ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT].includes(event.kind)) {
|
||||
return true
|
||||
}
|
||||
if (event.kind !== kinds.ShortTextNote) return false
|
||||
|
||||
const cache = EVENT_IS_REPLY_NOTE_CACHE.get(event.id)
|
||||
if (cache !== undefined) return cache
|
||||
@@ -44,12 +47,14 @@ export function isProtectedEvent(event: Event) {
|
||||
}
|
||||
|
||||
export function getParentETag(event?: Event) {
|
||||
if (!event || ![kinds.ShortTextNote, ExtendedKind.COMMENT].includes(event.kind)) return undefined
|
||||
if (!event) return undefined
|
||||
|
||||
if (event.kind === ExtendedKind.COMMENT) {
|
||||
if (event.kind === ExtendedKind.COMMENT || event.kind === ExtendedKind.VOICE_COMMENT) {
|
||||
return event.tags.find(tagNameEquals('e')) ?? event.tags.find(tagNameEquals('E'))
|
||||
}
|
||||
|
||||
if (event.kind !== kinds.ShortTextNote) return undefined
|
||||
|
||||
let tag = event.tags.find(([tagName, , , marker]) => {
|
||||
return tagName === 'e' && marker === 'reply'
|
||||
})
|
||||
@@ -63,7 +68,12 @@ export function getParentETag(event?: Event) {
|
||||
}
|
||||
|
||||
export function getParentATag(event?: Event) {
|
||||
if (!event || ![kinds.ShortTextNote, ExtendedKind.COMMENT].includes(event.kind)) return undefined
|
||||
if (
|
||||
!event ||
|
||||
![kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT].includes(event.kind)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return event.tags.find(tagNameEquals('a')) ?? event.tags.find(tagNameEquals('A'))
|
||||
}
|
||||
@@ -86,12 +96,14 @@ export function getParentBech32Id(event?: Event) {
|
||||
}
|
||||
|
||||
export function getRootETag(event?: Event) {
|
||||
if (!event || ![kinds.ShortTextNote, ExtendedKind.COMMENT].includes(event.kind)) return undefined
|
||||
if (!event) return undefined
|
||||
|
||||
if (event.kind === ExtendedKind.COMMENT) {
|
||||
if (event.kind === ExtendedKind.COMMENT || ExtendedKind.VOICE_COMMENT) {
|
||||
return event.tags.find(tagNameEquals('E'))
|
||||
}
|
||||
|
||||
if (event.kind !== kinds.ShortTextNote) return undefined
|
||||
|
||||
let tag = event.tags.find(([tagName, , , marker]) => {
|
||||
return tagName === 'e' && marker === 'root'
|
||||
})
|
||||
@@ -105,7 +117,12 @@ export function getRootETag(event?: Event) {
|
||||
}
|
||||
|
||||
export function getRootATag(event?: Event) {
|
||||
if (!event || event.kind !== ExtendedKind.COMMENT) return undefined
|
||||
if (
|
||||
!event ||
|
||||
![kinds.ShortTextNote, ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT].includes(event.kind)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return event.tags.find(tagNameEquals('A'))
|
||||
}
|
||||
|
||||
@@ -557,13 +557,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
|
||||
if (
|
||||
!specifiedRelayUrls?.length &&
|
||||
[
|
||||
kinds.ShortTextNote,
|
||||
kinds.Reaction,
|
||||
kinds.Repost,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.PICTURE
|
||||
].includes(draftEvent.kind)
|
||||
![kinds.Contacts, kinds.Mutelist].includes(draftEvent.kind)
|
||||
) {
|
||||
const mentions: string[] = []
|
||||
draftEvent.tags.forEach(([tagName, tagValue]) => {
|
||||
|
||||
@@ -50,10 +50,12 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
|
||||
{
|
||||
kinds: [
|
||||
kinds.ShortTextNote,
|
||||
ExtendedKind.COMMENT,
|
||||
kinds.Reaction,
|
||||
kinds.Repost,
|
||||
kinds.Zap
|
||||
kinds.Zap,
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.POLL_RESPONSE,
|
||||
ExtendedKind.VOICE_COMMENT
|
||||
],
|
||||
'#p': [pubkey],
|
||||
since: notificationsSeenAt,
|
||||
|
||||
64
src/services/media-manager.service.ts
Normal file
64
src/services/media-manager.service.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
class MediaManagerService {
|
||||
static instance: MediaManagerService
|
||||
|
||||
private currentMedia: HTMLMediaElement | null = null
|
||||
|
||||
constructor() {
|
||||
if (!MediaManagerService.instance) {
|
||||
MediaManagerService.instance = this
|
||||
}
|
||||
return MediaManagerService.instance
|
||||
}
|
||||
|
||||
pause(media: HTMLMediaElement) {
|
||||
if (isPipElement(media)) {
|
||||
return
|
||||
}
|
||||
if (this.currentMedia === media) {
|
||||
this.currentMedia = null
|
||||
}
|
||||
media.pause()
|
||||
}
|
||||
|
||||
autoPlay(media: HTMLMediaElement) {
|
||||
if (
|
||||
document.pictureInPictureElement &&
|
||||
isMediaPlaying(document.pictureInPictureElement as HTMLMediaElement)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.play(media)
|
||||
}
|
||||
|
||||
play(media: HTMLMediaElement) {
|
||||
if (document.pictureInPictureElement && document.pictureInPictureElement !== media) {
|
||||
;(document.pictureInPictureElement as HTMLMediaElement).pause()
|
||||
}
|
||||
if (this.currentMedia && this.currentMedia !== media) {
|
||||
this.currentMedia.pause()
|
||||
}
|
||||
this.currentMedia = media
|
||||
if (isMediaPlaying(media)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.currentMedia.play().catch((error) => {
|
||||
console.error('Error playing media:', error)
|
||||
this.currentMedia = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new MediaManagerService()
|
||||
export default instance
|
||||
|
||||
function isMediaPlaying(media: HTMLMediaElement) {
|
||||
return media.currentTime > 0 && !media.paused && !media.ended && media.readyState >= 2
|
||||
}
|
||||
|
||||
function isPipElement(media: HTMLMediaElement) {
|
||||
if (document.pictureInPictureElement === media) {
|
||||
return true
|
||||
}
|
||||
return (media as any).webkitPresentationMode === 'picture-in-picture'
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
class VideoManagerService {
|
||||
static instance: VideoManagerService
|
||||
|
||||
private currentVideo: HTMLVideoElement | null = null
|
||||
|
||||
constructor() {
|
||||
if (!VideoManagerService.instance) {
|
||||
VideoManagerService.instance = this
|
||||
}
|
||||
return VideoManagerService.instance
|
||||
}
|
||||
|
||||
pause(video: HTMLVideoElement) {
|
||||
if (isPipElement(video)) {
|
||||
return
|
||||
}
|
||||
if (this.currentVideo === video) {
|
||||
this.currentVideo = null
|
||||
}
|
||||
video.pause()
|
||||
}
|
||||
|
||||
autoPlay(video: HTMLVideoElement) {
|
||||
if (
|
||||
document.pictureInPictureElement &&
|
||||
isVideoPlaying(document.pictureInPictureElement as HTMLVideoElement)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.play(video)
|
||||
}
|
||||
|
||||
play(video: HTMLVideoElement) {
|
||||
if (document.pictureInPictureElement && document.pictureInPictureElement !== video) {
|
||||
;(document.pictureInPictureElement as HTMLVideoElement).pause()
|
||||
}
|
||||
if (this.currentVideo && this.currentVideo !== video) {
|
||||
this.currentVideo.pause()
|
||||
}
|
||||
this.currentVideo = video
|
||||
if (isVideoPlaying(video)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.currentVideo.play().catch((error) => {
|
||||
console.error('Error playing video:', error)
|
||||
this.currentVideo = null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new VideoManagerService()
|
||||
export default instance
|
||||
|
||||
function isVideoPlaying(video: HTMLVideoElement) {
|
||||
return video.currentTime > 0 && !video.paused && !video.ended && video.readyState >= 2
|
||||
}
|
||||
|
||||
function isPipElement(video: HTMLVideoElement) {
|
||||
if (document.pictureInPictureElement === video) {
|
||||
return true
|
||||
}
|
||||
return (video as any).webkitPresentationMode === 'picture-in-picture'
|
||||
}
|
||||
Reference in New Issue
Block a user