From 77f0f4d3772b65db7851f90f7c7103c327cb1729 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 22 Jul 2023 12:49:02 -0500 Subject: [PATCH] Add Streaming hooks --- app/soapbox/actions/streaming.ts | 8 ++-- app/soapbox/api/hooks/index.ts | 4 ++ .../api/hooks/streaming/useNostrStream.ts | 11 +++++ .../api/hooks/streaming/useTimelineStream.ts | 41 +++++++++++++++++ .../api/hooks/streaming/useUserStream.ts | 22 +++++++++ app/soapbox/features/ui/index.tsx | 46 ++----------------- 6 files changed, 88 insertions(+), 44 deletions(-) create mode 100644 app/soapbox/api/hooks/streaming/useNostrStream.ts create mode 100644 app/soapbox/api/hooks/streaming/useTimelineStream.ts create mode 100644 app/soapbox/api/hooks/streaming/useUserStream.ts diff --git a/app/soapbox/actions/streaming.ts b/app/soapbox/actions/streaming.ts index 65752c223..505c9445d 100644 --- a/app/soapbox/actions/streaming.ts +++ b/app/soapbox/actions/streaming.ts @@ -73,8 +73,9 @@ const updateChatQuery = (chat: IChat) => { queryClient.setQueryData(ChatKeys.chat(chat.id), newChat as any); }; -interface StreamOpts { +interface TimelineStreamOpts { statContext?: IStatContext + enabled?: boolean } const connectTimelineStream = ( @@ -82,7 +83,7 @@ const connectTimelineStream = ( path: string, pollingRefresh: ((dispatch: AppDispatch, done?: () => void) => void) | null = null, accept: ((status: APIEntity) => boolean) | null = null, - opts?: StreamOpts, + opts?: TimelineStreamOpts, ) => connectStream(path, pollingRefresh, (dispatch: AppDispatch, getState: () => RootState) => { const locale = getLocale(getState()); @@ -196,7 +197,7 @@ const refreshHomeTimelineAndNotification = (dispatch: AppDispatch, done?: () => dispatch(expandNotifications({}, () => dispatch(fetchAnnouncements(done)))))); -const connectUserStream = (opts?: StreamOpts) => +const connectUserStream = (opts?: TimelineStreamOpts) => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification, null, opts); const connectCommunityStream = ({ onlyMedia }: Record = {}) => @@ -236,4 +237,5 @@ export { connectListStream, connectGroupStream, connectNostrStream, + type TimelineStreamOpts, }; diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index e51a7d06c..47a9101a6 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -43,3 +43,7 @@ export { useSuggestedGroups } from './groups/useSuggestedGroups'; export { useUnmuteGroup } from './groups/useUnmuteGroup'; export { useUpdateGroup } from './groups/useUpdateGroup'; export { useUpdateGroupTag } from './groups/useUpdateGroupTag'; + +// Streaming +export { useUserStream } from './streaming/useUserStream'; +export { useNostrStream } from './streaming/useNostrStream'; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useNostrStream.ts b/app/soapbox/api/hooks/streaming/useNostrStream.ts new file mode 100644 index 000000000..3e6ef707f --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useNostrStream.ts @@ -0,0 +1,11 @@ +import { useFeatures } from 'soapbox/hooks'; + +import { useTimelineStream } from './useTimelineStream'; + +function useNostrStream() { + const features = useFeatures(); + const enabled = features.nostrSign && Boolean(window.nostr); + return useTimelineStream('nostr', 'nostr', null, null, { enabled }); +} + +export { useNostrStream }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useTimelineStream.ts b/app/soapbox/api/hooks/streaming/useTimelineStream.ts new file mode 100644 index 000000000..0e7a2a267 --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useTimelineStream.ts @@ -0,0 +1,41 @@ +import { useEffect, useRef } from 'react'; + +import { connectTimelineStream } from 'soapbox/actions/streaming'; +import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks'; +import { getAccessToken } from 'soapbox/utils/auth'; + +function useTimelineStream(...args: Parameters) { + // TODO: get rid of streaming.ts and move the actual opts here. + const { enabled = true } = args[4] ?? {}; + + const dispatch = useAppDispatch(); + const instance = useInstance(); + const stream = useRef<(() => void) | null>(null); + + const accessToken = useAppSelector(getAccessToken); + const streamingUrl = instance.urls.get('streaming_api'); + + const connect = () => { + if (enabled && accessToken && streamingUrl && !stream.current) { + stream.current = dispatch(connectTimelineStream(...args)); + } + }; + + const disconnect = () => { + if (stream.current) { + stream.current(); + stream.current = null; + } + }; + + useEffect(() => { + connect(); + return disconnect; + }, [accessToken, streamingUrl, enabled]); + + return { + disconnect, + }; +} + +export { useTimelineStream }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/streaming/useUserStream.ts b/app/soapbox/api/hooks/streaming/useUserStream.ts new file mode 100644 index 000000000..5e0bb9aed --- /dev/null +++ b/app/soapbox/api/hooks/streaming/useUserStream.ts @@ -0,0 +1,22 @@ +import { fetchAnnouncements } from 'soapbox/actions/announcements'; +import { expandNotifications } from 'soapbox/actions/notifications'; +import { expandHomeTimeline } from 'soapbox/actions/timelines'; +import { useStatContext } from 'soapbox/contexts/stat-context'; + +import { useTimelineStream } from './useTimelineStream'; + +import type { AppDispatch } from 'soapbox/store'; + +function useUserStream() { + const statContext = useStatContext(); + return useTimelineStream('home', 'user', refresh, null, { statContext }); +} + +/** Refresh home timeline and notifications. */ +function refresh(dispatch: AppDispatch, done?: () => void) { + return dispatch(expandHomeTimeline({}, () => + dispatch(expandNotifications({}, () => + dispatch(fetchAnnouncements(done)))))); +} + +export { useUserStream }; \ No newline at end of file diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 9da22891b..16e1861f3 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -14,16 +14,15 @@ import { openModal } from 'soapbox/actions/modals'; import { expandNotifications } from 'soapbox/actions/notifications'; import { register as registerPushNotifications } from 'soapbox/actions/push-notifications'; import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses'; -import { connectNostrStream, connectUserStream } from 'soapbox/actions/streaming'; import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions'; import { expandHomeTimeline } from 'soapbox/actions/timelines'; +import { useNostrStream, useUserStream } from 'soapbox/api/hooks'; import GroupLookupHoc from 'soapbox/components/hoc/group-lookup-hoc'; import withHoc from 'soapbox/components/hoc/with-hoc'; import SidebarNavigation from 'soapbox/components/sidebar-navigation'; import ThumbNavigation from 'soapbox/components/thumb-navigation'; import { Layout } from 'soapbox/components/ui'; -import { useStatContext } from 'soapbox/contexts/stat-context'; -import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useInstance, useDraggedFiles } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useDraggedFiles } from 'soapbox/hooks'; import AdminPage from 'soapbox/pages/admin-page'; import ChatsPage from 'soapbox/pages/chats-page'; import DefaultPage from 'soapbox/pages/default-page'; @@ -39,7 +38,7 @@ import RemoteInstancePage from 'soapbox/pages/remote-instance-page'; import SearchPage from 'soapbox/pages/search-page'; import StatusPage from 'soapbox/pages/status-page'; import { usePendingPolicy } from 'soapbox/queries/policies'; -import { getAccessToken, getVapidKey } from 'soapbox/utils/auth'; +import { getVapidKey } from 'soapbox/utils/auth'; import { isStandalone } from 'soapbox/utils/state'; import BackgroundShapes from './components/background-shapes'; @@ -363,21 +362,13 @@ const UI: React.FC = ({ children }) => { const history = useHistory(); const dispatch = useAppDispatch(); const { data: pendingPolicy } = usePendingPolicy(); - const instance = useInstance(); - const statContext = useStatContext(); - - const userStream = useRef(null); - const nostrStream = useRef(null); const node = useRef(null); - const me = useAppSelector(state => state.me); const { account } = useOwnAccount(); const features = useFeatures(); const vapidKey = useAppSelector(state => getVapidKey(state)); const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen); - const accessToken = useAppSelector(state => getAccessToken(state)); - const streamingUrl = instance.urls.get('streaming_api'); const standalone = useAppSelector(isStandalone); const { isDragging } = useDraggedFiles(node); @@ -390,28 +381,6 @@ const UI: React.FC = ({ children }) => { } }; - const connectStreaming = () => { - if (accessToken && streamingUrl) { - if (!userStream.current) { - userStream.current = dispatch(connectUserStream({ statContext })); - } - if (!nostrStream.current && features.nostrSign && window.nostr) { - nostrStream.current = dispatch(connectNostrStream()); - } - } - }; - - const disconnectStreaming = () => { - if (userStream.current) { - userStream.current(); - userStream.current = null; - } - if (nostrStream.current) { - nostrStream.current(); - nostrStream.current = null; - } - }; - const handleDragEnter = (e: DragEvent) => e.preventDefault(); const handleDragLeave = (e: DragEvent) => e.preventDefault(); const handleDragOver = (e: DragEvent) => e.preventDefault(); @@ -458,10 +427,6 @@ const UI: React.FC = ({ children }) => { if (window.Notification?.permission === 'default') { window.setTimeout(() => Notification.requestPermission(), 120 * 1000); } - - return () => { - disconnectStreaming(); - }; }, []); useEffect(() => { @@ -477,9 +442,8 @@ const UI: React.FC = ({ children }) => { }; }, []); - useEffect(() => { - connectStreaming(); - }, [accessToken, streamingUrl]); + useUserStream(); + useNostrStream(); // The user has logged in useEffect(() => {