Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
63e8c9a
enabling websocket creation in the internet connection watcher
anultravioletaurora Sep 1, 2025
8ab3529
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Sep 18, 2025
3c55500
websocket stuff
anultravioletaurora Sep 18, 2025
7d84a57
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Sep 20, 2025
13c3a46
styling and websocket adjustments
anultravioletaurora Sep 21, 2025
858b3bb
websocket
anultravioletaurora Sep 30, 2025
45b2457
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Oct 24, 2025
6ea2ada
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Oct 27, 2025
423967a
Merge branch 'main' into feature/websockets
anultravioletaurora Oct 27, 2025
a234598
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Nov 5, 2025
4ae4cb8
Merge branch 'main' into feature/websockets
anultravioletaurora Nov 7, 2025
5513c5a
Replace useJellifyContext with useApi in useWebSocket
anultravioletaurora Nov 7, 2025
8f63e88
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Nov 15, 2025
ad07201
Merge branch 'main' into feature/websockets
anultravioletaurora Nov 18, 2025
5dcd081
Merge branch 'main' into feature/websockets
anultravioletaurora Nov 20, 2025
0865db2
Merge branch 'main' into feature/websockets
anultravioletaurora Nov 24, 2025
7b16a00
Merge branch 'main' into feature/websockets
anultravioletaurora Nov 24, 2025
0b5c560
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Dec 8, 2025
de78eb4
Merge branch 'main' of github.com:Jellify-Music/App into feature/webs…
anultravioletaurora Dec 17, 2025
6ecbd9b
Merge branch 'main' into feature/websockets
anultravioletaurora Dec 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/api/queries/websocket/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Indicates the number of seconds Jellify will wait until attempting
* to ping the websocket again
*/
const WEBSOCKET_COOL_DOWN_SECONDS = 60

export default WEBSOCKET_COOL_DOWN_SECONDS
76 changes: 76 additions & 0 deletions src/api/queries/websocket/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Api } from '@jellyfin/sdk'
import { useCallback, useEffect, useRef, useState } from 'react'
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
import { useApi } from '../../../stores'

type SocketState = 'open' | 'closed'

/**
*
* @returns
*/
const useWebSocket: (networkStatus: networkStatusTypes) => SocketState = (networkStatus) => {
const api = useApi()

const [socketState, setSocketState] = useState<SocketState>('open')

const onOpen = useCallback(() => {
consoleOut(`WebSocket`, 'info', `Socket opened`)

setSocketState('open')
}, [setSocketState])

const onClose = useCallback(
(event: WebSocketCloseEvent) => {
consoleOut(`WebSocket`, `warn`, `Socket closed: ${event.reason}`)

setSocketState('closed')
socket.current.close()

setTimeout(
() => (socket.current = createJellyfinWebSocket(api!, onOpen, onClose)),
10000,
)
},
[setSocketState],
)

const socket = useRef<WebSocket>(createJellyfinWebSocket(api!, onOpen, onClose))

return socketState
}

function consoleOut(
key: string,
func: keyof Pick<Console, 'error' | 'info' | 'warn' | 'debug'>,
message: string,
error?: Error,
): void {
if (func === 'error') console[func](`**${key}**: ${message}`, error)
else console[func](`**${key}**: ${message}`)
}

function createJellyfinWebSocket(
api: Api,
onOpen: () => void,
onClose: (event: WebSocketCloseEvent) => void,
): WebSocket {
const url = new URL(`${api.basePath}`)

url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'

url.pathname = 'socket'
url.searchParams.append(`api_key`, api.accessToken)

consoleOut(`Websocket`, 'debug', `Building websocket with ${url.protocol.toUpperCase()}`)

const websocket = new WebSocket(url.toString(), url.protocol)

websocket.onopen = onOpen

websocket.onclose = onClose

return websocket
}

export default useWebSocket
2 changes: 1 addition & 1 deletion src/components/Artist/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import IconButton from '../Global/helpers/icon-button'
import { fetchAlbumDiscs } from '../../api/queries/item'
import { useLoadNewQueue } from '../../providers/Player/hooks/mutations'
import { QueuingType } from '../../enums/queuing-type'
import { useNetworkStatus } from '../../stores/network'
import useStreamingDeviceProfile from '../../stores/device-profile'
import { useNetworkStatus } from '../../stores/network'
import { useApi } from '../../stores'
import Icon from '../Global/components/icon'
import useTracks from '../../api/queries/track'
Expand Down
2 changes: 1 addition & 1 deletion src/components/Global/components/item-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import FavoriteIcon from './favorite-icon'
import navigationRef from '../../../../navigation'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../../screens/types'
import { useAddToQueue, useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
import { useNetworkStatus } from '../../../stores/network'
import { useAddToQueue, useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
import useStreamingDeviceProfile from '../../../stores/device-profile'
import useItemContext from '../../../hooks/use-item-context'
import { RouteProp, useRoute } from '@react-navigation/native'
Expand Down
4 changes: 3 additions & 1 deletion src/components/Global/components/track.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ export default function Track({
// Memoize expensive computations
const isPlaying = currentTrackId === track.Id

const isOffline = networkStatus === networkStatusTypes.DISCONNECTED
const isOffline = [networkStatusTypes.DISCONNECTED, networkStatusTypes.OFFLINE].includes(
networkStatus ?? networkStatusTypes.ONLINE,
)

// Memoize tracklist for queue loading
const memoizedTracklist = tracklist ?? playQueue?.map((track) => track.item) ?? []
Expand Down
31 changes: 20 additions & 11 deletions src/components/Network/internetConnectionWatcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,28 @@ import { runOnJS } from 'react-native-worklets'

import { Text } from '../Global/helpers/text'
import { useNetworkStatus } from '../../stores/network'
import useWebSocket from '../../api/queries/websocket'

const internetConnectionWatcher = {
NO_INTERNET: 'You are offline',
NO_WEBSOCKET: 'You are disconnected',
BACK_ONLINE: "And we're back!",
}

export enum networkStatusTypes {
ONLINE = 'ONLINE',
DISCONNECTED = 'DISCONNECTED',
OFFLINE = 'OFFLINE',
}

const isAndroid = Platform.OS === 'android'

const InternetConnectionWatcher = () => {
// const [networkStatus, setNetworkStatus] = useState<keyof typeof networkStatusTypes | null>(null)
const lastNetworkStatus = useRef<networkStatusTypes | null>(networkStatusTypes.ONLINE)
const [networkStatus, setNetworkStatus] = useNetworkStatus()

const socketState = useWebSocket(networkStatus ?? networkStatusTypes.ONLINE)

const bannerHeight = useSharedValue(0)
const opacity = useSharedValue(0)

Expand All @@ -54,7 +58,7 @@ const InternetConnectionWatcher = () => {
})

const changeNetworkStatus = () => {
if (lastNetworkStatus.current !== networkStatusTypes.DISCONNECTED) {
if (lastNetworkStatus.current !== networkStatusTypes.OFFLINE) {
setNetworkStatus(null)
}
}
Expand All @@ -71,7 +75,10 @@ const InternetConnectionWatcher = () => {
}, [networkStatus])

useEffect(() => {
if (networkStatus === networkStatusTypes.DISCONNECTED) {
if (
networkStatus === networkStatusTypes.OFFLINE ||
networkStatus === networkStatusTypes.DISCONNECTED
) {
animateBannerIn()
} else if (networkStatus === networkStatusTypes.ONLINE) {
animateBannerIn()
Expand All @@ -91,12 +98,7 @@ const InternetConnectionWatcher = () => {
)

if (isNetworkDisconnected) {
setNetworkStatus(networkStatusTypes.DISCONNECTED)
} else if (
!isNetworkDisconnected &&
lastNetworkStatus.current === networkStatusTypes.DISCONNECTED
) {
internetConnectionBack()
setNetworkStatus(networkStatusTypes.OFFLINE)
}
},
)
Expand All @@ -105,6 +107,11 @@ const InternetConnectionWatcher = () => {
}
}, [])

useEffect(() => {
if (socketState === 'open') internetConnectionBack()
else setNetworkStatus(networkStatusTypes.DISCONNECTED)
}, [socketState])

return (
<Animated.View style={[{ overflow: 'hidden' }, animatedStyle]}>
<YStack
Expand All @@ -115,10 +122,12 @@ const InternetConnectionWatcher = () => {
networkStatus === networkStatusTypes.ONLINE ? '$success' : '$warning'
}
>
<Text textAlign='center' color='$purpleDark'>
<Text textAlign='center' color='$black'>
{networkStatus === networkStatusTypes.ONLINE
? internetConnectionWatcher.BACK_ONLINE
: internetConnectionWatcher.NO_INTERNET}
: networkStatus === networkStatusTypes.DISCONNECTED
? internetConnectionWatcher.NO_WEBSOCKET
: internetConnectionWatcher.NO_INTERNET}
</Text>
</YStack>
</Animated.View>
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Login/server-address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export default function ServerAddress({
}}
testID='connect_button'
>
Connect
<Text>Connect</Text>
</Button>
)}
</YStack>
Expand Down
10 changes: 6 additions & 4 deletions src/screens/Login/server-authentication.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState } from 'react'
import _ from 'lodash'
import { H6, Spacer, Spinner, XStack, YStack } from 'tamagui'
import { H2 } from '../../components/Global/helpers/text'
import { H2, Text } from '../../components/Global/helpers/text'
import Button from '../../components/Global/helpers/button'
import { SafeAreaView } from 'react-native-safe-area-context'
import Input from '../../components/Global/helpers/input'
Expand Down Expand Up @@ -104,10 +104,12 @@ export default function ServerAuthentication({
navigation.popTo('ServerAddress', undefined)
}}
>
Switch Server
<Text>Switch Server</Text>
</Button>
{isPending ? (
<Spinner />
<XStack justifyContent='center'>
<Spinner />
</XStack>
) : (
<Button
marginVertical={0}
Expand All @@ -120,7 +122,7 @@ export default function ServerAuthentication({
}
}}
>
Sign in
<Text>Sign in</Text>
</Button>
)}
</XStack>
Expand Down
21 changes: 21 additions & 0 deletions src/stores/network/socket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { devtools } from 'zustand/middleware'
import { create } from 'zustand'

type SocketState = 'open' | 'closed'

type SocketStore = {
socketState: SocketState
setSocketState: (socketState: SocketState) => void
}

const useSocketStore = create<SocketStore>()(
devtools((set) => ({
socketState: 'closed',
setSocketState: (socketState) => set({ socketState }),
})),
)

const useSocketState: () => [SocketState, (socketState: SocketState) => void] = () =>
useSocketStore((state) => [state.socketState, state.setSocketState])

export default useSocketState